diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml index a22ecac7..03336a6a 100644 --- a/.github/workflows/dart_code_metrics.yaml +++ b/.github/workflows/dart_code_metrics.yaml @@ -8,11 +8,15 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Run Dart Code Metrics - uses: dart-code-checker/dart-code-metrics-action@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Set Up DCM + run: flutter pub get + - uses: CQLabs/setup-dcm@v1.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} - pull_request_comment: true - fatal_warnings: true - fatal_performance: true - fatal_style: true + + - run: dcm analyze --ci-key="${{ secrets.DCM_CI_KEY }}" --email="${{ secrets.DCM_EMAIL }}" lib diff --git a/.github/workflows/flutter_analysis.yml b/.github/workflows/flutter_analysis.yml index 4a4b58ff..fba04aa4 100644 --- a/.github/workflows/flutter_analysis.yml +++ b/.github/workflows/flutter_analysis.yml @@ -34,7 +34,3 @@ jobs: - name: Analyze code run: flutter analyze --fatal-infos . - - - name: Test code - run: flutter test - diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 00000000..8c64e72e --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,19 @@ +name: Flutter Analysis +on: [pull_request, workflow_dispatch] + +jobs: + package-analysis: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + - name: Test code + run: flutter test diff --git a/CHANGELOG.md b/CHANGELOG.md index 50742fa2..0c59bae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,63 @@ +## [2.0.0] +### 🚨 Breaking Changes 🚨 +* `macos_ui` has been migrated to utilize [macos_window_utils](https://pub.dev/packages/macos_window_utils) under the hood, which provides the following benefits: + * Window animation smoothness is drastically improved, particularly when miniaturizing and deminiaturizing the application window. + * Some visual artifacts that occurred while the window was being (de)miniaturized (such as the application's shadow going missing) no longer occur. + * The sidebar remains transparent when the app's brightness setting mismatches the OS setting. + * Wallpaper tinting is now supported. + * To migrate an existing application, please refer to the “Modern window look” section in the README. + +* Support for Flutter 3.10 and Dart 3 +* `PushButton` has been updated to support the `ControlSize` enum. + * The `buttonSize` property has been changed to `controlSize`. + * Buttons can now be any of the following sizes: mini, small, regular, or large. +* `PushButton.isSecondary` is now `PushButton.secondary`. +* `MacosAlertDialog`: `primaryButton` and `secondaryButton` are now declared to be of type `PushButton`. +* `RelevanceIndicator` has been deprecated +* `MacosTypography` white and black are now factory constructors called `darkOpaque()` and `lightOpaque()` to reflect + Apple's naming conventions. + +### ✨ New ✨ +* `MacosSwitch` has been completely rewritten and now matches the native macOS switch in appearance and behavior. +* A `ControlSize` enum has been introduced, which will allow widgets to more closely match their native counterparts. +* `MacosTypography` + * You can now call `MacosTypography.of(context)` as a shorthand for retrieving the typography used in your `MacosTheme`. + * `MacosFontWeight` allows using Apple-specific font weights like `w510`, `w590`, and `w860`. +* Localization + * Added support for `weekdayAbbreviations` and `monthAbbreviations` to `MacosDatePicker`. + * Added support for `dateFormat` to `MacosDatePicker`. + * Added support for `startWeekOnMonday` to `MacosDatePicker`. + +### 🔄 Updated 🔄 +* `MacosColor` has been updated with some previously missing elements. +* `PushButton` + * Now uses the correct `body` text style instead of the incorrect `headline` +* `PushButton`'s secondary and disabled colors more closely match their native counterparts. +* `MacosCheckbox` appearance more closely matches its native counterpart. +* `MacosAlertDialog` + * `primaryButton` and `secondaryButton` are now required to have `controlSize`s of `ControlSize.large`. + * Docs now suggest that `appIcon` should be of size 64x64. +* `Toolbar` now uses the correct `title3` text style instead of the incorrect `headline` +* `MacosTheme` sets the global typography more efficiently +* `HelpButton` now sizes itself according to specification +* `ResizablePane` can now disallow the usage of its internal scrollbar via the `ReziablePane.noScrollBar` constructor. + +### 🛠️ Fixed 🛠️ +* Clicking on the calendar elements in `MacosDatePicker` has better UX +* `ToolBar`s in use where a `SideBar` is not present will now have their title's avoid the traffic lights (native window controls). +* `MacosTypography.darkOpaque()` and `MacosTypography.lightOpaque()` now conform to specification by using `MacosColors.labelColor` +* Ensure builds targeting web do not utilize any `macos_window_utils` code +* Ensure builds targeting web are themed correctly + +## [1.12.5] +* Fixed a bug where the `Sidebar.key` parameter wasn't used, which caused certain layouts to be unachievable. + +## [1.12.4] +* Default the `_selectedDay` state variable to be 1 when selecting the previous/next month from widget to ensure new date is valid for `_formatAsDateTime()` method (https://github.com/flutter/flutter/issues/123669 & https://github.com/macosui/macos_ui/pull/402) + +## [1.12.3] +* Added support for `routerConfig` to `MacosApp.router`. ([#388](https://github.com/macosui/macos_ui/issues/388)) + ## [1.12.2] * Fixed a bug where clicking on a overflowed toolbar item with a navigation callback wouldn't work ([#346](https://github.com/GroovinChip/macos_ui/issues/346)). diff --git a/README.md b/README.md index b0925894..23a54c9a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Flutter widgets and themes implementing the current macOS design language. -Check out our **interactive widget gallery** online at https://groovinchip.github.io/macos_ui/#/ +Check out our **interactive widget gallery** online at https://macosui.github.io/macos_ui/#/ Guides, codelabs, and other documentation can be found at https://macosui.dev @@ -12,9 +12,9 @@ Guides, codelabs, and other documentation can be found at https://macosui.dev [![Flutter Analysis](https://github.com/GroovinChip/macos_ui/actions/workflows/flutter_analysis.yml/badge.svg?branch=stable)](https://github.com/GroovinChip/macos_ui/actions/workflows/flutter_analysis.yml) [![Pana Analysis](https://github.com/GroovinChip/macos_ui/actions/workflows/pana_analysis.yml/badge.svg)](https://github.com/GroovinChip/macos_ui/actions/workflows/pana_analysis.yml) [![codecov](https://github.com/GroovinChip/macos_ui/actions/workflows/codecov.yaml/badge.svg)](https://github.com/GroovinChip/macos_ui/actions/workflows/codecov.yaml) -[![codecov](https://codecov.io/gh/GroovinChip/macos_ui/branch/dev/graph/badge.svg?token=1SZGEVVMCH)](https://codecov.io/gh/GroovinChip/macos_ui) +[![codecov](https://codecov.io/gh/macosui/macos_ui/branch/dev/graph/badge.svg?token=1SZGEVVMCH)](https://codecov.io/gh/macosui/macos_ui) - + ## 🚨 Usage notes ### Flutter channel @@ -23,10 +23,13 @@ Guides, codelabs, and other documentation can be found at https://macosui.dev ### Platform Compatibility pub.dev shows that `macos_ui` only supports macOS. This is because `macos_ui` calls some native code, and therefore -specifies macOS as a plugin platform in the `pubspec.yaml` file. `macos_ui` _will_ work on any platform that -Flutter supports, **but you will get best results on macOS**. +specifies macOS as a plugin platform in the `pubspec.yaml` file. + +`macos_ui` _technically_ will work on any platform that +Flutter supports, **but you will get best results on macOS**. non-macOS platform support is ***not*** guaranteed. The features of `macos_ui` that will _not_ work on platforms other than macOS due to calling native code are: +* Anything related to `macos_window_utils` * The `MacosColors.controlAccentColor()` function * The `MacosColorWell` widget @@ -43,9 +46,8 @@ should avoid allowing your application window to be resized below the height of
Contributing & Resources -- [macos_ui](#macos_ui) - - [Contributing](#contributing) - - [Resources](#resources) +- [Contributing](#contributing) +- [Resources](#resources)
@@ -111,7 +113,6 @@ should avoid allowing your application window to be resized below the height of - [Level Indicators](#level-indicators) - [CapacityIndicator](#capacityindicator) - [RatingIndicator](#ratingindicator) - - [RelevanceIndicator](#relevanceindicator)
@@ -131,9 +132,9 @@ should avoid allowing your application window to be resized below the height of ## Resources +- [macOS Sonoma Figma kit](https://www.figma.com/file/M6K5L3GK0WJh6pnsASyVeE/macOS-Big-Sur-UI-Kit?node-id=1%3A2) +- [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/designing-for-macos) - [macOS Design Resources](https://developer.apple.com/design/resources/) -- [macOS Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/macos) -- [macOS Big Sur Figma kit](https://www.figma.com/file/M6K5L3GK0WJh6pnsASyVeE/macOS-Big-Sur-UI-Kit?node-id=1%3A2) # Layout @@ -158,7 +159,7 @@ A sidebar enables app navigation and provides quick access to top-level collecti Sidebars may be placed at the left or right of your app. To place a sidebar on the left, use the `MacosWindow.sidebar` property. To place a sidebar on the right, use the `MacosWindow.endSidebar` property. - + Example usage: @@ -217,105 +218,50 @@ covering the entire window. To push a route outside a `MacosScaffold` wrapped in See the documentation for customizations and `ToolBar` examples. - - - + - + ## Modern window look -A new look for macOS apps was introduced in Big Sur (macOS 11). To match that look -in your Flutter app, like our screenshots, your `macos/Runner/MainFlutterWindow.swift` -file should look like this: - -```swift -import Cocoa -import FlutterMacOS +A new look for macOS apps was introduced in Big Sur (macOS 11). To match that look in your Flutter app, macos_ui relies on [macos_window_utils](https://pub.dev/packages/macos_window_utils), which requires a minimum macOS deployment target of 10.14.6. Therefore, make sure to open the `macos/Runner.xcworkspace` folder of your project using Xcode and search for `Runner.xcodeproj`. Go to `Info` > `Deployment Target` and set the `macOS Deployment Target` to `10.14.6` or above. Then, open your project's `Podfile` (if it doesn't show up in Xcode, you can find it in your project's `macos` directory via VS Code) and set the minimum deployment version in the first line to `10.14.6` or above: -class BlurryContainerViewController: NSViewController { - let flutterViewController = FlutterViewController() - - init() { - super.init(nibName: nil, bundle: nil) - } +```podspec +platform :osx, '10.14.6' +``` - required init?(coder: NSCoder) { - fatalError() - } +You may also need to open up your app's `Runner.xcodeproj` in XCode and set the minimum deployment version there. - override func loadView() { - let blurView = NSVisualEffectView() - blurView.autoresizingMask = [.width, .height] - blurView.blendingMode = .behindWindow - blurView.state = .active - if #available(macOS 10.14, *) { - blurView.material = .sidebar - } - self.view = blurView - } +Now, configure your window inside your `main()` as follows: - override func viewDidLoad() { - super.viewDidLoad() +```dart +/// This method initializes macos_window_utils and styles the window. +Future _configureMacosWindowUtils() async { + const config = MacosWindowUtilsConfig( + toolbarStyle: NSWindowToolbarStyle.unified, + ); + await config.apply(); +} - self.addChild(flutterViewController) +void main() async { + await _configureMacosWindowUtils(); - flutterViewController.view.frame = self.view.bounds - flutterViewController.backgroundColor = .clear // **Required post-Flutter 3.7.0** - flutterViewController.view.autoresizingMask = [.width, .height] - self.view.addSubview(flutterViewController.view) - } + runApp(const YourAppHere()); } +``` -class MainFlutterWindow: NSWindow, NSWindowDelegate { - override func awakeFromNib() { - delegate = self - let blurryContainerViewController = BlurryContainerViewController() - let windowFrame = self.frame - self.contentViewController = blurryContainerViewController - self.setFrame(windowFrame, display: true) - - if #available(macOS 10.13, *) { - let customToolbar = NSToolbar() - customToolbar.showsBaselineSeparator = false - self.toolbar = customToolbar - } - self.titleVisibility = .hidden - self.titlebarAppearsTransparent = true - if #available(macOS 11.0, *) { - // Use .expanded if the app will have a title bar, else use .unified - self.toolbarStyle = .unified - } - - self.isMovableByWindowBackground = true - self.styleMask.insert(NSWindow.StyleMask.fullSizeContentView) - - self.isOpaque = false - self.backgroundColor = .clear - - RegisterGeneratedPlugins(registry: blurryContainerViewController.flutterViewController) - - super.awakeFromNib() - } - - func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions { - return [.autoHideToolbar, .autoHideMenuBar, .fullScreen] - } +Please note that if you are using a title bar (`TitleBar`) in your `MacosWindow`, you should set the `toolbarStyle` of your window to `NSWindowToolbarStyle.expanded`, in order to properly align the close, minimize, zoom window buttons: - func windowWillEnterFullScreen(_ notification: Notification) { - self.toolbar?.isVisible = false - } - - func windowDidExitFullScreen(_ notification: Notification) { - self.toolbar?.isVisible = true - } +```dart +Future _configureMacosWindowUtils() async { + const config = MacosWindowUtilsConfig( + toolbarStyle: NSWindowToolbarStyle.expanded, + ); + await config.apply(); } - ``` -See [this issue comment](https://github.com/flutter/flutter/issues/59969#issuecomment-916682559) for more details on the new look and explanations for how it works. - -Please note that if you are using a title bar (`TitleBar`) in your `MacosWindow`, you should set the `toolbarStyle` of NSWindow to `.expanded`, in order to properly align the close, minimize, zoom window buttons. In any other case, you should keep it as `.unified`. This must be set beforehand, i.e. it cannot be switched in runtime. +In any other case, you should keep it as `NSWindowToolbarStyle.unified`. ## ToolBar @@ -389,7 +335,7 @@ Other toolbar examples: - Toolbar with a pulldown button open: - + - Toolbar with title bar above (also see [the note above](#modern-window-look)): @@ -462,7 +408,7 @@ 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. @@ -526,9 +472,9 @@ A checkbox is a type of button that lets the user choose between two opposite st checkbox is considered on when it contains a checkmark and off when it's empty. A checkbox is almost always followed by a title unless it appears in a checklist. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/checkboxes/) -| Off | On | Mixed | +| Unchecked | Checked | Mixed | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | -| ![Off Checkbox](https://developer.apple.com/design/human-interface-guidelines/macos/images/CheckBoxes_Deselected.svg) | ![On Checkbox](https://developer.apple.com/design/human-interface-guidelines/macos/images/CheckBoxes_Selected.svg) | ![Mixed Checkbox](https://developer.apple.com/design/human-interface-guidelines/macos/images/CheckBoxes_Mixed.svg) | +| ![Unchecked Checkbox](https://imgur.com/Pu4EDAE.png) | ![Checked Checkbox](https://imgur.com/CB3Kmwo.png) | ![Mixed Checkbox](https://imgur.com/T44rV38.png) | Here's an example of how to create a basic checkbox: @@ -550,7 +496,7 @@ To make a checkbox in the `mixed` state, set `value` to `null`. A help button appears within a view and opens app-specific help documentation when clicked. All help buttons are circular, consistently sized buttons that contain a question mark icon. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/help-buttons/) -![HelpButton Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/buttonsHelp.png) +![HelpButton Example](https://imgur.com/DlP7uLV.png) Here's an example of how to create a help button: @@ -571,7 +517,7 @@ A radio button is a small, circular button followed by a title. Typically presen buttons provide the user a set of related but mutually exclusive choices. A radio button’s state is either on (a filled circle) or off (an empty circle). [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/radio-buttons/) -![RadioButton Preview](https://developer.apple.com/design/human-interface-guidelines/macos/images/radioButtons.png) +![RadioButton Preview](https://imgur.com/HI0eQsU.png) Here's an example of how to create a basic radio button: @@ -686,23 +632,22 @@ MacosPopupButton( ## PushButton -A push button appears within a view and initiates an instantaneous app-specific action, such as printing a document or -deleting a file. Push buttons contain text—not icons—and often open a separate window, dialog, or app so the user can +Push buttons are the standard button type in macOS. Push buttons contain text—not icons—and often open a separate window, dialog, or app so the user can complete a task. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/push-buttons/) | Dark Theme | Light Theme | | ------------------------------------------ | ------------------------------------------ | -| | | -| | | -| | | -| | | +| | | + +ℹ️ **Note** ℹ️ +Native push buttons can be styled as text-only, text with an icon, or icon-only. Currently, text-only push buttons are supported. To create an icon-only button, use the `MacosIconButton` widget. Here's an example of how to create a basic push button: ```dart PushButton( child: Text('button'), - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, onPressed: () { print('button pressed'); }, @@ -711,32 +656,40 @@ PushButton( ## 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. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/buttons/switches/) +A switch (also known as a toggle) is a control that offers a binary choice 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. + +The `ContolSize` enum can be passed to the `size` property to control the size of the switch. `MacosSwitch` supports the following +control sizes: +* `mini` +* `small` +* `regular` -| On | Off | +| Off | On | | ------------------------------------------ | ------------------------------------------ | -| | | +| | | Here's an example of how to create a basic toggle switch: ```dart -bool selected = false; +bool switchValue = false; MacosSwitch( - value: selected, + value: switchValue, onChanged: (value) { - setState(() => selected = value); + setState(() => switchValue = value); }, ), ``` +Learn more about switches [here](https://developer.apple.com/design/human-interface-guidelines/toggles). + ## 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. @@ -750,9 +703,7 @@ Usage: showMacosAlertDialog( context: context, builder: (_) => MacosAlertDialog( - appIcon: FlutterLogo( - size: 56, - ), + appIcon: FlutterLogo(size: 64), title: Text( 'Alert Dialog with Primary Action', style: MacosTheme.of(context).typography.headline, @@ -760,10 +711,10 @@ showMacosAlertDialog( message: Text( 'This is an alert dialog with a primary action and no secondary action', textAlign: TextAlign.center, - style: MacosTheme.of(context).typography.headline, + style: MacosTypography.of(context).headline, ), primaryButton: PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.large, child: Text('Primary'), onPressed: () {}, ), @@ -771,9 +722,10 @@ showMacosAlertDialog( ); ``` -![](https://imgur.com/G3dcjew.png) -![](https://imgur.com/YHtgv59.png) -![](https://imgur.com/xuBR5qK.png) +![](https://imgur.com/4zbGsFi.png) +![](https://imgur.com/5fgkRU9.png) +![](https://imgur.com/jOyJrZO.png) +![](https://imgur.com/NX9taPj.png) ## MacosSheet @@ -785,7 +737,7 @@ showMacosSheet( ); ``` -![](https://imgur.com/NV0o5Ws.png) +![](https://imgur.com/Mnw2ywm.png) # Fields @@ -872,8 +824,6 @@ Progress indicators have two distinct styles: People don't interact with progress indicators; however, they are often accompanied by a button for canceling the corresponding operation. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/indicators/progress-indicators/) -![Progress Indicator Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/ProgressIndicators_Lead.png) - ### ProgressCircle A `ProgressCircle` can be either determinate or indeterminate. @@ -919,10 +869,8 @@ indicator styles, each with a different appearance, for communicating capacity, A capacity indicator illustrates the current level in relation to a finite capacity. Capacity indicators are often used when communicating factors like disk and battery usage. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/indicators/level-indicators#capacity-indicators) -| Continuous | Discrete | -| ---------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| ![Continuous CapacityIndicator Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/indicators-continous.png) | ![Discrete CapacityIndicator Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/indicators-discrete.png) | -| A horizontal translucent track that fills with a colored bar to indicate the current value. Tick marks are often displayed to provide context. | A horizontal row of separate, equally sized, rectangular segments. The number of segments matches the total capacity, and the segments fill completely—never partially—with color to indicate the current value. | + + Here's an example of how to create an interactive continuous capacity indicator: @@ -968,7 +916,7 @@ MacosSlider( A rating indicator uses a series of horizontally arranged graphical symbols to communicate a ranking level. The default symbol is a star. -![RatingIndicator Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/indicator-rating.png) +![RatingIndicator Example](https://imgur.com/ySQBpL6.png) A rating indicator doesn’t display partial symbols—its value is rounded in order to display complete symbols only. Within a rating indicator, symbols are always the same distance apart and don't expand or shrink to fit the control. @@ -988,22 +936,6 @@ RatingIndicator( ) ``` -### RelevanceIndicator - -A relevance indicator communicates relevancy using a series of vertical bars. It often appears in a list of search -results for reference when sorting and comparing multiple items. [Learn more](https://developer.apple.com/design/human-interface-guidelines/macos/indicators/level-indicators#relevance-indicators) - -![RelevanceIndicator Example](https://developer.apple.com/design/human-interface-guidelines/macos/images/indicator-relevance.png) - -Here's an example of how to create a relevance indicator: - -```dart -RelevanceIndicator( - value: 15, - amount: 20, -) -``` - # Selectors ## MacosDatePicker @@ -1020,6 +952,22 @@ There are three styles of `MacosDatePickers`: calendar-like interface to select a date. * `combined`: provides both `textual` and `graphical` interfaces. +Localization of the time picker is supported by the `weekdayAbbreviations` and `monthAbbreviations` parameters (instead of e.g. standard `localizations.narrowWeekdays()` in order to match Apple's spec). +* `weekdayAbbreviations` should be a list of 7 strings, one for each day of the week, starting with Sunday +* `monthAbbreviations` should be a list of 12 strings, one for each month of the year, starting with January + +You can also define the `dateFormat` to change the way dates are displayed in the textual interface. +It takes a string of tokens (case-insensitive) and replaces them with their corresponding values. +The following tokens are supported: +* `D`: day of the month (1-31) +* `DD`: day of the month (01-31) +* `M`: month of the year (1-12) +* `MM`: month of the year (01-12) +* `YYYY`: year (0000-9999) +* Any separator between tokens is preserved (e.g. `/`, `-`, `.`) + +The default format is `M/D/YYYY`. + Example usage: ```dart MacosDatePicker( diff --git a/analysis_options.yaml b/analysis_options.yaml index d4a906dd..7ed9076e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -5,8 +5,6 @@ linter: - use_super_parameters analyzer: - plugins: - - dart_code_metrics exclude: - test/mock_canvas.dart - test/recording_canvas.dart @@ -14,13 +12,23 @@ analyzer: dart_code_metrics: metrics: cyclomatic-complexity: 20 - number-of-parameters: 4 maximum-nesting-level: 5 metrics-exclude: - test/** - example/test/** rules: - prefer-trailing-comma + - double-literal-format + - prefer-first + - prefer-last + - prefer-immediate-return + - avoid-global-state + - always-remove-listener + - avoid-unnecessary-setstate + - avoid-wrapping-in-padding + - prefer-const-border-radius + - prefer-correct-edge-insets-constructor + - use-setstate-synchronously - member-ordering: alphabetize: false order: diff --git a/example/.metadata b/example/.metadata index 140b9294..53830e36 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,10 +1,30 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 4d7946a68d26794349189cf21b3f68cc6fe61dcb + revision: 796c8ef79279f9c774545b3771238c3098dbefab channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: macos + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/lib/main.dart b/example/lib/main.dart index 3d1a24e7..193bef1f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,19 +1,38 @@ +import 'dart:io'; + import 'package:example/pages/buttons_page.dart'; import 'package:example/pages/colors_page.dart'; 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/resizable_pane_page.dart'; import 'package:example/pages/selectors_page.dart'; import 'package:example/pages/sliver_toolbar_page.dart'; import 'package:example/pages/tabview_page.dart'; import 'package:example/pages/toolbar_page.dart'; +import 'package:example/pages/typography_page.dart'; +import 'package:example/platform_menus.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'theme.dart'; -void main() { +/// This method initializes macos_window_utils and styles the window. +Future _configureMacosWindowUtils() async { + const config = MacosWindowUtilsConfig(); + await config.apply(); +} + +Future main() async { + if (!kIsWeb) { + if (Platform.isMacOS) { + await _configureMacosWindowUtils(); + } + } + runApp(const MacosUIGalleryApp()); } @@ -47,68 +66,14 @@ class WidgetGallery extends StatefulWidget { } class _WidgetGalleryState extends State { - double ratingValue = 0; - double sliderValue = 0; - bool value = false; - int pageIndex = 0; late final searchFieldController = TextEditingController(); - final List pages = [ - CupertinoTabView( - builder: (_) => const ButtonsPage(), - ), - const IndicatorsPage(), - const FieldsPage(), - const ColorsPage(), - const Center( - child: MacosIcon( - CupertinoIcons.add, - ), - ), - const DialogsPage(), - const ToolbarPage(), - const SliverToolbarPage(), - const TabViewPage(), - const SelectorsPage(), - ]; - @override Widget build(BuildContext context) { return PlatformMenuBar( - menus: const [ - PlatformMenu( - label: 'macos_ui Widget Gallery', - menus: [ - PlatformProvidedMenuItem( - type: PlatformProvidedMenuItemType.about, - ), - PlatformProvidedMenuItem( - type: PlatformProvidedMenuItemType.quit, - ), - ], - ), - PlatformMenu( - label: 'View', - menus: [ - PlatformProvidedMenuItem( - type: PlatformProvidedMenuItemType.toggleFullScreen, - ), - ], - ), - PlatformMenu( - label: 'Window', - menus: [ - PlatformProvidedMenuItem( - type: PlatformProvidedMenuItemType.minimizeWindow, - ), - PlatformProvidedMenuItem( - type: PlatformProvidedMenuItemType.zoomWindow, - ), - ], - ), - ], + menus: menuBarItems(), child: MacosWindow( sidebar: Sidebar( top: MacosSearchField( @@ -142,7 +107,7 @@ class _WidgetGalleryState extends State { break; case 'Dialogs and Sheets': setState(() { - pageIndex = 5; + pageIndex = 4; searchFieldController.clear(); }); break; @@ -152,12 +117,18 @@ class _WidgetGalleryState extends State { searchFieldController.clear(); }); break; - case 'Selectors': + case 'ResizablePane': setState(() { pageIndex = 7; searchFieldController.clear(); }); break; + case 'Selectors': + setState(() { + pageIndex = 8; + searchFieldController.clear(); + }); + break; default: searchFieldController.clear(); } @@ -169,6 +140,7 @@ class _WidgetGalleryState extends State { SearchResultItem('Colors'), SearchResultItem('Dialogs and Sheets'), SearchResultItem('Toolbar'), + SearchResultItem('ResizablePane'), SearchResultItem('Selectors'), ], ), @@ -176,20 +148,27 @@ class _WidgetGalleryState extends State { builder: (context, scrollController) { return SidebarItems( currentIndex: pageIndex, - onChanged: (i) => setState(() => pageIndex = i), + onChanged: (i) { + if (kIsWeb && i == 10) { + launchUrl( + Uri.parse( + 'https://www.figma.com/file/IX6ph2VWrJiRoMTI1Byz0K/Apple-Design-Resources---macOS-(Community)?node-id=0%3A1745&mode=dev', + ), + ); + } else { + setState(() => pageIndex = i); + } + }, scrollController: scrollController, itemSize: SidebarItemSize.large, - items: [ - const SidebarItem( - // leading: MacosIcon(CupertinoIcons.square_on_circle), + items: const [ + SidebarItem( leading: MacosImageIcon( - AssetImage( - 'assets/sf_symbols/button_programmable_2x.png', - ), + AssetImage('assets/sf_symbols/button_programmable_2x.png'), ), label: Text('Buttons'), ), - const SidebarItem( + SidebarItem( leading: MacosImageIcon( AssetImage( 'assets/sf_symbols/lines_measurement_horizontal_2x.png', @@ -197,7 +176,7 @@ class _WidgetGalleryState extends State { ), label: Text('Indicators'), ), - const SidebarItem( + SidebarItem( leading: MacosImageIcon( AssetImage( 'assets/sf_symbols/character_cursor_ibeam_2x.png', @@ -206,36 +185,16 @@ class _WidgetGalleryState extends State { label: Text('Fields'), ), SidebarItem( - leading: const MacosIcon(CupertinoIcons.folder), - label: const Text('Disclosure'), - trailing: Text( - '2', - style: TextStyle( - color: MacosTheme.brightnessOf(context) == Brightness.dark - ? MacosColors.tertiaryLabelColor.darkColor - : MacosColors.tertiaryLabelColor, - ), + leading: MacosImageIcon( + AssetImage('assets/sf_symbols/rectangle_3_group_2x.png'), ), - disclosureItems: [ - const SidebarItem( - leading: MacosImageIcon( - AssetImage( - 'assets/sf_symbols/rectangle_3_group_2x.png', - ), - ), - label: Text('Colors'), - ), - const SidebarItem( - leading: MacosIcon(CupertinoIcons.infinite), - label: Text('Item 3'), - ), - ], + label: Text('Colors'), ), - const SidebarItem( + SidebarItem( leading: MacosIcon(CupertinoIcons.square_on_square), label: Text('Dialogs & Sheets'), ), - const SidebarItem( + SidebarItem( leading: MacosImageIcon( AssetImage( 'assets/sf_symbols/macwindow.on.rectangle_2x.png', @@ -259,16 +218,23 @@ class _WidgetGalleryState extends State { leading: MacosIcon(CupertinoIcons.uiwindow_split_2x1), label: Text('TabView'), ), + SidebarItem( + leading: MacosIcon(CupertinoIcons.rectangle_split_3x1), + label: Text('ResizablePane'), + ), ], ), - const SidebarItem( + SidebarItem( leading: MacosImageIcon( AssetImage( - 'assets/sf_symbols/filemenu_and_selection_2x.png', - ), + 'assets/sf_symbols/filemenu_and_selection_2x.png'), ), label: Text('Selectors'), ), + SidebarItem( + leading: MacosIcon(CupertinoIcons.textformat_size), + label: Text('Typography'), + ), ], ); }, @@ -289,10 +255,19 @@ class _WidgetGalleryState extends State { ); }, ), - child: IndexedStack( - index: pageIndex, - children: pages, - ), + child: [ + CupertinoTabView(builder: (_) => const ButtonsPage()), + const IndicatorsPage(), + const FieldsPage(), + const ColorsPage(), + const DialogsPage(), + const ToolbarPage(), + const SliverToolbarPage(isVisible: !kIsWeb), + const TabViewPage(), + const ResizablePanePage(), + const SelectorsPage(), + const TypographyPage(), + ][pageIndex], ), ); } diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart index 6c373f5f..fd61774f 100644 --- a/example/lib/pages/buttons_page.dart +++ b/example/lib/pages/buttons_page.dart @@ -1,3 +1,5 @@ +import 'package:example/widgets/widget_text_title1.dart'; +import 'package:example/widgets/widget_text_title2.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; @@ -59,412 +61,719 @@ class _ButtonsPageState extends State { ], ), children: [ - ResizablePane( - minSize: 180, - startSize: 200, - windowBreakpoint: 700, - resizableSide: ResizableSide.right, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), - ); - }, - ), ContentArea( builder: (context, scrollController) { - return Column( - children: [ - Flexible( - fit: FlexFit.loose, - child: SingleChildScrollView( - controller: scrollController, - 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('MacosDisclosureButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosDisclosureButton( - isPressed: isDisclosureButtonPressed, - onPressed: () { - debugPrint('click'); - setState(() { - isDisclosureButtonPressed = - !isDisclosureButtonPressed; - }); - }), - ], - ), - const SizedBox(height: 20), - const Text('MacosIconButton'), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MacosIconButton( - icon: const MacosIcon( - CupertinoIcons.star_fill, - ), - shape: BoxShape.rectangle, - borderRadius: BorderRadius.circular(7), - onPressed: () {}, - ), - const SizedBox(width: 8), - const MacosIconButton( - icon: MacosIcon( - CupertinoIcons.plus_app, - ), - 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(); + return SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const WidgetTextTitle1(widgetName: 'PushButton'), + Divider(color: MacosTheme.of(context).dividerColor), + Text( + 'Primary', + style: MacosTypography.of(context).title2, + ), + Row( + children: [ + PushButton( + controlSize: ControlSize.mini, + child: const Text('Mini'), + onPressed: () { + MacosWindowScope.of(context).toggleSidebar(); + }, + ), + const SizedBox(width: 8), + PushButton( + controlSize: ControlSize.small, + child: const Text('Small'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], + ); }, ), - const SizedBox(width: 20), - PushButton( - buttonSize: ButtonSize.small, - child: const Text('Small'), - onPressed: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) { - return MacosScaffold( - toolBar: const ToolBar( - title: Text('New page'), - ), - children: [ - ContentArea( - builder: (context, _) { - return Center( - child: PushButton( - buttonSize: ButtonSize.large, - child: const Text('Go Back'), - onPressed: () { - Navigator.of(context) - .maybePop(); - }, - ), - ); + ); + }, + ), + const SizedBox(width: 8), + PushButton( + controlSize: ControlSize.regular, + child: const Text('Regular'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); }, ), - ResizablePane( - minSize: 180, - startSize: 200, - windowBreakpoint: 700, - resizableSide: ResizableSide.left, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), - ); + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], + ); + }, + ), + ); + }, + ), + const SizedBox(width: 8), + PushButton( + controlSize: ControlSize.large, + child: const Text('Large'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); }, ), - ], - ); - }, - ), + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 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: 16), + Text( + 'Disabled Primary', + style: MacosTypography.of(context).title2, + ), + const Row( + children: [ + PushButton( + controlSize: ControlSize.mini, + child: Text('Mini'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.small, + child: Text('Small'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.regular, + child: Text('Regular'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.large, + child: Text('Large'), + ), + ], + ), + const SizedBox(height: 16), + Text( + 'Secondary', + style: MacosTypography.of(context).title2, + ), + Row( + children: [ + PushButton( + controlSize: ControlSize.mini, + secondary: true, + child: const Text('Mini'), + onPressed: () { + MacosWindowScope.of(context).toggleSidebar(); + }, + ), + const SizedBox(width: 8), + PushButton( + controlSize: ControlSize.small, + secondary: true, + child: const Text('Small'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], + ); }, ), - ], - ), - 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), + ); + }, + ), + const SizedBox(width: 8), + PushButton( + controlSize: ControlSize.regular, + secondary: true, + child: const Text('Regular'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], ); - }).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(width: 8), + PushButton( + controlSize: ControlSize.large, + secondary: true, + child: const Text('Large'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) { + return MacosScaffold( + toolBar: const ToolBar( + title: Text('New page'), + ), + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: PushButton( + controlSize: ControlSize.regular, + child: const Text('Go Back'), + onPressed: () { + Navigator.of(context).maybePop(); + }, + ), + ); + }, + ), + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.left, + builder: (_, __) { + return const Center( + child: Text('Resizable Pane'), + ); + }, + ), + ], + ); }, ), - ], + ); + }, + ), + ], + ), + const SizedBox(height: 16), + Text( + 'Disabled Secondary', + style: MacosTypography.of(context).title2, + ), + const Row( + children: [ + PushButton( + controlSize: ControlSize.mini, + secondary: true, + child: Text('Mini'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.small, + secondary: true, + child: Text('Small'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.regular, + secondary: true, + child: Text('Regular'), + ), + SizedBox(width: 8), + PushButton( + controlSize: ControlSize.large, + secondary: true, + child: Text('Large'), + ), + ], + ), + const SizedBox(height: 16), + const WidgetTextTitle1(widgetName: 'HelpButton'), + Divider(color: MacosTheme.of(context).dividerColor), + HelpButton(onPressed: () {}), + const SizedBox(height: 16), + Text( + 'Icon Buttons', + style: MacosTypography.of(context).title1, + ), + Divider(color: MacosTheme.of(context).dividerColor), + const WidgetTextTitle2(widgetName: 'MacosBackButton'), + const SizedBox(height: 8), + Row( + children: [ + MacosBackButton( + onPressed: () => debugPrint('click'), + fillColor: Colors.transparent, + ), + const SizedBox(width: 16.0), + MacosBackButton( + onPressed: () => debugPrint('click'), + ), + ], + ), + const SizedBox(height: 20), + const WidgetTextTitle2(widgetName: 'MacosDisclosureButton'), + const SizedBox(height: 8), + Row( + children: [ + MacosDisclosureButton( + isPressed: isDisclosureButtonPressed, + onPressed: () { + debugPrint('click'); + setState(() { + isDisclosureButtonPressed = + !isDisclosureButtonPressed; + }); + }, + ), + ], + ), + const SizedBox(height: 20), + const WidgetTextTitle2(widgetName: 'MacosIconButton'), + const SizedBox(height: 8), + Row( + children: [ + MacosIconButton( + icon: const MacosIcon( + CupertinoIcons.star_fill, ), - 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!; - }, - ), - ], + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(7), + onPressed: () {}, + ), + const SizedBox(width: 8), + const MacosIconButton( + icon: MacosIcon( + CupertinoIcons.plus_app, ), - 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, - ), - ], + shape: BoxShape.circle, + //onPressed: () {}, + ), + const SizedBox(width: 8), + MacosIconButton( + icon: const MacosIcon( + CupertinoIcons.minus_square, ), - ], - ), + backgroundColor: Colors.transparent, + onPressed: () {}, + ), + ], ), - ), - ResizablePane( - minSize: 50, - startSize: 200, - //windowBreakpoint: 600, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), - ); - }, - resizableSide: ResizableSide.top, - ) - ], - ); - }, - ), - ResizablePane( - minSize: 180, - startSize: 200, - windowBreakpoint: 800, - resizableSide: ResizableSide.left, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), + const SizedBox(height: 20), + Text( + 'Switches, Checkboxes, & Radios', + style: MacosTypography.of(context).title1, + ), + Divider(color: MacosTheme.of(context).dividerColor), + const WidgetTextTitle2(widgetName: 'MacosSwitch'), + const SizedBox(height: 8), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + const Text('Mini'), + const SizedBox(width: 8), + MacosSwitch( + value: switchValue, + size: ControlSize.mini, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + ], + ), + const SizedBox(height: 8.0), + Row( + children: [ + const Text('Small'), + const SizedBox(width: 8), + MacosSwitch( + value: switchValue, + size: ControlSize.small, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + ], + ), + const SizedBox(height: 8.0), + Row( + children: [ + const Text('Regular'), + const SizedBox(width: 8), + MacosSwitch( + value: switchValue, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + ], + ), + ], + ), + const SizedBox(height: 16), + const WidgetTextTitle2(widgetName: 'MacosCheckbox'), + const SizedBox(height: 8), + MacosCheckbox( + value: switchValue, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + const SizedBox(height: 16), + const WidgetTextTitle2(widgetName: 'MacosRadioButton'), + const SizedBox(height: 8), + Row( + 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( + 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( + 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), + Text( + 'Pulldown & Popup Buttons', + style: MacosTypography.of(context).title1, + ), + Divider(color: MacosTheme.of(context).dividerColor), + const WidgetTextTitle2(widgetName: 'MacosPulldownButton'), + const SizedBox(height: 8), + Row( + 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( + 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 WidgetTextTitle2(widgetName: 'MacosPopupButton'), + const SizedBox(height: 8), + Row( + 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), + const WidgetTextTitle1(widgetName: 'MacosSegmentedControl'), + Divider(color: MacosTheme.of(context).dividerColor), + 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, + ), + ], + ), + ], + ), ); }, ), diff --git a/example/lib/pages/colors_page.dart b/example/lib/pages/colors_page.dart index 7a9df4fe..6eb3eac8 100644 --- a/example/lib/pages/colors_page.dart +++ b/example/lib/pages/colors_page.dart @@ -15,16 +15,27 @@ class _ColorsPageState extends State { toolBar: ToolBar( title: const Text('Colors'), titleWidth: 150.0, - actions: [ - ToolBarIconButton( - label: 'Toggle Sidebar', - icon: const MacosIcon( + 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(), - showLabel: false, ), - ], + ), ), children: [ ContentArea( diff --git a/example/lib/pages/dialogs_page.dart b/example/lib/pages/dialogs_page.dart index b7645978..33764bda 100644 --- a/example/lib/pages/dialogs_page.dart +++ b/example/lib/pages/dialogs_page.dart @@ -2,6 +2,9 @@ import 'package:macos_ui/macos_ui.dart'; // ignore: implementation_imports import 'package:macos_ui/src/library.dart'; +const dialogMessage = + 'Description text about this alert is shown here, explaining to users what the options underneath are about and what to do.'; + class DialogsPage extends StatefulWidget { const DialogsPage({super.key}); @@ -16,16 +19,27 @@ class _DialogsPageState extends State { toolBar: ToolBar( title: const Text('Dialogs and Sheets'), titleWidth: 150.0, - actions: [ - ToolBarIconButton( - label: 'Toggle Sidebar', - icon: const MacosIcon( + 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(), - showLabel: false, ), - ], + ), ), children: [ ContentArea( @@ -36,120 +50,101 @@ class _DialogsPageState extends State { child: Column( children: [ PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, child: const Text('Show Alert Dialog 1'), onPressed: () => showMacosAlertDialog( context: context, builder: (context) => MacosAlertDialog( - appIcon: const FlutterLogo( - size: 56, - ), - title: const Text( - 'Alert Dialog with Primary Action', - ), - message: const Text( - 'This is an alert dialog with a primary action and no secondary action', - ), + appIcon: const FlutterLogo(size: 64), + title: const Text('Title'), + message: const Text(dialogMessage), //horizontalActions: false, primaryButton: PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.large, onPressed: Navigator.of(context).pop, - child: const Text('Primary'), + child: const Text('Label'), ), ), ), ), const SizedBox(height: 16), PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, child: const Text('Show Alert Dialog 2'), onPressed: () => showMacosAlertDialog( context: context, builder: (context) => MacosAlertDialog( - appIcon: const FlutterLogo( - size: 56, - ), - title: const Text( - 'Alert Dialog with Secondary Action', - ), + appIcon: const FlutterLogo(size: 64), + title: const Text('Title'), message: const Text( - 'This is an alert dialog with primary action and secondary action laid out horizontally', + dialogMessage, textAlign: TextAlign.center, ), //horizontalActions: false, primaryButton: PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.large, onPressed: Navigator.of(context).pop, - child: const Text('Primary'), + child: const Text('Label'), ), secondaryButton: PushButton( - buttonSize: ButtonSize.large, - isSecondary: true, + controlSize: ControlSize.large, + secondary: true, onPressed: Navigator.of(context).pop, - child: const Text('Secondary'), + child: const Text('Label'), ), ), ), ), const SizedBox(height: 16), PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, child: const Text('Show Alert Dialog 3'), onPressed: () => showMacosAlertDialog( context: context, builder: (context) => MacosAlertDialog( - appIcon: const FlutterLogo( - size: 56, - ), - title: const Text( - 'Alert Dialog with Secondary Action', - ), + appIcon: const FlutterLogo(size: 64), + title: const Text('Title'), message: const Text( - 'This is an alert dialog with primary action and secondary action laid out vertically', + dialogMessage, textAlign: TextAlign.center, ), horizontalActions: false, primaryButton: PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.large, onPressed: Navigator.of(context).pop, - child: const Text('Primary'), + child: const Text('Label'), ), secondaryButton: PushButton( - buttonSize: ButtonSize.large, - isSecondary: true, + controlSize: ControlSize.large, + secondary: true, onPressed: Navigator.of(context).pop, - child: const Text('Secondary'), + child: const Text('Label'), ), ), ), ), const SizedBox(height: 16), PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, child: const Text('Show Alert Dialog 4'), onPressed: () => showMacosAlertDialog( context: context, builder: (context) => MacosAlertDialog( - appIcon: const FlutterLogo( - size: 56, - ), - title: const Text( - 'Alert Dialog with Secondary Action', - ), + appIcon: const FlutterLogo(size: 64), + title: const Text('Title'), message: const Text( - 'This is an alert dialog with primary action and secondary ' - 'action laid out vertically. It also contains a "suppress" option.', + dialogMessage, textAlign: TextAlign.center, ), horizontalActions: false, primaryButton: PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.large, onPressed: Navigator.of(context).pop, child: const Text('Primary'), ), secondaryButton: PushButton( - buttonSize: ButtonSize.large, - isSecondary: true, + controlSize: ControlSize.large, + secondary: true, onPressed: Navigator.of(context).pop, child: const Text('Secondary'), ), @@ -159,7 +154,7 @@ class _DialogsPageState extends State { ), const SizedBox(height: 16), PushButton( - buttonSize: ButtonSize.large, + controlSize: ControlSize.regular, child: const Text('Show sheet'), onPressed: () { showMacosSheet( @@ -214,47 +209,50 @@ class DemoSheet extends StatelessWidget { @override Widget build(BuildContext context) { return MacosSheet( - child: Center( - child: Column( - children: [ - const SizedBox(height: 50), - const FlutterLogo( - size: 56, - ), - const SizedBox(height: 24), - Text( - 'Welcome to macos_ui', - style: MacosTheme.of(context).typography.largeTitle.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 24), - const MacosListTile( - leading: MacosIcon(CupertinoIcons.lightbulb), - title: Text( - 'A robust library of Flutter components for macOS', - //style: MacosTheme.of(context).typography.headline, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Center( + child: Column( + children: [ + const SizedBox(height: 50), + const FlutterLogo( + size: 56, ), - subtitle: Text( - 'Create native looking macOS applications using Flutter', + const SizedBox(height: 24), + Text( + 'Welcome to macos_ui', + style: MacosTheme.of(context).typography.largeTitle.copyWith( + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 16), - const MacosListTile( - leading: MacosIcon(CupertinoIcons.bolt), - title: Text( - 'Create beautiful macOS applications in minutes', - //style: MacosTheme.of(context).typography.headline, + const SizedBox(height: 24), + const MacosListTile( + leading: MacosIcon(CupertinoIcons.lightbulb), + title: Text( + 'A robust library of Flutter components for macOS', + //style: MacosTheme.of(context).typography.headline, + ), + subtitle: Text( + 'Create native looking macOS applications using Flutter', + ), ), - ), - const Spacer(), - PushButton( - buttonSize: ButtonSize.large, - child: const Text('Get started'), - onPressed: () => Navigator.of(context).pop(), - ), - const SizedBox(height: 50), - ], + const SizedBox(height: 16), + const MacosListTile( + leading: MacosIcon(CupertinoIcons.bolt), + title: Text( + 'Create beautiful macOS applications in minutes', + //style: MacosTheme.of(context).typography.headline, + ), + ), + const Spacer(), + PushButton( + controlSize: ControlSize.regular, + child: const Text('Get started'), + onPressed: () => Navigator.of(context).pop(), + ), + const SizedBox(height: 50), + ], + ), ), ), ); diff --git a/example/lib/pages/fields_page.dart b/example/lib/pages/fields_page.dart index 94785708..fd9183bb 100644 --- a/example/lib/pages/fields_page.dart +++ b/example/lib/pages/fields_page.dart @@ -1,4 +1,6 @@ +import 'package:example/widgets/widget_text_title1.dart'; import 'package:flutter/cupertino.dart' hide OverlayVisibilityMode; +import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; class FieldsPage extends StatefulWidget { @@ -15,16 +17,27 @@ class _FieldsPageState extends State { toolBar: ToolBar( title: const Text('Fields'), titleWidth: 150.0, - actions: [ - ToolBarIconButton( - label: 'Toggle Sidebar', - icon: const MacosIcon( + 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(), - showLabel: false, ), - ], + ), ), children: [ ContentArea( @@ -32,7 +45,10 @@ class _FieldsPageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + const WidgetTextTitle1(widgetName: 'MacosTextField'), + Divider(color: MacosTheme.of(context).dividerColor), const SizedBox( width: 300.0, child: MacosTextField( @@ -84,6 +100,8 @@ class _FieldsPageState extends State { ), ), const SizedBox(height: 20), + const WidgetTextTitle1(widgetName: 'MacosSearchField'), + Divider(color: MacosTheme.of(context).dividerColor), SizedBox( width: 300.0, child: MacosSearchField( @@ -125,17 +143,6 @@ class _FieldsPageState extends State { ); }, ), - ResizablePane( - minSize: 180, - startSize: 200, - windowBreakpoint: 800, - resizableSide: ResizableSide.left, - builder: (_, __) { - return const Center( - child: Text('Resizable Pane'), - ); - }, - ), ], ); } @@ -352,8 +359,8 @@ const countries = [ var actionResults = [ SearchResultItem( 'Build project', - child: Row( - children: const [ + child: const Row( + children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.hammer), @@ -365,8 +372,8 @@ var actionResults = [ ), SearchResultItem( 'Debug project', - child: Row( - children: const [ + child: const Row( + children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.tickets), @@ -378,8 +385,8 @@ var actionResults = [ ), SearchResultItem( 'Open containing folder', - child: Row( - children: const [ + child: const Row( + children: [ Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: MacosIcon(CupertinoIcons.folder), diff --git a/example/lib/pages/indicators_page.dart b/example/lib/pages/indicators_page.dart index 1dd4caf5..511b40c6 100644 --- a/example/lib/pages/indicators_page.dart +++ b/example/lib/pages/indicators_page.dart @@ -1,3 +1,5 @@ +import 'package:example/widgets/widget_text_title1.dart'; +import 'package:example/widgets/widget_text_title2.dart'; import 'package:macos_ui/macos_ui.dart'; // ignore: implementation_imports import 'package:macos_ui/src/library.dart'; @@ -20,16 +22,27 @@ class _IndicatorsPageState extends State { toolBar: ToolBar( title: const Text('Indicators'), titleWidth: 150.0, - actions: [ - ToolBarIconButton( - label: 'Toggle Sidebar', - icon: const MacosIcon( + 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(), - showLabel: false, ), - ], + ), ), children: [ ContentArea( @@ -37,47 +50,103 @@ class _IndicatorsPageState extends State { return SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - CapacityIndicator( - value: capacitorValue, - onChanged: (v) => setState(() => capacitorValue = v), - splits: 20, - discrete: true, + const WidgetTextTitle1(widgetName: 'CapacityIndicator'), + Divider(color: MacosTheme.of(context).dividerColor), + Row( + children: [ + const Text('Standard'), + const SizedBox(width: 8), + Expanded( + child: CapacityIndicator( + value: capacitorValue, + onChanged: (v) => setState(() => capacitorValue = v), + ), + ), + ], ), - const SizedBox(height: 20), - CapacityIndicator( - value: capacitorValue, - onChanged: (v) => setState(() => capacitorValue = v), + const SizedBox(height: 16), + Row( + children: [ + const Text('Discrete'), + const SizedBox(width: 8), + Expanded( + child: CapacityIndicator( + value: capacitorValue, + onChanged: (v) => setState(() => capacitorValue = v), + splits: 20, + discrete: true, + ), + ), + ], ), const SizedBox(height: 20), - MacosSlider( - value: sliderValue, - onChanged: (v) => setState(() => sliderValue = v), + const WidgetTextTitle1(widgetName: 'MacosSlider'), + Divider(color: MacosTheme.of(context).dividerColor), + Row( + children: [ + const Text('Standard'), + const SizedBox(width: 8), + Expanded( + child: MacosSlider( + value: sliderValue, + onChanged: (v) => setState(() => sliderValue = v), + ), + ), + ], ), - const SizedBox(height: 20), - MacosSlider( - value: sliderValue, - discrete: true, - onChanged: (v) => setState(() => sliderValue = v), + const SizedBox(height: 16), + Row( + children: [ + const Text('Discrete'), + const SizedBox(width: 8), + Expanded( + child: MacosSlider( + value: sliderValue, + discrete: true, + onChanged: (v) => setState(() => sliderValue = v), + ), + ), + ], ), const SizedBox(height: 20), + const WidgetTextTitle1(widgetName: 'RatingIndicator'), + Divider(color: MacosTheme.of(context).dividerColor), RatingIndicator( value: ratingValue, onChanged: (v) => setState(() => ratingValue = v), ), const SizedBox(height: 20), - const ProgressCircle(), - const SizedBox(height: 20), - const RelevanceIndicator( - value: 25, - amount: 50, + Text( + 'Progress Indicators', + style: MacosTypography.of(context).title1, ), - const SizedBox(height: 20), - const Label( - icon: MacosIcon(CupertinoIcons.tag), - text: SelectableText('A determinate progress circle: '), - child: ProgressCircle(value: 50), + Divider(color: MacosTheme.of(context).dividerColor), + const WidgetTextTitle2(widgetName: 'ProgressBar'), + const SizedBox(height: 8), + const ProgressBar(value: 50), + const SizedBox(height: 16), + const WidgetTextTitle2(widgetName: 'ProgressCircle'), + const SizedBox(height: 8), + const Row( + children: [ + Text('Indeterminate'), + SizedBox(width: 8), + ProgressCircle(), + ], ), + const Row( + children: [ + Text('Determinate'), + SizedBox(width: 8), + ProgressCircle(value: 50), + ], + ), + const SizedBox(height: 20), + const WidgetTextTitle1(widgetName: 'RelevanceIndicator'), + Divider(color: MacosTheme.of(context).dividerColor), + const SizedBox(height: 8), ], ), ); diff --git a/example/lib/pages/resizable_pane_page.dart b/example/lib/pages/resizable_pane_page.dart new file mode 100644 index 00000000..78ed3320 --- /dev/null +++ b/example/lib/pages/resizable_pane_page.dart @@ -0,0 +1,86 @@ +import 'package:flutter/cupertino.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class ResizablePanePage extends StatefulWidget { + const ResizablePanePage({super.key}); + + @override + State createState() => _ResizablePanePageState(); +} + +class _ResizablePanePageState extends State { + @override + Widget build(BuildContext context) { + return MacosScaffold( + toolBar: ToolBar( + title: const Text('Resizable Pane'), + 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(), + ), + ), + ), + children: [ + ResizablePane( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.right, + builder: (_, __) { + return const Center( + child: Text('Left Resizable Pane'), + ); + }, + ), + ContentArea( + builder: (_, __) { + return Column( + children: [ + const Flexible( + fit: FlexFit.loose, + child: Center( + child: Text('Content Area'), + ), + ), + ResizablePane( + minSize: 50, + startSize: 200, + //windowBreakpoint: 600, + builder: (_, __) { + return const Center( + child: Text('Bottom Resizable Pane'), + ); + }, + resizableSide: ResizableSide.top, + ), + ], + ); + }, + ), + const ResizablePane.noScrollBar( + minSize: 180, + startSize: 200, + windowBreakpoint: 700, + resizableSide: ResizableSide.right, + child: Center(child: Text('Right non-scrollable Resizable Pane')), + ), + ], + ); + } +} diff --git a/example/lib/pages/selectors_page.dart b/example/lib/pages/selectors_page.dart index 8256a33e..74ed4b47 100644 --- a/example/lib/pages/selectors_page.dart +++ b/example/lib/pages/selectors_page.dart @@ -1,4 +1,8 @@ +import 'package:example/widgets/widget_text_title1.dart'; +import 'package:example/widgets/widget_text_title2.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; class SelectorsPage extends StatefulWidget { @@ -15,16 +19,27 @@ class _SelectorsPageState extends State { toolBar: ToolBar( title: const Text('Selectors'), titleWidth: 150.0, - actions: [ - ToolBarIconButton( - label: 'Toggle Sidebar', - icon: const MacosIcon( + 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(), - showLabel: false, ), - ], + ), ), children: [ ContentArea( @@ -33,23 +48,45 @@ class _SelectorsPageState extends State { controller: scrollController, padding: const EdgeInsets.all(20), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + 'Date & Time Pickers', + style: MacosTypography.of(context).title1, + ), + Divider(color: MacosTheme.of(context).dividerColor), Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: [ - MacosDatePicker( - onDateChanged: (date) => debugPrint('$date'), + Column( + children: [ + const WidgetTextTitle2(widgetName: 'MacosDatePicker'), + const SizedBox(height: 12), + MacosDatePicker( + onDateChanged: (date) => debugPrint('$date'), + ), + ], ), - MacosTimePicker( - onTimeChanged: (time) => debugPrint('$time'), + const SizedBox(width: 50), + Column( + children: [ + const WidgetTextTitle2(widgetName: 'MacosTimePicker'), + const SizedBox(height: 12), + MacosTimePicker( + onTimeChanged: (time) => debugPrint('$time'), + ), + ], ), ], ), - const SizedBox(height: 50), - MacosColorWell( - onColorSelected: (color) => debugPrint('$color'), - ), + const SizedBox(height: 20), + if (!kIsWeb) ...[ + const WidgetTextTitle1(widgetName: 'MacosColorWell'), + Divider(color: MacosTheme.of(context).dividerColor), + MacosColorWell( + onColorSelected: (color) => debugPrint('$color'), + ), + ], ], ), ); diff --git a/example/lib/pages/sliver_toolbar_page.dart b/example/lib/pages/sliver_toolbar_page.dart index 8c2039a5..be2e680c 100644 --- a/example/lib/pages/sliver_toolbar_page.dart +++ b/example/lib/pages/sliver_toolbar_page.dart @@ -3,7 +3,27 @@ import 'package:flutter/material.dart'; import 'package:macos_ui/macos_ui.dart'; class SliverToolbarPage extends StatefulWidget { - const SliverToolbarPage({super.key}); + const SliverToolbarPage({super.key, required this.isVisible}); + + /// Whether this [SliverToolbarPage] is currently visible on the screen + /// (that is, not e.g. hidden by an [IndexedStack]). + /// + /// By default, macos_ui applies wallpaper tinting to the application's + /// window to match macOS' native appearance: + /// + /// + /// + /// However, this effect is realized by inserting `NSVisualEffectView`s behind + /// Flutter's canvas and turning the background of areas that are meant to be + /// affected by wallpaper tinting transparent. Since Flutter's + /// [`ImageFilter.blur`](https://api.flutter.dev/flutter/dart-ui/ImageFilter/ImageFilter.blur.html) + /// does not support transparency, wallpaper tinting is disabled automatically + /// when this widget's [isVisible] is true. + /// + /// This is meant to be a temporary solution until + /// [#16296](https://github.com/flutter/flutter/issues/16296) is resolved in + /// the Flutter project. + final bool isVisible; @override State createState() => _SliverToolbarPageState(); @@ -27,6 +47,7 @@ class _SliverToolbarPageState extends State { floating: floating, pinned: pinned, toolbarOpacity: opacity, + allowWallpaperTintingOverrides: widget.isVisible, actions: [ ToolBarIconButton( label: 'Pinned', diff --git a/example/lib/pages/tabview_page.dart b/example/lib/pages/tabview_page.dart index 64d4d8ff..5e224429 100644 --- a/example/lib/pages/tabview_page.dart +++ b/example/lib/pages/tabview_page.dart @@ -17,8 +17,29 @@ class _TabViewPageState extends State { @override Widget build(BuildContext context) { return MacosScaffold( - toolBar: const ToolBar( - title: Text('TabView'), + toolBar: ToolBar( + title: const Text('TabView'), + 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(), + ), + ), ), children: [ ContentArea( diff --git a/example/lib/pages/toolbar_page.dart b/example/lib/pages/toolbar_page.dart index 6e1754ce..104bd7c1 100644 --- a/example/lib/pages/toolbar_page.dart +++ b/example/lib/pages/toolbar_page.dart @@ -160,9 +160,9 @@ class _ToolbarPageState extends State { return SingleChildScrollView( controller: scrollController, padding: const EdgeInsets.all(30), - child: Center( + child: const Center( child: Column( - children: const [ + children: [ Text( 'A toolbar provides convenient access to frequently used commands and controls that perform actions relevant to the current view.', textAlign: TextAlign.center, diff --git a/example/lib/pages/typography_page.dart b/example/lib/pages/typography_page.dart new file mode 100644 index 00000000..3779cb57 --- /dev/null +++ b/example/lib/pages/typography_page.dart @@ -0,0 +1,394 @@ +import 'package:flutter/cupertino.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class TypographyPage extends StatelessWidget { + const TypographyPage({super.key}); + + @override + Widget build(BuildContext context) { + final typography = MacosTypography.of(context); + final secondaryTypography = MacosTypography( + color: MacosTheme.brightnessOf(context).isDark + ? MacosColors.secondaryLabelColor.darkColor + : MacosColors.secondaryLabelColor, + ); + final tertiaryTypography = MacosTypography( + color: MacosTheme.brightnessOf(context).isDark + ? MacosColors.tertiaryLabelColor.darkColor + : MacosColors.tertiaryLabelColor, + ); + + return MacosScaffold( + toolBar: const ToolBar( + title: Text('Typography'), + ), + children: [ + ContentArea( + builder: (context, scrollController) { + return ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Label Color'), + const SizedBox(height: 42.0), + Text('LargeTitle', style: typography.largeTitle), + const SizedBox(height: 8.0), + Text( + 'LargeTitle', + style: typography.largeTitle + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text('Title1', style: typography.title1), + const SizedBox(height: 8.0), + Text( + 'Title1', + style: typography.title1 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text('Title2', style: typography.title2), + const SizedBox(height: 8.0), + Text( + 'Title2', + style: typography.title2 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text('Title3', style: typography.title3), + const SizedBox(height: 8.0), + Text( + 'Title3', + style: typography.title3 + .copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24.0), + Text('Headline', style: typography.headline), + const SizedBox(height: 8.0), + Text( + 'Headline', + style: typography.headline + .copyWith(fontWeight: MacosFontWeight.w860), + ), + const SizedBox(height: 24.0), + Text('Body', style: typography.body), + const SizedBox(height: 8.0), + Text( + 'Body', + style: typography.body + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text('Callout', style: typography.callout), + const SizedBox(height: 8.0), + Text( + 'Callout', + style: typography.callout + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text('Subheadline', style: typography.subheadline), + const SizedBox(height: 8.0), + Text( + 'Subheadline', + style: typography.subheadline + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text('Footnote', style: typography.subheadline), + const SizedBox(height: 8.0), + Text( + 'Footnote', + style: typography.subheadline + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text('Caption1', style: typography.caption1), + const SizedBox(height: 8.0), + Text( + 'Caption1', + style: typography.caption1 + .copyWith(fontWeight: MacosFontWeight.w510), + ), + const SizedBox(height: 24.0), + Text('Caption2', style: typography.caption2), + const SizedBox(height: 8.0), + Text( + 'Caption2', + style: typography.caption2 + .copyWith(fontWeight: MacosFontWeight.w590), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Secondary Label Color'), + const SizedBox(height: 42.0), + Text( + 'LargeTitle', + style: secondaryTypography.largeTitle, + ), + const SizedBox(height: 8.0), + Text( + 'LargeTitle', + style: secondaryTypography.largeTitle + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title1', + style: secondaryTypography.title1, + ), + const SizedBox(height: 8.0), + Text( + 'Title1', + style: secondaryTypography.title1 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title2', + style: secondaryTypography.title2, + ), + const SizedBox(height: 8.0), + Text( + 'Title2', + style: secondaryTypography.title2 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title3', + style: secondaryTypography.title3, + ), + const SizedBox(height: 8.0), + Text( + 'Title3', + style: secondaryTypography.title3 + .copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24.0), + Text( + 'Headline', + style: secondaryTypography.headline, + ), + const SizedBox(height: 8.0), + Text( + 'Headline', + style: secondaryTypography.headline + .copyWith(fontWeight: MacosFontWeight.w860), + ), + const SizedBox(height: 24.0), + Text( + 'Body', + style: secondaryTypography.body, + ), + const SizedBox(height: 8.0), + Text( + 'Body', + style: secondaryTypography.body + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Callout', + style: secondaryTypography.callout, + ), + const SizedBox(height: 8.0), + Text( + 'Callout', + style: secondaryTypography.callout + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Subheadline', + style: secondaryTypography.subheadline, + ), + const SizedBox(height: 8.0), + Text( + 'Subheadline', + style: secondaryTypography.subheadline + .copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24.0), + Text( + 'Footnote', + style: secondaryTypography.footnote, + ), + const SizedBox(height: 8.0), + Text( + 'Footnote', + style: secondaryTypography.footnote + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Caption1', + style: secondaryTypography.caption1, + ), + const SizedBox(height: 8.0), + Text( + 'Caption1', + style: secondaryTypography.caption1 + .copyWith(fontWeight: MacosFontWeight.w510), + ), + const SizedBox(height: 24.0), + Text( + 'Caption2', + style: secondaryTypography.caption2, + ), + const SizedBox(height: 8.0), + Text( + 'Caption2', + style: secondaryTypography.caption2 + .copyWith(fontWeight: MacosFontWeight.w590), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Tertiary Label Color'), + const SizedBox(height: 42.0), + Text( + 'LargeTitle', + style: tertiaryTypography.largeTitle, + ), + const SizedBox(height: 8.0), + Text( + 'LargeTitle', + style: tertiaryTypography.largeTitle + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title1', + style: tertiaryTypography.title1, + ), + const SizedBox(height: 8.0), + Text( + 'Title1', + style: tertiaryTypography.title1 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title2', + style: tertiaryTypography.title2, + ), + const SizedBox(height: 8.0), + Text( + 'Title2', + style: tertiaryTypography.title2 + .copyWith(fontWeight: FontWeight.w700), + ), + const SizedBox(height: 24.0), + Text( + 'Title3', + style: tertiaryTypography.title3, + ), + const SizedBox(height: 8.0), + Text( + 'Title3', + style: tertiaryTypography.title3 + .copyWith(fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24.0), + Text( + 'Headline', + style: tertiaryTypography.headline, + ), + const SizedBox(height: 8.0), + Text( + 'Headline', + style: tertiaryTypography.headline + .copyWith(fontWeight: MacosFontWeight.w860), + ), + const SizedBox(height: 24.0), + Text( + 'Body', + style: tertiaryTypography.body, + ), + const SizedBox(height: 8.0), + Text( + 'Body', + style: tertiaryTypography.body + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Callout', + style: tertiaryTypography.callout, + ), + const SizedBox(height: 8.0), + Text( + 'Callout', + style: tertiaryTypography.callout + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Subheadline', + style: tertiaryTypography.subheadline, + ), + const SizedBox(height: 8.0), + Text( + 'Subheadline', + style: tertiaryTypography.subheadline + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Footnote', + style: tertiaryTypography.footnote, + ), + const SizedBox(height: 8.0), + Text( + 'Footnote', + style: tertiaryTypography.footnote + .copyWith(fontWeight: MacosFontWeight.w590), + ), + const SizedBox(height: 24.0), + Text( + 'Caption1', + style: tertiaryTypography.caption1, + ), + const SizedBox(height: 8.0), + Text( + 'Caption1', + style: tertiaryTypography.caption1 + .copyWith(fontWeight: MacosFontWeight.w510), + ), + const SizedBox(height: 24.0), + Text( + 'Caption2', + style: tertiaryTypography.caption2, + ), + const SizedBox(height: 8.0), + Text( + 'Caption2', + style: tertiaryTypography.caption2 + .copyWith(fontWeight: MacosFontWeight.w590), + ), + ], + ), + ], + ), + ), + ], + ); + }, + ), + ], + ); + } +} diff --git a/example/lib/platform_menus.dart b/example/lib/platform_menus.dart new file mode 100644 index 00000000..91ba6a7c --- /dev/null +++ b/example/lib/platform_menus.dart @@ -0,0 +1,52 @@ +import 'package:flutter/foundation.dart' show kIsWeb; +import 'dart:io' as io; + +import 'package:flutter/widgets.dart' + show + PlatformMenu, + PlatformMenuItem, + PlatformProvidedMenuItem, + PlatformProvidedMenuItemType; + +List menuBarItems() { + if (kIsWeb) { + return []; + } else { + if (io.Platform.isMacOS) { + return const [ + PlatformMenu( + label: 'macos_ui Widget Gallery', + menus: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.about, + ), + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.quit, + ), + ], + ), + PlatformMenu( + label: 'View', + menus: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.toggleFullScreen, + ), + ], + ), + PlatformMenu( + label: 'Window', + menus: [ + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.minimizeWindow, + ), + PlatformProvidedMenuItem( + type: PlatformProvidedMenuItemType.zoomWindow, + ), + ], + ), + ]; + } else { + return []; + } + } +} diff --git a/example/lib/widgets/widget_text_title1.dart b/example/lib/widgets/widget_text_title1.dart new file mode 100644 index 00000000..77ab7314 --- /dev/null +++ b/example/lib/widgets/widget_text_title1.dart @@ -0,0 +1,30 @@ +import 'package:flutter/cupertino.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class WidgetTextTitle1 extends StatelessWidget { + const WidgetTextTitle1({super.key, required this.widgetName}); + + final String widgetName; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: MacosColors.systemGrayColor.withOpacity(0.5), + borderRadius: BorderRadius.circular(4.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + ), + child: Text( + widgetName, + style: MacosTypography.of(context) + .title1 + .copyWith(fontFamily: GoogleFonts.jetBrainsMono().fontFamily), + ), + ), + ); + } +} diff --git a/example/lib/widgets/widget_text_title2.dart b/example/lib/widgets/widget_text_title2.dart new file mode 100644 index 00000000..8da1135d --- /dev/null +++ b/example/lib/widgets/widget_text_title2.dart @@ -0,0 +1,30 @@ +import 'package:flutter/cupertino.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:macos_ui/macos_ui.dart'; + +class WidgetTextTitle2 extends StatelessWidget { + const WidgetTextTitle2({super.key, required this.widgetName}); + + final String widgetName; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: MacosColors.systemGrayColor.withOpacity(0.5), + borderRadius: BorderRadius.circular(4.0), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + ), + child: Text( + widgetName, + style: MacosTypography.of(context) + .title2 + .copyWith(fontFamily: GoogleFonts.jetBrainsMono().fontFamily), + ), + ), + ); + } +} diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 722b1dd3..0179c12c 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,13 @@ import FlutterMacOS import Foundation import macos_ui +import macos_window_utils +import path_provider_foundation +import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) + MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/example/macos/Podfile b/example/macos/Podfile index 049abe29..7ed4260c 100644 --- a/example/macos/Podfile +++ b/example/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.14.6' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 67acca3c..2fa81174 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -2,21 +2,40 @@ PODS: - FlutterMacOS (1.0.0) - macos_ui (0.1.0): - FlutterMacOS + - macos_window_utils (1.0.0): + - FlutterMacOS + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - macos_ui (from `Flutter/ephemeral/.symlinks/plugins/macos_ui/macos`) + - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral macos_ui: :path: Flutter/ephemeral/.symlinks/plugins/macos_ui/macos + macos_window_utils: + :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca + macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 + path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 -PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 +PODFILE CHECKSUM: ff0a9a3ce75ee73f200ca7e2f47745698c917ef9 COCOAPODS: 1.11.3 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj index e7c1634c..2c562a4e 100644 --- a/example/macos/Runner.xcodeproj/project.pbxproj +++ b/example/macos/Runner.xcodeproj/project.pbxproj @@ -61,7 +61,7 @@ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; tabWidth = 2; }; 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; @@ -94,7 +94,6 @@ 94ECD9F878BC8EB5F0E7094E /* Pods-Runner.release.xcconfig */, AFB798A3289226D0E5AB9985 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -405,7 +404,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -484,7 +483,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -531,7 +530,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.14.6; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f1..0a0928ee 100644 --- a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { "images" : [ { - "size" : "16x16", - "idiom" : "mac", "filename" : "app_icon_16.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" }, { - "size" : "16x16", + "filename" : "app_icon_32 1.png", "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" + "scale" : "2x", + "size" : "16x16" }, { - "size" : "32x32", - "idiom" : "mac", "filename" : "app_icon_32.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" }, { - "size" : "32x32", - "idiom" : "mac", "filename" : "app_icon_64.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" }, { - "size" : "128x128", - "idiom" : "mac", "filename" : "app_icon_128.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" }, { - "size" : "128x128", + "filename" : "app_icon_256 1.png", "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" + "scale" : "2x", + "size" : "128x128" }, { - "size" : "256x256", - "idiom" : "mac", "filename" : "app_icon_256.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" }, { - "size" : "256x256", + "filename" : "app_icon_512 1.png", "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" + "scale" : "2x", + "size" : "256x256" }, { - "size" : "512x512", - "idiom" : "mac", "filename" : "app_icon_512.png", - "scale" : "1x" + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" }, { - "size" : "512x512", - "idiom" : "mac", "filename" : "app_icon_1024.png", - "scale" : "2x" + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 3c4935a7..82b6f9d9 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index ed4cc164..13b35eba 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 483be613..0a3f5fa4 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256 1.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256 1.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256 1.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bcbf36df..bdb57226 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32 1.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32 1.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32 1.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 9c0a6528..f083318e 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512 1.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512 1.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512 1.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index e71a7261..326c0e72 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 8a31fe2d..2f1632cf 100644 Binary files a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index dddb8a30..c946719a 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift index 22fb7aee..2722837e 100644 --- a/example/macos/Runner/MainFlutterWindow.swift +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -1,82 +1,15 @@ import Cocoa import FlutterMacOS -class BlurryContainerViewController: NSViewController { - let flutterViewController = FlutterViewController() - - init() { - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError() - } - - override func loadView() { - let blurView = NSVisualEffectView() - blurView.autoresizingMask = [.width, .height] - blurView.blendingMode = .behindWindow - blurView.state = .active - if #available(macOS 10.14, *) { - blurView.material = .sidebar - } - self.view = blurView - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.addChild(flutterViewController) - - flutterViewController.view.frame = self.view.bounds - flutterViewController.backgroundColor = .clear - flutterViewController.view.autoresizingMask = [.width, .height] - self.view.addSubview(flutterViewController.view) - } -} - -class MainFlutterWindow: NSWindow, NSWindowDelegate { +class MainFlutterWindow: NSWindow { override func awakeFromNib() { - delegate = self - let blurryContainerViewController = BlurryContainerViewController() + let flutterViewController = FlutterViewController.init() let windowFrame = self.frame - self.contentViewController = blurryContainerViewController + self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) - if #available(macOS 10.13, *) { - let customToolbar = NSToolbar() - customToolbar.showsBaselineSeparator = false - self.toolbar = customToolbar - } - self.titleVisibility = .hidden - self.titlebarAppearsTransparent = true - if #available(macOS 11.0, *) { - // Use .expanded if the app will have a title bar, else use .unified - self.toolbarStyle = .unified - } - - self.isMovableByWindowBackground = true - self.styleMask.insert(NSWindow.StyleMask.fullSizeContentView) - - self.isOpaque = false - self.backgroundColor = .clear - - RegisterGeneratedPlugins(registry: blurryContainerViewController.flutterViewController) + RegisterGeneratedPlugins(registry: flutterViewController) super.awakeFromNib() } - - // Hides the toolbar when in fullscreen mode - func window(_ window: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions { - - return [.autoHideToolbar, .autoHideMenuBar, .fullScreen] - } - - func windowWillEnterFullScreen(_ notification: Notification) { - self.toolbar?.isVisible = false - } - - func windowDidExitFullScreen(_ notification: Notification) { - self.toolbar?.isVisible = true - } } diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..5418c9f5 --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import FlutterMacOS +import Cocoa +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/example/pubspec.lock b/example/pubspec.lock index cf8e59b8..dc586a2a 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -37,10 +37,18 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -57,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -66,46 +90,83 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: e20ff62b158b96f392bfc8afe29dee1503c94fbea2cbe8186fd59b756b8ae982 + url: "https://pub.dev" + source: hosted + version: "5.1.0" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" macos_ui: dependency: "direct main" description: path: ".." relative: true source: path - version: "1.12.2" + version: "2.0.0" + macos_window_utils: + dependency: transitive + description: + name: macos_window_utils + sha256: b78a210aa70ca7ccad6e7b7b810fb4689c507f4a46e299214900b2a1eb70ea23 + url: "https://pub.dev" + source: hosted + version: "1.1.3" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -118,10 +179,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" nested: dependency: transitive description: @@ -134,10 +195,82 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + url: "https://pub.dev" + source: hosted + version: "2.0.15" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" + source: hosted + version: "2.1.11" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "2.1.7" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" provider: dependency: "direct main" description: @@ -195,10 +328,82 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" + source: hosted + version: "0.5.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" + url: "https://pub.dev" + source: hosted + version: "6.1.12" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + url: "https://pub.dev" + source: hosted + version: "6.0.36" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + url: "https://pub.dev" + source: hosted + version: "2.1.3" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + url: "https://pub.dev" + source: hosted + version: "2.0.18" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -207,6 +412,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + win32: + dependency: transitive + description: + name: win32 + sha256: dfdf0136e0aa7a1b474ea133e67cb0154a0acd2599c4f3ada3b49d38d38793ee + url: "https://pub.dev" + source: hosted + version: "5.0.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e68c5535..0a008b51 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: flutter: @@ -13,12 +13,14 @@ dependencies: cupertino_icons: ^1.0.5 macos_ui: path: .. - provider: ^6.0.3 + provider: ^6.0.5 + google_fonts: ^5.1.0 + url_launcher: ^6.1.12 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^2.0.2 flutter: assets: diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index 6a71ceec..62dcc602 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -14,6 +14,9 @@ library macos_ui; +export 'package:macos_window_utils/macos/ns_window_delegate.dart'; +export 'package:macos_window_utils/macos_window_utils.dart'; + export 'src/buttons/back_button.dart'; export 'src/buttons/checkbox.dart'; export 'src/buttons/disclosure_button.dart'; @@ -29,6 +32,7 @@ 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/enums/control_size.dart'; export 'src/fields/search_field.dart'; export 'src/fields/text_field.dart'; export 'src/icon/image_icon.dart'; @@ -37,7 +41,6 @@ export 'src/indicators/capacity_indicators.dart'; export 'src/indicators/progress_indicators.dart'; export 'src/indicators/rating_indicator.dart'; export 'src/indicators/relevance_indicator.dart'; -export 'src/layout/scrollbar.dart'; export 'src/indicators/slider.dart'; export 'src/labels/label.dart'; export 'src/labels/tooltip.dart'; @@ -45,6 +48,7 @@ export 'src/layout/content_area.dart'; export 'src/layout/macos_list_tile.dart'; export 'src/layout/resizable_pane.dart'; export 'src/layout/scaffold.dart'; +export 'src/layout/scrollbar.dart'; export 'src/layout/sidebar/sidebar.dart'; export 'src/layout/sidebar/sidebar_item.dart'; export 'src/layout/sidebar/sidebar_items.dart'; @@ -60,8 +64,10 @@ export 'src/layout/toolbar/toolbar_overflow_menu.dart'; export 'src/layout/toolbar/toolbar_overflow_menu_item.dart'; export 'src/layout/toolbar/toolbar_popup.dart'; export 'src/layout/toolbar/toolbar_spacer.dart'; +export 'src/layout/wallpaper_tinted_area.dart'; export 'src/layout/window.dart'; export 'src/macos_app.dart'; +export 'src/macos_window_utils_config.dart'; export 'src/selectors/color_well.dart'; export 'src/selectors/date_picker.dart'; export 'src/selectors/time_picker.dart'; diff --git a/lib/src/buttons/back_button.dart b/lib/src/buttons/back_button.dart index 5bce067b..605356a4 100644 --- a/lib/src/buttons/back_button.dart +++ b/lib/src/buttons/back_button.dart @@ -196,7 +196,7 @@ class MacosBackButtonState extends State : _isHovered ? hoverColor : fillColor, - borderRadius: BorderRadius.circular(7), + borderRadius: const BorderRadius.all(Radius.circular(7)), ), child: Icon( CupertinoIcons.back, diff --git a/lib/src/buttons/checkbox.dart b/lib/src/buttons/checkbox.dart index 93e0b1c0..d7f8bcc3 100644 --- a/lib/src/buttons/checkbox.dart +++ b/lib/src/buttons/checkbox.dart @@ -15,7 +15,7 @@ class MacosCheckbox extends StatelessWidget { super.key, required this.value, required this.onChanged, - this.size = 16.0, + this.size = 14.0, this.activeColor, this.disabledColor = CupertinoColors.quaternaryLabel, this.offBorderColor = CupertinoColors.tertiaryLabel, @@ -106,20 +106,57 @@ class MacosCheckbox extends StatelessWidget { : activeColor ?? theme.primaryColor, context, ), - borderRadius: BorderRadius.circular(4.0), + borderRadius: const BorderRadius.all(Radius.circular(4.0)), ) - : BoxDecoration( - color: isLight ? null : CupertinoColors.tertiaryLabel, - border: Border.all( - style: isLight ? BorderStyle.solid : BorderStyle.none, - width: 0.5, - color: MacosDynamicColor.resolve( - offBorderColor, - context, + : isLight + ? ShapeDecoration( + gradient: LinearGradient( + begin: const Alignment(0.0, -1.0), + end: const Alignment(0, 0), + colors: [ + Colors.white.withOpacity(0.85), + Colors.white.withOpacity(1.0), + ], + ), + shadows: const [ + BoxShadow( + color: Color(0x3F000000), + blurRadius: 1, + blurStyle: BlurStyle.inner, + offset: Offset(0, 0), + spreadRadius: 0.0, + ), + ], + shape: RoundedRectangleBorder( + side: BorderSide( + width: 0.25, + color: Colors.black.withOpacity(0.35000000596046448), + ), + borderRadius: + const BorderRadius.all(Radius.circular(3.5)), + ), + ) + : ShapeDecoration( + gradient: LinearGradient( + begin: const Alignment(0.0, -1.0), + end: const Alignment(0, 1), + colors: [ + Colors.white.withOpacity(0.14000000059604645), + Colors.white.withOpacity(0.2800000011920929), + ], + ), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(3)), + ), + shadows: const [ + BoxShadow( + color: Color(0x3F000000), + blurRadius: 1, + offset: Offset(0, 0), + spreadRadius: 0, + ), + ], ), - ), - borderRadius: BorderRadius.circular(4.0), - ), child: Icon( isDisabled || value == false ? null diff --git a/lib/src/buttons/disclosure_button.dart b/lib/src/buttons/disclosure_button.dart index 3a8a3d6b..5cef6e0e 100644 --- a/lib/src/buttons/disclosure_button.dart +++ b/lib/src/buttons/disclosure_button.dart @@ -177,7 +177,7 @@ class MacosDisclosureButtonState extends State ? const MacosColor(0xff3C383C) : const MacosColor(0xffE5E5E5) : fillColor, - borderRadius: BorderRadius.circular(7), + borderRadius: const BorderRadius.all(Radius.circular(7)), ), child: RotatedBox( quarterTurns: widget.isPressed ? 1 : 3, diff --git a/lib/src/buttons/help_button.dart b/lib/src/buttons/help_button.dart index 0c5d5ece..f825baf5 100644 --- a/lib/src/buttons/help_button.dart +++ b/lib/src/buttons/help_button.dart @@ -190,6 +190,8 @@ class HelpButtonState extends State constraints: const BoxConstraints( minWidth: 20, minHeight: 20, + maxWidth: 20, + maxHeight: 20, ), child: FadeTransition( opacity: _opacityAnimation, @@ -212,16 +214,14 @@ class HelpButtonState extends State ), ], ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Align( - alignment: widget.alignment, - widthFactor: 1.0, - heightFactor: 1.0, - child: Icon( - CupertinoIcons.question, - color: foregroundColor, - ), + child: Align( + alignment: widget.alignment, + widthFactor: 1.0, + heightFactor: 1.0, + child: Icon( + CupertinoIcons.question, + color: foregroundColor, + size: 13, ), ), ), diff --git a/lib/src/buttons/icon_button.dart b/lib/src/buttons/icon_button.dart index 9b339aa7..88e8ef20 100644 --- a/lib/src/buttons/icon_button.dart +++ b/lib/src/buttons/icon_button.dart @@ -253,7 +253,7 @@ class MacosIconButtonState extends State borderRadius: widget.borderRadius != null ? widget.borderRadius : widget.shape == BoxShape.rectangle - ? BorderRadius.circular(7.0) + ? const BorderRadius.all(Radius.circular(7)) : null, color: !enabled ? disabledColor diff --git a/lib/src/buttons/popup_button.dart b/lib/src/buttons/popup_button.dart index a08cb113..8158e63c 100644 --- a/lib/src/buttons/popup_button.dart +++ b/lib/src/buttons/popup_button.dart @@ -1091,8 +1091,7 @@ class _MacosPopupButtonState extends State> void _handleTap() { final TextDirection? textDirection = Directionality.maybeOf(context); - const EdgeInsetsGeometry menuMargin = - EdgeInsetsDirectional.only(start: 4.0, end: 4.0); + const EdgeInsetsGeometry menuMargin = EdgeInsets.symmetric(horizontal: 4.0); final List<_MenuItem> menuItems = <_MenuItem>[ for (int index = 0; index < widget.items!.length; index += 1) @@ -1239,7 +1238,7 @@ class _MacosPopupButtonState extends State> boxShadow: [ BoxShadow( color: buttonStyles.borderColor, - offset: const Offset(0, .5), + offset: const Offset(0, 0.5), blurRadius: 0.2, spreadRadius: 0, ), @@ -1251,7 +1250,7 @@ class _MacosPopupButtonState extends State> ), borderRadius: _kBorderRadius, ), - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 2.0, 0.0), + padding: const EdgeInsets.only(left: 8.0, right: 2.0), height: _kPopupButtonHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/src/buttons/pulldown_button.dart b/lib/src/buttons/pulldown_button.dart index dbe7e0d1..91de97a7 100644 --- a/lib/src/buttons/pulldown_button.dart +++ b/lib/src/buttons/pulldown_button.dart @@ -807,8 +807,7 @@ class _MacosPulldownButtonState extends State void _handleTap() { final TextDirection? textDirection = Directionality.maybeOf(context); - const EdgeInsetsGeometry menuMargin = - EdgeInsetsDirectional.only(start: 4.0, end: 4.0); + const EdgeInsetsGeometry menuMargin = EdgeInsets.symmetric(horizontal: 4.0); final List<_MenuItem> menuItems = <_MenuItem>[ for (int index = 0; index < widget.items!.length; index += 1) @@ -904,7 +903,7 @@ class _MacosPulldownButtonState extends State boxShadow: [ BoxShadow( color: buttonStyles.borderColor, - offset: const Offset(0, .5), + offset: const Offset(0, 0.5), blurRadius: 0.2, spreadRadius: 0, ), @@ -913,7 +912,7 @@ class _MacosPulldownButtonState extends State color: buttonStyles.bgColor, borderRadius: borderRadius, ), - padding: const EdgeInsets.fromLTRB(8.0, 0.0, 2.0, 0.0), + padding: const EdgeInsets.only(left: 8.0, right: 2.0), height: buttonHeight, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/lib/src/buttons/push_button.dart b/lib/src/buttons/push_button.dart index 6f759c1d..19180016 100644 --- a/lib/src/buttons/push_button.dart +++ b/lib/src/buttons/push_button.dart @@ -5,36 +5,120 @@ 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, -} - -const EdgeInsetsGeometry _kSmallButtonPadding = EdgeInsets.symmetric( - vertical: 3.0, - horizontal: 8.0, +const _kMiniButtonSize = Size(26.0, 11.0); +const _kSmallButtonSize = Size(39.0, 14.0); +const _kRegularButtonSize = Size(60.0, 18.0); +const _kLargeButtonSize = Size(48.0, 26.0); + +const _kMiniButtonPadding = EdgeInsets.only(left: 6.0, right: 6.0, bottom: 1.0); +const _kSmallButtonPadding = EdgeInsets.symmetric( + vertical: 1.0, + horizontal: 7.0, ); -const EdgeInsetsGeometry _kLargeButtonPadding = EdgeInsets.symmetric( - vertical: 6.0, - horizontal: 8.0, +const _kRegularButtonPadding = EdgeInsets.only( + left: 8.0, + right: 8.0, + top: 1.0, + bottom: 4.0, ); +const _kLargeButtonPadding = EdgeInsets.only( + right: 8.0, + left: 8.0, + bottom: 1.0, +); + +const _kMiniButtonRadius = BorderRadius.all(Radius.circular(2.0)); +const _kSmallButtonRadius = BorderRadius.all(Radius.circular(2.0)); +const _kRegularButtonRadius = BorderRadius.all(Radius.circular(5.0)); +const _kLargeButtonRadius = BorderRadius.all(Radius.circular(7.0)); + +/// Shortcuts for various [PushButton] properties based on the [ControlSize]. +extension PushButtonControlSizeX on ControlSize { + /// Determines the padding of the button's text. + EdgeInsetsGeometry get padding { + switch (this) { + case ControlSize.mini: + return _kMiniButtonPadding; + case ControlSize.small: + return _kSmallButtonPadding; + case ControlSize.regular: + return _kRegularButtonPadding; + case ControlSize.large: + return _kLargeButtonPadding; + } + } + + /// Determines the button's border radius. + BorderRadiusGeometry get borderRadius { + switch (this) { + case ControlSize.mini: + return _kMiniButtonRadius; + case ControlSize.small: + return _kSmallButtonRadius; + case ControlSize.regular: + return _kRegularButtonRadius; + case ControlSize.large: + return _kLargeButtonRadius; + } + } + + /// Determines the styling of the button's text. + TextStyle textStyle(TextStyle baseStyle) { + switch (this) { + case ControlSize.mini: + return baseStyle.copyWith(fontSize: 9.0); + case ControlSize.small: + return baseStyle.copyWith(fontSize: 11.0); + case ControlSize.regular: + return baseStyle.copyWith(fontSize: 13.0); + case ControlSize.large: + return baseStyle; + } + } -const BorderRadius _kSmallButtonRadius = BorderRadius.all(Radius.circular(5.0)); -const BorderRadius _kLargeButtonRadius = BorderRadius.all(Radius.circular(7.0)); + /// Determines the button's minimum size. + BoxConstraints get constraints { + switch (this) { + case ControlSize.mini: + return BoxConstraints( + minHeight: _kMiniButtonSize.height, + minWidth: _kMiniButtonSize.width, + ); + case ControlSize.small: + return BoxConstraints( + minHeight: _kSmallButtonSize.height, + minWidth: _kSmallButtonSize.width, + ); + case ControlSize.regular: + return BoxConstraints( + minHeight: _kRegularButtonSize.height, + minWidth: _kRegularButtonSize.width, + ); + case ControlSize.large: + return BoxConstraints( + minHeight: _kLargeButtonSize.height, + minWidth: _kLargeButtonSize.width, + ); + } + } +} /// {@template pushButton} -/// A macOS-style button. +/// A control that initiates an action. +/// +/// Push Buttons are the standard button type in macOS. +/// +/// Reference: +/// * [Button (SwiftUI)](https://developer.apple.com/documentation/SwiftUI/Button) +/// * [NSButton (AppKit)](https://developer.apple.com/documentation/appkit/nsbutton) +/// * [Buttons (Human Interface Guidelines)](https://developer.apple.com/design/human-interface-guidelines/buttons) /// {@endtemplate} class PushButton extends StatefulWidget { /// {@macro pushButton} const PushButton({ super.key, required this.child, - required this.buttonSize, + required this.controlSize, this.padding, this.color, this.disabledColor, @@ -44,7 +128,7 @@ class PushButton extends StatefulWidget { this.alignment = Alignment.center, this.semanticLabel, this.mouseCursor = SystemMouseCursors.basic, - this.isSecondary, + this.secondary, }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)); @@ -55,12 +139,8 @@ class PushButton extends StatefulWidget { /// The size of the button. /// - /// Must be either [ButtonSize.small] or [ButtonSize.large]. /// - /// Small buttons have a `padding` of [_kSmallButtonPadding] and a - /// `borderRadius` of [_kSmallButtonRadius]. Large buttons have a `padding` - /// of [_kLargeButtonPadding] and a `borderRadius` of [_kLargeButtonRadius]. - final ButtonSize buttonSize; + final ControlSize controlSize; /// The amount of space to surround the child inside the bounds of the button. /// @@ -116,8 +196,8 @@ class PushButton extends StatefulWidget { /// Whether the button is used as a secondary action button (e.g. Cancel buttons in dialogs) /// /// Sets its background color to [PushButtonThemeData]'s [secondaryColor] attributes (defaults - /// are gray colors). Can still be overriden if the [color] attribute is non-null. - final bool? isSecondary; + /// are gray colors). Can still be overridden if the [color] attribute is non-null. + final bool? secondary; /// Whether the button is enabled or disabled. Buttons are disabled by default. To /// enable a button, set its [onPressed] property to a non-null value. @@ -126,7 +206,7 @@ class PushButton extends StatefulWidget { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(EnumProperty('buttonSize', buttonSize)); + properties.add(EnumProperty('controlSize', controlSize)); properties.add(ColorProperty('color', color)); properties.add(ColorProperty('disabledColor', disabledColor)); properties.add(DoubleProperty('pressedOpacity', pressedOpacity)); @@ -138,7 +218,7 @@ class PushButton extends StatefulWidget { value: enabled, ifFalse: 'disabled', )); - properties.add(DiagnosticsProperty('isSecondary', isSecondary)); + properties.add(DiagnosticsProperty('secondary', secondary)); } @override @@ -224,7 +304,7 @@ class PushButtonState extends State Widget build(BuildContext context) { assert(debugCheckHasMacosTheme(context)); final bool enabled = widget.enabled; - final bool isSecondary = widget.isSecondary != null && widget.isSecondary!; + final bool isSecondary = widget.secondary != null && widget.secondary!; final MacosThemeData theme = MacosTheme.of(context); final Color backgroundColor = MacosDynamicColor.resolve( widget.color ?? @@ -234,22 +314,9 @@ class PushButtonState extends State context, ); - final Color disabledColor = MacosDynamicColor.resolve( - widget.disabledColor ?? theme.pushButtonTheme.disabledColor!, - context, - ); - - final EdgeInsetsGeometry? buttonPadding = widget.padding == null - ? widget.buttonSize == ButtonSize.small - ? _kSmallButtonPadding - : _kLargeButtonPadding - : widget.padding; - - final BorderRadiusGeometry? borderRadius = widget.borderRadius == null - ? widget.buttonSize == ButtonSize.small - ? _kSmallButtonRadius - : _kLargeButtonRadius - : widget.borderRadius; + final disabledColor = !isSecondary + ? backgroundColor.withOpacity(0.5) + : backgroundColor.withOpacity(0.25); final Color foregroundColor = widget.enabled ? textLuminance(backgroundColor) @@ -257,8 +324,7 @@ class PushButtonState extends State ? const Color.fromRGBO(255, 255, 255, 0.25) : const Color.fromRGBO(0, 0, 0, 0.25); - final TextStyle textStyle = - theme.typography.headline.copyWith(color: foregroundColor); + final baseStyle = theme.typography.body.copyWith(color: foregroundColor); return MouseRegion( cursor: widget.mouseCursor!, @@ -272,25 +338,25 @@ class PushButtonState extends State button: true, label: widget.semanticLabel, child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 49, - minHeight: 20, - ), + constraints: widget.controlSize.constraints, child: FadeTransition( opacity: _opacityAnimation, child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: borderRadius, - color: !enabled ? disabledColor : backgroundColor, + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: widget.controlSize.borderRadius, + ), + // color: !enabled ? disabledColor : backgroundColor, + color: enabled ? backgroundColor : disabledColor, ), child: Padding( - padding: buttonPadding!, + padding: widget.controlSize.padding, child: Align( alignment: widget.alignment, widthFactor: 1.0, heightFactor: 1.0, child: DefaultTextStyle( - style: textStyle, + style: widget.controlSize.textStyle(baseStyle), child: widget.child, ), ), diff --git a/lib/src/buttons/segmented_control.dart b/lib/src/buttons/segmented_control.dart index 411fd8d1..45f36539 100644 --- a/lib/src/buttons/segmented_control.dart +++ b/lib/src/buttons/segmented_control.dart @@ -54,8 +54,8 @@ class _MacosSegmentedControlState extends State { const Color(0xFFDBDCDE), const Color(0xFF4F5155), ), - offset: const Offset(0, .5), - spreadRadius: .5, + offset: const Offset(0, 0.5), + spreadRadius: 0.5, ), ], borderRadius: const BorderRadius.all( diff --git a/lib/src/buttons/switch.dart b/lib/src/buttons/switch.dart index 6dd4375e..25d7f637 100644 --- a/lib/src/buttons/switch.dart +++ b/lib/src/buttons/switch.dart @@ -1,23 +1,48 @@ -import 'package:flutter/cupertino.dart' as c; -import 'package:flutter/foundation.dart'; +import 'dart:ui'; + import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +const _kDefaultBorderColor = CupertinoDynamicColor.withBrightness( + color: MacosColor.fromRGBO(215, 215, 215, 1.0), + darkColor: MacosColor.fromRGBO(101, 101, 101, 1.0), +); + +const _kDefaultTrackColor = CupertinoDynamicColor.withBrightness( + color: MacosColor.fromRGBO(228, 226, 228, 1.0), + darkColor: MacosColor.fromRGBO(66, 66, 66, 1.0), +); + +// Dark color might be Color.fromRGBO(255, 255, 255, 0.721)?? +const _kDefaultKnobColor = CupertinoDynamicColor.withBrightness( + color: MacosColors.white, + darkColor: MacosColor.fromRGBO(207, 207, 207, 1.0), +); + /// {@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. +/// A switch is a control that offers a binary choice between two mutually +/// exclusive states — on and off. +/// +/// A switch shows that it's on when the [activeColor] is visible and off when +/// the [trackColor] is visible. +/// +/// Additional Reference: +/// * [Toggles (Human Interface Guidelines)](https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles) +/// * [Toggles (Apple Developer)](https://developer.apple.com/documentation/swiftui/toggle) /// {@endtemplate} -class MacosSwitch extends StatelessWidget { +class MacosSwitch extends StatefulWidget { /// {@macro macosSwitch} const MacosSwitch({ super.key, required this.value, + this.size = ControlSize.regular, required this.onChanged, this.dragStartBehavior = DragStartBehavior.start, this.activeColor, this.trackColor, + this.knobColor, this.semanticLabel, }); @@ -26,6 +51,13 @@ class MacosSwitch extends StatelessWidget { /// Must not be null. final bool value; + /// The size of the switch, which is [ControlSize.regular] by default. + /// + /// Allowable sizes are [ControlSize.mini], [ControlSize.small], and + /// [ControlSize.regular]. If [ControlSize.large] is used, the switch will + /// size itself as a [ControlSize.regular] switch. + final ControlSize size; + /// Called when the user toggles with switch on or off. /// /// The switch passes the new value to the callback but does not actually @@ -39,7 +71,7 @@ class MacosSwitch extends StatelessWidget { /// gets rebuilt; for example: /// /// ```dart - /// Switch( + /// MacosSwitch( /// value: _giveVerse, /// onChanged: (bool newValue) { /// setState(() { @@ -53,19 +85,25 @@ class MacosSwitch extends StatelessWidget { /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior} final DragStartBehavior dragStartBehavior; - /// The color to use when this switch is on. + /// The color to use for the track when this switch is on. /// /// Defaults to [MacosThemeData.primaryColor] when null. - final Color? activeColor; + final MacosColor? activeColor; - /// The color to use for the background when the switch is off. + /// The color to use for track when this switch is off. /// - /// Defaults to [CupertinoColors.secondarySystemFill] when null. - final Color? trackColor; + /// Defaults to [MacosTheme.primaryColor] when null. + final MacosColor? trackColor; + + /// The color to use for the switch's knob. + final MacosColor? knobColor; /// The semantic label used by screen readers. final String? semanticLabel; + @override + State createState() => _MacosSwitchState(); + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -74,6 +112,7 @@ class MacosSwitch extends StatelessWidget { value: value, ifFalse: 'unchecked', )); + properties.add(EnumProperty('size', size)); properties.add(EnumProperty('dragStartBehavior', dragStartBehavior)); properties.add(FlagProperty( 'enabled', @@ -82,26 +121,606 @@ class MacosSwitch extends StatelessWidget { )); properties.add(ColorProperty('activeColor', activeColor)); properties.add(ColorProperty('trackColor', trackColor)); + properties.add(ColorProperty('knobColor', knobColor)); properties.add(StringProperty('semanticLabel', semanticLabel)); } +} + +class _MacosSwitchState extends State + with TickerProviderStateMixin { + late TapGestureRecognizer _tap; + late HorizontalDragGestureRecognizer _drag; + + late AnimationController _positionController; + late CurvedAnimation position; + + late AnimationController _reactionController; + late Animation _reaction; + + bool get isInteractive => widget.onChanged != null; + + // A non-null boolean value that changes to true at the end of a drag if the + // switch must be animated to the position indicated by the widget's value. + bool needsPositionAnimation = false; + + @override + void initState() { + super.initState(); + + _tap = TapGestureRecognizer() + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..onTap = _handleTap + ..onTapCancel = _handleTapCancel; + _drag = HorizontalDragGestureRecognizer() + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..dragStartBehavior = widget.dragStartBehavior; + + _positionController = AnimationController( + duration: _kToggleDuration, + value: widget.value ? 1.0 : 0.0, + vsync: this, + ); + position = CurvedAnimation( + parent: _positionController, + curve: Curves.linear, + ); + _reactionController = AnimationController( + duration: _kReactionDuration, + vsync: this, + ); + _reaction = CurvedAnimation( + parent: _reactionController, + curve: Curves.ease, + ); + } + + @override + void didUpdateWidget(MacosSwitch oldWidget) { + super.didUpdateWidget(oldWidget); + _drag.dragStartBehavior = widget.dragStartBehavior; + + if (needsPositionAnimation || oldWidget.value != widget.value) { + _resumePositionAnimation(isLinear: needsPositionAnimation); + } + } + + // `isLinear` must be true if the position animation is trying to move the + // knob to the closest end after the most recent drag animation, so the curve + // does not change when the controller's value is not 0 or 1. + // + // It can be set to false when it's an implicit animation triggered by + // widget.value changes. + void _resumePositionAnimation({bool isLinear = true}) { + needsPositionAnimation = false; + position + ..curve = isLinear ? Curves.linear : Curves.ease + ..reverseCurve = isLinear ? Curves.linear : Curves.ease.flipped; + if (widget.value) { + _positionController.forward(); + } else { + _positionController.reverse(); + } + } + + void _handleTapDown(TapDownDetails details) { + if (isInteractive) { + needsPositionAnimation = false; + } + _reactionController.forward(); + } + + void _handleTap() { + if (isInteractive) { + widget.onChanged!(!widget.value); + } + } + + void _handleTapUp(TapUpDetails details) { + if (isInteractive) { + needsPositionAnimation = false; + _reactionController.reverse(); + } + } + + void _handleTapCancel() { + if (isInteractive) { + _reactionController.reverse(); + } + } + + void _handleDragStart(DragStartDetails details) { + if (isInteractive) { + needsPositionAnimation = false; + _reactionController.forward(); + } + } + + void _handleDragUpdate(DragUpdateDetails details) { + if (isInteractive) { + position + ..curve = Curves.linear + ..reverseCurve = Curves.linear; + final double delta = details.primaryDelta! / widget.size.trackInnerLength; + switch (Directionality.of(context)) { + case TextDirection.rtl: + _positionController.value -= delta; + break; + case TextDirection.ltr: + _positionController.value += delta; + break; + } + } + } + + void _handleDragEnd(DragEndDetails details) { + // Deferring the animation to the next build phase. + setState(() => needsPositionAnimation = true); + // Call onChanged when the user's intent to change value is clear. + if (position.value >= 0.5 != widget.value) { + widget.onChanged!(!widget.value); + } + _reactionController.reverse(); + } + + @override + void dispose() { + _tap.dispose(); + _drag.dispose(); + + _positionController.dispose(); + _reactionController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { assert(debugCheckHasMacosTheme(context)); final MacosThemeData theme = MacosTheme.of(context); + MacosColor borderColor = + MacosDynamicColor.resolve(_kDefaultBorderColor, context).toMacosColor(); + MacosColor activeColor = MacosColor(MacosDynamicColor.resolve( + widget.activeColor ?? theme.primaryColor, + context, + ).value); + MacosColor trackColor = widget.trackColor ?? + MacosDynamicColor.resolve(_kDefaultTrackColor, context).toMacosColor(); + MacosColor knobColor = widget.knobColor ?? + MacosDynamicColor.resolve(_kDefaultKnobColor, context).toMacosColor(); + + // Shot in the dark to try and get the border color correct for each + // possible color + if (widget.value) { + if (theme.brightness.isDark) { + borderColor.computeLuminance() > 0.5 + ? borderColor = MacosColor.darken(activeColor, 20) + : borderColor = MacosColor.lighten(activeColor, 20); + } else { + borderColor.computeLuminance() > 0.5 + ? borderColor = MacosColor.darken(activeColor, 20) + : borderColor = MacosColor.lighten(activeColor, 20); + } + } + return Semantics( - label: semanticLabel, - checked: value, - child: c.CupertinoSwitch( - value: value, - onChanged: onChanged, - dragStartBehavior: dragStartBehavior, - activeColor: MacosDynamicColor.resolve( - activeColor ?? theme.primaryColor, - context, - ), + label: widget.semanticLabel, + checked: widget.value, + child: _MacosSwitchRenderObjectWidget( + value: widget.value, + size: widget.size, + activeColor: activeColor, trackColor: trackColor, + knobColor: knobColor, + borderColor: borderColor, + onChanged: widget.onChanged, + textDirection: Directionality.of(context), + state: this, ), ); } } + +class _MacosSwitchRenderObjectWidget extends LeafRenderObjectWidget { + const _MacosSwitchRenderObjectWidget({ + required this.value, + required this.size, + required this.activeColor, + required this.trackColor, + required this.knobColor, + required this.borderColor, + required this.onChanged, + required this.textDirection, + required this.state, + }); + final bool value; + final ControlSize size; + final MacosColor activeColor; + final MacosColor trackColor; + final MacosColor knobColor; + final MacosColor borderColor; + final ValueChanged? onChanged; + final TextDirection textDirection; + final _MacosSwitchState state; + + @override + _RenderMacosSwitch createRenderObject(BuildContext context) { + return _RenderMacosSwitch( + value: value, + size: size, + activeColor: activeColor, + trackColor: trackColor, + knobColor: knobColor, + borderColor: borderColor, + onChanged: onChanged, + textDirection: textDirection, + state: state, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderMacosSwitch renderObject, + ) { + assert(renderObject._state == state); + renderObject + ..value = value + ..controlSize = size + ..activeColor = activeColor + ..trackColor = trackColor + ..knobColor = knobColor + ..borderColor = borderColor + ..onChanged = onChanged + ..textDirection = textDirection; + } +} + +const Size _kMiniTrackSize = Size(26.0, 15.0); +const Size _kSmallTrackSize = Size(32.0, 18.0); +const Size _kRegularTrackSize = Size(38.0, 22.0); + +const double _kMiniKnobSize = 13.0; +const double _kSmallKnobSize = 16.0; +const double _kRegularKnobSize = 20.0; + +// Shortcuts for details about how to create the switch, based on the control +// size. +extension _ControlSizeX on ControlSize { + Size get trackSize { + switch (this) { + case ControlSize.mini: + return _kMiniTrackSize; + case ControlSize.small: + return _kSmallTrackSize; + default: + return _kRegularTrackSize; + } + } + + double get knobSize { + switch (this) { + case ControlSize.mini: + return _kMiniKnobSize; + case ControlSize.small: + return _kSmallKnobSize; + default: + return _kRegularKnobSize; + } + } + + double get knobRadius => knobSize / 2.0; + double get trackInnerStart => trackSize.height / 2.0; + double get trackInnerEnd => trackSize.width - trackInnerStart; + double get trackInnerLength => trackInnerEnd - trackInnerStart; +} + +const Duration _kReactionDuration = Duration(milliseconds: 400); +const Duration _kToggleDuration = Duration(milliseconds: 300); + +class _RenderMacosSwitch extends RenderConstrainedBox { + _RenderMacosSwitch({ + required bool value, + required ControlSize size, + required MacosColor activeColor, + required MacosColor trackColor, + required MacosColor knobColor, + required MacosColor borderColor, + required ValueChanged? onChanged, + required TextDirection textDirection, + required _MacosSwitchState state, + }) : _value = value, + _size = size, + _activeColor = activeColor, + _trackColor = trackColor, + _knobPainter = MacosSwitchKnobPainter(color: knobColor), + _borderColor = borderColor, + _onChanged = onChanged, + _textDirection = textDirection, + _state = state, + super( + additionalConstraints: BoxConstraints.tightFor( + width: size.trackSize.width, + height: size.trackSize.height, + ), + ) { + state.position.addListener(markNeedsPaint); + state._reaction.addListener(markNeedsPaint); + } + + final _MacosSwitchState _state; + + bool get value => _value; + bool _value; + set value(bool newValue) { + if (newValue == _value) { + return; + } + _value = newValue; + markNeedsSemanticsUpdate(); + } + + ControlSize get controlSize => _size; + ControlSize _size; + set controlSize(ControlSize value) { + if (value == _size) { + return; + } + _size = value; + markNeedsPaint(); + } + + MacosColor get activeColor => _activeColor; + MacosColor _activeColor; + set activeColor(MacosColor value) { + if (value == _activeColor) { + return; + } + _activeColor = value; + markNeedsPaint(); + } + + MacosColor get trackColor => _trackColor; + MacosColor _trackColor; + set trackColor(MacosColor value) { + if (value == _trackColor) { + return; + } + _trackColor = value; + markNeedsPaint(); + } + + MacosColor get knobColor => _knobPainter.color; + MacosSwitchKnobPainter _knobPainter; + set knobColor(MacosColor value) { + if (value == knobColor) { + return; + } + _knobPainter = MacosSwitchKnobPainter(color: value); + markNeedsPaint(); + } + + MacosColor get borderColor => _borderColor; + MacosColor _borderColor; + set borderColor(MacosColor value) { + if (value == borderColor) { + return; + } + _borderColor = value; + markNeedsPaint(); + } + + ValueChanged? get onChanged => _onChanged; + ValueChanged? _onChanged; + set onChanged(ValueChanged? value) { + if (value == _onChanged) { + return; + } + final bool wasInteractive = isInteractive; + _onChanged = value; + if (wasInteractive != isInteractive) { + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (value == _textDirection) { + return; + } + _textDirection = value; + markNeedsPaint(); + } + + bool get isInteractive => onChanged != null; + + @override + bool hitTestSelf(Offset position) => true; + + @override + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + assert(debugHandleEvent(event, entry)); + if (event is PointerDownEvent && isInteractive) { + _state._drag.addPointer(event); + _state._tap.addPointer(event); + } + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + if (isInteractive) { + config.onTap = _state._handleTap; + } + + config.isEnabled = isInteractive; + config.isToggled = _value; + } + + @override + void paint(PaintingContext context, Offset offset) { + final Canvas canvas = context.canvas; + final double currentValue = _state.position.value; + final trackSize = controlSize.trackSize; + final innerStart = controlSize.trackInnerStart; + final innerEnd = controlSize.trackInnerEnd; + + final double visualPosition; + switch (textDirection) { + case TextDirection.rtl: + visualPosition = 1.0 - currentValue; + break; + case TextDirection.ltr: + visualPosition = currentValue; + break; + } + + final Paint paint = Paint() + ..color = MacosColor.lerp(trackColor, activeColor, currentValue); + + final Rect trackRect = Rect.fromLTWH( + offset.dx + (size.width - trackSize.width) / 2.0, + offset.dy + (size.height - trackSize.height) / 2.0, + trackSize.width, + trackSize.height, + ); + final RRect trackRRect = RRect.fromRectAndRadius( + trackRect, + Radius.circular(trackSize.height / 2.0), + ); + canvas.drawRRect(trackRRect, paint); + canvas.drawRRect( + trackRRect, + Paint() + ..color = borderColor + ..style = PaintingStyle.stroke, + ); + + final double knobLeft = lerpDouble( + trackRect.left + innerStart - controlSize.knobRadius, + trackRect.left + innerEnd - controlSize.knobRadius, + visualPosition, + )!; + final double knobRight = lerpDouble( + trackRect.left + innerStart + controlSize.knobRadius, + trackRect.left + innerEnd + controlSize.knobRadius, + visualPosition, + )!; + final double knobCenterY = offset.dy + size.height / 2.0; + final Rect knobBounds = Rect.fromLTRB( + knobLeft, + knobCenterY - controlSize.knobRadius, + knobRight, + knobCenterY + controlSize.knobRadius, + ); + + _clipRRectLayer.layer = context.pushClipRRect( + needsCompositing, + Offset.zero, + knobBounds, + trackRRect, + (PaintingContext innerContext, Offset offset) { + _knobPainter.paint( + innerContext.canvas, + knobBounds, + visualPosition == 1.0, + ); + }, + oldLayer: _clipRRectLayer.layer, + ); + } + + final LayerHandle _clipRRectLayer = + LayerHandle(); + + @override + void dispose() { + _clipRRectLayer.layer = null; + super.dispose(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder description) { + super.debugFillProperties(description); + description.add(FlagProperty( + 'value', + value: value, + ifTrue: 'checked', + ifFalse: 'unchecked', + showName: true, + )); + description.add(FlagProperty( + 'isInteractive', + value: isInteractive, + ifTrue: 'enabled', + ifFalse: 'disabled', + showName: true, + defaultValue: true, + )); + } +} + +const List _kSwitchOffBoxShadows = [ + BoxShadow( + color: Color(0x26000000), + // offset: Offset(1, 1), + blurRadius: 8.0, + blurStyle: BlurStyle.inner, + ), + BoxShadow( + color: Color(0x0F000000), + // offset: Offset(1, 1), + blurRadius: 1.0, + blurStyle: BlurStyle.inner, + ), +]; + +const List _kSwitchOnBoxShadows = [ + BoxShadow( + color: Color(0x26000000), + // offset: Offset(-3, 1), + blurRadius: 8.0, + blurStyle: BlurStyle.inner, + ), + BoxShadow( + color: Color(0x0F000000), + // offset: Offset(-1, 1), + blurRadius: 1.0, + blurStyle: BlurStyle.inner, + ), +]; + +/// Paints a macOS-style switch knob. +/// +/// Used by [MacosSwitch]. +class MacosSwitchKnobPainter { + /// Creates an object that paints a macOS-style switch knob. + const MacosSwitchKnobPainter({required this.color}); + + /// The color of the interior of the knob. + final MacosColor color; + + /// Paints the knob onto the given canvas in the given rectangle. + void paint(Canvas canvas, Rect rect, bool isOn) { + final RRect rrect = RRect.fromRectAndRadius( + rect, + Radius.circular(rect.shortestSide / 2.0), + ); + + if (isOn) { + for (final BoxShadow shadow in _kSwitchOnBoxShadows) { + canvas.drawRRect(rrect.shift(shadow.offset), shadow.toPaint()); + } + } else { + for (final BoxShadow shadow in _kSwitchOffBoxShadows) { + canvas.drawRRect(rrect.shift(shadow.offset), shadow.toPaint()); + } + } + + canvas.drawRRect(rrect, Paint()..color = color); + } +} diff --git a/lib/src/buttons/toolbar/toolbar_icon_button.dart b/lib/src/buttons/toolbar/toolbar_icon_button.dart index 3d94a1f3..4b165a3d 100644 --- a/lib/src/buttons/toolbar/toolbar_icon_button.dart +++ b/lib/src/buttons/toolbar/toolbar_icon_button.dart @@ -77,7 +77,7 @@ class ToolBarIconButton extends ToolbarItem { if (showLabel) { iconButton = Padding( - padding: const EdgeInsets.fromLTRB(6.0, 6.0, 6.0, 0.0), + padding: const EdgeInsets.only(left: 6.0, top: 6.0, right: 6.0), child: Column( children: [ iconButton, diff --git a/lib/src/dialogs/macos_alert_dialog.dart b/lib/src/dialogs/macos_alert_dialog.dart index 057f04df..0dab2588 100644 --- a/lib/src/dialogs/macos_alert_dialog.dart +++ b/lib/src/dialogs/macos_alert_dialog.dart @@ -3,6 +3,10 @@ import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; const _kDialogBorderRadius = BorderRadius.all(Radius.circular(12.0)); +const _kDefaultDialogConstraints = BoxConstraints( + minWidth: 260, + maxWidth: 260, +); /// A macOS-style AlertDialog. /// @@ -17,14 +21,12 @@ const _kDialogBorderRadius = BorderRadius.all(Radius.circular(12.0)); /// appIcon: FlutterLogo( /// size: 56, /// ), -/// title: Text( -/// 'Alert Dialog with Primary Action', -/// ), +/// title: Text('Alert Dialog with Primary Action'), /// message: Text( /// 'This is an alert dialog with a primary action and no secondary action', /// ), /// primaryButton: PushButton( -/// buttonSize: ButtonSize.large, +/// controlSize: ControlSize.large, /// child: Text('Primary'), /// onPressed: Navigator.of(context).pop, /// ), @@ -46,7 +48,7 @@ class MacosAlertDialog extends StatelessWidget { /// This should be your application's icon. /// - /// The size of this widget should be 56x56. + /// The size of this widget should be 64x64. final Widget appIcon; /// The title for the dialog. @@ -61,13 +63,13 @@ class MacosAlertDialog extends StatelessWidget { /// The primary action a user can take. /// - /// Typically a [PushButton]. - final Widget primaryButton; + /// Must a [PushButton] with a [ControlSize] of `large`. + final PushButton primaryButton; /// The secondary action a user can take. /// - /// Typically a [PushButton]. - final Widget? secondaryButton; + /// Must a [PushButton] with a [ControlSize] of `large`. + final PushButton? secondaryButton; /// Determines whether to lay out [primaryButton] and [secondaryButton] /// horizontally or vertically. @@ -116,6 +118,11 @@ class MacosAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { assert(debugCheckHasMacosTheme(context)); + assert(primaryButton.controlSize == ControlSize.large); + if (secondaryButton != null) { + assert(secondaryButton is PushButton); + assert(secondaryButton!.controlSize == ControlSize.large); + } final brightness = MacosTheme.brightnessOf(context); final outerBorderColor = brightness.resolve( @@ -153,39 +160,35 @@ class MacosAlertDialog extends StatelessWidget { borderRadius: _kDialogBorderRadius, ), child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 260, - ), + constraints: _kDefaultDialogConstraints, child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SizedBox(height: 28), + const SizedBox(height: 20), ConstrainedBox( constraints: const BoxConstraints( - maxHeight: 56, - maxWidth: 56, + maxHeight: 64, + maxWidth: 64, ), child: appIcon, ), - const SizedBox(height: 28), + const SizedBox(height: 16), DefaultTextStyle( style: MacosTheme.of(context).typography.headline, textAlign: TextAlign.center, child: title, ), - const SizedBox(height: 16), + const SizedBox(height: 10), DefaultTextStyle( textAlign: TextAlign.center, style: MacosTheme.of(context).typography.headline, child: message, ), - const SizedBox(height: 18), + const SizedBox(height: 16), if (secondaryButton == null) ...[ Row( children: [ - Expanded( - child: primaryButton, - ), + Expanded(child: primaryButton), ], ), ] else ...[ @@ -193,9 +196,7 @@ class MacosAlertDialog extends StatelessWidget { Row( children: [ if (secondaryButton != null) ...[ - Expanded( - child: secondaryButton!, - ), + Expanded(child: secondaryButton!), const SizedBox(width: 8.0), ], Expanded( diff --git a/lib/src/enums/control_size.dart b/lib/src/enums/control_size.dart new file mode 100644 index 00000000..835812bf --- /dev/null +++ b/lib/src/enums/control_size.dart @@ -0,0 +1,25 @@ +/// The out-of-the-box sizes that certain "control" widgets can be. +/// +/// +/// +/// Not all controls support all sizes. For example, a [PushButton] can be any +/// size, but a [MacosSwitch] can be all but large. In cases where a control +/// doesn't support a certain size, the control will automatically fall back to +/// the nearest supported size. +/// +/// Reference: +/// * https://developer.apple.com/documentation/swiftui/controlsize +/// * https://developer.apple.com/documentation/swiftui/view/controlsize(_:) +enum ControlSize { + /// A control that is minimally sized. + mini, + + /// A control that is proportionally smaller size for space-constrained views. + small, + + /// A control that is the default size. + regular, + + /// A control that is prominently sized. + large, +} diff --git a/lib/src/fields/text_field.dart b/lib/src/fields/text_field.dart index 9387b029..8564301d 100644 --- a/lib/src/fields/text_field.dart +++ b/lib/src/fields/text_field.dart @@ -105,7 +105,7 @@ class _TextFieldSelectionGestureDetectorBuilder final _MacosTextFieldState _state; @override - void onSingleTapUp(TapUpDetails details) { + void onSingleTapUp(TapDragUpDetails details) { // Because TextSelectionGestureDetector listens to taps that happen on // widgets in front of it, tapping the clear button will also trigger // this handler. If the clear button widget recognizes the up event, @@ -124,11 +124,14 @@ class _TextFieldSelectionGestureDetectorBuilder } _state._requestKeyboard(); if (_state.widget.onTap != null) _state.widget.onTap!(); + + super.onSingleTapUp(details); } @override - void onDragSelectionEnd(DragEndDetails details) { + void onDragSelectionEnd(TapDragEndDetails details) { _state._requestKeyboard(); + super.onDragSelectionEnd(details); } } @@ -358,7 +361,7 @@ class MacosTextField extends StatefulWidget { this.focusNode, this.decoration, this.focusedDecoration, - this.padding = const EdgeInsets.fromLTRB(2.0, 4.0, 2.0, 4.0), + this.padding = const EdgeInsets.symmetric(horizontal: 2.0, vertical: 4.0), this.placeholder, this.placeholderStyle = _kDefaultPlaceholderStyle, this.prefix, @@ -756,7 +759,7 @@ class MacosTextField extends StatefulWidget { 'clearButtonMode', clearButtonMode, )); - properties.add(EnumProperty( + properties.add(DiagnosticsProperty( 'keyboardType', keyboardType, defaultValue: TextInputType.text, @@ -1157,6 +1160,7 @@ class _MacosTextFieldState extends State void dispose() { _focusNode?.dispose(); _controller?.dispose(); + _effectiveFocusNode.removeListener(_handleFocusChanged); super.dispose(); } diff --git a/lib/src/indicators/progress_indicators.dart b/lib/src/indicators/progress_indicators.dart index 9bc9196e..6fe47cd9 100644 --- a/lib/src/indicators/progress_indicators.dart +++ b/lib/src/indicators/progress_indicators.dart @@ -107,7 +107,7 @@ class _DeterminateCirclePainter extends CustomPainter { final Color? borderColor; static const double _twoPi = math.pi * 2.0; - static const double _epsilon = .001; + static const double _epsilon = 0.001; static const double _sweep = _twoPi - _epsilon; static const double _startAngle = -math.pi / 2.0; @@ -240,7 +240,7 @@ class _DeterminateBarPainter extends CustomPainter { void paint(Canvas canvas, Size size) { // Draw the background line canvas.drawRRect( - BorderRadius.circular(100).toRRect( + const BorderRadius.all(Radius.circular(100)).toRRect( Offset.zero & size, ), Paint() diff --git a/lib/src/indicators/relevance_indicator.dart b/lib/src/indicators/relevance_indicator.dart index fa4d6e19..5d8d0dd2 100644 --- a/lib/src/indicators/relevance_indicator.dart +++ b/lib/src/indicators/relevance_indicator.dart @@ -5,6 +5,7 @@ import 'package:macos_ui/src/library.dart'; /// A relevance indicator communicates relevancy using a series /// of vertical bars. It often appears in a list of search results /// for reference when sorting and comparing multiple items. +@Deprecated('Apple no longer supports this component.') class RelevanceIndicator extends StatelessWidget { /// Creates a relevance indicator. /// diff --git a/lib/src/indicators/slider.dart b/lib/src/indicators/slider.dart index 8a172c2e..9e5e7514 100644 --- a/lib/src/indicators/slider.dart +++ b/lib/src/indicators/slider.dart @@ -178,8 +178,9 @@ class MacosSlider extends StatelessWidget { backgroundColor, context, ), - borderRadius: - BorderRadius.circular(_kSliderBorderRadius), + borderRadius: const BorderRadius.all( + Radius.circular(_kSliderBorderRadius), + ), ), ), ), @@ -192,8 +193,9 @@ class MacosSlider extends StatelessWidget { width: width * _percentage, decoration: BoxDecoration( color: MacosDynamicColor.resolve(color, context), - borderRadius: - BorderRadius.circular(_kSliderBorderRadius), + borderRadius: const BorderRadius.all( + Radius.circular(_kSliderBorderRadius), + ), ), ), ), @@ -273,7 +275,8 @@ class _ContinuousThumb extends StatelessWidget { width: _kContinuousThumbSize, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(_kContinuousThumbSize), + borderRadius: + const BorderRadius.all(Radius.circular(_kContinuousThumbSize)), boxShadow: const [ BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.1), @@ -300,7 +303,9 @@ class _DiscreteThumb extends StatelessWidget { width: _kDiscreteThumbWidth, decoration: BoxDecoration( color: color, - borderRadius: BorderRadius.circular(_kDiscreteThumbBorderRadius), + borderRadius: const BorderRadius.all( + Radius.circular(_kDiscreteThumbBorderRadius), + ), boxShadow: const [ BoxShadow( color: Color.fromRGBO(0, 0, 0, 0.1), diff --git a/lib/src/layout/resizable_pane.dart b/lib/src/layout/resizable_pane.dart index 68ccfbed..97836bdd 100644 --- a/lib/src/layout/resizable_pane.dart +++ b/lib/src/layout/resizable_pane.dart @@ -30,10 +30,15 @@ enum ResizableSide { /// The [startSize] is the initial width or height depending on the orientation of the pane. /// {@endtemplate} class ResizablePane extends StatefulWidget { - /// {@macro resizablePane} + /// Creates a [ResizablePane] with an internal [MacosScrollbar]. + /// + /// Consider using [ResizablePane.noScrollBar] constructor when the internal + /// [MacosScrollbar] is not needed or when working with widgets which do not + /// expose their scroll controllers. + /// {@macro resizablePane}. const ResizablePane({ super.key, - required this.builder, + required ScrollableWidgetBuilder this.builder, this.decoration, this.maxSize = 500.0, required this.minSize, @@ -41,7 +46,38 @@ class ResizablePane extends StatefulWidget { required this.resizableSide, this.windowBreakpoint, required this.startSize, - }) : assert( + }) : child = null, + useScrollBar = true, + assert( + maxSize >= minSize, + 'minSize should not be more than maxSize.', + ), + assert( + (startSize >= minSize) && (startSize <= maxSize), + 'startSize must not be less than minSize or more than maxWidth', + ); + + /// Creates a [ResizablePane] without an internal [MacosScrollbar]. + /// + /// Useful when working with widgets which do not expose their scroll + /// controllers or when not using the platform scroll bar is preferred. + /// + /// Consider using the default constructor if showing a [MacosScrollbar] + /// when scrolling the content of this widget is the expected behavior. + /// {@macro resizablePane}. + const ResizablePane.noScrollBar({ + super.key, + required Widget this.child, + this.decoration, + this.maxSize = 500.0, + required this.minSize, + this.isResizable = true, + required this.resizableSide, + this.windowBreakpoint, + required this.startSize, + }) : builder = null, + useScrollBar = false, + assert( maxSize >= minSize, 'minSize should not be more than maxSize.', ), @@ -55,7 +91,15 @@ class ResizablePane extends StatefulWidget { /// /// Pass the [scrollController] obtained from this method, to a scrollable /// widget used in this method to work with the internal [MacosScrollbar]. - final ScrollableWidgetBuilder builder; + final ScrollableWidgetBuilder? builder; + + /// The child to display in this widget. + /// + /// This is only referenced when the constructor used is [ResizablePane.noScrollbar]. + final Widget? child; + + /// Specify if this [ResizablePane] should have an internal [MacosScrollbar]. + final bool useScrollBar; /// The [BoxDecoration] to paint behind the child in the [builder]. final BoxDecoration? decoration; @@ -238,10 +282,8 @@ class _ResizablePaneState extends State { oldWidget.minSize != widget.minSize || oldWidget.maxSize != widget.maxSize || oldWidget.resizableSide != widget.resizableSide) { - setState(() { - if (widget.minSize > _size) _size = widget.minSize; - if (widget.maxSize < _size) _size = widget.maxSize; - }); + if (widget.minSize > _size) _size = widget.minSize; + if (widget.maxSize < _size) _size = widget.maxSize; } } @@ -279,10 +321,12 @@ class _ResizablePaneState extends State { SafeArea( left: false, right: false, - child: MacosScrollbar( - controller: _scrollController, - child: widget.builder(context, _scrollController), - ), + child: widget.useScrollBar + ? MacosScrollbar( + controller: _scrollController, + child: widget.builder!(context, _scrollController), + ) + : widget.child!, ), if (widget.isResizable && !_resizeOnRight && !_resizeOnTop) Positioned( diff --git a/lib/src/layout/scaffold.dart b/lib/src/layout/scaffold.dart index 712ab1a7..59678372 100644 --- a/lib/src/layout/scaffold.dart +++ b/lib/src/layout/scaffold.dart @@ -1,14 +1,9 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; -import 'package:macos_ui/src/layout/content_area.dart'; -import 'package:macos_ui/src/layout/resizable_pane.dart'; -import 'package:macos_ui/src/layout/sidebar/sidebar.dart'; -import 'package:macos_ui/src/layout/title_bar.dart'; -import 'package:macos_ui/src/layout/toolbar/toolbar.dart'; -import 'package:macos_ui/src/layout/window.dart'; +import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; -import 'package:macos_ui/src/theme/macos_theme.dart'; /// A macOS page widget. /// @@ -69,7 +64,7 @@ class _MacosScaffoldState extends State { ); final MacosThemeData theme = MacosTheme.of(context); - late Color backgroundColor = widget.backgroundColor ?? theme.canvasColor; + Color backgroundColor = widget.backgroundColor ?? theme.canvasColor; return LayoutBuilder( builder: (context, constraints) { @@ -82,23 +77,42 @@ class _MacosScaffoldState extends State { return Stack( children: [ - // Background color - Positioned.fill( - child: ColoredBox(color: backgroundColor), - ), - - // Content Area - Positioned( - top: 0, - width: width, - height: height, - child: MediaQuery( - data: mediaQuery.copyWith( - padding: EdgeInsets.only(top: topPadding), + if (!kIsWeb) ...[ + // Content Area + Positioned( + top: 0, + width: width, + height: height, + child: WallpaperTintedArea( + backgroundColor: backgroundColor, + insertRepaintBoundary: true, + child: MediaQuery( + data: mediaQuery.copyWith( + padding: EdgeInsets.only(top: topPadding), + ), + child: _ScaffoldBody(children: children), + ), + ), + ), + ] else ...[ + // Background color + Positioned.fill( + child: ColoredBox(color: backgroundColor), + ), + + // Content Area + Positioned( + top: 0, + width: width, + height: height, + child: MediaQuery( + data: mediaQuery.copyWith( + padding: EdgeInsets.only(top: topPadding), + ), + child: _ScaffoldBody(children: children), ), - child: _ScaffoldBody(children: children), ), - ), + ], // Toolbar if (widget.toolBar != null) @@ -115,7 +129,7 @@ class _MacosScaffoldState extends State { } class _ScaffoldBody extends MultiChildRenderObjectWidget { - _ScaffoldBody({ + const _ScaffoldBody({ super.children, }); diff --git a/lib/src/layout/scrollbar.dart b/lib/src/layout/scrollbar.dart index 334aced5..1cfc819b 100644 --- a/lib/src/layout/scrollbar.dart +++ b/lib/src/layout/scrollbar.dart @@ -156,7 +156,7 @@ class _RawMacosScrollBarState extends RawScrollbarState<_RawMacosScrollBar> { ); _trackColorTween = ColorTween( begin: MacosColors.transparent, - end: widget.effectiveThumbColor.withOpacity(.15), + end: widget.effectiveThumbColor.withOpacity(0.15), ).animate(_trackColorAnimationController); _thumbThicknessAnimationController.addListener(() { updateScrollbarPainter(); diff --git a/lib/src/layout/sidebar/sidebar_items.dart b/lib/src/layout/sidebar/sidebar_items.dart index f5954951..65b3dadb 100644 --- a/lib/src/layout/sidebar/sidebar_items.dart +++ b/lib/src/layout/sidebar/sidebar_items.dart @@ -206,9 +206,7 @@ class _SidebarItem extends StatelessWidget { /// Typically a [Navigator] call final VoidCallback? onClick; - void _handleActionTap() async { - onClick?.call(); - } + void _handleActionTap() => onClick?.call(); Map> get _actionMap => >{ ActivateIntent: CallbackAction( @@ -286,9 +284,8 @@ class _SidebarItem extends StatelessWidget { padding: EdgeInsets.only(right: spacing), child: MacosIconTheme.merge( data: MacosIconThemeData( - color: selected - ? MacosColors.white - : theme.primaryColor, + color: + selected ? MacosColors.white : theme.primaryColor, size: itemSize.iconSize, ), child: item.leading!, diff --git a/lib/src/layout/toolbar/sliver_toolbar.dart b/lib/src/layout/toolbar/sliver_toolbar.dart index 8ec1b087..a96d571b 100644 --- a/lib/src/layout/toolbar/sliver_toolbar.dart +++ b/lib/src/layout/toolbar/sliver_toolbar.dart @@ -40,6 +40,7 @@ class SliverToolBar extends StatefulWidget with Diagnosticable { this.pinned = true, this.floating = false, this.toolbarOpacity = 0.9, + this.allowWallpaperTintingOverrides = true, }); /// Specifies the height of this [ToolBar]. @@ -138,6 +139,30 @@ class SliverToolBar extends StatefulWidget with Diagnosticable { /// Defaults to `0.9`. final double toolbarOpacity; + /// Whether this [SliverToolBar] is allowed to perform wallpaper tinting + /// overrides. + /// + /// This property is supposed to be set to true when this [SliverToolBar] is + /// currently visible on the screen (that is, not e.g. hidden by an + /// [IndexedStack]). + /// + /// By default, macos_ui applies wallpaper tinting to the application's + /// window to match macOS' native appearance: + /// + /// + /// + /// However, this effect is realized by inserting `NSVisualEffectView`s behind + /// Flutter's canvas and turning the background of areas that are meant to be + /// affected by wallpaper tinting transparent. Since Flutter's + /// [`ImageFilter.blur`](https://api.flutter.dev/flutter/dart-ui/ImageFilter/ImageFilter.blur.html) + /// does not support transparency, wallpaper tinting is disabled automatically + /// when this widget's [allowWallpaperTintingOverrides] is true. + /// + /// This is meant to be a temporary solution until + /// [#16296](https://github.com/flutter/flutter/issues/16296) is resolved in + /// the Flutter project. + final bool allowWallpaperTintingOverrides; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -204,6 +229,7 @@ class _SliverToolBarState extends State floating: widget.floating, pinned: widget.pinned, toolbarOpacity: widget.toolbarOpacity, + allowWallpaperTintingOverrides: widget.allowWallpaperTintingOverrides, vsync: this, ), ), @@ -228,6 +254,7 @@ class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { required this.floating, required this.pinned, required this.toolbarOpacity, + required this.allowWallpaperTintingOverrides, }); final double height; @@ -244,6 +271,7 @@ class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { final bool floating; final bool pinned; final double toolbarOpacity; + final bool allowWallpaperTintingOverrides; @override double get minExtent => _kToolbarHeight; @@ -275,7 +303,7 @@ class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { ); } - final Widget toolBar = FlexibleSpaceBar.createSettings( + return FlexibleSpaceBar.createSettings( minExtent: minExtent, maxExtent: maxExtent, currentExtent: math.max(minExtent, maxExtent - shrinkOffset), @@ -293,9 +321,10 @@ class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { dividerColor: dividerColor, alignment: alignment, height: height, + enableBlur: true, + allowWallpaperTintingOverrides: allowWallpaperTintingOverrides, ), ); - return toolBar; } @override @@ -312,6 +341,8 @@ class _SliverToolBarDelegate extends SliverPersistentHeaderDelegate { centerTitle != oldDelegate.centerTitle || dividerColor != oldDelegate.dividerColor || floating != oldDelegate.floating || - pinned != oldDelegate.pinned; + pinned != oldDelegate.pinned || + allowWallpaperTintingOverrides != + oldDelegate.allowWallpaperTintingOverrides; } } diff --git a/lib/src/layout/toolbar/toolbar.dart b/lib/src/layout/toolbar/toolbar.dart index a9685f6a..efd62e3c 100644 --- a/lib/src/layout/toolbar/toolbar.dart +++ b/lib/src/layout/toolbar/toolbar.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/layout/toolbar/overflow_handler.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/wallpaper_tinting_override.dart'; import 'package:macos_ui/src/library.dart'; /// Defines the height of a regular-sized [ToolBar] @@ -43,6 +44,8 @@ class ToolBar extends StatefulWidget with Diagnosticable { this.actions, this.centerTitle = false, this.dividerColor, + this.allowWallpaperTintingOverrides = true, + this.enableBlur = false, }); /// Specifies the height of this [ToolBar]. @@ -120,6 +123,35 @@ class ToolBar extends StatefulWidget with Diagnosticable { /// Set this to `MacosColors.transparent` to remove. final Color? dividerColor; + /// Whether this [ToolBar] is allowed to perform wallpaper tinting overrides. + /// + /// This property is supposed to be set to true when this [ToolBar] is + /// currently visible on the screen (that is, not e.g. hidden by an + /// [IndexedStack]). + /// + /// This parameter only needs to be supplied when [enableBlur] is true. + /// + /// By default, macos_ui applies wallpaper tinting to the application's + /// window to match macOS' native appearance: + /// + /// + /// + /// However, this effect is realized by inserting `NSVisualEffectView`s behind + /// Flutter's canvas and turning the background of areas that are meant to be + /// affected by wallpaper tinting transparent. Since Flutter's + /// [`ImageFilter.blur`](https://api.flutter.dev/flutter/dart-ui/ImageFilter/ImageFilter.blur.html) + /// does not support transparency, wallpaper tinting is disabled automatically + /// when this widget's [enableBlur] and [allowWallpaperTintingOverrides] is + /// true. + /// + /// This is meant to be a temporary solution until + /// [#16296](https://github.com/flutter/flutter/issues/16296) is resolved in + /// the Flutter project. + final bool allowWallpaperTintingOverrides; + + /// Whether this [ToolBar] should have a blur backdrop filter applied to it. + final bool enableBlur; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -191,13 +223,10 @@ class _ToolBarState extends State { title = SizedBox( width: widget.titleWidth, child: DefaultTextStyle( - style: MacosTheme.of(context).typography.headline.copyWith( - fontSize: 15, - fontWeight: FontWeight.w600, - color: theme.brightness.isDark - ? const Color(0xFFEAEAEA) - : const Color(0xFF4D4D4D), - ), + style: theme.typography.title3.copyWith( + fontSize: 15, + fontWeight: MacosFontWeight.w590, + ), child: title, ), ); @@ -233,57 +262,56 @@ class _ToolBarState extends State { left: !kIsWeb && isMacOS ? 70 : 0, ), ), - child: ClipRect( - child: BackdropFilter( - filter: widget.decoration?.color?.opacity == 1 - ? ImageFilter.blur() - : ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0), - child: Container( - alignment: widget.alignment, - padding: widget.padding, - decoration: BoxDecoration( - color: theme.canvasColor, - border: Border(bottom: BorderSide(color: dividerColor)), - ).copyWith( - color: widget.decoration?.color, - image: widget.decoration?.image, - border: widget.decoration?.border, - borderRadius: widget.decoration?.borderRadius, - boxShadow: widget.decoration?.boxShadow, - gradient: widget.decoration?.gradient, - ), - child: NavigationToolbar( - middle: title, - centerMiddle: widget.centerTitle, - trailing: OverflowHandler( - overflowBreakpoint: overflowBreakpoint, - overflowWidget: ToolbarOverflowButton( - isDense: doAllItemsShowLabel, - overflowContentBuilder: (context) => ToolbarOverflowMenu( - children: overflowedActions - .map((action) => action.build( - context, - ToolbarItemDisplayMode.overflowed, - )) - .toList(), - ), + child: _WallpaperTintedAreaOrBlurFilter( + enableWallpaperTintedArea: kIsWeb ? false : !widget.enableBlur, + isWidgetVisible: widget.allowWallpaperTintingOverrides, + backgroundColor: theme.canvasColor, + widgetOpacity: widget.decoration?.color?.opacity, + child: Container( + alignment: widget.alignment, + padding: widget.padding, + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: dividerColor)), + ).copyWith( + color: widget.decoration?.color, + image: widget.decoration?.image, + border: widget.decoration?.border, + borderRadius: widget.decoration?.borderRadius, + boxShadow: widget.decoration?.boxShadow, + gradient: widget.decoration?.gradient, + ), + child: NavigationToolbar( + middle: title, + centerMiddle: widget.centerTitle, + trailing: OverflowHandler( + overflowBreakpoint: overflowBreakpoint, + overflowWidget: ToolbarOverflowButton( + isDense: doAllItemsShowLabel, + overflowContentBuilder: (context) => ToolbarOverflowMenu( + children: overflowedActions + .map((action) => action.build( + context, + ToolbarItemDisplayMode.overflowed, + )) + .toList(), ), - children: inToolbarActions - .map((e) => - e.build(context, ToolbarItemDisplayMode.inToolbar)) - .toList(), - overflowChangedCallback: (hiddenItems) { - setState(() => overflowedActionsCount = hiddenItems.length); - }, - ), - middleSpacing: 8, - leading: SafeArea( - top: false, - right: false, - bottom: false, - left: !(scope?.isSidebarShown ?? false), - child: leading ?? const SizedBox.shrink(), ), + children: inToolbarActions + .map( + (e) => e.build(context, ToolbarItemDisplayMode.inToolbar), + ) + .toList(), + overflowChangedCallback: (hiddenItems) { + setState(() => overflowedActionsCount = hiddenItems.length); + }, + ), + middleSpacing: 8, + leading: SafeArea( + top: false, + right: false, + bottom: false, + left: !(scope?.isSidebarShown ?? false), + child: leading ?? const SizedBox.shrink(), ), ), ), @@ -319,3 +347,50 @@ abstract class ToolbarItem with Diagnosticable { /// for the given display mode (in toolbar or overflowed). Widget build(BuildContext context, ToolbarItemDisplayMode displayMode); } + +/// Wraps the widget in either a [WallpaperTintingOverride] or a blurry backdrop +/// filter. +class _WallpaperTintedAreaOrBlurFilter extends StatelessWidget { + const _WallpaperTintedAreaOrBlurFilter({ + required this.child, + required this.enableWallpaperTintedArea, + required this.backgroundColor, + required this.widgetOpacity, + required this.isWidgetVisible, + }); + + final Widget child; + final bool enableWallpaperTintedArea; + final Color backgroundColor; + final double? widgetOpacity; + final bool isWidgetVisible; + + @override + Widget build(BuildContext context) { + if (enableWallpaperTintedArea) { + return WallpaperTintedArea( + backgroundColor: backgroundColor, + insertRepaintBoundary: true, + child: child, + ); + } + + if (!isWidgetVisible) { + return child; + } + + return WallpaperTintingOverride( + child: ClipRect( + child: BackdropFilter( + filter: widgetOpacity == 1.0 + ? ImageFilter.blur() + : ImageFilter.blur( + sigmaX: 5.0, + sigmaY: 5.0, + ), + child: child, + ), + ), + ); + } +} diff --git a/lib/src/layout/toolbar/toolbar_divider.dart b/lib/src/layout/toolbar/toolbar_divider.dart index d20a3990..346a999c 100644 --- a/lib/src/layout/toolbar/toolbar_divider.dart +++ b/lib/src/layout/toolbar/toolbar_divider.dart @@ -23,15 +23,9 @@ class ToolBarDivider extends ToolbarItem { const Color.fromRGBO(255, 255, 255, 0.25), ); if (displayMode == ToolbarItemDisplayMode.inToolbar) { - return Padding( - padding: padding!, - child: Container(color: color, width: 1, height: 28), - ); + return Container(color: color, width: 1, height: 28, padding: padding!); } else { - return Padding( - padding: padding!, - child: Container(color: color, height: 1), - ); + return Container(color: color, height: 1, padding: padding!); } } } diff --git a/lib/src/layout/toolbar/toolbar_popup.dart b/lib/src/layout/toolbar/toolbar_popup.dart index 9d618128..87f1158b 100644 --- a/lib/src/layout/toolbar/toolbar_popup.dart +++ b/lib/src/layout/toolbar/toolbar_popup.dart @@ -203,7 +203,7 @@ class _ToolbarPopupMenuState extends State<_ToolbarPopupMenu> { super.initState(); _fadeOpacity = CurvedAnimation( parent: widget.route.animation!, - curve: const Interval(0.0, 0.50), + curve: const Interval(0.0, 0.5), reverseCurve: const Interval(0.75, 1.0), ); } @@ -335,7 +335,7 @@ class _ToolbarPopupRoute extends PopupRoute { @override Widget buildPage(context, animation, secondaryAnimation) { return LayoutBuilder(builder: (context, constraints) { - final page = _ToolbarPopupRoutePage( + return _ToolbarPopupRoutePage( target: target, placementOffset: placementOffset, placement: placement, @@ -349,7 +349,6 @@ class _ToolbarPopupRoute extends PopupRoute { horizontalOffset: horizontalOffset, position: position, ); - return page; }); } diff --git a/lib/src/layout/wallpaper_tinted_area.dart b/lib/src/layout/wallpaper_tinted_area.dart new file mode 100644 index 00000000..607ae808 --- /dev/null +++ b/lib/src/layout/wallpaper_tinted_area.dart @@ -0,0 +1,156 @@ +import 'package:flutter/cupertino.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_builder.dart'; +import 'package:macos_window_utils/macos/ns_visual_effect_view_material.dart'; +import 'package:macos_window_utils/widgets/visual_effect_subview_container/visual_effect_subview_container.dart'; + +/// A widget that applies a wallpaper tint to its child widget. +/// +/// This widget only works on macOS. +/// +/// The [backgroundColor] is the color to apply to the background when wallpaper +/// tinting is disabled. If [insertRepaintBoundary] is true, a [RepaintBoundary] +/// is inserted above this widget in the widget tree. In some instances, it may +/// be necessary to insert a [RepaintBoundary] to ensure proper rendering. +/// The [child] is the widget below this widget in the tree. +/// +/// Example: +/// +/// ```dart +/// WallpaperTintedArea( +/// backgroundColor: MacosColors.white, +/// child: Text('Hello World'), +/// ) +/// ``` +class WallpaperTintedArea extends StatelessWidget { + /// Creates a [WallpaperTintedArea]. + /// + /// Widgets wrapped in this widget will have a wallpaper tint applied to them. + /// + /// **Note:** This widget only works on macOS. + const WallpaperTintedArea({ + super.key, + required this.backgroundColor, + this.insertRepaintBoundary = false, + this.child, + }); + + /// The color to apply to the background when wallpaper tinting is disabled. + final Color backgroundColor; + + /// Whether to insert a [RepaintBoundary] above this widget in the widget + /// tree. + /// + /// In some instances, it may be necessary to insert a [RepaintBoundary] above + /// this widget into the widget tree to ensure that this widget is rendered + /// properly. + final bool insertRepaintBoundary; + + /// The widget below this widget in the tree. + final Widget? child; + + @override + Widget build(BuildContext context) { + if (insertRepaintBoundary) { + return RepaintBoundary( + child: _WallpaperTintedAreaLayoutBuilder( + backgroundColor: backgroundColor, + child: child, + ), + ); + } + + return _WallpaperTintedAreaLayoutBuilder( + backgroundColor: backgroundColor, + child: child, + ); + } +} + +class _WallpaperTintedAreaLayoutBuilder extends StatelessWidget { + const _WallpaperTintedAreaLayoutBuilder({ + required this.backgroundColor, + required this.child, + }); + + /// The color to apply to the background when wallpaper tinting is disabled. + final Color backgroundColor; + + /// The widget below this widget in the tree. + final Widget? child; + + @override + Widget build(BuildContext context) { + if (GlobalWallpaperTintingSettings + .data.isWallpaperTintingDisabledByWindow) { + return Container( + decoration: BoxDecoration( + color: backgroundColor, + ), + child: child, + ); + } + + // This LayoutBuilder forces the widget to be rebuilt when a layout change + // is detected. This is necessary for the VisualEffectSubviewContainer to + // be updated. + return LayoutBuilder( + builder: (context, _) { + return VisualEffectSubviewContainer( + material: NSVisualEffectViewMaterial.windowBackground, + child: WallpaperTintingSettingsBuilder( + builder: (context, data) { + final isWallpaperTintingEnabled = data.isWallpaperTintingEnabled; + + return _WallpaperTintedAreaTweenAnimationBuilder( + isWallpaperTintingEnabled: isWallpaperTintingEnabled, + backgroundColor: backgroundColor, + child: child, + ); + }, + ), + ); + }, + ); + } +} + +class _WallpaperTintedAreaTweenAnimationBuilder extends StatelessWidget { + const _WallpaperTintedAreaTweenAnimationBuilder({ + required this.isWallpaperTintingEnabled, + required this.backgroundColor, + required this.child, + }); + + /// Whether wallpaper tinting is enabled. + final bool isWallpaperTintingEnabled; + + /// The color to apply to the background when wallpaper tinting is disabled. + final Color backgroundColor; + + /// The widget below this widget in the tree. + final Widget? child; + + @override + Widget build(BuildContext context) { + return TweenAnimationBuilder( + duration: const Duration(milliseconds: 100), + tween: Tween( + begin: isWallpaperTintingEnabled ? 0.0 : 1.0, + end: isWallpaperTintingEnabled ? 0.0 : 1.0, + ), + builder: (context, value, child) { + return Container( + decoration: BoxDecoration( + color: backgroundColor.withOpacity(value), + backgroundBlendMode: BlendMode.src, + ), + child: child, + ); + }, + child: RepaintBoundary( + child: child, + ), + ); + } +} diff --git a/lib/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart b/lib/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart new file mode 100644 index 00000000..78ec46f1 --- /dev/null +++ b/lib/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_data.dart'; + +/// A class that provides a global instance of [WallpaperTintingSettingsData]. +class GlobalWallpaperTintingSettings { + /// The [WallpaperTintingSettingsData] instance. + static final WallpaperTintingSettingsData data = + WallpaperTintingSettingsData(); + + /// The [StreamController] for an event stream that is triggered when [data] + /// changes. + static final _onDataChangedStreamController = + StreamController.broadcast(); + + /// A stream that can be used to listen to [data] changes. + static Stream get onDataChangedStream => + _onDataChangedStreamController.stream; + + /// Gets whether wallpaper tinting should be enabled. + static bool get isWallpaperTintingEnabled => data.isWallpaperTintingEnabled; + + /// Increments the number of active overrides. + static void addWallpaperTintingOverride() { + data.addOverride(); + _onDataChangedStreamController.add(data); + } + + /// Decrements the number of active overrides. + static void removeWallpaperTintingOverride() { + data.removeOverride(); + _onDataChangedStreamController.add(data); + } + + /// Disables wallpaper tinting altogether. + static void disableWallpaperTinting() { + data.disableWallpaperTinting(); + _onDataChangedStreamController.add(data); + } + + /// Allows wallpaper tinting, unless overridden. + static void allowWallpaperTinting() { + data.allowWallpaperTinting(); + _onDataChangedStreamController.add(data); + } +} diff --git a/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_override.dart b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_override.dart new file mode 100644 index 00000000..7cd05629 --- /dev/null +++ b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_override.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart'; + +class WallpaperTintingOverride extends StatefulWidget { + /// Creates a [WallpaperTintingOverride]. + /// + /// Including this widget in the widget tree will disable wallpaper tinting + /// globally. It is intended to be used by [MacosOverlayFilter] to disable + /// wallpaper tinting when an overlay filter is active, since + /// [`ImageFilter.blur`](https://api.flutter.dev/flutter/dart-ui/ImageFilter/ImageFilter.blur.html) + /// does not support transparency. + const WallpaperTintingOverride({super.key, this.child}); + + /// The widget below this widget in the tree. + final Widget? child; + + @override + State createState() => + _WallpaperTintingOverrideState(); +} + +class _WallpaperTintingOverrideState extends State { + @override + void initState() { + super.initState(); + + GlobalWallpaperTintingSettings.addWallpaperTintingOverride(); + } + + @override + void dispose() { + GlobalWallpaperTintingSettings.removeWallpaperTintingOverride(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child ?? const SizedBox(); + } +} diff --git a/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_builder.dart b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_builder.dart new file mode 100644 index 00000000..19fe2873 --- /dev/null +++ b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_builder.dart @@ -0,0 +1,46 @@ +import 'package:flutter/widgets.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart'; + +import 'wallpaper_tinting_settings_data.dart'; + +/// A widget that listens for changes to [WallpaperTintingSettingsData] and +/// rebuilds with the latest data when a change is detected. +/// +/// The [builder] callback is called whenever [WallpaperTintingSettingsData] +/// changes. It should build a widget using the latest +/// [WallpaperTintingSettingsData]. +/// +/// Example: +/// +/// ```dart +/// WallpaperTintingSettingsBuilder( +/// builder: (context, data) { +/// return Text( +/// 'isWallpaperTintingEnabled: ${data.isWallpaperTintingEnabled}', +/// ); +/// }, +/// ) +/// ``` +class WallpaperTintingSettingsBuilder extends StatelessWidget { + /// Creates a [WallpaperTintingSettingsBuilder]. + /// + /// This widget can be used to listen to [WallpaperTintingSettingsData] + /// changes and rebuild if a change has been detected. + const WallpaperTintingSettingsBuilder({super.key, required this.builder}); + + /// Called when [WallpaperTintingSettingsData] changes. + final Widget Function(BuildContext, WallpaperTintingSettingsData) builder; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: GlobalWallpaperTintingSettings.onDataChangedStream, + initialData: GlobalWallpaperTintingSettings.data, + builder: (context, snapshot) { + final data = snapshot.data ?? GlobalWallpaperTintingSettings.data; + + return builder(context, data); + }, + ); + } +} diff --git a/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_data.dart b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_data.dart new file mode 100644 index 00000000..07c8aea4 --- /dev/null +++ b/lib/src/layout/wallpaper_tinting_settings/wallpaper_tinting_settings_data.dart @@ -0,0 +1,38 @@ +/// Holds data related to wallpaper tinting. +class WallpaperTintingSettingsData { + /// The number of wallpaper tinting overrides that are currently active. + /// + /// A wallpaper tinting override causes wallpaper tinting to be disabled. + int _numberOfWallpaperTintingOverrides = 0; + + /// Whether wallpaper tinting is disabled by the application's window. + bool _isWallpaperTintingDisabledByWindow = false; + + /// Gets whether wallpaper tinting should be enabled. + bool get isWallpaperTintingEnabled => + !_isWallpaperTintingDisabledByWindow && + _numberOfWallpaperTintingOverrides == 0; + + /// Gets whether wallpaper tinting is disabled by the application's window. + bool get isWallpaperTintingDisabledByWindow => + _isWallpaperTintingDisabledByWindow; + + /// Increments the number of active overrides. + void addOverride() => _numberOfWallpaperTintingOverrides += 1; + + /// Decrements the number of active overrides. + void removeOverride() { + _numberOfWallpaperTintingOverrides -= 1; + assert(_numberOfWallpaperTintingOverrides >= 0); + } + + /// Disables wallpaper tinting altogether. + void disableWallpaperTinting() { + _isWallpaperTintingDisabledByWindow = true; + } + + /// Allows wallpaper tinting, unless overridden. + void allowWallpaperTinting() { + _isWallpaperTintingDisabledByWindow = false; + } +} diff --git a/lib/src/layout/window.dart b/lib/src/layout/window.dart index 9ce38eb0..85c20417 100644 --- a/lib/src/layout/window.dart +++ b/lib/src/layout/window.dart @@ -2,14 +2,10 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:macos_ui/src/layout/scrollbar.dart'; -import 'package:macos_ui/src/layout/content_area.dart'; -import 'package:macos_ui/src/layout/resizable_pane.dart'; -import 'package:macos_ui/src/layout/scaffold.dart'; -import 'package:macos_ui/src/layout/sidebar/sidebar.dart'; -import 'package:macos_ui/src/layout/title_bar.dart'; +import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/global_wallpaper_tinting_settings.dart'; import 'package:macos_ui/src/library.dart'; -import 'package:macos_ui/src/theme/macos_theme.dart'; +import 'package:macos_window_utils/widgets/transparent_macos_sidebar.dart'; /// A basic frame layout. /// @@ -28,6 +24,8 @@ class MacosWindow extends StatefulWidget { this.sidebar, this.backgroundColor, this.endSidebar, + this.disableWallpaperTinting = false, + this.sidebarState = NSVisualEffectViewState.followsWindowActiveState, }); /// Specifies the background color for the Window. @@ -47,13 +45,47 @@ class MacosWindow extends StatefulWidget { /// A sidebar to display at the right of the window. final Sidebar? endSidebar; + /// Whether wallpaper tinting should be disabled. + /// + /// By default, `macos_ui` applies wallpaper tinting to the application's + /// window to match macOS' native appearance: + /// + /// + /// + /// However, this effect is realized by inserting `NSVisualEffectView`s behind + /// Flutter's canvas and turning the background of areas that are meant to be + /// affected by wallpaper tinting transparent. Since Flutter's + /// [`ImageFilter.blur`](https://api.flutter.dev/flutter/dart-ui/ImageFilter/ImageFilter.blur.html) + /// does not support transparency, wallpaper tinting is disabled automatically + /// when a [MacosOverlayFilter] is present in the widget tree. + /// + /// This is meant to be a temporary solution until + /// [#16296](https://github.com/flutter/flutter/issues/16296) is resolved in + /// the Flutter project. + /// + /// Since the disabling of wallpaper tinting may be found to be too noticeable, + /// this property may be used to disable wallpaper tinting outright. + final bool disableWallpaperTinting; + + /// The state of the sidebar's [NSVisualEffectView]. + /// + /// Possible values are: + /// + /// - [NSVisualEffectViewState.active]: The sidebar is always active. + /// - [NSVisualEffectViewState.inactive]: The sidebar is always inactive. + /// - [NSVisualEffectViewState.followsWindowActiveState]: The sidebar's state + /// follows the window's active state. + /// + /// Defaults to [NSVisualEffectViewState.followsWindowActiveState]. + final NSVisualEffectViewState sidebarState; + @override State createState() => _MacosWindowState(); } class _MacosWindowState extends State { - final _sidebarScrollController = ScrollController(); - final _endSidebarScrollController = ScrollController(); + var _sidebarScrollController = ScrollController(); + var _endSidebarScrollController = ScrollController(); double _sidebarWidth = 0.0; double _sidebarDragStartWidth = 0.0; double _sidebarDragStartPosition = 0.0; @@ -74,47 +106,71 @@ class _MacosWindowState extends State { _endSidebarWidth = (widget.endSidebar?.startWidth ?? widget.endSidebar?.minWidth) ?? _endSidebarWidth; - if (widget.sidebar?.builder != null) { - _sidebarScrollController.addListener(() => setState(() {})); - } - if (widget.endSidebar?.builder != null) { - _endSidebarScrollController.addListener(() => setState(() {})); - } + + widget.disableWallpaperTinting + ? GlobalWallpaperTintingSettings.disableWallpaperTinting() + : GlobalWallpaperTintingSettings.allowWallpaperTinting(); + + _addSidebarScrollControllerListenerIfNeeded(); + _addEndSidebarScrollControllerListenerIfNeeded(); } @override void didUpdateWidget(covariant MacosWindow old) { super.didUpdateWidget(old); - setState(() { - if (widget.sidebar == null) { - _sidebarWidth = 0.0; - } else if (widget.sidebar!.minWidth != old.sidebar!.minWidth || - widget.sidebar!.maxWidth != old.sidebar!.maxWidth) { - if (widget.sidebar!.minWidth > _sidebarWidth) { - _sidebarWidth = widget.sidebar!.minWidth; - } - if (widget.sidebar!.maxWidth! < _sidebarWidth) { - _sidebarWidth = widget.sidebar!.maxWidth!; - } + final sidebar = widget.sidebar; + if (sidebar == null) { + _sidebarWidth = 0.0; + } else if (sidebar.minWidth != old.sidebar!.minWidth || + sidebar.maxWidth != old.sidebar!.maxWidth) { + if (sidebar.minWidth > _sidebarWidth) { + _sidebarWidth = sidebar.minWidth; + } + if (sidebar.maxWidth! < _sidebarWidth) { + _sidebarWidth = sidebar.maxWidth!; + } + } + if (sidebar?.key != old.sidebar?.key) { + _sidebarScrollController.dispose(); + _sidebarScrollController = ScrollController(); + _addSidebarScrollControllerListenerIfNeeded(); + } + final endSidebar = widget.endSidebar; + if (endSidebar == null) { + _endSidebarWidth = 0.0; + } else if (endSidebar.minWidth != old.endSidebar!.minWidth || + endSidebar.maxWidth != old.endSidebar!.maxWidth) { + if (endSidebar.minWidth > _endSidebarWidth) { + _endSidebarWidth = endSidebar.minWidth; } - 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!; - } + if (endSidebar.maxWidth! < _endSidebarWidth) { + _endSidebarWidth = endSidebar.maxWidth!; } - }); + } + if (endSidebar?.key != old.endSidebar?.key) { + _endSidebarScrollController.dispose(); + _endSidebarScrollController = ScrollController(); + _addEndSidebarScrollControllerListenerIfNeeded(); + } + } + + void _addSidebarScrollControllerListenerIfNeeded() { + if (widget.sidebar?.builder != null) { + _sidebarScrollController.addListener(() => setState(() {})); + } + } + + void _addEndSidebarScrollControllerListenerIfNeeded() { + if (widget.endSidebar?.builder != null) { + _endSidebarScrollController.addListener(() => setState(() {})); + } } @override void dispose() { _sidebarScrollController.dispose(); _endSidebarScrollController.dispose(); + super.dispose(); } @@ -122,13 +178,15 @@ class _MacosWindowState extends State { // ignore: code-metrics Widget build(BuildContext context) { assert(debugCheckHasMacosTheme(context)); - if (widget.sidebar?.startWidth != null) { - assert((widget.sidebar!.startWidth! >= widget.sidebar!.minWidth) && - (widget.sidebar!.startWidth! <= widget.sidebar!.maxWidth!)); + final sidebar = widget.sidebar; + final endSidebar = widget.endSidebar; + if (sidebar?.startWidth != null) { + assert((sidebar!.startWidth! >= sidebar.minWidth) && + (sidebar.startWidth! <= sidebar.maxWidth!)); } - if (widget.endSidebar?.startWidth != null) { - assert((widget.endSidebar!.startWidth! >= widget.endSidebar!.minWidth) && - (widget.endSidebar!.startWidth! <= widget.endSidebar!.maxWidth!)); + if (endSidebar?.startWidth != null) { + assert((endSidebar!.startWidth! >= endSidebar.minWidth) && + (endSidebar.startWidth! <= endSidebar.maxWidth!)); } final MacosThemeData theme = MacosTheme.of(context); late Color backgroundColor = widget.backgroundColor ?? theme.canvasColor; @@ -139,28 +197,19 @@ class _MacosWindowState extends State { final isMac = !kIsWeb && defaultTargetPlatform == TargetPlatform.macOS; // Respect the sidebar color override from parent if one is given - if (widget.sidebar?.decoration?.color != null) { - sidebarBackgroundColor = widget.sidebar!.decoration!.color!; - } else if (isMac && - MediaQuery.of(context).platformBrightness.isDark == - theme.brightness.isDark) { - // Only show blurry, transparent sidebar when platform brightness and app - // brightness are the same, otherwise it looks awful. Also only make the - // sidebar transparent on native Mac, or it will just be flat black or - // white. - sidebarBackgroundColor = Colors.transparent; + if (sidebar?.decoration?.color != null) { + sidebarBackgroundColor = sidebar!.decoration!.color!; } else { - sidebarBackgroundColor = theme.brightness.isDark - ? CupertinoColors.tertiarySystemBackground.darkColor - : CupertinoColors.systemGrey6.color; + sidebarBackgroundColor = MacosColors.transparent; } + // Set the application window's brightness on macOS + MacOSBrightnessOverrideHandler.ensureMatchingBrightness(theme.brightness); + // 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) { + if (endSidebar?.decoration?.color != null) { + endSidebarBackgroundColor = endSidebar!.decoration!.color!; + } else if (isMac) { endSidebarBackgroundColor = theme.canvasColor; } else { endSidebarBackgroundColor = theme.brightness.isDark @@ -175,14 +224,16 @@ class _MacosWindowState extends State { builder: (context, constraints) { final width = constraints.maxWidth; final height = constraints.maxHeight; - final isAtBreakpoint = width <= (widget.sidebar?.windowBreakpoint ?? 0); - final isAtEndBreakpoint = - width <= (widget.endSidebar?.windowBreakpoint ?? 0); - final canShowSidebar = _showSidebar && !isAtBreakpoint; - final canShowEndSidebar = _showEndSidebar && !isAtEndBreakpoint; + final isAtBreakpoint = width <= (sidebar?.windowBreakpoint ?? 0); + final isAtEndBreakpoint = width <= (endSidebar?.windowBreakpoint ?? 0); + final canShowSidebar = + _showSidebar && !isAtBreakpoint && sidebar != null; + final canShowEndSidebar = + _showEndSidebar && !isAtEndBreakpoint && endSidebar != null; final visibleSidebarWidth = canShowSidebar ? _sidebarWidth : 0.0; final visibleEndSidebarWidth = canShowEndSidebar ? _endSidebarWidth : 0.0; + final sidebarState = widget.sidebarState; final layout = Stack( children: [ @@ -197,8 +248,9 @@ class _MacosWindowState extends State { ), // Sidebar - if (widget.sidebar != null) + if (sidebar != null) AnimatedPositioned( + key: sidebar.key, curve: curve, duration: duration, height: height, @@ -208,41 +260,99 @@ class _MacosWindowState extends State { curve: Curves.easeInOut, color: sidebarBackgroundColor, constraints: BoxConstraints( - minWidth: widget.sidebar!.minWidth, - maxWidth: widget.sidebar!.maxWidth!, + minWidth: sidebar.minWidth, + maxWidth: sidebar.maxWidth!, minHeight: height, maxHeight: height, ).normalize(), - child: Column( - children: [ - if ((widget.sidebar?.topOffset ?? 0) > 0) - SizedBox(height: widget.sidebar?.topOffset), - if (_sidebarScrollController.hasClients && - _sidebarScrollController.offset > 0.0) - Divider(thickness: 1, height: 1, color: dividerColor), - if (widget.sidebar!.top != null && - constraints.maxHeight > 81) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: widget.sidebar!.top!, - ), - Expanded( - child: MacosScrollbar( - controller: _sidebarScrollController, - child: Padding( - padding: widget.sidebar?.padding ?? EdgeInsets.zero, - child: widget.sidebar! - .builder(context, _sidebarScrollController), + child: kIsWeb ? ColoredBox( + color: theme.canvasColor, + child: Column( + children: [ + // If an app is running on macOS, apply + // sidebar.topOffset as needed in order to avoid the + // traffic lights. Otherwise, position the sidebar + // by the top of the application's bounds based on + // the presence of sidebar.top. + if (!kIsWeb && sidebar.topOffset > 0) ...[ + SizedBox(height: sidebar.topOffset), + ] else if (sidebar.top != null) ...[ + const SizedBox(height: 12), + ] else + const SizedBox.shrink(), + if (_sidebarScrollController.hasClients && + _sidebarScrollController.offset > 0.0) + Divider(thickness: 1, height: 1, color: dividerColor), + if (sidebar.top != null && constraints.maxHeight > 81) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: sidebar.top!, + ), + Expanded( + child: MacosScrollbar( + controller: _sidebarScrollController, + child: Padding( + padding: sidebar.padding, + child: sidebar.builder( + context, + _sidebarScrollController, + ), + ), ), ), - ), - if (widget.sidebar?.bottom != null && - constraints.maxHeight > 141) - Padding( - padding: const EdgeInsets.all(16.0), - child: widget.sidebar!.bottom!, + if (sidebar.bottom != null && + constraints.maxHeight > 141) + Padding( + padding: const EdgeInsets.all(16.0), + child: sidebar.bottom!, + ), + ], + ), + ) : TransparentMacOSSidebar( + state: sidebarState, + child: Column( + children: [ + // If an app is running on macOS, apply + // sidebar.topOffset as needed in order to avoid the + // traffic lights. Otherwise, position the sidebar + // by the top of the application's bounds based on + // the presence of sidebar.top. + if (!kIsWeb && sidebar.topOffset > 0) ...[ + SizedBox(height: sidebar.topOffset), + ] else if (sidebar.top != null) ...[ + const SizedBox(height: 12), + ] else + const SizedBox.shrink(), + if (_sidebarScrollController.hasClients && + _sidebarScrollController.offset > 0.0) + Divider(thickness: 1, height: 1, color: dividerColor), + if (sidebar.top != null && constraints.maxHeight > 81) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: sidebar.top!, + ), + Expanded( + child: MacosScrollbar( + controller: _sidebarScrollController, + child: Padding( + padding: sidebar.padding, + child: sidebar.builder( + context, + _sidebarScrollController, + ), + ), + ), ), - ], + if (sidebar.bottom != null && + constraints.maxHeight > 141) + Padding( + padding: const EdgeInsets.all(16.0), + child: sidebar.bottom!, + ), + ], + ), ), ), ), @@ -275,7 +385,7 @@ class _MacosWindowState extends State { ), // Sidebar resizer - if (widget.sidebar?.isResizable ?? false) + if (sidebar?.isResizable ?? false) AnimatedPositioned( curve: curve, duration: duration, @@ -289,13 +399,12 @@ class _MacosWindowState extends State { _sidebarDragStartPosition = details.globalPosition.dx; }, onHorizontalDragUpdate: (details) { - final sidebar = widget.sidebar!; setState(() { var newWidth = _sidebarDragStartWidth + details.globalPosition.dx - _sidebarDragStartPosition; - if (sidebar.startWidth != null && + if (sidebar!.startWidth != null && sidebar.snapToStartBuffer != null && (newWidth - sidebar.startWidth!).abs() <= sidebar.snapToStartBuffer!) { @@ -340,8 +449,9 @@ class _MacosWindowState extends State { ), // End sidebar - if (widget.endSidebar != null) + if (endSidebar != null) AnimatedPositioned( + key: endSidebar.key, left: width - visibleEndSidebarWidth, curve: curve, duration: duration, @@ -352,46 +462,56 @@ class _MacosWindowState extends State { curve: Curves.easeInOut, color: endSidebarBackgroundColor, constraints: BoxConstraints( - minWidth: widget.endSidebar!.minWidth, - maxWidth: widget.endSidebar!.maxWidth!, + minWidth: endSidebar.minWidth, + maxWidth: 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( + child: WallpaperTintedArea( + backgroundColor: endSidebarBackgroundColor, + insertRepaintBoundary: true, + child: Column( + children: [ + if (endSidebar.topOffset > 0) + SizedBox(height: endSidebar.topOffset), + if (_endSidebarScrollController.hasClients && + _endSidebarScrollController.offset > 0.0) + Divider( + thickness: 1, + height: 1, + color: dividerColor, + ), + if (endSidebar.top != null) + Padding( padding: - widget.endSidebar?.padding ?? EdgeInsets.zero, - child: widget.endSidebar! - .builder(context, _endSidebarScrollController), + const EdgeInsets.symmetric(horizontal: 8.0), + child: endSidebar.top!, + ), + Expanded( + child: MacosScrollbar( + controller: _endSidebarScrollController, + child: Padding( + padding: endSidebar.padding, + child: endSidebar.builder( + context, + _endSidebarScrollController, + ), + ), ), ), - ), - if (widget.endSidebar?.bottom != null) - Padding( - padding: const EdgeInsets.all(16.0), - child: widget.endSidebar!.bottom!, - ), - ], + if (endSidebar.bottom != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: endSidebar.bottom!, + ), + ], + ), ), ), ), // End sidebar resizer - if (widget.endSidebar?.isResizable ?? false) + if (endSidebar?.isResizable ?? false) AnimatedPositioned( curve: curve, duration: duration, @@ -405,13 +525,12 @@ class _MacosWindowState extends State { _endSidebarDragStartPosition = details.globalPosition.dx; }, onHorizontalDragUpdate: (details) { - final endSidebar = widget.endSidebar!; setState(() { var newWidth = _endSidebarDragStartWidth - details.globalPosition.dx + _endSidebarDragStartPosition; - if (endSidebar.startWidth != null && + if (endSidebar!.startWidth != null && endSidebar.snapToStartBuffer != null && (newWidth + endSidebar.startWidth!).abs() <= endSidebar.snapToStartBuffer!) { @@ -465,13 +584,17 @@ class _MacosWindowState extends State { setState(() => _sidebarSlideDuration = 300); setState(() => _showSidebar = !_showSidebar); await Future.delayed(Duration(milliseconds: _sidebarSlideDuration)); - setState(() => _sidebarSlideDuration = 0); + if (mounted) { + setState(() => _sidebarSlideDuration = 0); + } }, endSidebarToggler: () async { setState(() => _sidebarSlideDuration = 300); setState(() => _showEndSidebar = !_showEndSidebar); await Future.delayed(Duration(milliseconds: _sidebarSlideDuration)); - setState(() => _sidebarSlideDuration = 0); + if (mounted) { + setState(() => _sidebarSlideDuration = 0); + } }, child: layout, ); diff --git a/lib/src/macos_app.dart b/lib/src/macos_app.dart index c4068f30..459918fb 100644 --- a/lib/src/macos_app.dart +++ b/lib/src/macos_app.dart @@ -74,15 +74,17 @@ class MacosApp extends StatefulWidget { }) : routeInformationProvider = null, routeInformationParser = null, routerDelegate = null, - backButtonDispatcher = null; + backButtonDispatcher = null, + routerConfig = null; /// Creates a [MacosApp] that uses the [Router] instead of a [Navigator]. MacosApp.router({ super.key, this.routeInformationProvider, - required RouteInformationParser this.routeInformationParser, - required RouterDelegate this.routerDelegate, + this.routeInformationParser, + this.routerDelegate, this.backButtonDispatcher, + this.routerConfig, this.builder, this.title = '', this.onGenerateTitle, @@ -104,7 +106,8 @@ class MacosApp extends StatefulWidget { this.themeMode, this.theme, this.darkTheme, - }) : assert(supportedLocales.isNotEmpty), + }) : assert(routerDelegate != null || routerConfig != null), + assert(supportedLocales.isNotEmpty), navigatorObservers = null, navigatorKey = null, onGenerateRoute = null, @@ -157,6 +160,9 @@ class MacosApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.backButtonDispatcher} final BackButtonDispatcher? backButtonDispatcher; + /// {@macro flutter.widgets.widgetsApp.routerConfig} + final RouterConfig? routerConfig; + /// {@macro flutter.widgets.widgetsApp.builder} final TransitionBuilder? builder; @@ -300,7 +306,8 @@ class MacosApp extends StatefulWidget { } class _MacosAppState extends State { - bool get _usesRouter => widget.routerDelegate != null; + bool get _usesRouter => + widget.routerDelegate != null || widget.routerConfig != null; Widget _macosBuilder(BuildContext context, Widget? child) { final mode = widget.themeMode ?? ThemeMode.system; @@ -346,9 +353,10 @@ class _MacosAppState extends State { return c.CupertinoApp.router( key: GlobalObjectKey(this), routeInformationProvider: widget.routeInformationProvider, - routeInformationParser: widget.routeInformationParser!, - routerDelegate: widget.routerDelegate!, + routeInformationParser: widget.routeInformationParser, + routerDelegate: widget.routerDelegate, backButtonDispatcher: widget.backButtonDispatcher, + routerConfig: widget.routerConfig, builder: _macosBuilder, title: widget.title, onGenerateTitle: widget.onGenerateTitle, @@ -401,8 +409,7 @@ class _MacosAppState extends State { @override Widget build(BuildContext context) { // leaves room for assertions, etc - Widget result = _buildMacosApp(context); - return result; + return _buildMacosApp(context); } Iterable> get _localizationsDelegates sync* { diff --git a/lib/src/macos_window_utils_config.dart b/lib/src/macos_window_utils_config.dart new file mode 100644 index 00000000..32e52568 --- /dev/null +++ b/lib/src/macos_window_utils_config.dart @@ -0,0 +1,120 @@ +import 'package:flutter/widgets.dart'; +import 'package:macos_ui/macos_ui.dart'; + +/// A class for configuring macOS window properties. +/// +/// [toolbarStyle] is the style of the window toolbar. It should be +/// [NSWindowToolbarStyle.expanded] if the app will have a title bar and +/// [NSWindowToolbarStyle.unified] otherwise. +/// +/// Example: +/// ```dart +/// final config = MacosWindowUtilsConfig( +/// toolbarStyle: NSWindowToolbarStyle.expanded, +/// ); +/// await config.apply(); +/// ``` +class MacosWindowUtilsConfig { + /// Creates a [MacosWindowUtilsConfig]. + /// + /// The [toolbarStyle] is [NSWindowToolbarStyle.unified] by default. If the + /// app will have a title bar, use [NSWindowToolbarStyle.expanded] instead. + const MacosWindowUtilsConfig({ + this.toolbarStyle = NSWindowToolbarStyle.unified, + this.enableFullSizeContentView = true, + this.makeTitlebarTransparent = true, + this.hideTitle = true, + this.removeMenubarInFullScreenMode = true, + this.autoHideToolbarAndMenuBarInFullScreenMode = true, + }); + + /// The style of the window toolbar. + /// + /// Defaults to [NSWindowToolbarStyle.unified]. Use + /// [NSWindowToolbarStyle.expanded] instead if the app will have a title bar. + final NSWindowToolbarStyle toolbarStyle; + + /// Whether to enable the full-size content view. + final bool enableFullSizeContentView; + + /// Whether to make the title bar transparent. + final bool makeTitlebarTransparent; + + /// Whether to hide the title. + final bool hideTitle; + + /// Whether to remove the menubar in full-screen mode. + final bool removeMenubarInFullScreenMode; + + /// Whether to automatically hide the toolbar and menubar in full-screen mode. + final bool autoHideToolbarAndMenuBarInFullScreenMode; + + /// Applies the configuration to the macOS window. + /// + /// This method: + /// + /// - Initializes Flutter bindings + /// - Sets the window material to + /// [NSVisualEffectViewMaterial.windowBackground] + /// - Enables the full size content view if [enableFullSizeContentView] is + /// `true` + /// - Makes the title bar transparent if [makeTitlebarTransparent] is `true` + /// - Hides the title if [hideTitle] is `true` + /// - Adds a toolbar + /// - Sets the toolbar style to [toolbarStyle] + /// - Removes the menubar in full-screen mode if + /// [removeMenubarInFullScreenMode] is `true` + /// - Auto-hides the toolbar and menubar in full-screen mode if + /// [autoHideToolbarAndMenuBarInFullScreenMode] is `true` + Future apply() async { + WidgetsFlutterBinding.ensureInitialized(); + await WindowManipulator.initialize(enableWindowDelegate: true); + await WindowManipulator.setMaterial( + NSVisualEffectViewMaterial.windowBackground, + ); + if (enableFullSizeContentView) { + await WindowManipulator.enableFullSizeContentView(); + } + if (makeTitlebarTransparent) { + await WindowManipulator.makeTitlebarTransparent(); + } + if (hideTitle) { + await WindowManipulator.hideTitle(); + } + await WindowManipulator.addToolbar(); + + await WindowManipulator.setToolbarStyle( + toolbarStyle: toolbarStyle, + ); + + if (removeMenubarInFullScreenMode) { + final delegate = _FlutterWindowDelegate(); + WindowManipulator.addNSWindowDelegate(delegate); + } + + if (autoHideToolbarAndMenuBarInFullScreenMode) { + final options = NSAppPresentationOptions.from({ + NSAppPresentationOption.fullScreen, + NSAppPresentationOption.autoHideToolbar, + NSAppPresentationOption.autoHideMenuBar, + NSAppPresentationOption.autoHideDock, + }); + options.applyAsFullScreenPresentationOptions(); + } + } +} + +/// This delegate removes the toolbar in full-screen mode. +class _FlutterWindowDelegate extends NSWindowDelegate { + @override + void windowWillEnterFullScreen() { + WindowManipulator.removeToolbar(); + super.windowWillEnterFullScreen(); + } + + @override + void windowDidExitFullScreen() { + WindowManipulator.addToolbar(); + super.windowDidExitFullScreen(); + } +} diff --git a/lib/src/selectors/color_well.dart b/lib/src/selectors/color_well.dart index cff9cd9f..9f1c5685 100644 --- a/lib/src/selectors/color_well.dart +++ b/lib/src/selectors/color_well.dart @@ -106,7 +106,7 @@ class _MacosColorWellState extends State { Widget build(BuildContext context) { final theme = MacosTheme.of(context); final outerColor = theme.brightness.isDark - ? MacosColors.systemGrayColor.withOpacity(0.50) + ? MacosColors.systemGrayColor.withOpacity(0.5) : MacosColors.white; return GestureDetector( onTap: () async { diff --git a/lib/src/selectors/date_picker.dart b/lib/src/selectors/date_picker.dart index 4fec86a3..2b3cd9b0 100644 --- a/lib/src/selectors/date_picker.dart +++ b/lib/src/selectors/date_picker.dart @@ -44,6 +44,33 @@ class MacosDatePicker extends StatefulWidget { this.style = DatePickerStyle.combined, required this.onDateChanged, this.initialDate, + // Use this to get the weekday abbreviations instead of + // localizations.narrowWeekdays() in order to match Apple's spec + this.weekdayAbbreviations = const [ + 'Su', + 'Mo', + 'Tu', + 'We', + 'Th', + 'Fr', + 'Sa', + ], + this.monthAbbreviations = const [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ], + this.dateFormat, + this.startWeekOnMonday, }); /// The [DatePickerStyle] to use. @@ -59,23 +86,38 @@ class MacosDatePicker extends StatefulWidget { /// Defaults to `DateTime.now()`. final DateTime? initialDate; + /// A list of 7 strings, one for each day of the week, starting with Sunday. + final List weekdayAbbreviations; + + /// A list of 12 strings, one for each month of the year, starting with January. + final List monthAbbreviations; + + /// Changes the way dates are displayed in the textual interface. + /// + /// The following tokens are supported (case-insensitive): + /// * `D`: day of the month (1-31) + /// * `DD`: day of the month (01-31) + /// * `M`: month of the year (1-12) + /// * `MM`: month of the year (01-12) + /// * `YYYY`: year (0000-9999) + /// * Any separator between tokens is preserved (e.g. `/`, `-`, `.`) + /// + /// Defaults to `M/D/YYYY`. + final String? dateFormat; + + /// Allows for changing the order of day headers in the graphical Date Picker + /// to Mo, Tu, We, Th, Fr, Sa, Su. + /// + /// This is useful for internationalization purposes, as many countries begin their weeks on Mondays. + /// + /// Defaults to `false`. + final bool? startWeekOnMonday; + @override State createState() => _MacosDatePickerState(); } class _MacosDatePickerState extends State { - // Use this to get the weekday abbreviations instead of - // localizations.narrowWeekdays() in order to match Apple's spec - static const List _narrowWeekdays = [ - 'Su', - 'Mo', - 'Tu', - 'We', - 'Th', - 'Fr', - 'Sa', - ]; - final _today = DateTime.now(); late final _initialDate = widget.initialDate ?? _today; @@ -153,13 +195,24 @@ class _MacosDatePickerState extends State { } // Creates the day headers - Su, Mo, Tu, We, Th, Fr, Sa + // or Mo, Tu, We, Th, Fr, Sa, Su depending on the value of [startWeekOnMonday] List _dayHeaders( TextStyle? headerStyle, MaterialLocalizations localizations, ) { final result = []; - for (int i = localizations.firstDayOfWeekIndex; true; i = (i + 1) % 7) { - final weekday = _narrowWeekdays[i]; + + // Hack due to invalid "firstDayOfWeekIndex" implementation in MaterialLocalizations + // issue: https://github.com/flutter/flutter/issues/122274 + // TODO: remove this workaround once the issue is fixed. + // Then, "firstDayOfWeekIndex" can be controlled by passing "localizationsDelegates" and "supportedLocales" to MacosApp + int firstDayOfWeekIndex = localizations.firstDayOfWeekIndex; + if (widget.startWeekOnMonday == true) { + firstDayOfWeekIndex = 1; + } + + for (int i = firstDayOfWeekIndex; result.length < 7; i = (i + 1) % 7) { + final weekday = widget.weekdayAbbreviations[i]; result.add( ExcludeSemantics( child: Center( @@ -170,11 +223,88 @@ class _MacosDatePickerState extends State { ), ), ); - if (i == (localizations.firstDayOfWeekIndex - 1) % 7) break; } return result; } + // Creates textual date presentation based on "dateFormat" property + List _textualDateElements() { + final separator = widget.dateFormat != null + ? widget.dateFormat!.toLowerCase().replaceAll(RegExp(r'[dmy]'), '')[0] + : '/'; + + final List dateElements = widget.dateFormat != null + ? widget.dateFormat!.toLowerCase().split(RegExp(r'[^dmy]')) + : ['m', 'd', 'y']; + + final List dateFields = []; + for (var dateElement in dateElements) { + if (dateElement.startsWith('d')) { + String value = dateElement == 'dd' && _selectedDay < 10 + // Add a leading zero + ? '0$_selectedDay' + : '$_selectedDay'; + + dateFields.add( + DatePickerFieldElement( + isSelected: _isDaySelected, + element: value, + onSelected: () { + setState(() { + _focusNode.requestFocus(); + _isDaySelected = !_isDaySelected; + _isMonthSelected = false; + _isYearSelected = false; + }); + }, + ), + ); + } else if (dateElement.startsWith('m')) { + String value = dateElement == 'mm' && _selectedMonth < 10 + // Add a leading zero + ? '0$_selectedMonth' + : '$_selectedMonth'; + + dateFields.add( + DatePickerFieldElement( + isSelected: _isMonthSelected, + element: value, + onSelected: () { + setState(() { + _focusNode.requestFocus(); + _isMonthSelected = !_isMonthSelected; + _isDaySelected = false; + _isYearSelected = false; + }); + }, + ), + ); + } else if (dateElement.startsWith('y')) { + dateFields.add( + DatePickerFieldElement( + isSelected: _isYearSelected, + element: '$_selectedYear', + onSelected: () { + setState(() { + _focusNode.requestFocus(); + _isYearSelected = !_isYearSelected; + _isDaySelected = false; + _isMonthSelected = false; + }); + }, + ), + ); + } + dateFields.add( + Text(separator), + ); + } + + dateFields.removeLast(); + + return dateFields; + } + Widget _buildTextualPicker(MacosDatePickerThemeData datePickerTheme) { return KeyboardShortcutRunner( onUpArrowKeypress: _incrementElement, @@ -195,46 +325,7 @@ class _MacosDatePickerState extends State { ), child: Row( mainAxisSize: MainAxisSize.min, - children: [ - DatePickerFieldElement( - isSelected: _isMonthSelected, - element: '$_selectedMonth', - onSelected: () { - setState(() { - _focusNode.requestFocus(); - _isMonthSelected = !_isMonthSelected; - _isDaySelected = false; - _isYearSelected = false; - }); - }, - ), - const Text('/'), - DatePickerFieldElement( - isSelected: _isDaySelected, - element: '$_selectedDay', - onSelected: () { - setState(() { - _focusNode.requestFocus(); - _isDaySelected = !_isDaySelected; - _isMonthSelected = false; - _isYearSelected = false; - }); - }, - ), - const Text('/'), - DatePickerFieldElement( - isSelected: _isYearSelected, - element: '$_selectedYear', - onSelected: () { - setState(() { - _focusNode.requestFocus(); - _isYearSelected = !_isYearSelected; - _isDaySelected = false; - _isMonthSelected = false; - }); - }, - ), - ], + children: _textualDateElements(), ), ), ), @@ -319,13 +410,14 @@ class _MacosDatePickerState extends State { child: Column( children: [ Padding( - padding: const EdgeInsets.fromLTRB(2.0, 2.0, 0.0, 4.0), + padding: + const EdgeInsets.only(left: 2.0, top: 2.0, bottom: 4.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( - '${intToMonthAbbr(_selectedMonth)} $_selectedYear', + '${widget.monthAbbreviations[_selectedMonth - 1]} $_selectedYear', style: const TextStyle( fontSize: 13.0, fontWeight: FontWeight.w700, @@ -353,9 +445,13 @@ class _MacosDatePickerState extends State { setState(() { _selectedYear--; _selectedMonth = 12; + _selectedDay = 1; }); } else { - setState(() => _selectedMonth--); + setState(() { + _selectedMonth--; + _selectedDay = 1; + }); } widget.onDateChanged.call(_formatAsDateTime()); }, @@ -396,9 +492,13 @@ class _MacosDatePickerState extends State { setState(() { _selectedYear++; _selectedMonth = 1; + _selectedDay = 1; }); } else { - setState(() => _selectedMonth++); + setState(() { + _selectedMonth++; + _selectedDay = 1; + }); } widget.onDateChanged.call(_formatAsDateTime()); @@ -410,7 +510,7 @@ class _MacosDatePickerState extends State { ), ), Padding( - padding: const EdgeInsets.fromLTRB(6.0, 0.0, 5.0, 0.0), + padding: const EdgeInsets.only(left: 6.0, right: 5.0), child: Column( children: [ GridView.custom( @@ -465,9 +565,19 @@ class _MacosDatePickerState extends State { ), localizations, ); + + // Hack due to invalid "firstDayOfWeekIndex" implementation in MaterialLocalizations + // issue: https://github.com/flutter/flutter/issues/122274 + // TODO: remove this workaround once the issue is fixed. + // Then, DateUtils.getDaysInMonth will work as expected when proper "localizationsDelegates" and "supportedLocales" are provided to MacosApp + int fixedDayOffset = dayOffset; + if (widget.startWeekOnMonday == true) { + fixedDayOffset = dayOffset - 1; + } + // 1-based day of month, e.g. 1-31 for January, and 1-29 for February on // a leap year. - int day = -dayOffset; + int day = -fixedDayOffset; final dayItems = []; @@ -496,7 +606,7 @@ class _MacosDatePickerState extends State { ); decoration = BoxDecoration( color: datePickerTheme.monthViewCurrentDateColor, - borderRadius: BorderRadius.circular(3.0), + borderRadius: const BorderRadius.all(Radius.circular(3.0)), ); } else if (isToday) { dayText = Text( @@ -512,11 +622,12 @@ class _MacosDatePickerState extends State { ); decoration = BoxDecoration( color: datePickerTheme.monthViewSelectedDateColor, - borderRadius: BorderRadius.circular(3.0), + borderRadius: const BorderRadius.all(Radius.circular(3.0)), ); } Widget dayWidget = GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () { setState(() { _isDaySelected = true; @@ -524,20 +635,18 @@ class _MacosDatePickerState extends State { }); widget.onDateChanged.call(_formatAsDateTime()); }, - child: Padding( + child: Container( + decoration: decoration, padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Container( - decoration: decoration, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 2.0), - child: dayText ?? - Text( - localizations.formatDecimal(day), - style: dayStyle, - ), - ), + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: 2.0), + child: dayText ?? + Text( + localizations.formatDecimal(day), + style: dayStyle, + ), ), ), ), diff --git a/lib/src/theme/icon_theme.dart b/lib/src/theme/icon_theme.dart index 9e7377c8..f3b801f7 100644 --- a/lib/src/theme/icon_theme.dart +++ b/lib/src/theme/icon_theme.dart @@ -210,7 +210,7 @@ class MacosIconThemeData with Diagnosticable { @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - properties.add(ColorProperty('color', color, defaultValue: null)); + properties.add(ColorProperty('MacosColor', color, defaultValue: null)); properties.add(DoubleProperty('opacity', opacity, defaultValue: null)); properties.add(DoubleProperty('size', size, defaultValue: null)); } diff --git a/lib/src/theme/macos_colors.dart b/lib/src/theme/macos_colors.dart index f5d92d80..73985d8f 100644 --- a/lib/src/theme/macos_colors.dart +++ b/lib/src/theme/macos_colors.dart @@ -85,6 +85,77 @@ class MacosColor extends Color { static int getAlphaFromOpacity(double opacity) { return (opacity.clamp(0.0, 1.0) * 255).round(); } + + /// Returns a new color that matches this color with the alpha channel + /// replaced with the given `opacity` (which ranges from 0.0 to 1.0). + /// + /// Out of range values will have unexpected effects. + @override + MacosColor withOpacity(double opacity) { + assert(opacity >= 0.0 && opacity <= 1.0); + return withAlpha((255.0 * opacity).round()); + } + + /// Returns a new color that matches this color with the alpha channel + /// replaced with `a` (which ranges from 0 to 255). + /// + /// Out of range values will have unexpected effects. + @override + MacosColor withAlpha(int a) { + return MacosColor.fromARGB(a, red, green, blue); + } + + /// Darkens a [MacosColor] by a [percent] amount (100 = black) without + /// changing the tint of the color. + static MacosColor darken(MacosColor c, [int percent = 10]) { + assert(1 <= percent && percent <= 100); + var f = 1 - percent / 100; + return MacosColor.fromARGB( + c.alpha, + (c.red * f).round(), + (c.green * f).round(), + (c.blue * f).round(), + ); + } + + /// Lightens a [MacosColor] by a [percent] amount (100 = white) without + /// changing the tint of the color + static MacosColor lighten(MacosColor c, [int percent = 10]) { + assert(1 <= percent && percent <= 100); + var p = percent / 100; + return MacosColor.fromARGB( + c.alpha, + c.red + ((255 - c.red) * p).round(), + c.green + ((255 - c.green) * p).round(), + c.blue + ((255 - c.blue) * p).round(), + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is MacosColor && other.value == value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return 'MacosColor(0x${value.toRadixString(16).padLeft(8, '0')})'; + } +} + +extension ColorX on Color { + /// Returns a [MacosColor] with the same color values as this [Color]. + MacosColor toMacosColor() { + return MacosColor(value); + } } /// A collection of color values lifted from the macOS system color picker. diff --git a/lib/src/theme/macos_theme.dart b/lib/src/theme/macos_theme.dart index ac03ea2d..87405882 100644 --- a/lib/src/theme/macos_theme.dart +++ b/lib/src/theme/macos_theme.dart @@ -3,8 +3,8 @@ import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; CupertinoDynamicColor _kScrollbarColor = CupertinoDynamicColor.withBrightness( - color: MacosColors.systemGrayColor.color.withOpacity(.8), - darkColor: MacosColors.systemGrayColor.darkColor.withOpacity(.8), + color: MacosColors.systemGrayColor.color.withOpacity(0.8), + darkColor: MacosColors.systemGrayColor.darkColor.withOpacity(0.8), ); /// Applies a macOS-style theme to descendant macOS widgets. @@ -213,19 +213,16 @@ class MacosThemeData with Diagnosticable { final Brightness _brightness = brightness ?? Brightness.light; final bool isDark = _brightness == Brightness.dark; primaryColor ??= MacosColors.controlAccentColor; + canvasColor ??= isDark - ? CupertinoColors.systemBackground.darkElevatedColor - : CupertinoColors.systemBackground; - typography ??= MacosTypography( - color: _brightness == Brightness.light - ? CupertinoColors.black - : CupertinoColors.white, - ); + ? const Color.fromRGBO(40, 40, 40, 1.0) + : const Color.fromRGBO(246, 246, 246, 1.0); + typography ??= + isDark ? MacosTypography.lightOpaque() : MacosTypography.darkOpaque(); pushButtonTheme ??= PushButtonThemeData( color: primaryColor, - secondaryColor: isDark - ? const Color.fromRGBO(56, 56, 56, 1.0) - : const Color.fromRGBO(218, 218, 223, 1.0), + secondaryColor: + isDark ? const Color.fromRGBO(110, 109, 112, 1.0) : MacosColors.white, disabledColor: isDark ? const Color.fromRGBO(255, 255, 255, 0.1) : const Color.fromRGBO(244, 245, 245, 1.0), diff --git a/lib/src/theme/overlay_filter.dart b/lib/src/theme/overlay_filter.dart index 2dd9b78e..13ad0ccb 100644 --- a/lib/src/theme/overlay_filter.dart +++ b/lib/src/theme/overlay_filter.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/layout/wallpaper_tinting_settings/wallpaper_tinting_override.dart'; import 'package:macos_ui/src/library.dart'; /// {@template macosOverlayFilter} @@ -36,41 +37,43 @@ class MacosOverlayFilter extends StatelessWidget { Widget build(BuildContext context) { final brightness = MacosTheme.brightnessOf(context); - return Container( - decoration: BoxDecoration( - color: color ?? - (brightness.isDark - ? const Color.fromRGBO(30, 30, 30, 1) - : const Color.fromRGBO(242, 242, 247, 1)), - boxShadow: [ - BoxShadow( - color: brightness - .resolve( - CupertinoColors.systemGrey.color, - CupertinoColors.black, - ) - .withOpacity(0.25), - offset: const Offset(0, 4), - spreadRadius: 4.0, - blurRadius: 8.0, - ), - ], - border: Border.all( - color: brightness.resolve( - CupertinoColors.systemGrey3.color, - CupertinoColors.systemGrey3.darkColor, + return WallpaperTintingOverride( + child: Container( + decoration: BoxDecoration( + color: color ?? + (brightness.isDark + ? const Color.fromRGBO(30, 30, 30, 1) + : const Color.fromRGBO(242, 242, 247, 1)), + boxShadow: [ + BoxShadow( + color: brightness + .resolve( + CupertinoColors.systemGrey.color, + CupertinoColors.black, + ) + .withOpacity(0.25), + offset: const Offset(0, 4), + spreadRadius: 4.0, + blurRadius: 8.0, + ), + ], + border: Border.all( + color: brightness.resolve( + CupertinoColors.systemGrey3.color, + CupertinoColors.systemGrey3.darkColor, + ), ), + borderRadius: borderRadius, ), - borderRadius: borderRadius, - ), - child: ClipRRect( - borderRadius: borderRadius, - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 20.0, - sigmaY: 20.0, + child: ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 20.0, + sigmaY: 20.0, + ), + child: child, ), - child: child, ), ), ); diff --git a/lib/src/theme/tooltip_theme.dart b/lib/src/theme/tooltip_theme.dart index b0958ea7..1551c8bd 100644 --- a/lib/src/theme/tooltip_theme.dart +++ b/lib/src/theme/tooltip_theme.dart @@ -89,7 +89,7 @@ class MacosTooltipThemeData with Diagnosticable { brightness.isDark ? CupertinoColors.white : CupertinoColors.black, ), decoration: () { - final radius = BorderRadius.circular(2.0); + const radius = BorderRadius.all(Radius.circular(2.0)); final shadow = [ BoxShadow( color: brightness.isDark diff --git a/lib/src/theme/typography.dart b/lib/src/theme/typography.dart index 670d694c..7b2f72a7 100644 --- a/lib/src/theme/typography.dart +++ b/lib/src/theme/typography.dart @@ -1,5 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; +import 'package:macos_ui/src/theme/macos_colors.dart'; +import 'package:macos_ui/src/theme/macos_theme.dart'; const _kDefaultFontFamily = '.AppleSystemUIFont'; @@ -16,8 +18,8 @@ const _kDefaultFontFamily = '.AppleSystemUIFont'; class MacosTypography with Diagnosticable { /// Creates a typography that uses the given values. /// - /// Rather than creating a new typography, consider using [MacosTypography.black] - /// or [MacosTypography.white]. + /// Rather than creating a new typography, consider using [MacosTypography.darkOpaque] + /// or [MacosTypography.lightOpaque]. /// /// If you do decide to create your own typography, consider using one of /// those predefined themes as a starting point for [copyWith]. @@ -65,18 +67,11 @@ class MacosTypography with Diagnosticable { ); headline ??= TextStyle( fontFamily: _kDefaultFontFamily, - fontWeight: FontWeight.w400, + fontWeight: FontWeight.w700, fontSize: 13, letterSpacing: -0.08, color: color, ); - subheadline ??= TextStyle( - fontFamily: _kDefaultFontFamily, - fontWeight: FontWeight.w400, - fontSize: 11, - letterSpacing: 0.06, - color: color, - ); body ??= TextStyle( fontFamily: _kDefaultFontFamily, fontWeight: FontWeight.w400, @@ -90,6 +85,13 @@ class MacosTypography with Diagnosticable { fontSize: 12, color: color, ); + subheadline ??= TextStyle( + fontFamily: _kDefaultFontFamily, + fontWeight: FontWeight.w400, + fontSize: 11, + letterSpacing: 0.06, + color: color, + ); footnote ??= TextStyle( fontFamily: _kDefaultFontFamily, fontWeight: FontWeight.w400, @@ -106,7 +108,7 @@ class MacosTypography with Diagnosticable { ); caption2 ??= TextStyle( fontFamily: _kDefaultFontFamily, - fontWeight: FontWeight.w400, + fontWeight: MacosFontWeight.w510, fontSize: 10, letterSpacing: 0.12, color: color, @@ -140,8 +142,10 @@ class MacosTypography with Diagnosticable { required this.caption2, }); - static MacosTypography black = MacosTypography(color: CupertinoColors.black); - static MacosTypography white = MacosTypography(color: CupertinoColors.white); + factory MacosTypography.darkOpaque() => + MacosTypography(color: MacosColors.labelColor.color); + factory MacosTypography.lightOpaque() => + MacosTypography(color: MacosColors.labelColor.darkColor); /// Style used for body text. final TextStyle body; @@ -228,10 +232,15 @@ class MacosTypography with Diagnosticable { ); } + static MacosTypography of(BuildContext context) { + final theme = MacosTheme.of(context); + return theme.typography; + } + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); - final defaultStyle = MacosTypography.black; + final defaultStyle = MacosTypography.darkOpaque(); properties.add(DiagnosticsProperty( 'largeTitle', largeTitle, @@ -289,3 +298,150 @@ class MacosTypography with Diagnosticable { )); } } + +/// The thickness of the glyphs used to draw the text. +/// +/// Implements [FontWeight] in order to provide the following custom weight +/// values that Apple use in some of their text styles: +/// * [w510] +/// * [w590] +/// * [w860] +/// +/// Reference: +/// * [macOS Sonoma Figma Kit](https://www.figma.com/file/IX6ph2VWrJiRoMTI1Byz0K/Apple-Design-Resources---macOS-(Community)?node-id=0%3A1745&mode=dev) +class MacosFontWeight implements FontWeight { + const MacosFontWeight._(this.index, this.value); + + /// The encoded integer value of this font weight. + @override + final int index; + + /// The thickness value of this font weight. + @override + final int value; + + /// Thin, the least thick + static const MacosFontWeight w100 = MacosFontWeight._(0, 100); + + /// Extra-light + static const MacosFontWeight w200 = MacosFontWeight._(1, 200); + + /// Light + static const MacosFontWeight w300 = MacosFontWeight._(2, 300); + + /// Normal / regular / plain + static const MacosFontWeight w400 = MacosFontWeight._(3, 400); + + /// Medium + static const MacosFontWeight w500 = MacosFontWeight._(4, 500); + + /// An Apple-specific font weight. + /// + /// When [MacosTypography.caption1] needs to be bolded, use this value. + static const MacosFontWeight w510 = MacosFontWeight._(5, 510); + + /// An Apple-specific font weight. + /// + /// When [MacosTypography.body], [MacosTypography.callout], + /// [MacosTypography.subheadline], [MacosTypography.footnote], or + /// [MacosTypography.caption2] need to be bolded, use this value. + static const MacosFontWeight w590 = MacosFontWeight._(6, 590); + + /// Semi-bold + static const MacosFontWeight w600 = MacosFontWeight._(7, 600); + + /// Bold + static const MacosFontWeight w700 = MacosFontWeight._(8, 700); + + /// Extra-bold + static const MacosFontWeight w800 = MacosFontWeight._(9, 800); + + /// An Apple-specific font weight. + /// + /// When [MacosTypography.title3] needs to be bolded, use this value. + static const MacosFontWeight w860 = MacosFontWeight._(10, 860); + + /// Black, the most thick + static const MacosFontWeight w900 = MacosFontWeight._(11, 900); + + /// The default font weight. + static const MacosFontWeight normal = w400; + + /// A commonly used font weight that is heavier than normal. + static const MacosFontWeight bold = w700; + + /// A list of all the font weights. + static const List values = [ + w100, + w200, + w300, + w400, + w500, + w510, + w590, + w600, + w700, + w800, + w860, + w900, + ]; + + /// Linearly interpolates between two font weights. + /// + /// Rather than using fractional weights, the interpolation rounds to the + /// nearest weight. + /// + /// If both `a` and `b` are null, then this method will return null. Otherwise, + /// any null values for `a` or `b` are interpreted as equivalent to [normal] + /// (also known as [w400]). + /// + /// The `t` argument represents position on the timeline, with 0.0 meaning + /// that the interpolation has not started, returning `a` (or something + /// equivalent to `a`), 1.0 meaning that the interpolation has finished, + /// returning `b` (or something equivalent to `b`), and values in between + /// meaning that the interpolation is at the relevant point on the timeline + /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and + /// 1.0, so negative values and values greater than 1.0 are valid (and can + /// easily be generated by curves such as [Curves.elasticInOut]). The result + /// is clamped to the range [w100]–[w900]. + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + static MacosFontWeight? lerp( + MacosFontWeight? a, + MacosFontWeight? b, + double t, + ) { + if (a == null && b == null) { + return null; + } + return values[_lerpInt((a ?? normal).index, (b ?? normal).index, t) + .round() + .clamp(0, 8)]; + } + + @override + String toString() { + return const { + 0: 'MacosFontWeight.w100', + 1: 'MacosFontWeight.w200', + 2: 'MacosFontWeight.w300', + 3: 'MacosFontWeight.w400', + 4: 'MacosFontWeight.w500', + 5: 'MacosFontWeight.w510', + 6: 'MacosFontWeight.w590', + 7: 'MacosFontWeight.w600', + 8: 'MacosFontWeight.w700', + 9: 'MacosFontWeight.w800', + 10: 'MacosFontWeight.w860', + 11: 'MacosFontWeight.w900', + }[index]!; + } +} + +/// Linearly interpolate between two integers. +/// +/// Same as [lerpDouble] but specialized for non-null `int` type. +double _lerpInt(int a, int b, double t) { + return a + (b - a) * t; +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 08f2debc..1f2d8a1e 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,3 +1,6 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; @@ -57,39 +60,29 @@ Color iconLuminance(Color backgroundColor, bool isDark) { } } -String intToMonthAbbr(int month) { - switch (month) { - case 1: - return 'Jan'; - case 2: - return 'Feb'; - case 3: - return 'Mar'; - case 4: - return 'Apr'; - case 5: - return 'May'; - case 6: - return 'Jun'; - case 7: - return 'Jul'; - case 8: - return 'Aug'; - case 9: - return 'Sep'; - case 10: - return 'Oct'; - case 11: - return 'Nov'; - case 12: - return 'Dec'; - default: - throw Exception('Unsupported value'); - } -} - class Unsupported { const Unsupported(this.message); final String message; } + +/// A class that ensures that the application's macOS window's brightness +/// matches the given brightness. +class MacOSBrightnessOverrideHandler { + static Brightness? _lastBrightness; + + /// Ensures that the application's macOS window's brightness matches + /// [currentBrightness]. + /// + /// For performance reasons, the brightness setting will only be overridden if + /// [currentBrightness] differs from the value it had when this method was + /// previously called. Therefore, it is safe to call this method frequently. + static void ensureMatchingBrightness(Brightness currentBrightness) { + if (kIsWeb) return; + if (!Platform.isMacOS) return; + if (currentBrightness == _lastBrightness) return; + + WindowManipulator.overrideMacOSBrightness(dark: currentBrightness.isDark); + _lastBrightness = currentBrightness; + } +} diff --git a/pubspec.lock b/pubspec.lock index 6cfa9f14..f80cd1d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,50 +5,34 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" url: "https://pub.dev" source: hosted - version: "52.0.0" + version: "59.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 url: "https://pub.dev" source: hosted - version: "5.4.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d - url: "https://pub.dev" - source: hosted - version: "0.11.2" - ansicolor: - dependency: transitive - description: - name: ansicolor - sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "5.11.1" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -61,10 +45,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -77,10 +61,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -101,42 +85,10 @@ packages: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.dev" - source: hosted - version: "3.0.2" - csslib: - dependency: transitive - description: - name: csslib - sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 - url: "https://pub.dev" - source: hosted - version: "0.17.2" - dart_code_metrics: - dependency: "direct dev" - description: - name: dart_code_metrics - sha256: "026e28da197a03caeccccc0b174ec98ef03da3c81c4543314d7add121aab4375" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "5.6.0" - dart_code_metrics_presets: - dependency: transitive - description: - name: dart_code_metrics_presets - sha256: "9c51724f836aebc4465228954cb5757e5a99737af26a452b5dec0a2d5d0b4d66" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - dart_style: - dependency: transitive - description: - name: dart_style - sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" - url: "https://pub.dev" - source: hosted - version: "2.2.4" + version: "3.0.3" fake_async: dependency: transitive description: @@ -162,10 +114,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -187,22 +139,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - html: - dependency: transitive - description: - name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 - url: "https://pub.dev" - source: hosted - version: "0.15.1" - http: - dependency: transitive - description: - name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.dev" - source: hosted - version: "0.13.5" http_multi_server: dependency: transitive description: @@ -231,26 +167,18 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" - json_annotation: - dependency: transitive - description: - name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 - url: "https://pub.dev" - source: hosted - version: "4.8.0" + version: "0.6.7" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + sha256: "6b0206b0bf4f04961fc5438198ccb3a885685cd67d4d4a32cc20ad7f8adbe015" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" logging: dependency: transitive description: @@ -259,14 +187,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + macos_window_utils: + dependency: "direct main" + description: + name: macos_window_utils + sha256: b78a210aa70ca7ccad6e7b7b810fb4689c507f4a46e299214900b2a1eb70ea23 + url: "https://pub.dev" + source: hosted + version: "1.1.3" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -279,10 +215,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -303,10 +239,10 @@ packages: dependency: transitive description: name: node_preamble - sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" package_config: dependency: transitive description: @@ -319,26 +255,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" - url: "https://pub.dev" - source: hosted - version: "5.1.0" - platform: - dependency: transitive - description: - name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" - url: "https://pub.dev" - source: hosted - version: "3.1.0" + version: "1.8.3" pool: dependency: transitive description: @@ -347,62 +267,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" - pub_updater: - dependency: transitive - description: - name: pub_updater - sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" - url: "https://pub.dev" - source: hosted - version: "0.2.4" + version: "2.1.4" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" shelf_static: dependency: transitive description: name: shelf_static - sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -468,34 +372,34 @@ packages: dependency: transitive description: name: test - sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.22.0" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.4.20" + version: "0.5.1" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" vector_math: dependency: transitive description: @@ -508,26 +412,26 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: f3743ca475e0c9ef71df4ba15eb2d7684eecd5c8ba20a462462e4e8b561b2e11 url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "11.6.0" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webkit_inspection_protocol: dependency: transitive description: @@ -536,22 +440,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - xml: - dependency: transitive - description: - name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" - url: "https://pub.dev" - source: hosted - version: "6.2.2" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.18.0 <3.0.0" - flutter: ">=1.20.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4d660503..13865b31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,22 +1,22 @@ name: macos_ui description: Flutter widgets and themes implementing the current macOS design language. -version: 1.12.2 +version: 2.0.0 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" environment: - sdk: ">=2.17.0 <3.0.0" - flutter: ">=1.20.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: sdk: flutter + macos_window_utils: ^1.1.3 dev_dependencies: flutter_test: sdk: flutter - dart_code_metrics: ^5.6.0 - flutter_lints: ^2.0.1 + flutter_lints: ^2.0.2 mocktail: ^0.3.0 flutter: diff --git a/test/buttons/back_button_test.dart b/test/buttons/back_button_test.dart index 90e85f65..cd709ee1 100644 --- a/test/buttons/back_button_test.dart +++ b/test/buttons/back_button_test.dart @@ -44,6 +44,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( toolBar: ToolBar( leading: MacosBackButton( diff --git a/test/buttons/checkbox_test.dart b/test/buttons/checkbox_test.dart index 09881007..7ba24553 100644 --- a/test/buttons/checkbox_test.dart +++ b/test/buttons/checkbox_test.dart @@ -62,7 +62,7 @@ void main() { [ 'state: "unchecked"', 'enabled', - 'size: 16.0', + 'size: 14.0', 'activeColor: null', 'disabledColor: quaternaryLabel(*color = Color(0x2d3c3c43)*, darkColor = Color(0x28ebebf5), highContrastColor = Color(0x423c3c43), darkHighContrastColor = Color(0x3debebf5), resolved by: UNRESOLVED)', 'offBorderColor: tertiaryLabel(*color = Color(0x4c3c3c43)*, darkColor = Color(0x4cebebf5), highContrastColor = Color(0x603c3c43), darkHighContrastColor = Color(0x60ebebf5), resolved by: UNRESOLVED)', diff --git a/test/buttons/help_button_test.dart b/test/buttons/help_button_test.dart index 2f11f430..a18b7cf9 100644 --- a/test/buttons/help_button_test.dart +++ b/test/buttons/help_button_test.dart @@ -52,6 +52,7 @@ void main() { helpButtonTheme: darkHelpButtonThemeData, ), home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/buttons/icon_button_test.dart b/test/buttons/icon_button_test.dart index 057f6a3b..8e52e106 100644 --- a/test/buttons/icon_button_test.dart +++ b/test/buttons/icon_button_test.dart @@ -45,6 +45,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/buttons/pulldown_button_test.dart b/test/buttons/pulldown_button_test.dart index e22a355e..668914d0 100644 --- a/test/buttons/pulldown_button_test.dart +++ b/test/buttons/pulldown_button_test.dart @@ -75,7 +75,7 @@ void main() { MacosPulldownMenuItem( title: const Text('one'), onTap: () { - menuItemTapCounters[0] += 1; + menuItemTapCounters.first += 1; }, ), MacosPulldownMenuItem( diff --git a/test/buttons/push_button_test.dart b/test/buttons/push_button_test.dart index 96dc571e..8bb294a0 100644 --- a/test/buttons/push_button_test.dart +++ b/test/buttons/push_button_test.dart @@ -27,7 +27,7 @@ void main() { ContentArea( builder: (context, _) { return PushButton( - buttonSize: ButtonSize.small, + controlSize: ControlSize.regular, onPressed: mockOnPressedFunction.handler, child: const Text('Push me'), ); @@ -55,12 +55,13 @@ void main() { pushButtonTheme: darkPushButtonThemeData, ), home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( builder: (context, _) { return PushButton( - buttonSize: ButtonSize.small, + controlSize: ControlSize.regular, key: pushButtonKey, onPressed: mockOnTapCancelFunction.handler, child: const Text('Push me'), @@ -83,7 +84,7 @@ void main() { testWidgets('debugFillProperties', (tester) async { final builder = DiagnosticPropertiesBuilder(); const PushButton( - buttonSize: ButtonSize.small, + controlSize: ControlSize.regular, child: Text('Test'), ).debugFillProperties(builder); @@ -95,7 +96,7 @@ void main() { expect( description, [ - 'buttonSize: small', + 'controlSize: regular', 'color: null', 'disabledColor: null', 'pressedOpacity: 0.4', @@ -103,7 +104,7 @@ void main() { 'semanticLabel: null', 'borderRadius: BorderRadius.circular(4.0)', 'disabled', - 'isSecondary: null', + 'secondary: null', ], ); }); diff --git a/test/buttons/radio_button_test.dart b/test/buttons/radio_button_test.dart index d561b643..7d878d1c 100644 --- a/test/buttons/radio_button_test.dart +++ b/test/buttons/radio_button_test.dart @@ -17,6 +17,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/buttons/switch_test.dart b/test/buttons/switch_test.dart index 642f0f11..a02bc40b 100644 --- a/test/buttons/switch_test.dart +++ b/test/buttons/switch_test.dart @@ -52,10 +52,12 @@ void main() { description, [ 'unchecked', + 'size: regular', 'dragStartBehavior: start', 'disabled', 'activeColor: null', 'trackColor: null', + 'knobColor: null', 'semanticLabel: null', ], ); diff --git a/test/indicators/relevance_indicator_test.dart b/test/indicators/relevance_indicator_test.dart deleted file mode 100644 index dbdea9ba..00000000 --- a/test/indicators/relevance_indicator_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:macos_ui/macos_ui.dart'; - -void main() { - testWidgets('debugFillProperties', (tester) async { - final builder = DiagnosticPropertiesBuilder(); - const RelevanceIndicator( - value: 50, - amount: 100, - ).debugFillProperties(builder); - - final description = builder.properties - .where((node) => !node.isFiltered(DiagnosticLevel.info)) - .map((node) => node.toString()) - .toList(); - - expect( - description, - [ - 'value: 50', - 'amount: 100', - 'barHeight: 20.0', - 'barWidth: 0.8', - 'selectedColor: label(*color = Color(0xff000000)*, darkColor = Color(0xffffffff), resolved by: UNRESOLVED)', - 'unselectedColor: secondaryLabel(*color = Color(0x993c3c43)*, darkColor = Color(0x99ebebf5), highContrastColor = Color(0xad3c3c43), darkHighContrastColor = Color(0xadebebf5), resolved by: UNRESOLVED)', - 'semanticLabel: null', - ], - ); - }); -} diff --git a/test/indicators/scrollbar_test.dart b/test/indicators/scrollbar_test.dart index f5ffe2c2..1173e096 100644 --- a/test/indicators/scrollbar_test.dart +++ b/test/indicators/scrollbar_test.dart @@ -16,8 +16,8 @@ void main() { testWidgets( 'Scrollbar changes position when scrolled with the mouse wheel', (tester) async { - final Size screenSize = tester.binding.window.physicalSize / - tester.binding.window.devicePixelRatio; + final Size screenSize = + tester.view.physicalSize / tester.view.devicePixelRatio; await tester.pumpWidget( Directionality( diff --git a/test/indicators/slider_test.dart b/test/indicators/slider_test.dart index 23bb0b35..99f963ff 100644 --- a/test/indicators/slider_test.dart +++ b/test/indicators/slider_test.dart @@ -4,8 +4,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:macos_ui/macos_ui.dart'; void main() { - final TestWidgetsFlutterBinding binding = - TestWidgetsFlutterBinding.ensureInitialized(); testWidgets('debugFillProperties', (tester) async { final builder = DiagnosticPropertiesBuilder(); MacosSlider( @@ -36,8 +34,8 @@ void main() { }); testWidgets('Continuous slider can move when tapped', (tester) async { - tester.binding.window.physicalSizeTestValue = const Size(100, 50); - binding.window.devicePixelRatioTestValue = 1.0; + tester.view.physicalSize = const Size(100, 50); + tester.view.devicePixelRatio = 1.0; final value = ValueNotifier(0.25); await tester.pumpWidget( @@ -65,12 +63,12 @@ void main() { await tester.pumpAndSettle(); expect(value.value, 0.0); - addTearDown(tester.binding.window.clearPhysicalSizeTestValue); + addTearDown(tester.view.resetPhysicalSize); }); testWidgets('Discrete slider snaps to correct values', (widgetTester) async { - widgetTester.binding.window.physicalSizeTestValue = const Size(100, 50); - binding.window.devicePixelRatioTestValue = 1.0; + widgetTester.view.physicalSize = const Size(100, 50); + widgetTester.view.devicePixelRatio = 1.0; final value = ValueNotifier(0.25); await widgetTester.pumpWidget( @@ -109,6 +107,6 @@ void main() { expect(value.value, 0.5); - addTearDown(widgetTester.binding.window.clearPhysicalSizeTestValue); + addTearDown(widgetTester.view.resetPhysicalSize); }); } diff --git a/test/layout/macos_list_tile_test.dart b/test/layout/macos_list_tile_test.dart index e019a007..b8b65f7f 100644 --- a/test/layout/macos_list_tile_test.dart +++ b/test/layout/macos_list_tile_test.dart @@ -20,6 +20,7 @@ void main() { pushButtonTheme: const PushButtonThemeData(), ), home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/layout/resizeable_pane_test.dart b/test/layout/resizeable_pane_test.dart index 86638a6a..f3ac9f83 100644 --- a/test/layout/resizeable_pane_test.dart +++ b/test/layout/resizeable_pane_test.dart @@ -6,6 +6,10 @@ void main() { const matrix = ResizableSide.values; group('ResizablePane', () { + const double maxSize = 300; + const double minSize = 100; + const double startSize = 200; + for (var side in matrix) { bool verticallyResizable = side == ResizableSide.top; @@ -14,10 +18,6 @@ void main() { ? 'top' : (side == ResizableSide.left ? 'left' : 'right'), () { - const double maxSize = 300; - const double minSize = 100; - const double startSize = 200; - final resizablePane = ResizablePane( builder: (context, scrollController) => const Text('Hello there'), minSize: minSize, @@ -29,6 +29,231 @@ void main() { final view = side == ResizableSide.top ? MacosApp( home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, scrollController) { + return Column( + children: [ + const Flexible( + fit: FlexFit.loose, + child: Center( + child: Text('Hello there'), + ), + ), + resizablePane, + ], + ); + }, + ), + ], + ), + ), + ) + : MacosApp( + home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + resizablePane, + ContentArea( + builder: (context, scrollController) { + return const Text('Hello there'); + }, + ), + ], + ), + ), + ); + + final resizablePaneFinder = find.byWidget(resizablePane); + final dragFinder = find.descendant( + of: resizablePaneFinder, + matching: find.byType(GestureDetector), + ); + + // No need to check if the resizable side is top because directionModifier + // would take -1 if it is the case + final directionModifier = side == ResizableSide.right ? 1 : -1; + final double safeDelta = 50.0 * directionModifier; + final double overflowDelta = 500.0 * directionModifier; + + testWidgets( + 'Default ResizablePane Constructor comes with an internal MacosScrollBar', + (WidgetTester tester) async { + await tester.pumpWidget(view); + expect(find.byType(MacosScrollbar), findsOneWidget); + }, + ); + + testWidgets('initial size equals startSize', (tester) async { + await tester.pumpWidget(view); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var initialSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + + expect(initialSize, startSize); + }); + + testWidgets('dragging wider works $side', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable ? Offset(0, safeDelta) : Offset(safeDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + expect( + verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width, + startSize + safeDelta * directionModifier, + ); + }); + + testWidgets('dragging wider respects maxSize', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, overflowDelta) + : Offset(overflowDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, maxSize); + }); + + testWidgets( + 'drag events past maxSize have no effect $side', + (tester) async { + await tester.pumpWidget(view); + + final dragStartLocation = tester.getCenter(dragFinder); + final drag = await tester.startGesture(dragStartLocation); + await drag.moveBy( + verticallyResizable + ? Offset(0, overflowDelta) + : Offset(overflowDelta, 0), + ); + await drag.moveBy( + verticallyResizable + ? Offset(0, -10.0 * directionModifier) + : Offset(-10.0 * directionModifier, 0), + ); + await drag.up(); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, maxSize); + }, + ); + + testWidgets('dragging narrower works', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, -safeDelta) + : Offset(-safeDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect( + currentSize, + startSize - safeDelta * directionModifier, + ); + }); + + testWidgets('dragging narrower respects minSize', (tester) async { + await tester.pumpWidget(view); + + await tester.drag( + dragFinder, + verticallyResizable + ? Offset(0, -overflowDelta) + : Offset(-overflowDelta, 0), + ); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, minSize); + }); + + testWidgets( + 'drag events past minSize have no effect', + (tester) async { + await tester.pumpWidget(view); + + final dragStartLocation = tester.getCenter(dragFinder); + final drag = await tester.startGesture(dragStartLocation); + await drag.moveBy( + verticallyResizable + ? Offset(0, -overflowDelta) + : Offset(-overflowDelta, 0), + ); + await drag.moveBy( + verticallyResizable + ? Offset(0, 10.0 * directionModifier) + : Offset(10.0 * directionModifier, 0), + ); + await drag.up(); + await tester.pump(); + + var resizablePaneRenderObject = + tester.renderObject(resizablePaneFinder); + var currentSize = verticallyResizable + ? resizablePaneRenderObject.size.height + : resizablePaneRenderObject.size.width; + expect(currentSize, minSize); + }, + ); + }, + ); + group( + side == ResizableSide.top + ? 'top' + : (side == ResizableSide.left ? 'left' : 'right'), + () { + final resizablePane = ResizablePane.noScrollBar( + minSize: minSize, + startSize: startSize, + maxSize: maxSize, + resizableSide: side, + child: const Text('Hello there'), + ); + + final view = side == ResizableSide.top + ? MacosApp( + home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( @@ -52,6 +277,7 @@ void main() { ) : MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ resizablePane, @@ -77,6 +303,14 @@ void main() { final double safeDelta = 50.0 * directionModifier; final double overflowDelta = 500.0 * directionModifier; + testWidgets( + 'ResizablePane.noScrollBar Constructor does not come with an internal MacosScrollBar', + (WidgetTester tester) async { + await tester.pumpWidget(view); + expect(find.byType(MacosScrollbar), findsNothing); + }, + ); + testWidgets('initial size equals startSize', (tester) async { await tester.pumpWidget(view); diff --git a/test/layout/sliver_toolbar_test.dart b/test/layout/sliver_toolbar_test.dart index bcfeb9d4..0d693771 100644 --- a/test/layout/sliver_toolbar_test.dart +++ b/test/layout/sliver_toolbar_test.dart @@ -81,6 +81,8 @@ void main() { tester.getBottomLeft(find.byKey(leadingKey)), const Offset(8.0, 47.0), ); + + await tester.pump(Duration.zero); }, ); @@ -97,6 +99,8 @@ void main() { expect(tester.getTopLeft(navToolbar).dy, 4.0); expect(tester.getSize(toolbar).height, 52.0); expect(tester.getSize(navToolbar).height, 43.0); + + await tester.pump(Duration.zero); }, ); @@ -116,6 +120,8 @@ void main() { expect(tester.getTopLeft(toolbar).dy, 0.0); expect(tester.getTopLeft(navToolbar).dy, 4.0); + + await tester.pump(Duration.zero); }, ); @@ -137,6 +143,8 @@ void main() { expect(toolbar, findsNothing); expect(navToolbar, findsNothing); + + await tester.pump(Duration.zero); }, ); @@ -177,6 +185,8 @@ void main() { expect(tester.getTopLeft(toolbar).dy, 0.0); expect(navToolbar, findsOneWidget); expect(tester.getTopLeft(navToolbar).dy, 4.0); + + await tester.pump(Duration.zero); }, ); } diff --git a/test/layout/window_test.dart b/test/layout/window_test.dart index 262b5ded..92f1a80a 100644 --- a/test/layout/window_test.dart +++ b/test/layout/window_test.dart @@ -30,6 +30,7 @@ void main() { viewBuilder(Sidebar sidebar) { return MacosApp( home: MacosWindow( + disableWallpaperTinting: true, sidebar: sidebar, child: const MacosScaffold( children: [], @@ -70,6 +71,8 @@ void main() { await tester.pumpWidget(view); expectSidebarOpen(tester, width: startWidth); + + await tester.pump(Duration.zero); }); test('dragClosedBuffer defaults to half minWidth', () { @@ -84,6 +87,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: startWidth + safeDelta); + + await tester.pump(Duration.zero); }); testWidgets('dragging wider respects maxWidth', (tester) async { @@ -93,6 +98,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: maxWidth); + + await tester.pump(Duration.zero); }); testWidgets('drag events past maxWidth have no effect', (tester) async { @@ -106,6 +113,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: maxWidth); + + await tester.pump(Duration.zero); }); testWidgets('dragging narrower works', (tester) async { @@ -115,6 +124,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: startWidth - safeDelta); + + await tester.pump(Duration.zero); }); group('when dragClosed is true', () { @@ -135,6 +146,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: minWidth); + + await tester.pump(Duration.zero); }, ); testWidgets( @@ -146,6 +159,8 @@ void main() { await tester.pump(); expectSidebarClosed(tester); + + await tester.pump(Duration.zero); }, ); @@ -172,6 +187,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: startWidth - safeDelta); + + await tester.pump(Duration.zero); }, ); @@ -186,6 +203,8 @@ void main() { await tester.pump(); expectSidebarClosed(tester); + + await tester.pump(Duration.zero); }); }); @@ -199,6 +218,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: minWidth); + + await tester.pump(Duration.zero); }, ); @@ -213,6 +234,8 @@ void main() { await tester.pump(); expectSidebarOpen(tester, width: minWidth); + + await tester.pump(Duration.zero); }); }); @@ -243,6 +266,8 @@ void main() { tester, width: startWidth + snapToStartBuffer * 2, ); + + await tester.pump(Duration.zero); }, ); @@ -273,6 +298,8 @@ void main() { ); await tester.pump(); expectSidebarOpen(tester, width: startWidth); + + await tester.pump(Duration.zero); }, ); }); diff --git a/test/selectors/date_picker_test.dart b/test/selectors/date_picker_test.dart index f0c5afbc..f0996546 100644 --- a/test/selectors/date_picker_test.dart +++ b/test/selectors/date_picker_test.dart @@ -12,6 +12,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( @@ -50,6 +51,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( @@ -80,6 +82,61 @@ void main() { }, ); + testWidgets( + 'Textual MacosDatePicker renders the date with respect to "dateFormat" property', + (tester) async { + renderWidget(String dateFormat) => MacosApp( + home: MacosWindow( + disableWallpaperTinting: true, + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: MacosDatePicker( + initialDate: DateTime.parse('2023-04-01'), + onDateChanged: (date) {}, + dateFormat: dateFormat, + style: DatePickerStyle.textual, + ), + ); + }, + ), + ], + ), + ), + ); + + getNthTextFromWidget(int index) => + (find.byType(Text).at(index).evaluate().first.widget as Text).data + as String; + + await tester.pumpWidget(renderWidget('dd.mm.yyyy')); + String firstDateElement = getNthTextFromWidget(0); + expect(firstDateElement, '01'); + String secondDateElement = getNthTextFromWidget(1); + expect(secondDateElement, '.'); + String thirdDateElement = getNthTextFromWidget(2); + expect(thirdDateElement, '04'); + String fourthDateElement = getNthTextFromWidget(3); + expect(fourthDateElement, '.'); + String fifthDateElement = getNthTextFromWidget(4); + expect(fifthDateElement, '2023'); + + await tester.pumpWidget(renderWidget('yyyy-m-d')); + firstDateElement = getNthTextFromWidget(0); + expect(firstDateElement, '2023'); + secondDateElement = getNthTextFromWidget(1); + expect(secondDateElement, '-'); + thirdDateElement = getNthTextFromWidget(2); + expect(thirdDateElement, '4'); + fourthDateElement = getNthTextFromWidget(3); + expect(fourthDateElement, '-'); + fifthDateElement = getNthTextFromWidget(4); + expect(fifthDateElement, '1'); + }, + ); + testWidgets( 'Can select the date field element and change the value', (tester) async { @@ -305,5 +362,159 @@ void main() { } }, ); + + testWidgets( + 'Graphical MacosDatePicker renders abbreviations based on "weekdayAbbreviations" and "monthAbbreviations" properties', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: MacosDatePicker( + initialDate: DateTime.parse('2023-04-01'), + onDateChanged: (date) {}, + weekdayAbbreviations: const [ + 'Nd', + 'Po', + 'Wt', + 'Śr', + 'Cz', + 'Pt', + 'So', + ], + monthAbbreviations: const [ + 'Sty', + 'Lut', + 'Mar', + 'Kwi', + 'Maj', + 'Cze', + 'Lip', + 'Sie', + 'Wrz', + 'Paź', + 'Lis', + 'Gru', + ], + ), + ); + }, + ), + ], + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('Kwi 2023'), findsOneWidget); + expect(find.text('Nd'), findsOneWidget); + expect(find.text('Po'), findsOneWidget); + expect(find.text('Wt'), findsOneWidget); + expect(find.text('Śr'), findsOneWidget); + expect(find.text('Cz'), findsOneWidget); + expect(find.text('Pt'), findsOneWidget); + expect(find.text('So'), findsOneWidget); + }, + ); }); + + testWidgets( + 'Graphical MacosDatePicker with "startWeekOnMonday" set to true shows Monday as the first day of the week', + (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: MacosDatePicker( + startWeekOnMonday: true, + initialDate: DateTime.parse('2023-04-01'), + onDateChanged: (date) {}, + ), + ); + }, + ), + ], + ), + ), + ), + ); + + final dayHeadersRow = find.byType(GridView).first; + final dayHeaders = find.descendant( + of: dayHeadersRow, + matching: find.byType(Text), + ); + final firstWeekday = dayHeaders.first; + final firstWeekdayText = + (firstWeekday.evaluate().first.widget as Text).data; + await tester.pumpAndSettle(); + + expect(firstWeekdayText, 'Mo'); + + final calendarGrid = find.byType(GridView).last; + final dayOffsetWidgets = find.descendant( + of: calendarGrid, + matching: find.byType(SizedBox), + ); + final dayOffset = dayOffsetWidgets.evaluate().length; + + expect(dayOffset, 5); + }, + ); + + // Regression test due to invalid "firstDayOfWeekIndex" implementation in MaterialLocalizations + // issue: https://github.com/flutter/flutter/issues/122274 + // TODO: remove this once the issue is fixed and test starts failing + testWidgets( + 'Graphical MacosDatePicker still needs "startWeekOnMonday" to show Monday as the first day of the week, even when the locale is set to something other than "en_US"', + (tester) async { + await tester.pumpWidget( + MacosApp( + supportedLocales: const [ + Locale('en', 'PL'), + ], + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: MacosDatePicker( + startWeekOnMonday: true, + initialDate: DateTime.parse('2023-04-01'), + onDateChanged: (date) {}, + ), + ); + }, + ), + ], + ), + ), + ), + ); + + final dayHeadersRow = find.byType(GridView).first; + final dayHeaders = find.descendant( + of: dayHeadersRow, + matching: find.byType(Text), + ); + final firstWeekday = dayHeaders.first; + final firstWeekdayText = + (firstWeekday.evaluate().first.widget as Text).data; + await tester.pumpAndSettle(); + + // The result will be 'Tu' if the fix is no longer needed and can be removed + expect(firstWeekdayText, 'Mo'); + }, + ); } diff --git a/test/theme/help_button_theme_test.dart b/test/theme/help_button_theme_test.dart index 702a85ed..1e127f80 100644 --- a/test/theme/help_button_theme_test.dart +++ b/test/theme/help_button_theme_test.dart @@ -45,8 +45,8 @@ void main() { expect( description, [ - 'color: Color(0xff0433ff)', - 'disabledColor: Color(0xff8e8e93)', + 'color: MacosColor(0xff0433ff)', + 'disabledColor: MacosColor(0xff8e8e93)', ], ); }); @@ -56,6 +56,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/theme/icon_button_theme_test.dart b/test/theme/icon_button_theme_test.dart index af7a40e6..7d42593e 100644 --- a/test/theme/icon_button_theme_test.dart +++ b/test/theme/icon_button_theme_test.dart @@ -63,6 +63,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/theme/icon_theme_test.dart b/test/theme/icon_theme_test.dart index 8e98f80a..79076b18 100644 --- a/test/theme/icon_theme_test.dart +++ b/test/theme/icon_theme_test.dart @@ -51,7 +51,7 @@ void main() { expect( description, [ - 'color: Color(0xffffffff)', + 'MacosColor: MacosColor(0xffffffff)', 'opacity: 0.0', 'size: 20.0', ], @@ -63,6 +63,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/theme/popup_button_theme_test.dart b/test/theme/popup_button_theme_test.dart index daa039de..7cbe6b7a 100644 --- a/test/theme/popup_button_theme_test.dart +++ b/test/theme/popup_button_theme_test.dart @@ -54,8 +54,8 @@ void main() { expect( description, [ - 'highlightColor: Color(0xff8e8e93)', - 'backgroundColor: Color(0xff0433ff)', + 'highlightColor: MacosColor(0xff8e8e93)', + 'backgroundColor: MacosColor(0xff0433ff)', 'popupColor: Color(0x19000000)', ], ); @@ -67,6 +67,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/theme/pulldown_button_theme_test.dart b/test/theme/pulldown_button_theme_test.dart index 373ac0c4..87fc9666 100644 --- a/test/theme/pulldown_button_theme_test.dart +++ b/test/theme/pulldown_button_theme_test.dart @@ -53,10 +53,10 @@ void main() { expect( description, [ - 'highlightColor: Color(0xff8e8e93)', - 'backgroundColor: Color(0xff0433ff)', + 'highlightColor: MacosColor(0xff8e8e93)', + 'backgroundColor: MacosColor(0xff0433ff)', 'pulldownColor: Color(0x19000000)', - 'iconColor: Color(0xff00f900)', + 'iconColor: MacosColor(0xff00f900)', ], ); }); @@ -66,6 +66,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( diff --git a/test/theme/push_button_theme_test.dart b/test/theme/push_button_theme_test.dart index 5bcc4ef1..2ff90243 100644 --- a/test/theme/push_button_theme_test.dart +++ b/test/theme/push_button_theme_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/library.dart'; void main() { group('PushButton theme tests', () { @@ -46,8 +46,8 @@ void main() { expect( description, [ - 'color: Color(0xff0433ff)', - 'disabledColor: Color(0xff8e8e93)', + 'color: MacosColor(0xff0433ff)', + 'disabledColor: MacosColor(0xff8e8e93)', 'secondaryColor: Color(0x19000000)', ], ); @@ -58,13 +58,14 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea( builder: (context, _) { capturedContext = context; return const PushButton( - buttonSize: ButtonSize.small, + controlSize: ControlSize.regular, child: Text('Push me'), ); }, @@ -77,8 +78,8 @@ void main() { final theme = PushButtonTheme.of(capturedContext); expect(theme.color, const Color(0xff007aff)); - expect(theme.disabledColor, const Color(0xfff4f5f5)); - expect(theme.secondaryColor, const Color(0xffdadadf)); + expect(theme.disabledColor, const Color.fromRGBO(244, 245, 245, 1.0)); + expect(theme.secondaryColor, MacosColors.white); }); }); } diff --git a/test/theme/search_field_theme_test.dart b/test/theme/search_field_theme_test.dart index da838f37..6f0851b9 100644 --- a/test/theme/search_field_theme_test.dart +++ b/test/theme/search_field_theme_test.dart @@ -62,6 +62,7 @@ void main() { await tester.pumpWidget( MacosApp( home: MacosWindow( + disableWallpaperTinting: true, child: MacosScaffold( children: [ ContentArea(