diff --git a/example/lib/src/storybook/stories/primitives/toast.dart b/example/lib/src/storybook/stories/primitives/toast.dart index bd736d7e..9a98e76f 100644 --- a/example/lib/src/storybook/stories/primitives/toast.dart +++ b/example/lib/src/storybook/stories/primitives/toast.dart @@ -33,6 +33,22 @@ class ToastStory extends StatelessWidget { ], ); + final headerAlignmentKnob = context.knobs.nullable.options( + label: "headerAlignment", + description: + "MainAxisAlignment for MoonToast header slots (width has to be provided).", + enabled: false, + initial: MainAxisAlignment.center, + options: const [ + Option(label: "start", value: MainAxisAlignment.start), + Option(label: "center", value: MainAxisAlignment.center), + Option(label: "end", value: MainAxisAlignment.end), + Option(label: "spaceBetween", value: MainAxisAlignment.spaceBetween), + Option(label: "spaceAround", value: MainAxisAlignment.spaceAround), + Option(label: "spaceEvenly", value: MainAxisAlignment.spaceEvenly), + ], + ); + final toastVariantKnob = context.knobs.nullable.options( label: "variant", description: "The color variant for MoonToast.", @@ -85,15 +101,6 @@ class ToastStory extends StatelessWidget { max: 32, ); - final displayDurationKnob = context.knobs.nullable.sliderInt( - label: "displayDuration", - description: "Display duration for MoonToast.", - enabled: false, - initial: 3, - min: 1, - max: 10, - ); - final widthKnob = context.knobs.nullable.slider( label: "width", description: @@ -104,11 +111,31 @@ class ToastStory extends StatelessWidget { max: MediaQuery.of(context).size.width, ); - final isPersistentKnob = context.knobs.boolean( - label: "isPersistent", + final horizontalGapKnob = context.knobs.nullable.sliderInt( + label: "horizontalGap", + description: + "Horizontal gap between leading, label and trailing slots of MoonToast.", + enabled: false, + initial: 16, + max: 32, + ); + + final verticalGapKnob = context.knobs.nullable.sliderInt( + label: "verticalGap", description: - "Whether MoonToast is persistent across screens (will not behave as " - "expected only in Storybook).", + "Vertical gap between leading, label and trailing slots of MoonToast.", + enabled: false, + initial: 16, + max: 32, + ); + + final displayDurationKnob = context.knobs.nullable.sliderInt( + label: "displayDuration", + description: "Display duration for MoonToast.", + enabled: false, + initial: 3, + min: 1, + max: 10, ); final useSafeAreaKnob = context.knobs.boolean( @@ -134,33 +161,35 @@ class ToastStory extends StatelessWidget { onTap: () { MoonToast.show( context, - backgroundColor: backgroundColor, - isPersistent: isPersistentKnob, - useSafeArea: useSafeAreaKnob, width: widthKnob, - toastAlignment: toastAlignmentKnob ?? Alignment.bottomCenter, + useSafeArea: useSafeAreaKnob, + horizontalGap: horizontalGapKnob?.toDouble(), + verticalGap: verticalGapKnob?.toDouble(), + backgroundColor: backgroundColor, variant: toastVariantKnob ?? MoonToastVariant.original, - displayDuration: displayDurationKnob != null - ? Duration(seconds: displayDurationKnob) - : null, + headerAlignment: headerAlignmentKnob, + toastAlignment: toastAlignmentKnob ?? Alignment.bottomCenter, + displayDuration: Duration(seconds: displayDurationKnob ?? 3), borderRadius: borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, leading: Icon( - MoonIcons.generic_info_24_light, + MoonIcons.generic_about_24_light, color: iconColor, ), - label: Text( - customLabelTextKnob, - style: TextStyle(color: textColor), + label: Flexible( + child: Text( + customLabelTextKnob, + style: TextStyle(color: textColor), + ), ), trailing: Icon( - MoonIcons.generic_star_24_light, + MoonIcons.generic_info_24_light, color: iconColor, ), content: showContentKnob ? Align( - alignment: AlignmentDirectional.centerStart, + alignment: AlignmentDirectional.center, child: Text( "Here goes MoonToast content", style: TextStyle(color: textColor), diff --git a/lib/src/widgets/toast/toast.dart b/lib/src/widgets/toast/toast.dart index d793776f..288ee904 100644 --- a/lib/src/widgets/toast/toast.dart +++ b/lib/src/widgets/toast/toast.dart @@ -1,18 +1,13 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; +import 'package:mix/mix.dart'; import 'package:moon_core/moon_core.dart'; -import 'package:moon_design/src/theme/theme.dart'; -import 'package:moon_design/src/theme/toast/toast_theme.dart'; import 'package:moon_design/src/theme/tokens/borders.dart'; import 'package:moon_design/src/theme/tokens/shadows.dart'; import 'package:moon_design/src/theme/tokens/sizes.dart'; import 'package:moon_design/src/theme/tokens/transitions.dart'; import 'package:moon_design/src/theme/tokens/typography/typography.dart'; -import 'package:moon_design/src/utils/extensions.dart'; -import 'package:moon_design/src/utils/squircle/squircle_border.dart'; import 'package:moon_tokens/moon_tokens.dart'; @@ -22,14 +17,6 @@ enum MoonToastVariant { } class MoonToast { - static const double _toastTravelDistance = 64.0; - static const Duration _timeBetweenToasts = Duration(milliseconds: 200); - - static final _toastQueue = <_ToastEntry>[]; - - static Timer? _timer; - static OverlayEntry? _entry; - /// Creates a Moon Design toast. const MoonToast(); @@ -39,11 +26,15 @@ class MoonToast { /// The alignment (position) of the toast. AlignmentGeometry toastAlignment = Alignment.bottomCenter, + /// The alignment of the [leading], [label] and [trailing] slots of the + /// toast header. Applies only when the [width] is specified. + MainAxisAlignment? headerAlignment, + /// Whether the toast is persistent (attaches to root navigator). bool isPersistent = true, - /// Whether to use the [SafeArea] for the toast (takes into account notches - /// and native system bars). + /// Whether to use the [SafeArea] for the toast. + /// Takes into account notches and native system bars. bool useSafeArea = true, /// The border radius of the toast. @@ -64,7 +55,7 @@ class MoonToast { double? width, /// The duration to display the toast. - Duration? displayDuration, + Duration displayDuration = const Duration(seconds: 3), /// The duration of the toast transition animation (slide in or out). Duration? transitionDuration, @@ -104,250 +95,106 @@ class MoonToast { /// The widget to display below the toast header. Widget? content, }) { - assert( - displayDuration == null || (displayDuration > _timeBetweenToasts), - 'The display duration must be greater than the time between toasts ' - '(200 ms).', - ); + final MainAxisAlignment effectiveHeaderAlignment = width != null + ? headerAlignment ?? MainAxisAlignment.spaceBetween + : MainAxisAlignment.center; - final MoonToastTheme? toastTheme = context.moonTheme?.toastTheme; + final BorderRadiusGeometry effectiveBorderRadius = + borderRadius ?? MoonBorders.borders.surfaceSm; - final BorderRadiusGeometry effectiveBorderRadius = borderRadius ?? - toastTheme?.properties.borderRadius ?? - MoonBorders.borders.surfaceSm; + final bool isOriginal = variant == MoonToastVariant.original; final Color effectiveBackgroundColor = backgroundColor ?? - (variant == MoonToastVariant.original - ? (toastTheme?.colors.lightVariantBackgroundColor ?? - MoonColors.light.goku) - : (toastTheme?.colors.darkVariantBackgroundColor ?? - MoonColors.dark.goku)); - - final Color effectiveTextColor = variant == MoonToastVariant.original - ? (toastTheme?.colors.lightVariantTextColor ?? - MoonColors.light.textPrimary) - : (toastTheme?.colors.darkVariantTextColor ?? - MoonColors.dark.textPrimary); - - final Color effectiveIconColor = variant == MoonToastVariant.original - ? (toastTheme?.colors.lightVariantIconColor ?? - MoonColors.light.iconPrimary) - : (toastTheme?.colors.darkVariantIconColor ?? - MoonColors.dark.iconPrimary); - - final TextStyle effectiveTextStyle = toastTheme?.properties.textStyle ?? - MoonTypography.typography.body.textDefault; + (isOriginal ? MoonColors.light.goku : MoonColors.dark.goku); - final double effectiveHorizontalGap = horizontalGap ?? - toastTheme?.properties.horizontalGap ?? - MoonSizes.sizes.x2s; + final Color effectiveTextColor = + isOriginal ? MoonColors.light.textPrimary : MoonColors.dark.textPrimary; - final double effectiveVerticalGap = verticalGap ?? - toastTheme?.properties.verticalGap ?? - MoonSizes.sizes.x3s; + final Color effectiveIconColor = + isOriginal ? MoonColors.light.iconPrimary : MoonColors.dark.iconPrimary; - final Duration effectiveDisplayDuration = displayDuration ?? - toastTheme?.properties.displayDuration ?? - const Duration(seconds: 3); + final double effectiveHorizontalGap = horizontalGap ?? MoonSizes.sizes.x2s; + + final double effectiveVerticalGap = verticalGap ?? MoonSizes.sizes.x3s; final Duration effectiveTransitionDuration = transitionDuration ?? - toastTheme?.properties.transitionDuration ?? MoonTransitions.transitions.defaultTransitionDuration; - final Curve effectiveTransitionCurve = transitionCurve ?? - toastTheme?.properties.transitionCurve ?? - MoonTransitions.transitions.defaultTransitionCurve; + final Curve effectiveTransitionCurve = + transitionCurve ?? MoonTransitions.transitions.defaultTransitionCurve; - final EdgeInsetsGeometry effectiveContentPadding = padding ?? - toastTheme?.properties.contentPadding ?? - EdgeInsets.all(MoonSizes.sizes.x2s); + final EdgeInsetsGeometry effectiveContentPadding = + padding ?? EdgeInsets.all(MoonSizes.sizes.x2s); final EdgeInsets resolvedContentPadding = effectiveContentPadding.resolve(Directionality.of(context)); - final List effectiveToastShadows = toastShadows ?? - toastTheme?.shadows.toastShadows ?? - MoonShadows.light.lg; - - final effectiveContext = isPersistent - ? (Navigator.maybeOf(context, rootNavigator: true)?.context ?? context) - : context; + final List effectiveToastShadows = + toastShadows ?? MoonShadows.light.lg; - final CapturedThemes themes = InheritedTheme.capture( - from: context, - to: Navigator.of(effectiveContext).context, - ); + final TextStyle effectiveTextStyle = + MoonTypography.typography.body.textDefault; - final OverlayEntry entry = OverlayEntry( - builder: (BuildContext context) { - return RepaintBoundary( - child: TweenAnimationBuilder( - duration: effectiveTransitionDuration, - curve: effectiveTransitionCurve, - tween: Tween(begin: 0.0, end: 1.0), - builder: (BuildContext context, double progress, Widget? child) { - return SafeArea( - left: useSafeArea, - top: useSafeArea, - right: useSafeArea, - bottom: useSafeArea, - maintainBottomViewPadding: true, - child: Align( - alignment: toastAlignment, - child: Transform( - transform: Matrix4.translationValues( - switch (toastAlignment) { - Alignment.topLeft || - Alignment.centerLeft || - Alignment.bottomLeft => - -_toastTravelDistance + - progress * _toastTravelDistance, - Alignment.topRight || - Alignment.centerRight || - Alignment.bottomRight => - (1 - progress) * _toastTravelDistance, - _ => 0 - }, - switch (toastAlignment) { - Alignment.topCenter => -_toastTravelDistance + - progress * _toastTravelDistance, - Alignment.bottomCenter => - (1 - progress) * _toastTravelDistance, - _ => 0 - }, - 0, - ), - child: Opacity( - opacity: progress, - child: child, - ), - ), - ), - ); - }, - child: themes.wrap( - Semantics( - label: semanticLabel, - child: IconTheme( - data: IconThemeData(color: effectiveIconColor), - child: DefaultTextStyle( - style: effectiveTextStyle.copyWith( - color: effectiveTextColor, - ), - child: Container( - margin: margin ?? resolvedContentPadding, - padding: resolvedContentPadding, - width: width, - decoration: decoration ?? - ShapeDecorationWithPremultipliedAlpha( - color: effectiveBackgroundColor, - shadows: effectiveToastShadows, - shape: MoonSquircleBorder( - borderRadius: effectiveBorderRadius - .squircleBorderRadius(context), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: width != null - ? MainAxisAlignment.spaceBetween - : MainAxisAlignment.center, - textDirection: Directionality.of(context), - children: [ - if (leading != null) ...[ - leading, - SizedBox(width: effectiveHorizontalGap), - ], - Flexible(child: label), - if (trailing != null) ...[ - SizedBox(width: effectiveHorizontalGap), - trailing, - ], - ], - ), - if (content != null) ...[ - SizedBox(height: effectiveVerticalGap), - content, - ], - ], - ), - ), - ), - ), - ), + final TextStyle resolvedTextStyle = + effectiveTextStyle.copyWith(color: effectiveTextColor); + + final Style toastStyle = Style( + $box.chain + ..margin.as(margin ?? resolvedContentPadding) + ..padding.as(resolvedContentPadding), + width != null ? $box.width(width) : null, + decorationToAttribute( + decoration ?? + ShapeDecorationWithPremultipliedAlpha( + color: effectiveBackgroundColor, + shadows: effectiveToastShadows, + shape: MoonBorder(borderRadius: effectiveBorderRadius), ), - ), - ); - }, + ), + $with.defaultTextStyle.style.as(resolvedTextStyle), + $with.iconTheme.data.color(effectiveIconColor), ); - final toastEntry = _ToastEntry( - buildContext: effectiveContext, - overlayEntry: entry, + final Style childColumnStyle = Style( + $flex.chain + ..mainAxisSize.min() + ..gap.call(effectiveVerticalGap), ); - _toastQueue.add(toastEntry); - - if (_timer == null) _showToastOverlay(duration: effectiveDisplayDuration); - } - - /// Clear the toast queue. - static void clearToastQueue() { - _timer?.cancel(); - _timer = null; - - _entry?.remove(); - _entry = null; - - _toastQueue.clear(); - } - - static void _showToastOverlay({required Duration duration}) { - if (_toastQueue.isEmpty) { - _entry = null; - return; - } - - final toastEntry = _toastQueue.removeAt(0); - - if (!toastEntry.buildContext.mounted) { - clearToastQueue(); - return; - } - - _entry = toastEntry.overlayEntry; - _timer = Timer(duration, () => _removeToastOverlay(duration: duration)); - - Future.delayed( - _timeBetweenToasts, - () { - if (toastEntry.buildContext.mounted) { - Navigator.of(toastEntry.buildContext).overlay?.insert(_entry!); - } - }, + final Style childHeaderStyle = Style( + $flex.chain + ..gap.call(effectiveHorizontalGap) + ..textDirection.call(Directionality.of(context)) + ..mainAxisAlignment.call(effectiveHeaderAlignment), + width == null ? $flex.mainAxisSize.min() : null, ); - } - static void _removeToastOverlay({required Duration duration}) { - _timer?.cancel(); - _timer = null; - - _entry?.remove(); - _entry = null; + final Widget child = StyledColumn( + style: childColumnStyle, + children: [ + StyledRow( + style: childHeaderStyle, + children: [ + if (leading != null) leading, + label, + if (trailing != null) trailing, + ], + ), + if (content != null) content, + ], + ); - _showToastOverlay(duration: duration); + MoonRawToast.show( + context, + style: toastStyle, + semanticLabel: semanticLabel, + transitionCurve: effectiveTransitionCurve, + transitionDuration: effectiveTransitionDuration, + isPersistent: isPersistent, + displayDuration: displayDuration, + toastAlignment: toastAlignment, + useSafeArea: useSafeArea, + child: child, + ); } } - -class _ToastEntry { - final BuildContext buildContext; - final OverlayEntry overlayEntry; - - _ToastEntry({ - required this.buildContext, - required this.overlayEntry, - }); -}