import "dart:ui" show ImageFilter; import "package:flutter/widgets.dart"; import "package:garage_ui/theme/garage_theme.dart"; /// The entrance a Garage pane dialog makes: blurred backdrop, dimmed scrim, /// fade and scale in, width clamped to the viewport. /// /// This lived twice in Arcs & Angles - once for the titled pane and once for /// the Blender-language one - identical apart from the shell widget wrapped /// around the content. The presentation is the generic half, so it lives here /// and [builder] supplies the shell. /// /// [builder] is handed an `onClose` that pops the route, or null when /// [dismissible] is false, so the shell can wire its own close affordance. /// Content pops with its own result the usual way. Future showPaneOverlay({ required BuildContext context, required Widget Function(BuildContext context, VoidCallback? onClose) builder, double maxWidth = 450, bool dismissible = true, String? barrierLabel, Duration duration = const Duration(milliseconds: 180), double blurSigma = 3, double scrimOpacity = 0.35, double enterScale = 0.93, double viewportInset = 24, }) { // read before the route is pushed so it reflects the theme at the call site // rather than whatever sits above the navigator. final scrim = GarageTheme.of(context).colorScheme.background; return showGeneralDialog( context: context, barrierDismissible: false, barrierColor: const Color(0x00000000), barrierLabel: barrierLabel, transitionDuration: duration, pageBuilder: (ctx, anim, secondaryAnim) => const SizedBox.shrink(), transitionBuilder: (ctx, anim, secondaryAnim, _) { final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut); final scale = Tween( begin: enterScale, end: 1.0, ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut)); final screenW = MediaQuery.sizeOf(ctx).width; final maxW = (screenW - viewportInset).clamp(0.0, maxWidth); void close() => Navigator.of(ctx).pop(); return Stack( children: [ Positioned.fill( child: FadeTransition( opacity: fade, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma), child: GestureDetector( // a dismiss scrim, not a control - Escape and the dialog's // own close button are the reachable ways out. Left in the // tree it reads as a full-screen unnamed button. excludeFromSemantics: true, onTap: dismissible ? close : null, child: ColoredBox( color: scrim.withValues(alpha: scrimOpacity), ), ), ), ), ), Center( child: FadeTransition( opacity: fade, child: ScaleTransition( scale: scale, child: ConstrainedBox( constraints: BoxConstraints(minWidth: maxW, maxWidth: maxW), child: builder(ctx, dismissible ? close : null), ), ), ), ), ], ); }, ); }