From 6e0eefd9a340f18f495863c7ed280ec6bb423b84 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 13 Oct 2022 18:04:07 -0300 Subject: [PATCH 1/5] Add the TeachingTip widget --- example/lib/main.dart | 8 ++ example/lib/routes/surfaces.dart | 1 + example/lib/screens/surface/teaching_tip.dart | 57 +++++++++++ example/pubspec.lock | 2 +- lib/fluent_ui.dart | 1 + lib/src/controls/surfaces/teaching_tip.dart | 99 +++++++++++++++++++ 6 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 example/lib/screens/surface/teaching_tip.dart create mode 100644 lib/src/controls/surfaces/teaching_tip.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index ffdeaeeb7..21b783502 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -296,6 +296,14 @@ class _MyHomePageState extends State with WindowListener { () => surfaces.ProgressIndicatorsPage(), ), ), + PaneItem( + icon: const Icon(FluentIcons.field_filled), + title: const Text('Teaching Tip'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.TeachingTipPage(), + ), + ), PaneItem( icon: const Icon(FluentIcons.tiles), title: const Text('Tiles'), diff --git a/example/lib/routes/surfaces.dart b/example/lib/routes/surfaces.dart index 530d1cc2b..c13eb54a8 100644 --- a/example/lib/routes/surfaces.dart +++ b/example/lib/routes/surfaces.dart @@ -5,5 +5,6 @@ export '../screens/surface/expander.dart'; export '../screens/surface/flyouts.dart'; export '../screens/surface/info_bars.dart'; export '../screens/surface/progress_indicators.dart'; +export '../screens/surface/teaching_tip.dart'; export '../screens/surface/tiles.dart'; export '../screens/surface/tooltip.dart'; diff --git a/example/lib/screens/surface/teaching_tip.dart b/example/lib/screens/surface/teaching_tip.dart new file mode 100644 index 000000000..be260a7f9 --- /dev/null +++ b/example/lib/screens/surface/teaching_tip.dart @@ -0,0 +1,57 @@ +import 'package:example/theme.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:provider/provider.dart'; + +class TeachingTipPage extends StatefulWidget { + const TeachingTipPage({Key? key}) : super(key: key); + + @override + State createState() => _TeachingTipPageState(); +} + +class _TeachingTipPageState extends State with PageMixin { + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + final appTheme = context.watch(); + + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Teaching Tip')), + children: [ + description( + content: const Text( + 'A teaching tip is a semi-persistent and content-rich flyout ' + 'that provides contextual information. It is often used for ' + 'informing, reminding, and teaching users about important and new ' + 'features that may enhance their experience.', + ), + ), + Center( + child: TeachingTip( + title: const Text('Change themes without hassle'), + subtitle: const Text( + 'It\'s easier to see control samples in both light and dark theme', + ), + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + if (theme.brightness.isDark) { + appTheme.mode = ThemeMode.light; + } else { + appTheme.mode = ThemeMode.dark; + } + }, + ), + Button( + child: const Text('Got it'), + onPressed: () {}, + ), + ], + ), + ), + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 76cff645a..46ac0cf08 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -91,7 +91,7 @@ packages: path: ".." relative: true source: path - version: "4.0.1" + version: "4.0.2" flutter: dependency: "direct main" description: flutter diff --git a/lib/fluent_ui.dart b/lib/fluent_ui.dart index a6dca333a..136d90451 100644 --- a/lib/fluent_ui.dart +++ b/lib/fluent_ui.dart @@ -77,6 +77,7 @@ export 'src/controls/surfaces/info_bar.dart'; export 'src/controls/surfaces/list_tile.dart'; export 'src/controls/surfaces/progress_indicators.dart'; export 'src/controls/surfaces/snackbar.dart'; +export 'src/controls/surfaces/teaching_tip.dart'; export 'src/controls/surfaces/tooltip.dart'; export 'src/controls/utils/divider.dart'; diff --git a/lib/src/controls/surfaces/teaching_tip.dart b/lib/src/controls/surfaces/teaching_tip.dart new file mode 100644 index 000000000..dbea02f23 --- /dev/null +++ b/lib/src/controls/surfaces/teaching_tip.dart @@ -0,0 +1,99 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +/// A teaching tip is a semi-persistent and content-rich flyout that provides +/// contextual information. It is often used for informing, reminding, and +/// teaching users about important and new features that may enhance their +/// experience. +/// +/// A teaching tip may be light-dismiss or require explicit action to close. A +/// teaching tip can target a specific UI element with its tail and also be used +/// without a tail or target. +/// +/// See also: +/// +/// * [ContentDialog], modal UI overlays that provide contextual app information. +/// * [Tooltip], a popup that contains additional information about another object. +/// * +class TeachingTip extends StatelessWidget { + /// Creates a teaching tip + const TeachingTip({ + Key? key, + this.alignment = Alignment.center, + required this.title, + required this.subtitle, + this.buttons = const [], + }) : super(key: key); + + /// Where the teaching tip should be displayed + final Alignment alignment; + + /// The title of the teaching tip + /// + /// Usually a [Text] + final Widget title; + + /// The subttile of the teaching tip + /// + /// Usually a [Text] + final Widget subtitle; + + final List buttons; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + final theme = FluentTheme.of(context); + + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 40.0, + maxHeight: 520.0, + minWidth: 320.0, + maxWidth: 336.0, + ), + child: Acrylic( + elevation: 2.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + side: BorderSide( + color: theme.resources.surfaceStrokeColorDefault, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DefaultTextStyle( + style: theme.typography.bodyStrong ?? const TextStyle(), + child: title, + ), + subtitle, + if (buttons.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 6.0), + child: Row( + children: List.generate(buttons.length, (index) { + final isLast = buttons.length - 1 == index; + final button = buttons[index]; + if (isLast) return Expanded(child: button); + return Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 6.0), + child: button, + ), + ); + }), + // children: buttons.map((button) { + // return Expanded(child: button); + // }).toList(), + ), + ), + ], + ), + ), + ), + ); + } +} From 7f7086089df7ab39bef8261d0ecb11ffee2f8e15 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 13 Oct 2022 18:49:00 -0300 Subject: [PATCH 2/5] Show non-targeted teaching tips --- example/lib/screens/surface/teaching_tip.dart | 85 ++++++++--- lib/src/controls/surfaces/dialog.dart | 8 +- lib/src/controls/surfaces/teaching_tip.dart | 140 +++++++++++++++++- 3 files changed, 202 insertions(+), 31 deletions(-) diff --git a/example/lib/screens/surface/teaching_tip.dart b/example/lib/screens/surface/teaching_tip.dart index be260a7f9..0947e83ac 100644 --- a/example/lib/screens/surface/teaching_tip.dart +++ b/example/lib/screens/surface/teaching_tip.dart @@ -1,4 +1,5 @@ import 'package:example/theme.dart'; +import 'package:example/widgets/card_highlight.dart'; import 'package:example/widgets/page.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:provider/provider.dart'; @@ -27,29 +28,69 @@ class _TeachingTipPageState extends State with PageMixin { 'features that may enhance their experience.', ), ), - Center( - child: TeachingTip( - title: const Text('Change themes without hassle'), - subtitle: const Text( - 'It\'s easier to see control samples in both light and dark theme', - ), - buttons: [ - Button( - child: const Text('Toggle theme now'), - onPressed: () { - if (theme.brightness.isDark) { - appTheme.mode = ThemeMode.light; - } else { - appTheme.mode = ThemeMode.dark; - } - }, - ), - Button( - child: const Text('Got it'), - onPressed: () {}, - ), - ], + subtitle( + content: const Text('Show a non-targeted TeachingTip with buttons'), + ), + CardHighlight( + child: Button( + child: const Text('Show TeachingTip'), + onPressed: () { + showTeachingTip( + context: context, + teachingTip: TeachingTip( + alignment: Alignment.bottomCenter, + placementMargin: const EdgeInsets.all(20.0), + title: const Text('Change themes without hassle'), + subtitle: const Text( + 'It\'s easier to see control samples in both light and dark theme', + ), + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + if (theme.brightness.isDark) { + appTheme.mode = ThemeMode.light; + } else { + appTheme.mode = ThemeMode.dark; + } + Navigator.of(context).pop(); + }, + ), + Button( + child: const Text('Got it'), + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, ), + codeSnippet: '''final teachingTip = TeachingTip( + title: Text('Change themes without hassle'), + subtitle: Text( + 'It's easier to see control samples in both light and dark theme', + ), + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + // toggle theme here + + // then close the popup + Navigator.of(context).pop(); + }, + ), + Button( + child: const Text('Got it'), + onPressed: Navigator.of(context).pop, + ), + ], +), + +showTeachingTip( + context: context, + teachingTip: teachingTip, +);''', ), ], ); diff --git a/lib/src/controls/surfaces/dialog.dart b/lib/src/controls/surfaces/dialog.dart index 74f7507ce..6a598f214 100644 --- a/lib/src/controls/surfaces/dialog.dart +++ b/lib/src/controls/surfaces/dialog.dart @@ -161,8 +161,8 @@ class ContentDialog extends StatelessWidget { } } -/// Displays a Material dialog above the current contents of the app, with -/// Material entrance and exit animations, modal barrier color, and modal +/// Displays a Fluent dialog above the current contents of the app, with +/// Fluent entrance and exit animations, modal barrier color, and modal /// barrier behavior (dialog is dismissible with a tap on the barrier). /// /// This function takes a `builder` which typically builds a [Dialog] widget. @@ -262,7 +262,7 @@ Future showDialog({ /// onto the [Navigator] stack to enable state restoration. See /// [showDialog] for a state restoration app example. /// -/// This function takes a `builder` which typically builds a [Dialog] widget. +/// This function takes a `builder` which typically builds a [ContentDialog] widget. /// Content below the dialog is dimmed with a [ModalBarrier]. The widget /// returned by the `builder` does not share a context with the location that /// `showDialog` is originally called from. Use a [StatefulBuilder] or a @@ -294,7 +294,7 @@ Future showDialog({ /// * [showDialog], which is a way to display a DialogRoute. /// * [showGeneralDialog], which allows for customization of the dialog popup. class FluentDialogRoute extends RawDialogRoute { - /// A dialog route with Material entrance and exit animations, + /// A dialog route with Fluent entrance and exit animations, /// modal barrier color FluentDialogRoute({ required WidgetBuilder builder, diff --git a/lib/src/controls/surfaces/teaching_tip.dart b/lib/src/controls/surfaces/teaching_tip.dart index dbea02f23..15b024791 100644 --- a/lib/src/controls/surfaces/teaching_tip.dart +++ b/lib/src/controls/surfaces/teaching_tip.dart @@ -1,5 +1,117 @@ import 'package:fluent_ui/fluent_ui.dart'; +/// Displays a Fluent teaching tip at the desired position, with Fluent entrance +/// and exit animations, modal barrier color, and modal barrier behavior +/// (dialog is dismissible with a tap on the barrier). +/// +/// This function takes a `teachingTip`, which typically builds a [TeachingTip] +/// +/// The `context` argument is used to look up the [Navigator] and [FluentTheme] for +/// the dialog. It is only used when the method is called. Its corresponding +/// widget can be safely removed from the tree before the dialog is closed. +/// +/// The `barrierDismissible` argument is used to indicate whether tapping on the +/// barrier will dismiss the dialog. It is `true` by default and can not be `null`. +/// +/// The `barrierColor` argument is used to specify the color of the modal +/// barrier that darkens everything below the dialog. If `null` the default color +/// `Colors.black54` is used. +/// +/// The `useSafeArea` argument is used to indicate if the dialog should only +/// display in 'safe' areas of the screen not used by the operating system +/// (see [SafeArea] for more details). It is `true` by default, which means +/// the dialog will not overlap operating system areas. If it is set to `false` +/// the dialog will only be constrained by the screen size. It can not be `null`. +/// +/// The `useRootNavigator` argument is used to determine whether to push the +/// dialog to the [Navigator] furthest from or nearest to the given `context`. +/// By default, `useRootNavigator` is `true` and the dialog route created by +/// this method is pushed to the root navigator. It can not be `null`. +/// +/// The `routeSettings` argument is passed to [showGeneralDialog], +/// see [RouteSettings] for details. +/// +/// If the application has multiple [Navigator] objects, it may be necessary to +/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the +/// dialog rather than just `Navigator.pop(context, result)`. +/// +/// Returns a [Future] that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the dialog was closed. +/// +/// ### State Restoration in Popups +/// +/// Using this method will not enable state restoration for the dialog. In order +/// to enable state restoration for a dialog, use [Navigator.restorablePush] +/// or [Navigator.restorablePushNamed] with [FluentDialogRoute]. +/// +/// For more information about state restoration, see [RestorationManager]. +/// +/// See also: +/// +/// * [ContentDialog], for dialogs that have a row of buttons below a body. +/// * [showGeneralDialog], which allows for customization of the dialog popup. +/// * +Future showTeachingTip({ + required BuildContext context, + required Widget teachingTip, + Duration? transitionDuration, + bool useRootNavigator = true, + RouteSettings? routeSettings, + String? barrierLabel, + Color? barrierColor = Colors.transparent, + bool barrierDismissible = true, +}) { + assert(debugCheckHasFluentLocalizations(context)); + + final CapturedThemes themes = InheritedTheme.capture( + from: context, + to: Navigator.of( + context, + rootNavigator: useRootNavigator, + ).context, + ); + + final alignment = + teachingTip is TeachingTip ? teachingTip.alignment : Alignment.center; + + return Navigator.of( + context, + rootNavigator: useRootNavigator, + ).push(FluentDialogRoute( + context: context, + builder: (context) { + final placementMargin = teachingTip is TeachingTip + ? teachingTip.placementMargin + : EdgeInsets.zero; + + return Align( + alignment: alignment, + child: Padding( + padding: placementMargin, + child: teachingTip, + ), + ); + }, + barrierColor: barrierColor, + barrierDismissible: barrierDismissible, + barrierLabel: FluentLocalizations.of(context).modalBarrierDismissLabel, + settings: routeSettings, + transitionBuilder: (context, animation, secondaryAnimation, child) { + return TeachingTip._defaultTransitionBuilder( + context, + animation, + secondaryAnimation, + Alignment.center, + child, + ); + }, + transitionDuration: transitionDuration ?? + FluentTheme.maybeOf(context)?.fastAnimationDuration ?? + const Duration(milliseconds: 300), + themes: themes, + )); +} + /// A teaching tip is a semi-persistent and content-rich flyout that provides /// contextual information. It is often used for informing, reminding, and /// teaching users about important and new features that may enhance their @@ -18,15 +130,13 @@ class TeachingTip extends StatelessWidget { /// Creates a teaching tip const TeachingTip({ Key? key, - this.alignment = Alignment.center, required this.title, required this.subtitle, this.buttons = const [], + this.alignment = Alignment.center, + this.placementMargin = EdgeInsets.zero, }) : super(key: key); - /// Where the teaching tip should be displayed - final Alignment alignment; - /// The title of the teaching tip /// /// Usually a [Text] @@ -39,6 +149,25 @@ class TeachingTip extends StatelessWidget { final List buttons; + /// Where the teaching tip should be displayed + final Alignment alignment; + + final EdgeInsetsGeometry placementMargin; + + static Widget _defaultTransitionBuilder( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Alignment alignment, + Widget child, + ) { + return ScaleTransition( + alignment: alignment, + scale: animation, + child: child, + ); + } + @override Widget build(BuildContext context) { assert(debugCheckHasFluentTheme(context)); @@ -52,7 +181,8 @@ class TeachingTip extends StatelessWidget { maxWidth: 336.0, ), child: Acrylic( - elevation: 2.0, + elevation: 1.0, + shadowColor: Colors.black, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6.0), side: BorderSide( From 8b217e38671b0474593ddf89ffbf09b98f8793a3 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 13 Oct 2022 18:57:43 -0300 Subject: [PATCH 3/5] Add TeachingTipTarget --- example/lib/screens/surface/teaching_tip.dart | 58 +++++++++++++++++++ lib/src/controls/surfaces/teaching_tip.dart | 34 +++++++++++ 2 files changed, 92 insertions(+) diff --git a/example/lib/screens/surface/teaching_tip.dart b/example/lib/screens/surface/teaching_tip.dart index 0947e83ac..85c9a8930 100644 --- a/example/lib/screens/surface/teaching_tip.dart +++ b/example/lib/screens/surface/teaching_tip.dart @@ -12,6 +12,8 @@ class TeachingTipPage extends StatefulWidget { } class _TeachingTipPageState extends State with PageMixin { + final targetKey = GlobalKey(); + @override Widget build(BuildContext context) { final theme = FluentTheme.of(context); @@ -87,6 +89,62 @@ class _TeachingTipPageState extends State with PageMixin { ], ), +showTeachingTip( + context: context, + teachingTip: teachingTip, +);''', + ), + subtitle( + content: const Text('Show a targeted TeachingTip'), + ), + CardHighlight( + child: Row( + children: [ + Button( + child: const Text('Show TeachingTip'), + onPressed: () => targetKey.currentState?.showTeachingTip(), + ), + const Spacer(), + TeachingTipTarget( + key: targetKey, + teachingTip: const TeachingTip( + alignment: Alignment.bottomCenter, + placementMargin: EdgeInsets.all(20.0), + title: Text('Change themes without hassle'), + subtitle: Text( + 'It\'s easier to see control samples in both light and dark theme', + ), + ), + child: Container( + height: 100, + width: 200, + color: theme.accentColor.defaultBrushFor(theme.brightness), + ), + ), + ], + ), + codeSnippet: '''final teachingTip = TeachingTip( + title: Text('Change themes without hassle'), + subtitle: Text( + 'It's easier to see control samples in both light and dark theme', + ), + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + // toggle theme here + + // then close the popup + Navigator.of(context).pop(); + }, + ), + Button( + child: const Text('Got it'), + onPressed: Navigator.of(context).pop, + ), + ], +), + showTeachingTip( context: context, teachingTip: teachingTip, diff --git a/lib/src/controls/surfaces/teaching_tip.dart b/lib/src/controls/surfaces/teaching_tip.dart index 15b024791..a24da64e1 100644 --- a/lib/src/controls/surfaces/teaching_tip.dart +++ b/lib/src/controls/surfaces/teaching_tip.dart @@ -60,6 +60,7 @@ Future showTeachingTip({ String? barrierLabel, Color? barrierColor = Colors.transparent, bool barrierDismissible = true, + Offset? at, }) { assert(debugCheckHasFluentLocalizations(context)); @@ -227,3 +228,36 @@ class TeachingTip extends StatelessWidget { ); } } + +class TeachingTipTarget extends StatefulWidget { + const TeachingTipTarget({ + Key? key, + required this.teachingTip, + required this.child, + }) : super(key: key); + + final Widget teachingTip; + + final Widget child; + + @override + State createState() => TeachingTipTargetState(); +} + +class TeachingTipTargetState extends State { + final _targetKey = GlobalKey(); + + void showTeachingTip() { + final box = _targetKey.currentContext!.findRenderObject() as RenderBox; + final offset = box.localToGlobal(Offset.zero); + print(offset); + } + + @override + Widget build(BuildContext context) { + return KeyedSubtree( + key: _targetKey, + child: widget.child, + ); + } +} From af66a1cd3bbf3ea59d7e7f768f330e98c3e6d2a0 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Thu, 26 Jan 2023 12:51:19 -0300 Subject: [PATCH 4/5] Use the new Flyout implementation to display tooltip --- example/lib/main.dart | 28 +-- example/lib/screens/surface/teaching_tip.dart | 185 ++++++++------- lib/src/controls/flyouts/flyout.dart | 6 +- lib/src/controls/surfaces/teaching_tip.dart | 212 +++++++++--------- 4 files changed, 223 insertions(+), 208 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 66a2efddc..78bdd939e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -288,14 +288,6 @@ class _MyHomePageState extends State with WindowListener { () => surfaces.ProgressIndicatorsPage(), ), ), - PaneItem( - icon: const Icon(FluentIcons.field_filled), - title: const Text('Teaching Tip'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.TeachingTipPage(), - ), - ), PaneItem( icon: const Icon(FluentIcons.tiles), title: const Text('Tiles'), @@ -314,19 +306,27 @@ class _MyHomePageState extends State with WindowListener { ), ), PaneItem( - icon: const Icon(FluentIcons.hint_text), - title: const Text('Tooltip'), + icon: const Icon(FluentIcons.pop_expand), + title: const Text('Flyout'), body: DeferredWidget( surfaces.loadLibrary, - () => popups.TooltipPage(), + () => popups.Flyout2Screen(), ), ), PaneItem( - icon: const Icon(FluentIcons.pop_expand), - title: const Text('Flyout'), + icon: const Icon(FluentIcons.field_filled), + title: const Text('Teaching Tip'), body: DeferredWidget( surfaces.loadLibrary, - () => popups.Flyout2Screen(), + () => surfaces.TeachingTipPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.hint_text), + title: const Text('Tooltip'), + body: DeferredWidget( + surfaces.loadLibrary, + () => popups.TooltipPage(), ), ), PaneItemHeader(header: const Text('Theming')), diff --git a/example/lib/screens/surface/teaching_tip.dart b/example/lib/screens/surface/teaching_tip.dart index 85c9a8930..f51f4a48e 100644 --- a/example/lib/screens/surface/teaching_tip.dart +++ b/example/lib/screens/surface/teaching_tip.dart @@ -12,7 +12,15 @@ class TeachingTipPage extends StatefulWidget { } class _TeachingTipPageState extends State with PageMixin { - final targetKey = GlobalKey(); + final nonTargetedController = FlyoutController(); + final targetedController = FlyoutController(); + + @override + void dispose() { + nonTargetedController.dispose(); + targetedController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -34,38 +42,40 @@ class _TeachingTipPageState extends State with PageMixin { content: const Text('Show a non-targeted TeachingTip with buttons'), ), CardHighlight( - child: Button( - child: const Text('Show TeachingTip'), - onPressed: () { - showTeachingTip( - context: context, - teachingTip: TeachingTip( - alignment: Alignment.bottomCenter, - placementMargin: const EdgeInsets.all(20.0), - title: const Text('Change themes without hassle'), - subtitle: const Text( - 'It\'s easier to see control samples in both light and dark theme', - ), - buttons: [ - Button( - child: const Text('Toggle theme now'), - onPressed: () { - if (theme.brightness.isDark) { - appTheme.mode = ThemeMode.light; - } else { - appTheme.mode = ThemeMode.dark; - } - Navigator.of(context).pop(); - }, - ), - Button( - child: const Text('Got it'), - onPressed: Navigator.of(context).pop, + child: FlyoutTarget( + controller: nonTargetedController, + child: Button( + child: const Text('Show TeachingTip'), + onPressed: () { + showTeachingTip( + flyoutController: nonTargetedController, + nonTargetedAlignment: Alignment.bottomCenter, + builder: (context) => TeachingTip( + title: const Text('Change themes without hassle'), + subtitle: const Text( + 'It\'s easier to see control samples in both light and dark theme', ), - ], - ), - ); - }, + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + if (theme.brightness.isDark) { + appTheme.mode = ThemeMode.light; + } else { + appTheme.mode = ThemeMode.dark; + } + Navigator.of(context).pop(); + }, + ), + Button( + child: const Text('Got it'), + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, + ), ), codeSnippet: '''final teachingTip = TeachingTip( title: Text('Change themes without hassle'), @@ -94,62 +104,65 @@ showTeachingTip( teachingTip: teachingTip, );''', ), - subtitle( - content: const Text('Show a targeted TeachingTip'), - ), - CardHighlight( - child: Row( - children: [ - Button( - child: const Text('Show TeachingTip'), - onPressed: () => targetKey.currentState?.showTeachingTip(), - ), - const Spacer(), - TeachingTipTarget( - key: targetKey, - teachingTip: const TeachingTip( - alignment: Alignment.bottomCenter, - placementMargin: EdgeInsets.all(20.0), - title: Text('Change themes without hassle'), - subtitle: Text( - 'It\'s easier to see control samples in both light and dark theme', - ), - ), - child: Container( - height: 100, - width: 200, - color: theme.accentColor.defaultBrushFor(theme.brightness), - ), - ), - ], - ), - codeSnippet: '''final teachingTip = TeachingTip( - title: Text('Change themes without hassle'), - subtitle: Text( - 'It's easier to see control samples in both light and dark theme', - ), - buttons: [ - Button( - child: const Text('Toggle theme now'), - onPressed: () { - // toggle theme here +// subtitle( +// content: const Text('Show a targeted TeachingTip'), +// ), +// CardHighlight( +// child: Row( +// children: [ +// Button( +// child: const Text('Show TeachingTip'), +// onPressed: () { +// targetKey.currentState?.showTeachingTip(builder: (context) { +// return const TeachingTip( +// alignment: Alignment.bottomCenter, +// placementMargin: EdgeInsets.all(20.0), +// title: Text('Change themes without hassle'), +// subtitle: Text( +// 'It\'s easier to see control samples in both light and dark theme', +// ), +// ); +// }); +// }, +// ), +// const Spacer(), +// TeachingTipTarget( +// key: targetKey, +// child: Container( +// height: 100, +// width: 200, +// color: theme.accentColor.defaultBrushFor(theme.brightness), +// ), +// ), +// ], +// ), +// codeSnippet: '''final teachingTip = TeachingTip( +// title: Text('Change themes without hassle'), +// subtitle: Text( +// 'It's easier to see control samples in both light and dark theme', +// ), +// buttons: [ +// Button( +// child: const Text('Toggle theme now'), +// onPressed: () { +// // toggle theme here - // then close the popup - Navigator.of(context).pop(); - }, - ), - Button( - child: const Text('Got it'), - onPressed: Navigator.of(context).pop, - ), - ], -), +// // then close the popup +// Navigator.of(context).pop(); +// }, +// ), +// Button( +// child: const Text('Got it'), +// onPressed: Navigator.of(context).pop, +// ), +// ], +// ), -showTeachingTip( - context: context, - teachingTip: teachingTip, -);''', - ), +// showTeachingTip( +// context: context, +// teachingTip: teachingTip, +// );''', +// ), ], ); } diff --git a/lib/src/controls/flyouts/flyout.dart b/lib/src/controls/flyouts/flyout.dart index a45ffa46d..8e93039e6 100644 --- a/lib/src/controls/flyouts/flyout.dart +++ b/lib/src/controls/flyouts/flyout.dart @@ -351,10 +351,12 @@ class _FlyoutPositionDelegate extends SingleChildLayoutDelegate { double clampHorizontal(double x) { if (!shouldConstrainToRootBounds) return x; + final max = rootSize.width - flyoutSize.width - margin; + return clampDouble( x, - margin, - rootSize.width - flyoutSize.width - margin, + clampDouble(margin, double.negativeInfinity, max), + max, ); } diff --git a/lib/src/controls/surfaces/teaching_tip.dart b/lib/src/controls/surfaces/teaching_tip.dart index a24da64e1..205d834cb 100644 --- a/lib/src/controls/surfaces/teaching_tip.dart +++ b/lib/src/controls/surfaces/teaching_tip.dart @@ -1,4 +1,13 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; + +const kTeachingTipConstraints = BoxConstraints( + minHeight: 40.0, + maxHeight: 520.0, + minWidth: 320.0, + maxWidth: 336.0, +); /// Displays a Fluent teaching tip at the desired position, with Fluent entrance /// and exit animations, modal barrier color, and modal barrier behavior @@ -52,65 +61,77 @@ import 'package:fluent_ui/fluent_ui.dart'; /// * [showGeneralDialog], which allows for customization of the dialog popup. /// * Future showTeachingTip({ - required BuildContext context, - required Widget teachingTip, + required WidgetBuilder builder, + required FlyoutController flyoutController, + Alignment? nonTargetedAlignment, + FlyoutPlacementMode placementMode = FlyoutPlacementMode.auto, Duration? transitionDuration, - bool useRootNavigator = true, - RouteSettings? routeSettings, - String? barrierLabel, + FlyoutTransitionBuilder transitionBuilder = + TeachingTip.defaultTransitionBuilder, Color? barrierColor = Colors.transparent, bool barrierDismissible = true, - Offset? at, }) { - assert(debugCheckHasFluentLocalizations(context)); + return flyoutController.showFlyout( + placementMode: placementMode, + position: nonTargetedAlignment != null ? Offset.zero : null, + additionalOffset: 0.0, + transitionDuration: transitionDuration, + transitionBuilder: TeachingTip.defaultTransitionBuilder, + builder: (context) { + final teachingTip = builder(context); - final CapturedThemes themes = InheritedTheme.capture( - from: context, - to: Navigator.of( - context, - rootNavigator: useRootNavigator, - ).context, + if (nonTargetedAlignment != null) { + return CustomSingleChildLayout( + delegate: _TeachingTipNonTargetedPositionDelegate( + alignment: nonTargetedAlignment, + ), + child: teachingTip, + ); + } + + return teachingTip; + }, ); +} - final alignment = - teachingTip is TeachingTip ? teachingTip.alignment : Alignment.center; +class _TeachingTipNonTargetedPositionDelegate + extends SingleChildLayoutDelegate { + final Alignment alignment; - return Navigator.of( - context, - rootNavigator: useRootNavigator, - ).push(FluentDialogRoute( - context: context, - builder: (context) { - final placementMargin = teachingTip is TeachingTip - ? teachingTip.placementMargin - : EdgeInsets.zero; + const _TeachingTipNonTargetedPositionDelegate({ + required this.alignment, + }); - return Align( - alignment: alignment, - child: Padding( - padding: placementMargin, - child: teachingTip, - ), - ); - }, - barrierColor: barrierColor, - barrierDismissible: barrierDismissible, - barrierLabel: FluentLocalizations.of(context).modalBarrierDismissLabel, - settings: routeSettings, - transitionBuilder: (context, animation, secondaryAnimation, child) { - return TeachingTip._defaultTransitionBuilder( - context, - animation, - secondaryAnimation, - Alignment.center, - child, - ); - }, - transitionDuration: transitionDuration ?? - FluentTheme.maybeOf(context)?.fastAnimationDuration ?? - const Duration(milliseconds: 300), - themes: themes, - )); + @override + BoxConstraints getConstraintsForChild(BoxConstraints constraints) { + return constraints.loosen(); + } + + @override + Offset getPositionForChild(Size rootSize, Size flyoutSize) { + var pos = alignment.alongSize(rootSize); + + if (alignment.x == 0.0) { + pos = pos - Offset(flyoutSize.width / 2, 0.0); + } + + if (alignment.y == 0.0) { + pos = pos - Offset(0.0, flyoutSize.height / 2); + } + + /// Hardcoded margin because the flyout will always overflow + const margin = 16.0; + + return Offset( + clampDouble(pos.dx, margin, rootSize.width - flyoutSize.width), + clampDouble(pos.dy, 0.0, rootSize.height - flyoutSize.height - margin), + ); + } + + @override + bool shouldRelayout(covariant SingleChildLayoutDelegate oldDelegate) { + return true; + } } /// A teaching tip is a semi-persistent and content-rich flyout that provides @@ -134,8 +155,6 @@ class TeachingTip extends StatelessWidget { required this.title, required this.subtitle, this.buttons = const [], - this.alignment = Alignment.center, - this.placementMargin = EdgeInsets.zero, }) : super(key: key); /// The title of the teaching tip @@ -150,23 +169,41 @@ class TeachingTip extends StatelessWidget { final List buttons; - /// Where the teaching tip should be displayed - final Alignment alignment; - - final EdgeInsetsGeometry placementMargin; - - static Widget _defaultTransitionBuilder( + static Widget defaultTransitionBuilder( BuildContext context, Animation animation, - Animation secondaryAnimation, - Alignment alignment, - Widget child, + FlyoutPlacementMode placementMode, + Widget flyout, ) { - return ScaleTransition( - alignment: alignment, - scale: animation, - child: child, - ); + switch (placementMode) { + case FlyoutPlacementMode.bottomCenter: + case FlyoutPlacementMode.bottomLeft: + case FlyoutPlacementMode.bottomRight: + return ScaleTransition( + // position: Tween( + // begin: const Offset(0, -0.05), + // end: const Offset(0, 0), + // ).animate(animation), + alignment: Alignment.bottomCenter, + scale: CurvedAnimation( + curve: Curves.ease, + parent: animation, + ), + child: flyout, + ); + case FlyoutPlacementMode.topCenter: + case FlyoutPlacementMode.topLeft: + case FlyoutPlacementMode.topRight: + return SlideTransition( + position: Tween( + begin: const Offset(0, 0.05), + end: const Offset(0, 0), + ).animate(animation), + child: flyout, + ); + default: + return flyout; + } } @override @@ -175,12 +212,7 @@ class TeachingTip extends StatelessWidget { final theme = FluentTheme.of(context); return ConstrainedBox( - constraints: const BoxConstraints( - minHeight: 40.0, - maxHeight: 520.0, - minWidth: 320.0, - maxWidth: 336.0, - ), + constraints: kTeachingTipConstraints, child: Acrylic( elevation: 1.0, shadowColor: Colors.black, @@ -190,7 +222,8 @@ class TeachingTip extends StatelessWidget { color: theme.resources.surfaceStrokeColorDefault, ), ), - child: Padding( + child: Container( + color: theme.menuColor.withOpacity(0.6), padding: const EdgeInsets.all(12.0), child: Column( mainAxisSize: MainAxisSize.min, @@ -228,36 +261,3 @@ class TeachingTip extends StatelessWidget { ); } } - -class TeachingTipTarget extends StatefulWidget { - const TeachingTipTarget({ - Key? key, - required this.teachingTip, - required this.child, - }) : super(key: key); - - final Widget teachingTip; - - final Widget child; - - @override - State createState() => TeachingTipTargetState(); -} - -class TeachingTipTargetState extends State { - final _targetKey = GlobalKey(); - - void showTeachingTip() { - final box = _targetKey.currentContext!.findRenderObject() as RenderBox; - final offset = box.localToGlobal(Offset.zero); - print(offset); - } - - @override - Widget build(BuildContext context) { - return KeyedSubtree( - key: _targetKey, - child: widget.child, - ); - } -} From de118209eb5cb381d75b9e06f92e527125a787c9 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 29 Jan 2023 17:52:36 -0300 Subject: [PATCH 5/5] Correct transitions --- example/lib/routes/surfaces.dart | 2 +- .../{surface => popups}/teaching_tip.dart | 108 ++++++++++++------ lib/src/controls/surfaces/teaching_tip.dart | 45 +++++--- 3 files changed, 102 insertions(+), 53 deletions(-) rename example/lib/screens/{surface => popups}/teaching_tip.dart (58%) diff --git a/example/lib/routes/surfaces.dart b/example/lib/routes/surfaces.dart index 0465a7bfd..caad0a2b8 100644 --- a/example/lib/routes/surfaces.dart +++ b/example/lib/routes/surfaces.dart @@ -3,5 +3,5 @@ export '../screens/surface/commandbars.dart'; export '../screens/surface/expander.dart'; export '../screens/surface/info_bars.dart'; export '../screens/surface/progress_indicators.dart'; -export '../screens/surface/teaching_tip.dart'; +export '../screens/popups/teaching_tip.dart'; export '../screens/surface/tiles.dart'; diff --git a/example/lib/screens/surface/teaching_tip.dart b/example/lib/screens/popups/teaching_tip.dart similarity index 58% rename from example/lib/screens/surface/teaching_tip.dart rename to example/lib/screens/popups/teaching_tip.dart index f51f4a48e..a4da9f160 100644 --- a/example/lib/screens/surface/teaching_tip.dart +++ b/example/lib/screens/popups/teaching_tip.dart @@ -13,6 +13,26 @@ class TeachingTipPage extends StatefulWidget { class _TeachingTipPageState extends State with PageMixin { final nonTargetedController = FlyoutController(); + + static const alignments = { + 'Bottom left': Alignment.bottomLeft, + 'Bottom center': Alignment.bottomCenter, + 'Bottom right': Alignment.bottomRight, + 'Center': Alignment.center, + 'Top left': Alignment.topLeft, + 'Top center': Alignment.topCenter, + 'Top right': Alignment.topRight, + }; + static const placements = { + 'Bottom left': FlyoutPlacementMode.bottomLeft, + 'Bottom center': FlyoutPlacementMode.bottomCenter, + 'Bottom right': FlyoutPlacementMode.bottomRight, + 'Center': FlyoutPlacementMode.left, + 'Top left': FlyoutPlacementMode.topLeft, + 'Top center': FlyoutPlacementMode.topCenter, + 'Top right': FlyoutPlacementMode.topRight, + }; + late String alignment = 'Bottom center'; final targetedController = FlyoutController(); @override @@ -42,41 +62,63 @@ class _TeachingTipPageState extends State with PageMixin { content: const Text('Show a non-targeted TeachingTip with buttons'), ), CardHighlight( - child: FlyoutTarget( - controller: nonTargetedController, - child: Button( - child: const Text('Show TeachingTip'), - onPressed: () { - showTeachingTip( - flyoutController: nonTargetedController, - nonTargetedAlignment: Alignment.bottomCenter, - builder: (context) => TeachingTip( - title: const Text('Change themes without hassle'), - subtitle: const Text( - 'It\'s easier to see control samples in both light and dark theme', - ), - buttons: [ - Button( - child: const Text('Toggle theme now'), - onPressed: () { - if (theme.brightness.isDark) { - appTheme.mode = ThemeMode.light; - } else { - appTheme.mode = ThemeMode.dark; - } - Navigator.of(context).pop(); - }, + child: Row(children: [ + FlyoutTarget( + controller: nonTargetedController, + child: Button( + child: const Text('Show TeachingTip'), + onPressed: () { + showTeachingTip( + flyoutController: nonTargetedController, + nonTargetedAlignment: alignments[alignment], + placementMode: placements[alignment]!, + builder: (context) => TeachingTip( + title: const Text('Change themes without hassle'), + subtitle: const Text( + 'It\'s easier to see control samples in both light and dark theme', ), - Button( - child: const Text('Got it'), - onPressed: Navigator.of(context).pop, - ), - ], - ), - ); - }, + buttons: [ + Button( + child: const Text('Toggle theme now'), + onPressed: () { + if (theme.brightness.isDark) { + appTheme.mode = ThemeMode.light; + } else { + appTheme.mode = ThemeMode.dark; + } + Navigator.of(context).pop(); + }, + ), + Button( + child: const Text('Got it'), + onPressed: Navigator.of(context).pop, + ), + ], + ), + ); + }, + ), ), - ), + const SizedBox(width: 18.0), + SizedBox( + width: 150.0, + child: ComboBox( + placeholder: const Text('Alignment'), + items: List.generate(alignments.length, (index) { + final entry = alignments.entries.elementAt(index); + + return ComboBoxItem( + value: entry.key, + child: Text(entry.key.uppercaseFirst()), + ); + }), + value: alignment, + onChanged: (a) { + if (a != null) setState(() => alignment = a); + }, + ), + ), + ]), codeSnippet: '''final teachingTip = TeachingTip( title: Text('Change themes without hassle'), subtitle: Text( diff --git a/lib/src/controls/surfaces/teaching_tip.dart b/lib/src/controls/surfaces/teaching_tip.dart index 205d834cb..87d4fb0d3 100644 --- a/lib/src/controls/surfaces/teaching_tip.dart +++ b/lib/src/controls/surfaces/teaching_tip.dart @@ -175,35 +175,42 @@ class TeachingTip extends StatelessWidget { FlyoutPlacementMode placementMode, Widget flyout, ) { + late Alignment alignment; switch (placementMode) { case FlyoutPlacementMode.bottomCenter: + alignment = const Alignment(0.0, 0.75); + break; case FlyoutPlacementMode.bottomLeft: + alignment = const Alignment(-0.65, 0.75); + break; case FlyoutPlacementMode.bottomRight: - return ScaleTransition( - // position: Tween( - // begin: const Offset(0, -0.05), - // end: const Offset(0, 0), - // ).animate(animation), - alignment: Alignment.bottomCenter, - scale: CurvedAnimation( - curve: Curves.ease, - parent: animation, - ), - child: flyout, - ); + alignment = const Alignment(0.75, 0.75); + break; case FlyoutPlacementMode.topCenter: + alignment = const Alignment(0.0, -0.75); + break; case FlyoutPlacementMode.topLeft: + alignment = const Alignment(-0.65, -0.75); + break; case FlyoutPlacementMode.topRight: - return SlideTransition( - position: Tween( - begin: const Offset(0, 0.05), - end: const Offset(0, 0), - ).animate(animation), - child: flyout, - ); + alignment = const Alignment(0.75, -0.75); + break; + case FlyoutPlacementMode.left: + case FlyoutPlacementMode.right: + alignment = Alignment.center; + break; default: return flyout; } + + return ScaleTransition( + alignment: alignment, + scale: CurvedAnimation( + curve: Curves.ease, + parent: animation, + ), + child: flyout, + ); } @override