From 45f67625291af6432c42b99a3b68b2b77fb8ad9c Mon Sep 17 00:00:00 2001 From: nasso <nassomails@gmail.com> Date: Sun, 11 Oct 2020 02:53:37 +0200 Subject: [PATCH] Add a setting to use the system theme --- src/librustdoc/html/render/mod.rs | 103 ++++++++++++++++++++---- src/librustdoc/html/static/settings.css | 40 ++++++++- src/librustdoc/html/static/settings.js | 55 +++++++++---- src/librustdoc/html/static/storage.js | 72 +++++++++++++++-- 4 files changed, 233 insertions(+), 37 deletions(-) diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 76334f0213d..a9d4c2cc813 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -575,7 +575,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())?; @@ -810,6 +811,7 @@ themePicker.onblur = handleThemeButtonsBlur; but.textContent = item; but.onclick = function(el) {{ switchTheme(currentTheme, mainTheme, item, true); + useSystemTheme(false); }}; but.onblur = handleThemeButtonsBlur; themes.appendChild(but); @@ -1343,12 +1345,25 @@ 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!( "<div class='setting-line'>\ @@ -1356,9 +1371,9 @@ impl Setting { <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 { ref js_data_name, ref description, ref default_value } => format!( "<div class='setting-line'>\ <label class='toggle'>\ <input type='checkbox' id='{}' {}>\ @@ -1370,13 +1385,40 @@ impl Setting { if *default_value { " checked" } else { "" }, description, ), + Setting::Select { + ref js_data_name, + ref description, + ref 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 } } } @@ -1389,9 +1431,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![ @@ -1413,16 +1485,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 { diff --git a/src/librustdoc/html/static/settings.css b/src/librustdoc/html/static/settings.css index d03cf7fcc45..7c91f6b7d18 100644 --- a/src/librustdoc/html/static/settings.css +++ b/src/librustdoc/html/static/settings.css @@ -4,7 +4,6 @@ } .setting-line > div { - max-width: calc(100% - 74px); display: inline-block; vertical-align: top; font-size: 17px; @@ -30,6 +29,45 @@ 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; diff --git a/src/librustdoc/html/static/settings.js b/src/librustdoc/html/static/settings.js index 427a74c0c87..67dc77330ee 100644 --- a/src/librustdoc/html/static/settings.js +++ b/src/librustdoc/html/static/settings.js @@ -2,8 +2,16 @@ /* global getCurrentValue, updateLocalStorage */ (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) { @@ -11,20 +19,37 @@ } 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") + }; + + if (elems.toggles && elems.toggles.length > 0) { + for (var 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 (var 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); - }; } } diff --git a/src/librustdoc/html/static/storage.js b/src/librustdoc/html/static/storage.js index 0a2fae274fa..3ee693d6eac 100644 --- a/src/librustdoc/html/static/storage.js +++ b/src/librustdoc/html/static/storage.js @@ -118,11 +118,71 @@ function switchTheme(styleElem, mainStyleElem, newTheme, saveTheme) { } } -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) { + // call the function to initialize the theme at least once! + updateSystemTheme(); +} else { + switchTheme(currentTheme, mainTheme, + getCurrentValue("rustdoc-theme") || "light", + false); +}