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](b1af43bc63/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 :).
This commit is contained in:
bors 2020-10-16 09:58:45 +00:00
commit 6999ff33c9
4 changed files with 241 additions and 43 deletions

View File

@ -576,7 +576,8 @@ fn after_krate(&mut self, krate: &clean::Crate, cache: &Cache) -> Result<(), Err
settings(
self.shared.static_root_path.as_deref().unwrap_or("./"),
&self.shared.resource_suffix,
),
&self.shared.style_files,
)?,
&style_files,
);
self.shared.fs.write(&settings_file, v.as_bytes())?;
@ -811,6 +812,7 @@ fn write_shared(
but.textContent = item;
but.onclick = function(el) {{
switchTheme(currentTheme, mainTheme, item, true);
useSystemTheme(false);
}};
but.onblur = handleThemeButtonsBlur;
themes.appendChild(but);
@ -1344,22 +1346,35 @@ fn print(self, f: &mut Buffer) {
#[derive(Debug)]
enum Setting {
Section { description: &'static str, sub_settings: Vec<Setting> },
Entry { js_data_name: &'static str, description: &'static str, default_value: bool },
Section {
description: &'static str,
sub_settings: Vec<Setting>,
},
Toggle {
js_data_name: &'static str,
description: &'static str,
default_value: bool,
},
Select {
js_data_name: &'static str,
description: &'static str,
default_value: &'static str,
options: Vec<(String, String)>,
},
}
impl Setting {
fn display(&self) -> String {
fn display(&self, root_path: &str, suffix: &str) -> String {
match *self {
Setting::Section { ref description, ref sub_settings } => format!(
Setting::Section { description, ref sub_settings } => format!(
"<div class='setting-line'>\
<div class='title'>{}</div>\
<div class='sub-settings'>{}</div>
</div>",
description,
sub_settings.iter().map(|s| s.display()).collect::<String>()
sub_settings.iter().map(|s| s.display(root_path, suffix)).collect::<String>()
),
Setting::Entry { ref js_data_name, ref description, ref default_value } => format!(
Setting::Toggle { js_data_name, description, default_value } => format!(
"<div class='setting-line'>\
<label class='toggle'>\
<input type='checkbox' id='{}' {}>\
@ -1368,16 +1383,38 @@ fn display(&self) -> String {
<div>{}</div>\
</div>",
js_data_name,
if *default_value { " checked" } else { "" },
if default_value { " checked" } else { "" },
description,
),
Setting::Select { js_data_name, description, default_value, ref options } => format!(
"<div class=\"setting-line\">\
<div>{}</div>\
<label class=\"select-wrapper\">\
<select id=\"{}\" autocomplete=\"off\">{}</select>\
<img src=\"{}down-arrow{}.svg\" alt=\"Select item\">\
</label>\
</div>",
description,
js_data_name,
options
.iter()
.map(|opt| format!(
"<option value=\"{}\" {}>{}</option>",
opt.0,
if &opt.0 == default_value { "selected" } else { "" },
opt.1,
))
.collect::<String>(),
root_path,
suffix,
),
}
}
}
impl From<(&'static str, &'static str, bool)> for Setting {
fn from(values: (&'static str, &'static str, bool)) -> Setting {
Setting::Entry { js_data_name: values.0, description: values.1, default_value: values.2 }
Setting::Toggle { js_data_name: values.0, description: values.1, default_value: values.2 }
}
}
@ -1390,9 +1427,39 @@ fn from(values: (&'static str, Vec<T>)) -> Setting {
}
}
fn settings(root_path: &str, suffix: &str) -> String {
fn settings(root_path: &str, suffix: &str, themes: &[StylePath]) -> Result<String, Error> {
let theme_names: Vec<(String, String)> = themes
.iter()
.map(|entry| {
let theme =
try_none!(try_none!(entry.path.file_stem(), &entry.path).to_str(), &entry.path)
.to_string();
Ok((theme.clone(), theme))
})
.collect::<Result<_, Error>>()?;
// (id, explanation, default value)
let settings: &[Setting] = &[
(
"Theme preferences",
vec![
Setting::from(("use-system-theme", "Use system theme", true)),
Setting::Select {
js_data_name: "preferred-dark-theme",
description: "Preferred dark theme",
default_value: "dark",
options: theme_names.clone(),
},
Setting::Select {
js_data_name: "preferred-light-theme",
description: "Preferred light theme",
default_value: "light",
options: theme_names,
},
],
)
.into(),
(
"Auto-hide item declarations",
vec![
@ -1414,16 +1481,17 @@ fn settings(root_path: &str, suffix: &str) -> String {
("line-numbers", "Show line numbers on code examples", false).into(),
("disable-shortcuts", "Disable keyboard shortcuts", false).into(),
];
format!(
Ok(format!(
"<h1 class='fqn'>\
<span class='in-band'>Rustdoc settings</span>\
</h1>\
<div class='settings'>{}</div>\
<script src='{}settings{}.js'></script>",
settings.iter().map(|s| s.display()).collect::<String>(),
<span class='in-band'>Rustdoc settings</span>\
</h1>\
<div class='settings'>{}</div>\
<script src='{}settings{}.js'></script>",
settings.iter().map(|s| s.display(root_path, suffix)).collect::<String>(),
root_path,
suffix
)
))
}
impl Context {

View File

@ -4,7 +4,6 @@
}
.setting-line > div {
max-width: calc(100% - 74px);
display: inline-block;
vertical-align: top;
font-size: 17px;
@ -30,6 +29,38 @@
display: none;
}
.select-wrapper {
float: right;
position: relative;
height: 27px;
min-width: 25%;
}
.select-wrapper select {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
background: none;
border: 2px solid #ccc;
padding-right: 28px;
width: 100%;
}
.select-wrapper img {
pointer-events: none;
position: absolute;
right: 0;
bottom: 0;
background: #ccc;
height: 100%;
width: 28px;
padding: 0px 4px;
}
.select-wrapper select option {
color: initial;
}
.slider {
position: absolute;
cursor: pointer;

View File

@ -1,30 +1,56 @@
// Local js definitions:
/* global getCurrentValue, updateLocalStorage */
/* global getCurrentValue, updateLocalStorage, updateSystemTheme */
(function () {
function changeSetting(settingName, isEnabled) {
updateLocalStorage('rustdoc-' + settingName, isEnabled);
function changeSetting(settingName, value) {
updateLocalStorage("rustdoc-" + settingName, value);
switch (settingName) {
case "preferred-dark-theme":
case "preferred-light-theme":
case "use-system-theme":
updateSystemTheme();
break;
}
}
function getSettingValue(settingName) {
return getCurrentValue('rustdoc-' + settingName);
return getCurrentValue("rustdoc-" + settingName);
}
function setEvents() {
var elems = document.getElementsByClassName("slider");
if (!elems || elems.length === 0) {
return;
}
for (var i = 0; i < elems.length; ++i) {
var toggle = elems[i].previousElementSibling;
var settingId = toggle.id;
var settingValue = getSettingValue(settingId);
if (settingValue !== null) {
toggle.checked = settingValue === "true";
var elems = {
toggles: document.getElementsByClassName("slider"),
selects: document.getElementsByClassName("select-wrapper")
};
var i;
if (elems.toggles && elems.toggles.length > 0) {
for (i = 0; i < elems.toggles.length; ++i) {
var toggle = elems.toggles[i].previousElementSibling;
var settingId = toggle.id;
var settingValue = getSettingValue(settingId);
if (settingValue !== null) {
toggle.checked = settingValue === "true";
}
toggle.onchange = function() {
changeSetting(this.id, this.checked);
};
}
}
if (elems.selects && elems.selects.length > 0) {
for (i = 0; i < elems.selects.length; ++i) {
var select = elems.selects[i].getElementsByTagName("select")[0];
var settingId = select.id;
var settingValue = getSettingValue(settingId);
if (settingValue !== null) {
select.value = settingValue;
}
select.onchange = function() {
changeSetting(this.id, this.value);
};
}
toggle.onchange = function() {
changeSetting(this.id, this.checked);
};
}
}

View File

@ -1,8 +1,10 @@
// From rust:
/* global resourcesSuffix */
var darkThemes = ["dark", "ayu"];
var currentTheme = document.getElementById("themeStyle");
var mainTheme = document.getElementById("mainThemeStyle");
var localStoredTheme = getCurrentValue("rustdoc-theme");
var savedHref = [];
@ -110,19 +112,90 @@ function switchTheme(styleElem, mainStyleElem, newTheme, saveTheme) {
});
if (found === true) {
styleElem.href = newHref;
// If this new value comes from a system setting or from the previously saved theme, no
// need to save it.
// If this new value comes from a system setting or from the previously
// saved theme, no need to save it.
if (saveTheme === true) {
updateLocalStorage("rustdoc-theme", newTheme);
}
}
}
function getSystemValue() {
var property = getComputedStyle(document.documentElement).getPropertyValue('content');
return property.replace(/[\"\']/g, "");
function useSystemTheme(value) {
if (value === undefined) {
value = true;
}
updateLocalStorage("rustdoc-use-system-theme", value);
// update the toggle if we're on the settings page
var toggle = document.getElementById("use-system-theme");
if (toggle && toggle instanceof HTMLInputElement) {
toggle.checked = value;
}
}
switchTheme(currentTheme, mainTheme,
getCurrentValue("rustdoc-theme") || getSystemValue() || "light",
false);
var updateSystemTheme = (function() {
if (!window.matchMedia) {
// fallback to the CSS computed value
return function() {
let cssTheme = getComputedStyle(document.documentElement)
.getPropertyValue('content');
switchTheme(
currentTheme,
mainTheme,
JSON.parse(cssTheme) || light,
true
);
};
}
// only listen to (prefers-color-scheme: dark) because light is the default
var mql = window.matchMedia("(prefers-color-scheme: dark)");
function handlePreferenceChange(mql) {
// maybe the user has disabled the setting in the meantime!
if (getCurrentValue("rustdoc-use-system-theme") !== "false") {
var lightTheme = getCurrentValue("rustdoc-preferred-light-theme") || "light";
var darkTheme = getCurrentValue("rustdoc-preferred-dark-theme") || "dark";
if (mql.matches) {
// prefers a dark theme
switchTheme(currentTheme, mainTheme, darkTheme, true);
} else {
// prefers a light theme, or has no preference
switchTheme(currentTheme, mainTheme, lightTheme, true);
}
// note: we save the theme so that it doesn't suddenly change when
// the user disables "use-system-theme" and reloads the page or
// navigates to another page
}
}
mql.addListener(handlePreferenceChange);
return function() {
handlePreferenceChange(mql);
};
})();
if (getCurrentValue("rustdoc-use-system-theme") !== "false" && window.matchMedia) {
// update the preferred dark theme if the user is already using a dark theme
// See https://github.com/rust-lang/rust/pull/77809#issuecomment-707875732
if (getCurrentValue("rustdoc-use-system-theme") === null
&& getCurrentValue("rustdoc-preferred-dark-theme") === null
&& darkThemes.indexOf(localStoredTheme) >= 0) {
updateLocalStorage("rustdoc-preferred-dark-theme", localStoredTheme);
}
// call the function to initialize the theme at least once!
updateSystemTheme();
} else {
switchTheme(
currentTheme,
mainTheme,
getCurrentValue("rustdoc-theme") || "light",
false
);
}