Add dark mode settings

Closes #5394
See #2377
This commit is contained in:
Florian Bruhin 2020-05-27 15:02:44 +02:00
parent 70625c3f87
commit 85aee23639
6 changed files with 534 additions and 2 deletions

View File

@ -41,6 +41,9 @@ Added
- New `colors.contextmenu.disabled.{fg,bg}` settings to customize colors for
disabled items in the context menu.
- New line selection mode (`:toggle-selection --line`), bound to `Shift-V` in caret mode.
- New `colors.webpage.darkmode.*` settings to control Chromium's dark mode.
Note that those settings only work with QtWebEngine on Qt >= 5.14 and require
a restart of qutebrowser.
Changed
~~~~~~~

View File

@ -111,6 +111,15 @@
|<<colors.tabs.selected.odd.bg,colors.tabs.selected.odd.bg>>|Background color of selected odd tabs.
|<<colors.tabs.selected.odd.fg,colors.tabs.selected.odd.fg>>|Foreground color of selected odd tabs.
|<<colors.webpage.bg,colors.webpage.bg>>|Background color for webpages if unset (or empty to use the theme's color).
|<<colors.webpage.darkmode.algorithm,colors.webpage.darkmode.algorithm>>|Which algorithm to use for modifying how colors are rendered with darkmode.
|<<colors.webpage.darkmode.contrast,colors.webpage.darkmode.contrast>>|Contrast for dark mode.
|<<colors.webpage.darkmode.enabled,colors.webpage.darkmode.enabled>>|Render all web contents using a dark theme.
|<<colors.webpage.darkmode.grayscale.all,colors.webpage.darkmode.grayscale.all>>|Render all colors as grayscale.
|<<colors.webpage.darkmode.grayscale.images,colors.webpage.darkmode.grayscale.images>>|Desaturation factor for images in dark mode.
|<<colors.webpage.darkmode.policy.images,colors.webpage.darkmode.policy.images>>|Which images to apply dark mode to.
|<<colors.webpage.darkmode.policy.page,colors.webpage.darkmode.policy.page>>|Which pages to apply dark mode to.
|<<colors.webpage.darkmode.threshold.background,colors.webpage.darkmode.threshold.background>>|Threshold for inverting background elements with dark mode.
|<<colors.webpage.darkmode.threshold.text,colors.webpage.darkmode.threshold.text>>|Threshold for inverting text with dark mode.
|<<colors.webpage.prefers_color_scheme_dark,colors.webpage.prefers_color_scheme_dark>>|Force `prefers-color-scheme: dark` colors for websites.
|<<completion.cmd_history_max_items,completion.cmd_history_max_items>>|Number of commands to save in the command history.
|<<completion.delay,completion.delay>>|Delay (in milliseconds) before updating completions after typing a character.
@ -1534,6 +1543,160 @@ Type: <<types,QtColor>>
Default: +pass:[white]+
[[colors.webpage.darkmode.algorithm]]
=== colors.webpage.darkmode.algorithm
Which algorithm to use for modifying how colors are rendered with darkmode.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +lightness-cielab+: Modify colors by converting them to CIELAB color space and inverting the L value.
* +lightness-hsl+: Modify colors by converting them to the HSL color space and inverting the lightness (i.e. the "L" in HSL).
* +brightness-rgb+: Modify colors by subtracting each of r, g, and b from their maximum value.
Default: +pass:[lightness-cielab]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.contrast]]
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Float>>
Default: +pass:[0.0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
Render all web contents using a dark theme.
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
`colors.webpage.darkmode.algorithm` accordingly.
- "With selective image inversion": Set
`colors.webpage.darkmode.policy.images` to `smart`.
- "With selective inversion of non-image elements": Set
`colors.webpage.darkmode.threshold.text` to 150 and
`colors.webpage.darkmode.threshold.background` to 205.
- "With selective inversion of everything": Combines the two variants
above.
This setting requires a restart.
Type: <<types,Bool>>
Default: +pass:[false]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.grayscale.all]]
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to `lightness-hsl` or `brightness-rgb`.
This setting requires a restart.
Type: <<types,Bool>>
Default: +pass:[false]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.grayscale.images]]
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely grayscale. Values between 0 and 1 desaturate the colors accordingly.
This setting requires a restart.
Type: <<types,Float>>
Default: +pass:[0.0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +always+: Apply dark mode filter to all images.
* +never+: Never apply dark mode filter to any images.
* +smart+: Apply dark mode based on image content.
Default: +pass:[never]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
This setting requires a restart.
Type: <<types,String>>
Valid values:
* +always+: Apply dark mode filter to all frames, regardless of content.
* +smart+: Apply dark mode filter to frames based on background color.
Default: +pass:[smart]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.threshold.background]]
=== colors.webpage.darkmode.threshold.background
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted, and below it will be left as in the original, non-dark-mode page. Set to 256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.threshold.text]]
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and above it will be left as in the original, non-dark-mode page. Set to 256 to always invert text color or to 0 to never invert text color.
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[256]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.prefers_color_scheme_dark]]
=== colors.webpage.prefers_color_scheme_dark
Force `prefers-color-scheme: dark` colors for websites.

View File

@ -2612,6 +2612,169 @@ colors.webpage.prefers_color_scheme_dark:
QtWebEngine: Qt 5.14
QtWebKit: false
## dark mode
# darkModeClassifierType is not exposed, as the icon classifier isn't actually
# implemented in Chromium:
#
# https://source.chromium.org/chromium/chromium/src/+/master:third_party/blink/renderer/platform/graphics/dark_mode_icon_classifier.cc
colors.webpage.darkmode.enabled:
default: false
type: Bool
desc: >-
Render all web contents using a dark theme.
Example configurations from Chromium's `chrome://flags`:
- "With simple HSL/CIELAB/RGB-based inversion": Set
`colors.webpage.darkmode.algorithm` accordingly.
- "With selective image inversion": Set
`colors.webpage.darkmode.policy.images` to `smart`.
- "With selective inversion of non-image elements": Set
`colors.webpage.darkmode.threshold.text` to 150 and
`colors.webpage.darkmode.threshold.background` to 205.
- "With selective inversion of everything": Combines the two variants
above.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.algorithm:
default: lightness-cielab
desc: "Which algorithm to use for modifying how colors are rendered with
darkmode."
type:
name: String
valid_values:
- lightness-cielab: Modify colors by converting them to CIELAB color
space and inverting the L value.
- lightness-hsl: Modify colors by converting them to the HSL color space
and inverting the lightness (i.e. the "L" in HSL).
- brightness-rgb: Modify colors by subtracting each of r, g, and b from
their maximum value.
# kSimpleInvertForTesting is not exposed, as it's equivalent to
# kInvertBrightness without gamma correction, and only available for
# Chromium's automated tests
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.contrast:
default: 0.0
type:
name: Float
minval: -1.0
maxval: 1.0
desc: >-
Contrast for dark mode.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.policy.images:
default: never
type:
name: String
valid_values:
- always: Apply dark mode filter to all images.
- never: Never apply dark mode filter to any images.
- smart: Apply dark mode based on image content.
desc: Which images to apply dark mode to.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.policy.page:
default: smart
type:
name: String
valid_values:
- always: Apply dark mode filter to all frames, regardless of content.
- smart: Apply dark mode filter to frames based on background color.
desc: Which pages to apply dark mode to.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.threshold.text:
default: 256
type:
name: Int
minval: 0
maxval: 256
desc: >-
Threshold for inverting text with dark mode.
Text colors with brightness below this threshold will be inverted, and
above it will be left as in the original, non-dark-mode page. Set to 256
to always invert text color or to 0 to never invert text color.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.threshold.background:
default: 0
type:
name: Int
minval: 0
maxval: 256
desc: >-
Threshold for inverting background elements with dark mode.
Background elements with brightness above this threshold will be inverted,
and below it will be left as in the original, non-dark-mode page. Set to
256 to never invert the color or to 0 to always invert it.
Note: This behavior is the opposite of
`colors.webpage.darkmode.threshold.text`!
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.grayscale.all:
default: false
type: Bool
desc: >-
Render all colors as grayscale.
This only has an effect when `colors.webpage.darkmode.algorithm` is set to
`lightness-hsl` or `brightness-rgb`.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
colors.webpage.darkmode.grayscale.images:
default: 0.0
type:
name: Float
minval: 0.0
maxval: 1.0
desc: >-
Desaturation factor for images in dark mode.
If set to 0, images are left as-is. If set to 1, images are completely
grayscale. Values between 0 and 1 desaturate the colors accordingly.
restart: true
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
# emacs: '
## fonts

View File

@ -199,6 +199,90 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]:
return argv
def _darkmode_settings() -> typing.Iterator[typing.Tuple[str, str]]:
"""Get necessary blink settings to configure dark mode for QtWebEngine."""
if not config.val.colors.webpage.darkmode.enabled:
return
# Mapping from a colors.webpage.darkmode.algorithm setting value to
# Chromium's DarkModeInversionAlgorithm enum values.
algorithms = {
# 0: kOff (not exposed)
# 1: kSimpleInvertForTesting (not exposed)
'brightness-rgb': 2, # kInvertBrightness
'lightness-hsl': 3, # kInvertLightness
'lightness-cielab': 4, # kInvertLightnessLAB
}
# Mapping from a colors.webpage.darkmode.policy.images setting value to
# Chromium's DarkModeImagePolicy enum values.
image_policies = {
'always': 0, # kFilterAll
'never': 1, # kFilterNone
'smart': 2, # kFilterSmart
}
# Mapping from a colors.webpage.darkmode.policy.page setting value to
# Chromium's DarkModePagePolicy enum values.
page_policies = {
'always': 0, # kFilterAll
'smart': 1, # kFilterByBackground
}
bools = {
True: 'true',
False: 'false',
}
_setting_description_type = typing.Tuple[
str, # qutebrowser option name
str, # darkmode setting name
# Mapping from the config value to a string (or something convertable
# to a string) which gets passed to Chromium.
typing.Optional[typing.Mapping[typing.Any, typing.Union[str, int]]],
]
if qtutils.version_check('5.15', compiled=False):
settings = [
('enabled', 'Enabled', bools),
('algorithm', 'InversionAlgorithm', algorithms),
] # type: typing.List[_setting_description_type]
mandatory_setting = 'enabled'
else:
settings = [
('algorithm', '', algorithms),
]
mandatory_setting = 'algorithm'
settings += [
('contrast', 'Contrast', None),
('policy.images', 'ImagePolicy', image_policies),
('policy.page', 'PagePolicy', page_policies),
('threshold.text', 'TextBrightnessThreshold', None),
('threshold.background', 'BackgroundBrightnessThreshold', None),
('grayscale.all', 'Grayscale', bools),
('grayscale.images', 'ImageGrayscale', None),
]
for setting, key, mapping in settings:
# To avoid blowing up the commandline length, we only pass modified
# settings to Chromium, as our defaults line up with Chromium's.
# However, we always pass enabled/algorithm to make sure dark mode gets
# actually turned on.
value = config.instance.get(
'colors.webpage.darkmode.' + setting,
fallback=setting == mandatory_setting)
if isinstance(value, usertypes.Unset):
continue
if mapping is not None:
value = mapping[value]
# FIXME: This is "forceDarkMode" starting with Chromium 83
prefix = 'darkMode'
yield prefix + key, str(value)
def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]:
"""Get the QtWebEngine arguments to use based on the config."""
if not qtutils.version_check('5.11', compiled=False):
@ -224,6 +308,11 @@ def _qtwebengine_args(namespace: argparse.Namespace) -> typing.Iterator[str]:
yield '--enable-logging'
yield '--v=1'
blink_settings = list(_darkmode_settings())
if blink_settings:
yield '--blink-settings=' + ','.join('{}={}'.format(k, v)
for k, v in blink_settings)
settings = {
'qt.force_software_rendering': {
'software-opengl': None,

View File

@ -91,7 +91,7 @@ def check_spelling():
'[Mm]ininum', '[Rr]esett?ed', '[Rr]ecieved', '[Rr]egularily',
'[Uu]nderlaying', '[Ii]nexistant', '[Ee]lipsis', 'commiting',
'existant', '[Rr]esetted', '[Ss]imilarily', '[Ii]nformations',
'[Aa]n [Uu][Rr][Ll]'}
'[Aa]n [Uu][Rr][Ll]', '[Tt]reshold'}
# Words which look better when splitted, but might need some fine tuning.
words |= {'[Ww]ebelements', '[Mm]ouseevent', '[Kk]eysequence',

View File

@ -28,7 +28,7 @@ import pytest
from qutebrowser import qutebrowser
from qutebrowser.config import (config, configexc, configfiles, configinit,
configdata, configtypes)
from qutebrowser.utils import objreg, usertypes
from qutebrowser.utils import objreg, usertypes, version
from helpers import utils
@ -705,6 +705,120 @@ class TestQtArgs:
assert ('--force-dark-mode' in args) == added
def test_blink_settings(self, config_stub, monkeypatch, parser):
monkeypatch.setattr(configinit.objects, 'backend',
usertypes.Backend.QtWebEngine)
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, exact=False, compiled=True:
True)
config_stub.val.colors.webpage.darkmode.enabled = True
parsed = parser.parse_args([])
args = configinit.qt_args(parsed)
assert '--blink-settings=darkModeEnabled=true' in args
class TestDarkMode:
@pytest.mark.parametrize('settings, new_qt, expected', [
# Disabled
({}, True, []),
({}, False, []),
# Enabled without customization
(
{'enabled': True},
True,
[('darkModeEnabled', 'true')]
),
(
{'enabled': True},
False,
[('darkMode', '4')]
),
# Algorithm
(
{'enabled': True, 'algorithm': 'brightness-rgb'},
True,
[('darkModeEnabled', 'true'),
('darkModeInversionAlgorithm', '2')],
),
(
{'enabled': True, 'algorithm': 'brightness-rgb'},
False,
[('darkMode', '2')],
),
])
@utils.qt514
def test_basics(self, config_stub, monkeypatch,
settings, new_qt, expected):
for k, v in settings.items():
config_stub.set_obj('colors.webpage.darkmode.' + k, v)
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, exact=False, compiled=True:
new_qt)
assert list(configinit._darkmode_settings()) == expected
@pytest.mark.parametrize('setting, value, exp_key, exp_val', [
('contrast', -0.5,
'darkModeContrast', '-0.5'),
('policy.page', 'smart',
'darkModePagePolicy', '1'),
('policy.images', 'smart',
'darkModeImagePolicy', '2'),
('threshold.text', 100,
'darkModeTextBrightnessThreshold', '100'),
('threshold.background', 100,
'darkModeBackgroundBrightnessThreshold', '100'),
('grayscale.all', True,
'darkModeGrayscale', 'true'),
('grayscale.images', 0.5,
'darkModeImageGrayscale', '0.5'),
])
def test_customization(self, config_stub, monkeypatch,
setting, value, exp_key, exp_val):
config_stub.val.colors.webpage.darkmode.enabled = True
config_stub.set_obj('colors.webpage.darkmode.' + setting, value)
monkeypatch.setattr(configinit.qtutils, 'version_check',
lambda version, exact=False, compiled=True:
True)
expected = [('darkModeEnabled', 'true'), (exp_key, exp_val)]
assert list(configinit._darkmode_settings()) == expected
@utils.qt514
def test_new_chromium(self):
"""Fail if we encounter an unknown Chromium version.
Dark mode in Chromium currently is undergoing various changes (as it's
relatively recent), and Qt 5.15 is supposed to update the underlying
Chromium at some point.
Make this test fail deliberately with newer Chromium versions, so that
we can test whether dark mode still works manually, and adjust if not.
"""
assert version._chromium_version() in [
'unavailable', # QtWebKit
'77.0.3865.129', # Qt 5.14
'80.0.3987.163', # Qt 5.15
]
def test_options(self, configdata_init):
"""Make sure all darkmode options have the right attributes set."""
for name, opt in configdata.DATA.items():
if not name.startswith('colors.webpage.darkmode.'):
continue
backends = {'QtWebEngine': 'Qt 5.14', 'QtWebKit': False}
assert not opt.supports_pattern, name
assert opt.restart, name
assert opt.raw_backends == backends, name
@pytest.mark.parametrize('arg, confval, used', [
# overridden by commandline arg