+
<
+
+
>
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.
*/