Bug 1794628 - Implement inverted-colors media feature r=geckoview-reviewers,morgan,emilio,m_kato,cmartin

Implemented the inverted-colors media feature from Media Queries Level 5
for all platforms.
Spec: https://drafts.csswg.org/mediaqueries-5/#inverted

Platform specific implementations:
- Windows: Checks system color filter setting, and if it is inverted
  (note: Windows does not live update due to having to read a reg key)
- Mac: Checks dedicated inverted accessibility system setting
- Android: Checks dedicated inverted system setting
- Linux: No GTK API exposes anything like it so always none

Locked behind new pref `layout.css.inverted-colors.enabled`,
always off by default for now.

Also added new WPT tests (none previously).

Other browsers:
- WebKit: shipped since Safari 9.1 (Jan 2017)
- Blink: no signal

Test page: https://goose.icu/inverted-colors

Differential Revision: https://phabricator.services.mozilla.com/D173201
This commit is contained in:
CanadaHonk 2023-04-11 13:34:00 +00:00
Родитель 7beef23771
Коммит 2d520652e0
22 изменённых файлов: 192 добавлений и 65 удалений

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

@ -636,6 +636,7 @@ mozilla::StylePrefersContrast Gecko_MediaFeatures_PrefersContrast(
const mozilla::dom::Document*);
mozilla::StylePrefersColorScheme Gecko_MediaFeatures_PrefersColorScheme(
const mozilla::dom::Document*, bool aUseContent);
bool Gecko_MediaFeatures_InvertedColors(const mozilla::dom::Document*);
mozilla::StyleScripting Gecko_MediaFeatures_Scripting(
const mozilla::dom::Document*);

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

@ -325,6 +325,13 @@ StylePrefersContrast Gecko_MediaFeatures_PrefersContrast(
return StylePrefersContrast::Custom;
}
bool Gecko_MediaFeatures_InvertedColors(const Document* aDocument) {
if (aDocument->ShouldResistFingerprinting()) {
return false;
}
return LookAndFeel::GetInt(LookAndFeel::IntID::InvertedColors, 0) == 1;
}
StyleScripting Gecko_MediaFeatures_Scripting(const Document* aDocument) {
const auto* doc = aDocument;
if (aDocument->IsStaticDocument()) {

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

@ -887,6 +887,16 @@ slot {
display: contents;
}
/* Un-invert images and videos for users using inverted colors.
* "User agents must add the following rule to their UA style sheet"
* https://www.w3.org/TR/mediaqueries-5/#inverted
*/
@media (inverted-colors) {
img:not(picture > img), picture, video {
filter: invert(100%);
}
}
/* Hide noscript elements if scripting is enabled */
@media (scripting) {
noscript {

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

@ -59,6 +59,10 @@ public class GeckoSystemStateListener implements InputManager.InputDeviceListene
};
contentResolver.registerContentObserver(animationSetting, false, mContentObserver);
final Uri invertSetting =
Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
contentResolver.registerContentObserver(invertSetting, false, mContentObserver);
mIsNightMode =
(sApplicationContext.getResources().getConfiguration().uiMode
& Configuration.UI_MODE_NIGHT_MASK)
@ -106,6 +110,26 @@ public class GeckoSystemStateListener implements InputManager.InputDeviceListene
== 0.0f;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@WrapForJNI(calledFrom = "gecko")
/**
* For inverted-colors queries feature.
*
* <p>Uses `Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED` which was introduced in API
* version 21.
*/
private static boolean isInvertedColors() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
final ContentResolver contentResolver = sApplicationContext.getContentResolver();
return Settings.Secure.getInt(
contentResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0)
== 1;
}
/** For prefers-color-scheme media queries feature. */
public boolean isNightMode() {
return mIsNightMode;

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

@ -8821,6 +8821,13 @@
mirror: always
rust: true
# Dictates whether or not the inverted-colors media query is enabled.
- name: layout.css.inverted-colors.enabled
type: RelaxedAtomicBool
value: false
mirror: always
rust: true
# Is support for forced-color-adjust properties enabled?
- name: layout.css.forced-color-adjust.enabled
type: RelaxedAtomicBool

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

@ -297,6 +297,35 @@ fn eval_forced_colors(context: &Context, query_value: Option<ForcedColors>) -> b
}
}
/// Possible values for the inverted-colors media query.
/// https://drafts.csswg.org/mediaqueries-5/#inverted
#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
#[repr(u8)]
enum InvertedColors {
/// Colors are displayed normally.
None,
/// All pixels within the displayed area have been inverted.
Inverted,
}
/// https://drafts.csswg.org/mediaqueries-5/#inverted
fn eval_inverted_colors(
context: &Context,
query_value: Option<InvertedColors>,
) -> bool {
let inverted_colors =
unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) };
let query_value = match query_value {
Some(v) => v,
None => return inverted_colors,
};
match query_value {
InvertedColors::None => !inverted_colors,
InvertedColors::Inverted => inverted_colors,
}
}
#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
#[repr(u8)]
enum OverflowBlock {
@ -669,7 +698,7 @@ macro_rules! bool_pref_feature {
/// to support new types in these entries and (2) ensuring that either
/// nsPresContext::MediaFeatureValuesChanged is called when the value that
/// would be returned by the evaluator function could change.
pub static MEDIA_FEATURES: [QueryFeatureDescription; 66] = [
pub static MEDIA_FEATURES: [QueryFeatureDescription; 67] = [
feature!(
atom!("width"),
AllowsRanges::Yes,
@ -816,6 +845,12 @@ pub static MEDIA_FEATURES: [QueryFeatureDescription; 66] = [
keyword_evaluator!(eval_forced_colors, ForcedColors),
FeatureFlags::empty(),
),
feature!(
atom!("inverted-colors"),
AllowsRanges::No,
keyword_evaluator!(eval_inverted_colors, InvertedColors),
FeatureFlags::empty(),
),
feature!(
atom!("overflow-block"),
AllowsRanges::No,

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

@ -329,6 +329,13 @@ fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
return !context.in_ua_or_chrome_sheet() &&
!static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
}
// inverted-colors is always enabled in the ua and chrome. On
// the web it is hidden behind a preferenc.
if *feature == atom!("inverted-colors") {
return !context.in_ua_or_chrome_sheet() &&
!static_prefs::pref!("layout.css.inverted-colors.enabled");
}
}
false
}

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

@ -0,0 +1,2 @@
[inverted-colors.html]
prefs: [layout.css.inverted-colors.enabled:true]

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

@ -0,0 +1,29 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/mediaqueries-5/#inverted" />
<script type="text/javascript" src="/resources/testharness.js"></script>
<script type="text/javascript" src="/resources/testharnessreport.js"></script>
<script type="text/javascript" src="resources/matchmedia-utils.js"></script>
<script>
query_should_be_known("(inverted-colors)");
query_should_be_known("(inverted-colors: none)");
query_should_be_known("(inverted-colors: inverted)");
query_should_be_unknown("(inverted-colors: 0)");
query_should_be_unknown("(inverted-colors: no-preference)");
query_should_be_unknown("(inverted-colors: 10px)");
query_should_be_unknown("(inverted-colors: none inverted)");
query_should_be_unknown("(inverted-colors: none/inverted)");
test(() => {
// https://drafts.csswg.org/mediaqueries-5/#boolean-context
let booleanContext = window.matchMedia("(inverted-colors)");
let none = window.matchMedia("(inverted-colors: none)");
assert_equals(booleanContext.matches, !none.matches);
}, "Check that none evaluates to false in the boolean context");
test(() => {
let invalid = window.matchMedia("(inverted-colors: 10px)");
assert_equals(invalid.matches, false);
}, "Check that invalid evaluates to false");
</script>

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

@ -1,4 +0,0 @@
<!DOCTYPE html>
<noscript>
Script is disabled
</noscript>

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

@ -1,13 +0,0 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/mediaqueries/#scripting">
<link rel="match" href="scripting-print-noscript-ref.html">
<style>
@media (scripting) {
#noscript {
display: none;
}
}
</style>
<div id="noscript">
Script is disabled
</div>

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

@ -1,4 +0,0 @@
<!DOCTYPE html>
<div>
Script is enabled
</div>

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

@ -1,17 +0,0 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/mediaqueries/#scripting">
<link rel="match" href="scripting-print-script-ref.html">
<style>
#script {
display: none;
}
@media (scripting) {
#script {
display: block;
}
}
</style>
<div id="script">
Script is enabled
</div>

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

@ -1,26 +0,0 @@
<!DOCTYPE html>
<link rel="help" href="https://drafts.csswg.org/mediaqueries-5/#scripting">
<script type="text/javascript" src="/resources/testharness.js"></script>
<script type="text/javascript" src="/resources/testharnessreport.js"></script>
<script type="text/javascript" src="resources/matchmedia-utils.js"></script>
<script>
query_should_be_known("(scripting)");
query_should_be_known("(scripting: enabled)");
query_should_be_known("(scripting: initial-only)");
query_should_be_known("(scripting: none)");
query_should_be_unknown("(scripting: 0)");
query_should_be_unknown("(scripting: 10px)");
query_should_be_unknown("(scripting: invalid)");
test(() => {
let match_enabled = window.matchMedia("(scripting: enabled)");
assert_true(match_enabled.matches);
}, "Check that scripting currently matches 'enabled'");
test(() => {
let booleanContext = window.matchMedia("(scripting)");
assert_true(booleanContext.matches);
}, "Check that scripting currently evaluates to true in the boolean context");
</script>

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

@ -302,6 +302,14 @@ class LookAndFeel {
*/
PrefersReducedTransparency,
/**
* Corresponding to inverted-colors.
* https://drafts.csswg.org/mediaqueries-5/#inverted
* 0: none
* 1: inverted
*/
InvertedColors,
/**
* Corresponding to PointerCapabilities in ServoTypes.h
* 0: None

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

@ -350,6 +350,10 @@ nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
aResult = 0;
break;
case IntID::InvertedColors:
aResult = java::GeckoSystemStateListener::IsInvertedColors();
break;
case IntID::PrimaryPointerCapabilities:
aResult = java::GeckoAppShell::GetAllPointerCapabilities();

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

@ -488,6 +488,9 @@ nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
case IntID::PrefersReducedTransparency:
aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldReduceTransparency;
break;
case IntID::InvertedColors:
aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldInvertColors;
break;
case IntID::UseAccessibilityTheme:
aResult = NSWorkspace.sharedWorkspace.accessibilityDisplayShouldIncreaseContrast;
break;

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

@ -952,6 +952,10 @@ nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
EnsureInit();
aResult = mSystemTheme.mHighContrast;
break;
case IntID::InvertedColors:
// No GTK API for checking if inverted colors is enabled
aResult = 0;
break;
case IntID::TitlebarRadius: {
EnsureInit();
aResult = EffectiveTheme().mTitlebarRadius;

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

@ -168,6 +168,9 @@ nsresult HeadlessLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
case IntID::PrefersReducedTransparency:
aResult = 0;
break;
case IntID::InvertedColors:
aResult = 0;
break;
case IntID::PrimaryPointerCapabilities:
aResult = 0;
break;

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

@ -182,6 +182,7 @@ static const char sIntPrefs[][45] = {
"ui.systemUsesDarkTheme",
"ui.prefersReducedMotion",
"ui.prefersReducedTransparency",
"ui.invertedColors",
"ui.primaryPointerCapabilities",
"ui.allPointerCapabilities",
"ui.systemScrollbarSize",

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

@ -70,6 +70,42 @@ static nsresult SystemWantsDarkTheme(int32_t& darkThemeEnabled) {
return rv;
}
static int32_t SystemColorFilter() {
nsresult rv = NS_OK;
nsCOMPtr<nsIWindowsRegKey> colorFilteringKey =
do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv);
if (NS_WARN_IF(NS_FAILED(rv))) {
return 0;
}
rv = colorFilteringKey->Open(
nsIWindowsRegKey::ROOT_KEY_CURRENT_USER,
u"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Accessibility\\ATConfig\\colorfiltering"_ns,
nsIWindowsRegKey::ACCESS_QUERY_VALUE);
if (NS_FAILED(rv)) {
return 0;
}
// The Active value is set to 1 when the "Turn on color filters" setting
// in the Color filters section of Windows' Ease of Access settings is turned
// on. If it is disabled (Active == 0 or does not exist), do not report having
// a color filter.
uint32_t active;
rv = colorFilteringKey->ReadIntValue(u"Active"_ns, &active);
if (NS_FAILED(rv) || active == 0) {
return 0;
}
// The FilterType value is set to whichever filter is enabled.
uint32_t filterType;
rv = colorFilteringKey->ReadIntValue(u"FilterType"_ns, &filterType);
if (NS_SUCCEEDED(rv)) {
return filterType;
}
return 0;
}
nsLookAndFeel::nsLookAndFeel() {
mozilla::Telemetry::Accumulate(mozilla::Telemetry::TOUCH_ENABLED_DEVICE,
WinUtils::IsTouchDeviceSupportPresent());
@ -614,6 +650,15 @@ nsresult nsLookAndFeel::NativeGetInt(IntID aID, int32_t& aResult) {
aResult = !WindowsUIUtils::ComputeTransparencyEffects();
break;
}
case IntID::InvertedColors: {
int32_t colorFilter = SystemColorFilter();
// Color filter values
// 1: Inverted
// 2: Grayscale inverted
aResult = colorFilter == 1 || colorFilter == 2 ? 1 : 0;
break;
}
case IntID::PrimaryPointerCapabilities: {
aResult = static_cast<int32_t>(
widget::WinUtils::GetPrimaryPointerCapabilities());

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

@ -464,6 +464,7 @@ STATIC_ATOMS = [
Atom("_for", "for"),
Atom("forEach", "for-each"),
Atom("forcedColors", "forced-colors"),
Atom("invertedColors", "inverted-colors"),
Atom("forceOwnRefreshDriver", "forceOwnRefreshDriver"),
Atom("form", "form"),
Atom("formaction", "formaction"),