Rollup merge of #115660 - notriddle:notriddle/sidebar-resize, r=GuillaumeGomez

rustdoc: allow resizing the sidebar / hiding the top bar

Fixes #97306

Preview: http://notriddle.com/rustdoc-html-demo-4/sidebar-resize/std/index.html

![image](https://github.com/rust-lang/rust/assets/1593513/a2f40ea2-0436-4e44-99e8-d160dab2a680)

## Summary

This feature adds:

1. A checkbox to the Settings popover to hide the persistent navigation bar (the sidebar on large viewports and the top bar on small ones).
2. On large viewports, it adds a resize handle to the persistent sidebar. Resizing it into nothing is equivalent to turning off the persistent navigation bar checkbox in Settings.
3. If the navigation bar is hidden, a toolbar button to the left of the search appears. Clicking it brings the navigation bar back.

## Motivation

While "mobile mode" is definitely a good default, it's not the only reason people have wanted to hide the sidebar:

* Some people use tiling window managers, and don't like rustdoc's current breakpoints. Changing the breakpoints might help with that, but there's no perfect solution, because there's a gap between "huge screen" and "smartphone" where reasonable people can disagree about whether it makes sense for the sidebar to be on-screen. https://github.com/rust-lang/rust/issues/97306

* Some people ask for ways to reduce on-screen clutter because it makes it easier to focus. There's not a media query for that (and if there was, privacy-conscious users would turn it off). https://github.com/rust-lang/rust/issues/59829

This feature is designed to avoid these problems. Resizing the sidebar especially helps, because it provides a way to hide the sidebar without adding a new top-level button (which would add clutter), and it provides a way to make rustdoc play nicer in complex, custom screen layouts.

## Guide and Reference-level explanation

On a desktop or laptop with a mouse, resize the sidebar by dragging its right edge.

On any browser, including mobile phones, the sticky top bar or side bar can be hidden from the Settings area (the button with the cog wheel, next to the search bar). When it's hidden, a convenient button will appear on the search bar's left.

## Drawbacks

This adds more JavaScript code to the render blocking area.

## Rationale and alternatives

The most obvious way to allow people to hide the sidebar would have been to let them "manually enter mobile mode." The upside is that it's a feature we already have. The downside is that it's actually really hard to come up with a terse description. Is it:

* A Setting that forces desktop viewers to always have the mobile-style top bar? If so, how do we label it? Should it be visible on mobile, and, if so, does it just not do anything?
* A persistent hide/show sidebar button, present on desktop, just like on mobile? That's clutter that I'd like to avoid.

## Prior art

* The new file browser in GitHub uses a similar divider with a mouse-over indicator
* mdBook and macOS Finder both allow you to resize the sidebar to nothing as a gesture to hide it
* https://www.nngroup.com/articles/drag-drop/

## Future possibilities

https://rust-lang.zulipchat.com/#narrow/stream/266220-rustdoc/topic/Table.20of.20contents proposes a new, second sidebar (a table of contents). How should it fit in with this feature? Should it be resizeable? Hideable? Can it be accessed on mobile?
This commit is contained in:
Guillaume Gomez 2023-12-15 11:51:23 +01:00 committed by GitHub
commit f8b92697a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 625 additions and 30 deletions

View File

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

View File

@ -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 */
}

View File

@ -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 */

View File

@ -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;

View File

@ -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;
}
}
@ -186,6 +193,11 @@
"js_name": "line-numbers",
"default": false,
},
{
"name": "Hide persistent navigation bar",
"js_name": "hide-sidebar",
"default": false,
},
{
"name": "Disable keyboard shortcuts",
"js_name": "disable-shortcuts",
@ -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) {

View File

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

View File

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

View File

@ -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"> {# #}

View File

@ -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"})

View File

@ -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|,

View File

@ -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|},

View File

@ -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"})

View File

@ -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])

View File

@ -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"})

View File

@ -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".

View File

@ -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")

View File

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