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:
commit
6999ff33c9
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user