Skip to content

Commit

Permalink
Auto merge of #77809 - nasso:master, r=jyn514,guillaumegomez
Browse files Browse the repository at this point in the history
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 :).
  • Loading branch information
bors committed Oct 16, 2020
2 parents 95b4a4f + 59f9cf2 commit 6999ff3
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 42 deletions.
102 changes: 85 additions & 17 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,8 @@ impl FormatRenderer for Context {
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())?;
Expand Down Expand Up @@ -811,6 +812,7 @@ themePicker.onblur = handleThemeButtonsBlur;
but.textContent = item;
but.onclick = function(el) {{
switchTheme(currentTheme, mainTheme, item, true);
useSystemTheme(false);
}};
but.onblur = handleThemeButtonsBlur;
themes.appendChild(but);
Expand Down Expand Up @@ -1344,22 +1346,35 @@ impl AllTypes {

#[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='{}' {}>\
Expand All @@ -1368,16 +1383,38 @@ impl Setting {
<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 }
}
}

Expand All @@ -1390,9 +1427,39 @@ impl<T: Into<Setting>> From<(&'static str, Vec<T>)> for 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![
Expand All @@ -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 {
Expand Down
33 changes: 32 additions & 1 deletion src/librustdoc/html/static/settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
}

.setting-line > div {
max-width: calc(100% - 74px);
display: inline-block;
vertical-align: top;
font-size: 17px;
Expand All @@ -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;
Expand Down
58 changes: 42 additions & 16 deletions src/librustdoc/html/static/settings.js
Original file line number Diff line number Diff line change
@@ -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;
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);
};
}
}
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";

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);
};
}
}

Expand Down
89 changes: 81 additions & 8 deletions src/librustdoc/html/static/storage.js
Original file line number Diff line number Diff line change
@@ -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 = [];

Expand Down Expand Up @@ -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
);
}

0 comments on commit 6999ff3

Please sign in to comment.