about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/librustdoc/html/markdown.rs1
-rw-r--r--src/librustdoc/html/static/css/noscript.css6
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css171
-rw-r--r--src/librustdoc/html/static/js/main.js268
-rw-r--r--src/librustdoc/html/static/js/settings.js19
-rw-r--r--src/librustdoc/html/static/js/src-script.js27
-rw-r--r--src/librustdoc/html/static/js/storage.js28
-rw-r--r--src/librustdoc/html/templates/page.html10
-rw-r--r--tests/rustdoc-gui/hide-mobile-topbar.goml20
-rw-r--r--tests/rustdoc-gui/links-color.goml4
-rw-r--r--tests/rustdoc-gui/sidebar-links-color.goml4
-rw-r--r--tests/rustdoc-gui/sidebar-resize-setting.goml23
-rw-r--r--tests/rustdoc-gui/sidebar-resize-window.goml37
-rw-r--r--tests/rustdoc-gui/sidebar-resize.goml28
-rw-r--r--tests/rustdoc-gui/sidebar-source-code.goml3
-rw-r--r--tests/rustdoc-gui/sidebar.goml4
-rw-r--r--tests/rustdoc-gui/src/theme_css/custom-theme.css2
17 files changed, 625 insertions, 30 deletions
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index dfad1ab10db..dbd78ea0dc9 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -2013,6 +2013,7 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
     map.insert("themeStyle".into(), 1);
     map.insert("settings-menu".into(), 1);
     map.insert("help-button".into(), 1);
+    map.insert("sidebar-button".into(), 1);
     map.insert("main-content".into(), 1);
     map.insert("toggle-all-docs".into(), 1);
     map.insert("all-types".into(), 1);
diff --git a/src/librustdoc/html/static/css/noscript.css b/src/librustdoc/html/static/css/noscript.css
index fe0cf6dc8cc..390e812772a 100644
--- a/src/librustdoc/html/static/css/noscript.css
+++ b/src/librustdoc/html/static/css/noscript.css
@@ -9,7 +9,7 @@ rules.
 	margin-left: 0 !important;
 }
 
-#copy-path {
+#copy-path, #sidebar-button, .sidebar-resizer {
 	/* It requires JS to work so no need to display it in this case. */
 	display: none;
 }
@@ -132,6 +132,8 @@ nav.sub {
 	--scrape-example-help-hover-color: #000;
 	--scrape-example-code-wrapper-background-start: rgba(255, 255, 255, 1);
 	--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
+	--sidebar-resizer-hover: hsl(207, 90%, 66%);
+	--sidebar-resizer-active: hsl(207, 90%, 54%);
 }
 /* End theme: light */
 
@@ -238,6 +240,8 @@ nav.sub {
 		--scrape-example-help-hover-color: #fff;
 		--scrape-example-code-wrapper-background-start: rgba(53, 53, 53, 1);
 		--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
+		--sidebar-resizer-hover: hsl(207, 30%, 54%);
+		--sidebar-resizer-active: hsl(207, 90%, 54%);
 	}
 /* End theme: dark */
 }
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 6e61969a8c1..6e673aa77c5 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -9,6 +9,11 @@
 :root {
 	--nav-sub-mobile-padding: 8px;
 	--search-typename-width: 6.75rem;
+	/* DEFAULT_SIDEBAR_WIDTH
+	   see main.js for information on these values
+	   and on the RUSTDOC_MOBILE_BREAKPOINT */
+	--desktop-sidebar-width: 200px;
+	--src-sidebar-width: 300px;
 }
 
 /* See FiraSans-LICENSE.txt for the Fira Sans license. */
@@ -383,13 +388,15 @@ img {
 
 .sidebar {
 	font-size: 0.875rem;
-	flex: 0 0 200px;
+	flex: 0 0 var(--desktop-sidebar-width);
+	width: var(--desktop-sidebar-width);
 	overflow-y: scroll;
 	overscroll-behavior: contain;
 	position: sticky;
 	height: 100vh;
 	top: 0;
 	left: 0;
+	z-index: 100;
 }
 
 .rustdoc.src .sidebar {
@@ -398,7 +405,95 @@ img {
 	overflow-x: hidden;
 	/* The sidebar is by default hidden  */
 	overflow-y: hidden;
-	z-index: 1;
+}
+
+.hide-sidebar .sidebar,
+.hide-sidebar .sidebar-resizer {
+	display: none;
+}
+
+.sidebar-resizer {
+	touch-action: none;
+	width: 9px;
+	cursor: col-resize;
+	z-index: 200;
+	position: fixed;
+	height: 100%;
+	/* make sure there's a 1px gap between the scrollbar and resize handle */
+	left: calc(var(--desktop-sidebar-width) + 1px);
+}
+
+.rustdoc.src .sidebar-resizer {
+	/* when closed, place resizer glow on top of the normal src sidebar border (no need to worry
+	   about sidebar) */
+	left: 49px;
+}
+
+.src-sidebar-expanded .rustdoc.src .sidebar-resizer {
+	/* for src sidebar, gap is already provided by 1px border on sidebar itself, so place resizer
+	   to right of it */
+	left: var(--src-sidebar-width);
+}
+
+.sidebar-resizing {
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	-ms-user-select: none;
+	user-select: none;
+}
+
+.sidebar-resizing * {
+	cursor: col-resize !important;
+}
+
+.sidebar-resizing .sidebar {
+	position: fixed;
+	z-index: 100;
+}
+.sidebar-resizing > body {
+	padding-left: var(--resizing-sidebar-width);
+}
+
+.sidebar-resizer:hover,
+.sidebar-resizer:active,
+.sidebar-resizer:focus,
+.sidebar-resizer.active {
+	width: 10px;
+	margin: 0;
+	/* when active or hovered, place resizer glow on top of the sidebar (right next to, or even
+	   on top of, the scrollbar) */
+	left: var(--desktop-sidebar-width);
+	border-left: solid 1px var(--sidebar-resizer-hover);
+}
+
+.src-sidebar-expanded .rustdoc.src .sidebar-resizer:hover,
+.src-sidebar-expanded .rustdoc.src .sidebar-resizer:active,
+.src-sidebar-expanded .rustdoc.src .sidebar-resizer:focus,
+.src-sidebar-expanded .rustdoc.src .sidebar-resizer.active {
+	/* when active or hovered, place resizer glow on top of the normal src sidebar border */
+	left: calc(var(--src-sidebar-width) - 1px);
+}
+
+@media (pointer: coarse) {
+	.sidebar-resizer {
+		/* too easy to hit the resizer while trying to hit the [-] toggle */
+		display: none !important;
+	}
+}
+
+.sidebar-resizer.active {
+	/* make the resize tool bigger when actually resizing, to avoid :hover styles on other stuff
+		while resizing */
+	padding: 0 140px;
+	width: 2px;
+	margin-left: -140px;
+	border-left: none;
+}
+.sidebar-resizer.active:before {
+	border-left: solid 2px var(--sidebar-resizer-active);
+	display: block;
+	height: 100%;
+	content: "";
 }
 
 .sidebar, .mobile-topbar, .sidebar-menu-toggle,
@@ -416,7 +511,8 @@ img {
 
 .src-sidebar-expanded .src .sidebar {
 	overflow-y: auto;
-	flex-basis: 300px;
+	flex-basis: var(--src-sidebar-width);
+	width: var(--src-sidebar-width);
 }
 
 .src-sidebar-expanded .src .sidebar > *:not(#src-sidebar-toggle) {
@@ -477,6 +573,7 @@ ul.block, .block li {
 	display: block;
 	padding: 0.25rem; /* 4px */
 	margin-left: -0.25rem;
+	margin-right: 0.25rem;
 }
 
 .sidebar h2 {
@@ -775,7 +872,7 @@ h2.section-header > .anchor {
 	text-decoration: underline;
 }
 
-.crate.block a.current { font-weight: 500; }
+.crate.block li.current a { font-weight: 500; }
 
 /*  In most contexts we use `overflow-wrap: anywhere` to ensure that we can wrap
 	as much as needed on mobile (see
@@ -1478,7 +1575,20 @@ a.tooltip:hover::after {
 	margin-left: 4px;
 	display: flex;
 }
-#settings-menu > a, #help-button > a {
+#sidebar-button {
+	display: none;
+}
+.hide-sidebar #sidebar-button {
+	display: flex;
+	margin-right: 4px;
+	position: fixed;
+	left: 6px;
+	height: 34px;
+	width: 34px;
+	background-color: var(--main-background-color);
+	z-index: 1;
+}
+#settings-menu > a, #help-button > a, #sidebar-button > a {
 	display: flex;
 	align-items: center;
 	justify-content: center;
@@ -1493,10 +1603,21 @@ a.tooltip:hover::after {
 }
 
 #settings-menu > a:hover, #settings-menu > a:focus,
-#help-button > a:hover, #help-button > a:focus {
+#help-button > a:hover, #help-button > a:focus,
+#sidebar-button > a:hover, #sidebar-button > a:focus {
 	border-color: var(--settings-button-border-focus);
 }
 
+#sidebar-button > a:before {
+	content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" \
+		fill="none" stroke="black">\
+		<rect x="1" y="1" width="20" height="20" ry="1.5" stroke-width="1.5"/>\
+		<circle cx="4.375" cy="4.375" r="1" stroke-width=".75"/>\
+		<path d="m7.6121 3v16 M5.375 7.625h-2 m2 3h-2 m2 3h-2" stroke-width="1.25"/></svg>');
+	width: 22px;
+	height: 22px;
+}
+
 #copy-path {
 	color: var(--copy-path-button-color);
 	background: var(--main-background-color);
@@ -1711,7 +1832,7 @@ However, it's not needed with smaller screen width because the doc/code block is
 /*
 WARNING: RUSTDOC_MOBILE_BREAKPOINT MEDIA QUERY
 If you update this line, then you also need to update the line with the same warning
-in src-script.js
+in src-script.js and main.js
 */
 @media (max-width: 700px) {
 	/* When linking to an item with an `id` (for instance, by clicking a link in the sidebar,
@@ -1722,6 +1843,10 @@ in src-script.js
 		scroll-margin-top: 45px;
 	}
 
+	.hide-sidebar #sidebar-button {
+		position: static;
+	}
+
 	.rustdoc {
 		/* Sidebar should overlay main content, rather than pushing main content to the right.
 		   Turn off `display: flex` on the body element. */
@@ -1750,7 +1875,8 @@ in src-script.js
 	/* Hide the logo and item name from the sidebar. Those are displayed
 	   in the mobile-topbar instead. */
 	.sidebar .logo-container,
-	.sidebar .location {
+	.sidebar .location,
+	.sidebar-resizer {
 		display: none;
 	}
 
@@ -1818,6 +1944,10 @@ in src-script.js
 		top: 0;
 	}
 
+	.hide-sidebar .mobile-topbar {
+		display: none;
+	}
+
 	.sidebar-menu-toggle {
 		width: 45px;
 		/* Rare exception to specifying font sizes in rem. Since this is acting
@@ -1827,6 +1957,10 @@ in src-script.js
 		color: var(--main-color);
 	}
 
+	.hide-sidebar .sidebar-menu-toggle {
+		display: none;
+	}
+
 	.sidebar-elems {
 		margin-top: 1em;
 	}
@@ -1870,6 +2004,17 @@ in src-script.js
 		display: none;
 	}
 
+	/* sidebar button becomes topbar button */
+	#sidebar-button > a:before {
+		content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" \
+			viewBox="0 0 22 22" fill="none" stroke="black">\
+			<rect x="1" y="1" width="20" height="20" ry="1.5" stroke-width="1.5"/>\
+			<circle cx="4.375" cy="4.375" r="1" stroke-width=".75"/>\
+			<path d="m3 7.375h16m0-3h-4" stroke-width="1.25"/></svg>');
+		width: 22px;
+		height: 22px;
+	}
+
 	/* Display an alternating layout on tablets and phones */
 	.item-table, .item-row, .item-table > li, .item-table > li > div,
 	.search-results > a, .search-results > a > div {
@@ -2274,6 +2419,8 @@ in src-script.js
 	--scrape-example-help-hover-color: #000;
 	--scrape-example-code-wrapper-background-start: rgba(255, 255, 255, 1);
 	--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
+	--sidebar-resizer-hover: hsl(207, 90%, 66%);
+	--sidebar-resizer-active: hsl(207, 90%, 54%);
 }
 /* End theme: light */
 
@@ -2379,6 +2526,8 @@ in src-script.js
 	--scrape-example-help-hover-color: #fff;
 	--scrape-example-code-wrapper-background-start: rgba(53, 53, 53, 1);
 	--scrape-example-code-wrapper-background-end: rgba(53, 53, 53, 0);
+	--sidebar-resizer-hover: hsl(207, 30%, 54%);
+	--sidebar-resizer-active: hsl(207, 90%, 54%);
 }
 /* End theme: dark */
 
@@ -2488,6 +2637,8 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 	--scrape-example-help-hover-color: #fff;
 	--scrape-example-code-wrapper-background-start: rgba(15, 20, 25, 1);
 	--scrape-example-code-wrapper-background-end: rgba(15, 20, 25, 0);
+	--sidebar-resizer-hover: hsl(34, 50%, 33%);
+	--sidebar-resizer-active: hsl(34, 100%, 66%);
 }
 
 :root[data-theme="ayu"] h1,
@@ -2519,6 +2670,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 }
 
 :root[data-theme="ayu"] .sidebar .current,
+:root[data-theme="ayu"] .sidebar .current a,
 :root[data-theme="ayu"] .sidebar a:hover,
 :root[data-theme="ayu"] #src-sidebar div.files > a:hover,
 :root[data-theme="ayu"] details.dir-entry summary:hover,
@@ -2569,7 +2721,8 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 	border-bottom: 1px solid rgba(242, 151, 24, 0.3);
 }
 
-:root[data-theme="ayu"] #settings-menu > a img {
+:root[data-theme="ayu"] #settings-menu > a img,
+:root[data-theme="ayu"] #sidebar-button > a:before {
 	filter: invert(100);
 }
 /* End theme: ayu */
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index d613997cd7f..63ab56053af 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -1,5 +1,5 @@
 // Local js definitions:
-/* global addClass, getSettingValue, hasClass, searchState */
+/* global addClass, getSettingValue, hasClass, searchState, updateLocalStorage */
 /* global onEach, onEachLazy, removeClass, getVar */
 
 "use strict";
@@ -495,7 +495,7 @@ function preLoadCss(cssUrl) {
                 }
                 const link = document.createElement("a");
                 link.href = path;
-                if (link.href === current_page) {
+                if (path === current_page) {
                     link.className = "current";
                 }
                 link.textContent = name;
@@ -857,12 +857,12 @@ function preLoadCss(cssUrl) {
         for (const crate of window.ALL_CRATES) {
             const link = document.createElement("a");
             link.href = window.rootPath + crate + "/index.html";
-            if (window.rootPath !== "./" && crate === window.currentCrate) {
-                link.className = "current";
-            }
             link.textContent = crate;
 
             const li = document.createElement("li");
+            if (window.rootPath !== "./" && crate === window.currentCrate) {
+                li.className = "current";
+            }
             li.appendChild(link);
             ul.appendChild(li);
         }
@@ -1473,6 +1473,264 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     searchState.setup();
 }());
 
+// Hide, show, and resize the sidebar
+//
+// The body class and CSS variable are initially set up in storage.js,
+// but in this file, we implement:
+//
+//   * the show sidebar button, which appears if the sidebar is hidden
+//     and, by clicking on it, will bring it back
+//   * the sidebar resize handle, which appears only on large viewports
+//     with a [fine precision pointer] to allow the user to change
+//     the size of the sidebar
+//
+// [fine precision pointer]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer
+(function() {
+    // 100 is the size of the logo
+    // don't let the sidebar get smaller than that, or it'll get squished
+    const SIDEBAR_MIN = 100;
+    // Don't let the sidebar get bigger than this
+    const SIDEBAR_MAX = 500;
+    // Don't let the body (including the gutter) get smaller than this
+    //
+    // WARNING: RUSTDOC_MOBILE_BREAKPOINT MEDIA QUERY
+    // Acceptable values for BODY_MIN are constrained by the mobile breakpoint
+    // (which is the minimum size of the whole page where the sidebar exists)
+    // and the default sidebar width:
+    //
+    //     BODY_MIN <= RUSTDOC_MOBILE_BREAKPOINT - DEFAULT_SIDEBAR_WIDTH
+    //
+    // At the time of this writing, the DEFAULT_SIDEBAR_WIDTH on src pages is
+    // 300px, and the RUSTDOC_MOBILE_BREAKPOINT is 700px, so BODY_MIN must be
+    // at most 400px. Otherwise, it would start out at the default size, then
+    // grabbing the resize handle would suddenly cause it to jank to
+    // its contraint-generated maximum.
+    const RUSTDOC_MOBILE_BREAKPOINT = 700;
+    const BODY_MIN = 400;
+    // At half-way past the minimum size, vanish the sidebar entirely
+    const SIDEBAR_VANISH_THRESHOLD = SIDEBAR_MIN / 2;
+
+    // Toolbar button to show the sidebar.
+    //
+    // On small, "mobile-sized" viewports, it's not persistent and it
+    // can only be activated by going into Settings and hiding the nav bar.
+    // On larger, "desktop-sized" viewports (though that includes many
+    // tablets), it's fixed-position, appears in the left side margin,
+    // and it can be activated by resizing the sidebar into nothing.
+    const sidebarButton = document.getElementById("sidebar-button");
+    if (sidebarButton) {
+        sidebarButton.addEventListener("click", e => {
+            removeClass(document.documentElement, "hide-sidebar");
+            updateLocalStorage("hide-sidebar", "false");
+            e.preventDefault();
+        });
+    }
+
+    // Pointer capture.
+    //
+    // Resizing is a single-pointer gesture. Any secondary pointer is ignored
+    let currentPointerId = null;
+
+    // "Desired" sidebar size.
+    //
+    // This is stashed here for window resizing. If the sidebar gets
+    // shrunk to maintain BODY_MIN, and then the user grows the window again,
+    // it gets the sidebar to restore its size.
+    let desiredSidebarSize = null;
+
+    // Sidebar resize debouncer.
+    //
+    // The sidebar itself is resized instantly, but the body HTML can be too
+    // big for that, causing reflow jank. To reduce this, we queue up a separate
+    // animation frame and throttle it.
+    let pendingSidebarResizingFrame = false;
+
+    // If this page has no sidebar at all, bail out.
+    const resizer = document.querySelector(".sidebar-resizer");
+    const sidebar = document.querySelector(".sidebar");
+    if (!resizer || !sidebar) {
+        return;
+    }
+
+    // src page and docs page use different variables, because the contents of
+    // the sidebar are so different that it's reasonable to thing the user
+    // would want them to have different sizes
+    const isSrcPage = hasClass(document.body, "src");
+
+    // Call this function to hide the sidebar when using the resize handle
+    //
+    // This function also nulls out the sidebar width CSS variable and setting,
+    // causing it to return to its default. This does not happen if you do it
+    // from settings.js, which uses a separate function. It's done here because
+    // the minimum sidebar size is rather uncomfortable, and it must pass
+    // through that size when using the shrink-to-nothing gesture.
+    function hideSidebar() {
+        if (isSrcPage) {
+            window.rustdocCloseSourceSidebar();
+            updateLocalStorage("src-sidebar-width", null);
+            // [RUSTDOCIMPL] CSS variable fast path
+            //
+            // The sidebar width variable is attached to the <html> element by
+            // storage.js, because the sidebar and resizer don't exist yet.
+            // But the resize code, in `resize()`, sets the property on the
+            // sidebar and resizer elements (which are the only elements that
+            // use the variable) to avoid recalculating CSS on the entire
+            // document on every frame.
+            //
+            // So, to clear it, we need to clear all three.
+            document.documentElement.style.removeProperty("--src-sidebar-width");
+            sidebar.style.removeProperty("--src-sidebar-width");
+            resizer.style.removeProperty("--src-sidebar-width");
+        } else {
+            addClass(document.documentElement, "hide-sidebar");
+            updateLocalStorage("hide-sidebar", "true");
+            updateLocalStorage("desktop-sidebar-width", null);
+            document.documentElement.style.removeProperty("--desktop-sidebar-width");
+            sidebar.style.removeProperty("--desktop-sidebar-width");
+            resizer.style.removeProperty("--desktop-sidebar-width");
+        }
+    }
+
+    // Call this function to show the sidebar from the resize handle.
+    // On docs pages, this can only happen if the user has grabbed the resize
+    // handle, shrunk the sidebar down to nothing, and then pulls back into
+    // the visible range without releasing it. You can, however, grab the
+    // resize handle on a source page with the sidebar closed, because it
+    // remains visible all the time on there.
+    function showSidebar() {
+        if (isSrcPage) {
+            window.rustdocShowSourceSidebar();
+        } else {
+            removeClass(document.documentElement, "hide-sidebar");
+            updateLocalStorage("hide-sidebar", "false");
+        }
+    }
+
+    /**
+     * Call this to set the correct CSS variable and setting.
+     * This function doesn't enforce size constraints. Do that before calling it!
+     *
+     * @param {number} size - CSS px width of the sidebar.
+     */
+    function changeSidebarSize(size) {
+        if (isSrcPage) {
+            updateLocalStorage("src-sidebar-width", size);
+            // [RUSTDOCIMPL] CSS variable fast path
+            //
+            // While this property is set on the HTML element at load time,
+            // because the sidebar isn't actually loaded yet,
+            // we scope this update to the sidebar to avoid hitting a slow
+            // path in WebKit.
+            sidebar.style.setProperty("--src-sidebar-width", size + "px");
+            resizer.style.setProperty("--src-sidebar-width", size + "px");
+        } else {
+            updateLocalStorage("desktop-sidebar-width", size);
+            sidebar.style.setProperty("--desktop-sidebar-width", size + "px");
+            resizer.style.setProperty("--desktop-sidebar-width", size + "px");
+        }
+    }
+
+    // Check if the sidebar is hidden. Since src pages and doc pages have
+    // different settings, this function has to check that.
+    function isSidebarHidden() {
+        return isSrcPage ?
+            !hasClass(document.documentElement, "src-sidebar-expanded") :
+            hasClass(document.documentElement, "hide-sidebar");
+    }
+
+    // Respond to the resize handle event.
+    // This function enforces size constraints, and implements the
+    // shrink-to-nothing gesture based on thresholds defined above.
+    function resize(e) {
+        if (currentPointerId === null || currentPointerId !== e.pointerId) {
+            return;
+        }
+        e.preventDefault();
+        const pos = e.clientX - sidebar.offsetLeft - 3;
+        if (pos < SIDEBAR_VANISH_THRESHOLD) {
+            hideSidebar();
+        } else if (pos >= SIDEBAR_MIN) {
+            if (isSidebarHidden()) {
+                showSidebar();
+            }
+            // don't let the sidebar get wider than SIDEBAR_MAX, or the body narrower
+            // than BODY_MIN
+            const constrainedPos = Math.min(pos, window.innerWidth - BODY_MIN, SIDEBAR_MAX);
+            changeSidebarSize(constrainedPos);
+            desiredSidebarSize = constrainedPos;
+            if (pendingSidebarResizingFrame !== false) {
+                clearTimeout(pendingSidebarResizingFrame);
+            }
+            pendingSidebarResizingFrame = setTimeout(() => {
+                if (currentPointerId === null || pendingSidebarResizingFrame === false) {
+                    return;
+                }
+                pendingSidebarResizingFrame = false;
+                document.documentElement.style.setProperty(
+                    "--resizing-sidebar-width",
+                    desiredSidebarSize + "px"
+                );
+            }, 100);
+        }
+    }
+    // Respond to the window resize event.
+    window.addEventListener("resize", () => {
+        if (window.innerWidth < RUSTDOC_MOBILE_BREAKPOINT) {
+            return;
+        }
+        stopResize();
+        if (desiredSidebarSize >= (window.innerWidth - BODY_MIN)) {
+            changeSidebarSize(window.innerWidth - BODY_MIN);
+        } else if (desiredSidebarSize !== null && desiredSidebarSize > SIDEBAR_MIN) {
+            changeSidebarSize(desiredSidebarSize);
+        }
+    });
+    function stopResize(e) {
+        if (currentPointerId === null) {
+            return;
+        }
+        if (e) {
+            e.preventDefault();
+        }
+        desiredSidebarSize = sidebar.getBoundingClientRect().width;
+        removeClass(resizer, "active");
+        window.removeEventListener("pointermove", resize, false);
+        window.removeEventListener("pointerup", stopResize, false);
+        removeClass(document.documentElement, "sidebar-resizing");
+        document.documentElement.style.removeProperty( "--resizing-sidebar-width");
+        if (resizer.releasePointerCapture) {
+            resizer.releasePointerCapture(currentPointerId);
+            currentPointerId = null;
+        }
+    }
+    function initResize(e) {
+        if (currentPointerId !== null || e.altKey || e.ctrlKey || e.metaKey || e.button !== 0) {
+            return;
+        }
+        if (resizer.setPointerCapture) {
+            resizer.setPointerCapture(e.pointerId);
+            if (!resizer.hasPointerCapture(e.pointerId)) {
+                // unable to capture pointer; something else has it
+                // on iOS, this usually means you long-clicked a link instead
+                resizer.releasePointerCapture(e.pointerId);
+                return;
+            }
+            currentPointerId = e.pointerId;
+        }
+        e.preventDefault();
+        window.addEventListener("pointermove", resize, false);
+        window.addEventListener("pointercancel", stopResize, false);
+        window.addEventListener("pointerup", stopResize, false);
+        addClass(resizer, "active");
+        addClass(document.documentElement, "sidebar-resizing");
+        const pos = e.clientX - sidebar.offsetLeft - 3;
+        document.documentElement.style.setProperty( "--resizing-sidebar-width", pos + "px");
+        desiredSidebarSize = null;
+    }
+    resizer.addEventListener("pointerdown", initResize, false);
+}());
+
+// This section handles the copy button that appears next to the path breadcrumbs
 (function() {
     let reset_button_timeout = null;
 
diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js
index 70a2825265e..2b42fbebb80 100644
--- a/src/librustdoc/html/static/js/settings.js
+++ b/src/librustdoc/html/static/js/settings.js
@@ -29,6 +29,13 @@
                     window.rustdoc_remove_line_numbers_from_examples();
                 }
                 break;
+            case "hide-sidebar":
+                if (value === true) {
+                    addClass(document.documentElement, "hide-sidebar");
+                } else {
+                    removeClass(document.documentElement, "hide-sidebar");
+                }
+                break;
         }
     }
 
@@ -187,6 +194,11 @@
                 "default": false,
             },
             {
+                "name": "Hide persistent navigation bar",
+                "js_name": "hide-sidebar",
+                "default": false,
+            },
+            {
                 "name": "Disable keyboard shortcuts",
                 "js_name": "disable-shortcuts",
                 "default": false,
@@ -216,6 +228,13 @@
 
     function displaySettings() {
         settingsMenu.style.display = "";
+        onEachLazy(settingsMenu.querySelectorAll("input[type='checkbox']"), el => {
+            const val = getSettingValue(el.id);
+            const checked = val === "true";
+            if (checked !== el.checked && val !== null) {
+                el.checked = checked;
+            }
+        });
     }
 
     function settingsBlurHandler(event) {
diff --git a/src/librustdoc/html/static/js/src-script.js b/src/librustdoc/html/static/js/src-script.js
index 27b5cf1e2ae..bbb0527a833 100644
--- a/src/librustdoc/html/static/js/src-script.js
+++ b/src/librustdoc/html/static/js/src-script.js
@@ -71,16 +71,31 @@ function createDirEntry(elem, parent, fullPath, hasFoundFile) {
     return hasFoundFile;
 }
 
+let toggleLabel;
+
+function getToggleLabel() {
+    toggleLabel = toggleLabel || document.querySelector("#src-sidebar-toggle button");
+    return toggleLabel;
+}
+
+window.rustdocCloseSourceSidebar = () => {
+    removeClass(document.documentElement, "src-sidebar-expanded");
+    getToggleLabel().innerText = ">";
+    updateLocalStorage("source-sidebar-show", "false");
+};
+
+window.rustdocShowSourceSidebar = () => {
+    addClass(document.documentElement, "src-sidebar-expanded");
+    getToggleLabel().innerText = "<";
+    updateLocalStorage("source-sidebar-show", "true");
+};
+
 function toggleSidebar() {
     const child = this.parentNode.children[0];
     if (child.innerText === ">") {
-        addClass(document.documentElement, "src-sidebar-expanded");
-        child.innerText = "<";
-        updateLocalStorage("source-sidebar-show", "true");
+        window.rustdocShowSourceSidebar();
     } else {
-        removeClass(document.documentElement, "src-sidebar-expanded");
-        child.innerText = ">";
-        updateLocalStorage("source-sidebar-show", "false");
+        window.rustdocCloseSourceSidebar();
     }
 }
 
diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js
index 37250ba5a1f..ac9c6f377b8 100644
--- a/src/librustdoc/html/static/js/storage.js
+++ b/src/librustdoc/html/static/js/storage.js
@@ -183,11 +183,38 @@ if (getSettingValue("use-system-theme") !== "false" && window.matchMedia) {
 
 updateTheme();
 
+// Hide, show, and resize the sidebar at page load time
+//
+// This needs to be done here because this JS is render-blocking,
+// so that the sidebar doesn't "jump" after appearing on screen.
+// The user interaction to change this is set up in main.js.
 if (getSettingValue("source-sidebar-show") === "true") {
     // At this point in page load, `document.body` is not available yet.
     // Set a class on the `<html>` element instead.
     addClass(document.documentElement, "src-sidebar-expanded");
 }
+if (getSettingValue("hide-sidebar") === "true") {
+    // At this point in page load, `document.body` is not available yet.
+    // Set a class on the `<html>` element instead.
+    addClass(document.documentElement, "hide-sidebar");
+}
+function updateSidebarWidth() {
+    const desktopSidebarWidth = getSettingValue("desktop-sidebar-width");
+    if (desktopSidebarWidth && desktopSidebarWidth !== "null") {
+        document.documentElement.style.setProperty(
+            "--desktop-sidebar-width",
+            desktopSidebarWidth + "px"
+        );
+    }
+    const srcSidebarWidth = getSettingValue("src-sidebar-width");
+    if (srcSidebarWidth && srcSidebarWidth !== "null") {
+        document.documentElement.style.setProperty(
+            "--src-sidebar-width",
+            srcSidebarWidth + "px"
+        );
+    }
+}
+updateSidebarWidth();
 
 // If we navigate away (for example to a settings page), and then use the back or
 // forward button to get back to a page, the theme may have changed in the meantime.
@@ -201,5 +228,6 @@ if (getSettingValue("source-sidebar-show") === "true") {
 window.addEventListener("pageshow", ev => {
     if (ev.persisted) {
         setTimeout(updateTheme, 0);
+        setTimeout(updateSidebarWidth, 0);
     }
 });
diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html
index 3f6147bb916..60ca5660c02 100644
--- a/src/librustdoc/html/templates/page.html
+++ b/src/librustdoc/html/templates/page.html
@@ -114,6 +114,7 @@
         {% endif %}
         {{ sidebar|safe }}
     </nav> {# #}
+    <div class="sidebar-resizer"></div>
     <main> {# #}
         {% if page.css_class != "src" %}<div class="width-limiter">{% endif %}
             <nav class="sub"> {# #}
@@ -128,6 +129,11 @@
                 {% endif %}
                 <form class="search-form"> {# #}
                     <span></span> {# This empty span is a hacky fix for Safari - See #93184 #}
+                    {% if page.css_class != "src" %}
+                    <div id="sidebar-button" tabindex="-1"> {# #}
+                        <a href="{{page.root_path|safe}}{{layout.krate|safe}}/all.html" title="show sidebar"></a> {# #}
+                    </div> {# #}
+                    {% endif %}
                     <input {#+ #}
                         class="search-input" {#+ #}
                         name="search" {#+ #}
@@ -136,8 +142,8 @@
                         spellcheck="false" {#+ #}
                         placeholder="Click or press ‘S’ to search, ‘?’ for more options…" {#+ #}
                         type="search"> {# #}
-                    <div id="help-button" title="help" tabindex="-1"> {# #}
-                        <a href="{{page.root_path|safe}}help.html">?</a> {# #}
+                    <div id="help-button" tabindex="-1"> {# #}
+                        <a href="{{page.root_path|safe}}help.html" title="help">?</a> {# #}
                     </div> {# #}
                     <div id="settings-menu" tabindex="-1"> {# #}
                         <a href="{{page.root_path|safe}}settings.html" title="settings"> {# #}
diff --git a/tests/rustdoc-gui/hide-mobile-topbar.goml b/tests/rustdoc-gui/hide-mobile-topbar.goml
new file mode 100644
index 00000000000..46eb8acfe8c
--- /dev/null
+++ b/tests/rustdoc-gui/hide-mobile-topbar.goml
@@ -0,0 +1,20 @@
+// Checks sidebar resizing stays synced with the setting
+go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
+set-window-size: (400, 600)
+
+// Verify that the "hide" option is unchecked
+click: "#settings-menu"
+wait-for: "#settings"
+assert-css: ("#settings", {"display": "block"})
+assert-property: ("#hide-sidebar", {"checked": "false"})
+assert-css: (".mobile-topbar", {"display": "flex"})
+
+// Toggle it
+click: "#hide-sidebar"
+assert-property: ("#hide-sidebar", {"checked": "true"})
+assert-css: (".mobile-topbar", {"display": "none"})
+
+// Toggle it again
+click: "#hide-sidebar"
+assert-property: ("#hide-sidebar", {"checked": "false"})
+assert-css: (".mobile-topbar", {"display": "flex"})
diff --git a/tests/rustdoc-gui/links-color.goml b/tests/rustdoc-gui/links-color.goml
index 0789d785f58..d88ebfb40d7 100644
--- a/tests/rustdoc-gui/links-color.goml
+++ b/tests/rustdoc-gui/links-color.goml
@@ -26,12 +26,12 @@ define-function: (
         assert-css: (".item-table .keyword", {"color": |keyword|}, ALL)
         // Checking sidebar elements.
         assert-css: (
-            ".sidebar-elems a:not(.current)",
+            ".sidebar-elems li:not(.current) a",
             {"color": |sidebar|, "background-color": "rgba(0, 0, 0, 0)", "font-weight": "400"},
             ALL,
         )
         assert-css: (
-            ".sidebar-elems a.current",
+            ".sidebar-elems li.current a",
             {
                 "color": |sidebar_current|,
                 "background-color": |sidebar_current_background|,
diff --git a/tests/rustdoc-gui/sidebar-links-color.goml b/tests/rustdoc-gui/sidebar-links-color.goml
index 079d582a567..774fbcac1e2 100644
--- a/tests/rustdoc-gui/sidebar-links-color.goml
+++ b/tests/rustdoc-gui/sidebar-links-color.goml
@@ -17,10 +17,10 @@ define-function: (
         reload:
         // Struct
         assert-css: (
-            ".sidebar .block.struct a:not(.current)",
+            ".sidebar .block.struct li:not(.current) a",
             {"color": |struct|, "background-color": "rgba(0, 0, 0, 0)"},
         )
-        move-cursor-to: ".sidebar .block.struct a:not(.current)"
+        move-cursor-to: ".sidebar .block.struct li:not(.current) a"
         assert-css: (
             ".sidebar .block.struct a:hover",
             {"color": |struct_hover|, "background-color": |struct_hover_background|},
diff --git a/tests/rustdoc-gui/sidebar-resize-setting.goml b/tests/rustdoc-gui/sidebar-resize-setting.goml
new file mode 100644
index 00000000000..2fdb2faa864
--- /dev/null
+++ b/tests/rustdoc-gui/sidebar-resize-setting.goml
@@ -0,0 +1,23 @@
+// Checks sidebar resizing stays synced with the setting
+go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
+assert-property: (".sidebar", {"clientWidth": "200"})
+show-text: true
+
+// Verify that the "hide" option is unchecked
+click: "#settings-menu"
+wait-for: "#settings"
+assert-css: ("#settings", {"display": "block"})
+assert-property: ("#hide-sidebar", {"checked": "false"})
+press-key: "Escape"
+wait-for-css: ("#settings", {"display": "none"})
+
+drag-and-drop: ((205, 100), (5, 100))
+assert-css: (".sidebar", {"display": "none"})
+
+// Verify that the "hide" option is checked
+focus: "#settings-menu a"
+press-key: "Enter"
+wait-for-css: ("#settings", {"display": "block"})
+assert-property: ("#hide-sidebar", {"checked": "true"})
+click: "#hide-sidebar"
+wait-for-css: (".sidebar", {"display": "block"})
diff --git a/tests/rustdoc-gui/sidebar-resize-window.goml b/tests/rustdoc-gui/sidebar-resize-window.goml
new file mode 100644
index 00000000000..fb6baafda71
--- /dev/null
+++ b/tests/rustdoc-gui/sidebar-resize-window.goml
@@ -0,0 +1,37 @@
+// Checks sidebar resizing
+go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
+set-window-size: (1280, 600)
+wait-for-property: (".sidebar", {"clientWidth": 200}, [NEAR])
+
+// resize past maximum (don't grow past 500)
+drag-and-drop: ((205, 100), (600, 100))
+wait-for-property: (".sidebar", {"clientWidth": 500}, [NEAR])
+
+// make the window small enough that the sidebar has to shrink
+set-window-size: (750, 600)
+wait-for-property: (".sidebar", {"clientWidth": 350}, [NEAR])
+
+// grow the window again to make the sidebar bigger
+set-window-size: (1280, 600)
+wait-for-property: (".sidebar", {"clientWidth": 500}, [NEAR])
+
+// make the window small enough that the sidebar has to shrink
+set-window-size: (750, 600)
+wait-for-property: (".sidebar", {"clientWidth": 350}, [NEAR])
+assert-local-storage: {"rustdoc-desktop-sidebar-width": "350"}
+set-window-size: (400, 600)
+wait-for-css: (".sidebar", {"display": "block", "left": "-1000px"})
+assert-local-storage: {"rustdoc-desktop-sidebar-width": "350"}
+
+// grow the window again to make the sidebar bigger
+set-window-size: (1280, 600)
+wait-for-property: (".sidebar", {"clientWidth": 500}, [NEAR])
+
+// shrink back down again, then reload the page
+// the "desired size" is a bit of remembered implicit state,
+// and rustdoc tries to minimize things like this
+set-window-size: (800, 600)
+wait-for-property: (".sidebar", {"clientWidth": 400}, [NEAR])
+reload:
+set-window-size: (1280, 600)
+wait-for-property: (".sidebar", {"clientWidth": 400}, [NEAR])
diff --git a/tests/rustdoc-gui/sidebar-resize.goml b/tests/rustdoc-gui/sidebar-resize.goml
new file mode 100644
index 00000000000..543d5d390c7
--- /dev/null
+++ b/tests/rustdoc-gui/sidebar-resize.goml
@@ -0,0 +1,28 @@
+// Checks sidebar resizing
+go-to: "file://" + |DOC_PATH| + "/test_docs/index.html"
+assert-property: (".sidebar", {"clientWidth": "200"})
+show-text: true
+// normal resizing
+drag-and-drop: ((205, 100), (185, 100))
+assert-property: (".sidebar", {"clientWidth": "182"})
+// resize past maximum (don't grow past 500)
+drag-and-drop: ((185, 100), (600, 100))
+assert-property: (".sidebar", {"clientWidth": "500"})
+// resize past minimum (hide sidebar)
+drag-and-drop: ((501, 100), (5, 100))
+assert-property: (".sidebar", {"clientWidth": "0"})
+assert-css: (".sidebar", {"display": "none"})
+assert-local-storage: {"rustdoc-hide-sidebar": "true"}
+set-local-storage: {"rustdoc-hide-sidebar": "false"}
+
+// Now same thing, but for source code
+go-to: "file://" + |DOC_PATH| + "/src/test_docs/lib.rs.html"
+assert-property: (".sidebar", {"clientWidth": "49"})
+drag-and-drop: ((52, 100), (185, 100))
+assert-property: (".sidebar", {"clientWidth": "181"})
+drag-and-drop: ((185, 100), (600, 100))
+assert-property: (".sidebar", {"clientWidth": "499"})
+drag-and-drop: ((500, 100), (5, 100))
+// instead of hiding the sidebar entirely, this
+// will switch to the toggle mode
+assert-property: (".sidebar", {"clientWidth": "49"})
diff --git a/tests/rustdoc-gui/sidebar-source-code.goml b/tests/rustdoc-gui/sidebar-source-code.goml
index 0d72e670cf4..9fc1409e86f 100644
--- a/tests/rustdoc-gui/sidebar-source-code.goml
+++ b/tests/rustdoc-gui/sidebar-source-code.goml
@@ -48,6 +48,7 @@ call-function: (
 
 // Next, desktop mode layout.
 set-window-size: (1100, 800)
+wait-for: "#src-sidebar-toggle"
 // We check that the sidebar isn't expanded and has the expected width.
 assert-css: ("nav.sidebar", {"width": "50px"})
 // We now click on the button to expand the sidebar.
@@ -58,7 +59,7 @@ assert-css: (".src-sidebar-expanded nav.sidebar a", {"font-size": "14px"})
 // We collapse the sidebar.
 click: (10, 10)
 // We ensure that the class has been removed.
-wait-for: "html:not(.expanded)"
+wait-for: "html:not(.src-sidebar-expanded)"
 assert: "nav.sidebar"
 
 // Checking that only the path to the current file is "open".
diff --git a/tests/rustdoc-gui/sidebar.goml b/tests/rustdoc-gui/sidebar.goml
index eff66d803d2..82b4f2e9429 100644
--- a/tests/rustdoc-gui/sidebar.goml
+++ b/tests/rustdoc-gui/sidebar.goml
@@ -57,7 +57,7 @@ assert-count: (".sidebar h2", 1)
 assert-text: ("#all-types", "All Items")
 assert-css: ("#all-types", {"color": "#356da4"})
 // We check that we have the crates list and that the "current" on is "test_docs".
-assert-text: (".sidebar-elems ul.crate > li > a.current", "test_docs")
+assert-text: (".sidebar-elems ul.crate > li.current > a", "test_docs")
 // And we're also supposed to have the list of items in the current module.
 assert-text: (".sidebar-elems section ul > li:nth-child(1)", "Re-exports")
 assert-text: (".sidebar-elems section ul > li:nth-child(2)", "Modules")
@@ -98,7 +98,7 @@ assert-property: (".sidebar", {"clientWidth": "200"})
 assert-text: (".sidebar > .sidebar-crate > h2 > a", "lib2")
 assert-count: (".sidebar .location", 0)
 // We check that we have the crates list and that the "current" on is now "lib2".
-assert-text: (".sidebar-elems ul.crate > li > a.current", "lib2")
+assert-text: (".sidebar-elems ul.crate > li.current > a", "lib2")
 // We now go to the "foobar" function page.
 assert-text: (".sidebar-elems > section ul.block > li:nth-child(1)", "Modules")
 assert-text: (".sidebar-elems > section ul.block > li:nth-child(2)", "Structs")
diff --git a/tests/rustdoc-gui/src/theme_css/custom-theme.css b/tests/rustdoc-gui/src/theme_css/custom-theme.css
index 260ef87f6ea..49227d9ea11 100644
--- a/tests/rustdoc-gui/src/theme_css/custom-theme.css
+++ b/tests/rustdoc-gui/src/theme_css/custom-theme.css
@@ -96,4 +96,6 @@
 	--scrape-example-help-hover-color: #000;
 	--scrape-example-code-wrapper-background-start: rgba(255, 255, 255, 1);
 	--scrape-example-code-wrapper-background-end: rgba(255, 255, 255, 0);
+	--sidebar-resizer-hover: hsl(207, 90%, 66%);
+	--sidebar-resizer-active: hsl(207, 90%, 54%);
 }