Bug 1750932 - Add color_scheme / content_color_scheme properties to theme API. r=robwu,dao

This allows themes to override our light / dark theme heuristics if they
choose to, so that we don't have to complicate the heuristics too much.

This is specially useful for themes with images, where the image might
be "light", but still have enough contrast with light text. A good
example is the theme mentioned in bug 1749837 comment 0.

The semantics are:

 * color_scheme: If set, overrides the general "toolbar theme" (so
   window and context menu appearance and so on), otherwise we fall back
   to heuristics.

 * content_color_scheme: If set, overrides the color scheme for the
   content area. Otherwise we fall back to color_scheme if present, or
   heuristics otherwise.

One thing that I didn't include was a sort of "system" option, which
might be useful to say "this theme is neutral, and works both for light
and dark themes". Let me know if you think that's a must-have, otherwise
I think it's probably worth deferring to a follow-up if it's needed at
all.

Differential Revision: https://phabricator.services.mozilla.com/D136354
This commit is contained in:
Emilio Cobos Álvarez 2022-03-17 14:44:03 +00:00
Родитель 519a2c6f51
Коммит 3d2bf29691
6 изменённых файлов: 265 добавлений и 38 удалений

Просмотреть файл

@ -200,11 +200,12 @@ add_task(function test_frame() {
const kDark = 0;
const kLight = 1;
const kDefault = 2;
const kSystem = 2;
const kToolbar = 3;
// The above tests should be enough to make sure that the prefs behave as
// expected, the following ones test various edge cases in a simpler way.
async function testTheme(description, toolbar, content, themeData) {
async function testTheme(description, toolbar, content, themeManifestData) {
info(description);
let extension = ExtensionTestUtils.loadExtension({
@ -214,7 +215,7 @@ async function testTheme(description, toolbar, content, themeData) {
id: "dummy@mochi.test",
},
},
theme: themeData,
...themeManifestData,
},
});
@ -245,33 +246,191 @@ add_task(async function test_dark_toolbar_dark_text() {
await testTheme(
"Dark toolbar color, dark toolbar background",
kDark,
kDefault,
kToolbar,
{
colors: {
toolbar: "rgb(20, 17, 26)",
toolbar_text: "rgb(251, 29, 78)",
theme: {
colors: {
toolbar: "rgb(20, 17, 26)",
toolbar_text: "rgb(251, 29, 78)",
},
},
}
);
// Dark frame text is ignored as it might be overlaid with an image,
// see bug 1741931.
await testTheme("Dark frame is ignored", kLight, kDefault, {
colors: {
frame: "#000000",
tab_background_text: "#000000",
await testTheme("Dark frame is ignored", kLight, kToolbar, {
theme: {
colors: {
frame: "#000000",
tab_background_text: "#000000",
},
},
});
await testTheme(
"Semi-transparent toolbar backgrounds are ignored.",
kLight,
kDefault,
kToolbar,
{
colors: {
toolbar: "rgba(0, 0, 0, .2)",
toolbar_text: "#000",
theme: {
colors: {
toolbar: "rgba(0, 0, 0, .2)",
toolbar_text: "#000",
},
},
}
);
});
add_task(async function dark_theme_presence_overrides_heuristics() {
const systemScheme = window.matchMedia("(-moz-system-dark-theme)").matches
? kDark
: kLight;
await testTheme(
"darkTheme presence overrides heuristics",
systemScheme,
systemScheme,
{
theme: {
colors: {
toolbar: "#000",
toolbar_text: "#fff",
},
},
dark_theme: {
colors: {
toolbar: "#000",
toolbar_text: "#fff",
},
},
}
);
});
add_task(async function color_scheme_override() {
await testTheme(
"color_scheme overrides toolbar / toolbar_text pair (dark)",
kDark,
kDark,
{
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
},
properties: {
color_scheme: "dark",
},
},
}
);
await testTheme(
"color_scheme overrides toolbar / toolbar_text pair (light)",
kLight,
kLight,
{
theme: {
colors: {
toolbar: "#000",
toolbar_text: "#fff",
},
properties: {
color_scheme: "light",
},
},
}
);
await testTheme(
"content_color_scheme overrides ntp_text / ntp_background (dark)",
kLight,
kDark,
{
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
ntp_background: "#fff",
ntp_text: "#000",
},
properties: {
content_color_scheme: "dark",
},
},
}
);
await testTheme(
"content_color_scheme overrides ntp_text / ntp_background (light)",
kLight,
kLight,
{
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
ntp_background: "#000",
ntp_text: "#fff",
},
properties: {
content_color_scheme: "light",
},
},
}
);
await testTheme(
"content_color_scheme overrides color_scheme only for content",
kLight,
kDark,
{
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
ntp_background: "#fff",
ntp_text: "#000",
},
properties: {
content_color_scheme: "dark",
},
},
}
);
await testTheme(
"content_color_scheme sytem overrides color_scheme only for content",
kLight,
kSystem,
{
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
ntp_background: "#fff",
ntp_text: "#000",
},
properties: {
content_color_scheme: "system",
},
},
}
);
await testTheme("color_scheme: sytem override", kSystem, kSystem, {
theme: {
colors: {
toolbar: "#fff",
toolbar_text: "#000",
ntp_background: "#fff",
ntp_text: "#000",
},
properties: {
color_scheme: "system",
content_color_scheme: "system",
},
},
});
});

Просмотреть файл

@ -1305,7 +1305,7 @@
# Communicates the preferred content theme color to platform (for e.g.,
# prefers-color-scheme).
#
# dark (0), light (1), or toolbar (2).
# dark (0), light (1), system (2), or toolbar (3).
#
# Default to "toolbar", the theming code sets it appropriately.
- name: browser.theme.content-theme

Просмотреть файл

@ -352,6 +352,11 @@ class Theme {
styles.backgroundsTiling = tiling.join(",");
break;
}
case "color_scheme":
case "content_color_scheme": {
styles[property] = val;
break;
}
default: {
if (
this.experiment &&

Просмотреть файл

@ -303,6 +303,16 @@
},
"maxItems": 15,
"optional": true
},
"color_scheme": {
"optional": true,
"type": "string",
"enum": ["auto", "light", "dark", "system"]
},
"content_color_scheme": {
"optional": true,
"type": "string",
"enum": ["auto", "light", "dark", "system"]
}
},
"additionalProperties": { "type": "string" }

Просмотреть файл

@ -239,9 +239,10 @@ LightweightThemeConsumer.prototype = {
_update(themeData) {
this._lastData = themeData;
const hasDarkTheme = !!themeData.darkTheme;
let updateGlobalThemeData = true;
let useDarkTheme = (() => {
if (!themeData.darkTheme) {
if (!hasDarkTheme) {
return false;
}
if (this.darkThemeMediaQuery?.matches) {
@ -311,7 +312,12 @@ LightweightThemeConsumer.prototype = {
if (theme.id != DEFAULT_THEME_ID || useDarkTheme) {
if (updateGlobalThemeData) {
_determineToolbarAndContentTheme(this._doc, theme._processedColors);
_determineToolbarAndContentTheme(
this._doc,
theme,
hasDarkTheme,
useDarkTheme
);
}
root.setAttribute("lwtheme", "true");
} else {
@ -435,54 +441,101 @@ function _setProperty(elem, active, variableName, value) {
}
}
function _determineToolbarAndContentTheme(aDoc, aColors) {
function _determineToolbarAndContentTheme(
aDoc,
aTheme,
aHasDarkTheme = false,
aIsDarkTheme = false
) {
const kDark = 0;
const kLight = 1;
const kSystem = 2;
const kToolbar = 3; // Only valid for content theme
const colors = aTheme?._processedColors;
function prefValue(aColor, aIsForeground = false) {
if (typeof aColor != "object") {
aColor = _cssColorToRGBA(aDoc, aColor);
}
return _isColorDark(aColor.r, aColor.g, aColor.b) == aIsForeground ? 1 : 0;
return _isColorDark(aColor.r, aColor.g, aColor.b) == aIsForeground
? kLight
: kDark;
}
function colorSchemeValue(aColorScheme) {
if (!aColorScheme) {
return null;
}
switch (aColorScheme) {
case "light":
return kLight;
case "dark":
return kDark;
case "system":
return kSystem;
case "auto":
default:
break;
}
return null;
}
let toolbarTheme = (function() {
if (!aColors) {
if (!aTheme) {
if (!DEFAULT_THEME_RESPECTS_SYSTEM_COLOR_SCHEME) {
return 1;
return kLight;
}
return 2;
return kSystem;
}
let themeValue = colorSchemeValue(aTheme.color_scheme);
if (themeValue !== null) {
return themeValue;
}
if (aHasDarkTheme) {
return aIsDarkTheme ? kDark : kLight;
}
// We prefer looking at toolbar background first (if it's opaque) because
// some text colors can be dark enough for our heuristics, but still
// contrast well enough with a dark background, see bug 1743010.
if (aColors.toolbarColor) {
let color = _cssColorToRGBA(aDoc, aColors.toolbarColor);
if (colors.toolbarColor) {
let color = _cssColorToRGBA(aDoc, colors.toolbarColor);
if (color.a == 1) {
return prefValue(color);
}
}
if (aColors.toolbar_text) {
return prefValue(aColors.toolbar_text, /* aIsForeground = */ true);
if (colors.toolbar_text) {
return prefValue(colors.toolbar_text, /* aIsForeground = */ true);
}
// It'd seem sensible to try looking at the "frame" background (accentcolor),
// but we don't because some themes that use background images leave it to
// black, see bug 1741931.
//
// Fall back to black as per the textcolor processing above.
return prefValue(aColors.textcolor || "black", /* aIsForeground = */ true);
return prefValue(colors.textcolor || "black", /* aIsForeground = */ true);
})();
let contentTheme = (function() {
if (!aColors) {
return 2;
if (!aTheme) {
return kToolbar;
}
if (aColors.ntp_background) {
let themeValue = colorSchemeValue(
aTheme.content_color_scheme || aTheme.color_scheme
);
if (themeValue !== null) {
return themeValue;
}
if (aHasDarkTheme) {
return aIsDarkTheme ? kDark : kLight;
}
if (colors.ntp_background) {
// We don't care about transparency here as ntp background can't have
// transparency (alpha channel is dropped).
return prefValue(aColors.ntp_background);
return prefValue(colors.ntp_background);
}
if (aColors.ntp_text) {
return prefValue(aColors.ntp_text, /* aIsForeground = */ true);
if (colors.ntp_text) {
return prefValue(colors.ntp_text, /* aIsForeground = */ true);
}
return 2;
return kToolbar;
})();
Services.prefs.setIntPref("browser.theme.toolbar-theme", toolbarTheme);

Просмотреть файл

@ -1165,6 +1165,8 @@ ColorScheme LookAndFeel::ThemeDerivedColorSchemeForContent() {
return ColorScheme::Dark;
case 1: // Light
return ColorScheme::Light;
case 2: // System
return SystemColorScheme();
default:
return ColorSchemeForChrome();
}
@ -1194,10 +1196,8 @@ void LookAndFeel::RecomputeColorSchemes() {
case 2:
return SystemColorScheme();
default:
break; // Use the browser theme.
return ThemeDerivedColorSchemeForContent();
}
return ThemeDerivedColorSchemeForContent();
}();
}