Make inactive mode-switch button bleed to the header's true edge
The inactive Plan/Fly button used to sit inset by the header's own padding on all sides, leaving a visible strip of the header tint around it instead of filling the header exactly like the HTML prototype. CSS solves this with a negative margin, which Flutter's Padding widget rejects (padding.isNonNegative assertion) - restructured so only the active button stays wrapped in the inset padding, while the inactive one is a raw Row child that naturally touches the header container's true top/bottom/outer edges and gets clipped to its rounded corner. Also adds a short gradient from the header's own tint into the button's solid color at the seam, softening what used to be a hard color edge.
This commit is contained in:
@@ -29,9 +29,26 @@ import 'map_search_controls.dart';
|
||||
/// (FlyMapControls) - Ortssuche/Home entfallen dort, da im Fly-Modus keine
|
||||
/// Missionsbearbeitung stattfindet und "Home" (erster Wegpunkt) waehrend
|
||||
/// des Flugs kein sinnvoller Bezugspunkt mehr ist.
|
||||
///
|
||||
/// Der inaktive Moduswechsel-Button (im HTML-Demonstrator per negativem
|
||||
/// CSS-Margin geloest, `#topBarWrap.mode-edit #flyModeBtn{margin:-5px -10px
|
||||
/// -5px 0}`) sitzt hier bewusst als ROHES Row-Kind ohne umgebendes Padding
|
||||
/// (Flutters `Padding`-Widget erlaubt anders als CSS-Margin keine negativen
|
||||
/// Werte, siehe `padding.isNonNegative`-Assertion) - dadurch beruehrt er von
|
||||
/// selbst alle drei Aussenkanten der Leiste (oben/unten/aussen) und wird von
|
||||
/// deren abgerundetem Container (clipBehavior: Clip.antiAlias) exakt an der
|
||||
/// Kontur beschnitten. Der aktive Button bleibt dagegen ueber ein
|
||||
/// zusaetzliches Padding im Leisteninneren eingerueckt.
|
||||
class TopModeBar extends ConsumerWidget {
|
||||
const TopModeBar({super.key});
|
||||
|
||||
/// Einrueckung des jeweils AKTIVEN Moduswechsel-Buttons gegenueber der
|
||||
/// Leistenkante (HTML-Demonstrator: `#topBarWrap{padding:5px 10px}`) - der
|
||||
/// inaktive Button verzichtet bewusst darauf, siehe Klassendoku oben.
|
||||
static const _outerPaddingH = 10.0;
|
||||
static const _outerPaddingV = 5.0;
|
||||
static const _pillRadius = 26.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isFly = context.select((AppModeCubit c) => c.state.mode == AppMode.fly);
|
||||
@@ -50,6 +67,20 @@ class TopModeBar extends ConsumerWidget {
|
||||
MediaQuery.of(context).padding.right,
|
||||
);
|
||||
|
||||
final middleContent = isFly
|
||||
? const [
|
||||
HeaderWindPill(),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: FlightModePill()),
|
||||
SizedBox(width: 8),
|
||||
FlyMapControls(),
|
||||
]
|
||||
: const [
|
||||
HeaderWindPill(),
|
||||
SizedBox(width: 8),
|
||||
Expanded(child: MapSearchControls()),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: topInset,
|
||||
@@ -59,12 +90,18 @@ class TopModeBar extends ConsumerWidget {
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 0.77,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
// Schneidet den bis an den Rand reichenden inaktiven
|
||||
// Moduswechsel-Button (siehe Klassendoku) exakt an der
|
||||
// abgerundeten Leistenkontur ab, statt dass er rechteckig ueber
|
||||
// die Rundung hinaussteht - dieselbe Kontur uebernimmt gleich
|
||||
// auch die Eckenrundung fuer beide Buttons, eine eigene
|
||||
// BorderRadius je Button ist dadurch nicht mehr noetig.
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: BoxDecoration(
|
||||
color: activeTint.withValues(alpha: 0.55),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
bottomRight: Radius.circular(26),
|
||||
bottomLeft: Radius.circular(_pillRadius),
|
||||
bottomRight: Radius.circular(_pillRadius),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -72,34 +109,28 @@ class TopModeBar extends ConsumerWidget {
|
||||
_ModeButton(
|
||||
label: 'Plan',
|
||||
isActive: !isFly,
|
||||
activeTint: activeTint,
|
||||
inactiveColor: DmcColors.planModeColor,
|
||||
cornerRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(26),
|
||||
),
|
||||
isLeftButton: true,
|
||||
onTap: () => context.read<AppModeCubit>().toPlan(),
|
||||
),
|
||||
if (isFly) ...[
|
||||
const SizedBox(width: 8),
|
||||
const HeaderWindPill(),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(child: FlightModePill()),
|
||||
const SizedBox(width: 8),
|
||||
const FlyMapControls(),
|
||||
const SizedBox(width: 8),
|
||||
] else ...[
|
||||
const SizedBox(width: 8),
|
||||
const HeaderWindPill(),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(child: MapSearchControls()),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: isFly ? 8 : 12,
|
||||
top: _outerPaddingV,
|
||||
bottom: _outerPaddingV,
|
||||
),
|
||||
child: Row(children: middleContent),
|
||||
),
|
||||
),
|
||||
_ModeButton(
|
||||
label: 'Fly',
|
||||
isActive: isFly,
|
||||
activeTint: activeTint,
|
||||
inactiveColor: DmcColors.flyModeColor,
|
||||
cornerRadius: const BorderRadius.only(
|
||||
bottomRight: Radius.circular(26),
|
||||
),
|
||||
isLeftButton: false,
|
||||
onTap: () {
|
||||
context.read<AppModeCubit>().toFly();
|
||||
// Eintritt in den Fly-Modus setzt immer auf Waypoint
|
||||
@@ -126,29 +157,62 @@ class _ModeButton extends StatelessWidget {
|
||||
const _ModeButton({
|
||||
required this.label,
|
||||
required this.isActive,
|
||||
required this.activeTint,
|
||||
required this.inactiveColor,
|
||||
required this.cornerRadius,
|
||||
required this.isLeftButton,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final bool isActive;
|
||||
|
||||
/// Aktuelle Kopfleisten-Toenung (TopModeBar.activeTint) - dient nur als
|
||||
/// Startfarbe des Farbverlaufs im inaktiven Zustand (siehe unten), der
|
||||
/// aktive Button hat ohnehin keinen eigenen Hintergrund.
|
||||
final Color activeTint;
|
||||
final Color inactiveColor;
|
||||
final BorderRadius cornerRadius;
|
||||
|
||||
/// true = Plan-Button (linkes Ende der Leiste), false = Fly-Button
|
||||
/// (rechtes Ende) - bestimmt sowohl die Seite der Einrueckung im aktiven
|
||||
/// Zustand als auch die Verlaufsrichtung des Farbverlaufs im inaktiven.
|
||||
final bool isLeftButton;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
final button = GestureDetector(
|
||||
onTap: isActive ? null : onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: isActive ? 6 : 11,
|
||||
// Grosszuegiger als der aktive Zustand (6), damit dieser
|
||||
// Button - roh, ohne umgebendes Leisten-Padding, siehe
|
||||
// TopModeBar-Klassendoku - garantiert der hoechste Row-Inhalt
|
||||
// bleibt und dadurch die Leiste exakt auf ihre volle Hoehe
|
||||
// (Text + diese Polsterung) bringt, statt von einem anderen
|
||||
// Zeileninhalt (z.B. den 34px hohen Pillen in der Mitte)
|
||||
// ueberragt zu werden und sichtbaren Rand stehen zu lassen.
|
||||
vertical: isActive ? 6 : 16,
|
||||
),
|
||||
decoration: isActive
|
||||
? null
|
||||
: BoxDecoration(
|
||||
// Kurzer Farbverlauf von der Kopfleisten-eigenen Toenung zur
|
||||
// vollen Button-Farbe statt einer harten Kante an der
|
||||
// Nahtstelle zum mittleren Inhalt (Nutzerwunsch: "linearer
|
||||
// kurzer Farbverlauf zwischen button und rest header") - nur
|
||||
// die ersten 30% der Breite blenden ein, der Rest bleibt
|
||||
// solide, damit der Text weiterhin auf vollem Kontrast sitzt.
|
||||
gradient: LinearGradient(
|
||||
begin: isLeftButton ? Alignment.centerRight : Alignment.centerLeft,
|
||||
end: isLeftButton ? Alignment.centerLeft : Alignment.centerRight,
|
||||
colors: [
|
||||
activeTint.withValues(alpha: 0.55),
|
||||
inactiveColor.withValues(alpha: 0.85),
|
||||
],
|
||||
stops: const [0.0, 0.3],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? null : inactiveColor.withValues(alpha: 0.85),
|
||||
borderRadius: isActive ? null : cornerRadius,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
@@ -160,5 +224,20 @@ class _ModeButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!isActive) return button;
|
||||
|
||||
// Nur der aktive Button bleibt gegenueber der Leistenkante eingerueckt
|
||||
// (HTML-Demonstrator: unveraendertes #topBarWrap-Padding gilt fuer ihn
|
||||
// weiterhin) - der inaktive bleibt roh (siehe Klassendoku).
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: TopModeBar._outerPaddingV,
|
||||
bottom: TopModeBar._outerPaddingV,
|
||||
left: isLeftButton ? TopModeBar._outerPaddingH : 0,
|
||||
right: isLeftButton ? 0 : TopModeBar._outerPaddingH,
|
||||
),
|
||||
child: button,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user