about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/librustdoc/html/sources.rs35
-rw-r--r--src/librustdoc/html/static/css/noscript.css5
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css221
-rw-r--r--src/librustdoc/html/static/js/main.js122
-rw-r--r--src/librustdoc/html/static/js/search.js6
-rw-r--r--src/librustdoc/html/static/js/settings.js19
-rw-r--r--src/librustdoc/html/static/js/storage.js29
-rw-r--r--src/librustdoc/html/templates/print_item.html13
-rw-r--r--src/librustdoc/html/templates/source.html13
-rw-r--r--src/librustdoc/lib.rs1
-rw-r--r--src/tools/compiletest/src/runtest.rs2
-rw-r--r--src/tools/html-checker/main.rs2
12 files changed, 313 insertions, 155 deletions
diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs
index 551bb56685c..bbd427bbdd2 100644
--- a/src/librustdoc/html/sources.rs
+++ b/src/librustdoc/html/sources.rs
@@ -26,8 +26,11 @@ pub(crate) fn render(cx: &mut Context<'_>, krate: &clean::Crate) -> Result<(), E
 
     let dst = cx.dst.join("src").join(krate.name(cx.tcx()).as_str());
     cx.shared.ensure_dir(&dst)?;
+    let crate_name = krate.name(cx.tcx());
+    let crate_name = crate_name.as_str();
 
-    let mut collector = SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default() };
+    let mut collector =
+        SourceCollector { dst, cx, emitted_local_sources: FxHashSet::default(), crate_name };
     collector.visit_crate(krate);
     Ok(())
 }
@@ -115,6 +118,8 @@ struct SourceCollector<'a, 'tcx> {
     /// Root destination to place all HTML output into
     dst: PathBuf,
     emitted_local_sources: FxHashSet<PathBuf>,
+
+    crate_name: &'a str,
 }
 
 impl DocVisitor for SourceCollector<'_, '_> {
@@ -210,6 +215,9 @@ impl SourceCollector<'_, '_> {
             },
         );
 
+        let src_fname = p.file_name().expect("source has no filename").to_os_string();
+        let mut fname = src_fname.clone();
+
         let root_path = PathBuf::from("../../").join(root_path.into_inner());
         let mut root_path = root_path.to_string_lossy();
         if let Some(c) = root_path.as_bytes().last()
@@ -217,12 +225,12 @@ impl SourceCollector<'_, '_> {
         {
             root_path += "/";
         }
+        let mut file_path = Path::new(&self.crate_name).join(&*cur.borrow());
+        file_path.push(&fname);
+        fname.push(".html");
         let mut cur = self.dst.join(cur.into_inner());
         shared.ensure_dir(&cur)?;
 
-        let src_fname = p.file_name().expect("source has no filename").to_os_string();
-        let mut fname = src_fname.clone();
-        fname.push(".html");
         cur.push(&fname);
 
         let title = format!("{} - source", src_fname.to_string_lossy());
@@ -250,7 +258,7 @@ impl SourceCollector<'_, '_> {
                     cx,
                     &root_path,
                     highlight::DecorationInfo::default(),
-                    SourceContext::Standalone,
+                    SourceContext::Standalone { file_path },
                 )
             },
             &shared.style_files,
@@ -313,10 +321,11 @@ struct ScrapedSource<'a, Code: std::fmt::Display> {
 struct Source<Code: std::fmt::Display> {
     lines: RangeInclusive<usize>,
     code_html: Code,
+    file_path: Option<(String, String)>,
 }
 
 pub(crate) enum SourceContext<'a> {
-    Standalone,
+    Standalone { file_path: PathBuf },
     Embedded(ScrapedInfo<'a>),
 }
 
@@ -345,9 +354,19 @@ pub(crate) fn print_src(
     });
     let lines = s.lines().count();
     match source_context {
-        SourceContext::Standalone => {
-            Source { lines: (1..=lines), code_html: code }.render_into(&mut writer).unwrap()
+        SourceContext::Standalone { file_path } => Source {
+            lines: (1..=lines),
+            code_html: code,
+            file_path: if let Some(file_name) = file_path.file_name()
+                && let Some(file_path) = file_path.parent()
+            {
+                Some((file_path.display().to_string(), file_name.display().to_string()))
+            } else {
+                None
+            },
         }
+        .render_into(&mut writer)
+        .unwrap(),
         SourceContext::Embedded(info) => {
             let lines = (1 + info.offset)..=(lines + info.offset);
             ScrapedSource { info, lines, code_html: code }.render_into(&mut writer).unwrap();
diff --git a/src/librustdoc/html/static/css/noscript.css b/src/librustdoc/html/static/css/noscript.css
index e62b16267f1..477a79d63e9 100644
--- a/src/librustdoc/html/static/css/noscript.css
+++ b/src/librustdoc/html/static/css/noscript.css
@@ -61,6 +61,8 @@ nav.sub {
 	--copy-path-img-hover-filter: invert(35%);
 	--code-example-button-color: #7f7f7f;
 	--code-example-button-hover-color: #595959;
+	--settings-menu-filter: invert(50%);
+	--settings-menu-hover-filter: invert(35%);
 	--codeblock-error-hover-color: rgb(255, 0, 0);
 	--codeblock-error-color: rgba(255, 0, 0, .5);
 	--codeblock-ignore-hover-color: rgb(255, 142, 0);
@@ -87,7 +89,6 @@ nav.sub {
 	--search-tab-button-not-selected-background: #e6e6e6;
 	--search-tab-button-selected-border-top-color: #0089ff;
 	--search-tab-button-selected-background: #fff;
-	--settings-menu-filter: none;
 	--stab-background-color: #fff5d6;
 	--stab-code-color: #000;
 	--code-highlight-kw-color: #8959a8;
@@ -192,6 +193,8 @@ nav.sub {
 		--search-tab-button-not-selected-background: #252525;
 		--search-tab-button-selected-border-top-color: #0089ff;
 		--search-tab-button-selected-background: #353535;
+		--settings-menu-filter: invert(50%);
+		--settings-menu-hover-filter: invert(65%);
 		--stab-background-color: #314559;
 		--stab-code-color: #e6e1cf;
 		--code-highlight-kw-color: #ab8ac1;
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 38154dee3e2..2a9b4c95883 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -165,7 +165,7 @@ h1, h2, h3, h4 {
 .main-heading h1 {
 	margin: 0;
 	padding: 0;
-	flex-grow: 1;
+	grid-area: main-heading-h1;
 	/* We use overflow-wrap: break-word for Safari, which doesn't recognize
 	   `anywhere`: https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap */
 	overflow-wrap: break-word;
@@ -174,8 +174,12 @@ h1, h2, h3, h4 {
 	overflow-wrap: anywhere;
 }
 .main-heading {
-	display: flex;
-	flex-wrap: wrap;
+	position: relative;
+	display: grid;
+	grid-template-areas:
+		"main-heading-h1 main-heading-toolbar"
+		"main-heading-sub-heading main-heading-toolbar";
+	grid-template-columns: 1fr max-content;
 	padding-bottom: 6px;
 	margin-bottom: 15px;
 }
@@ -218,9 +222,10 @@ h1, h2, h3, h4, h5, h6,
 .search-results .result-name,
 .item-name > a,
 .out-of-band,
+.sub-heading,
 span.since,
 a.src,
-#help-button > a,
+rustdoc-toolbar,
 summary.hideme,
 .scraped-example-list,
 /* This selector is for the items listed in the "all items" page. */
@@ -884,9 +889,19 @@ both the code example and the line numbers, so we need to remove the radius in t
 	overflow-x: auto;
 }
 
-.out-of-band {
+.sub-heading {
 	flex-grow: 0;
 	font-size: 1.125rem;
+	grid-area: main-heading-sub-heading;
+}
+
+.main-heading rustdoc-toolbar, .main-heading .out-of-band {
+	grid-area: main-heading-toolbar;
+}
+rustdoc-toolbar {
+	display: flex;
+	flex-direction: row;
+	flex-wrap: nowrap;
 }
 
 .docblock code, .docblock-short code,
@@ -1282,10 +1297,16 @@ so that we can apply CSS-filters to change the arrow color in themes */
 	border-color: var(--settings-input-color) !important;
 }
 
+#settings.popover {
+	--popover-arrow-offset: 118px;
+	top: 26px;
+}
+
 /* use larger max-width for help popover, but not for help.html */
 #help.popover {
 	max-width: 600px;
-	--popover-arrow-offset: 48px;
+	--popover-arrow-offset: 36px;
+	top: 26px;
 }
 
 #help dt {
@@ -1293,10 +1314,15 @@ so that we can apply CSS-filters to change the arrow color in themes */
 	clear: left;
 	margin-right: 0.5rem;
 }
+#help dd {
+	margin-bottom: 0.5rem;
+}
 #help span.top, #help span.bottom {
 	text-align: center;
 	display: block;
 	font-size: 1.125rem;
+	padding: 0 0.5rem;
+	text-wrap-style: balance;
 }
 #help span.top {
 	margin: 10px 0;
@@ -1308,10 +1334,13 @@ so that we can apply CSS-filters to change the arrow color in themes */
 	clear: both;
 	border-top: 1px solid var(--border-color);
 }
+.side-by-side {
+	display: flex;
+	margin-bottom: 20px;
+}
 .side-by-side > div {
 	width: 50%;
-	float: left;
-	padding: 0 20px 20px 17px;
+	padding: 0 20px 0 17px;
 }
 
 .item-info .stab {
@@ -1374,7 +1403,9 @@ so that we can apply CSS-filters to change the arrow color in themes */
 }
 
 .rightside:not(a),
-.out-of-band {
+.out-of-band,
+.sub-heading,
+rustdoc-toolbar {
 	color: var(--right-side-color);
 }
 
@@ -1588,8 +1619,8 @@ instead, we check that it's not a "finger" cursor.
 	display: block;
 }
 
-.out-of-band > span.since {
-	font-size: 1.25rem;
+.main-heading span.since::before {
+	content: "Since ";
 }
 
 .sub-variant h4 {
@@ -1691,6 +1722,8 @@ a.tooltip:hover::after {
 }
 
 #search-tabs {
+	grid-area: main-heading-sub-heading;
+	margin-top: 0.25rem;
 	display: flex;
 	flex-direction: row;
 	gap: 1px;
@@ -1752,9 +1785,10 @@ a.tooltip:hover::after {
 	border-bottom: 1px solid var(--border-color);
 }
 
-#settings-menu, #help-button {
+#settings-menu, #help-button, button#toggle-all-docs {
 	margin-left: var(--button-left-margin);
 	display: flex;
+	line-height: initial;
 }
 #sidebar-button {
 	display: none;
@@ -1778,33 +1812,35 @@ a.tooltip:hover::after {
 .hide-sidebar .src #sidebar-button {
 	position: static;
 }
-#settings-menu > a, #help-button > a, #sidebar-button > a {
+#settings-menu > a, #help-button > a, #sidebar-button > a, button#toggle-all-docs {
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	background-color: var(--button-background-color);
-	border: 1px solid var(--border-color);
+	flex-direction: column;
+	border: 1px solid transparent;
 	border-radius: var(--button-border-radius);
-	color: var(--settings-button-color);
-	/* Rare exception to specifying font sizes in rem. Since this is acting
-	   as an icon, it's okay to specify their sizes in pixels. */
-	font-size: 20px;
+	color: var(--main-color);
+}
+#settings-menu > a, #help-button > a, button#toggle-all-docs {
+	width: 80px;
+}
+#sidebar-button > a {
+	background-color: var(--button-background-color);
+	border-color: var(--border-color);
 	width: 33px;
 }
 
-#settings-menu > a:hover, #settings-menu > a:focus,
-#help-button > a:hover, #help-button > a:focus,
-#sidebar-button > a:hover, #sidebar-button > a:focus {
+#settings-menu > a:hover, #settings-menu > a:focus-visible,
+#help-button > a:hover, #help-button > a:focus-visible,
+#sidebar-button > a:hover, #sidebar-button > a:focus-visible,
+button#toggle-all-docs:hover, button#toggle-all-docs:focus-visible {
 	border-color: var(--settings-button-border-focus);
+	text-decoration: none;
 }
 
-#settings-menu > a {
-	line-height: 0;
-	font-size: 0;
-}
 #settings-menu > a:before {
 	/* Wheel <https://www.svgrepo.com/svg/384069/settings-cog-gear> */
-	content: url('data:image/svg+xml,<svg width="22" height="22" viewBox="0 0 12 12" \
+	content: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 12 12" \
 	enable-background="new 0 0 12 12" xmlns="http://www.w3.org/2000/svg">\
 	<path d="M10.25,6c0-0.1243286-0.0261841-0.241333-0.0366211-0.362915l1.6077881-1.5545654l\
 	-1.25-2.1650391  c0,0-1.2674561,0.3625488-2.1323853,0.6099854c-0.2034912-0.1431885-0.421875\
@@ -1817,16 +1853,59 @@ a.tooltip:hover::after {
 	-0.3701782l2.1323853,0.6099854l1.25-2.1650391L10.2133789,6.362915  C10.2238159,6.241333,\
 	10.25,6.1243286,10.25,6z M6,7.5C5.1715698,7.5,4.5,6.8284302,4.5,6S5.1715698,4.5,6,4.5S7.5\
 	,5.1715698,7.5,6  S6.8284302,7.5,6,7.5z" fill="black"/></svg>');
-	width: 22px;
-	height: 22px;
+	width: 18px;
+	height: 18px;
 	filter: var(--settings-menu-filter);
 }
 
+button#toggle-all-docs:before {
+	/* Custom arrow icon */
+	content: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 12 12" \
+	enable-background="new 0 0 12 12" xmlns="http://www.w3.org/2000/svg">\
+	<path d="M2,2l4,4l4,-4M2,6l4,4l4,-4" stroke="black" fill="none" stroke-width="2px"/></svg>');
+	width: 18px;
+	height: 18px;
+	filter: var(--settings-menu-filter);
+}
+
+#help-button > a:before {
+	/* Question mark with circle */
+	content: url('data:image/svg+xml,<svg width="18" height="18" viewBox="0 0 12 12" \
+	enable-background="new 0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="none">\
+	<circle r="5.25" cx="6" cy="6" stroke-width="1.25" stroke="black"/>\
+	<text x="4.25" y="9" style="font:8px sans-serif;font-weight:1000" fill="black">?</text></svg>');
+	width: 18px;
+	height: 18px;
+	filter: var(--settings-menu-filter);
+}
+
+button#toggle-all-docs:before,
+#help-button > a:before,
+#settings-menu > a:before {
+	filter: var(--settings-menu-filter);
+	margin: 8px;
+}
+
+@media not (pointer: coarse) {
+	button#toggle-all-docs:hover:before,
+	#help-button > a:hover:before,
+	#settings-menu > a:hover:before {
+		filter: var(--settings-menu-hover-filter);
+	}
+}
+
+rustdoc-toolbar span.label {
+	font-size: initial;
+	font-variant-caps: small-caps;
+	text-transform: lowercase;
+	flex-grow: 1;
+}
+
 #sidebar-button > a:before {
 	/* sidebar resizer image */
 	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"/>\
+		<rect x="1" y="1" width="20" height="20" ry="1.5" stroke-width="1.5" stroke="%23777"/>\
 		<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;
@@ -1941,10 +2020,10 @@ details.toggle > summary.hideme > span {
 }
 
 details.toggle > summary::before {
-	/* toggle plus */
-	background: url('data:image/svg+xml,<svg width="17" height="17" \
-shape-rendering="crispEdges" stroke="black" fill="none" xmlns="http://www.w3.org/2000/svg"><path \
-d="M5 2.5H2.5v12H5m7-12h2.5v12H12M5 8.5h7M8.5 12V8.625v0V5"/></svg>') no-repeat top left;
+	/* arrow pointing left */
+	background: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 12 12" \
+	enable-background="new 0 0 12 12" xmlns="http://www.w3.org/2000/svg">\
+	<path d="M4,2l4,4l-4,4" stroke="black" fill="none" stroke-width="1px"/></svg>');
 	content: "";
 	cursor: pointer;
 	width: 16px;
@@ -2027,10 +2106,10 @@ details.toggle[open] > summary.hideme > span {
 }
 
 details.toggle[open] > summary::before {
-	/* toggle minus */
-	background: url('data:image/svg+xml,<svg width="17" height="17" \
-shape-rendering="crispEdges" stroke="black" fill="none" xmlns="http://www.w3.org/2000/svg"><path \
-d="M5 2.5H2.5v12H5m7-12h2.5v12H12M5 8.5h7"/></svg>') no-repeat top left;
+	/* arrow pointing down */
+	background: url('data:image/svg+xml,<svg width="16" height="16" viewBox="0 0 12 12" \
+	enable-background="new 0 0 12 12" xmlns="http://www.w3.org/2000/svg">\
+	<path d="M2,4l4,4l4,-4" stroke="black" fill="none" stroke-width="1px"/></svg>');
 }
 
 details.toggle[open] > summary::after {
@@ -2076,6 +2155,15 @@ However, it's not needed with smaller screen width because the doc/code block is
 	opacity: 0.75;
 }
 
+/* help button is mostly for search */
+#help-button:not(.help-open),
+#alternative-display #toggle-all-docs {
+	display: none;
+}
+#alternative-display #help-button {
+	display: flex;
+}
+
 /* Media Queries */
 
 /* Make sure all the buttons line wrap at the same time */
@@ -2083,6 +2171,12 @@ However, it's not needed with smaller screen width because the doc/code block is
 	#search-tabs .count {
 		display: block;
 	}
+	.side-by-side {
+		flex-direction: column-reverse;
+	}
+	.side-by-side > div {
+		width: auto;
+	}
 }
 
 /*
@@ -2099,6 +2193,27 @@ in src-script.js and main.js
 		scroll-margin-top: 45px;
 	}
 
+	/* We don't display this button on mobile devices. */
+	#copy-path {
+		display: none;
+	}
+
+	/* Text label takes up too much space at this size. */
+	rustdoc-toolbar span.label {
+		display: none;
+	}
+	#settings-menu > a, #help-button > a, button#toggle-all-docs {
+		width: 33px;
+	}
+	#settings.popover {
+		--popover-arrow-offset: 48px;
+		top: calc(100% - 8px);
+	}
+	#help.popover {
+		--popover-arrow-offset: 12px;
+		top: calc(100% - 8px);
+	}
+
 	.rustdoc {
 		/* Sidebar should overlay main content, rather than pushing main content to the right.
 		   Turn off `display: flex` on the body element. */
@@ -2110,20 +2225,6 @@ in src-script.js and main.js
 		padding-top: 0px;
 	}
 
-	.main-heading {
-		flex-direction: column;
-	}
-
-	.out-of-band {
-		text-align: left;
-		margin-left: initial;
-		padding: initial;
-	}
-
-	.out-of-band .since::before {
-		content: "Since ";
-	}
-
 	/* Hide the logo and item name from the sidebar. Those are displayed
 	   in the mobile-topbar instead. */
 	.sidebar .logo-container,
@@ -2227,16 +2328,11 @@ in src-script.js and main.js
 		left: -11px;
 	}
 
-	/* We don't display these buttons on mobile devices. */
-	#copy-path, #help-button {
-		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"/>\
+			<rect x="1" y="1" width="20" height="20" ry="1.5" stroke-width="1.5" stroke="%23777"/>\
 			<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;
@@ -2536,6 +2632,8 @@ by default.
 	--copy-path-img-hover-filter: invert(35%);
 	--code-example-button-color: #7f7f7f;
 	--code-example-button-hover-color: #595959;
+	--settings-menu-filter: invert(50%);
+	--settings-menu-hover-filter: invert(35%);
 	--codeblock-error-hover-color: rgb(255, 0, 0);
 	--codeblock-error-color: rgba(255, 0, 0, .5);
 	--codeblock-ignore-hover-color: rgb(255, 142, 0);
@@ -2562,7 +2660,6 @@ by default.
 	--search-tab-button-not-selected-background: #e6e6e6;
 	--search-tab-button-selected-border-top-color: #0089ff;
 	--search-tab-button-selected-background: #fff;
-	--settings-menu-filter: none;
 	--stab-background-color: #fff5d6;
 	--stab-code-color: #000;
 	--code-highlight-kw-color: #8959a8;
@@ -2666,7 +2763,8 @@ by default.
 	--search-tab-button-not-selected-background: #252525;
 	--search-tab-button-selected-border-top-color: #0089ff;
 	--search-tab-button-selected-background: #353535;
-	--settings-menu-filter: none;
+	--settings-menu-filter: invert(50%);
+	--settings-menu-hover-filter: invert(65%);
 	--stab-background-color: #314559;
 	--stab-code-color: #e6e1cf;
 	--code-highlight-kw-color: #ab8ac1;
@@ -2777,7 +2875,8 @@ Original by Dempfi (https://github.com/dempfi/ayu)
 	--search-tab-button-not-selected-background: transparent !important;
 	--search-tab-button-selected-border-top-color: none;
 	--search-tab-button-selected-background: #141920 !important;
-	--settings-menu-filter: invert(100%);
+	--settings-menu-filter: invert(70%);
+	--settings-menu-hover-filter: invert(100%);
 	--stab-background-color: #314559;
 	--stab-code-color: #e6e1cf;
 	--code-highlight-kw-color: #ff7733;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index 858753a1917..53326f0fcad 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -22,14 +22,14 @@ function hideMain() {
 }
 
 function showMain() {
-    removeClass(document.getElementById(MAIN_ID), "hidden");
-}
-
-function blurHandler(event, parentElem, hideCallback) {
-    if (!parentElem.contains(document.activeElement) &&
-        !parentElem.contains(event.relatedTarget)
-    ) {
-        hideCallback();
+    const main = document.getElementById(MAIN_ID);
+    removeClass(main, "hidden");
+    const mainHeading = main.querySelector(".main-heading");
+    if (mainHeading && searchState.rustdocToolbar) {
+        if (searchState.rustdocToolbar.parentElement) {
+            searchState.rustdocToolbar.parentElement.removeChild(searchState.rustdocToolbar);
+        }
+        mainHeading.appendChild(searchState.rustdocToolbar);
     }
 }
 
@@ -167,6 +167,14 @@ function switchDisplayedElement(elemToDisplay) {
     el.appendChild(elemToDisplay);
     hideMain();
     removeClass(el, "hidden");
+
+    const mainHeading = elemToDisplay.querySelector(".main-heading");
+    if (mainHeading && searchState.rustdocToolbar) {
+        if (searchState.rustdocToolbar.parentElement) {
+            searchState.rustdocToolbar.parentElement.removeChild(searchState.rustdocToolbar);
+        }
+        mainHeading.appendChild(searchState.rustdocToolbar);
+    }
 }
 
 function browserSupportsHistoryApi() {
@@ -194,33 +202,36 @@ function preLoadCss(cssUrl) {
         document.head.append(script);
     }
 
-    getSettingsButton().onclick = event => {
-        if (event.ctrlKey || event.altKey || event.metaKey) {
-            return;
-        }
-        window.hideAllModals(false);
-        addClass(getSettingsButton(), "rotate");
-        event.preventDefault();
-        // Sending request for the CSS and the JS files at the same time so it will
-        // hopefully be loaded when the JS will generate the settings content.
-        loadScript(getVar("static-root-path") + getVar("settings-js"));
-        // Pre-load all theme CSS files, so that switching feels seamless.
-        //
-        // When loading settings.html as a standalone page, the equivalent HTML is
-        // generated in context.rs.
-        setTimeout(() => {
-            const themes = getVar("themes").split(",");
-            for (const theme of themes) {
-                // if there are no themes, do nothing
-                // "".split(",") == [""]
-                if (theme !== "") {
-                    preLoadCss(getVar("root-path") + theme + ".css");
-                }
+    if (getSettingsButton()) {
+        getSettingsButton().onclick = event => {
+            if (event.ctrlKey || event.altKey || event.metaKey) {
+                return;
             }
-        }, 0);
-    };
+            window.hideAllModals(false);
+            addClass(getSettingsButton(), "rotate");
+            event.preventDefault();
+            // Sending request for the CSS and the JS files at the same time so it will
+            // hopefully be loaded when the JS will generate the settings content.
+            loadScript(getVar("static-root-path") + getVar("settings-js"));
+            // Pre-load all theme CSS files, so that switching feels seamless.
+            //
+            // When loading settings.html as a standalone page, the equivalent HTML is
+            // generated in context.rs.
+            setTimeout(() => {
+                const themes = getVar("themes").split(",");
+                for (const theme of themes) {
+                    // if there are no themes, do nothing
+                    // "".split(",") == [""]
+                    if (theme !== "") {
+                        preLoadCss(getVar("root-path") + theme + ".css");
+                    }
+                }
+            }, 0);
+        };
+    }
 
     window.searchState = {
+        rustdocToolbar: document.querySelector("rustdoc-toolbar"),
         loadingText: "Loading search results...",
         input: document.getElementsByClassName("search-input")[0],
         outputElement: () => {
@@ -919,8 +930,7 @@ function preLoadCss(cssUrl) {
                 e.open = true;
             }
         });
-        innerToggle.title = "collapse all docs";
-        innerToggle.children[0].innerText = "\u2212"; // "\u2212" is "−" minus sign
+        innerToggle.children[0].innerText = "Summary";
     }
 
     function collapseAllDocs() {
@@ -934,8 +944,7 @@ function preLoadCss(cssUrl) {
                 e.open = false;
             }
         });
-        innerToggle.title = "expand all docs";
-        innerToggle.children[0].innerText = "+";
+        innerToggle.children[0].innerText = "Show all";
     }
 
     function toggleAllDocs() {
@@ -1328,7 +1337,13 @@ function preLoadCss(cssUrl) {
     }
 
     function helpBlurHandler(event) {
-        blurHandler(event, getHelpButton(), window.hidePopoverMenus);
+        if (!getHelpButton().contains(document.activeElement) &&
+            !getHelpButton().contains(event.relatedTarget) &&
+            !getSettingsButton().contains(document.activeElement) &&
+            !getSettingsButton().contains(event.relatedTarget)
+        ) {
+            window.hidePopoverMenus();
+        }
     }
 
     function buildHelpMenu() {
@@ -1431,9 +1446,13 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
      * Hide all the popover menus.
      */
     window.hidePopoverMenus = () => {
-        onEachLazy(document.querySelectorAll(".search-form .popover"), elem => {
+        onEachLazy(document.querySelectorAll("rustdoc-toolbar .popover"), elem => {
             elem.style.display = "none";
         });
+        const button = getHelpButton();
+        if (button) {
+            removeClass(button, "help-open");
+        }
     };
 
     /**
@@ -1458,7 +1477,9 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
     function showHelp() {
         // Prevent `blur` events from being dispatched as a result of closing
         // other modals.
-        getHelpButton().querySelector("a").focus();
+        const button = getHelpButton();
+        addClass(button, "help-open");
+        button.querySelector("a").focus();
         const menu = getHelpMenu(true);
         if (menu.style.display === "none") {
             window.hideAllModals();
@@ -1466,28 +1487,15 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/read-documentation/search.htm
         }
     }
 
+    const helpLink = document.querySelector(`#${HELP_BUTTON_ID} > a`);
     if (isHelpPage) {
-        showHelp();
-        document.querySelector(`#${HELP_BUTTON_ID} > a`).addEventListener("click", event => {
-            // Already on the help page, make help button a no-op.
-            const target = event.target;
-            if (target.tagName !== "A" ||
-                target.parentElement.id !== HELP_BUTTON_ID ||
-                event.ctrlKey ||
-                event.altKey ||
-                event.metaKey) {
-                return;
-            }
-            event.preventDefault();
-        });
-    } else {
-        document.querySelector(`#${HELP_BUTTON_ID} > a`).addEventListener("click", event => {
+        buildHelpMenu();
+    } else if (helpLink) {
+        helpLink.addEventListener("click", event => {
             // By default, have help button open docs in a popover.
             // If user clicks with a moderator, though, use default browser behavior,
             // probably opening in a new window or tab.
-            const target = event.target;
-            if (target.tagName !== "A" ||
-                target.parentElement.id !== HELP_BUTTON_ID ||
+            if (!helpLink.contains(helpLink) ||
                 event.ctrlKey ||
                 event.altKey ||
                 event.metaKey) {
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js
index 4da0bbc787d..15b1046a27c 100644
--- a/src/librustdoc/html/static/js/search.js
+++ b/src/librustdoc/html/static/js/search.js
@@ -3605,7 +3605,8 @@ async function showResults(results, go_to_first, filterCrates) {
         crates += "</select></div>";
     }
 
-    let output = `<h1 class="search-results-title">Results${crates}</h1>`;
+    let output = `<div class="main-heading">\
+        <h1 class="search-results-title">Results${crates}</h1></div>`;
     if (results.query.error !== null) {
         const error = results.query.error;
         error.forEach((value, index) => {
@@ -3662,6 +3663,9 @@ async function showResults(results, go_to_first, filterCrates) {
     resultsElem.appendChild(ret_returned[0]);
 
     search.innerHTML = output;
+    if (searchState.rustdocToolbar) {
+        search.querySelector(".main-heading").appendChild(searchState.rustdocToolbar);
+    }
     const crateSearch = document.getElementById("crate-search");
     if (crateSearch) {
         crateSearch.addEventListener("input", updateCrate);
diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js
index c52a19ef987..183663b94fc 100644
--- a/src/librustdoc/html/static/js/settings.js
+++ b/src/librustdoc/html/static/js/settings.js
@@ -1,7 +1,7 @@
 // Local js definitions:
 /* global getSettingValue, updateLocalStorage, updateTheme */
-/* global addClass, removeClass, onEach, onEachLazy, blurHandler */
-/* global MAIN_ID, getVar, getSettingsButton */
+/* global addClass, removeClass, onEach, onEachLazy */
+/* global MAIN_ID, getVar, getSettingsButton, getHelpButton */
 
 "use strict";
 
@@ -267,15 +267,16 @@
     }
 
     function settingsBlurHandler(event) {
-        blurHandler(event, getSettingsButton(), window.hidePopoverMenus);
+        if (!getHelpButton().contains(document.activeElement) &&
+            !getHelpButton().contains(event.relatedTarget) &&
+            !getSettingsButton().contains(document.activeElement) &&
+            !getSettingsButton().contains(event.relatedTarget)
+        ) {
+            window.hidePopoverMenus();
+        }
     }
 
-    if (isSettingsPage) {
-        // We replace the existing "onclick" callback to do nothing if clicked.
-        getSettingsButton().onclick = event => {
-            event.preventDefault();
-        };
-    } else {
+    if (!isSettingsPage) {
         // We replace the existing "onclick" callback.
         const settingsButton = getSettingsButton();
         const settingsMenu = document.getElementById("settings");
diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js
index d75fb7a7fb5..344743c87ed 100644
--- a/src/librustdoc/html/static/js/storage.js
+++ b/src/librustdoc/html/static/js/storage.js
@@ -274,16 +274,29 @@ class RustdocSearchElement extends HTMLElement {
                     spellcheck="false"
                     placeholder="Type ‘S’ or ‘/’ to search, ‘?’ for more options…"
                     type="search">
-                <div id="help-button" tabindex="-1">
-                    <a href="${rootPath}help.html" title="help">?</a>
-                </div>
-                <div id="settings-menu" tabindex="-1">
-                    <a href="${rootPath}settings.html" title="settings">
-                        Settings
-                    </a>
-                </div>
             </form>
         </nav>`;
     }
 }
 window.customElements.define("rustdoc-search", RustdocSearchElement);
+class RustdocToolbarElement extends HTMLElement {
+    constructor() {
+        super();
+    }
+    connectedCallback() {
+        // Avoid replacing the children after they're already here.
+        if (this.firstElementChild) {
+            return;
+        }
+        const rootPath = getVar("root-path");
+        this.innerHTML = `
+        <div id="settings-menu" tabindex="-1">
+            <a href="${rootPath}settings.html"><span class="label">Settings</span></a>
+        </div>
+        <button id="toggle-all-docs"><span class="label">Summary</span></button>
+        <div id="help-button" tabindex="-1">
+            <a href="${rootPath}help.html"><span class="label">Help</span></a>
+        </div>`;
+    }
+}
+window.customElements.define("rustdoc-toolbar", RustdocToolbarElement);
diff --git a/src/librustdoc/html/templates/print_item.html b/src/librustdoc/html/templates/print_item.html
index 76e770453b6..1b8c8293088 100644
--- a/src/librustdoc/html/templates/print_item.html
+++ b/src/librustdoc/html/templates/print_item.html
@@ -10,17 +10,16 @@
             Copy item path {# #}
         </button> {# #}
     </h1> {# #}
-    <span class="out-of-band">
+    <rustdoc-toolbar></rustdoc-toolbar>
+    <span class="sub-heading">
         {% if !stability_since_raw.is_empty() %}
-        {{ stability_since_raw|safe +}} · {#+ #}
+        {{ stability_since_raw|safe +}}
         {% endif %}
         {% match src_href %}
             {% when Some with (href) %}
-                <a class="src" href="{{href|safe}}">source</a> · {#+ #}
+                {% if !stability_since_raw.is_empty() +%} · {%+ endif %}
+                <a class="src" href="{{href|safe}}">source</a> {#+ #}
             {% else %}
         {% endmatch %}
-        <button id="toggle-all-docs" title="collapse all docs"> {# #}
-            [<span>&#x2212;</span>] {# #}
-        </button> {# #}
-    </span> {# #}
+    </span>
 </div> {# #}
diff --git a/src/librustdoc/html/templates/source.html b/src/librustdoc/html/templates/source.html
index 60a47f1b5de..9daa0cf8ff6 100644
--- a/src/librustdoc/html/templates/source.html
+++ b/src/librustdoc/html/templates/source.html
@@ -1,4 +1,15 @@
-<div class="example-wrap">
+{% match file_path %}
+{% when Some with ((path, name)) %}
+<div class="main-heading"> {# #}
+    <h1> {# #}
+        <div class="sub-heading">{{path}}/</div>
+        {{name}}
+    </h1> {# #}
+    <rustdoc-toolbar></rustdoc-toolbar> {# #}
+</div>
+{% else %}
+{% endmatch %}
+<div class="example-wrap"> {# #}
     {# https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag#data-nosnippet-attr
        Do not show "1 2 3 4 5 ..." in web search results. #}
     <div data-nosnippet><pre class="src-line-numbers">
diff --git a/src/librustdoc/lib.rs b/src/librustdoc/lib.rs
index 6649e1721a4..5f303d96c40 100644
--- a/src/librustdoc/lib.rs
+++ b/src/librustdoc/lib.rs
@@ -10,6 +10,7 @@
 #![feature(iter_intersperse)]
 #![feature(let_chains)]
 #![feature(never_type)]
+#![feature(os_str_display)]
 #![feature(round_char_boundary)]
 #![feature(test)]
 #![feature(type_alias_impl_trait)]
diff --git a/src/tools/compiletest/src/runtest.rs b/src/tools/compiletest/src/runtest.rs
index c18f569e528..d24dd334a34 100644
--- a/src/tools/compiletest/src/runtest.rs
+++ b/src/tools/compiletest/src/runtest.rs
@@ -2739,7 +2739,7 @@ impl<'test> TestCx<'test> {
 
         #[rustfmt::skip]
         let tidy_args = [
-            "--new-blocklevel-tags", "rustdoc-search",
+            "--new-blocklevel-tags", "rustdoc-search,rustdoc-toolbar",
             "--indent", "yes",
             "--indent-spaces", "2",
             "--wrap", "0",
diff --git a/src/tools/html-checker/main.rs b/src/tools/html-checker/main.rs
index ecfbb1955e7..5cdc4d53ab5 100644
--- a/src/tools/html-checker/main.rs
+++ b/src/tools/html-checker/main.rs
@@ -31,7 +31,7 @@ fn check_html_file(file: &Path) -> usize {
         .arg("--mute-id") // this option is useful in case we want to mute more warnings
         .arg("yes")
         .arg("--new-blocklevel-tags")
-        .arg("rustdoc-search") // custom elements
+        .arg("rustdoc-search,rustdoc-toolbar") // custom elements
         .arg("--mute")
         .arg(&to_mute_s)
         .arg(file);