Files
Garage-SDKs/garage_ui/lib/pane_overlay.dart
T
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

90 lines
3.2 KiB
Dart

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<T?> showPaneOverlay<T>({
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<T>(
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<double>(
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),
),
),
),
),
],
);
},
);
}