diff --git a/engine/src/flutter/lib/ui/platform_dispatcher.dart b/engine/src/flutter/lib/ui/platform_dispatcher.dart index 80997e29e0..d2a42974f4 100644 --- a/engine/src/flutter/lib/ui/platform_dispatcher.dart +++ b/engine/src/flutter/lib/ui/platform_dispatcher.dart @@ -1526,10 +1526,6 @@ class PlatformDispatcher { /// A color specified in the operating system UI color palette. /// -/// The static getters in this class, such as [accentColor] and [buttonText], -/// provide standard system colors defined by the -/// [W3C CSS specification](https://drafts.csswg.org/css-color/#css-system-colors). -/// /// As of the current release, system colors are supported on web only. To check /// if the current platform supports system colors, use the static /// [platformProvidesSystemColors] field. If the field is `false`, other @@ -1543,6 +1539,28 @@ class PlatformDispatcher { /// recommended that widgets use system-specified colors to make content more /// legible for users. /// +/// The "light" system colors are available through [SystemColor.light], and the "dark" system +/// colors are available through [SystemColor.dark]. +/// +/// Example: +/// +/// ```dart +/// import 'dart:ui'; +/// +/// Color getSystemAccentColor() { +/// Color? systemAccentColor; +/// if (SystemColor.platformProvidesSystemColors) { +/// if (PlatformDispatcher.instance.platformBrightness == Brightness.light) { +/// systemAccentColor = SystemColor.light.accentColor.value; +/// } else { +/// systemAccentColor = SystemColor.dark.accentColor.value; +/// } +/// } +/// +/// return systemAccentColor ?? const Color(0xFF007AFF); +/// } +/// ``` +/// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors @@ -1551,8 +1569,8 @@ class PlatformDispatcher { final class SystemColor { /// Creates an instance of a system color. /// - /// [name] is the name of the color. System colors provided by static getters - /// in this class, such as [accentColor] and [buttonText], use standard names + /// [name] is the name of the color. System colors provided by [SystemColorPalette], such as + /// [SystemColorPalette.accentColor] and [SystemColorPalette.buttonText], use standard names /// defined by the [W3C CSS specification](https://drafts.csswg.org/css-color/#css-system-colors). /// /// [value] is the color value, if this color name is supported, and null if @@ -1596,6 +1614,23 @@ final class SystemColor { /// * [isSupported], which returns whether a specific color is supported. static bool get platformProvidesSystemColors => false; + /// A palette of system colors for light mode. + static final SystemColorPalette light = SystemColorPalette._(Brightness.light); + + /// A palette of system colors for dark mode. + static final SystemColorPalette dark = SystemColorPalette._(Brightness.dark); +} + +/// A palette of system colors specified in the operating system for a given [brightness]. +/// +/// The getters in this class, such as [accentColor] and [buttonText], provide standard system +/// colors defined by the [W3C CSS specification](https://drafts.csswg.org/css-color/#css-system-colors). +final class SystemColorPalette { + SystemColorPalette._(this.brightness); + + /// The brightness mode for which this palette is defined. + final Brightness brightness; + static UnsupportedError _systemColorUnsupportedError() { return UnsupportedError('SystemColor not supported on the current platform.'); } @@ -1605,133 +1640,133 @@ final class SystemColor { /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get accentColor => throw _systemColorUnsupportedError(); + SystemColor get accentColor => throw _systemColorUnsupportedError(); /// Returns system color named "AccentColorText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get accentColorText => throw _systemColorUnsupportedError(); + SystemColor get accentColorText => throw _systemColorUnsupportedError(); /// Returns system color named "ActiveText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get activeText => throw _systemColorUnsupportedError(); + SystemColor get activeText => throw _systemColorUnsupportedError(); /// Returns system color named "ButtonBorder". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get buttonBorder => throw _systemColorUnsupportedError(); + SystemColor get buttonBorder => throw _systemColorUnsupportedError(); /// Returns system color named "ButtonFace". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get buttonFace => throw _systemColorUnsupportedError(); + SystemColor get buttonFace => throw _systemColorUnsupportedError(); /// Returns system color named "ButtonText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get buttonText => throw _systemColorUnsupportedError(); + SystemColor get buttonText => throw _systemColorUnsupportedError(); /// Returns system color named "Canvas". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get canvas => throw _systemColorUnsupportedError(); + SystemColor get canvas => throw _systemColorUnsupportedError(); /// Returns system color named "CanvasText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get canvasText => throw _systemColorUnsupportedError(); + SystemColor get canvasText => throw _systemColorUnsupportedError(); /// Returns system color named "Field". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get field => throw _systemColorUnsupportedError(); + SystemColor get field => throw _systemColorUnsupportedError(); /// Returns system color named "FieldText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get fieldText => throw _systemColorUnsupportedError(); + SystemColor get fieldText => throw _systemColorUnsupportedError(); /// Returns system color named "GrayText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get grayText => throw _systemColorUnsupportedError(); + SystemColor get grayText => throw _systemColorUnsupportedError(); /// Returns system color named "Highlight". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get highlight => throw _systemColorUnsupportedError(); + SystemColor get highlight => throw _systemColorUnsupportedError(); /// Returns system color named "HighlightText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get highlightText => throw _systemColorUnsupportedError(); + SystemColor get highlightText => throw _systemColorUnsupportedError(); /// Returns system color named "LinkText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get linkText => throw _systemColorUnsupportedError(); + SystemColor get linkText => throw _systemColorUnsupportedError(); /// Returns system color named "Mark". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get mark => throw _systemColorUnsupportedError(); + SystemColor get mark => throw _systemColorUnsupportedError(); /// Returns system color named "MarkText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get markText => throw _systemColorUnsupportedError(); + SystemColor get markText => throw _systemColorUnsupportedError(); /// Returns system color named "SelectedItem". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get selectedItem => throw _systemColorUnsupportedError(); + SystemColor get selectedItem => throw _systemColorUnsupportedError(); /// Returns system color named "SelectedItemText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get selectedItemText => throw _systemColorUnsupportedError(); + SystemColor get selectedItemText => throw _systemColorUnsupportedError(); /// Returns system color named "VisitedText". /// /// See also: /// /// * https://drafts.csswg.org/css-color/#css-system-colors - static SystemColor get visitedText => throw _systemColorUnsupportedError(); + SystemColor get visitedText => throw _systemColorUnsupportedError(); } /// Configuration of the platform. diff --git a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart index ca59012d30..b4dd689d5a 100644 --- a/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/platform_dispatcher.dart @@ -162,66 +162,45 @@ final class SystemColor { bool get isSupported => value != null; static bool get platformProvidesSystemColors => true; - static SystemColor _lookUp(String name) { - return engine.SystemColorPaletteDetector.instance.systemColors[name]!; + static final SystemColorPalette light = SystemColorPalette._( + engine.SystemColorPaletteDetector.light, + ); + + static final SystemColorPalette dark = SystemColorPalette._( + engine.SystemColorPaletteDetector.dark, + ); +} + +final class SystemColorPalette { + SystemColorPalette._(this._detector); + + Brightness get brightness => _detector.brightness; + + final engine.SystemColorPaletteDetector _detector; + + SystemColor _lookUp(String name) { + return _detector.systemColors[name]!; } - static SystemColor get accentColor => _accentColor; - static final SystemColor _accentColor = _lookUp('AccentColor'); - - static SystemColor get accentColorText => _accentColorText; - static final SystemColor _accentColorText = _lookUp('AccentColorText'); - - static SystemColor get activeText => _activeText; - static final SystemColor _activeText = _lookUp('ActiveText'); - - static SystemColor get buttonBorder => _buttonBorder; - static final SystemColor _buttonBorder = _lookUp('ButtonBorder'); - - static SystemColor get buttonFace => _buttonFace; - static final SystemColor _buttonFace = _lookUp('ButtonFace'); - - static SystemColor get buttonText => _buttonText; - static final SystemColor _buttonText = _lookUp('ButtonText'); - - static SystemColor get canvas => _canvas; - static final SystemColor _canvas = _lookUp('Canvas'); - - static SystemColor get canvasText => _canvasText; - static final SystemColor _canvasText = _lookUp('CanvasText'); - - static SystemColor get field => _field; - static final SystemColor _field = _lookUp('Field'); - - static SystemColor get fieldText => _fieldText; - static final SystemColor _fieldText = _lookUp('FieldText'); - - static SystemColor get grayText => _grayText; - static final SystemColor _grayText = _lookUp('GrayText'); - - static SystemColor get highlight => _highlight; - static final SystemColor _highlight = _lookUp('Highlight'); - - static SystemColor get highlightText => _highlightText; - static final SystemColor _highlightText = _lookUp('HighlightText'); - - static SystemColor get linkText => _linkText; - static final SystemColor _linkText = _lookUp('LinkText'); - - static SystemColor get mark => _mark; - static final SystemColor _mark = _lookUp('Mark'); - - static SystemColor get markText => _markText; - static final SystemColor _markText = _lookUp('MarkText'); - - static SystemColor get selectedItem => _selectedItem; - static final SystemColor _selectedItem = _lookUp('SelectedItem'); - - static SystemColor get selectedItemText => _selectedItemText; - static final SystemColor _selectedItemText = _lookUp('SelectedItemText'); - - static SystemColor get visitedText => _visitedText; - static final SystemColor _visitedText = _lookUp('VisitedText'); + SystemColor get accentColor => _lookUp('AccentColor'); + SystemColor get accentColorText => _lookUp('AccentColorText'); + SystemColor get activeText => _lookUp('ActiveText'); + SystemColor get buttonBorder => _lookUp('ButtonBorder'); + SystemColor get buttonFace => _lookUp('ButtonFace'); + SystemColor get buttonText => _lookUp('ButtonText'); + SystemColor get canvas => _lookUp('Canvas'); + SystemColor get canvasText => _lookUp('CanvasText'); + SystemColor get field => _lookUp('Field'); + SystemColor get fieldText => _lookUp('FieldText'); + SystemColor get grayText => _lookUp('GrayText'); + SystemColor get highlight => _lookUp('Highlight'); + SystemColor get highlightText => _lookUp('HighlightText'); + SystemColor get linkText => _lookUp('LinkText'); + SystemColor get mark => _lookUp('Mark'); + SystemColor get markText => _lookUp('MarkText'); + SystemColor get selectedItem => _lookUp('SelectedItem'); + SystemColor get selectedItemText => _lookUp('SelectedItemText'); + SystemColor get visitedText => _lookUp('VisitedText'); } enum FramePhase { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/high_contrast.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/high_contrast.dart index 2dd947e144..cb9b050177 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/high_contrast.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/high_contrast.dart @@ -75,46 +75,54 @@ const List systemColorNames = [ ]; class SystemColorPaletteDetector { - SystemColorPaletteDetector() { - final hostDetector = createDomHTMLDivElement(); - hostDetector.style - ..position = 'absolute' - ..transform = 'translate(-10000, -10000)'; - domDocument.body!.appendChild(hostDetector); + SystemColorPaletteDetector(this.brightness) : systemColors = _detectSystemColors(brightness); - final colorDetectors = {}; + static SystemColorPaletteDetector light = SystemColorPaletteDetector(ui.Brightness.light); + static SystemColorPaletteDetector dark = SystemColorPaletteDetector(ui.Brightness.dark); - for (final systemColorName in systemColorNames) { - final detector = createDomHTMLDivElement(); - detector.style.backgroundColor = systemColorName; - detector.innerText = '$systemColorName detector'; - hostDetector.appendChild(detector); - colorDetectors[systemColorName] = detector; - } + final ui.Brightness brightness; - final results = {}; + final Map systemColors; +} - colorDetectors.forEach((systemColorName, detector) { - final computedDetector = domWindow.getComputedStyle(detector); - final computedColor = computedDetector.backgroundColor; +Map _detectSystemColors(ui.Brightness brightness) { + final hostDetector = createDomHTMLDivElement(); + hostDetector.style + ..position = 'absolute' + ..transform = 'translate(-10000, -10000)' + // Force the browser to use light mode colors or dark mode colors. + ..setProperty('color-scheme', brightness == ui.Brightness.light ? 'light' : 'dark'); + domDocument.body!.appendChild(hostDetector); - final isSupported = domCSS.supports('color', systemColorName); - ui.Color? value; - if (isSupported) { - value = parseCssRgb(computedColor); - } + final colorDetectors = {}; - results[systemColorName] = ui.SystemColor(name: systemColorName, value: value); - }); - systemColors = results; - - // Once colors have been detected, this element is no longer needed. - hostDetector.remove(); + for (final systemColorName in systemColorNames) { + final detector = createDomHTMLDivElement(); + detector.style.backgroundColor = systemColorName; + detector.innerText = '$systemColorName detector'; + hostDetector.appendChild(detector); + colorDetectors[systemColorName] = detector; } - static SystemColorPaletteDetector instance = SystemColorPaletteDetector(); + final results = {}; - late final Map systemColors; + colorDetectors.forEach((systemColorName, detector) { + final computedDetector = domWindow.getComputedStyle(detector); + final computedColor = computedDetector.backgroundColor; + + final isSupported = domCSS.supports('color', systemColorName); + ui.Color? value; + if (isSupported) { + value = parseCssRgb(computedColor); + } + + results[systemColorName] = ui.SystemColor(name: systemColorName, value: value); + }); + + // Once colors have been detected, this element is no longer needed. + hostDetector.remove(); + + return results; } /// Parses CSS RGB color written as `rgb(r, g, b)` or `rgba(r, g, b, a)`. diff --git a/engine/src/flutter/lib/web_ui/test/engine/high_contrast_test.dart b/engine/src/flutter/lib/web_ui/test/engine/high_contrast_test.dart index feaa157ce6..2aacbc98d7 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/high_contrast_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/high_contrast_test.dart @@ -93,11 +93,14 @@ void testMain() { 'VisitedText', ]; - final detector = SystemColorPaletteDetector(); - expect(detector.systemColors.keys, containsAll(systemColorNames)); + final detectorLight = SystemColorPaletteDetector(ui.Brightness.light); + expect(detectorLight.systemColors.keys, containsAll(systemColorNames)); + + final detectorDark = SystemColorPaletteDetector(ui.Brightness.dark); + expect(detectorDark.systemColors.keys, containsAll(systemColorNames)); expect( - detector.systemColors.values.where((color) => color.isSupported), + detectorLight.systemColors.values.where((color) => color.isSupported), // Different browser/OS combinations support different colors. It's // impractical to encode the precise number for each combo. Instead, this // test only makes sure that at least some "reasonable" number of colors @@ -106,6 +109,26 @@ void testMain() { // colors. hasLength(greaterThan(15)), ); + expect( + detectorDark.systemColors.values.where((color) => color.isSupported), + hasLength(greaterThan(15)), + ); + + // Ensure that at least some colors are different between light and dark mode. + int differentCount = 0; + for (final colorName in systemColorNames) { + final lightColor = detectorLight.systemColors[colorName]; + final darkColor = detectorDark.systemColors[colorName]; + if (lightColor != null && + darkColor != null && + lightColor.isSupported && + darkColor.isSupported && + lightColor.value != darkColor.value) { + differentCount++; + } + } + // The number 3 has no special meaning. It's just to ensure that "some" colors are different. + expect(differentCount, greaterThan(3)); }); test('SystemColor', () { @@ -121,51 +144,63 @@ void testMain() { expect(unsupportedColor.name, 'UnsupportedColor'); expect(unsupportedColor.value, isNull); expect(unsupportedColor.isSupported, isFalse); + }); - expect(ui.SystemColor.accentColor.name, 'AccentColor'); - expect(ui.SystemColor.accentColorText.name, 'AccentColorText'); - expect(ui.SystemColor.activeText.name, 'ActiveText'); - expect(ui.SystemColor.buttonBorder.name, 'ButtonBorder'); - expect(ui.SystemColor.buttonFace.name, 'ButtonFace'); - expect(ui.SystemColor.buttonText.name, 'ButtonText'); - expect(ui.SystemColor.canvas.name, 'Canvas'); - expect(ui.SystemColor.canvasText.name, 'CanvasText'); - expect(ui.SystemColor.field.name, 'Field'); - expect(ui.SystemColor.fieldText.name, 'FieldText'); - expect(ui.SystemColor.grayText.name, 'GrayText'); - expect(ui.SystemColor.highlight.name, 'Highlight'); - expect(ui.SystemColor.highlightText.name, 'HighlightText'); - expect(ui.SystemColor.linkText.name, 'LinkText'); - expect(ui.SystemColor.mark.name, 'Mark'); - expect(ui.SystemColor.markText.name, 'MarkText'); - expect(ui.SystemColor.selectedItem.name, 'SelectedItem'); - expect(ui.SystemColor.selectedItemText.name, 'SelectedItemText'); - expect(ui.SystemColor.visitedText.name, 'VisitedText'); + group('SystemColorPalette', () { + test('.light', () { + testPalette(ui.SystemColor.light); + }); - final allColors = [ - ui.SystemColor.accentColor, - ui.SystemColor.accentColorText, - ui.SystemColor.activeText, - ui.SystemColor.buttonBorder, - ui.SystemColor.buttonFace, - ui.SystemColor.buttonText, - ui.SystemColor.canvas, - ui.SystemColor.canvasText, - ui.SystemColor.field, - ui.SystemColor.fieldText, - ui.SystemColor.grayText, - ui.SystemColor.highlight, - ui.SystemColor.highlightText, - ui.SystemColor.linkText, - ui.SystemColor.mark, - ui.SystemColor.markText, - ui.SystemColor.selectedItem, - ui.SystemColor.selectedItemText, - ui.SystemColor.visitedText, - ]; - - for (final color in allColors) { - expect(color.value != null, color.isSupported); - } + test('.dark', () { + testPalette(ui.SystemColor.dark); + }); }); } + +void testPalette(ui.SystemColorPalette palette) { + expect(palette.accentColor.name, 'AccentColor'); + expect(palette.accentColorText.name, 'AccentColorText'); + expect(palette.activeText.name, 'ActiveText'); + expect(palette.buttonBorder.name, 'ButtonBorder'); + expect(palette.buttonFace.name, 'ButtonFace'); + expect(palette.buttonText.name, 'ButtonText'); + expect(palette.canvas.name, 'Canvas'); + expect(palette.canvasText.name, 'CanvasText'); + expect(palette.field.name, 'Field'); + expect(palette.fieldText.name, 'FieldText'); + expect(palette.grayText.name, 'GrayText'); + expect(palette.highlight.name, 'Highlight'); + expect(palette.highlightText.name, 'HighlightText'); + expect(palette.linkText.name, 'LinkText'); + expect(palette.mark.name, 'Mark'); + expect(palette.markText.name, 'MarkText'); + expect(palette.selectedItem.name, 'SelectedItem'); + expect(palette.selectedItemText.name, 'SelectedItemText'); + expect(palette.visitedText.name, 'VisitedText'); + + final allColors = [ + palette.accentColor, + palette.accentColorText, + palette.activeText, + palette.buttonBorder, + palette.buttonFace, + palette.buttonText, + palette.canvas, + palette.canvasText, + palette.field, + palette.fieldText, + palette.grayText, + palette.highlight, + palette.highlightText, + palette.linkText, + palette.mark, + palette.markText, + palette.selectedItem, + palette.selectedItemText, + palette.visitedText, + ]; + + for (final color in allColors) { + expect(color.value != null, color.isSupported); + } +}