diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c59bae9..fd70a111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [2.0.2] +### 🛠️ Fixed 🛠️ +* Fixed images in generated documentation. + +## [2.0.1] +### 🔄 Updated 🔄 +* `PushButton` has received a facelift. It now mimics the look and feel of native macOS buttons more closely. + * **Note:** As a result, its `pressedOpacity` property and the `PushButtonTheme` class have been deprecated. + ## [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: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 15075873..97e7a96c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,8 +25,7 @@ This repository uses [conventional commits](https://www.conventionalcommits.org/ ## Pull Requests As mentioned above, all pull requests should target `dev`. -Before opening your pull request, please ensure that the following -following requirements are met: +Before opening your pull request, please ensure that the following requirements are met: * You have run `flutter pub get` at the package level * You have incremented the version number in `pubspec.yaml` properly * You have updated the `CHANGELOG.md` file accordingly @@ -38,4 +37,4 @@ A note for authorized maintainers: Pull requests should **always** be merged via ### Versioning -`macos_ui` uses semantic versioning. Please ensure your updates follow this method accordingly. \ No newline at end of file +`macos_ui` uses semantic versioning. Please ensure your updates follow this method accordingly. diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index 0179c12c..1e64f18d 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,12 +5,14 @@ import FlutterMacOS import Foundation +import appkit_ui_element_colors import macos_ui import macos_window_utils import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index 2fa81174..6aa1d86e 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - appkit_ui_element_colors (1.0.0): + - FlutterMacOS - FlutterMacOS (1.0.0) - macos_ui (0.1.0): - FlutterMacOS @@ -11,6 +13,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - appkit_ui_element_colors (from `Flutter/ephemeral/.symlinks/plugins/appkit_ui_element_colors/macos`) - 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`) @@ -18,6 +21,8 @@ DEPENDENCIES: - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: + appkit_ui_element_colors: + :path: Flutter/ephemeral/.symlinks/plugins/appkit_ui_element_colors/macos FlutterMacOS: :path: Flutter/ephemeral macos_ui: @@ -30,6 +35,7 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: + appkit_ui_element_colors: 39bb2d80be3f19b152ccf4c70d5bbe6cba43d74a FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 macos_ui: 6229a8922cd97bafb7d9636c8eb8dfb0744183ca macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 @@ -38,4 +44,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: ff0a9a3ce75ee73f200ca7e2f47745698c917ef9 -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.1 diff --git a/example/pubspec.lock b/example/pubspec.lock index dc586a2a..253f08cb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + appkit_ui_element_colors: + dependency: transitive + description: + name: appkit_ui_element_colors + sha256: c3e50f900aae314d339de489535736238627071457c4a4a2dbbb1545b4f04f22 + url: "https://pub.dev" + source: hosted + version: "1.0.0" async: dependency: transitive description: @@ -37,10 +45,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" crypto: dependency: transitive description: @@ -57,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -112,6 +128,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.1.0" + gradient_borders: + dependency: transitive + description: + name: gradient_borders + sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309" + url: "https://pub.dev" + source: hosted + version: "1.0.0" http: dependency: transitive description: @@ -128,14 +152,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" lints: dependency: transitive description: @@ -150,31 +166,31 @@ packages: path: ".." relative: true source: path - version: "2.0.0" + version: "2.0.2" macos_window_utils: dependency: transitive description: name: macos_window_utils - sha256: b78a210aa70ca7ccad6e7b7b810fb4689c507f4a46e299214900b2a1eb70ea23 + sha256: "43a90473f8786f00f07203e6819dab67e032f8896dafa4a6f85fbc71fba32c0b" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -288,10 +304,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -328,10 +344,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" typed_data: dependency: transitive description: @@ -412,6 +428,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" win32: dependency: transitive description: @@ -429,5 +453,5 @@ packages: source: hosted version: "1.0.0" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/lib/src/buttons/popup_button.dart b/lib/src/buttons/popup_button.dart index 8158e63c..37c6559b 100644 --- a/lib/src/buttons/popup_button.dart +++ b/lib/src/buttons/popup_button.dart @@ -1194,10 +1194,13 @@ class _MacosPopupButtonState extends State> } hintIndex = items.length; - items.add(IgnorePointer( - ignoringSemantics: false, - child: displayedHint, - )); + items.add( + ExcludeSemantics( + child: IgnorePointer( + child: displayedHint, + ), + ), + ); } // If value is null (then _selectedIndex is null) then we diff --git a/lib/src/buttons/push_button.dart b/lib/src/buttons/push_button.dart index 19180016..df0cf4fe 100644 --- a/lib/src/buttons/push_button.dart +++ b/lib/src/buttons/push_button.dart @@ -2,7 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'package:gradient_borders/gradient_borders.dart'; import 'package:macos_ui/macos_ui.dart'; +import 'package:macos_ui/src/enums/accent_color.dart'; import 'package:macos_ui/src/library.dart'; const _kMiniButtonSize = Size(26.0, 11.0); @@ -169,6 +171,9 @@ class PushButton extends StatefulWidget { /// /// This defaults to 0.4. If null, opacity will not change on pressed if using /// your own custom effects is desired. + @Deprecated("'PushButton' animations now match their native macOS’ " + "counterparts. Therefore, its opacity no longer changes when it is " + "pressed.") final double? pressedOpacity; /// The radius of the button's corners when it has a background color. @@ -195,8 +200,7 @@ 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 overridden if the [color] attribute is non-null. + /// 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 @@ -209,7 +213,6 @@ class PushButton extends StatefulWidget { properties.add(EnumProperty('controlSize', controlSize)); properties.add(ColorProperty('color', color)); properties.add(ColorProperty('disabledColor', disabledColor)); - properties.add(DoubleProperty('pressedOpacity', pressedOpacity)); properties.add(DiagnosticsProperty('alignment', alignment)); properties.add(StringProperty('semanticLabel', semanticLabel)); properties.add(DiagnosticsProperty('borderRadius', borderRadius)); @@ -227,104 +230,102 @@ class PushButton extends StatefulWidget { class PushButtonState extends State with SingleTickerProviderStateMixin { - // Eyeballed values. Feel free to tweak. - static const Duration kFadeOutDuration = Duration(milliseconds: 10); - static const Duration kFadeInDuration = Duration(milliseconds: 100); - final Tween _opacityTween = Tween(begin: 1.0); - - late AnimationController _animationController; - late Animation _opacityAnimation; - - @override - void initState() { - super.initState(); - _animationController = AnimationController( - duration: const Duration(milliseconds: 200), - value: 0.0, - vsync: this, - ); - _opacityAnimation = _animationController - .drive(CurveTween(curve: Curves.decelerate)) - .drive(_opacityTween); - _setTween(); - } - @override void didUpdateWidget(PushButton oldWidget) { super.didUpdateWidget(oldWidget); - _setTween(); - } - - void _setTween() { - _opacityTween.end = widget.pressedOpacity ?? 1.0; } void _handleTapDown(TapDownDetails event) { if (!buttonHeldDown) { - buttonHeldDown = true; - _animate(); + setState(() => buttonHeldDown = true); } } void _handleTapUp(TapUpDetails event) { if (buttonHeldDown) { - buttonHeldDown = false; - _animate(); + setState(() => buttonHeldDown = false); } } void _handleTapCancel() { if (buttonHeldDown) { - buttonHeldDown = false; - _animate(); + setState(() => buttonHeldDown = false); } } - void _animate() { - if (_animationController.isAnimating) return; - final bool wasHeldDown = buttonHeldDown; - final TickerFuture ticker = buttonHeldDown - ? _animationController.animateTo(1.0, duration: kFadeOutDuration) - : _animationController.animateTo(0.0, duration: kFadeInDuration); - ticker.then((void value) { - if (mounted && wasHeldDown != buttonHeldDown) _animate(); - }); - } - - @override - void dispose() { - _animationController.dispose(); - super.dispose(); - } - @visibleForTesting bool buttonHeldDown = false; - @override - Widget build(BuildContext context) { - assert(debugCheckHasMacosTheme(context)); + AccentColor get _accentColor => + AccentColorListener.instance.currentAccentColor ?? AccentColor.blue; + + BoxDecoration _getBoxDecoration() { + // If the window isn’t currently the main window (that is, it is not in + // focus), make the button look as if it was a secondary button. + final isMainWindow = WindowMainStateListener.instance.isMainWindow; + + return _BoxDecorationBuilder.buildBoxDecoration( + accentColor: _accentColor, + isEnabled: widget.enabled, + isDarkModeEnabled: MacosTheme.of(context).brightness.isDark, + isSecondary: !isMainWindow || (widget.secondary ?? false), + ); + } + + Color _getBackgroundColor() { final bool enabled = widget.enabled; final bool isSecondary = widget.secondary != null && widget.secondary!; final MacosThemeData theme = MacosTheme.of(context); - final Color backgroundColor = MacosDynamicColor.resolve( + + // If the window isn’t currently the main window (that is, it is not in + // focus), make the button look as if it was a secondary button. + final isWindowMain = WindowMainStateListener.instance.isMainWindow; + + return MacosDynamicColor.resolve( widget.color ?? - (isSecondary - ? theme.pushButtonTheme.secondaryColor! - : theme.pushButtonTheme.color!), + _BoxDecorationBuilder.getGradientColors( + accentColor: _accentColor, + isEnabled: enabled, + isDarkModeEnabled: theme.brightness.isDark, + isSecondary: isSecondary || !isWindowMain, + ).first, context, ); + } - final disabledColor = !isSecondary - ? backgroundColor.withOpacity(0.5) - : backgroundColor.withOpacity(0.25); + Color _getForegroundColor(Color backgroundColor) { + final MacosThemeData theme = MacosTheme.of(context); - final Color foregroundColor = widget.enabled - ? textLuminance(backgroundColor) - : theme.brightness.isDark - ? const Color.fromRGBO(255, 255, 255, 0.25) - : const Color.fromRGBO(0, 0, 0, 0.25); + final blendedBackgroundColor = Color.lerp( + theme.canvasColor, + backgroundColor, + backgroundColor.opacity, + )!; - final baseStyle = theme.typography.body.copyWith(color: foregroundColor); + return widget.enabled + ? textLuminance(blendedBackgroundColor) + : textLuminance(blendedBackgroundColor).withOpacity(0.25); + } + + BoxDecoration _getClickEffectBoxDecoration() { + final MacosThemeData theme = MacosTheme.of(context); + final isDark = theme.brightness.isDark; + + final color = isDark + ? const MacosColor.fromRGBO(255, 255, 255, 0.15) + : const MacosColor.fromRGBO(0, 0, 0, 0.06); + + return BoxDecoration( + color: color, + borderRadius: widget.controlSize.borderRadius, + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMacosTheme(context)); + final bool enabled = widget.enabled; + final MacosThemeData theme = MacosTheme.of(context); return MouseRegion( cursor: widget.mouseCursor!, @@ -339,29 +340,45 @@ class PushButtonState extends State label: widget.semanticLabel, child: ConstrainedBox( constraints: widget.controlSize.constraints, - child: FadeTransition( - opacity: _opacityAnimation, - child: DecoratedBox( - decoration: ShapeDecoration( - shape: RoundedRectangleBorder( - borderRadius: widget.controlSize.borderRadius, - ), - // color: !enabled ? disabledColor : backgroundColor, - color: enabled ? backgroundColor : disabledColor, - ), - child: Padding( - padding: widget.controlSize.padding, - child: Align( - alignment: widget.alignment, - widthFactor: 1.0, - heightFactor: 1.0, - child: DefaultTextStyle( - style: widget.controlSize.textStyle(baseStyle), - child: widget.child, - ), - ), - ), - ), + child: StreamBuilder( + stream: AccentColorListener.instance.onChanged, + builder: (context, _) { + return StreamBuilder( + stream: WindowMainStateListener.instance.onChanged, + builder: (context, _) { + final Color backgroundColor = _getBackgroundColor(); + + final Color foregroundColor = + _getForegroundColor(backgroundColor); + + final baseStyle = + theme.typography.body.copyWith(color: foregroundColor); + + return DecoratedBox( + decoration: _getBoxDecoration().copyWith( + borderRadius: widget.controlSize.borderRadius, + ), + child: Container( + foregroundDecoration: buttonHeldDown + ? _getClickEffectBoxDecoration() + : const BoxDecoration(), + child: Padding( + padding: widget.controlSize.padding, + child: Align( + alignment: widget.alignment, + widthFactor: 1.0, + heightFactor: 1.0, + child: DefaultTextStyle( + style: widget.controlSize.textStyle(baseStyle), + child: widget.child, + ), + ), + ), + ), + ); + }, + ); + }, ), ), ), @@ -369,3 +386,317 @@ class PushButtonState extends State ); } } + +class _BoxDecorationBuilder { + /// Gets the colors to use for the [BoxDecoration]’s gradient based on the + /// provided [accentColor], [isEnabled], and [isDarkModeEnabled] properties. + static List getGradientColors({ + required AccentColor accentColor, + required bool isEnabled, + required bool isDarkModeEnabled, + required bool isSecondary, + }) { + final isEnabledFactor = isEnabled ? 1.0 : 0.5; + + if (isSecondary) { + return isDarkModeEnabled + ? [ + MacosColor.fromRGBO(255, 255, 255, 0.251 * isEnabledFactor), + MacosColor.fromRGBO(255, 255, 255, 0.251 * isEnabledFactor), + ] + : [ + MacosColor.fromRGBO(255, 255, 255, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(255, 255, 255, 1.0 * isEnabledFactor), + ]; + } + + if (isDarkModeEnabled) { + switch (accentColor) { + case AccentColor.blue: + return [ + MacosColor.fromRGBO(0, 114, 238, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(0, 94, 211, 1.0 * isEnabledFactor), + ]; + + case AccentColor.purple: + return [ + MacosColor.fromRGBO(135, 65, 131, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(120, 57, 116, 1.0 * isEnabledFactor), + ]; + + case AccentColor.pink: + return [ + MacosColor.fromRGBO(188, 52, 105, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(168, 46, 93, 1.0 * isEnabledFactor), + ]; + + case AccentColor.red: + return [ + MacosColor.fromRGBO(186, 53, 46, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(166, 48, 41, 1.0 * isEnabledFactor), + ]; + + case AccentColor.orange: + return [ + MacosColor.fromRGBO(212, 133, 33, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(189, 118, 30, 1.0 * isEnabledFactor), + ]; + + case AccentColor.yellow: + return [ + MacosColor.fromRGBO(229, 203, 35, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(204, 179, 21, 1.0 * isEnabledFactor), + ]; + + case AccentColor.green: + return [ + MacosColor.fromRGBO(58, 138, 46, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(52, 123, 39, 1.0 * isEnabledFactor), + ]; + + case AccentColor.graphite: + return [ + MacosColor.fromRGBO(64, 64, 64, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(57, 57, 57, 1.0 * isEnabledFactor), + ]; + + default: + throw UnimplementedError(); + } + } else { + switch (accentColor) { + case AccentColor.blue: + return [ + MacosColor.fromRGBO(39, 125, 255, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(1, 101, 255, 1.0 * isEnabledFactor), + ]; + + case AccentColor.purple: + return [ + MacosColor.fromRGBO(148, 73, 143, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(128, 39, 121, 1.0 * isEnabledFactor), + ]; + + case AccentColor.pink: + return [ + MacosColor.fromRGBO(212, 71, 125, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(203, 36, 101, 1.0 * isEnabledFactor), + ]; + + case AccentColor.red: + return [ + MacosColor.fromRGBO(198, 64, 57, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(188, 29, 21, 1.0 * isEnabledFactor), + ]; + + case AccentColor.orange: + return [ + MacosColor.fromRGBO(237, 154, 51, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(234, 136, 13, 1.0 * isEnabledFactor), + ]; + + case AccentColor.yellow: + return [ + MacosColor.fromRGBO(242, 211, 61, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(240, 203, 25, 1.0 * isEnabledFactor), + ]; + + case AccentColor.green: + return [ + MacosColor.fromRGBO(77, 161, 63, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(45, 143, 28, 1.0 * isEnabledFactor), + ]; + + case AccentColor.graphite: + return [ + MacosColor.fromRGBO(86, 86, 86, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(55, 55, 55, 1.0 * isEnabledFactor), + ]; + + default: + throw UnimplementedError(); + } + } + } + + /// Gets the shadow to use for the [BoxDecoration] based on the provided + /// [accentColor], [isEnabled], and [isDarkModeEnabled] properties. + static List _getShadow({ + required AccentColor accentColor, + required bool isEnabled, + required bool isDarkModeEnabled, + required bool isSecondary, + }) { + final isEnabledFactor = isEnabled ? 1.0 : 0.5; + + if (isSecondary) { + return isDarkModeEnabled + ? [ + BoxShadow( + color: MacosColor.fromRGBO(0, 0, 0, 0.4 * isEnabledFactor), + blurRadius: 0.5, + offset: Offset.zero, + spreadRadius: 0.0, + blurStyle: BlurStyle.outer, + ), + ] + : [ + BoxShadow( + color: MacosColor.fromRGBO(0, 0, 0, 0.4 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + } + + if (isDarkModeEnabled) { + return [ + BoxShadow( + color: MacosColor.fromRGBO(0, 0, 0, 0.4 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + } else { + switch (accentColor) { + case AccentColor.blue: + return [ + BoxShadow( + color: MacosColor.fromRGBO(0, 103, 255, 0.21 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.purple: + return [ + BoxShadow( + color: MacosColor.fromRGBO(139, 29, 125, 0.21 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.pink: + return [ + BoxShadow( + color: MacosColor.fromRGBO(222, 0, 101, 0.21 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.red: + return [ + BoxShadow( + color: MacosColor.fromRGBO(188, 29, 21, 0.35 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.orange: + return [ + BoxShadow( + color: MacosColor.fromRGBO(234, 136, 13, 0.35 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.yellow: + return [ + BoxShadow( + color: MacosColor.fromRGBO(240, 203, 25, 0.35 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.green: + return [ + BoxShadow( + color: MacosColor.fromRGBO(45, 143, 28, 0.35 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + case AccentColor.graphite: + return [ + BoxShadow( + color: MacosColor.fromRGBO(55, 55, 55, 0.35 * isEnabledFactor), + blurRadius: 0.5, + offset: isEnabled ? const Offset(0.0, 0.3) : Offset.zero, + spreadRadius: 0.0, + blurStyle: isEnabled ? BlurStyle.normal : BlurStyle.outer, + ), + ]; + + default: + throw UnimplementedError(); + } + } + } + + /// Builds a [BoxDecoration] for a [MacosPushButton]. + static BoxDecoration buildBoxDecoration({ + required AccentColor accentColor, + required bool isEnabled, + required bool isDarkModeEnabled, + required bool isSecondary, + }) { + final isEnabledFactor = isEnabled ? 1.0 : 0.5; + + return BoxDecoration( + border: isDarkModeEnabled + ? GradientBoxBorder( + gradient: LinearGradient( + colors: [ + MacosColor.fromRGBO(255, 255, 255, 0.43 * isEnabledFactor), + const MacosColor.fromRGBO(255, 255, 255, 0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.2], + ), + width: 0.7, + ) + : null, + gradient: LinearGradient( + colors: getGradientColors( + accentColor: accentColor, + isEnabled: isEnabled, + isDarkModeEnabled: isDarkModeEnabled, + isSecondary: isSecondary, + ), + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + boxShadow: _getShadow( + accentColor: accentColor, + isEnabled: isEnabled, + isDarkModeEnabled: isDarkModeEnabled, + isSecondary: isSecondary, + ), + ); + } +} diff --git a/lib/src/enums/accent_color.dart b/lib/src/enums/accent_color.dart new file mode 100644 index 00000000..95ab8ea2 --- /dev/null +++ b/lib/src/enums/accent_color.dart @@ -0,0 +1,27 @@ +/// The macOS accent color which can be changed by the user in *System Settings* +/// → *Appearance* → *Accent color*. +enum AccentColor { + /// The blue accent color. + blue, + + /// The purple accent color. + purple, + + /// The pink accent color. + pink, + + /// The red accent color. + red, + + /// The orange accent color. + orange, + + /// The yellow accent color. + yellow, + + /// The green accent color. + green, + + /// The graphite accent color. + graphite, +} diff --git a/lib/src/indicators/slider.dart b/lib/src/indicators/slider.dart index 9e5e7514..e32fc360 100644 --- a/lib/src/indicators/slider.dart +++ b/lib/src/indicators/slider.dart @@ -20,7 +20,6 @@ const double _kDiscreteThumbBorderRadius = 8; /// The slider doesn't maintain any state itself, instead the user is expected to /// update this widget with a new [value] whenever the slider changes. /// -/// {@image } /// {@endtemplate} class MacosSlider extends StatelessWidget { /// {@macro macosSlider} diff --git a/lib/src/layout/sidebar/sidebar_item.dart b/lib/src/layout/sidebar/sidebar_item.dart index b7e2d487..1454ccbd 100644 --- a/lib/src/layout/sidebar/sidebar_item.dart +++ b/lib/src/layout/sidebar/sidebar_item.dart @@ -62,7 +62,7 @@ class SidebarItem with Diagnosticable { /// /// Typically a text indicator of a count of items, like in this /// screenshots from the Apple Notes app: - /// {@image } + /// final Widget? trailing; @override diff --git a/lib/src/layout/tab_view/tab_view.dart b/lib/src/layout/tab_view/tab_view.dart index fa32c734..cca58d2a 100644 --- a/lib/src/layout/tab_view/tab_view.dart +++ b/lib/src/layout/tab_view/tab_view.dart @@ -26,7 +26,7 @@ enum MacosTabPosition { /// {@template macosTabView} /// A multipage interface that displays one page at a time. /// -/// {@image } +/// /// /// A tab view contains a row of navigational items, [tabs], that move the /// user through the provided views ([children]). The user selects the desired diff --git a/lib/src/library.dart b/lib/src/library.dart index 37fb1228..2273d447 100644 --- a/lib/src/library.dart +++ b/lib/src/library.dart @@ -32,4 +32,4 @@ export 'package:flutter/material.dart' MaterialState; export 'package:flutter/widgets.dart'; -export 'utils.dart'; +export 'utils/utils.dart'; diff --git a/lib/src/theme/macos_theme.dart b/lib/src/theme/macos_theme.dart index 87405882..0bda4d11 100644 --- a/lib/src/theme/macos_theme.dart +++ b/lib/src/theme/macos_theme.dart @@ -514,8 +514,7 @@ class MacosThemeData with Diagnosticable { typography: MacosTypography.lerp(a.typography, b.typography, t), helpButtonTheme: HelpButtonThemeData.lerp(a.helpButtonTheme, b.helpButtonTheme, t), - pushButtonTheme: - PushButtonThemeData.lerp(a.pushButtonTheme, b.pushButtonTheme, t), + pushButtonTheme: a.pushButtonTheme, tooltipTheme: MacosTooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t), visualDensity: VisualDensity.lerp(a.visualDensity, b.visualDensity, t), @@ -581,7 +580,7 @@ class MacosThemeData with Diagnosticable { canvasColor: canvasColor ?? this.canvasColor, dividerColor: dividerColor ?? this.dividerColor, typography: this.typography.merge(typography), - pushButtonTheme: this.pushButtonTheme.merge(pushButtonTheme), + pushButtonTheme: this.pushButtonTheme, helpButtonTheme: this.helpButtonTheme.merge(helpButtonTheme), tooltipTheme: this.tooltipTheme.merge(tooltipTheme), visualDensity: visualDensity ?? this.visualDensity, @@ -605,7 +604,7 @@ class MacosThemeData with Diagnosticable { canvasColor: other.canvasColor, dividerColor: other.dividerColor, typography: typography.merge(other.typography), - pushButtonTheme: pushButtonTheme.merge(other.pushButtonTheme), + pushButtonTheme: pushButtonTheme, helpButtonTheme: helpButtonTheme.merge(other.helpButtonTheme), tooltipTheme: tooltipTheme.merge(other.tooltipTheme), visualDensity: other.visualDensity, diff --git a/lib/src/theme/push_button_theme.dart b/lib/src/theme/push_button_theme.dart index c7d27039..0c69aebd 100644 --- a/lib/src/theme/push_button_theme.dart +++ b/lib/src/theme/push_button_theme.dart @@ -9,8 +9,6 @@ import 'package:macos_ui/src/library.dart'; /// * [PushButtonThemeData], which is used to configure this theme. class PushButtonTheme extends InheritedTheme { /// Create a [PushButtonTheme]. - /// - /// The [data] parameter must not be null. const PushButtonTheme({ super.key, required this.data, @@ -18,6 +16,9 @@ class PushButtonTheme extends InheritedTheme { }); /// The configuration of this theme. + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") final PushButtonThemeData data; /// The closest instance of this class that encloses the given context. @@ -30,6 +31,9 @@ class PushButtonTheme extends InheritedTheme { /// ```dart /// PushButtonTheme theme = PushButtonTheme.of(context); /// ``` + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") static PushButtonThemeData of(BuildContext context) { final PushButtonTheme? buttonTheme = context.dependOnInheritedWidgetOfExactType(); @@ -37,11 +41,17 @@ class PushButtonTheme extends InheritedTheme { } @override + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") Widget wrap(BuildContext context, Widget child) { return PushButtonTheme(data: data, child: child); } @override + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") bool updateShouldNotify(PushButtonTheme oldWidget) => data != oldWidget.data; } @@ -63,15 +73,27 @@ class PushButtonThemeData with Diagnosticable { }); /// The default background color for [PushButton] + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") final Color? color; /// The default disabled color for [PushButton] + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") final Color? disabledColor; /// The default secondary color (e.g. Cancel/Go back buttons) for [PushButton] + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") final Color? secondaryColor; /// Copies this [PushButtonThemeData] into another. + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") PushButtonThemeData copyWith({ Color? color, Color? disabledColor, @@ -87,6 +109,9 @@ class PushButtonThemeData with Diagnosticable { /// Linearly interpolate between two [PushButtonThemeData]. /// /// All the properties must be non-null. + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") static PushButtonThemeData lerp( PushButtonThemeData a, PushButtonThemeData b, @@ -100,6 +125,9 @@ class PushButtonThemeData with Diagnosticable { } @override + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") bool operator ==(Object other) => identical(this, other) || other is PushButtonThemeData && @@ -109,9 +137,15 @@ class PushButtonThemeData with Diagnosticable { secondaryColor?.value == other.secondaryColor?.value; @override + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") int get hashCode => color.hashCode ^ disabledColor.hashCode; @override + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(ColorProperty('color', color)); @@ -120,6 +154,9 @@ class PushButtonThemeData with Diagnosticable { } /// Merges this [PushButtonThemeData] with another. + @Deprecated( + "'PushButton' no longer uses singular colors and therefore cannot " + "be themed using a 'PushButtonTheme'.") PushButtonThemeData merge(PushButtonThemeData? other) { if (other == null) return this; return copyWith( diff --git a/lib/src/utils/accent_color_listener.dart b/lib/src/utils/accent_color_listener.dart new file mode 100644 index 00000000..5965e3ab --- /dev/null +++ b/lib/src/utils/accent_color_listener.dart @@ -0,0 +1,160 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:appkit_ui_element_colors/appkit_ui_element_colors.dart'; +import 'package:flutter/foundation.dart'; +import 'package:macos_ui/src/enums/accent_color.dart'; + +/// A class that listens for changes to the user’s selected system accent color. +/// +/// Native macOS applications respond to such changes immediately. +/// +/// Example using [StreamBuilder]: +/// +/// ```dart +/// StreamBuilder( +/// stream: AccentColorListener.instance.onChanged, +/// builder: (context, _) { +/// final AccentColor? accentColor = +/// AccentColorListener.instance.currentAccentColor; +/// +/// return SomeWidget( +/// accentColor: accentColor, +/// child: … +/// ); +/// }, +/// ); +/// ``` +class AccentColorListener { + /// A class that listens to accent color changes. + AccentColorListener() { + _init(); + } + + /// A shared instance of [AccentColorListener]. + static final instance = AccentColorListener(); + + /// A map which maps hue components of the [UiElementColor.controlAccentColor] + /// color captured with the [NSAppearanceName.aqua] appearance in the + /// [NSColorSpace.genericRGB] color space to the corresponding [AccentColor]. + static final hueComponentToAccentColor = { + 0.6085324903200698: AccentColor.blue, + 0.8285987697113538: AccentColor.purple, + 0.9209523937489168: AccentColor.pink, + 0.9861913496946438: AccentColor.red, + 0.06543037411201169: AccentColor.orange, + 0.11813830353929083: AccentColor.yellow, + 0.29428158007138466: AccentColor.green, + 0.0: AccentColor.graphite, + }; + + /// The active accent color selection. + AccentColor? _currentAccentColor; + + /// The currently active accent color. + AccentColor? get currentAccentColor => _currentAccentColor; + + /// Notifies listeners when the accent color changes. + final _accentColorStreamController = StreamController.broadcast(); + + /// Streams the user’s system accent color selection. + /// + /// Emits a new value whenever the system accent color selection changes. + Stream get onChanged => _accentColorStreamController.stream; + + /// A stream subscription for the [SystemColorObserver] stream. + StreamSubscription? _systemColorObserverStreamSubscription; + + /// Initializes this class. + void _init() { + if (kIsWeb) return; + if (!Platform.isMacOS) return; + + _initCurrentAccentColor(); + _initSystemColorObserver(); + } + + /// Disposes this listener. + void dispose() { + _systemColorObserverStreamSubscription?.cancel(); + } + + /// Initializes the current accent color. This method is to be called whenever + /// a change is detected. + Future _initCurrentAccentColor() async { + final hueComponent = await _getHueComponent(); + _currentAccentColor = _resolveAccentColorFromHueComponent(hueComponent); + _accentColorStreamController.add(null); + } + + /// Initializes the current system color observer. This method may only be + /// called once. + void _initSystemColorObserver() { + assert(_systemColorObserverStreamSubscription == null); + + _systemColorObserverStreamSubscription = + AppkitUiElementColors.systemColorObserver.stream.listen((_) { + _initCurrentAccentColor(); + _accentColorStreamController.add(null); + }); + } + + /// Returns the hue component of the active accent color selection on macOS. + Future _getHueComponent() async { + final color = await AppkitUiElementColors.getColorComponents( + uiElementColor: UiElementColor.controlAccentColor, + components: const { + NSColorComponent.hueComponent, + }, + colorSpace: NSColorSpace.genericRGB, + appearance: NSAppearanceName.aqua, + ); + + assert(color.containsKey("hueComponent")); + + return color["hueComponent"]!; + } + + /// Returns the [AccentColor] which corresponds to the provided + /// [hueComponent]. + AccentColor _resolveAccentColorFromHueComponent(double hueComponent) { + if (hueComponentToAccentColor.containsKey(hueComponent)) { + return hueComponentToAccentColor[hueComponent]!; + } + + debugPrint( + 'Warning: Falling back on slow accent color resolution. It’s possible ' + 'that the accent colors have changed in a recent version of macOS, thus ' + 'invalidating macos_ui’s accent colors, which were captured on macOS ' + 'Ventura. If you see this message, please notify a maintainer of the ' + 'macos_ui package.', + ); + + return _slowlyResolveAccentColorFromHueComponent(hueComponent); + } + + /// This is a fallback method in case [_resolveAccentColorFromHueComponent] + /// fails. + AccentColor _slowlyResolveAccentColorFromHueComponent(double hueComponent) { + final entries = hueComponentToAccentColor.entries; + var lowestDistance = double.maxFinite; + var toBeReturnedAccentColor = AccentColor.values.first; + + for (final entry in entries) { + final distance = _distanceBetweenHueComponents(hueComponent, entry.key); + if (distance < lowestDistance) { + lowestDistance = distance; + toBeReturnedAccentColor = entry.value; + } + } + + return toBeReturnedAccentColor; + } + + /// Returns the distance between two hue components. + double _distanceBetweenHueComponents(double component1, double component2) { + final rawDifference = (component1 - component2).abs(); + return sin(rawDifference * pi); + } +} diff --git a/lib/src/utils/macos_brightness_override_handler.dart b/lib/src/utils/macos_brightness_override_handler.dart new file mode 100644 index 00000000..3fad912b --- /dev/null +++ b/lib/src/utils/macos_brightness_override_handler.dart @@ -0,0 +1,25 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:macos_ui/macos_ui.dart'; + +/// 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/lib/src/utils.dart b/lib/src/utils/utils.dart similarity index 95% rename from lib/src/utils.dart rename to lib/src/utils/utils.dart index 1f2d8a1e..29be5e12 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils/utils.dart @@ -4,6 +4,10 @@ import 'package:flutter/foundation.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; +export 'window_main_state_listener.dart'; +export 'accent_color_listener.dart'; +export 'macos_brightness_override_handler.dart'; + /// Asserts that the given context has a [MacosTheme] ancestor. /// /// To call this function, use the following pattern, typically in the diff --git a/lib/src/utils/window_main_state_listener.dart b/lib/src/utils/window_main_state_listener.dart new file mode 100644 index 00000000..c926f2b3 --- /dev/null +++ b/lib/src/utils/window_main_state_listener.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:macos_ui/macos_ui.dart'; + +/// A class that listens for changes to the application’s main window. +/// +/// A common use-case for responding to such changes would be to mute the colors +/// of certain primary UI elements when the window is no longer in focus, which +/// is something native macOS applications do out of the box. +/// +/// Example using [StreamBuilder]: +/// +/// ```dart +/// StreamBuilder( +/// stream: WindowMainStateListener.instance.onChanged, +/// builder: (context, _) { +/// final bool isMainWindow +/// = WindowMainStateListener.instance.isMainWindow; +/// +/// return SomeWidget( +/// isMainWindow: isMainWindow, +/// child: … +/// ); +/// }, +/// ); +/// ``` +class WindowMainStateListener { + /// A class that listens for changes to the application’s window being the + /// main window, and notifies listeners. + WindowMainStateListener() { + _init(); + } + + /// A shared instance of [WindowMainStateListener]. + static final instance = WindowMainStateListener(); + + /// A [NSWindowDelegateHandle], to be used when disposing the listener. + NSWindowDelegateHandle? handle; + + /// Whether the window is currently the main window. + bool _isMainWindow = true; + + /// Whether the window is currently the main window. + bool get isMainWindow => _isMainWindow; + + /// Notifies listeners when the window’s main state changes. + final _windowMainStateStreamController = StreamController.broadcast(); + + /// A stream of the window’s main state. Emits a new value whenever the state + /// changes. + Stream get onChanged => _windowMainStateStreamController.stream; + + /// Initializes the listener. This should only be called once. + void _init() { + if (kIsWeb) return; + if (!Platform.isMacOS) return; + + _initDelegate(); + _initIsWindowMain(); + } + + /// Initializes the [NSWindowDelegate] to listen for main window changes. + void _initDelegate() { + final delegate = _WindowMainStateListenerDelegate( + onWindowDidBecomeMain: () { + _isMainWindow = true; + _windowMainStateStreamController.add(true); + }, + onWindowDidResignMain: () { + _isMainWindow = false; + _windowMainStateStreamController.add(false); + }, + ); + handle = WindowManipulator.addNSWindowDelegate(delegate); + } + + /// Initializes the [_isMainWindow] variable. + Future _initIsWindowMain() async { + _isMainWindow = await WindowManipulator.isMainWindow(); + _windowMainStateStreamController.add(_isMainWindow); + } + + /// Disposes this listener. + void dispose() { + handle?.removeFromHandler(); + } +} + +/// The [NSWindowDelegate] used by [WindowMainStateListener]. +class _WindowMainStateListenerDelegate extends NSWindowDelegate { + _WindowMainStateListenerDelegate({ + required this.onWindowDidBecomeMain, + required this.onWindowDidResignMain, + }); + + /// Called when the window becomes the main window. + final void Function() onWindowDidBecomeMain; + + /// Called when the window resigns as the main window. + final void Function() onWindowDidResignMain; + + @override + void windowDidBecomeMain() => onWindowDidBecomeMain(); + + @override + void windowDidResignMain() => onWindowDidResignMain(); +} diff --git a/pubspec.lock b/pubspec.lock index f80cd1d5..62a3f534 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a url: "https://pub.dev" source: hosted - version: "59.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 url: "https://pub.dev" source: hosted - version: "5.11.1" + version: "5.13.0" + appkit_ui_element_colors: + dependency: "direct main" + description: + name: appkit_ui_element_colors + sha256: c3e50f900aae314d339de489535736238627071457c4a4a2dbbb1545b4f04f22 + url: "https://pub.dev" + source: hosted + version: "1.0.0" args: dependency: transitive description: @@ -61,10 +69,10 @@ packages: dependency: transitive description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.17.2" convert: dependency: transitive description: @@ -89,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -139,6 +155,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + gradient_borders: + dependency: "direct main" + description: + name: gradient_borders + sha256: "69eeaff519d145a4c6c213ada1abae386bcc8981a4970d923e478ce7ba19e309" + url: "https://pub.dev" + source: hosted + version: "1.0.0" http_multi_server: dependency: transitive description: @@ -191,26 +215,26 @@ packages: dependency: "direct main" description: name: macos_window_utils - sha256: b78a210aa70ca7ccad6e7b7b810fb4689c507f4a46e299214900b2a1eb70ea23 + sha256: "43a90473f8786f00f07203e6819dab67e032f8896dafa4a6f85fbc71fba32c0b" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -259,6 +283,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" pool: dependency: transitive description: @@ -332,10 +364,10 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: @@ -372,26 +404,26 @@ packages: dependency: transitive description: name: test - sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" url: "https://pub.dev" source: hosted - version: "1.24.1" + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.0" test_core: dependency: transitive description: name: test_core - sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" typed_data: dependency: transitive description: @@ -424,6 +456,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -449,5 +489,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.0 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 13865b31..8d81c2da 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: macos_ui description: Flutter widgets and themes implementing the current macOS design language. -version: 2.0.0 +version: 2.0.2 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" @@ -11,7 +11,9 @@ environment: dependencies: flutter: sdk: flutter - macos_window_utils: ^1.1.3 + macos_window_utils: ^1.2.0 + gradient_borders: ^1.0.0 + appkit_ui_element_colors: ^1.0.0 dev_dependencies: flutter_test: diff --git a/test/buttons/push_button_test.dart b/test/buttons/push_button_test.dart index 8bb294a0..981c21df 100644 --- a/test/buttons/push_button_test.dart +++ b/test/buttons/push_button_test.dart @@ -99,7 +99,6 @@ void main() { 'controlSize: regular', 'color: null', 'disabledColor: null', - 'pressedOpacity: 0.4', 'alignment: Alignment.center', 'semanticLabel: null', 'borderRadius: BorderRadius.circular(4.0)', diff --git a/test/indicators/capacity_indicators_test.dart b/test/indicators/capacity_indicators_test.dart index 5f5eb33a..1780a362 100644 --- a/test/indicators/capacity_indicators_test.dart +++ b/test/indicators/capacity_indicators_test.dart @@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:macos_ui/macos_ui.dart'; +// TODO(): Remove once mock_canvas in flutter_test reaches stable. import '../mock_canvas.dart'; void main() { @@ -81,7 +82,7 @@ void main() { expect( find.byType(CapacityIndicator), // each discrete segment is drawn 3 times, two times with fill, last time with stroke - paintsExactlyCountTimes(#drawRRect, 20 * 3), + paintedExactlyCountTimes(#drawRRect, 20 * 3), ); }, ); @@ -110,7 +111,7 @@ void main() { // each discrete segment is drawn 3 times, background - fill - stroke // a filled segment is drawn by fromLTRBR with LTRB=0,0,8,16 // an empty segment is drawnby fromLTRBAndCorners with LTRB=0,0,0,16 - paints + painted ..rrect( rrect: RRect.fromLTRBR( 0.0, diff --git a/test/mock_canvas.dart b/test/mock_canvas.dart index c86c7482..ee18e4f3 100644 --- a/test/mock_canvas.dart +++ b/test/mock_canvas.dart @@ -40,16 +40,16 @@ import 'recording_canvas.dart'; /// To match something which paints nothing, see [paintsNothing]. /// /// To match something which asserts instead of painting, see [paintsAssertion]. -PaintPattern get paints => _TestRecordingCanvasPatternMatcher(); +PaintPattern get painted => _TestRecordingCanvasPatternMatcher(); /// Matches objects or functions that does not paint anything on the canvas. -Matcher get paintsNothing => _TestRecordingCanvasPaintsNothingMatcher(); +Matcher get paintedNothing => _TestRecordingCanvasPaintsNothingMatcher(); /// Matches objects or functions that assert when they try to paint. -Matcher get paintsAssertion => _TestRecordingCanvasPaintsAssertionMatcher(); +Matcher get paintedAssertion => _TestRecordingCanvasPaintsAssertionMatcher(); /// Matches objects or functions that draw `methodName` exactly `count` number of times. -Matcher paintsExactlyCountTimes(Symbol methodName, int count) { +Matcher paintedExactlyCountTimes(Symbol methodName, int count) { return _TestRecordingCanvasPaintsCountMatcher(methodName, count); } @@ -564,7 +564,7 @@ class _MismatchedCall { const _MismatchedCall(this.message, this.callIntroduction, this.call); final String message; final String callIntroduction; - final RecordedInvocation call; + final RecordInvocation call; } bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) { @@ -593,9 +593,9 @@ bool _evaluatePainter(Object? object, Canvas canvas, PaintingContext context) { abstract class _TestRecordingCanvasMatcher extends Matcher { @override bool matches(Object? object, Map matchState) { - final TestRecordingCanvas canvas = TestRecordingCanvas(); - final TestRecordingPaintingContext context = - TestRecordingPaintingContext(canvas); + final TestRecordCanvas canvas = TestRecordCanvas(); + final TestRecordPaintingContext context = + TestRecordPaintingContext(canvas); final StringBuffer description = StringBuffer(); String prefixMessage = 'unexpectedly failed.'; bool result = false; @@ -618,7 +618,7 @@ abstract class _TestRecordingCanvasMatcher extends Matcher { if (!result) { if (canvas.invocations.isNotEmpty) { description.write('The complete display list was:'); - for (final RecordedInvocation call in canvas.invocations) { + for (final RecordInvocation call in canvas.invocations) { description.write('\n * $call'); } } @@ -628,7 +628,7 @@ abstract class _TestRecordingCanvasMatcher extends Matcher { } bool _evaluatePredicates( - Iterable calls, StringBuffer description); + Iterable calls, StringBuffer description); @override Description describeMismatch( @@ -658,9 +658,9 @@ class _TestRecordingCanvasPaintsCountMatcher @override bool _evaluatePredicates( - Iterable calls, StringBuffer description) { + Iterable calls, StringBuffer description) { int count = 0; - for (final RecordedInvocation call in calls) { + for (final RecordInvocation call in calls) { if (call.invocation.isMethod && call.invocation.memberName == _methodName) { count++; @@ -683,8 +683,8 @@ class _TestRecordingCanvasPaintsNothingMatcher @override bool _evaluatePredicates( - Iterable calls, StringBuffer description) { - final Iterable paintingCalls = + Iterable calls, StringBuffer description) { + final Iterable paintingCalls = _filterCanvasCalls(calls); if (paintingCalls.isEmpty) { return true; @@ -702,10 +702,10 @@ class _TestRecordingCanvasPaintsNothingMatcher ]; // Filters out canvas calls that are not painting anything. - static Iterable _filterCanvasCalls( - Iterable canvasCalls) { + static Iterable _filterCanvasCalls( + Iterable canvasCalls) { return canvasCalls.where( - (RecordedInvocation canvasCall) => + (RecordInvocation canvasCall) => !_nonPaintingOperations.contains(canvasCall.invocation.memberName), ); } @@ -714,9 +714,9 @@ class _TestRecordingCanvasPaintsNothingMatcher class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { @override bool matches(Object? object, Map matchState) { - final TestRecordingCanvas canvas = TestRecordingCanvas(); - final TestRecordingPaintingContext context = - TestRecordingPaintingContext(canvas); + final TestRecordCanvas canvas = TestRecordCanvas(); + final TestRecordPaintingContext context = + TestRecordPaintingContext(canvas); final StringBuffer description = StringBuffer(); String prefixMessage = 'unexpectedly failed.'; bool result = false; @@ -738,7 +738,7 @@ class _TestRecordingCanvasPaintsAssertionMatcher extends Matcher { if (!result) { if (canvas.invocations.isNotEmpty) { description.write('The complete display list was:'); - for (final RecordedInvocation call in canvas.invocations) { + for (final RecordInvocation call in canvas.invocations) { description.write('\n * $call'); } } @@ -1017,7 +1017,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher @override bool _evaluatePredicates( - Iterable calls, StringBuffer description) { + Iterable calls, StringBuffer description) { if (calls.isEmpty) { description.writeln('It painted nothing.'); return false; @@ -1030,7 +1030,7 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher return false; } final Iterator<_PaintPredicate> predicate = _predicates.iterator; - final Iterator call = calls.iterator..moveNext(); + final Iterator call = calls.iterator..moveNext(); try { while (predicate.moveNext()) { predicate.current.match(call); @@ -1056,12 +1056,12 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher } abstract class _PaintPredicate { - void match(Iterator call); + void match(Iterator call); @protected - void checkMethod(Iterator call, Symbol symbol) { + void checkMethod(Iterator call, Symbol symbol) { int others = 0; - final RecordedInvocation firstCall = call.current; + final RecordInvocation firstCall = call.current; while (!call.current.invocation.isMethod || call.current.invocation.memberName != symbol) { others += 1; @@ -1108,7 +1108,7 @@ abstract class _DrawCommandPaintPredicate extends _PaintPredicate { String get methodName => _symbolName(symbol); @override - void match(Iterator call) { + void match(Iterator call) { checkMethod(call, symbol); final int actualArgumentCount = call.current.invocation.positionalArguments.length; @@ -1605,7 +1605,7 @@ class _ShadowPredicate extends _PaintPredicate { } @override - void match(Iterator call) { + void match(Iterator call) { checkMethod(call, symbol); verifyArguments(call.current.invocation.positionalArguments); call.moveNext(); @@ -1771,8 +1771,8 @@ class _SomethingPaintPredicate extends _PaintPredicate { final PaintPatternPredicate predicate; @override - void match(Iterator call) { - RecordedInvocation currentCall; + void match(Iterator call) { + RecordInvocation currentCall; bool testedAllCalls = false; do { if (testedAllCalls) { @@ -1807,9 +1807,9 @@ class _EverythingPaintPredicate extends _PaintPredicate { final PaintPatternPredicate predicate; @override - void match(Iterator call) { + void match(Iterator call) { do { - final RecordedInvocation currentCall = call.current; + final RecordInvocation currentCall = call.current; if (!currentCall.invocation.isMethod) { throw 'It called $currentCall, which was not a method, when the paint pattern expected a method call'; } @@ -1842,7 +1842,7 @@ class _FunctionPaintPredicate extends _PaintPredicate { final List arguments; @override - void match(Iterator call) { + void match(Iterator call) { checkMethod(call, symbol); if (call.current.invocation.positionalArguments.length != arguments.length) { @@ -1874,7 +1874,7 @@ class _FunctionPaintPredicate extends _PaintPredicate { class _SaveRestorePairPaintPredicate extends _PaintPredicate { @override - void match(Iterator call) { + void match(Iterator call) { checkMethod(call, #save); int depth = 1; while (depth > 0) { diff --git a/test/recording_canvas.dart b/test/recording_canvas.dart index d2bd2f33..8ff6abe4 100644 --- a/test/recording_canvas.dart +++ b/test/recording_canvas.dart @@ -7,9 +7,9 @@ import 'package:flutter/rendering.dart'; /// An [Invocation] and the [stack] trace that led to it. /// /// Used by [TestRecordingCanvas] to trace canvas calls. -class RecordedInvocation { +class RecordInvocation { /// Create a record for an invocation list. - const RecordedInvocation(this.invocation, {required this.stack}); + const RecordInvocation(this.invocation, {required this.stack}); /// The method that was called and its arguments. /// @@ -37,13 +37,13 @@ class RecordedInvocation { /// A [Canvas] for tests that records its method calls. /// -/// This class can be used in conjunction with [TestRecordingPaintingContext] +/// This class can be used in conjunction with [TestRecordPaintingContext] /// to record the [Canvas] method calls made by a renderer. For example: /// /// ```dart /// RenderBox box = tester.renderObject(find.text('ABC')); -/// TestRecordingCanvas canvas = TestRecordingCanvas(); -/// TestRecordingPaintingContext context = TestRecordingPaintingContext(canvas); +/// TestRecordCanvas canvas = TestRecordCanvas(); +/// TestRecordPaintingContext context = TestRecordPaintingContext(canvas); /// box.paint(context, Offset.zero); /// // Now test the expected canvas.invocations. /// ``` @@ -53,10 +53,10 @@ class RecordedInvocation { /// that the test requires. /// /// For simple tests, consider using the [paints] matcher, which overlays a -/// pattern matching API over [TestRecordingCanvas]. -class TestRecordingCanvas implements Canvas { +/// pattern matching API over [TestRecordCanvas]. +class TestRecordCanvas implements Canvas { /// All of the method calls on this canvas. - final List invocations = []; + final List invocations = []; int _saveCount = 0; @@ -67,13 +67,13 @@ class TestRecordingCanvas implements Canvas { void save() { _saveCount += 1; invocations - .add(RecordedInvocation(_MethodCall(#save), stack: StackTrace.current)); + .add(RecordInvocation(_MethodCall(#save), stack: StackTrace.current)); } @override void saveLayer(Rect? bounds, Paint paint) { _saveCount += 1; - invocations.add(RecordedInvocation( + invocations.add(RecordInvocation( _MethodCall(#saveLayer, [bounds, paint]), stack: StackTrace.current)); } @@ -83,20 +83,20 @@ class TestRecordingCanvas implements Canvas { _saveCount -= 1; assert(_saveCount >= 0); invocations.add( - RecordedInvocation(_MethodCall(#restore), stack: StackTrace.current)); + RecordInvocation(_MethodCall(#restore), stack: StackTrace.current)); } @override void noSuchMethod(Invocation invocation) { - invocations.add(RecordedInvocation(invocation, stack: StackTrace.current)); + invocations.add(RecordInvocation(invocation, stack: StackTrace.current)); } } /// A [PaintingContext] for tests that use [TestRecordingCanvas]. -class TestRecordingPaintingContext extends ClipContext +class TestRecordPaintingContext extends ClipContext implements PaintingContext { /// Creates a [PaintingContext] for tests that use [TestRecordingCanvas]. - TestRecordingPaintingContext(this.canvas); + TestRecordPaintingContext(this.canvas); @override final Canvas canvas; diff --git a/test/theme/push_button_theme_test.dart b/test/theme/push_button_theme_test.dart deleted file mode 100644 index 2ff90243..00000000 --- a/test/theme/push_button_theme_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:flutter/foundation.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', () { - test('lerps from light to dark', () { - final actual = - PushButtonThemeData.lerp(_pushButtonTheme, _pushButtonThemeDark, 1); - - expect(actual, _pushButtonThemeDark); - }); - - test('lerps from dark to light', () { - final actual = - PushButtonThemeData.lerp(_pushButtonThemeDark, _pushButtonTheme, 1); - - expect(actual, _pushButtonTheme); - }); - - test('copyWith, hashCode, ==', () { - expect( - const PushButtonThemeData(), - const PushButtonThemeData().copyWith(), - ); - expect( - const PushButtonThemeData().hashCode, - const PushButtonThemeData().copyWith().hashCode, - ); - }); - - testWidgets('debugFillProperties', (tester) async { - final builder = DiagnosticPropertiesBuilder(); - PushButtonThemeData( - color: MacosColors.appleBlue, - disabledColor: MacosColors.systemGrayColor.color, - secondaryColor: MacosColors.controlColor.color, - ).debugFillProperties(builder); - - final description = builder.properties - .where((node) => !node.isFiltered(DiagnosticLevel.info)) - .map((node) => node.toString()) - .toList(); - - expect( - description, - [ - 'color: MacosColor(0xff0433ff)', - 'disabledColor: MacosColor(0xff8e8e93)', - 'secondaryColor: Color(0x19000000)', - ], - ); - }); - - testWidgets('Default values in widget tree', (tester) async { - late BuildContext capturedContext; - await tester.pumpWidget( - MacosApp( - home: MacosWindow( - disableWallpaperTinting: true, - child: MacosScaffold( - children: [ - ContentArea( - builder: (context, _) { - capturedContext = context; - return const PushButton( - controlSize: ControlSize.regular, - child: Text('Push me'), - ); - }, - ), - ], - ), - ), - ), - ); - - final theme = PushButtonTheme.of(capturedContext); - expect(theme.color, const Color(0xff007aff)); - expect(theme.disabledColor, const Color.fromRGBO(244, 245, 245, 1.0)); - expect(theme.secondaryColor, MacosColors.white); - }); - }); -} - -final _pushButtonTheme = PushButtonThemeData( - color: MacosColors.appleRed, - disabledColor: MacosColors.systemGrayColor.color, - secondaryColor: MacosColors.controlColor.color, -); - -final _pushButtonThemeDark = PushButtonThemeData( - color: MacosColors.appleBlue, - disabledColor: MacosColors.systemGrayColor.darkColor, - secondaryColor: MacosColors.controlColor.darkColor, -);