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 <noreply@anthropic.com>
This commit is contained in:
Constantin Leue
2026-08-06 21:35:19 +02:00
co-authored by Claude Sonnet 5
parent 1b92e6a452
commit 7ff88c5435
2 changed files with 79 additions and 57 deletions
+60 -57
View File
@@ -138,11 +138,27 @@ class BottomStatsBar extends ConsumerWidget {
), ),
child: Row( child: Row(
children: [ children: [
// Nur noch die Mission-Pille links - sie darf die gesamte // Mission-Pille links bleibt das einzige flexible/absorbierende
// verfuegbare Breite dieser Seite nutzen (Align loosent die // Element (Doku: "erlaube dass die Pille fuer den Drohnenstatus
// Constraints statt sie wie Expanded zu erzwingen, das // groesser skalieren kann ... und dafuer die Missionspille
// begrenzt auf die verfuegbare Breite, ohne einen kuenstlich // kleiner skalieren kann", "darauf achten dass drone status
// kleinen Fixwert vorzugeben). // 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( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
@@ -166,58 +182,45 @@ class BottomStatsBar extends ConsumerWidget {
const SizedBox(width: _footerElementSpacing), const SizedBox(width: _footerElementSpacing),
_detailsPill(), _detailsPill(),
const SizedBox(width: _footerElementSpacing), const SizedBox(width: _footerElementSpacing),
// Drohnen-Pille + Warnungs-/Settings-Knopf rechts. Die Pille // Drohnen-Pille + Warnungs-/Settings-Knopf rechts - bewusst kein
// sitzt in Flexible, damit sie bei langen Namen zu Gunsten der // Expanded/Flexible (siehe Kommentar oben): immer intrinsische
// beiden festgroessen Rundknoepfe schrumpft/ellipsiert, statt // Breite, FittedBox in _DroneStatusPill bleibt nur als letztes
// sie aus dem sichtbaren Bereich zu draengen. // Sicherheitsnetz fuer wirklich extreme Faelle (z.B.
Expanded( // Grossschrift-Bedienungshilfen), statt schon im Normalbetrieb
child: Align( // zu greifen.
alignment: Alignment.centerRight, droneStatus != null
child: Row( ? _DroneStatusPill(
mainAxisSize: MainAxisSize.min, status: droneStatus!,
children: [ // Oeffnet im Fly-Modus das neue Status-Menue (Doku: "ein
Flexible( // neues menue anlegen ... nach dem vorbild des menues
child: droneStatus != null // fuer drones and missions") direkt auf dem Status-Tab,
? _DroneStatusPill( // statt wie zuvor die Drohnenprofil-Verwaltung - Profile
status: droneStatus!, // werden waehrend des Fluges ohnehin nicht gewechselt.
// Oeffnet im Fly-Modus das neue Status-Menue onTap: () => Navigator.of(context).push(MaterialPageRoute(
// (Doku: "ein neues menue anlegen ... nach dem builder: (_) => const DroneStatusMessagesPanel.status(),
// vorbild des menues fuer drones and )),
// missions") direkt auf dem Status-Tab, statt )
// wie zuvor die Drohnenprofil-Verwaltung - : _NameChip(
// Profile werden waehrend des Fluges ohnehin // Gleicher Hintergrund wie die Details-Pille (Doku:
// nicht gewechselt. // "gleichen Hintergrund wie bei der Detail Pille fuer
onTap: () => Navigator.of(context).push(MaterialPageRoute( // die Drohne Pille verwenden") statt des vorherigen
builder: (_) => const DroneStatusMessagesPanel.status(), // blauen Farbtons.
)), label: 'Drone: $droneName',
) color: Colors.white.withValues(alpha: 0.12),
: _NameChip( textColor: Colors.white,
// Gleicher Hintergrund wie die Details-Pille onTap: () => Navigator.of(context).push(MaterialPageRoute(
// (Doku: "gleichen Hintergrund wie bei der builder: (_) => const MissionsDronesPanel.drones(),
// Detail Pille fuer die Drohne Pille )),
// verwenden") statt des vorherigen blauen ),
// Farbtons. const SizedBox(width: _footerElementSpacing),
label: 'Drone: $droneName', if (onSendTap != null) ...[
color: Colors.white.withValues(alpha: 0.12), _SendButton(sending: sending, onTap: onSendTap),
textColor: Colors.white, const SizedBox(width: _footerElementSpacing),
onTap: () => Navigator.of(context).push(MaterialPageRoute( ] else if (warnings.isNotEmpty) ...[
builder: (_) => const MissionsDronesPanel.drones(), _WarningButton(onTap: onWarningsTap),
)), const SizedBox(width: _footerElementSpacing),
), ],
), _SettingsButton(onTap: onSettingsTap),
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),
],
),
),
),
], ],
), ),
), ),
+19
View File
@@ -1003,6 +1003,18 @@ void main() {
testWidgets( testWidgets(
'Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet die Warnungsliste', 'Warnungs-Button erscheint bei Reichweiten-Ueberschreitung und oeffnet die Warnungsliste',
(tester) async { (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(); final container = _testContainer();
addTearDown(container.dispose); addTearDown(container.dispose);
@@ -1187,6 +1199,13 @@ void main() {
testWidgets( testWidgets(
'Antippen einer Speed-Warnung oeffnet die Wegpunktliste direkt auf dem Speed-Tab', 'Antippen einer Speed-Warnung oeffnet die Wegpunktliste direkt auf dem Speed-Tab',
(tester) async { (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(); final container = _testContainer();
addTearDown(container.dispose); addTearDown(container.dispose);