diff --git a/.gitignore b/.gitignore index c8887dcd..144640c7 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,4 @@ metrics coverage_report coverage example/macos/Flutter/GeneratedPluginRegistrant.swift +example/devtools_options.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cacfe3..85e8c48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [2.0.7] +### 🛠️ Updated 🛠️ +* Made most widgets aware of the user’s accent color and window state by adding respective fields to `MacosThemeData`. +* `MacosCheckbox` has received a facelift to mimic the look and feel of native macOS checkboxes better. + ## [2.0.6] ### 🛠️ Updated 🛠️ * Implemented value equality for `MacosThemeData`. diff --git a/example/lib/main.dart b/example/lib/main.dart index 193bef1f..d59b1dcc 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -47,8 +47,6 @@ class MacosUIGalleryApp extends StatelessWidget { final appTheme = context.watch(); return MacosApp( title: 'macos_ui Widget Gallery', - theme: MacosThemeData.light(), - darkTheme: MacosThemeData.dark(), themeMode: appTheme.mode, debugShowCheckedModeBanner: false, home: const WidgetGallery(), diff --git a/example/lib/pages/buttons_page.dart b/example/lib/pages/buttons_page.dart index fd61774f..26952d90 100644 --- a/example/lib/pages/buttons_page.dart +++ b/example/lib/pages/buttons_page.dart @@ -558,11 +558,20 @@ class _ButtonsPageState extends State { const SizedBox(height: 16), const WidgetTextTitle2(widgetName: 'MacosCheckbox'), const SizedBox(height: 8), - MacosCheckbox( - value: switchValue, - onChanged: (value) { - setState(() => switchValue = value); - }, + Row( + children: [ + MacosCheckbox( + value: switchValue, + onChanged: (value) { + setState(() => switchValue = value); + }, + ), + const SizedBox(width: 8), + MacosCheckbox( + value: switchValue, + onChanged: null, + ), + ], ), const SizedBox(height: 16), const WidgetTextTitle2(widgetName: 'MacosRadioButton'), diff --git a/lib/macos_ui.dart b/lib/macos_ui.dart index 62dcc602..d47a163a 100644 --- a/lib/macos_ui.dart +++ b/lib/macos_ui.dart @@ -88,3 +88,4 @@ export 'src/theme/search_field_theme.dart'; export 'src/theme/time_picker_theme.dart'; export 'src/theme/tooltip_theme.dart'; export 'src/theme/typography.dart'; +export 'src/enums/accent_color.dart'; diff --git a/lib/src/buttons/checkbox.dart b/lib/src/buttons/checkbox.dart index d7f8bcc3..2d21e7c7 100644 --- a/lib/src/buttons/checkbox.dart +++ b/lib/src/buttons/checkbox.dart @@ -1,4 +1,5 @@ import 'package:flutter/rendering.dart'; +import 'package:gradient_borders/gradient_borders.dart'; import 'package:macos_ui/macos_ui.dart'; import 'package:macos_ui/src/library.dart'; @@ -82,92 +83,403 @@ class MacosCheckbox extends StatelessWidget { assert(debugCheckHasMacosTheme(context)); final MacosThemeData theme = MacosTheme.of(context); bool isLight = !theme.brightness.isDark; - return GestureDetector( - onTap: () { - if (value == null || value == false) { - onChanged?.call(true); - } else { - onChanged?.call(false); - } - }, - child: Semantics( - // value == true because [value] can be null - checked: value == true, - label: semanticLabel, - child: Container( - height: size, - width: size, - alignment: Alignment.center, - decoration: isDisabled || value == null || value == true - ? BoxDecoration( - color: MacosDynamicColor.resolve( - isDisabled - ? disabledColor - : activeColor ?? theme.primaryColor, - context, - ), - borderRadius: const BorderRadius.all(Radius.circular(4.0)), - ) - : isLight - ? ShapeDecoration( - gradient: LinearGradient( - begin: const Alignment(0.0, -1.0), - end: const Alignment(0, 0), - colors: [ - Colors.white.withOpacity(0.85), - Colors.white.withOpacity(1.0), - ], - ), - shadows: const [ - BoxShadow( - color: Color(0x3F000000), - blurRadius: 1, - blurStyle: BlurStyle.inner, - offset: Offset(0, 0), - spreadRadius: 0.0, - ), - ], - shape: RoundedRectangleBorder( - side: BorderSide( - width: 0.25, - color: Colors.black.withOpacity(0.35000000596046448), + return StreamBuilder( + stream: AccentColorListener.instance.onChanged, + builder: (context, _) { + return StreamBuilder( + stream: WindowMainStateListener.instance.onChanged, + builder: (context, _) { + final accentColor = + MacosTheme.of(context).accentColor ?? AccentColor.blue; + final isMainWindow = + MacosTheme.of(context).isMainWindow ?? true; + + return GestureDetector( + onTap: () { + if (value == null || value == false) { + onChanged?.call(true); + } else { + onChanged?.call(false); + } + }, + child: Semantics( + // value == true because [value] can be null + checked: value == true, + label: semanticLabel, + child: Container( + height: size, + width: size, + alignment: Alignment.center, + child: SizedBox.expand( + child: _DecoratedContainer( + accentColor: accentColor, + isDisabled: isDisabled, + isLight: isLight, + isMainWindow: isMainWindow, + value: value, + isMixed: isMixed, + theme: theme, + size: size, ), - borderRadius: - const BorderRadius.all(Radius.circular(3.5)), - ), - ) - : ShapeDecoration( - gradient: LinearGradient( - begin: const Alignment(0.0, -1.0), - end: const Alignment(0, 1), - colors: [ - Colors.white.withOpacity(0.14000000059604645), - Colors.white.withOpacity(0.2800000011920929), - ], - ), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(3)), ), - shadows: const [ - BoxShadow( - color: Color(0x3F000000), - blurRadius: 1, - offset: Offset(0, 0), - spreadRadius: 0, - ), - ], ), + ), + ); + }); + }); + } +} + +/// A widget that builds a decorated checkbox. +class _DecoratedContainer extends StatelessWidget { + const _DecoratedContainer({ + required this.accentColor, + required this.isDisabled, + required this.isLight, + required this.isMainWindow, + required this.value, + required this.isMixed, + required this.theme, + required this.size, + }); + + final AccentColor accentColor; + final bool isDisabled; + final bool isLight; + final bool isMainWindow; + final bool? value; + final bool isMixed; + final MacosThemeData theme; + final double size; + + @override + Widget build(BuildContext context) { + return Container( + decoration: _BoxDecorationBuilder.buildBoxDecoration( + accentColor: accentColor, + isEnabled: !isDisabled, + isDarkModeEnabled: !isLight, + isMainWindow: isMainWindow, + value: value, + ), + child: _CheckboxStack( + value: value, + isDisabled: isDisabled, + isMixed: isMixed, + theme: theme, + isMainWindow: isMainWindow, + size: size, + ), + ); + } +} + +/// A stack containing the checkbox’s inner drop shadow and checkmark icon. +class _CheckboxStack extends StatelessWidget { + const _CheckboxStack({ + required this.value, + required this.isDisabled, + required this.isMixed, + required this.theme, + required this.isMainWindow, + required this.size, + }); + + final bool? value; + final bool isDisabled; + final bool isMixed; + final MacosThemeData theme; + final bool isMainWindow; + final double size; + + @override + Widget build(BuildContext context) { + final icon = value == false + ? null + : isMixed + ? CupertinoIcons.minus + : CupertinoIcons.checkmark; + + return Stack( + children: [ + _InnerDropShadow( + value: value, + isEnabled: !isDisabled, + ), + Center( child: Icon( - isDisabled || value == false - ? null - : isMixed - ? CupertinoIcons.minus - : CupertinoIcons.check_mark, - color: CupertinoColors.white, + icon, + color: _getCheckmarkColor(), size: (size - 3).clamp(0, size), ), ), + ], + ); + } + + _getCheckmarkColor() { + if (isDisabled) { + return const MacosColor.fromRGBO(172, 172, 172, 1.0); + } + + if (theme.brightness.isDark) { + return theme.accentColor == AccentColor.graphite && isMainWindow + ? CupertinoColors.black + : CupertinoColors.white; + } + + if (theme.isMainWindow == false) { + return CupertinoColors.black; + } + + return CupertinoColors.white; + } +} + +/// A widget that paints an inner drop shadow for the checkbox in light mode. +class _InnerDropShadow extends StatelessWidget { + /// The value of the checkbox. + final bool? value; + + /// Whether the checkbox is enabled. + final bool isEnabled; + + /// Creates a widget that paints an inner drop shadow for a checkbox. + const _InnerDropShadow({ + required this.value, + required this.isEnabled, + }); + + @override + Widget build(BuildContext context) { + final theme = MacosTheme.of(context); + + if (theme.brightness.isDark) { + return const SizedBox(); + } + + if (value == true && theme.isMainWindow == true && isEnabled) { + return const SizedBox(); + } + + final color = isEnabled + ? CupertinoColors.white + : const MacosColor.fromRGBO(255, 255, 255, 0.8); + + return SizedBox.expand( + child: Container( + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + color: MacosColor.fromRGBO(0, 0, 0, 0.15), + borderRadius: BorderRadius.all(Radius.circular(3.5)), + ), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(3.5)), + boxShadow: [ + BoxShadow( + color: color, + offset: const Offset(0.0, 0.5), + blurRadius: 1.0, + ), + ], + ), + ), + ), + ); + } +} + +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 isMainWindow, + required bool? value, + }) { + final isEnabledFactor = isEnabled || !isDarkModeEnabled ? 1.0 : 0.5; + + final showDisabledCheckbox = !isMainWindow || !isEnabled || value == false; + + if (showDisabledCheckbox) { + return isDarkModeEnabled + ? [ + MacosColor.fromRGBO(74, 74, 74, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(101, 101, 101, 1.0 * isEnabledFactor), + ] + : const [ + MacosColors.transparent, + MacosColors.transparent, + ]; + } + + if (isDarkModeEnabled) { + switch (accentColor) { + case AccentColor.blue: + return [ + MacosColor.fromRGBO(23, 105, 229, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(20, 94, 203, 1.0 * isEnabledFactor), + ]; + + case AccentColor.purple: + return [ + MacosColor.fromRGBO(203, 46, 202, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(182, 40, 182, 1.0 * isEnabledFactor), + ]; + + case AccentColor.pink: + return [ + MacosColor.fromRGBO(229, 75, 145, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(205, 61, 129, 1.0 * isEnabledFactor), + ]; + + case AccentColor.red: + return [ + MacosColor.fromRGBO(237, 64, 68, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(213, 56, 61, 1.0 * isEnabledFactor), + ]; + + case AccentColor.orange: + return [ + MacosColor.fromRGBO(244, 114, 0, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(219, 102, 0, 1.0 * isEnabledFactor), + ]; + + case AccentColor.yellow: + return [ + MacosColor.fromRGBO(233, 176, 4, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(209, 157, 3, 1.0 * isEnabledFactor), + ]; + + case AccentColor.green: + return [ + MacosColor.fromRGBO(75, 177, 45, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(67, 159, 40, 1.0 * isEnabledFactor), + ]; + + case AccentColor.graphite: + return [ + MacosColor.fromRGBO(148, 148, 148, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(148, 148, 148, 1.0 * isEnabledFactor), + ]; + + default: + throw UnimplementedError(); + } + } else { + switch (accentColor) { + case AccentColor.blue: + return [ + MacosColor.fromRGBO(39, 145, 255, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(1, 123, 255, 1.0 * isEnabledFactor), + ]; + + case AccentColor.purple: + return [ + MacosColor.fromRGBO(173, 56, 177, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(159, 19, 163, 1.0 * isEnabledFactor), + ]; + + case AccentColor.pink: + return [ + MacosColor.fromRGBO(237, 102, 165, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(234, 76, 149, 1.0 * isEnabledFactor), + ]; + + case AccentColor.red: + return [ + MacosColor.fromRGBO(225, 60, 66, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(220, 26, 31, 1.0 * isEnabledFactor), + ]; + + case AccentColor.orange: + return [ + MacosColor.fromRGBO(247, 130, 31, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(245, 108, 0, 1.0 * isEnabledFactor), + ]; + + case AccentColor.yellow: + return [ + MacosColor.fromRGBO(242, 189, 32, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(240, 178, 0, 1.0 * isEnabledFactor), + ]; + + case AccentColor.green: + return [ + MacosColor.fromRGBO(90, 185, 59, 1.0 * isEnabledFactor), + MacosColor.fromRGBO(60, 172, 25, 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(); + } + } + } + + /// Builds a [BoxDecoration] for a [MacosPushButton]. + static BoxDecoration buildBoxDecoration({ + required AccentColor accentColor, + required bool isEnabled, + required bool isDarkModeEnabled, + required bool isMainWindow, + required bool? value, + }) { + final isEnabledFactor = isEnabled ? 1.0 : 0.5; + + final List shadows = isDarkModeEnabled + ? const [ + BoxShadow( + color: MacosColor.fromRGBO(0, 0, 0, 0.8), + blurRadius: 0.7, + spreadRadius: -0.5, + offset: Offset(0.0, 0.5), + blurStyle: BlurStyle.outer, + ) + ] + : const []; + + 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, + ) + : Border.all( + color: value == false && isEnabled + ? const MacosColor.fromRGBO(0, 0, 0, 0.15) + : const MacosColor.fromRGBO(0, 0, 0, 0.12), + width: 0.5, + ), + gradient: LinearGradient( + colors: getGradientColors( + accentColor: accentColor, + isEnabled: isEnabled, + isDarkModeEnabled: isDarkModeEnabled, + isMainWindow: isMainWindow, + value: value, + ), + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), + boxShadow: shadows, + borderRadius: const BorderRadius.all(Radius.circular(3.5)), ); } } diff --git a/lib/src/buttons/push_button.dart b/lib/src/buttons/push_button.dart index df0cf4fe..e95a9857 100644 --- a/lib/src/buttons/push_button.dart +++ b/lib/src/buttons/push_button.dart @@ -4,7 +4,6 @@ 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); @@ -256,8 +255,8 @@ class PushButtonState extends State @visibleForTesting bool buttonHeldDown = false; - AccentColor get _accentColor => - AccentColorListener.instance.currentAccentColor ?? AccentColor.blue; + AccentColor _getAccentColor(BuildContext context) => + MacosTheme.of(context).accentColor ?? AccentColor.blue; BoxDecoration _getBoxDecoration() { // If the window isn’t currently the main window (that is, it is not in @@ -265,7 +264,7 @@ class PushButtonState extends State final isMainWindow = WindowMainStateListener.instance.isMainWindow; return _BoxDecorationBuilder.buildBoxDecoration( - accentColor: _accentColor, + accentColor: _getAccentColor(context), isEnabled: widget.enabled, isDarkModeEnabled: MacosTheme.of(context).brightness.isDark, isSecondary: !isMainWindow || (widget.secondary ?? false), @@ -284,7 +283,7 @@ class PushButtonState extends State return MacosDynamicColor.resolve( widget.color ?? _BoxDecorationBuilder.getGradientColors( - accentColor: _accentColor, + accentColor: _getAccentColor(context), isEnabled: enabled, isDarkModeEnabled: theme.brightness.isDark, isSecondary: isSecondary || !isWindowMain, diff --git a/lib/src/layout/sidebar/sidebar_items.dart b/lib/src/layout/sidebar/sidebar_items.dart index e24f90d3..5dd4d7d1 100644 --- a/lib/src/layout/sidebar/sidebar_items.dart +++ b/lib/src/layout/sidebar/sidebar_items.dart @@ -1,5 +1,4 @@ import 'package:macos_ui/macos_ui.dart'; -import 'package:macos_ui/src/enums/accent_color.dart'; import 'package:macos_ui/src/library.dart'; const Duration _kExpand = Duration(milliseconds: 200); @@ -100,15 +99,15 @@ class SidebarItems extends StatelessWidget { final MouseCursor? cursor; /// The user’s selected system accent color. - AccentColor get _accentColor => - AccentColorListener.instance.currentAccentColor ?? AccentColor.blue; + AccentColor _getAccentColor(BuildContext context) => + MacosTheme.of(context).accentColor ?? AccentColor.blue; /// Returns the sidebar item’s selected color. Color _getColor(BuildContext context) { final isMainWindow = WindowMainStateListener.instance.isMainWindow; return _ColorProvider.getSelectedColor( - accentColor: _accentColor, + accentColor: _getAccentColor(context), isDarkModeEnabled: MacosTheme.of(context).brightness.isDark, isWindowMain: isMainWindow, ); diff --git a/lib/src/macos_app.dart b/lib/src/macos_app.dart index 459918fb..1589821e 100644 --- a/lib/src/macos_app.dart +++ b/lib/src/macos_app.dart @@ -310,41 +310,65 @@ class _MacosAppState extends State { widget.routerDelegate != null || widget.routerConfig != null; Widget _macosBuilder(BuildContext context, Widget? child) { - final mode = widget.themeMode ?? ThemeMode.system; - final platformBrightness = MediaQuery.platformBrightnessOf(context); - final useDarkTheme = mode == ThemeMode.dark || - (mode == ThemeMode.system && platformBrightness == Brightness.dark); - - late MacosThemeData theme; - if (useDarkTheme) { - theme = widget.darkTheme ?? MacosThemeData.dark(); - } else { - theme = widget.theme ?? MacosThemeData.light(); - } - - return MacosTheme( - data: theme, - child: DefaultTextStyle( - style: TextStyle(color: theme.typography.body.color), - child: widget.builder != null - // See the MaterialApp source code for the explanation for - // wrapping a builder in a builder - ? Builder( - builder: (context) { - // An Overlay is used here because MacosTooltip needs an - // Overlay as an ancestor in the widget tree. - return Overlay( - initialEntries: [ - OverlayEntry( - builder: (context) => widget.builder!(context, child), - ), - ], - ); - }, - ) - : child ?? const SizedBox.shrink(), - ), - ); + return StreamBuilder( + stream: WindowMainStateListener.instance.onChanged, + builder: (context, _) { + return StreamBuilder( + stream: AccentColorListener.instance.onChanged, + builder: (context, _) { + final mode = widget.themeMode ?? ThemeMode.system; + final platformBrightness = + MediaQuery.platformBrightnessOf(context); + final useDarkTheme = mode == ThemeMode.dark || + (mode == ThemeMode.system && + platformBrightness == Brightness.dark); + + final accentColor = + AccentColorListener.instance.currentAccentColor; + final isMainWindow = + WindowMainStateListener.instance.isMainWindow; + + late MacosThemeData theme; + if (useDarkTheme) { + theme = widget.darkTheme ?? + MacosThemeData.dark( + accentColor: accentColor, + isMainWindow: isMainWindow, + ); + } else { + theme = widget.theme ?? + MacosThemeData.light( + accentColor: accentColor, + isMainWindow: isMainWindow, + ); + } + + return MacosTheme( + data: theme, + child: DefaultTextStyle( + style: TextStyle(color: theme.typography.body.color), + child: widget.builder != null + // See the MaterialApp source code for the explanation for + // wrapping a builder in a builder + ? Builder( + builder: (context) { + // An Overlay is used here because MacosTooltip needs an + // Overlay as an ancestor in the widget tree. + return Overlay( + initialEntries: [ + OverlayEntry( + builder: (context) => + widget.builder!(context, child), + ), + ], + ); + }, + ) + : child ?? const SizedBox.shrink(), + ), + ); + }); + }); } Widget _buildMacosApp(BuildContext context) { diff --git a/lib/src/theme/macos_theme.dart b/lib/src/theme/macos_theme.dart index 59776de6..7b51b3a0 100644 --- a/lib/src/theme/macos_theme.dart +++ b/lib/src/theme/macos_theme.dart @@ -189,8 +189,8 @@ class MacosThemeData extends Equatable with Diagnosticable { /// /// See also: /// - /// * [MacosThemeData.light], which creates a light blue theme. - /// * [MacosThemeData.dark], which creates a dark blue theme. + /// * [MacosThemeData.light], which creates a light theme. + /// * [MacosThemeData.dark], which creates a dark theme. factory MacosThemeData({ Brightness? brightness, Color? primaryColor, @@ -209,11 +209,17 @@ class MacosThemeData extends Equatable with Diagnosticable { MacosDatePickerThemeData? datePickerTheme, MacosTimePickerThemeData? timePickerTheme, MacosSearchFieldThemeData? searchFieldTheme, + AccentColor? accentColor, + bool? isMainWindow, }) { // ignore: no_leading_underscores_for_local_identifiers final Brightness _brightness = brightness ?? Brightness.light; final bool isDark = _brightness == Brightness.dark; - primaryColor ??= MacosColors.controlAccentColor; + primaryColor ??= _ColorProvider.getPrimaryColor( + accentColor: accentColor ?? AccentColor.blue, + isDarkModeEnabled: isDark, + isWindowMain: isMainWindow ?? true, + ); canvasColor ??= isDark ? const Color.fromRGBO(40, 40, 40, 1.0) @@ -266,16 +272,20 @@ class MacosThemeData extends Equatable with Diagnosticable { visualDensity ??= VisualDensity.adaptivePlatformDensity; iconTheme ??= MacosIconThemeData( - color: isDark - ? CupertinoColors.activeBlue.darkColor - : CupertinoColors.activeBlue.color, + color: _ColorProvider.getActiveColor( + accentColor: accentColor ?? AccentColor.blue, + isDarkModeEnabled: isDark, + isWindowMain: isMainWindow ?? true, + ), size: 20, ); popupButtonTheme ??= MacosPopupButtonThemeData( - highlightColor: isDark - ? CupertinoColors.activeBlue.darkColor - : CupertinoColors.activeBlue.color, + highlightColor: _ColorProvider.getActiveColor( + accentColor: accentColor ?? AccentColor.blue, + isDarkModeEnabled: isDark, + isWindowMain: isMainWindow ?? true, + ), backgroundColor: isDark ? const Color.fromRGBO(255, 255, 255, 0.247) : const Color.fromRGBO(255, 255, 255, 1), @@ -285,9 +295,11 @@ class MacosThemeData extends Equatable with Diagnosticable { ); pulldownButtonTheme ??= MacosPulldownButtonThemeData( - highlightColor: isDark - ? CupertinoColors.activeBlue.darkColor - : CupertinoColors.activeBlue.color, + highlightColor: _ColorProvider.getActiveColor( + accentColor: accentColor ?? AccentColor.blue, + isDarkModeEnabled: isDark, + isWindowMain: isMainWindow ?? true, + ), backgroundColor: isDark ? const Color.fromRGBO(255, 255, 255, 0.247) : const Color.fromRGBO(255, 255, 255, 1), @@ -355,9 +367,11 @@ class MacosThemeData extends Equatable with Diagnosticable { ); searchFieldTheme ??= MacosSearchFieldThemeData( - highlightColor: isDark - ? CupertinoColors.activeBlue.darkColor - : CupertinoColors.activeBlue.color, + highlightColor: _ColorProvider.getActiveColor( + accentColor: accentColor ?? AccentColor.blue, + isDarkModeEnabled: isDark, + isWindowMain: isMainWindow ?? true, + ), resultsBackgroundColor: isDark ? const Color.fromRGBO(30, 30, 30, 1) : const Color.fromRGBO(242, 242, 247, 1), @@ -381,6 +395,8 @@ class MacosThemeData extends Equatable with Diagnosticable { datePickerTheme: datePickerTheme, timePickerTheme: timePickerTheme, searchFieldTheme: searchFieldTheme, + accentColor: accentColor, + isMainWindow: isMainWindow, ); final customizedData = defaultData.copyWith( @@ -400,6 +416,8 @@ class MacosThemeData extends Equatable with Diagnosticable { pulldownButtonTheme: pulldownButtonTheme, datePickerTheme: datePickerTheme, searchFieldTheme: searchFieldTheme, + accentColor: accentColor, + isMainWindow: isMainWindow, ); return defaultData.merge(customizedData); @@ -429,14 +447,31 @@ class MacosThemeData extends Equatable with Diagnosticable { required this.datePickerTheme, required this.timePickerTheme, required this.searchFieldTheme, + required this.accentColor, + required this.isMainWindow, }); /// A default light theme. - factory MacosThemeData.light() => - MacosThemeData(brightness: Brightness.light); + factory MacosThemeData.light({ + AccentColor? accentColor, + bool? isMainWindow, + }) => + MacosThemeData( + brightness: Brightness.light, + accentColor: accentColor, + isMainWindow: isMainWindow, + ); /// A default dark theme. - factory MacosThemeData.dark() => MacosThemeData(brightness: Brightness.dark); + factory MacosThemeData.dark({ + AccentColor? accentColor, + bool? isMainWindow, + }) => + MacosThemeData( + brightness: Brightness.dark, + accentColor: accentColor, + isMainWindow: isMainWindow, + ); /// The default color theme. Same as [ThemeData.light]. /// @@ -505,6 +540,13 @@ class MacosThemeData extends Equatable with Diagnosticable { /// The default style for [MacosSearchField]s below the overall [MacosTheme] final MacosSearchFieldThemeData searchFieldTheme; + /// The accent color to use for the application. + final AccentColor? accentColor; + + /// Whether the app is running in the main (i.e., the currently active) + /// window. + final bool? isMainWindow; + /// Linearly interpolate between two themes. static MacosThemeData lerp(MacosThemeData a, MacosThemeData b, double t) { return MacosThemeData.raw( @@ -552,6 +594,8 @@ class MacosThemeData extends Equatable with Diagnosticable { b.searchFieldTheme, t, ), + accentColor: t < 0.5 ? a.accentColor : b.accentColor, + isMainWindow: t < 0.5 ? a.isMainWindow : b.isMainWindow, ); } @@ -574,6 +618,8 @@ class MacosThemeData extends Equatable with Diagnosticable { MacosDatePickerThemeData? datePickerTheme, MacosTimePickerThemeData? timePickerTheme, MacosSearchFieldThemeData? searchFieldTheme, + AccentColor? accentColor, + bool? isMainWindow, }) { return MacosThemeData.raw( brightness: brightness ?? this.brightness, @@ -593,6 +639,8 @@ class MacosThemeData extends Equatable with Diagnosticable { datePickerTheme: this.datePickerTheme.merge(datePickerTheme), timePickerTheme: this.timePickerTheme.merge(timePickerTheme), searchFieldTheme: this.searchFieldTheme.merge(searchFieldTheme), + accentColor: accentColor ?? this.accentColor, + isMainWindow: isMainWindow ?? this.isMainWindow, ); } @@ -617,6 +665,8 @@ class MacosThemeData extends Equatable with Diagnosticable { datePickerTheme: datePickerTheme.merge(other.datePickerTheme), timePickerTheme: timePickerTheme.merge(other.timePickerTheme), searchFieldTheme: searchFieldTheme.merge(other.searchFieldTheme), + accentColor: other.accentColor, + isMainWindow: other.isMainWindow, ); } @@ -682,6 +732,18 @@ class MacosThemeData extends Equatable with Diagnosticable { searchFieldTheme, ), ); + properties.add( + DiagnosticsProperty( + 'accentColor', + accentColor, + ), + ); + properties.add( + DiagnosticsProperty( + 'isMainWindow', + isMainWindow, + ), + ); } @override @@ -703,6 +765,8 @@ class MacosThemeData extends Equatable with Diagnosticable { datePickerTheme, timePickerTheme, searchFieldTheme, + accentColor, + isMainWindow, ]; } @@ -717,3 +781,145 @@ extension BrightnessX on Brightness { return light; } } + +class _ColorProvider { + _ColorProvider._(); + + /// Returns the primary color based on the provided parameters. + static Color getPrimaryColor({ + required AccentColor accentColor, + required bool isDarkModeEnabled, + required bool isWindowMain, + }) { + if (isDarkModeEnabled) { + if (!isWindowMain) { + return const MacosColor.fromRGBO(100, 100, 100, 0.625); + } + + switch (accentColor) { + case AccentColor.blue: + return const MacosColor.fromRGBO(29, 151, 255, 1.0); + + case AccentColor.purple: + return const MacosColor.fromRGBO(204, 118, 207, 1.0); + + case AccentColor.pink: + return const MacosColor.fromRGBO(255, 114, 194, 1.0); + + case AccentColor.red: + return const MacosColor.fromRGBO(225, 118, 124, 1.0); + + case AccentColor.orange: + return const MacosColor.fromRGBO(255, 147, 44, 1.0); + + case AccentColor.yellow: + return const MacosColor.fromRGBO(255, 220, 24, 1.0); + + case AccentColor.green: + return const MacosColor.fromRGBO(114, 202, 87, 1.0); + + case AccentColor.graphite: + return const MacosColor.fromRGBO(152, 152, 152, 1.0); + } + } + + if (!isWindowMain) { + return const MacosColor.fromRGBO(190, 190, 190, 1.0); + } + + switch (accentColor) { + case AccentColor.blue: + return const MacosColor.fromRGBO(0, 88, 224, 1.0); + + case AccentColor.purple: + return const MacosColor.fromRGBO(131, 44, 134, 1.0); + + case AccentColor.pink: + return const MacosColor.fromRGBO(212, 45, 126, 1.0); + + case AccentColor.red: + return const MacosColor.fromRGBO(203, 45, 43, 1.0); + + case AccentColor.orange: + return const MacosColor.fromRGBO(198, 82, 0, 1.0); + + case AccentColor.yellow: + return const MacosColor.fromRGBO(206, 154, 2, 1.0); + + case AccentColor.green: + return const MacosColor.fromRGBO(56, 146, 30, 1.0); + + case AccentColor.graphite: + return const MacosColor.fromRGBO(100, 100, 100, 1.0); + } + } + + /// Returns the active color based on the provided parameters. + static Color getActiveColor({ + required AccentColor accentColor, + required bool isDarkModeEnabled, + required bool isWindowMain, + }) { + if (isDarkModeEnabled) { + if (!isWindowMain) { + return const MacosColor.fromRGBO(76, 78, 65, 1.0); + } + + switch (accentColor) { + case AccentColor.blue: + return const MacosColor.fromRGBO(22, 105, 229, 0.749); + + case AccentColor.purple: + return const MacosColor.fromRGBO(204, 45, 202, 0.749); + + case AccentColor.pink: + return const MacosColor.fromRGBO(229, 74, 145, 0.749); + + case AccentColor.red: + return const MacosColor.fromRGBO(238, 64, 68, 0.749); + + case AccentColor.orange: + return const MacosColor.fromRGBO(244, 114, 0, 0.749); + + case AccentColor.yellow: + return const MacosColor.fromRGBO(233, 176, 0, 0.749); + + case AccentColor.green: + return const MacosColor.fromRGBO(76, 177, 45, 0.749); + + case AccentColor.graphite: + return const MacosColor.fromRGBO(129, 129, 122, 0.824); + } + } + + if (!isWindowMain) { + return const MacosColor.fromRGBO(180, 180, 180, 1.0); + } + + switch (accentColor) { + case AccentColor.blue: + return const MacosColor.fromRGBO(9, 129, 255, 0.749); + + case AccentColor.purple: + return const MacosColor.fromRGBO(162, 28, 165, 0.749); + + case AccentColor.pink: + return const MacosColor.fromRGBO(234, 81, 152, 0.749); + + case AccentColor.red: + return const MacosColor.fromRGBO(220, 32, 40, 0.749); + + case AccentColor.orange: + return const MacosColor.fromRGBO(245, 113, 0, 0.749); + + case AccentColor.yellow: + return const MacosColor.fromRGBO(240, 180, 2, 0.749); + + case AccentColor.green: + return const MacosColor.fromRGBO(66, 174, 33, 0.749); + + case AccentColor.graphite: + return const MacosColor.fromRGBO(174, 174, 167, 0.847); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 63220838..547ae60c 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.6 +version: 2.0.7 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" diff --git a/test/theme/icon_theme_test.dart b/test/theme/icon_theme_test.dart index 79076b18..7efe67fa 100644 --- a/test/theme/icon_theme_test.dart +++ b/test/theme/icon_theme_test.dart @@ -81,7 +81,7 @@ void main() { ); final theme = MacosIconTheme.of(capturedContext); - expect(theme.color, CupertinoColors.activeBlue.color); + expect(theme.color, const MacosColor(0xbe0981ff)); expect(theme.size, 20); }); } diff --git a/test/theme/popup_button_theme_test.dart b/test/theme/popup_button_theme_test.dart index 7cbe6b7a..f704da09 100644 --- a/test/theme/popup_button_theme_test.dart +++ b/test/theme/popup_button_theme_test.dart @@ -96,7 +96,7 @@ void main() { final theme = MacosPopupButtonTheme.of(capturedContext); expect(theme.backgroundColor, const Color(0xffffffff)); - expect(theme.highlightColor, const Color(0xff007aff)); + expect(theme.highlightColor, const MacosColor(0xbe0981ff)); expect(theme.popupColor, const Color(0xfff2f2f7)); }); }); diff --git a/test/theme/pulldown_button_theme_test.dart b/test/theme/pulldown_button_theme_test.dart index 87fc9666..a082b42e 100644 --- a/test/theme/pulldown_button_theme_test.dart +++ b/test/theme/pulldown_button_theme_test.dart @@ -97,7 +97,7 @@ void main() { final theme = MacosPulldownButtonTheme.of(capturedContext); expect(theme.backgroundColor, const Color(0xffffffff)); - expect(theme.highlightColor, const Color(0xff007aff)); + expect(theme.highlightColor, const MacosColor(0xbe0981ff)); expect(theme.pulldownColor, const Color(0xfff2f2f7)); }); }); diff --git a/test/theme/search_field_theme_test.dart b/test/theme/search_field_theme_test.dart index 6f0851b9..39aa577f 100644 --- a/test/theme/search_field_theme_test.dart +++ b/test/theme/search_field_theme_test.dart @@ -80,7 +80,7 @@ void main() { ); final theme = MacosSearchFieldTheme.of(capturedContext); - expect(theme.highlightColor, const Color(0xff007aff)); + expect(theme.highlightColor, const MacosColor(0xbe0981ff)); expect(theme.resultsBackgroundColor, const Color(0xfff2f2f7)); }); });