diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c59bae9..f27d69cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [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/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..f61168d0 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.1" 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/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 67% rename from lib/src/utils.dart rename to lib/src/utils/utils.dart index 1f2d8a1e..3d63855c 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils/utils.dart @@ -1,9 +1,10 @@ -import 'dart:io'; - -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 @@ -65,24 +66,3 @@ class Unsupported { 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/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 dfa8a61c..62a3f534 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted 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..8ffa130b 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.1 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/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, -);