diff --git a/CHANGELOG.md b/CHANGELOG.md index 54254a25..b19a3aa1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ +## [1.5.0] +* Adds `endSidebar` to `MacosWindow` + ## [1.4.2] -* Fixes RenderFlex overflowed in `MacosListTile` [#264](https://github.com/GroovinChip/macos_ui/issues/264) +* Fixes RenderFlex overflowed in `MacosListTile` [#264](https://github.com/GroovinChip/macos_ui/issues/264) ## [1.4.1+1] * Update `pubspec.yaml` with `repository` and new `homepage` field. diff --git a/example/lib/main.dart b/example/lib/main.dart index 3c243a57..29c401ef 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -221,6 +221,17 @@ class _WidgetGalleryState extends State { subtitle: Text('tim@apple.com'), ), ), + endSidebar: Sidebar( + startWidth: 200, + minWidth: 200, + maxWidth: 300, + shownByDefault: false, + builder: (context, scrollController) { + return const Center( + child: Text('End Sidebar'), + ); + }, + ), child: IndexedStack( index: pageIndex, children: pages, diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart index 8dd31032..600c5e89 100644 --- a/example/lib/pages/buttons_page.dart +++ b/example/lib/pages/buttons_page.dart @@ -23,13 +23,35 @@ class _ButtonsPageState extends State { toolBar: ToolBar( title: const Text('Buttons'), titleWidth: 150.0, + leading: MacosTooltip( + message: 'Toggle Sidebar', + useMousePosition: false, + child: MacosIconButton( + icon: MacosIcon( + CupertinoIcons.sidebar_left, + color: MacosTheme.brightnessOf(context).resolve( + const Color.fromRGBO(0, 0, 0, 0.5), + const Color.fromRGBO(255, 255, 255, 0.5), + ), + size: 20.0, + ), + boxConstraints: const BoxConstraints( + minHeight: 20, + minWidth: 20, + maxWidth: 48, + maxHeight: 38, + ), + onPressed: () => MacosWindowScope.of(context).toggleSidebar(), + ), + ), actions: [ ToolBarIconButton( - label: 'Toggle Sidebar', + label: 'Toggle End Sidebar', + tooltipMessage: 'Toggle End Sidebar', icon: const MacosIcon( - CupertinoIcons.sidebar_left, + CupertinoIcons.sidebar_right, ), - onPressed: () => MacosWindowScope.of(context).toggleSidebar(), + onPressed: () => MacosWindowScope.of(context).toggleEndSidebar(), showLabel: false, ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index 283c358d..f652e649 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -87,7 +87,7 @@ packages: path: ".." relative: true source: path - version: "1.4.1+1" + version: "1.5.0" matcher: dependency: transitive description: diff --git a/lib/src/buttons/icon_button.dart b/lib/src/buttons/icon_button.dart index b87dae5b..e8e6e9f5 100644 --- a/lib/src/buttons/icon_button.dart +++ b/lib/src/buttons/icon_button.dart @@ -146,7 +146,7 @@ class MacosIconButtonState extends State vsync: this, ); _opacityAnimation = _animationController - .drive(CurveTween(curve: Curves.decelerate)) + .drive(CurveTween(curve: const Interval(0.0, 0.25))) .drive(_opacityTween); _setTween(); } diff --git a/lib/src/layout/sidebar/sidebar.dart b/lib/src/layout/sidebar/sidebar.dart index d9050f2d..1c9f7ae2 100644 --- a/lib/src/layout/sidebar/sidebar.dart +++ b/lib/src/layout/sidebar/sidebar.dart @@ -23,6 +23,7 @@ class Sidebar { this.top, this.bottom, this.topOffset = 51.0, + this.shownByDefault = true, }) : dragClosedBuffer = dragClosedBuffer ?? minWidth / 2; /// The builder that creates a child to display in this widget, which will @@ -111,4 +112,11 @@ class Sidebar { /// /// Defaults to `51.0` which levels it up with the default height of the [TitleBar] final double topOffset; + + /// Whether the sidebar should be open by default or not. + /// + /// Most useful for end sidebars. + /// + /// Defaults to `true`. + final bool shownByDefault; } diff --git a/lib/src/layout/window.dart b/lib/src/layout/window.dart index 940f5870..3baafa9a 100644 --- a/lib/src/layout/window.dart +++ b/lib/src/layout/window.dart @@ -27,6 +27,7 @@ class MacosWindow extends StatefulWidget { this.titleBar, this.sidebar, this.backgroundColor, + this.endSidebar, }); /// Specifies the background color for the Window. @@ -37,38 +38,54 @@ class MacosWindow extends StatefulWidget { /// The child of the [MacosWindow] final Widget? child; - /// An app bar to display at the top of the scaffold. + /// An app bar to display at the top of the window. final TitleBar? titleBar; - /// A sidebar to display at the left of the scaffold. + /// A sidebar to display at the left of the window. final Sidebar? sidebar; + /// A sidebar to display at the right of the window. + final Sidebar? endSidebar; + @override State createState() => _MacosWindowState(); } class _MacosWindowState extends State { final _sidebarScrollController = ScrollController(); + final _endSidebarScrollController = ScrollController(); double _sidebarWidth = 0.0; double _sidebarDragStartWidth = 0.0; double _sidebarDragStartPosition = 0.0; + double _endSidebarWidth = 0.0; + double _endSidebarDragStartWidth = 0.0; + double _endSidebarDragStartPosition = 0.0; bool _showSidebar = true; + late bool _showEndSidebar = widget.endSidebar?.shownByDefault ?? false; int _sidebarSlideDuration = 0; SystemMouseCursor _sidebarCursor = SystemMouseCursors.resizeColumn; + SystemMouseCursor _endSidebarCursor = SystemMouseCursors.resizeLeft; @override void initState() { super.initState(); _sidebarWidth = (widget.sidebar?.startWidth ?? widget.sidebar?.minWidth) ?? _sidebarWidth; + _endSidebarWidth = + (widget.endSidebar?.startWidth ?? widget.endSidebar?.minWidth) ?? + _endSidebarWidth; if (widget.sidebar?.builder != null) { _sidebarScrollController.addListener(() => setState(() {})); } + if (widget.endSidebar?.builder != null) { + _endSidebarScrollController.addListener(() => setState(() {})); + } } @override void dispose() { _sidebarScrollController.dispose(); + _endSidebarScrollController.dispose(); super.dispose(); } @@ -87,6 +104,17 @@ class _MacosWindowState extends State { _sidebarWidth = widget.sidebar!.maxWidth!; } } + if (widget.endSidebar == null) { + _endSidebarWidth = 0.0; + } else if (widget.endSidebar!.minWidth != old.endSidebar!.minWidth || + widget.endSidebar!.maxWidth != old.endSidebar!.maxWidth) { + if (widget.endSidebar!.minWidth > _endSidebarWidth) { + _endSidebarWidth = widget.endSidebar!.minWidth; + } + if (widget.endSidebar!.maxWidth! < _endSidebarWidth) { + _endSidebarWidth = widget.endSidebar!.maxWidth!; + } + } }); } @@ -98,9 +126,14 @@ class _MacosWindowState extends State { assert((widget.sidebar!.startWidth! >= widget.sidebar!.minWidth) && (widget.sidebar!.startWidth! <= widget.sidebar!.maxWidth!)); } + if (widget.endSidebar?.startWidth != null) { + assert((widget.endSidebar!.startWidth! >= widget.endSidebar!.minWidth) && + (widget.endSidebar!.startWidth! <= widget.endSidebar!.maxWidth!)); + } final MacosThemeData theme = MacosTheme.of(context); late Color backgroundColor = widget.backgroundColor ?? theme.canvasColor; late Color sidebarBackgroundColor; + late Color endSidebarBackgroundColor; Color dividerColor = theme.dividerColor; final isMac = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; @@ -122,6 +155,19 @@ class _MacosWindowState extends State { : CupertinoColors.systemGrey6.color; } + // Respect the end sidebar color override from parent if one is given + if (widget.endSidebar?.decoration?.color != null) { + endSidebarBackgroundColor = widget.endSidebar!.decoration!.color!; + } else if (isMac && + MediaQuery.of(context).platformBrightness.isDark == + theme.brightness.isDark) { + endSidebarBackgroundColor = theme.canvasColor; + } else { + endSidebarBackgroundColor = theme.brightness.isDark + ? CupertinoColors.tertiarySystemBackground.darkColor + : CupertinoColors.systemGrey6.color; + } + const curve = Curves.linearToEaseOut; final duration = Duration(milliseconds: _sidebarSlideDuration); @@ -131,10 +177,23 @@ class _MacosWindowState extends State { final height = constraints.maxHeight; final isAtBreakpoint = width <= (widget.sidebar?.windowBreakpoint ?? 0); final canShowSidebar = _showSidebar && !isAtBreakpoint; + final canShowEndSidebar = _showEndSidebar && !isAtBreakpoint; final visibleSidebarWidth = canShowSidebar ? _sidebarWidth : 0.0; + final visibleEndSidebarWidth = + canShowEndSidebar ? _endSidebarWidth : 0.0; final layout = Stack( children: [ + // Background color + AnimatedPositioned( + curve: curve, + duration: duration, + height: height, + left: visibleSidebarWidth, + width: width, + child: ColoredBox(color: backgroundColor), + ), + // Sidebar if (widget.sidebar != null) AnimatedPositioned( @@ -184,22 +243,12 @@ class _MacosWindowState extends State { ), ), - // Background color - AnimatedPositioned( - curve: curve, - duration: duration, - left: visibleSidebarWidth, - height: height, - width: width, - child: ColoredBox(color: backgroundColor), - ), - // Content Area AnimatedPositioned( curve: curve, duration: duration, left: visibleSidebarWidth, - width: width - visibleSidebarWidth, + width: width - visibleSidebarWidth - visibleEndSidebarWidth, height: height, child: ClipRect( child: Padding( @@ -285,18 +334,141 @@ class _MacosWindowState extends State { ), ), ), + + // End sidebar + if (widget.endSidebar != null) + AnimatedPositioned( + left: width - visibleEndSidebarWidth, + curve: curve, + duration: duration, + height: height, + width: _endSidebarWidth, + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + color: endSidebarBackgroundColor, + constraints: BoxConstraints( + minWidth: widget.endSidebar!.minWidth, + maxWidth: widget.endSidebar!.maxWidth!, + minHeight: height, + maxHeight: height, + ).normalize(), + child: Column( + children: [ + if ((widget.endSidebar?.topOffset ?? 0) > 0) + SizedBox(height: widget.endSidebar?.topOffset), + if (_endSidebarScrollController.hasClients && + _endSidebarScrollController.offset > 0.0) + Divider(thickness: 1, height: 1, color: dividerColor), + if (widget.endSidebar!.top != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: widget.endSidebar!.top!, + ), + Expanded( + child: MacosScrollbar( + controller: _endSidebarScrollController, + child: Padding( + padding: + widget.endSidebar?.padding ?? EdgeInsets.zero, + child: widget.endSidebar! + .builder(context, _endSidebarScrollController), + ), + ), + ), + if (widget.endSidebar?.bottom != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: widget.endSidebar!.bottom!, + ), + ], + ), + ), + ), + + // End sidebar resizer + if (widget.endSidebar?.isResizable ?? false) + AnimatedPositioned( + curve: curve, + duration: duration, + right: visibleEndSidebarWidth - 4, + width: 7, + height: height, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragStart: (details) { + _endSidebarDragStartWidth = _endSidebarWidth; + _endSidebarDragStartPosition = details.globalPosition.dx; + }, + onHorizontalDragUpdate: (details) { + final endSidebar = widget.endSidebar!; + setState(() { + var newWidth = _endSidebarDragStartWidth - + details.globalPosition.dx + + _endSidebarDragStartPosition; + + if (endSidebar.startWidth != null && + endSidebar.snapToStartBuffer != null && + (newWidth + endSidebar.startWidth!).abs() <= + endSidebar.snapToStartBuffer!) { + newWidth = endSidebar.startWidth!; + } + + if (endSidebar.dragClosed) { + final closeBelow = + endSidebar.minWidth - endSidebar.dragClosedBuffer; + _showEndSidebar = newWidth >= closeBelow; + } + + _endSidebarWidth = math.max( + endSidebar.minWidth, + math.min( + endSidebar.maxWidth!, + newWidth, + ), + ); + + if (_endSidebarWidth == endSidebar.minWidth) { + _endSidebarCursor = SystemMouseCursors.resizeLeft; + } else if (_endSidebarWidth == endSidebar.maxWidth) { + _endSidebarCursor = SystemMouseCursors.resizeRight; + } else { + _endSidebarCursor = SystemMouseCursors.resizeColumn; + } + }); + }, + child: MouseRegion( + cursor: _endSidebarCursor, + child: Align( + alignment: Alignment.center, + child: VerticalDivider( + thickness: 1, + width: 1, + color: dividerColor, + ), + ), + ), + ), + ), ], ); return MacosWindowScope( constraints: constraints, isSidebarShown: canShowSidebar, + isEndSidebarShown: canShowEndSidebar, sidebarToggler: () async { setState(() => _sidebarSlideDuration = 300); setState(() => _showSidebar = !_showSidebar); await Future.delayed(Duration(milliseconds: _sidebarSlideDuration)); setState(() => _sidebarSlideDuration = 0); }, + endSidebarToggler: () async { + setState(() => _sidebarSlideDuration = 300); + setState(() => _showEndSidebar = !_showEndSidebar); + await Future.delayed(Duration(milliseconds: _sidebarSlideDuration)); + setState(() => _sidebarSlideDuration = 0); + }, child: layout, ); }, @@ -326,8 +498,11 @@ class MacosWindowScope extends InheritedWidget { required this.constraints, required super.child, required this.isSidebarShown, + required this.isEndSidebarShown, required VoidCallback sidebarToggler, - }) : _sidebarToggler = sidebarToggler; + required VoidCallback endSidebarToggler, + }) : _sidebarToggler = sidebarToggler, + _endSidebarToggler = endSidebarToggler; /// Provides the constraints from the [MacosWindow] to its descendants. final BoxConstraints constraints; @@ -335,6 +510,9 @@ class MacosWindowScope extends InheritedWidget { /// Provides a callback which will be used to privately toggle the sidebar. final Function _sidebarToggler; + /// Provides a callback which will be used to privately toggle the sidebar. + final Function _endSidebarToggler; + /// Returns the [MacosWindowScope] of the [MacosWindow] that most tightly encloses /// the given [context]. /// @@ -363,6 +541,9 @@ class MacosWindowScope extends InheritedWidget { /// Provides the current visible state of the [Sidebar]. final bool isSidebarShown; + /// Provides the current visible state of the end [Sidebar]. + final bool isEndSidebarShown; + /// Toggles the [Sidebar] of the [MacosWindow]. /// /// This does not change the current width of the [Sidebar]. It only @@ -371,6 +552,14 @@ class MacosWindowScope extends InheritedWidget { _sidebarToggler(); } + /// Toggles the [endSidebar] of the [MacosWindow]. + /// + /// This does not change the current width of the [endSidebar]. It only + /// hides or shows it. + void toggleEndSidebar() { + _endSidebarToggler(); + } + @override bool updateShouldNotify(MacosWindowScope oldWidget) { return constraints != oldWidget.constraints || diff --git a/lib/src/theme/macos_theme.dart b/lib/src/theme/macos_theme.dart index c7be3236..2560cc6b 100644 --- a/lib/src/theme/macos_theme.dart +++ b/lib/src/theme/macos_theme.dart @@ -628,7 +628,9 @@ class MacosThemeData with Diagnosticable { ); properties.add( DiagnosticsProperty( - 'scrollbarTheme', scrollbarTheme), + 'scrollbarTheme', + scrollbarTheme, + ), ); properties.add( DiagnosticsProperty( diff --git a/pr_prelaunch_tasks.sh b/pr_prelaunch_tasks.sh index 9133c92f..049e655f 100644 --- a/pr_prelaunch_tasks.sh +++ b/pr_prelaunch_tasks.sh @@ -32,7 +32,7 @@ if [ "$applyResponse" = "y" ]; then fi echo "Run tests? [y/n]" read testResponse -if [ "$applyResponse" = "y" ]; then +if [ "$testResponse" = "y" ]; then flutter test else exit 0 diff --git a/pubspec.yaml b/pubspec.yaml index 9ebd7c0e..66ec750a 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: 1.4.2 +version: 1.5.0 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" @@ -25,6 +25,3 @@ flutter: macos: package: dev.groovinchip.macos_ui pluginClass: MacOSUiPlugin - -false_secrets: - - /website/node_modules/** \ No newline at end of file diff --git a/test/layout/window_test.dart b/test/layout/window_test.dart index f5d32089..262b5ded 100644 --- a/test/layout/window_test.dart +++ b/test/layout/window_test.dart @@ -38,9 +38,9 @@ void main() { ); } - final sidebarFinder = find.byType(AnimatedPositioned).first; + final sidebarFinder = find.byType(AnimatedPositioned).at(1); final resizerFinder = find.byType(AnimatedPositioned).at(3); - final backgroundFinder = find.byType(AnimatedPositioned).at(1); + final backgroundFinder = find.byType(AnimatedPositioned).at(0); expectSidebarOpen(tester, {required double width}) { expect(