diff --git a/CHANGELOG.md b/CHANGELOG.md index b4911245..b8f3bf4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [1.6.0] +* New widgets: `MacosTabView` and `MacosTabView` +* BREAKING CHANGE: `Label.yAxis` has been renamed to `Label.crossAxisAlignment` +* BREAKING CHANGE: `TooltipTheme` and `TooltipThemeData` have been renamed to `MacosTooltipTheme` and +`MacosTooltipThemeData` + ## [1.5.1] * Correct the placement of the leading widget in disclosure sidebar items [#268](https://github.com/GroovinChip/macos_ui/issues/268) * Improve the sizing of the disclosure item indicator diff --git a/README.md b/README.md index 4c3e7f54..7d18b01c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Guides, codelabs, and other documentation can be found at https://macosui.dev - [Modern Window Look](#modern-window-look) - [ToolBar](#toolbar) - [MacosListTile](#MacosListTile) + - [MacosTabView](#MacosTabView)
@@ -55,6 +56,7 @@ Guides, codelabs, and other documentation can be found at https://macosui.dev - [PopupButton](#popupbutton) - [PushButton](#pushbutton) - [MacosSwitch](#macosswitch) + - [MacosSegmentedControl](#macossegmentedcontrol)
@@ -354,7 +356,49 @@ MacosListTile( ), ``` +## MacosTabView +A multipage interface that displays one page at a time. Must be used in a `StatefulWidget`. + + +You can control the placement of the tabs using the `position` property. + +Usage: +```dart +final _controller = MacosTabController( + initialIndex: 0, + length: 3, +); + +... + +MacosTabView( + controller: _controller, + tabs: const [ + MacosTab( + label: 'Tab 1', + ), + MacosTab( + label: 'Tab 2', + ), + MacosTab( + label: 'Tab 3', + ), + ], + children: const [ + Center( + child: Text('Tab 1'), + ), + Center( + child: Text('Tab 2'), + ), + Center( + child: Text('Tab 3'), + ), + ], +), + +``` # Icons @@ -584,6 +628,16 @@ MacosSwitch( ), ``` +## MacosSegmentedControl + +Displays one or more navigational tabs in a single horizontal group. Used by `MacosTabView` to navigate between the +different tabs of the tab bar. + + + +The typical usage of this widget is by `MacosTabView`, to control the navigation of its children. You do not need to +specify a `MacosSegmentedControl` with your `MacosTabView`, as it is built by that widget. + # Dialogs and Sheets ## MacosAlertDialog diff --git a/example/lib/main.dart b/example/lib/main.dart index e12ac6d1..83964684 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -4,6 +4,7 @@ import 'package:example/pages/dialogs_page.dart'; import 'package:example/pages/fields_page.dart'; import 'package:example/pages/indicators_page.dart'; import 'package:example/pages/selectors_page.dart'; +import 'package:example/pages/tabview_page.dart'; import 'package:example/pages/toolbar_page.dart'; import 'package:flutter/cupertino.dart'; import 'package:macos_ui/macos_ui.dart'; @@ -68,6 +69,7 @@ class _WidgetGalleryState extends State { const DialogsPage(), const ToolbarPage(), const SelectorsPage(), + const TabViewPage(), ]; @override @@ -213,6 +215,10 @@ class _WidgetGalleryState extends State { leading: MacosIcon(CupertinoIcons.calendar), label: Text('Selectors'), ), + SidebarItem( + leading: MacosIcon(CupertinoIcons.table_fill), + label: Text('TabView'), + ), ], ); }, diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart index 600c5e89..22b03b57 100644 --- a/example/lib/pages/buttons_page.dart +++ b/example/lib/pages/buttons_page.dart @@ -16,6 +16,7 @@ class _ButtonsPageState extends State { String popupValue = 'One'; String languagePopupValue = 'English'; bool switchValue = false; + final _tabController = MacosTabController(initialIndex: 0, length: 3); @override Widget build(BuildContext context) { @@ -68,325 +69,348 @@ class _ButtonsPageState extends State { ); }, ), - ContentArea(builder: (context, scrollController) { - return SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - children: [ - const Text('MacosBackButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosBackButton( - onPressed: () => debugPrint('click'), - fillColor: Colors.transparent, - ), - const SizedBox(width: 16.0), - MacosBackButton( - onPressed: () => debugPrint('click'), - ), - ], - ), - const SizedBox(height: 20), - const Text('MacosIconButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosIconButton( - icon: const MacosIcon( - CupertinoIcons.star_fill, + ContentArea( + builder: (context, scrollController) { + return SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + const Text('MacosBackButton'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosBackButton( + onPressed: () => debugPrint('click'), + fillColor: Colors.transparent, ), - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(7), - onPressed: () {}, - ), - const SizedBox(width: 8), - const MacosIconButton( - icon: MacosIcon( - CupertinoIcons.plus_app, + const SizedBox(width: 16.0), + MacosBackButton( + onPressed: () => debugPrint('click'), ), - shape: BoxShape.circle, - //onPressed: () {}, - ), - const SizedBox(width: 8), - MacosIconButton( - icon: const MacosIcon( - CupertinoIcons.minus_square, - ), - backgroundColor: Colors.transparent, - onPressed: () {}, - ), - ], - ), - const SizedBox(height: 20), - const Text('PushButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - PushButton( - buttonSize: ButtonSize.large, - child: const Text('Large'), - onPressed: () { - MacosWindowScope.of(context).toggleSidebar(); - }, - ), - const SizedBox(width: 20), - PushButton( - buttonSize: ButtonSize.small, - child: const Text('Small'), - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) { - return MacosScaffold( - toolBar: const ToolBar( - title: Text("New page"), - ), - children: [ - ContentArea( - builder: (context, scrollController) { - return Center( - child: PushButton( - buttonSize: ButtonSize.large, - child: const Text('Go Back'), - onPressed: () { - Navigator.maybePop(context); - }, - ), - ); - }, - ), - ResizablePane( - minWidth: 180, - startWidth: 200, - windowBreakpoint: 700, - resizableSide: ResizableSide.left, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), - ); - }, - ), - ], - ); - }, - ), - ); - }, - ), - const SizedBox(width: 20), - PushButton( - buttonSize: ButtonSize.large, - isSecondary: true, - child: const Text('Secondary'), - onPressed: () { - MacosWindowScope.of(context).toggleSidebar(); - }, - ), - ], - ), - const SizedBox(height: 20), - const Text('MacosSwitch'), - const SizedBox(height: 8), - MacosSwitch( - value: switchValue, - onChanged: (value) { - setState(() => switchValue = value); - }, - ), - const SizedBox(height: 20), - const Text('MacosPulldownButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosPulldownButton( - title: "PDF", - items: [ - MacosPulldownMenuItem( - title: const Text('Open in Preview'), - onTap: () => debugPrint("Opening in preview..."), - ), - MacosPulldownMenuItem( - title: const Text('Save as PDF...'), - onTap: () => debugPrint("Saving as PDF..."), - ), - MacosPulldownMenuItem( - enabled: false, - title: const Text('Save as Postscript'), - onTap: () => debugPrint("Saving as Postscript..."), - ), - const MacosPulldownMenuDivider(), - MacosPulldownMenuItem( - enabled: false, - title: const Text('Save to iCloud Drive'), - onTap: () => debugPrint("Saving to iCloud..."), - ), - MacosPulldownMenuItem( - enabled: false, - title: const Text('Save to Web Receipts'), - onTap: () => debugPrint("Saving to Web Receipts..."), - ), - MacosPulldownMenuItem( - title: const Text('Send in Mail...'), - onTap: () => debugPrint("Sending via Mail..."), - ), - const MacosPulldownMenuDivider(), - MacosPulldownMenuItem( - title: const Text('Edit Menu...'), - onTap: () => debugPrint("Editing menu..."), - ), - ], - ), - const SizedBox(width: 20), - const MacosPulldownButton( - title: "PDF", - disabledTitle: "Disabled", - items: [], - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosPulldownButton( - icon: CupertinoIcons.ellipsis_circle, - items: [ - MacosPulldownMenuItem( - title: const Text('New Folder'), - onTap: () => debugPrint("Creating new folder..."), - ), - MacosPulldownMenuItem( - title: const Text('Open'), - onTap: () => debugPrint("Opening..."), - ), - MacosPulldownMenuItem( - title: const Text('Open with...'), - onTap: () => debugPrint("Opening with..."), - ), - MacosPulldownMenuItem( - title: const Text('Import from iPhone...'), - onTap: () => debugPrint("Importing..."), + ], + ), + const SizedBox(height: 20), + const Text('MacosIconButton'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosIconButton( + icon: const MacosIcon( + CupertinoIcons.star_fill, ), - const MacosPulldownMenuDivider(), - MacosPulldownMenuItem( - enabled: false, - title: const Text('Remove'), - onTap: () => debugPrint("Deleting..."), - ), - MacosPulldownMenuItem( - title: const Text('Move to Bin'), - onTap: () => debugPrint("Moving to Bin..."), + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(7), + onPressed: () {}, + ), + const SizedBox(width: 8), + const MacosIconButton( + icon: MacosIcon( + CupertinoIcons.plus_app, ), - const MacosPulldownMenuDivider(), - MacosPulldownMenuItem( - title: const Text('Tags...'), - onTap: () => debugPrint("Tags..."), + shape: BoxShape.circle, + //onPressed: () {}, + ), + const SizedBox(width: 8), + MacosIconButton( + icon: const MacosIcon( + CupertinoIcons.minus_square, ), - ], - ), - const SizedBox(width: 20), - const MacosPulldownButton( - icon: CupertinoIcons.square_grid_3x2, - items: [], - ), - ], - ), - const SizedBox(height: 20), - const Text('MacosPopupButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosPopupButton( - value: popupValue, - onChanged: (String? newValue) { - setState(() => popupValue = newValue!); - }, - items: ['One', 'Two', 'Three', 'Four'] - .map>((String value) { - return MacosPopupMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - const SizedBox(width: 20), - MacosPopupButton( - disabledHint: const Text("Disabled"), - onChanged: null, - items: null, - ), - ], - ), - const SizedBox(height: 20), - MacosPopupButton( - value: languagePopupValue, - onChanged: (String? newValue) { - setState(() => languagePopupValue = newValue!); - }, - items: - languages.map>((String value) { - return MacosPopupMenuItem( - value: value, - child: Text(value), - ); - }).toList(), - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('System Theme'), - const SizedBox(width: 8), - MacosRadioButton( - groupValue: context.watch().mode, - value: ThemeMode.system, - onChanged: (value) { - context.read().mode = value!; - }, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Light Theme'), - const SizedBox(width: 24), - MacosRadioButton( - groupValue: context.watch().mode, - value: ThemeMode.light, - onChanged: (value) { - context.read().mode = value!; - }, - ), - ], - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Dark Theme'), - const SizedBox(width: 26), - MacosRadioButton( - groupValue: context.watch().mode, - value: ThemeMode.dark, - onChanged: (value) { - context.read().mode = value!; - }, - ), - ], - ), - ], - ), - ); - }), + backgroundColor: Colors.transparent, + onPressed: () {}, + ), + ], + ), + const SizedBox(height: 20), + const Text('PushButton'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + PushButton( + buttonSize: ButtonSize.large, + child: const Text('Large'), + onPressed: () { + MacosWindowScope.of(context).toggleSidebar(); + }, + ), + const SizedBox(width: 20), + PushButton( + buttonSize: ButtonSize.small, + child: const Text('Small'), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text("New page"), + ), + children: [ + ContentArea( + builder: (context, scrollController) { + return Center( + child: PushButton( + buttonSize: ButtonSize.large, + child: const Text('Go Back'), + onPressed: () { + Navigator.maybePop(context); + }, + ), + ); + }, + ), + ResizablePane( + minWidth: 180, + startWidth: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], + ); + }, + ), + ); + }, + ), + const SizedBox(width: 20), + PushButton( + buttonSize: ButtonSize.large, + isSecondary: true, + child: const Text('Secondary'), + onPressed: () { + MacosWindowScope.of(context).toggleSidebar(); + }, + ), + ], + ), + const SizedBox(height: 20), + const Text('MacosSwitch'), + const SizedBox(height: 8), + MacosSwitch( + value: switchValue, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + const SizedBox(height: 20), + const Text('MacosPulldownButton'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosPulldownButton( + title: "PDF", + items: [ + MacosPulldownMenuItem( + title: const Text('Open in Preview'), + onTap: () => debugPrint("Opening in preview..."), + ), + MacosPulldownMenuItem( + title: const Text('Save as PDF...'), + onTap: () => debugPrint("Saving as PDF..."), + ), + MacosPulldownMenuItem( + enabled: false, + title: const Text('Save as Postscript'), + onTap: () => debugPrint("Saving as Postscript..."), + ), + const MacosPulldownMenuDivider(), + MacosPulldownMenuItem( + enabled: false, + title: const Text('Save to iCloud Drive'), + onTap: () => debugPrint("Saving to iCloud..."), + ), + MacosPulldownMenuItem( + enabled: false, + title: const Text('Save to Web Receipts'), + onTap: () => + debugPrint("Saving to Web Receipts..."), + ), + MacosPulldownMenuItem( + title: const Text('Send in Mail...'), + onTap: () => debugPrint("Sending via Mail..."), + ), + const MacosPulldownMenuDivider(), + MacosPulldownMenuItem( + title: const Text('Edit Menu...'), + onTap: () => debugPrint("Editing menu..."), + ), + ], + ), + const SizedBox(width: 20), + const MacosPulldownButton( + title: "PDF", + disabledTitle: "Disabled", + items: [], + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosPulldownButton( + icon: CupertinoIcons.ellipsis_circle, + items: [ + MacosPulldownMenuItem( + title: const Text('New Folder'), + onTap: () => debugPrint("Creating new folder..."), + ), + MacosPulldownMenuItem( + title: const Text('Open'), + onTap: () => debugPrint("Opening..."), + ), + MacosPulldownMenuItem( + title: const Text('Open with...'), + onTap: () => debugPrint("Opening with..."), + ), + MacosPulldownMenuItem( + title: const Text('Import from iPhone...'), + onTap: () => debugPrint("Importing..."), + ), + const MacosPulldownMenuDivider(), + MacosPulldownMenuItem( + enabled: false, + title: const Text('Remove'), + onTap: () => debugPrint("Deleting..."), + ), + MacosPulldownMenuItem( + title: const Text('Move to Bin'), + onTap: () => debugPrint("Moving to Bin..."), + ), + const MacosPulldownMenuDivider(), + MacosPulldownMenuItem( + title: const Text('Tags...'), + onTap: () => debugPrint("Tags..."), + ), + ], + ), + const SizedBox(width: 20), + const MacosPulldownButton( + icon: CupertinoIcons.square_grid_3x2, + items: [], + ), + ], + ), + const SizedBox(height: 20), + const Text('MacosPopupButton'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MacosPopupButton( + value: popupValue, + onChanged: (String? newValue) { + setState(() => popupValue = newValue!); + }, + items: ['One', 'Two', 'Three', 'Four'] + .map>((String value) { + return MacosPopupMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const SizedBox(width: 20), + MacosPopupButton( + disabledHint: const Text("Disabled"), + onChanged: null, + items: null, + ), + ], + ), + const SizedBox(height: 20), + MacosPopupButton( + value: languagePopupValue, + onChanged: (String? newValue) { + setState(() => languagePopupValue = newValue!); + }, + items: languages + .map>((String value) { + return MacosPopupMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('System Theme'), + const SizedBox(width: 8), + MacosRadioButton( + groupValue: context.watch().mode, + value: ThemeMode.system, + onChanged: (value) { + context.read().mode = value!; + }, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Light Theme'), + const SizedBox(width: 24), + MacosRadioButton( + groupValue: context.watch().mode, + value: ThemeMode.light, + onChanged: (value) { + context.read().mode = value!; + }, + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Dark Theme'), + const SizedBox(width: 26), + MacosRadioButton( + groupValue: context.watch().mode, + value: ThemeMode.dark, + onChanged: (value) { + context.read().mode = value!; + }, + ), + ], + ), + const SizedBox(height: 20), + const Text('MacosSegmentedControl'), + const SizedBox(height: 8), + MacosSegmentedControl( + controller: _tabController, + tabs: [ + MacosTab( + label: 'Tab 1', + active: _tabController.index == 0, + ), + MacosTab( + label: 'Tab 2', + active: _tabController.index == 1, + ), + MacosTab( + label: 'Tab 3', + active: _tabController.index == 2, + ), + ], + ), + ], + ), + ); + }, + ), ResizablePane( minWidth: 180, startWidth: 200, diff --git a/example/lib/pages/tabview_page.dart b/example/lib/pages/tabview_page.dart new file mode 100644 index 00000000..786b11b0 --- /dev/null +++ b/example/lib/pages/tabview_page.dart @@ -0,0 +1,59 @@ +import 'package:flutter/cupertino.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class TabViewPage extends StatefulWidget { + const TabViewPage({super.key}); + + @override + State createState() => _TabViewPageState(); +} + +class _TabViewPageState extends State { + final _controller = MacosTabController( + initialIndex: 0, + length: 3, + ); + + @override + Widget build(BuildContext context) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('TabView'), + ), + children: [ + ContentArea( + builder: (context, scrollController) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: MacosTabView( + controller: _controller, + tabs: const [ + MacosTab( + label: 'Tab 1', + ), + MacosTab( + label: 'Tab 2', + ), + MacosTab( + label: 'Tab 3', + ), + ], + children: const [ + Center( + child: Text('Tab 1'), + ), + Center( + child: Text('Tab 2'), + ), + Center( + child: Text('Tab 3'), + ), + ], + ), + ); + }, + ), + ], + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index a5c70a7e..779cfa22 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -87,7 +87,7 @@ packages: path: ".." relative: true source: path - version: "1.5.1" + version: "1.6.0" matcher: dependency: transitive description: diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index 8a7c8a4e..08a2ba91 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -6,7 +6,11 @@ /// other Flutter-supported platforms, we encourage the use of the following /// libraries for apps that run on other desktop platforms: /// * For Windows, [fluent_ui](https://pub.dev/packages/fluent_ui) -/// * For Linux, [yaru](https://pub.dev/packages/yaru) +/// * For Linux: +/// * [yaru](https://pub.dev/packages/yaru) +/// * [yaru_widgets](https://pub.dev/packages/yaru_widgets) +/// * [yaru_icons](https://pub.dev/packages/yaru_icons) +/// * [yaru_colors](https://pub.dev/packages/yaru_colors) library macos_ui; @@ -18,13 +22,14 @@ export 'src/buttons/popup_button.dart'; export 'src/buttons/pulldown_button.dart'; export 'src/buttons/push_button.dart'; export 'src/buttons/radio_button.dart'; +export 'src/buttons/segmented_control.dart'; export 'src/buttons/switch.dart'; export 'src/buttons/toolbar/toolbar_icon_button.dart'; export 'src/buttons/toolbar/toolbar_overflow_button.dart'; export 'src/buttons/toolbar/toolbar_pulldown_button.dart'; export 'src/dialogs/macos_alert_dialog.dart'; -export 'src/fields/text_field.dart'; export 'src/fields/search_field.dart'; +export 'src/fields/text_field.dart'; export 'src/icon/macos_icon.dart'; export 'src/indicators/capacity_indicators.dart'; export 'src/indicators/progress_indicators.dart'; @@ -40,6 +45,9 @@ export 'src/layout/scaffold.dart'; export 'src/layout/sidebar/sidebar.dart'; export 'src/layout/sidebar/sidebar_item.dart'; export 'src/layout/sidebar/sidebar_items.dart'; +export 'src/layout/tab_view/tab.dart'; +export 'src/layout/tab_view/tab_controller.dart'; +export 'src/layout/tab_view/tab_view.dart'; export 'src/layout/title_bar.dart'; export 'src/layout/toolbar/custom_toolbar_item.dart'; export 'src/layout/toolbar/toolbar.dart'; diff --git a/lib/src/buttons/push_button.dart b/lib/src/buttons/push_button.dart index 639b60ad..27759e1c 100644 --- a/lib/src/buttons/push_button.dart +++ b/lib/src/buttons/push_button.dart @@ -5,8 +5,12 @@ import 'package:flutter/rendering.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +/// The sizes a [PushButton] can be. enum ButtonSize { + /// A large [PushButton]. large, + + /// A small [PushButton]. small, } @@ -22,8 +26,11 @@ const EdgeInsetsGeometry _kLargeButtonPadding = EdgeInsets.symmetric( const BorderRadius _kSmallButtonRadius = BorderRadius.all(Radius.circular(5.0)); const BorderRadius _kLargeButtonRadius = BorderRadius.all(Radius.circular(7.0)); +/// {@template pushButton} /// A macOS-style button. +/// {@endtemplate} class PushButton extends StatefulWidget { + /// {@macro pushButton} const PushButton({ super.key, required this.child, diff --git a/lib/src/buttons/radio_button.dart b/lib/src/buttons/radio_button.dart index aa053543..57097758 100644 --- a/lib/src/buttons/radio_button.dart +++ b/lib/src/buttons/radio_button.dart @@ -63,6 +63,7 @@ class MacosRadioButton extends StatelessWidget { /// Whether the button is disabled or not bool get isDisabled => onChanged == null; + /// Whether the button is selected or not. bool get selected => value == groupValue; @override diff --git a/lib/src/buttons/segmented_control.dart b/lib/src/buttons/segmented_control.dart new file mode 100644 index 00000000..411fd8d1 --- /dev/null +++ b/lib/src/buttons/segmented_control.dart @@ -0,0 +1,119 @@ +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; + +/// {@template macosSegmentedControl} +/// Displays one or more navigational tabs in a single horizontal group. +/// +/// Used by [MacosTabView] to navigate between the different tabs of the tab bar. +/// +/// [MacosSegmentedControl] can be considered somewhat analogous to Flutter's +/// material `TabBar` in that it requires a list of [tabs]. Unlike `TabBar`, +/// however, [MacosSegmentedControl] explicitly requires a [controller]. +/// +/// See also: +/// * [MacosTab], which is a navigational item in a [MacosSegmentedControl]. +/// * [MacosTabView], which is a multi-page navigational view. +/// {@endtemplate} +class MacosSegmentedControl extends StatefulWidget { + /// {@macro macosSegmentedControl} + /// + /// [tabs] and [controller] must not be null. [tabs] must contain at least one + /// tab. + const MacosSegmentedControl({ + super.key, + required this.tabs, + required this.controller, + }) : assert(tabs.length > 0); + + /// The navigational items of this [MacosSegmentedControl]. + final List tabs; + + /// The [MacosTabController] that manages the [tabs] in this + /// [MacosSegmentedControl]. + final MacosTabController controller; + + @override + State createState() => _MacosSegmentedControlState(); +} + +class _MacosSegmentedControlState extends State { + @override + Widget build(BuildContext context) { + final brightness = MacosTheme.brightnessOf(context); + + return DecoratedBox( + decoration: BoxDecoration( + // Background color + color: brightness.resolve( + const Color(0xFFE2E3E6), + const Color(0xFF2B2E33), + ), + boxShadow: [ + BoxShadow( + color: brightness.resolve( + const Color(0xFFDBDCDE), + const Color(0xFF4F5155), + ), + offset: const Offset(0, .5), + spreadRadius: .5, + ), + ], + borderRadius: const BorderRadius.all( + Radius.circular(5.0), + ), + ), + child: Padding( + padding: const EdgeInsets.all(0.5), + child: IntrinsicHeight( + child: IntrinsicWidth( + child: Row( + children: widget.tabs.map((t) { + final row = Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + widget.controller.index = widget.tabs.indexOf(t); + }); + }, + child: t.copyWith( + active: + widget.controller.index == widget.tabs.indexOf(t), + ), + ), + ], + ); + bool showDividerColor = true; + final last = widget.tabs.indexOf(t) == widget.tabs.length - 1; + if ((widget.controller.index - 1 == widget.tabs.indexOf(t)) || + (widget.controller.index + 1 == + widget.tabs.indexOf(t) + 1) || + last) { + showDividerColor = false; + } + + if (!last) { + row.children.add( + VerticalDivider( + color: showDividerColor + ? brightness.resolve( + const Color(0xFFC9C9C9), + const Color(0xFF26222C), + ) + : MacosColors.transparent, + width: 2.0, + indent: 5.0, + endIndent: 5.0, + ), + ); + } + + return row; + }).toList(growable: false), + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/buttons/switch.dart b/lib/src/buttons/switch.dart index 53538de1..6dd4375e 100644 --- a/lib/src/buttons/switch.dart +++ b/lib/src/buttons/switch.dart @@ -4,10 +4,13 @@ import 'package:flutter/gestures.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +/// {@template macosSwitch} /// A switch is a visual toggle between two mutually exclusive /// states — on and off. A switch shows that it's on when the /// accent color is visible and off when the switch appears colorless. +/// {@endtemplate} class MacosSwitch extends StatelessWidget { + /// {@macro macosSwitch} const MacosSwitch({ super.key, required this.value, diff --git a/lib/src/fields/search_field.dart b/lib/src/fields/search_field.dart index 820f523f..ccece251 100644 --- a/lib/src/fields/search_field.dart +++ b/lib/src/fields/search_field.dart @@ -1,7 +1,8 @@ import 'dart:async'; + +import 'package:flutter/services.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; -import 'package:flutter/services.dart'; const BorderRadius _kBorderRadius = BorderRadius.all(Radius.circular(7.0)); const double _kResultHeight = 20.0; diff --git a/lib/src/indicators/capacity_indicators.dart b/lib/src/indicators/capacity_indicators.dart index bcdb33eb..461f3055 100644 --- a/lib/src/indicators/capacity_indicators.dart +++ b/lib/src/indicators/capacity_indicators.dart @@ -174,10 +174,16 @@ class CapacityIndicatorCell extends StatelessWidget { this.backgroundColor = CupertinoColors.tertiarySystemGroupedBackground, }) : assert(value >= 0 && value <= 100); + /// The color of the cell. final Color color; + + /// The background color of the cell. final Color backgroundColor; + + /// The border color of the cell. final Color borderColor; + /// The current value of the cell. final double value; @override diff --git a/lib/src/labels/label.dart b/lib/src/labels/label.dart index 34fcea42..78bdb84e 100644 --- a/lib/src/labels/label.dart +++ b/lib/src/labels/label.dart @@ -13,7 +13,7 @@ class Label extends StatelessWidget { this.icon, required this.text, this.child, - this.yAlignment = CrossAxisAlignment.start, + this.crossAxisAlignment = CrossAxisAlignment.start, }); /// The icon used by the label. If non-null, it's rendered horizontally @@ -29,7 +29,8 @@ class Label extends StatelessWidget { /// The widget at the right of [text]. final Widget? child; - final CrossAxisAlignment yAlignment; + /// The cross-axis alignment of the label. + final CrossAxisAlignment crossAxisAlignment; @override Widget build(BuildContext context) { @@ -43,7 +44,7 @@ class Label extends StatelessWidget { child: this.text, ); return Row( - crossAxisAlignment: yAlignment, + crossAxisAlignment: crossAxisAlignment, mainAxisSize: MainAxisSize.min, children: [ if (icon != null) diff --git a/lib/src/labels/tooltip.dart b/lib/src/labels/tooltip.dart index e055b75b..dbc530b3 100644 --- a/lib/src/labels/tooltip.dart +++ b/lib/src/labels/tooltip.dart @@ -17,7 +17,7 @@ import 'package:macos_ui/src/library.dart'; /// ![Tooltip Preview](https://developer.apple.com/design/human-interface-guidelines/macos/images/help_Tooltip.png) /// /// See also: -/// * [TooltipThemeData], used to define how the tooltip will look like +/// * [MacosTooltipThemeData], used to define how the tooltip will look like class MacosTooltip extends StatefulWidget { /// Creates a tooltip. /// @@ -257,7 +257,7 @@ class _MacosTooltipState extends State Widget build(BuildContext context) { assert(debugCheckHasMacosTheme(context)); assert(Overlay.of(context, debugRequiredFor: widget) != null); - final tooltipTheme = TooltipTheme.of(context); + final tooltipTheme = MacosTooltipTheme.of(context); height = tooltipTheme.height!; padding = tooltipTheme.padding!; diff --git a/lib/src/layout/resizable_pane.dart b/lib/src/layout/resizable_pane.dart index 91e09f4b..4c6c78f2 100644 --- a/lib/src/layout/resizable_pane.dart +++ b/lib/src/layout/resizable_pane.dart @@ -9,16 +9,25 @@ import 'package:macos_ui/src/theme/macos_theme.dart'; const EdgeInsets kResizablePaneSafeArea = EdgeInsets.only(top: 52); /// Indicates the draggable side of the [ResizablePane] for resizing -enum ResizableSide { left, right } +enum ResizableSide { + /// The left side of the [ResizablePane]. + left, + /// The right side of the [ResizablePane]. + right, +} + +/// {@template resizablePane} +/// A widget that can be resized horizontally. +/// +/// The [builder], [minWidth] and [resizableSide] can not be null. +/// The [maxWidth] and the [windowBreakpoint] default to `500.00`. +/// [isResizable] defaults to `true`. +/// +/// The [startWidth] is the initial width. +/// {@endtemplate} class ResizablePane extends StatefulWidget { - /// Creates a widget that can be resized horizontally. - /// - /// The [builder], [minWidth] and [resizableSide] can not be null. - /// The [maxWidth] and the [windowBreakpoint] default to `500.00`. - /// [isResizable] defaults to `true`. - /// - /// The [startWidth] is the initial width. + /// {@macro resizablePane} const ResizablePane({ super.key, required this.builder, diff --git a/lib/src/layout/tab_view/tab.dart b/lib/src/layout/tab_view/tab.dart new file mode 100644 index 00000000..d8edcdd8 --- /dev/null +++ b/lib/src/layout/tab_view/tab.dart @@ -0,0 +1,62 @@ +import 'package:macos_ui/src/library.dart'; +import 'package:macos_ui/src/theme/macos_colors.dart'; +import 'package:macos_ui/src/theme/macos_theme.dart'; + +const _kTabBorderRadius = BorderRadius.all( + Radius.circular(4.0), +); + +/// {@template macosTab} +/// A macOS-style navigational button used to move between the views of a +/// [MacosTabView]. +/// {@endtemplate} +class MacosTab extends StatelessWidget { + /// {@macro macosTab} + const MacosTab({ + super.key, + required this.label, + this.active = false, + }); + + /// The display label for this tab. + final String label; + + /// Whether this [MacosTab] is currently selected. Handled internally by + /// [MacosSegmentedControl]'s build function. + final bool active; + + @override + Widget build(BuildContext context) { + final brightness = MacosTheme.brightnessOf(context); + return PhysicalModel( + color: active ? const Color(0xFF2B2E33) : MacosColors.transparent, + borderRadius: _kTabBorderRadius, + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _kTabBorderRadius, + color: active + ? brightness.resolve( + MacosColors.white, + const Color(0xFF646669), + ) + : MacosColors.transparent, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + child: Text(label), + ), + ), + ); + } + + /// Copies this [MacosTab] into another. + MacosTab copyWith({ + String? label, + bool? active, + }) { + return MacosTab( + label: label ?? this.label, + active: active ?? this.active, + ); + } +} diff --git a/lib/src/layout/tab_view/tab_controller.dart b/lib/src/layout/tab_view/tab_controller.dart new file mode 100644 index 00000000..2eab2521 --- /dev/null +++ b/lib/src/layout/tab_view/tab_controller.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; + +/// {@template macosTabController} +/// Coordinates tab selection for [MacosSegmentedControl] and [MacosTabView]. +/// +/// The [index] property is the index of the selected tab. +/// +/// A stateful widget that builds a [MacosSegmentedControl] and a +/// [MacosTabView] can create a MacosTabController and share it between them. +/// {@endtemplate} +class MacosTabController extends ChangeNotifier { + /// {@macro macosTabController} + MacosTabController({ + int initialIndex = 0, + required this.length, + }) : assert(length >= 0), + assert(initialIndex >= 0 && (length == 0 || initialIndex < length)), + _index = initialIndex, + _previousIndex = initialIndex; + + /// The total number of tabs. + /// + /// Typically greater than one. Must match [MacosTabView.tabs]'s and + /// [MacosTabView.children]'s length. + final int length; + + void _changeIndex(int value) { + assert(value >= 0 && (value < length || length == 0)); + if (value == _index || length < 2) { + return; + } + _previousIndex = index; + _index = value; + notifyListeners(); + } + + /// The index of the currently selected tab. + /// + /// Changing the index also updates [previousIndex] and notifies listeners. + int get index => _index; + int _index; + set index(int value) { + _changeIndex(value); + } + + /// The index of the previously selected tab. + /// + /// Initially the same as index.` + int get previousIndex => _previousIndex; + int _previousIndex; +} diff --git a/lib/src/layout/tab_view/tab_view.dart b/lib/src/layout/tab_view/tab_view.dart new file mode 100644 index 00000000..c12375c2 --- /dev/null +++ b/lib/src/layout/tab_view/tab_view.dart @@ -0,0 +1,191 @@ +import 'package:macos_ui/src/buttons/segmented_control.dart'; +import 'package:macos_ui/src/layout/tab_view/tab.dart'; +import 'package:macos_ui/src/layout/tab_view/tab_controller.dart'; +import 'package:macos_ui/src/library.dart'; +import 'package:macos_ui/src/theme/macos_theme.dart'; + +const _kTabViewRadius = BorderRadius.all( + Radius.circular(5.0), +); + +/// Specifies layout position for [MacosTab] options inside [MacosTabView]. +enum MacosTabPosition { + /// The left side of the [MacosTabView]. + left, + + /// The right side of the [MacosTabView]. + right, + + /// The top side of the [MacosTabView]. + top, + + /// The bottom side of the [MacosTabView]. + bottom, +} + +/// {@template macosTabView} +/// A multipage interface that displays one page at a time. +/// +/// {@image } +/// +/// A tab view contains a row of navigational items, [tabs], that move the +/// user through the provided views ([children]). The user selects the desired +/// page by clicking the appropriate tab. +/// +/// The tab controller's [MacosTabController.length] must equal the length of +/// the [children] list and the length of the [tabs] list. +/// {@endtemplate} +class MacosTabView extends StatefulWidget { + /// {@macro macosTabView} + const MacosTabView({ + super.key, + required this.controller, + required this.tabs, + required this.children, + this.position = MacosTabPosition.top, + }) : assert(controller.length == children.length && + controller.length == tabs.length); + + /// This widget's selection state. + final MacosTabController controller; + + /// A list of navigational items, typically a length of two or more. + final List tabs; + + /// The views to navigate between. + /// + /// There must be one widget per tab. + final List children; + + /// The placement of the [tabs], typically [MacosTabPosition.top]. + final MacosTabPosition position; + + @override + State createState() => _MacosTabViewState(); +} + +class _MacosTabViewState extends State { + late List _childrenWithKey; + int? _currentIndex; + + int get _tabRotation { + switch (widget.position) { + case MacosTabPosition.left: + return 3; + case MacosTabPosition.right: + return 1; + case MacosTabPosition.top: + return 0; + case MacosTabPosition.bottom: + return 0; + } + } + + void _updateTabController() { + widget.controller.addListener(_handleTabControllerTick); + } + + void _handleTabControllerTick() { + if (widget.controller.index != _currentIndex) { + _currentIndex = widget.controller.index; + } + setState(() { + // Rebuild the children after an index change + // has completed. + }); + } + + @override + void initState() { + super.initState(); + _updateChildren(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _updateTabController(); + _currentIndex = widget.controller.index; + } + + @override + void didUpdateWidget(MacosTabView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + _updateTabController(); + _currentIndex = widget.controller.index; + } + if (widget.children != oldWidget.children) { + _updateChildren(); + } + } + + @override + void dispose() { + widget.controller.removeListener(_handleTabControllerTick); + super.dispose(); + } + + void _updateChildren() { + _childrenWithKey = KeyedSubtree.ensureUniqueKeysForList(widget.children); + } + + @override + Widget build(BuildContext context) { + assert(() { + if (widget.controller.length != widget.children.length) { + throw FlutterError( + "Controller's length property (${widget.controller.length}) does not match the " + "number of tabs (${widget.children.length}) present in TabBar's tabs property.", + ); + } + return true; + }()); + + final brightness = MacosTheme.brightnessOf(context); + + final outerBorderColor = brightness.resolve( + const Color(0xFFE1E2E4), + const Color(0xFF3E4045), + ); + + return Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: DecoratedBox( + decoration: BoxDecoration( + color: brightness.resolve( + const Color(0xFFE6E9EA), + const Color(0xFF2B2E33), + ), + border: Border.all( + color: outerBorderColor, + width: 1.0, + ), + borderRadius: _kTabViewRadius, + ), + child: IndexedStack( + index: _currentIndex, + children: _childrenWithKey, + ), + ), + ), + Positioned( + top: widget.position == MacosTabPosition.top ? 0 : null, + bottom: widget.position == MacosTabPosition.bottom ? 0 : null, + left: widget.position == MacosTabPosition.left ? 0 : null, + right: widget.position == MacosTabPosition.right ? 0 : null, + child: RotatedBox( + quarterTurns: _tabRotation, + child: MacosSegmentedControl( + controller: widget.controller, + tabs: widget.tabs, + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/layout/toolbar/overflow_handler.dart b/lib/src/layout/toolbar/overflow_handler.dart index b6b2241b..dd1a1d5a 100644 --- a/lib/src/layout/toolbar/overflow_handler.dart +++ b/lib/src/layout/toolbar/overflow_handler.dart @@ -11,6 +11,7 @@ typedef OverflowHandlerChangedCallback = void Function( List hiddenChildren, ); +/// {@template overflowHandler} /// Lays out children widgets in a single run, and if there is not /// room to display them all, it will hide widgets that don't fit, /// and display the "overflow widget" at the end. Optionally, the @@ -18,7 +19,9 @@ typedef OverflowHandlerChangedCallback = void Function( /// overflow widget will take precedence over any children widgets. /// /// Adapted from [Wrap]. +/// {@endtemplate} class OverflowHandler extends MultiChildRenderObjectWidget { + /// {@macro overflowHandler} OverflowHandler({ super.key, this.alignment = MainAxisAlignment.start, @@ -47,18 +50,25 @@ class OverflowHandler extends MultiChildRenderObjectWidget { /// Defaults to [Clip.none]. final Clip clipBehavior; + /// The breakpoint at which the items should overflow. final double overflowBreakpoint; + /// {@template overflowWidgetAlignment} /// The alignment of the overflow widget between the end of the /// visible regular children and the end of the container. + /// {@endtemplate} final MainAxisAlignment overflowWidgetAlignment; + /// {@template alwaysDisplayOverflowWidget} /// Whether or not to always display the overflowWidget, even if /// all other widgets are able to be displayed. + /// {@endtemplate} final bool alwaysDisplayOverflowWidget; + /// {@template overflowChangedCallback} /// Function that is called when the list of children that are /// hidden because of the dynamic overflow has changed. + /// {@endtemplate} final OverflowHandlerChangedCallback? overflowChangedCallback; @override @@ -124,12 +134,15 @@ class OverflowHandlerParentData extends ContainerBoxParentData { bool _isHidden = false; } +/// {@template renderOverflowHandler} /// Rendering logic for [OverflowHandler] widget. /// Adapted from [RenderWrap]. +/// {@endtemplate} class RenderOverflowHandler extends RenderBox with ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { + /// {@macro renderOverflowHandler} RenderOverflowHandler({ required MainAxisAlignment alignment, required CrossAxisAlignment crossAxisAlignment, @@ -148,6 +161,8 @@ class RenderOverflowHandler extends RenderBox _alwaysDisplayOverflowWidget = alwaysDisplayOverflowWidget; double _overflowBreakpoint; + + /// The breakpoint at which the items should overflow. double get overflowBreakpoint => _overflowBreakpoint; set overflowBreakpoint(double value) { if (_overflowBreakpoint != value) { @@ -157,6 +172,8 @@ class RenderOverflowHandler extends RenderBox } MainAxisAlignment _alignment; + + /// {@macro flutter.widgets.wrap.alignment} MainAxisAlignment get alignment => _alignment; set alignment(MainAxisAlignment value) { if (_alignment != value) { @@ -166,6 +183,8 @@ class RenderOverflowHandler extends RenderBox } CrossAxisAlignment _crossAxisAlignment; + + /// {@macro flutter.widgets.wrap.crossAxisAlignment} CrossAxisAlignment get crossAxisAlignment => _crossAxisAlignment; set crossAxisAlignment(CrossAxisAlignment value) { if (_crossAxisAlignment != value) { @@ -175,6 +194,8 @@ class RenderOverflowHandler extends RenderBox } TextDirection? _textDirection; + + /// {@macro flutter.widgets.wrap.textDirection} TextDirection? get textDirection => _textDirection; set textDirection(TextDirection? value) { if (_textDirection != value) { @@ -184,6 +205,8 @@ class RenderOverflowHandler extends RenderBox } Clip _clipBehavior; + + /// {@macro flutter.material.Material.clipBehavior} Clip get clipBehavior => _clipBehavior; set clipBehavior(Clip value) { if (_clipBehavior != value) { @@ -194,6 +217,8 @@ class RenderOverflowHandler extends RenderBox } MainAxisAlignment _overflowWidgetAlignment; + + /// {@macro overflowWidgetAlignment} MainAxisAlignment get overflowWidgetAlignment => _overflowWidgetAlignment; set overflowWidgetAlignment(MainAxisAlignment value) { if (_overflowWidgetAlignment != value) { @@ -203,6 +228,8 @@ class RenderOverflowHandler extends RenderBox } bool _alwaysDisplayOverflowWidget; + + /// {@macro alwaysDisplayOverflowWidget} bool get alwaysDisplayOverflowWidget => _alwaysDisplayOverflowWidget; set alwaysDisplayOverflowWidget(bool value) { if (_alwaysDisplayOverflowWidget != value) { @@ -211,6 +238,7 @@ class RenderOverflowHandler extends RenderBox } } + /// {@macro overflowChangedCallback} OverflowHandlerChangedCallback? overflowChangedCallback; bool get _debugHasNecessaryDirections { diff --git a/lib/src/layout/toolbar/toolbar.dart b/lib/src/layout/toolbar/toolbar.dart index 8d98f0a6..01a3c12d 100644 --- a/lib/src/layout/toolbar/toolbar.dart +++ b/lib/src/layout/toolbar/toolbar.dart @@ -275,11 +275,14 @@ enum ToolbarItemDisplayMode { overflowed, } +/// {@template toolbarItem} /// An individual action displayed within a [Toolbar]. Sub-class this /// to build a new type of widget that appears inside of a toolbar. /// It knows how to build an appropriate widget for the given /// [ToolbarItemDisplayMode] during build time. +/// {@endtemplate} abstract class ToolbarItem with Diagnosticable { + /// {@macro toolbarItem} const ToolbarItem({required this.key}); final Key? key; diff --git a/lib/src/layout/toolbar/toolbar_overflow_menu.dart b/lib/src/layout/toolbar/toolbar_overflow_menu.dart index df45ff8e..d384df66 100644 --- a/lib/src/layout/toolbar/toolbar_overflow_menu.dart +++ b/lib/src/layout/toolbar/toolbar_overflow_menu.dart @@ -1,5 +1,5 @@ -import 'package:macos_ui/src/library.dart'; import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; const BorderRadius _kBorderRadius = BorderRadius.all(Radius.circular(5.0)); diff --git a/lib/src/layout/toolbar/toolbar_popup.dart b/lib/src/layout/toolbar/toolbar_popup.dart index 4a04256f..9d618128 100644 --- a/lib/src/layout/toolbar/toolbar_popup.dart +++ b/lib/src/layout/toolbar/toolbar_popup.dart @@ -1,6 +1,7 @@ -import 'package:macos_ui/src/library.dart'; import 'dart:math' as math; +import 'package:macos_ui/src/library.dart'; + /// Where the popup will be placed vertically relative to the child enum ToolbarPopupPosition { /// The popup will be above the child, if there is enough space available @@ -26,10 +27,14 @@ enum ToolbarPopupPlacement { end, } +/// {@template toolbarPopup} /// A popup widget for the toolbar. +/// +/// Used for the menu that encapsulates the overflowed toolbar actions and +/// its possible submenus. +/// {@endtemplate} class ToolbarPopup extends StatefulWidget { - /// Creates a popup for the toolbar. Used for the menu that encapsulates - /// the overflowed toolbar actions and its possible submenus. + /// {@macro toolbarPopup} const ToolbarPopup({ super.key, required this.child, @@ -40,11 +45,26 @@ class ToolbarPopup extends StatefulWidget { this.position = ToolbarPopupPosition.above, }); + /// The child widget to show in the popup final Widget child; + + /// The content of the popup final WidgetBuilder content; + + /// The vertical offset of the popup final double verticalOffset; + + /// The horizontal offset of the popup final double horizontalOffset; + + /// The placement of the popup. + /// + /// Defaults to [ToolbarPopupPlacement.center]. final ToolbarPopupPlacement placement; + + /// The position of the popup. + /// + /// Defaults to [ToolbarPopupPosition.above]. final ToolbarPopupPosition position; @override diff --git a/lib/src/selectors/caret_painters.dart b/lib/src/selectors/caret_painters.dart index fc9eddd7..b3259e13 100644 --- a/lib/src/selectors/caret_painters.dart +++ b/lib/src/selectors/caret_painters.dart @@ -2,13 +2,20 @@ import 'package:flutter/widgets.dart'; const _buttonRadius = 5.0; +/// {@template downCaretPainter} +/// A painter that draws a caret pointing down. +/// {@endtemplate} class DownCaretPainter extends CustomPainter { + /// {@macro downCaretPainter} const DownCaretPainter({ required this.color, required this.backgroundColor, }); + /// The color of the caret. final Color color; + + /// The background color of the caret. final Color backgroundColor; @override @@ -44,13 +51,20 @@ class DownCaretPainter extends CustomPainter { bool shouldRebuildSemantics(DownCaretPainter oldDelegate) => false; } +/// {@template upCaretPainter} +/// A painter that draws a caret pointing up. +/// {@endtemplate} class UpCaretPainter extends CustomPainter { + /// {@macro upCaretPainter} const UpCaretPainter({ required this.color, required this.backgroundColor, }); + /// The color of the caret. final Color color; + + /// The background color of the caret. final Color backgroundColor; @override diff --git a/lib/src/selectors/date_picker.dart b/lib/src/selectors/date_picker.dart index 74455245..32c1501d 100644 --- a/lib/src/selectors/date_picker.dart +++ b/lib/src/selectors/date_picker.dart @@ -8,8 +8,13 @@ import 'package:macos_ui/src/selectors/keyboard_shortcut_runner.dart'; /// Defines the possibles [MacosDatePicker] styles. enum DatePickerStyle { + /// A text-only date picker. textual, + + /// A graphical-only date picker. graphical, + + /// A text-and-graphical date picker. combined, } diff --git a/lib/src/sheets/macos_sheet.dart b/lib/src/sheets/macos_sheet.dart index 46112c85..9e19c691 100644 --- a/lib/src/sheets/macos_sheet.dart +++ b/lib/src/sheets/macos_sheet.dart @@ -6,9 +6,12 @@ const _kSheetBorderRadius = BorderRadius.all(Radius.circular(12.0)); const EdgeInsets _defaultInsetPadding = EdgeInsets.symmetric(horizontal: 140.0, vertical: 48.0); +/// {@template macosSheet} /// A modal dialog that’s attached to a particular window and prevents further /// interaction with the window until the sheet is dismissed. +/// {@endtemplate} class MacosSheet extends StatelessWidget { + /// {@macro macosSheet} const MacosSheet({ super.key, required this.child, diff --git a/lib/src/theme/date_picker_theme.dart b/lib/src/theme/date_picker_theme.dart index 59f73f28..e83f2917 100644 --- a/lib/src/theme/date_picker_theme.dart +++ b/lib/src/theme/date_picker_theme.dart @@ -46,7 +46,19 @@ class MacosDatePickerTheme extends InheritedTheme { data != oldWidget.data; } +/// {@template macosDatePickerThemeData} +/// A style that overrides the default appearance of +/// [MacosDatePicker]s when it's used with [MacosDatePickerTheme] or with the +/// overall [MacosTheme]'s [MacosThemeData.datePickerTheme]. +/// +/// See also: +/// +/// * [MacosDatePickerTheme], the theme which is configured with this class. +/// * [MacosThemeData.datePickerTheme], which can be used to override +/// the default style for [MacosDatePicker]s below the overall [MacosTheme]. +/// {@endtemplate} class MacosDatePickerThemeData with Diagnosticable { + /// {@macro macosDatePickerThemeData} MacosDatePickerThemeData({ this.backgroundColor, this.selectedElementColor, @@ -65,20 +77,49 @@ class MacosDatePickerThemeData with Diagnosticable { this.shadowColor, }); + /// The background color of the date picker. final Color? backgroundColor; + + /// The color of the selected element in the textual picker. final Color? selectedElementColor; + + /// The text color of the selected element in the textual picker. final Color? selectedElementTextColor; + + /// The color of the caret in the textual picker. final Color? caretColor; + + /// The color of the controls in the textual picker. final Color? caretControlsBackgroundColor; + + /// The color of the controls separator in the textual picker. final Color? caretControlsSeparatorColor; + + /// The color of the month view controls. final Color? monthViewControlsColor; + + /// The color of the month view header. final Color? monthViewHeaderColor; + + /// The color of the selected date in the month view. final Color? monthViewSelectedDateColor; + + /// The text color of the selected date in the month view. final Color? monthViewSelectedDateTextColor; + + /// The color of the current date in the month view. final Color? monthViewCurrentDateColor; + + /// The color of the weekday header in the month view. final Color? monthViewWeekdayHeaderColor; + + /// The color of the header divider in the month view. final Color? monthViewHeaderDividerColor; + + /// The color of the date in the month view. final Color? monthViewDateColor; + + /// The color of the shadow in the month view. final Color? shadowColor; /// Copies this [MacosDatePickerThemeData] into another. @@ -187,6 +228,7 @@ class MacosDatePickerThemeData with Diagnosticable { ); } + /// Merges this [MacosDatePickerThemeData] with another. MacosDatePickerThemeData merge(MacosDatePickerThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/help_button_theme.dart b/lib/src/theme/help_button_theme.dart index e11e2da8..6c3cfae6 100644 --- a/lib/src/theme/help_button_theme.dart +++ b/lib/src/theme/help_button_theme.dart @@ -113,6 +113,7 @@ class HelpButtonThemeData with Diagnosticable { properties.add(ColorProperty('disabledColor', disabledColor)); } + /// Merges this [HelpButtonThemeData] with another. HelpButtonThemeData merge(HelpButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/icon_button_theme.dart b/lib/src/theme/icon_button_theme.dart index 19f439de..95e13c6c 100644 --- a/lib/src/theme/icon_button_theme.dart +++ b/lib/src/theme/icon_button_theme.dart @@ -129,6 +129,7 @@ class MacosIconButtonThemeData with Diagnosticable { ); } + /// Merges this [MacosIconButtonThemeData] with another. MacosIconButtonThemeData merge(MacosIconButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/macos_colors.dart b/lib/src/theme/macos_colors.dart index 49daca37..59bad194 100644 --- a/lib/src/theme/macos_colors.dart +++ b/lib/src/theme/macos_colors.dart @@ -89,8 +89,13 @@ class MacosColor extends Color { /// A collection of color values lifted from the macOS system color picker. class MacosColors { + /// A fully transparent color. static const Color transparent = MacosColor(0x00000000); + + /// A fully opaque black color. static const black = MacosColor(0xff000000); + + /// A fully opaque white color. static const white = MacosColor(0xffffffff); /// The text of a label containing primary content. diff --git a/lib/src/theme/macos_dynamic_color.dart b/lib/src/theme/macos_dynamic_color.dart index 36399a0b..4a23b159 100644 --- a/lib/src/theme/macos_dynamic_color.dart +++ b/lib/src/theme/macos_dynamic_color.dart @@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'macos_theme.dart'; +/// Extension methods on [CupertinoDynamicColor]. extension MacosDynamicColor on CupertinoDynamicColor { /// Resolves the given [Color] by calling [resolveFrom]. /// diff --git a/lib/src/theme/macos_theme.dart b/lib/src/theme/macos_theme.dart index 2560cc6b..e2c084d8 100644 --- a/lib/src/theme/macos_theme.dart +++ b/lib/src/theme/macos_theme.dart @@ -13,7 +13,7 @@ import 'package:macos_ui/src/library.dart'; /// /// See also: /// -/// * [MacosThemeData], specifies the theme's visual styling +/// * [MacosThemeData], which specifies the theme's visual styling /// * [MacosApp], which will automatically add a [MacosTheme] based on the /// value of [MacosApp.theme]. class MacosTheme extends StatelessWidget { @@ -193,7 +193,7 @@ class MacosThemeData with Diagnosticable { PushButtonThemeData? pushButtonTheme, Color? dividerColor, HelpButtonThemeData? helpButtonTheme, - TooltipThemeData? tooltipTheme, + MacosTooltipThemeData? tooltipTheme, VisualDensity? visualDensity, MacosScrollbarThemeData? scrollbarTheme, MacosIconButtonThemeData? macosIconButtonTheme, @@ -234,7 +234,7 @@ class MacosThemeData with Diagnosticable { ? const Color.fromRGBO(255, 255, 255, 0.1) : const Color.fromRGBO(244, 245, 245, 1.0), ); - tooltipTheme ??= TooltipThemeData.standard( + tooltipTheme ??= MacosTooltipThemeData.standard( brightness: _brightness, textStyle: typography.callout, ); @@ -465,7 +465,7 @@ class MacosThemeData with Diagnosticable { final HelpButtonThemeData helpButtonTheme; /// The default style for [MacosTooltip]s below the overall [MacosTheme] - final TooltipThemeData tooltipTheme; + final MacosTooltipThemeData tooltipTheme; /// The density value for specifying the compactness of various UI components. /// @@ -484,12 +484,16 @@ class MacosThemeData with Diagnosticable { /// The default style for [MacosPopupButton]s below the overall [MacosTheme] final MacosPopupButtonThemeData popupButtonTheme; + /// The default style for [MacosPulldownButton]s below the overall [MacosTheme] final MacosPulldownButtonThemeData pulldownButtonTheme; + /// The default style for [MacosDatePicker]s below the overall [MacosTheme] final MacosDatePickerThemeData datePickerTheme; + /// The default style for [MacosTimePicker]s below the overall [MacosTheme] final MacosTimePickerThemeData timePickerTheme; + /// The default style for [MacosSearchField]s below the overall [MacosTheme] final MacosSearchFieldThemeData searchFieldTheme; /// Linearly interpolate between two themes. @@ -504,7 +508,8 @@ class MacosThemeData with Diagnosticable { HelpButtonThemeData.lerp(a.helpButtonTheme, b.helpButtonTheme, t), pushButtonTheme: PushButtonThemeData.lerp(a.pushButtonTheme, b.pushButtonTheme, t), - tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t), + tooltipTheme: + MacosTooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t), visualDensity: VisualDensity.lerp(a.visualDensity, b.visualDensity, t), scrollbarTheme: MacosScrollbarThemeData.lerp(a.scrollbarTheme, b.scrollbarTheme, t), @@ -551,7 +556,7 @@ class MacosThemeData with Diagnosticable { PushButtonThemeData? pushButtonTheme, Color? dividerColor, HelpButtonThemeData? helpButtonTheme, - TooltipThemeData? tooltipTheme, + MacosTooltipThemeData? tooltipTheme, VisualDensity? visualDensity, MacosScrollbarThemeData? scrollbarTheme, MacosIconButtonThemeData? iconButtonTheme, @@ -583,6 +588,7 @@ class MacosThemeData with Diagnosticable { ); } + /// Merges this [MacosThemeData] with another. MacosThemeData merge(MacosThemeData? other) { if (other == null) return this; return copyWith( @@ -624,7 +630,7 @@ class MacosThemeData with Diagnosticable { helpButtonTheme, )); properties.add( - DiagnosticsProperty('tooltipTheme', tooltipTheme), + DiagnosticsProperty('tooltipTheme', tooltipTheme), ); properties.add( DiagnosticsProperty( @@ -671,10 +677,12 @@ class MacosThemeData with Diagnosticable { } } +/// Brightness extensions extension BrightnessX on Brightness { /// Check if the brightness is dark or not. bool get isDark => this == Brightness.dark; + /// Resolves the given colors based on the current brightness. T resolve(T light, T dark) { if (isDark) return dark; return light; diff --git a/lib/src/theme/overlay_filter.dart b/lib/src/theme/overlay_filter.dart index 7f0e8da4..2dd9b78e 100644 --- a/lib/src/theme/overlay_filter.dart +++ b/lib/src/theme/overlay_filter.dart @@ -1,4 +1,5 @@ import 'dart:ui'; + import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; diff --git a/lib/src/theme/popup_button_theme.dart b/lib/src/theme/popup_button_theme.dart index 81540bd6..41c710d0 100644 --- a/lib/src/theme/popup_button_theme.dart +++ b/lib/src/theme/popup_button_theme.dart @@ -74,6 +74,7 @@ class MacosPopupButtonThemeData with Diagnosticable { /// The default popup menu color for [MacosPopupButton] final Color? popupColor; + /// Copies this [MacosPopupButtonThemeData] into another. MacosPopupButtonThemeData copyWith({ Color? highlightColor, Color? backgroundColor, @@ -121,6 +122,7 @@ class MacosPopupButtonThemeData with Diagnosticable { properties.add(ColorProperty('popupColor', popupColor)); } + /// Merges this [MacosPopupButtonThemeData] with another. MacosPopupButtonThemeData merge(MacosPopupButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/pulldown_button_theme.dart b/lib/src/theme/pulldown_button_theme.dart index 44c5748a..49e82700 100644 --- a/lib/src/theme/pulldown_button_theme.dart +++ b/lib/src/theme/pulldown_button_theme.dart @@ -78,6 +78,7 @@ class MacosPulldownButtonThemeData with Diagnosticable { /// The default color for a [MacosPulldownButton]'s icon. final Color? iconColor; + /// Copies this [MacosPulldownButtonThemeData] into another. MacosPulldownButtonThemeData copyWith({ Color? highlightColor, Color? backgroundColor, @@ -130,6 +131,7 @@ class MacosPulldownButtonThemeData with Diagnosticable { properties.add(ColorProperty('iconColor', iconColor)); } + /// Merges this [MacosPulldownButtonThemeData] with another. MacosPulldownButtonThemeData merge(MacosPulldownButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/push_button_theme.dart b/lib/src/theme/push_button_theme.dart index e7cea5ff..c7d27039 100644 --- a/lib/src/theme/push_button_theme.dart +++ b/lib/src/theme/push_button_theme.dart @@ -71,6 +71,7 @@ class PushButtonThemeData with Diagnosticable { /// The default secondary color (e.g. Cancel/Go back buttons) for [PushButton] final Color? secondaryColor; + /// Copies this [PushButtonThemeData] into another. PushButtonThemeData copyWith({ Color? color, Color? disabledColor, @@ -118,6 +119,7 @@ class PushButtonThemeData with Diagnosticable { properties.add(ColorProperty('secondaryColor', secondaryColor)); } + /// Merges this [PushButtonThemeData] with another. PushButtonThemeData merge(PushButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/scrollbar_theme.dart b/lib/src/theme/scrollbar_theme.dart index fffc3b26..08329b69 100644 --- a/lib/src/theme/scrollbar_theme.dart +++ b/lib/src/theme/scrollbar_theme.dart @@ -366,6 +366,7 @@ class MacosScrollbarThemeData with Diagnosticable { )); } + /// Merges this [MacosScrollbarThemeData] with another. MacosScrollbarThemeData merge(MacosScrollbarThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/search_field_theme.dart b/lib/src/theme/search_field_theme.dart index 443215bb..0f49d9e0 100644 --- a/lib/src/theme/search_field_theme.dart +++ b/lib/src/theme/search_field_theme.dart @@ -125,6 +125,7 @@ class MacosSearchFieldThemeData with Diagnosticable { )); } + /// Merges this [MacosSearchFieldThemeData] with another. MacosSearchFieldThemeData merge(MacosSearchFieldThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/time_picker_theme.dart b/lib/src/theme/time_picker_theme.dart index 8cd99e73..47777a7d 100644 --- a/lib/src/theme/time_picker_theme.dart +++ b/lib/src/theme/time_picker_theme.dart @@ -46,7 +46,19 @@ class MacosTimePickerTheme extends InheritedTheme { data != oldWidget.data; } +/// {@template macosTimePickerThemeData} +/// A style that overrides the default appearance of +/// [MacosTimePicker]s when it's used with [MacosTimePickerTheme] or with the +/// overall [MacosTheme]'s [MacosThemeData.timePickerTheme]. +/// +/// See also: +/// +/// * [MacosTimePickerTheme], the theme which is configured with this class. +/// * [MacosThemeData.timePickerTheme], which can be used to override +/// the default style for [MacosTimePicker]s below the overall [MacosTheme]. +/// {@endtemplate} class MacosTimePickerThemeData with Diagnosticable { + /// {@macro macosTimePickerThemeData} MacosTimePickerThemeData({ this.backgroundColor, this.selectedElementColor, @@ -64,19 +76,47 @@ class MacosTimePickerThemeData with Diagnosticable { this.shadowColor, }); + /// The background color of the time picker. final Color? backgroundColor; + + /// The color of the selected element in the textual time picker. final Color? selectedElementColor; + + /// The text color of the selected element in the textual time picker. final Color? selectedElementTextColor; + + /// The color of the caret in the textual time picker controls. final Color? caretColor; + + /// The background color of the caret controls in the textual time picker. final Color? caretControlsBackgroundColor; + + /// The color of the separator between the caret controls in the textual + /// time picker. final Color? caretControlsSeparatorColor; + + /// The background color of the graphical time picker. final Color? clockViewBackgroundColor; + + /// The color of the hour hand in the graphical time picker. final Color? hourHandColor; + + /// The color of the minute hand in the graphical time picker. final Color? minuteHandColor; + + /// The color of the second hand in the graphical time picker. final Color? secondHandColor; + + /// The color of the hour text in the graphical time picker. final Color? hourTextColor; + + /// The color of the day period text in the graphical time picker. final Color? dayPeriodTextColor; + + /// The color of the clock's outer border in the graphical time picker. final Color? clockViewBorderColor; + + /// The color of the shadow in the graphical time picker. final Color? shadowColor; /// Copies this [MacosTimePickerThemeData] into another. @@ -175,6 +215,7 @@ class MacosTimePickerThemeData with Diagnosticable { ); } + /// Merges this [MacosTimePickerThemeData] with another. MacosTimePickerThemeData merge(MacosTimePickerThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/theme/tooltip_theme.dart b/lib/src/theme/tooltip_theme.dart index 943114b3..b0958ea7 100644 --- a/lib/src/theme/tooltip_theme.dart +++ b/lib/src/theme/tooltip_theme.dart @@ -7,20 +7,20 @@ import 'package:macos_ui/src/library.dart'; /// See also: /// /// * [MacosTooltipThemeData], which is used to configure this theme. -class TooltipTheme extends InheritedTheme { +class MacosTooltipTheme extends InheritedTheme { /// Builds a [MacosTooltipTheme]. /// /// The data argument must not be null. - const TooltipTheme({ + const MacosTooltipTheme({ super.key, required this.data, required super.child, }); /// The configuration for this theme - final TooltipThemeData data; + final MacosTooltipThemeData data; - /// Returns the [data] from the closest [TooltipTheme] ancestor. If there is + /// Returns the [data] from the closest [MacosTooltipTheme] ancestor. If there is /// no ancestor, it returns [MacosThemeData.tooltipTheme]. /// /// Typical usage is as follows: @@ -28,23 +28,36 @@ class TooltipTheme extends InheritedTheme { /// ```dart /// TooltipThemeData theme = TooltipTheme.of(context); /// ``` - static TooltipThemeData of(BuildContext context) { - final TooltipTheme? tooltipTheme = - context.dependOnInheritedWidgetOfExactType(); + static MacosTooltipThemeData of(BuildContext context) { + final MacosTooltipTheme? tooltipTheme = + context.dependOnInheritedWidgetOfExactType(); return tooltipTheme?.data ?? MacosTheme.of(context).tooltipTheme; } @override Widget wrap(BuildContext context, Widget child) { - return TooltipTheme(data: data, child: child); + return MacosTooltipTheme(data: data, child: child); } @override - bool updateShouldNotify(TooltipTheme oldWidget) => data != oldWidget.data; + bool updateShouldNotify(MacosTooltipTheme oldWidget) => + data != oldWidget.data; } -class TooltipThemeData with Diagnosticable { - const TooltipThemeData({ +/// {@template macosTooltipThemeData} +/// A style that overrides the default appearance of +/// [MacosTooltip]s when it's used with [MacosTooltipTheme] or with the +/// overall [MacosTheme]'s [MacosThemeData.tooltipTheme]. +/// +/// See also: +/// +/// * [MacosTooltipTheme], the theme which is configured with this class. +/// * [MacosThemeData.tooltipTheme], which can be used to override +/// the default style for [MacosTooltip]s below the overall [MacosTheme]. +/// {@endtemplate} +class MacosTooltipThemeData with Diagnosticable { + /// {@macro macosTooltipThemeData} + const MacosTooltipThemeData({ this.height, this.verticalOffset, this.padding, @@ -59,11 +72,11 @@ class TooltipThemeData with Diagnosticable { /// Creates a default tooltip theme. /// /// [textStyle] is usually [MacosTypography.callout] - factory TooltipThemeData.standard({ + factory MacosTooltipThemeData.standard({ required Brightness brightness, required TextStyle textStyle, }) { - return TooltipThemeData( + return MacosTooltipThemeData( height: 20.0, verticalOffset: 18.0, preferBelow: true, @@ -170,8 +183,8 @@ class TooltipThemeData with Diagnosticable { /// If null, [MacosTypography.callout] is used final TextStyle? textStyle; - /// Copies this [TooltipThemeData] into another. - TooltipThemeData copyWith({ + /// Copies this [MacosTooltipThemeData] into another. + MacosTooltipThemeData copyWith({ Decoration? decoration, double? height, EdgeInsetsGeometry? margin, @@ -182,7 +195,7 @@ class TooltipThemeData with Diagnosticable { double? verticalOffset, Duration? waitDuration, }) { - return TooltipThemeData( + return MacosTooltipThemeData( decoration: decoration ?? this.decoration, height: height ?? this.height, margin: margin ?? this.margin, @@ -198,12 +211,12 @@ class TooltipThemeData with Diagnosticable { /// Linearly interpolate between two tooltip themes. /// /// All the properties must be non-null. - static TooltipThemeData lerp( - TooltipThemeData a, - TooltipThemeData b, + static MacosTooltipThemeData lerp( + MacosTooltipThemeData a, + MacosTooltipThemeData b, double t, ) { - return TooltipThemeData( + return MacosTooltipThemeData( decoration: Decoration.lerp(a.decoration, b.decoration, t), height: t < 0.5 ? a.height : b.height, margin: EdgeInsetsGeometry.lerp(a.margin, b.margin, t), @@ -216,7 +229,8 @@ class TooltipThemeData with Diagnosticable { ); } - TooltipThemeData merge(TooltipThemeData? other) { + /// Merges this [MacosTooltipThemeData] with another. + MacosTooltipThemeData merge(MacosTooltipThemeData? other) { if (other == null) return this; return copyWith( decoration: other.decoration, @@ -234,7 +248,7 @@ class TooltipThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is TooltipThemeData && + other is MacosTooltipThemeData && runtimeType == other.runtimeType && height == other.height && verticalOffset == other.verticalOffset && diff --git a/pr_prelaunch_tasks.sh b/pr_prelaunch_tasks.sh index 049e655f..0d836662 100644 --- a/pr_prelaunch_tasks.sh +++ b/pr_prelaunch_tasks.sh @@ -4,18 +4,18 @@ if [ $? -eq 1 ]; then git add . git commit -m "chore: run flutter format ." echo "push changes? [y/n]" - read pushResponse - if [ $pushResponse = "y" ]; then + read -r pushResponse + if [ "$pushResponse" = "y" ]; then git push origin fi fi echo "Run dart fix --dry-run? [y/n]" -read dryRunResponse +read -r dryRunResponse if [ "$dryRunResponse" = "y" ]; then dart fix --dry-run fi echo "Run dart fix --apply? [y/n]" -read applyResponse +read -r applyResponse if [ "$applyResponse" = "y" ]; then dart fix --apply if [ -z "$(git status --porcelain)" ]; then @@ -24,14 +24,14 @@ if [ "$applyResponse" = "y" ]; then git add . git commit -m "chore: run dart fix --apply" echo "push changes? [y/n]" - read pushResponse - if [ $pushResponse = "y" ]; then + read -r pushResponse + if [ "$pushResponse" = "y" ]; then git push origin fi fi fi echo "Run tests? [y/n]" -read testResponse +read -r testResponse if [ "$testResponse" = "y" ]; then flutter test else diff --git a/publish_tasks.sh b/publish_tasks.sh index 21913030..a6aa33e1 100644 --- a/publish_tasks.sh +++ b/publish_tasks.sh @@ -1,14 +1,14 @@ # MAINTAINER ONLY SCRIPT. DO NOT RUN THIS SCRIPT UNLESS YOU ARE THE MAINTAINER. pana --no-warning echo "Are you ready to dry-run publish macos_ui? [y/n]" -read dryRunResponse +read -r dryRunResponse if [ "$dryRunResponse" = "y" ]; then flutter pub publish --dry-run else exit 0 fi echo "Are you ready to publish macos_ui to pub.dev? [y/n]" -read publishResponse +read -r publishResponse if [ "$publishResponse" = "y" ]; then flutter pub publish else diff --git a/pubspec.lock b/pubspec.lock index 95cc4dea..810a1738 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -119,7 +119,7 @@ packages: name: dart_code_metrics url: "https://pub.dartlang.org" source: hosted - version: "4.15.0" + version: "4.16.0" dart_style: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ed1c8632..a46af1e1 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.5.1 +version: 1.6.0 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" @@ -15,7 +15,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - dart_code_metrics: ^4.14.0 + dart_code_metrics: ^4.16.0 flutter_lints: ^2.0.1 mocktail: ^0.3.0 diff --git a/test/buttons/segmented_control_test.dart b/test/buttons/segmented_control_test.dart new file mode 100644 index 00000000..8e19831e --- /dev/null +++ b/test/buttons/segmented_control_test.dart @@ -0,0 +1,97 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; + +void main() { + group('MacosSegmentedControl tests', () { + testWidgets( + 'Tapping an unselected item changes the currently selected item', + (tester) async { + final controller = MacosTabController(length: 3, initialIndex: 0); + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return Center( + child: MacosSegmentedControl( + controller: controller, + tabs: const [ + MacosTab( + label: 'Tab 1', + ), + MacosTab( + label: 'Tab 2', + ), + MacosTab( + label: 'Tab 3', + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + final macosSegmentedControl = find.byType(MacosSegmentedControl); + expect(macosSegmentedControl, findsOneWidget); + final secondTab = find.byType(MacosTab).at(1); + expect(secondTab, findsOneWidget); + await tester.tap(secondTab); + await tester.pumpAndSettle(); + expect(controller.index, 1); + }, + ); + + testWidgets( + 'Tapping the currently selected item does nothing', + (tester) async { + final controller = MacosTabController(length: 3, initialIndex: 0); + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return Center( + child: MacosSegmentedControl( + controller: controller, + tabs: const [ + MacosTab( + label: 'Tab 1', + ), + MacosTab( + label: 'Tab 2', + ), + MacosTab( + label: 'Tab 3', + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + final macosSegmentedControl = find.byType(MacosSegmentedControl); + expect(macosSegmentedControl, findsOneWidget); + final firstTab = find.byType(MacosTab).first; + expect(firstTab, findsOneWidget); + await tester.tap(firstTab); + await tester.pumpAndSettle(); + expect(controller.index, 0); + }, + ); + }); +} diff --git a/test/layout/tab_view_test.dart b/test/layout/tab_view_test.dart new file mode 100644 index 00000000..3673f4d1 --- /dev/null +++ b/test/layout/tab_view_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; + +void main() { + group('MacosTabView tests', () { + testWidgets( + 'Tapping a tab changes the child in view', + (tester) async { + final controller = MacosTabController(length: 3, initialIndex: 0); + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return Padding( + padding: const EdgeInsets.all(24.0), + child: MacosTabView( + controller: controller, + tabs: const [ + MacosTab( + label: 'Tab 1', + ), + MacosTab( + label: 'Tab 2', + ), + MacosTab( + label: 'Tab 3', + ), + ], + children: const [ + Center( + child: Text('Tab child 1'), + ), + Center( + child: Text('Tab child 2'), + ), + Center( + child: Text('Tab child 3'), + ), + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + final segmentedControl = find.byType(MacosSegmentedControl); + expect(segmentedControl, findsOneWidget); + final secondTab = find.byType(MacosTab).at(1); + expect(secondTab, findsOneWidget); + await tester.tap(secondTab); + await tester.pumpAndSettle(); + expect(controller.index, 1); + }, + ); + }); +} diff --git a/test/theme/tooltip_theme_test.dart b/test/theme/tooltip_theme_test.dart index 7f506851..cb4bcb79 100644 --- a/test/theme/tooltip_theme_test.dart +++ b/test/theme/tooltip_theme_test.dart @@ -6,17 +6,17 @@ import 'package:macos_ui/src/library.dart'; void main() { test('==, hashCode, copyWith basics', () { expect( - const TooltipThemeData(), - const TooltipThemeData().copyWith(), + const MacosTooltipThemeData(), + const MacosTooltipThemeData().copyWith(), ); expect( - const TooltipThemeData().hashCode, - const TooltipThemeData().copyWith().hashCode, + const MacosTooltipThemeData().hashCode, + const MacosTooltipThemeData().copyWith().hashCode, ); }); test('lerps from light to dark', () { - final actual = TooltipThemeData.lerp( + final actual = MacosTooltipThemeData.lerp( _tooltipThemeData, _tooltipThemeDataDark, 1, @@ -26,7 +26,7 @@ void main() { }); test('lerps from dark to light', () { - final actual = TooltipThemeData.lerp( + final actual = MacosTooltipThemeData.lerp( _tooltipThemeDataDark, _tooltipThemeData, 1, @@ -37,7 +37,7 @@ void main() { testWidgets('debugFillProperties', (tester) async { final builder = DiagnosticPropertiesBuilder(); - const TooltipThemeData().debugFillProperties(builder); + const MacosTooltipThemeData().debugFillProperties(builder); final description = builder.properties .where((node) => !node.isFiltered(DiagnosticLevel.info)) @@ -60,7 +60,7 @@ void main() { }); } -const _tooltipThemeData = TooltipThemeData( +const _tooltipThemeData = MacosTooltipThemeData( decoration: BoxDecoration( color: Colors.red, ), @@ -71,7 +71,7 @@ const _tooltipThemeData = TooltipThemeData( ), ); -const _tooltipThemeDataDark = TooltipThemeData( +const _tooltipThemeDataDark = MacosTooltipThemeData( decoration: BoxDecoration( color: Colors.blue, ),