From d60aab9250009bb7d6a7d17a98951de6c10c5865 Mon Sep 17 00:00:00 2001 From: Guillaume Gomez Date: Wed, 7 Oct 2026 17:10:27 +0200 Subject: [PATCH] rustdoc: Make sticky impl headings --- src/librustdoc/html/static/css/noscript.css | 29 +++++ src/librustdoc/html/static/css/rustdoc.css | 134 +++++++++++++++----- src/librustdoc/html/static/js/main.js | 70 ++++++++++ 3 files changed, 202 insertions(+), 31 deletions(-) diff --git a/src/librustdoc/html/static/css/noscript.css b/src/librustdoc/html/static/css/noscript.css index 085880b215d14..56d7d84a14c89 100644 --- a/src/librustdoc/html/static/css/noscript.css +++ b/src/librustdoc/html/static/css/noscript.css @@ -33,6 +33,11 @@ nav.sub { display: block; } +details.toggle [id] { + /* This is used to prevent anchors from going below the sticky `details`. */ + scroll-margin-top: 30vh; +} + /* Begin: styles for themes Keep the default light and dark themes synchronized with the ones in rustdoc.css */ @@ -147,6 +152,18 @@ nav.sub { --notable-badge-green: oklch(0.88 0.21 150); --notable-badge-blue: oklch(0.88 0.21 240); --notable-badge-violet: oklch(0.88 0.21 300); + --collapse-toggle-closed-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-closed-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-opaque: url('data:image/svg+xml,'); } /* End theme: light */ @@ -264,6 +281,18 @@ nav.sub { --notable-badge-green: oklch(0.55 0.21 150); --notable-badge-blue: oklch(0.55 0.21 240); --notable-badge-violet: oklch(0.55 0.21 300); + --collapse-toggle-closed-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-closed-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-opaque: url('data:image/svg+xml,'); } /* End theme: dark */ } diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 6e7a6daa307cd..13818e7a568ca 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -61,6 +61,7 @@ xmlns="http://www.w3.org/2000/svg" fill="black" height="18px">\ --hamburger-image: url('data:image/svg+xml,\ '); + --sticky-offset: 0; } :root.sans-serif-fonts { @@ -2205,10 +2206,10 @@ button#toggle-all-docs:hover, button#toggle-all-docs:focus-visible { /* Magnifying glass */ content: url('data:image/svg+xml,\ - \ - Search\ - '); + \ + Search\ + '); width: 18px; height: 18px; filter: var(--settings-menu-filter); @@ -2403,6 +2404,21 @@ details.toggle { position: relative; } +h2.section-header + div > details.implementors-toggle > summary, +details.toggle.big-toggle > summary { + position: sticky; + top: 0; + background: var(--main-background-color); + z-index: 1; +} + +h2.section-header + div > details.implementors-toggle > summary.sticky .item-info, +h2.section-header + div > details.implementors-toggle > summary.sticky .docblock, +details.toggle.big-toggle > summary.sticky .item-info, +details.toggle.big-toggle > summary.sticky .docblock { + display: none; +} + details.big-toggle { /* This makes [-] on the same line as . */ contain: inline-size; @@ -2431,18 +2447,41 @@ details.toggle > summary.hideme > span { } details.toggle > summary::before { - /* arrow pointing left */ - background: url('data:image/svg+xml,\ - '); + /* arrow pointing right */ + background-image: var(--collapse-toggle-closed-semi-opaque); + background-color: var(--main-background-color); + background-repeat: no-repeat; + background-position: left 4px; content: ""; cursor: pointer; - width: 16px; - height: 16px; + width: 24px; + height: 100%; display: inline-block; vertical-align: middle; - opacity: .5; - filter: var(--toggle-filter); +} + +details.toggle > summary:focus::before, +details.toggle > summary:hover::before { + background-image: var(--collapse-toggle-closed-opaque); +} + +details.toggle[open] > summary::before { + /* arrow pointing down */ + background-image: var(--collapse-toggle-open-semi-opaque); +} + +details.toggle[open] > summary:focus::before, +details.toggle[open] > summary:hover::before { + background-image: var(--collapse-toggle-open-opaque); +} + +details.toggle[open] > summary::after { + content: "Collapse"; +} + +details.toggle [id] { + /* This is used to prevent anchors from going below the sticky `details`. */ + scroll-margin-top: var(--sticky-offset); } details.toggle > summary.hideme > span, @@ -2465,11 +2504,6 @@ details.toggle > summary.hideme::after { content: ""; } -details.toggle > summary:focus::before, -details.toggle > summary:hover::before { - opacity: 1; -} - details.toggle > summary:focus-visible::before { /* The SVG is black, and gets turned white using a filter in the dark themes. Do the same with the outline. @@ -2490,7 +2524,7 @@ details.toggle > summary.hideme::before { details.toggle > summary:not(.hideme)::before { position: absolute; left: -24px; - top: 4px; + top: 0; } .impl-items > details.toggle > summary:not(.hideme)::before, @@ -2515,7 +2549,9 @@ details.toggle > summary:not(.hideme)::before { details.big-toggle > summary:not(.hideme)::before { left: -34px; - top: 9px; + width: 34px; + height: 100%; + background-position: left 9px; } /* When a "hideme" summary is open and the "Expand description" or "Show @@ -2531,17 +2567,6 @@ details.toggle[open] > summary.hideme > span { display: none; } -details.toggle[open] > summary::before { - /* arrow pointing down */ - background: url('data:image/svg+xml,\ - '); -} - -details.toggle[open] > summary::after { - content: "Collapse"; -} - details.toggle:not([open]) > summary .docblock { max-height: calc(1.5em + 0.75em); overflow-y: hidden; @@ -2726,6 +2751,9 @@ in src-script.js and main.js --impl-items-indent: 0.7em; --topbar-height: 45px; } + :root.hide-sidebar { + --topbar-height: 0; + } /* When linking to an item with an `id` (for instance, by clicking a link in the sidebar, or visiting a URL with a fragment like `#method.new`, we don't want the item to be obscured @@ -2906,7 +2934,8 @@ in src-script.js and main.js #main-content > details.toggle > summary::before, #main-content > div > details.toggle > summary::before { - left: -11px; + left: -24px; + width: 24px; } /* sidebar button becomes topbar button */ @@ -2966,6 +2995,7 @@ in src-script.js and main.js .impl-items > details.toggle > summary:not(.hideme)::before, #main-content > .methods > details.toggle > summary:not(.hideme)::before { left: -20px; + width: 20px; } /* Align summary-nested and unnested item-info gizmos. */ @@ -2988,6 +3018,12 @@ in src-script.js and main.js html:not(.src-sidebar-expanded) .src #sidebar-button > a:focus-visible { background-color: var(--sidebar-background-color); } + + h2.section-header + div > details.implementors-toggle > summary, + details.toggle.big-toggle > summary { + top: var(--topbar-height); + margin-left: 10px; + } } @@ -3316,6 +3352,18 @@ by default. --notable-badge-green: oklch(0.88 0.21 150); --notable-badge-blue: oklch(0.88 0.21 240); --notable-badge-violet: oklch(0.88 0.21 300); + --collapse-toggle-closed-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-closed-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-opaque: url('data:image/svg+xml,'); } /* End theme: light */ @@ -3432,6 +3480,18 @@ by default. --notable-badge-green: oklch(0.55 0.21 150); --notable-badge-blue: oklch(0.55 0.21 240); --notable-badge-violet: oklch(0.55 0.21 300); + --collapse-toggle-closed-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-closed-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-opaque: url('data:image/svg+xml,'); } /* End theme: dark */ @@ -3552,6 +3612,18 @@ Original by Dempfi (https://github.com/dempfi/ayu) --notable-badge-green: oklch(0.49 0.21 150); --notable-badge-blue: oklch(0.49 0.21 240); --notable-badge-violet: oklch(0.49 0.21 300); + --collapse-toggle-closed-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-closed-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-semi-opaque: url('data:image/svg+xml,'); + --collapse-toggle-open-opaque: url('data:image/svg+xml,'); } :root[data-theme="ayu"] h1, diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index e957a13a2b038..b6a2ab0cf033b 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1293,6 +1293,7 @@ function preLoadCss(cssUrl) { base.TOOLTIP_FORCE_VISIBLE = true; } } + setupScrollObserver(); }); const mainElem = document.getElementById(MAIN_ID); @@ -2249,4 +2250,73 @@ function preLoadCss(cssUrl) { elem.addEventListener("mouseover", addCopyButton); elem.addEventListener("click", showHideCodeExampleButtons); }); + + /* Setting the "sticky offset" to prevent the heading to hide the target below it. */ + function setupScrollObserver() { + function topbarHeight() { + if (window.innerWidth > 700) { + return 0; + } + return parseInt( + window.getComputedStyle(document.documentElement) + .getPropertyValue('--topbar-height'), + 10, + ); + } + + if (window.scrollObserver) { + window.scrollObserver.disconnect(); + } + + window.scrollObserver = new IntersectionObserver(entries => { + for (const entry of entries) { + // The ratio drops below 1 in two cases: stuck at the top, or partly + // below the bottom of the viewport. Checking `top` tells them apart. + let top = topbarHeight(); + if (entry.intersectionRatio < 1 && entry.boundingClientRect.top < (top + 1)) { + if (!entry.target.classList.contains("sticky")) { + entry.target.classList.add("sticky"); + document.documentElement.style.setProperty( + "--sticky-offset", `${entry.boundingClientRect.height + top}px`); + const details = entry.target.parentElement; + // We store the callback into `details` to be reused when we want to remove + // the event listener. + details.toggleCallback = () => { + if (!details.open) { + // When we scroll the docs, we're actually scrolling the `html` + // element, so we need to take the current scroll position... + const currentScroll = document.documentElement.scrollTop; + // ... and then we add the current "sticky" heading scroll position + // which is negative so jump back to where it's located. + document.documentElement.scrollTo({ + top: details.getBoundingClientRect().top + currentScroll - top, + }); + } + }; + details.addEventListener("toggle", details.toggleCallback); + } + } else if (entry.target.classList.contains("sticky")) { + entry.target.classList.remove("sticky"); + const details = entry.target.parentElement; + details.removeEventListener("toggle", details.toggleCallback); + } + } + }, { + rootMargin: `-${topbarHeight() + 1}px 0px 0px 0px`, + threshold: [1], + }); + + onEachLazy( + document.querySelectorAll( + "h2.section-header + div > details.implementors-toggle > summary" + ), + elem => window.scrollObserver.observe(elem), + ); + onEachLazy( + document.querySelectorAll("details.toggle.big-toggle > summary"), + elem => window.scrollObserver.observe(elem), + ); + } + + setupScrollObserver(); }());