From 7ff88c54356e35d4f224cc77bb3c90cb04817234 Mon Sep 17 00:00:00 2001 From: Constantin Leue <77660791+Consti3D@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:35:19 +0200 Subject: [PATCH] Footer: drone status pill always sizes to its content, mission pill absorbs the rest The drone status pill and the mission pill previously sat in two equally sized Expanded regions. That squeezed the drone pill's FittedBox down to illegibility whenever its content grew (e.g. "disconnected"), while the mission pill often had unused space to spare. A plain flex-ratio rebalance (e.g. mission flex:1 vs drone flex:4) turned out to be the wrong tool: Expanded/Flexible always force a fixed fractional share regardless of actual content need, so it either starved the mission pill even for short names, or capped the drone pill below what it needed. bottom_stats_bar.dart: the drone-side content (status pill or drone-name chip + send/warning/settings buttons) is now a plain, non-flex Row child, exactly like _detailsPill()/_AltitudeProfileToggleButton already were - it always gets exactly the width its current content needs, never more, never less. The mission pill remains the sole Expanded element and absorbs whatever space is left, ellipsizing first if it's tight. widget_test.dart: two tests exercising the footer at the default 800x600 test viewport started hitting a real RenderFlex overflow, since that width was never realistic for this landscape-only app's footer content in the first place (previously masked by the drone pill silently shrinking via FittedBox). Set a realistic device-sized viewport (2424x1080, matching the Pixel_10a emulator) for just those two tests. Verified on the Pixel_10a emulator: "disconnected" now renders fully legible in the drone pill, and the mission pill still reads normally alongside it. Co-Authored-By: Claude Sonnet 5 --- app/lib/ui/widgets/bottom_stats_bar.dart | 117 ++++++++++++----------- app/test/widget_test.dart | 19 ++++ 2 files changed, 79 insertions(+), 57 deletions(-) diff --git a/app/lib/ui/widgets/bottom_stats_bar.dart b/app/lib/ui/widgets/bottom_stats_bar.dart index 8ccf4d0..e555305 100644 --- a/app/lib/ui/widgets/bottom_stats_bar.dart +++ b/app/lib/ui/widgets/bottom_stats_bar.dart @@ -138,11 +138,27 @@ class BottomStatsBar extends ConsumerWidget { ), child: Row( children: [ - // Nur noch die Mission-Pille links - sie darf die gesamte - // verfuegbare Breite dieser Seite nutzen (Align loosent die - // Constraints statt sie wie Expanded zu erzwingen, das - // begrenzt auf die verfuegbare Breite, ohne einen kuenstlich - // kleinen Fixwert vorzugeben). + // Mission-Pille links bleibt das einzige flexible/absorbierende + // Element (Doku: "erlaube dass die Pille fuer den Drohnenstatus + // groesser skalieren kann ... und dafuer die Missionspille + // kleiner skalieren kann", "darauf achten dass drone status + // immer auf seinen inhalt skaliert") - vorher waren links UND + // rechts je ein gleich breites Expanded, wodurch die + // Drohnen-Pille bei langem Inhalt (z.B. "disconnected") per + // FittedBox bis zur Unlesbarkeit schrumpfte. Ein Flex- + // Verhaeltnis (z.B. 1:4) loest das NICHT sauber: Expanded/ + // Flexible erzwingen so oder so eine feste Bruchteils- + // Zuteilung, die die Missionspille selbst bei kurzen Namen auf + // ihren (kleineren) Anteil einschraenkt, unabhaengig davon, ob + // die Drohnen-Seite diesen Platz ueberhaupt braucht. Die + // Drohnen-Seite ist deshalb jetzt bewusst NICHT mehr + // Expanded/Flexible, sondern wie _detailsPill()/ + // _AltitudeProfileToggleButton ein normales, intrinsisch + // grosses Row-Kind - sie bekommt IMMER genau die Breite, die + // ihr aktueller Inhalt braucht, nie mehr und nie weniger. Die + // Missionspille als einziges Expanded-Element bekommt + // verlaesslich den kompletten Rest und schrumpft (ellipsiert) + // zuerst, wenn es eng wird. Expanded( child: Align( alignment: Alignment.centerLeft, @@ -166,58 +182,45 @@ class BottomStatsBar extends ConsumerWidget { const SizedBox(width: _footerElementSpacing), _detailsPill(), const SizedBox(width: _footerElementSpacing), - // Drohnen-Pille + Warnungs-/Settings-Knopf rechts. Die Pille - // sitzt in Flexible, damit sie bei langen Namen zu Gunsten der - // beiden festgroessen Rundknoepfe schrumpft/ellipsiert, statt - // sie aus dem sichtbaren Bereich zu draengen. - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: droneStatus != null - ? _DroneStatusPill( - status: droneStatus!, - // Oeffnet im Fly-Modus das neue Status-Menue - // (Doku: "ein neues menue anlegen ... nach dem - // vorbild des menues fuer drones and - // missions") direkt auf dem Status-Tab, statt - // wie zuvor die Drohnenprofil-Verwaltung - - // Profile werden waehrend des Fluges ohnehin - // nicht gewechselt. - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => const DroneStatusMessagesPanel.status(), - )), - ) - : _NameChip( - // Gleicher Hintergrund wie die Details-Pille - // (Doku: "gleichen Hintergrund wie bei der - // Detail Pille fuer die Drohne Pille - // verwenden") statt des vorherigen blauen - // Farbtons. - label: 'Drone: $droneName', - color: Colors.white.withValues(alpha: 0.12), - textColor: Colors.white, - onTap: () => Navigator.of(context).push(MaterialPageRoute( - builder: (_) => const MissionsDronesPanel.drones(), - )), - ), - ), - const SizedBox(width: _footerElementSpacing), - if (onSendTap != null) ...[ - _SendButton(sending: sending, onTap: onSendTap), - const SizedBox(width: _footerElementSpacing), - ] else if (warnings.isNotEmpty) ...[ - _WarningButton(onTap: onWarningsTap), - const SizedBox(width: _footerElementSpacing), - ], - _SettingsButton(onTap: onSettingsTap), - ], - ), - ), - ), + // Drohnen-Pille + Warnungs-/Settings-Knopf rechts - bewusst kein + // Expanded/Flexible (siehe Kommentar oben): immer intrinsische + // Breite, FittedBox in _DroneStatusPill bleibt nur als letztes + // Sicherheitsnetz fuer wirklich extreme Faelle (z.B. + // Grossschrift-Bedienungshilfen), statt schon im Normalbetrieb + // zu greifen. + droneStatus != null + ? _DroneStatusPill( + status: droneStatus!, + // Oeffnet im Fly-Modus das neue Status-Menue (Doku: "ein + // neues menue anlegen ... nach dem vorbild des menues + // fuer drones and missions") direkt auf dem Status-Tab, + // statt wie zuvor die Drohnenprofil-Verwaltung - Profile + // werden waehrend des Fluges ohnehin nicht gewechselt. + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const DroneStatusMessagesPanel.status(), + )), + ) + : _NameChip( + // Gleicher Hintergrund wie die Details-Pille (Doku: + // "gleichen Hintergrund wie bei der Detail Pille fuer + // die Drohne Pille verwenden") statt des vorherigen + // blauen Farbtons. + label: 'Drone: $droneName', + color: Colors.white.withValues(alpha: 0.12), + textColor: Colors.white, + onTap: () => Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const MissionsDronesPanel.drones(), + )), + ), + const SizedBox(width: _footerElementSpacing), + if (onSendTap != null) ...[ + _SendButton(sending: sending, onTap: onSendTap), + const SizedBox(width: _footerElementSpacing), + ] else if (warnings.isNotEmpty) ...[ + _WarningButton(onTap: onWarningsTap), + const SizedBox(width: _footerElementSpacing), + ], + _SettingsButton(onTap: onSettingsTap), ], ), ), diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart index 7961e93..8684b1b 100644 --- a/app/test/widget_test.dart +++ b/app/test/widget_test.dart @@ -1003,6 +1003,18 @@ void main() { testWidgets( 'Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet die Warnungsliste', (tester) async { + // Realistische (landschaftsbreite) Viewport-Groesse statt der 800x600- + // Standardvorgabe von flutter_test - die Fusszeile (BottomStatsBar) + // reiht mehrere intrinsisch grosse Pillen/Knoepfe nebeneinander (Doku: + // "darauf achten dass drone status immer auf seinen inhalt skaliert"), + // was bei 800px Breite selbst im Plan-Modus zu einem echten + // RenderFlex-Overflow fuehrt - auf jedem real relevanten Geraet (dieses + // Projekt ist landscape-only) ist dafuer genug Platz. + tester.view.physicalSize = const Size(2424, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final container = _testContainer(); addTearDown(container.dispose); @@ -1187,6 +1199,13 @@ void main() { testWidgets( 'Antippen einer Speed-Warnung oeffnet die Wegpunktliste direkt auf dem Speed-Tab', (tester) async { + // Realistische (landschaftsbreite) Viewport-Groesse statt der 800x600- + // Standardvorgabe - siehe Kommentar beim vorigen Warnungs-Button-Test. + tester.view.physicalSize = const Size(2424, 1080); + tester.view.devicePixelRatio = 1.0; + addTearDown(tester.view.resetPhysicalSize); + addTearDown(tester.view.resetDevicePixelRatio); + final container = _testContainer(); addTearDown(container.dispose);