import "package:flutter/widgets.dart"; import "package:garage_ui/theme/garage_theme.dart"; /// How long a field takes to admit theres something wrong with it, and how /// long the row takes to make room for saying so. One number for both: the /// outline and the line under it are one event and shouldnt arrive at /// different times. const Duration kFieldErrorDuration = Duration(milliseconds: 180); const Curve kFieldErrorCurve = Curves.easeInOutCubic; /// "This one was rejected", published over a control rather than set on it. /// /// A row knows the value is wrong. What the row is HOLDING could be anything - /// a text box, a select, a group of them - so the row cant reach in and colour /// it. It says so here instead, and every control that draws itself a border /// reads this on the way past. /// /// Only the border moves. A field whose fill goes red reads as a state the /// thing is permanently in; a red outline reads as a correction, which is what /// this is - and it goes back to normal the moment the value does. class FieldErrorScope extends InheritedWidget { const FieldErrorScope({ super.key, required this.invalid, required super.child, }); final bool invalid; static bool of(BuildContext context) => context.dependOnInheritedWidgetOfExactType()?.invalid ?? false; @override bool updateShouldNotify(FieldErrorScope old) => old.invalid != invalid; } /// The destructive outline, faded in OVER whatever border is allready there. /// /// Over rather than instead of, deliberately. A control's fill changes the /// instant you hover it - button.dart says so in as many words, and a /// TextField matches it - so rebuilding the decoration with an animated /// colour in it would have dragged the fill into the transition too and made /// every hover ease. This sits on top and is the only thing that moves. /// /// It also means a control with no border at all - a ghost select, say - gets /// one here for nothing, which is what "the border goes red" needs in order /// to mean anything. class FieldErrorOutline extends StatelessWidget { const FieldErrorOutline({ super.key, required this.invalid, required this.borderRadius, required this.child, }); final bool invalid; final BorderRadiusGeometry borderRadius; final Widget child; @override Widget build(BuildContext context) { final theme = GarageTheme.of(context); return Stack( children: [ child, // Positioned.fill, so it takes the child's size rather than joining in // deciding what that size is. Positioned.fill( child: IgnorePointer( child: AnimatedOpacity( opacity: invalid ? 1.0 : 0.0, duration: kFieldErrorDuration, curve: kFieldErrorCurve, child: DecoratedBox( decoration: BoxDecoration( borderRadius: borderRadius, border: Border.all( color: theme.colorScheme.destructive, width: theme.density.controlBorderWidth, ), ), ), ), ), ), ], ); } }