Support detection of light and dark system colors (#164933)

Part of https://github.com/flutter/flutter/issues/118853

This PR is an enhancement to
https://github.com/flutter/flutter/pull/163335 to provide detection of
system colors for both light and dark mode. This is needed for the
construction of a
[`highContrastTheme`](https://api.flutter.dev/flutter/material/MaterialApp/highContrastTheme.html)
and
[`highContrastDarkTheme`](https://api.flutter.dev/flutter/material/MaterialApp/highContrastDarkTheme.html).
This commit is contained in:
Mouad Debbar 2025-03-12 12:36:11 -04:00 committed by GitHub
parent 963f23e30e
commit 212f66fbe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 218 additions and 161 deletions

View File

@ -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.

View File

@ -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 {

View File

@ -75,46 +75,54 @@ const List<String> systemColorNames = <String>[
];
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 = <String, DomHTMLElement>{};
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 = <String, ui.SystemColor>{};
final Map<String, ui.SystemColor> systemColors;
}
colorDetectors.forEach((systemColorName, detector) {
final computedDetector = domWindow.getComputedStyle(detector);
final computedColor = computedDetector.backgroundColor;
Map<String, ui.SystemColor> _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 = <String, DomHTMLElement>{};
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 = <String, ui.SystemColor>{};
late final Map<String, ui.SystemColor> 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)`.

View File

@ -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>[
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 = <ui.SystemColor>[
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);
}
}