From edb580b73f81e4cb2593502cb643be252e4b868b Mon Sep 17 00:00:00 2001 From: Marco Date: Thu, 5 Nov 2026 19:28:50 +0100 Subject: [PATCH] feat(text_field): macOS-like selection color, selectionColor parameter The selection highlight was the accent color at 20% opacity, hard to read in the dark appearance. It is now an opaque tint of the accent, close to NSColor.selectedTextBackgroundColor: with the blue accent #B3D7FF in the light appearance and about #3F638B in the dark one. The new selectionColor parameter overrides it. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 ++++ lib/src/fields/text_field.dart | 41 +++++++++++++++++++-- pubspec.yaml | 2 +- test/fields/text_field_test.dart | 63 ++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bacfa4a9..e35ebeda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.3.0] +### ✨ New ✨ +- `MacosTextField.selectionColor` sets the color of the selection highlight. + +### 🔄 Updated 🔄 +- The default selection highlight of `MacosTextField` is an opaque tint of the accent color, close to the one macOS paints behind selected text, instead of the accent at 20% opacity, which was hard to read in the dark appearance. + ## [2.2.3] ### 🛠 Fixed 🛠 - Fixed sidebar background painting when wallpaper tinting is disabled: the macOS sidebar now stays transparent unless a decoration color is provided (previously showed a black strip). Addresses #587. diff --git a/lib/src/fields/text_field.dart b/lib/src/fields/text_field.dart index c62f2039..64946440 100644 --- a/lib/src/fields/text_field.dart +++ b/lib/src/fields/text_field.dart @@ -283,6 +283,7 @@ class MacosTextField extends StatefulWidget { this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), this.cursorColor, + this.selectionColor, this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.keyboardAppearance, @@ -407,6 +408,7 @@ class MacosTextField extends StatefulWidget { this.cursorHeight, this.cursorRadius = const Radius.circular(2.0), this.cursorColor, + this.selectionColor, this.selectionHeightStyle = ui.BoxHeightStyle.tight, this.selectionWidthStyle = ui.BoxWidthStyle.tight, this.keyboardAppearance, @@ -684,6 +686,13 @@ class MacosTextField extends StatefulWidget { /// and [CupertinoColors.activeOrange] in the dark theme. final Color? cursorColor; + /// The color of the selection highlight. + /// + /// Defaults to an opaque tint of the accent color, close to the one macOS + /// paints behind selected text (`NSColor.selectedTextBackgroundColor`) in the + /// light and in the dark appearance. + final Color? selectionColor; + /// Controls how tall the selection highlight boxes are computed to be. /// /// See [ui.BoxHeightStyle] for details on available styles. @@ -867,6 +876,13 @@ class MacosTextField extends StatefulWidget { defaultValue: null, ), ); + properties.add( + createCupertinoColorProperty( + 'selectionColor', + selectionColor, + defaultValue: null, + ), + ); properties.add( FlagProperty( 'selectionEnabled', @@ -1186,6 +1202,18 @@ class _MacosTextFieldState extends State ); } + /// An opaque selection color derived from [accent], like the one macOS + /// paints behind selected text: a light tint of the accent over white in the + /// light appearance, a muted one over gray in the dark appearance. With the + /// blue accent they come out as `#B3D7FF` and about `#3F638B`, the system's + /// own values. + static Color _defaultSelectionColor(Color accent, Brightness brightness) { + final base = brightness.isDark + ? const Color(0xFF5A5A5A) + : const Color(0xFFFFFFFF); + return Color.lerp(base, accent.withValues(alpha: 1), 0.3)!; + } + Color? _resolveAccentColor(BuildContext context, AccentColor? accentColor) { if (accentColor == null) { return null; @@ -1442,9 +1470,16 @@ class _MacosTextFieldState extends State color: focusedDecoration.color ?? const Color(0x00000000), ); - final Color selectionColor = MacosTheme.of( - context, - ).primaryColor.withValues(alpha: 0.2); + final Color selectionColor = + MacosDynamicColor.maybeResolve(widget.selectionColor, context) ?? + _defaultSelectionColor( + _resolveAccentColor( + context, + AccentColorListener.instance.currentAccentColor, + ) ?? + themeData.primaryColor, + themeData.brightness, + ); final Widget paddedEditable = Padding( padding: widget.padding, diff --git a/pubspec.yaml b/pubspec.yaml index c6355f3f..b34d2f9d 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.2.3 +version: 2.3.0 homepage: "https://macosui.dev" repository: "https://github.com/GroovinChip/macos_ui" diff --git a/test/fields/text_field_test.dart b/test/fields/text_field_test.dart index 25d034ef..83195ef1 100644 --- a/test/fields/text_field_test.dart +++ b/test/fields/text_field_test.dart @@ -26,4 +26,67 @@ void main() { await tester.enterText(textField, input); expect(controller.text, input); }); + Widget field(TextEditingController controller, {Brightness? brightness}) { + return MacosApp( + theme: MacosThemeData(brightness: brightness ?? Brightness.light), + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) { + return Center( + child: SizedBox( + width: 400, + child: MacosTextField(controller: controller), + ), + ); + }, + ), + ], + ), + ), + ); + } + + testWidgets('The default selection color is opaque in both appearances', ( + tester, + ) async { + final controller = TextEditingController(text: 'Hello'); + await tester.pumpWidget(field(controller)); + await tester.pump(const Duration(milliseconds: 1)); + final light = tester + .widget(find.byType(EditableText)) + .selectionColor!; + + await tester.pumpWidget(field(controller, brightness: Brightness.dark)); + await tester.pump(const Duration(milliseconds: 1)); + final dark = tester + .widget(find.byType(EditableText)) + .selectionColor!; + + expect(light.a, 1.0); + expect(dark.a, 1.0); + expect(dark.computeLuminance(), lessThan(light.computeLuminance())); + }); + + testWidgets('selectionColor overrides the default', (tester) async { + await tester.pumpWidget( + MacosApp( + home: MacosWindow( + child: MacosScaffold( + children: [ + ContentArea( + builder: (context, _) => const Center( + child: MacosTextField(selectionColor: Color(0xFF123456)), + ), + ), + ], + ), + ), + ), + ); + await tester.pump(const Duration(milliseconds: 1)); + final editable = tester.widget(find.byType(EditableText)); + expect(editable.selectionColor, const Color(0xFF123456)); + }); }