diff --git a/CHANGELOG.md b/CHANGELOG.md index c4fa1607..5e64e6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [0.8.0] +* New Widget: `MacoSheet` +* New Widget: `MacosListTile` + ## [0.7.3] * Fixed bug where cursor would not change caret location on mouse click diff --git a/README.md b/README.md index 81b06367..71d59c73 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,16 @@ Flutter widgets and themes implementing the current macOS design language. - [Layout](#layout) - [MacosWindow](#macoswindow) - [MacosScaffold](#macosscaffold) + - [MacosListTile](#MacosListTile) - [Buttons](#buttons) - [MacosCheckbox](#macoscheckbox) - [HelpButton](#helpbutton) - [RadioButton](#radiobutton) - [PushButton](#pushbutton) - [MacosSwitch](#macosswitch) -- [Dialogs](#dialogs) +- [Dialogs and Sheets](#dialogs) - [MacosAlertDialog](#MacosAlertDialog) + - [MacosSheet](#MacosSheet) - [Fields](#fields) - [MacosTextField](#macostextfield) - [Labels](#labels) @@ -111,6 +113,30 @@ class MainFlutterWindow: NSWindow { ``` +## MacosListTile + +A widget that aims to approximate the [ListTile] widget found in +Flutter's material library. + +![MacosListTile](https://imgur.com/pQB99M2.png) + +Usage: +```dart +MacosListTile( + leading: const Icon(CupertinoIcons.lightbulb), + title: Text( + 'A robust library of Flutter components for macOS', + style: MacosTheme.of(context).typography.headline, + ), + subtitle: Text( + 'Create native looking macOS applications using Flutter', + style: MacosTheme.of(context).typography.subheadline.copyWith( + color: MacosColors.systemGrayColor, + ), + ), +), +``` + # Buttons ## MacosCheckbox @@ -217,13 +243,13 @@ MacosSwitch( ), ``` -# Dialogs +# Dialogs and Sheets ## MacosAlertDialog Usage: ```dart -showDialog( +showMacosAlertDialog( context: context, builder: (_) => MacosAlertDialog( appIcon: FlutterLogo( @@ -251,6 +277,18 @@ showDialog( ![](https://imgur.com/YHtgv59.png) ![](https://imgur.com/xuBR5qK.png) +## MacosSheet + +Usage: +```dart +showMacosSheet( + context: context, + builder: (_) => const MacosuiSheet(), +); +``` + +![](https://imgur.com/NV0o5Ws.png) + # Fields ## MacosTextField diff --git a/example/lib/main.dart b/example/lib/main.dart index bad91756..76d7a457 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -125,7 +125,7 @@ class _DemoState extends State { ), const SidebarItem( leading: Icon(CupertinoIcons.rectangle), - label: Text('Dialogs'), + label: Text('Dialogs and Sheets'), ), ], ); diff --git a/example/lib/pages/dialogs_page.dart b/example/lib/pages/dialogs_page.dart index df954951..222b134e 100644 --- a/example/lib/pages/dialogs_page.dart +++ b/example/lib/pages/dialogs_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; class DialogsPage extends StatefulWidget { const DialogsPage({Key? key}) : super(key: key); @@ -174,6 +175,17 @@ class _DialogsPageState extends State { ), ), ), + const SizedBox(height: 16), + PushButton( + buttonSize: ButtonSize.large, + child: const Text('Show sheet'), + onPressed: () { + showMacosSheet( + context: context, + builder: (_) => const MacosuiSheet(), + ); + }, + ), ], ), ), @@ -211,3 +223,57 @@ class _DoNotNotifyRowState extends State { ); } } + +class MacosuiSheet extends StatelessWidget { + const MacosuiSheet({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MacosSheet( + child: Center( + child: Column( + children: [ + const SizedBox(height: 50), + const FlutterLogo( + size: 56, + ), + const SizedBox(height: 24), + Text( + 'Welcome to macos_ui', + style: MacosTheme.of(context).typography.largeTitle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosListTile( + leading: const Icon(CupertinoIcons.lightbulb), + title: Text( + 'A robust library of Flutter components for macOS', + style: MacosTheme.of(context).typography.headline, + ), + subtitle: Text( + 'Create native looking macOS applications using Flutter', + style: + MacosTheme.of(context).typography.subheadline.copyWith( + color: MacosColors.systemGrayColor, + ), + ), + ), + ], + ), + const Spacer(), + PushButton( + buttonSize: ButtonSize.large, + child: const Text('Dismiss'), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 50), + ], + ), + ), + ); + } +} diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index 1bcd04e9..42109cc8 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -27,6 +27,7 @@ export 'src/indicators/scrollbar.dart'; export 'src/labels/label.dart'; export 'src/labels/tooltip.dart'; export 'src/layout/content_area.dart'; +export 'src/layout/macos_list_tile.dart'; export 'src/layout/resizable_pane.dart'; export 'src/layout/scaffold.dart'; export 'src/layout/sidebar.dart'; @@ -34,6 +35,7 @@ export 'src/layout/sidebar_item.dart'; export 'src/layout/title_bar.dart'; export 'src/layout/window.dart'; export 'src/macos_app.dart'; +export 'src/sheets/macos_sheet.dart'; export 'src/theme/macos_colors.dart'; export 'src/theme/macos_dynamic_color.dart'; export 'src/theme/macos_theme.dart'; diff --git a/lib/src/layout/macos_list_tile.dart b/lib/src/layout/macos_list_tile.dart new file mode 100644 index 00000000..c46403e7 --- /dev/null +++ b/lib/src/layout/macos_list_tile.dart @@ -0,0 +1,40 @@ +import 'package:macos_ui/src/library.dart'; + +/// A widget that aims to approximate the [ListTile] widget found in +/// Flutter's material library. +class MacosListTile extends StatelessWidget { + /// Builds a [MacosListTile]. + const MacosListTile({ + Key? key, + this.leading, + required this.title, + this.subtitle, + }) : super(key: key); + + /// A widget to display before the [title]. + final Widget? leading; + + /// The primary content of the list tile. + final Widget title; + + /// Additional content displayed below the [title]. + final Widget? subtitle; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (leading != null) leading!, + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + if (subtitle != null) subtitle!, + ], + ), + ], + ); + } +} diff --git a/lib/src/library.dart b/lib/src/library.dart index 0906a133..3eb2d145 100644 --- a/lib/src/library.dart +++ b/lib/src/library.dart @@ -21,6 +21,8 @@ export 'package:flutter/material.dart' SelectableText, VisualDensity, Colors, + MaterialLocalizations, + Dialog, kElevationToShadow; export 'package:flutter/widgets.dart'; export 'util.dart'; diff --git a/lib/src/sheets/macos_sheet.dart b/lib/src/sheets/macos_sheet.dart new file mode 100644 index 00000000..250f7828 --- /dev/null +++ b/lib/src/sheets/macos_sheet.dart @@ -0,0 +1,214 @@ +import 'package:flutter/physics.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; + +const _kSheetBorderRadius = BorderRadius.all(Radius.circular(12.0)); +const EdgeInsets _defaultInsetPadding = + EdgeInsets.symmetric(horizontal: 140.0, vertical: 48.0); + +/// A modal dialog that’s attached to a particular window and prevents further +/// interaction with the window until the sheet is dismissed. +class MacosSheet extends StatelessWidget { + const MacosSheet({ + Key? key, + required this.child, + this.insetPadding = _defaultInsetPadding, + this.insetAnimationDuration = const Duration(milliseconds: 100), + this.insetAnimationCurve = Curves.decelerate, + }) : super(key: key); + + /// The widget below this widget in the tree. + final Widget child; + + /// The amount of padding added to [MediaQueryData.viewInsets] on the outside + /// of the dialog. This defines the minimum space between the screen's edges + /// and the dialog. + final EdgeInsets? insetPadding; + + /// The duration of the animation to show when the system keyboard intrudes + /// into the space that the dialog is placed in. + final Duration insetAnimationDuration; + + /// The curve to use for the animation shown when the system keyboard intrudes + /// into the space that the dialog is placed in. + final Curve insetAnimationCurve; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMacosTheme(context)); + final brightness = MacosTheme.brightnessOf(context); + + final outerBorderColor = brightness.resolve( + Colors.black.withOpacity(0.23), + Colors.black.withOpacity(0.76), + ); + + final innerBorderColor = brightness.resolve( + Colors.white.withOpacity(0.45), + Colors.white.withOpacity(0.15), + ); + + final EdgeInsets effectivePadding = + MediaQuery.of(context).viewInsets + (insetPadding ?? EdgeInsets.zero); + + return AnimatedPadding( + padding: effectivePadding, + duration: insetAnimationDuration, + curve: insetAnimationCurve, + child: DecoratedBox( + decoration: BoxDecoration( + color: brightness.resolve( + CupertinoColors.systemGrey6.color, + MacosColors.controlBackgroundColor.darkColor, + ), + ), + child: Container( + decoration: BoxDecoration( + border: Border.all( + width: 2, + color: innerBorderColor, + ), + borderRadius: _kSheetBorderRadius, + ), + foregroundDecoration: BoxDecoration( + border: Border.all( + width: 1, + color: outerBorderColor, + ), + borderRadius: _kSheetBorderRadius, + ), + child: child, + ), + ), + ); + } +} + +/// Displays a [MacosSheet] above the current application. +Future showMacosSheet({ + required BuildContext context, + required WidgetBuilder builder, + bool barrierDismissible = false, + Color? barrierColor, + String? barrierLabel, + bool useRootNavigator = true, + RouteSettings? routeSettings, +}) { + barrierColor ??= MacosDynamicColor.resolve( + MacosColors.controlBackgroundColor, + context, + ).withOpacity(0.6); + + return Navigator.of(context, rootNavigator: useRootNavigator).push( + _MacosSheetRoute( + settings: routeSettings, + pageBuilder: (context, animation, secondaryAnimation) { + return builder(context); + }, + barrierDismissible: barrierDismissible, + barrierColor: barrierColor, + barrierLabel: barrierLabel ?? + MaterialLocalizations.of(context).modalBarrierDismissLabel, + ), + ); +} + +class _MacosSheetRoute extends PopupRoute { + _MacosSheetRoute({ + required RoutePageBuilder pageBuilder, + bool barrierDismissible = false, + Color? barrierColor = const Color(0x80000000), + String? barrierLabel, + RouteSettings? settings, + }) : _pageBuilder = pageBuilder, + _barrierDismissible = barrierDismissible, + _barrierLabel = barrierLabel, + _barrierColor = barrierColor, + super(settings: settings); + + final RoutePageBuilder _pageBuilder; + + @override + bool get barrierDismissible => _barrierDismissible; + final bool _barrierDismissible; + + @override + String? get barrierLabel => _barrierLabel; + final String? _barrierLabel; + + @override + Color? get barrierColor => _barrierColor; + final Color? _barrierColor; + + @override + Curve get barrierCurve => Curves.linear; + + @override + Duration get transitionDuration => const Duration(milliseconds: 450); + + @override + Duration get reverseTransitionDuration => const Duration(milliseconds: 120); + + @override + Widget buildPage( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return Semantics( + scopesRoute: true, + explicitChildNodes: true, + child: _pageBuilder(context, animation, secondaryAnimation), + ); + } + + @override + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + if (animation.status == AnimationStatus.reverse) { + return FadeTransition( + opacity: CurvedAnimation( + parent: animation, + curve: Curves.easeOutSine, + ), + child: child, + ); + } + return ScaleTransition( + scale: CurvedAnimation( + parent: animation, + curve: _SubtleBounceCurve(), + ), + child: FadeTransition( + opacity: CurvedAnimation( + parent: animation, + curve: Curves.fastLinearToSlowEaseIn, + ), + child: child, + ), + ); + } +} + +class _SubtleBounceCurve extends Curve { + _SubtleBounceCurve(); + + @override + double transform(double t) { + final simulation = SpringSimulation( + const SpringDescription( + damping: 14, + mass: 1.4, + stiffness: 180, + ), + 0.0, + 1.0, + 0.1, + ); + return simulation.x(t) + t * (1 - simulation.x(1.0)); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 89d7b670..073e5b10 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: macos_ui description: Flutter widgets and themes implementing the current macOS design language. -version: 0.7.3 +version: 0.8.0 homepage: "https://github.com/GroovinChip/macos_ui" environment: