From 273a302ac8a5fed73bb17988ed571339dc6217ad Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Wed, 27 Sep 2023 08:59:32 -0700 Subject: [PATCH] rustdoc: enforce BODY_MIN constraint on sidebar resize --- src/librustdoc/html/static/css/rustdoc.css | 19 ++-- src/librustdoc/html/static/js/main.js | 113 +++++++++++++++++-- src/librustdoc/html/static/js/storage.js | 7 +- tests/rustdoc-gui/sidebar-resize-window.goml | 33 ++++++ 4 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 tests/rustdoc-gui/sidebar-resize-window.goml diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 85a4ff7a621..0bc687da6bf 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,7 +388,7 @@ img { .sidebar { font-size: 0.875rem; - flex: 0 0 var(--desktop-sidebar-width, 200px); + flex: 0 0 var(--desktop-sidebar-width); overflow-y: scroll; overscroll-behavior: contain; position: sticky; @@ -414,7 +419,7 @@ img { position: absolute; height: 100%; /* make sure there's a 1px gap between the scrollbar and resize handle */ - left: calc(var(--desktop-sidebar-width, 200px) + 1px); + left: calc(var(--desktop-sidebar-width) + 1px); } .rustdoc.src .sidebar-resizer { @@ -426,7 +431,7 @@ img { .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, 300px); + left: var(--src-sidebar-width); } .sidebar-resizing { @@ -448,7 +453,7 @@ img { 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, 200px); + left: var(--desktop-sidebar-width); border-left: solid 1px var(--sidebar-resizer-hover); } @@ -457,7 +462,7 @@ img { .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, 300px) - 1px); + left: calc(var(--src-sidebar-width) - 1px); } @media (pointer: coarse) { @@ -497,7 +502,7 @@ img { .src-sidebar-expanded .src .sidebar { overflow-y: auto; - flex-basis: var(--src-sidebar-width, 300px); + flex-basis: var(--src-sidebar-width); } .src-sidebar-expanded .src .sidebar > *:not(#src-sidebar-toggle) { @@ -1806,7 +1811,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, diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index 851ee795d4d..e1d674e7d04 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1273,8 +1273,49 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ searchState.setup(); }()); -// This section handles sidebar resizing +// 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 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 => { @@ -1283,13 +1324,38 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ 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; + + // 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(); @@ -1302,6 +1368,13 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ document.documentElement.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(); @@ -1310,6 +1383,9 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ 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! function changeSidebarSize(size) { if (isSrcPage) { updateLocalStorage("src-sidebar-width", size); @@ -1319,34 +1395,54 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ document.documentElement.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 < 50) { + if (pos < SIDEBAR_VANISH_THRESHOLD) { hideSidebar(); - } else if (pos >= 100) { - // 100 is the size of the logo - // don't let the sidebar get smaller than that, or it'll get squished + } else if (pos >= SIDEBAR_MIN) { if (isSidebarHidden()) { showSidebar(); } - // don't let the sidebar get wider than 500 - changeSidebarSize(Math.min(pos, window.innerWidth - 100, 500)); + // 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; } } + // Respond to the window resize event. + window.addEventListener("resize", () => { + 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; } - e.preventDefault(); + if (e) { + e.preventDefault(); + } + desiredSidebarSize = sidebar.getBoundingClientRect().width; removeClass(resizer, "active"); window.removeEventListener("pointermove", resize, false); window.removeEventListener("pointerup", stopResize, false); @@ -1376,6 +1472,7 @@ href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ window.addEventListener("pointerup", stopResize, false); addClass(resizer, "active"); addClass(document.documentElement, "sidebar-resizing"); + desiredSidebarSize = null; } resizer.addEventListener("pointerdown", initResize, false); }()); diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js index 20220f2e69a..18a127fbbd1 100644 --- a/src/librustdoc/html/static/js/storage.js +++ b/src/librustdoc/html/static/js/storage.js @@ -196,18 +196,21 @@ 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 `` 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 `` element instead. addClass(document.documentElement, "hide-sidebar"); } - function updateSidebarWidth() { const desktopSidebarWidth = getSettingValue("desktop-sidebar-width"); if (desktopSidebarWidth && desktopSidebarWidth !== "null") { diff --git a/tests/rustdoc-gui/sidebar-resize-window.goml b/tests/rustdoc-gui/sidebar-resize-window.goml new file mode 100644 index 00000000000..04321acc105 --- /dev/null +++ b/tests/rustdoc-gui/sidebar-resize-window.goml @@ -0,0 +1,33 @@ +// 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]) + +// 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])