diff --git a/index.html b/index.html index 064bc904..84648efc 100644 --- a/index.html +++ b/index.html @@ -62,9 +62,11 @@

diff --git a/public/css/SampleLayout.css b/public/css/SampleLayout.css index 72b1b8f8..fff6e564 100644 --- a/public/css/SampleLayout.css +++ b/public/css/SampleLayout.css @@ -47,12 +47,18 @@ transform: translateY(0.25em); } +nav.sourceFileNav { + display: flex; + align-items: flex-start; +} + nav.sourceFileNav ul { box-sizing: border-box; list-style-type: none; padding: 0; margin: 0; margin-top: 15px; + position: relative; } nav.sourceFileNav li { @@ -97,12 +103,59 @@ nav.sourceFileNav[data-right=true]::after { background: linear-gradient(270deg, rgba(0, 0, 0, 0.35), transparent); } +.sourceLR { + display: none; + cursor: pointer; + width: 5em; + padding: 10px; + margin-top: 15px; + text-align: center; + color: var(--source-tab-color); + background-color: var(--source-tab-background); + border-left: 1px solid rgba(0, 0, 0, 0.5); + border-right: 1px solid rgba(0, 0, 0, 0.5); +} +.sourceLR:hover { + text-decoration: underline; +} +.sourceLRShow .sourceLR { + display: block; +} + +nav.sourceFileNav div.sourceFileScrollContainer { + white-space: nowrap; + overflow-x: auto; + scrollbar-width: thin; +} + +nav.sourceFileNav div.sourceFileScrollContainer::-webkit-scrollbar { + display: inline; + margin-top: 10px; + margin-bottom: 10px; + height: 11px; + width: 10px; +} + +nav.sourceFileNav div.sourceFileScrollContainer::-webkit-scrollbar-thumb { + background: rgb(200, 200, 200); + height: 4px; + border-radius: 20px; + -webkit-box-shadow: inset 0px 0px 10px rgb(45, 33, 33); + border: 0.5px solid transparent; + background-clip: content-box; +} + +nav.sourceFileNav div.sourceFileScrollContainer::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0); +} + nav.sourceFileNav li a { display: block; margin: 0; padding: 10px; color: var(--source-tab-color); background-color: var(--source-tab-background); + border-right: 1px solid rgba(0, 0, 0, 0.5); } nav.sourceFileNav li:hover { diff --git a/public/css/styles.css b/public/css/styles.css index 0c2b4a4f..21c54675 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -5,6 +5,9 @@ * { box-sizing: border-box; } +*, *:before, *:after { + box-sizing: inherit; +} html, body { margin: 0; diff --git a/src/main.ts b/src/main.ts index 6423347f..5be29878 100644 --- a/src/main.ts +++ b/src/main.ts @@ -33,6 +33,10 @@ const sampleContainerElem = getElem('.sampleContainer', sampleElem); const titleElem = getElem('#title', sampleElem); const descriptionElem = getElem('#description', sampleElem); const menuToggleElem = getElem('#menuToggle') as HTMLInputElement; +const codeElem = getElem('#code'); +const sourceTabsElem = getElem('#sourceTabs'); +const sourceLElem = getElem('#sourceL'); +const sourceRElem = getElem('#sourceR'); const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)'); @@ -78,7 +82,39 @@ function setURL(url: string) { } // Handle when the URL changes (browser back / forward) -window.addEventListener('popstate', parseURL); +window.addEventListener('popstate', (e) => { + e.preventDefault(); + parseURL(); +}); + +/** + * Scrolls the current tab into view. + */ +function moveIntoView(parent: HTMLElement, element: HTMLElement) { + const parentLeft = parent.scrollLeft; + const parentRight = parentLeft + parent.clientWidth; + + const elemLeft = element.offsetLeft; + const elemRight = elemLeft + element.clientWidth; + + if (elemLeft < parentLeft) { + parent.scrollLeft -= parentLeft - elemLeft; + } else if (elemRight > parentRight) { + parent.scrollLeft += elemRight - parentRight; + } +} + +/** + * Switches to a tab relative to the current tab + */ +function switchToRelativeTab(offsetFromCurrentTab: number) { + const tabs = [...sourceTabsElem.querySelectorAll('a')]; + const activeNdx = tabs.findIndex((tab) => tab.dataset.active === 'true'); + const newNdx = (activeNdx + tabs.length + offsetFromCurrentTab) % tabs.length; + const tab = tabs[newNdx]; + moveIntoView(sourceTabsElem, tab.parentElement!); + return tab; +} /** * Show/hide source tabs @@ -89,6 +125,8 @@ function setSourceTab(sourceInfo: SourceInfo) { const elem = e as HTMLElement; elem.dataset.active = (elem.dataset.name === name).toString(); }); + // Effectively makes the tab entirely visible if part of it is scrolled off. + switchToRelativeTab(0); } /** @@ -151,6 +189,7 @@ function setSampleIFrame( }; titleElem.textContent = name; + document.title = `WebGPU Samples - ${name}`; descriptionElem.innerHTML = markdownConverter.makeHtml(description); // Replace the iframe because changing src adds to the user's history. @@ -271,6 +310,43 @@ for (const { title, description, samples } of pageCategories) { ); } +sourceLElem.addEventListener('click', () => switchToRelativeTab(-1).click()); +sourceRElem.addEventListener('click', () => switchToRelativeTab(1).click()); + +function checkIfSourceTabsFit() { + const parentWidth = sourceTabsElem.clientWidth; + const childWidth = [...sourceTabsElem.querySelectorAll('li')].reduce( + (sum, elem) => sum + elem.clientWidth, + 0 + ); + const showLR = childWidth > parentWidth; + codeElem.classList.toggle('sourceLRShow', showLR); +} + +const registerResizeCallback = (() => { + const elemToResizeCallback = new Map< + Element, + (entry: ResizeObserverEntry) => void + >(); + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const cb = elemToResizeCallback.get(entry.target); + if (cb) { + cb(entry); + } + } + }); + return function ( + elem: Element, + callback: (entry: ResizeObserverEntry) => void + ) { + elemToResizeCallback.set(elem, callback); + observer.observe(elem); + }; +})(); + +registerResizeCallback(sourceTabsElem, checkIfSourceTabsFit); + /** * Parse the page's current URL and then set the iframe appropriately. */