Repository: macosui/macos_ui

Issue:
`PushButton` fidelity
`PushButton` requires several fidelity improvements for parity with native push buttons

```[tasklist]
### Tasks
- [ ] Determine and apply the appropriate gradients, box shadows, padding, and border radii for all control sizes
- [ ] Determine and apply correct press animation
- [ ] Apply correct secondary background colors
- [ ] Apply correct disabled background colors
```





Fix patch:
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<T> extends State<MacosPopupButton<T>>
       }
 
       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', 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<PushButton>
     with SingleTickerProviderStateMixin {
-  // Eyeballed values. Feel free to tweak.
-  static const Duration kFadeOutDuration = Duration(milliseconds: 10);
-  static const Duration kFadeInDuration = Duration(milliseconds: 100);
-  final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
-
-  late AnimationController _animationController;
-  late Animation<double> _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>((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<PushButton>
           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<bool>(
+                  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<PushButton>
     );
   }
 }
+
+class _BoxDecorationBuilder {
+  /// Gets the colors to use for the [BoxDecoration]’s gradient based on the
+  /// provided [accentColor], [isEnabled], and [isDarkModeEnabled] properties.
+  static List<Color> 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<BoxShadow> _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<PushButtonTheme>();
@@ -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 = <double, AccentColor>{
+    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<void>.broadcast();
+
+  /// Streams the user’s system accent color selection.
+  ///
+  /// Emits a new value whenever the system accent color selection changes.
+  Stream<void> get onChanged => _accentColorStreamController.stream;
+
+  /// A stream subscription for the [SystemColorObserver] stream.
+  StreamSubscription<void>? _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<void> _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<double> _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<bool>.broadcast();
+
+  /// A stream of the window’s main state. Emits a new value whenever the state
+  /// changes.
+  Stream<bool> 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<void> _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:


Test patch:
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,
-);