Skip to content

Commit 6999ff3

Browse files
committed
Auto merge of #77809 - nasso:master, r=jyn514,guillaumegomez
Add settings to rustdoc to use the system theme This PR adds new settings to `rustdoc` to use the operating system color scheme. ![click](https://user-images.githubusercontent.com/11479594/95668052-bf604e80-0b6e-11eb-8a17-473aaae510c9.gif) `rustdoc` actually had [basic support for this](https://github.com/rust-lang/rust/blob/b1af43bc63bc7417938df056f7f25d456cc11b0e/src/librustdoc/html/static/storage.js#L121), but the setting wasn't visible and couldn't be set back once the theme was explicitly set by the user. It also didn't update if the operating system theme preference changed while viewing a page. I'm using [this method](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Testing_media_queries#Receiving_query_notifications) to query and listen to changes to the `(prefers-color-scheme: dark)` media query. I kept the old method (based on `getComputedStyle`) as a fallback in case the user-agent doesn't support `window.matchMedia` (so like... [pretty much nobody](https://caniuse.com/?search=matchMedia)). Since there's now more than one official ""dark"" theme in `rustdoc` (and also to support custom/third-party themes), the preferred dark and light themes can be configured in the settings page (the defaults are just "dark" and "light"). This is also my very first "proper" PR to Rust! Please let me know if I did anything wrong :).
2 parents 95b4a4f + 59f9cf2 commit 6999ff3

File tree

4 files changed

+240
-42
lines changed

4 files changed

+240
-42
lines changed

src/librustdoc/html/render/mod.rs

+85-17
Original file line numberDiff line numberDiff line change
@@ -576,7 +576,8 @@ impl FormatRenderer for Context {
576576
settings(
577577
self.shared.static_root_path.as_deref().unwrap_or("./"),
578578
&self.shared.resource_suffix,
579-
),
579+
&self.shared.style_files,
580+
)?,
580581
&style_files,
581582
);
582583
self.shared.fs.write(&settings_file, v.as_bytes())?;
@@ -811,6 +812,7 @@ themePicker.onblur = handleThemeButtonsBlur;
811812
but.textContent = item;
812813
but.onclick = function(el) {{
813814
switchTheme(currentTheme, mainTheme, item, true);
815+
useSystemTheme(false);
814816
}};
815817
but.onblur = handleThemeButtonsBlur;
816818
themes.appendChild(but);
@@ -1344,22 +1346,35 @@ impl AllTypes {
13441346

13451347
#[derive(Debug)]
13461348
enum Setting {
1347-
Section { description: &'static str, sub_settings: Vec<Setting> },
1348-
Entry { js_data_name: &'static str, description: &'static str, default_value: bool },
1349+
Section {
1350+
description: &'static str,
1351+
sub_settings: Vec<Setting>,
1352+
},
1353+
Toggle {
1354+
js_data_name: &'static str,
1355+
description: &'static str,
1356+
default_value: bool,
1357+
},
1358+
Select {
1359+
js_data_name: &'static str,
1360+
description: &'static str,
1361+
default_value: &'static str,
1362+
options: Vec<(String, String)>,
1363+
},
13491364
}
13501365

13511366
impl Setting {
1352-
fn display(&self) -> String {
1367+
fn display(&self, root_path: &str, suffix: &str) -> String {
13531368
match *self {
1354-
Setting::Section { ref description, ref sub_settings } => format!(
1369+
Setting::Section { description, ref sub_settings } => format!(
13551370
"<div class='setting-line'>\
13561371
<div class='title'>{}</div>\
13571372
<div class='sub-settings'>{}</div>
13581373
</div>",
13591374
description,
1360-
sub_settings.iter().map(|s| s.display()).collect::<String>()
1375+
sub_settings.iter().map(|s| s.display(root_path, suffix)).collect::<String>()
13611376
),
1362-
Setting::Entry { ref js_data_name, ref description, ref default_value } => format!(
1377+
Setting::Toggle { js_data_name, description, default_value } => format!(
13631378
"<div class='setting-line'>\
13641379
<label class='toggle'>\
13651380
<input type='checkbox' id='{}' {}>\
@@ -1368,16 +1383,38 @@ impl Setting {
13681383
<div>{}</div>\
13691384
</div>",
13701385
js_data_name,
1371-
if *default_value { " checked" } else { "" },
1386+
if default_value { " checked" } else { "" },
13721387
description,
13731388
),
1389+
Setting::Select { js_data_name, description, default_value, ref options } => format!(
1390+
"<div class=\"setting-line\">\
1391+
<div>{}</div>\
1392+
<label class=\"select-wrapper\">\
1393+
<select id=\"{}\" autocomplete=\"off\">{}</select>\
1394+
<img src=\"{}down-arrow{}.svg\" alt=\"Select item\">\
1395+
</label>\
1396+
</div>",
1397+
description,
1398+
js_data_name,
1399+
options
1400+
.iter()
1401+
.map(|opt| format!(
1402+
"<option value=\"{}\" {}>{}</option>",
1403+
opt.0,
1404+
if &opt.0 == default_value { "selected" } else { "" },
1405+
opt.1,
1406+
))
1407+
.collect::<String>(),
1408+
root_path,
1409+
suffix,
1410+
),
13741411
}
13751412
}
13761413
}
13771414

13781415
impl From<(&'static str, &'static str, bool)> for Setting {
13791416
fn from(values: (&'static str, &'static str, bool)) -> Setting {
1380-
Setting::Entry { js_data_name: values.0, description: values.1, default_value: values.2 }
1417+
Setting::Toggle { js_data_name: values.0, description: values.1, default_value: values.2 }
13811418
}
13821419
}
13831420

@@ -1390,9 +1427,39 @@ impl<T: Into<Setting>> From<(&'static str, Vec<T>)> for Setting {
13901427
}
13911428
}
13921429

1393-
fn settings(root_path: &str, suffix: &str) -> String {
1430+
fn settings(root_path: &str, suffix: &str, themes: &[StylePath]) -> Result<String, Error> {
1431+
let theme_names: Vec<(String, String)> = themes
1432+
.iter()
1433+
.map(|entry| {
1434+
let theme =
1435+
try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path)
1436+
.to_string();
1437+
1438+
Ok((theme.clone(), theme))
1439+
})
1440+
.collect::<Result<_, Error>>()?;
1441+
13941442
// (id, explanation, default value)
13951443
let settings: &[Setting] = &[
1444+
(
1445+
"Theme preferences",
1446+
vec![
1447+
Setting::from(("use-system-theme", "Use system theme", true)),
1448+
Setting::Select {
1449+
js_data_name: "preferred-dark-theme",
1450+
description: "Preferred dark theme",
1451+
default_value: "dark",
1452+
options: theme_names.clone(),
1453+
},
1454+
Setting::Select {
1455+
js_data_name: "preferred-light-theme",
1456+
description: "Preferred light theme",
1457+
default_value: "light",
1458+
options: theme_names,
1459+
},
1460+
],
1461+
)
1462+
.into(),
13961463
(
13971464
"Auto-hide item declarations",
13981465
vec![
@@ -1414,16 +1481,17 @@ fn settings(root_path: &str, suffix: &str) -> String {
14141481
("line-numbers", "Show line numbers on code examples", false).into(),
14151482
("disable-shortcuts", "Disable keyboard shortcuts", false).into(),
14161483
];
1417-
format!(
1484+
1485+
Ok(format!(
14181486
"<h1 class='fqn'>\
1419-
<span class='in-band'>Rustdoc settings</span>\
1420-
</h1>\
1421-
<div class='settings'>{}</div>\
1422-
<script src='{}settings{}.js'></script>",
1423-
settings.iter().map(|s| s.display()).collect::<String>(),
1487+
<span class='in-band'>Rustdoc settings</span>\
1488+
</h1>\
1489+
<div class='settings'>{}</div>\
1490+
<script src='{}settings{}.js'></script>",
1491+
settings.iter().map(|s| s.display(root_path, suffix)).collect::<String>(),
14241492
root_path,
14251493
suffix
1426-
)
1494+
))
14271495
}
14281496

14291497
impl Context {

src/librustdoc/html/static/settings.css

+32-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
}
55

66
.setting-line > div {
7-
max-width: calc(100% - 74px);
87
display: inline-block;
98
vertical-align: top;
109
font-size: 17px;
@@ -30,6 +29,38 @@
3029
display: none;
3130
}
3231

32+
.select-wrapper {
33+
float: right;
34+
position: relative;
35+
height: 27px;
36+
min-width: 25%;
37+
}
38+
39+
.select-wrapper select {
40+
appearance: none;
41+
-moz-appearance: none;
42+
-webkit-appearance: none;
43+
background: none;
44+
border: 2px solid #ccc;
45+
padding-right: 28px;
46+
width: 100%;
47+
}
48+
49+
.select-wrapper img {
50+
pointer-events: none;
51+
position: absolute;
52+
right: 0;
53+
bottom: 0;
54+
background: #ccc;
55+
height: 100%;
56+
width: 28px;
57+
padding: 0px 4px;
58+
}
59+
60+
.select-wrapper select option {
61+
color: initial;
62+
}
63+
3364
.slider {
3465
position: absolute;
3566
cursor: pointer;

src/librustdoc/html/static/settings.js

+42-16
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,56 @@
11
// Local js definitions:
2-
/* global getCurrentValue, updateLocalStorage */
2+
/* global getCurrentValue, updateLocalStorage, updateSystemTheme */
33

44
(function () {
5-
function changeSetting(settingName, isEnabled) {
6-
updateLocalStorage('rustdoc-' + settingName, isEnabled);
5+
function changeSetting(settingName, value) {
6+
updateLocalStorage("rustdoc-" + settingName, value);
7+
8+
switch (settingName) {
9+
case "preferred-dark-theme":
10+
case "preferred-light-theme":
11+
case "use-system-theme":
12+
updateSystemTheme();
13+
break;
14+
}
715
}
816

917
function getSettingValue(settingName) {
10-
return getCurrentValue('rustdoc-' + settingName);
18+
return getCurrentValue("rustdoc-" + settingName);
1119
}
1220

1321
function setEvents() {
14-
var elems = document.getElementsByClassName("slider");
15-
if (!elems || elems.length === 0) {
16-
return;
22+
var elems = {
23+
toggles: document.getElementsByClassName("slider"),
24+
selects: document.getElementsByClassName("select-wrapper")
25+
};
26+
var i;
27+
28+
if (elems.toggles && elems.toggles.length > 0) {
29+
for (i = 0; i < elems.toggles.length; ++i) {
30+
var toggle = elems.toggles[i].previousElementSibling;
31+
var settingId = toggle.id;
32+
var settingValue = getSettingValue(settingId);
33+
if (settingValue !== null) {
34+
toggle.checked = settingValue === "true";
35+
}
36+
toggle.onchange = function() {
37+
changeSetting(this.id, this.checked);
38+
};
39+
}
1740
}
18-
for (var i = 0; i < elems.length; ++i) {
19-
var toggle = elems[i].previousElementSibling;
20-
var settingId = toggle.id;
21-
var settingValue = getSettingValue(settingId);
22-
if (settingValue !== null) {
23-
toggle.checked = settingValue === "true";
41+
42+
if (elems.selects && elems.selects.length > 0) {
43+
for (i = 0; i < elems.selects.length; ++i) {
44+
var select = elems.selects[i].getElementsByTagName("select")[0];
45+
var settingId = select.id;
46+
var settingValue = getSettingValue(settingId);
47+
if (settingValue !== null) {
48+
select.value = settingValue;
49+
}
50+
select.onchange = function() {
51+
changeSetting(this.id, this.value);
52+
};
2453
}
25-
toggle.onchange = function() {
26-
changeSetting(this.id, this.checked);
27-
};
2854
}
2955
}
3056

src/librustdoc/html/static/storage.js

+81-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// From rust:
22
/* global resourcesSuffix */
33

4+
var darkThemes = ["dark", "ayu"];
45
var currentTheme = document.getElementById("themeStyle");
56
var mainTheme = document.getElementById("mainThemeStyle");
7+
var localStoredTheme = getCurrentValue("rustdoc-theme");
68

79
var savedHref = [];
810

@@ -110,19 +112,90 @@ function switchTheme(styleElem, mainStyleElem, newTheme, saveTheme) {
110112
});
111113
if (found === true) {
112114
styleElem.href = newHref;
113-
// If this new value comes from a system setting or from the previously saved theme, no
114-
// need to save it.
115+
// If this new value comes from a system setting or from the previously
116+
// saved theme, no need to save it.
115117
if (saveTheme === true) {
116118
updateLocalStorage("rustdoc-theme", newTheme);
117119
}
118120
}
119121
}
120122

121-
function getSystemValue() {
122-
var property = getComputedStyle(document.documentElement).getPropertyValue('content');
123-
return property.replace(/[\"\']/g, "");
123+
function useSystemTheme(value) {
124+
if (value === undefined) {
125+
value = true;
126+
}
127+
128+
updateLocalStorage("rustdoc-use-system-theme", value);
129+
130+
// update the toggle if we're on the settings page
131+
var toggle = document.getElementById("use-system-theme");
132+
if (toggle && toggle instanceof HTMLInputElement) {
133+
toggle.checked = value;
134+
}
124135
}
125136

126-
switchTheme(currentTheme, mainTheme,
127-
getCurrentValue("rustdoc-theme") || getSystemValue() || "light",
128-
false);
137+
var updateSystemTheme = (function() {
138+
if (!window.matchMedia) {
139+
// fallback to the CSS computed value
140+
return function() {
141+
let cssTheme = getComputedStyle(document.documentElement)
142+
.getPropertyValue('content');
143+
144+
switchTheme(
145+
currentTheme,
146+
mainTheme,
147+
JSON.parse(cssTheme) || light,
148+
true
149+
);
150+
};
151+
}
152+
153+
// only listen to (prefers-color-scheme: dark) because light is the default
154+
var mql = window.matchMedia("(prefers-color-scheme: dark)");
155+
156+
function handlePreferenceChange(mql) {
157+
// maybe the user has disabled the setting in the meantime!
158+
if (getCurrentValue("rustdoc-use-system-theme") !== "false") {
159+
var lightTheme = getCurrentValue("rustdoc-preferred-light-theme") || "light";
160+
var darkTheme = getCurrentValue("rustdoc-preferred-dark-theme") || "dark";
161+
162+
if (mql.matches) {
163+
// prefers a dark theme
164+
switchTheme(currentTheme, mainTheme, darkTheme, true);
165+
} else {
166+
// prefers a light theme, or has no preference
167+
switchTheme(currentTheme, mainTheme, lightTheme, true);
168+
}
169+
170+
// note: we save the theme so that it doesn't suddenly change when
171+
// the user disables "use-system-theme" and reloads the page or
172+
// navigates to another page
173+
}
174+
}
175+
176+
mql.addListener(handlePreferenceChange);
177+
178+
return function() {
179+
handlePreferenceChange(mql);
180+
};
181+
})();
182+
183+
if (getCurrentValue("rustdoc-use-system-theme") !== "false" && window.matchMedia) {
184+
// update the preferred dark theme if the user is already using a dark theme
185+
// See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732
186+
if (getCurrentValue("rustdoc-use-system-theme") === null
187+
&& getCurrentValue("rustdoc-preferred-dark-theme") === null
188+
&& darkThemes.indexOf(localStoredTheme) >= 0) {
189+
updateLocalStorage("rustdoc-preferred-dark-theme", localStoredTheme);
190+
}
191+
192+
// call the function to initialize the theme at least once!
193+
updateSystemTheme();
194+
} else {
195+
switchTheme(
196+
currentTheme,
197+
mainTheme,
198+
getCurrentValue("rustdoc-theme") || "light",
199+
false
200+
);
201+
}

0 commit comments

Comments
 (0)