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