Register the windows

This commit is contained in:
Daniel Imms
2025-12-24 17:09:20 -08:00
parent 40b2bbef34
commit 6bde923f07
8 changed files with 813 additions and 464 deletions
+39 -270
View File
@@ -19,7 +19,12 @@ if ('WebAssembly' in window) {
import { Terminal, ITerminalOptions, type IDisposable, type ITheme } from '@xterm/xterm';
import { AttachAddon } from '@xterm/addon-attach';
import { initControlBar } from './components/controlBar';
import { ControlBar } from './components/controlBar';
import { GpuWindow } from './components/window/gpuWindow';
import { OptionsWindow } from './components/window/optionsWindow';
import { StyleWindow } from './components/window/styleWindow';
import { TestWindow } from './components/window/testWindow';
import { VtWindow } from './components/window/vtWindow';
import { ClipboardAddon } from '@xterm/addon-clipboard';
import { FitAddon } from '@xterm/addon-fit';
import { LigaturesAddon } from '@xterm/addon-ligatures';
@@ -56,7 +61,11 @@ let protocol;
let socketURL;
let socket;
let pid;
let autoResize: boolean = true;
let gpuWindow: GpuWindow;
let optionsWindow: OptionsWindow;
let styleWindow: StyleWindow;
let testWindow: TestWindow;
let vtWindow: VtWindow;
type AddonType = 'attach' | 'clipboard' | 'fit' | 'image' | 'progress' | 'search' | 'serialize' | 'unicode11' | 'unicodeGraphemes' | 'webLinks' | 'webgl' | 'ligatures';
@@ -117,7 +126,7 @@ const actionElements = {
findPrevious: document.querySelector('#find-previous') as HTMLInputElement,
findResults: document.querySelector('#find-results')
};
const paddingElement = document.getElementById('padding') as HTMLInputElement;
let paddingElement: HTMLInputElement;
const xtermjsTheme = {
foreground: '#F8F8F8',
@@ -231,8 +240,21 @@ if (document.location.pathname === '/test') {
window.WebLinksAddon = WebLinksAddon;
window.WebglAddon = WebglAddon;
} else {
const controlBar = new ControlBar(document.getElementById('sidebar'), document.querySelector('.banner-tabs'), [
{ id: 'addons', label: 'Addons' }
]);
optionsWindow = new OptionsWindow(updateTerminalSize, updateTerminalContainerBackground);
controlBar.registerWindow(optionsWindow);
gpuWindow = new GpuWindow();
controlBar.registerWindow(gpuWindow);
styleWindow = new StyleWindow();
controlBar.registerWindow(styleWindow);
paddingElement = styleWindow.paddingElement;
testWindow = new TestWindow();
controlBar.registerWindow(testWindow);
vtWindow = new VtWindow();
controlBar.registerWindow(vtWindow);
createTerminal();
initControlBar();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('create-new-window').addEventListener('click', createNewWindowButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
@@ -255,7 +277,6 @@ if (document.location.pathname === '/test') {
document.getElementById('ligatures-test').addEventListener('click', ligaturesTest);
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons();
initImageAddonExposed();
testEvents();
progressButtons();
@@ -323,10 +344,10 @@ function createTerminal(): void {
try {
typedTerm.loadAddon(addons.webgl.instance);
term.open(terminalContainer);
setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e));
gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => gpuWindow.appendTextureAtlas(e));
addons.webgl.instance.onRemoveTextureAtlasCanvas(e => gpuWindow.removeTextureAtlas(e));
} catch (e) {
console.warn('error during loading webgl addon:', e);
addons.webgl.instance.dispose();
@@ -340,9 +361,10 @@ function createTerminal(): void {
term.focus();
updateTerminalContainerBackground();
vtWindow?.initTerminal(term);
const resizeObserver = new ResizeObserver(entries => {
if (autoResize) {
if (optionsWindow.autoResize) {
addons.fit.instance.fit();
}
});
@@ -377,7 +399,7 @@ function createTerminal(): void {
// fit is called within a setTimeout, cols and rows need this.
setTimeout(async () => {
initOptions(term);
optionsWindow.initOptions(term, addDomListener);
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo
@@ -441,186 +463,6 @@ function runFakeTerminal(): void {
});
}
function initOptions(term: Terminal): void {
const blacklistedOptions = [
// Internal only options
'cancelEvents',
'convertEol',
'termName',
'cols', 'rows', // subsumed by "size" (colsRows) option
// Complex option
'documentOverride',
'linkHandler',
'logger',
'overviewRuler',
'theme',
'windowOptions',
'windowsPty',
];
const stringOptions = {
cursorStyle: ['block', 'underline', 'bar'],
cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'],
theme: ['default', 'xtermjs', 'sapphire', 'light'],
wordSeparator: null,
colsRows: null
};
const options = Object.getOwnPropertyNames(term.options);
const booleanOptions = [];
const numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.options[o]) {
case 'boolean':
booleanOptions.push(o);
break;
case 'number':
numberOptions.push(o);
break;
default:
if (Object.keys(stringOptions).indexOf(o) === -1 && numberOptions.indexOf(o) === -1 && booleanOptions.indexOf(o) === -1) {
console.warn(`Unrecognized option: "${o}"`);
}
}
});
let html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (o === 'colsRows') {
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
} else if (stringOptions[o]) {
const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o];
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
}
});
html += '</div>';
const container = document.getElementById('options-container');
container.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'lineHeight') {
term.options.lineHeight = parseFloat(input.value);
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
} else if (o === 'scrollback') {
term.options.scrollback = parseInt(input.value);
setTimeout(() => updateTerminalSize(), 5);
} else {
term.options[o] = parseInt(input.value);
}
// Always update terminal size in case the option changes the dimensions
updateTerminalSize();
});
});
Object.keys(stringOptions).forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: any = input.value;
if (o === 'colsRows') {
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
if (m) {
autoResize = false;
term.resize(parseInt(m[1]), parseInt(m[2]));
} else {
autoResize = true;
input.value = 'auto';
updateTerminalSize();
}
} else if (o === 'theme') {
switch (input.value) {
case 'default':
value = undefined;
break;
case 'xtermjs':
// Custom theme to match style of xterm.js logo
value = xtermjsTheme;
case 'sapphire':
// Color source: https://github.com/Tyriar/vscode-theme-sapphire
value = {
background: '#1c2431',
foreground: '#cccccc',
selectionBackground: '#399ef440',
black: '#666666',
blue: '#399ef4',
brightBlack: '#666666',
brightBlue: '#399ef4',
brightCyan: '#21c5c7',
brightGreen: '#4eb071',
brightMagenta: '#b168df',
brightRed: '#da6771',
brightWhite: '#efefef',
brightYellow: '#fff099',
cyan: '#21c5c7',
green: '#4eb071',
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
yellow: '#fff099'
};
break;
case 'light':
// Color source: https://github.com/microsoft/vscode/blob/main/extensions/theme-defaults/themes/light_plus.json
value = {
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
cursorAccent: '#ffffff',
selectionBackground: '#add6ff',
overviewRulerBorder: '#aaaaaa',
black: '#000000',
blue: '#0451a5',
brightBlack: '#666666',
brightBlue: '#0451a5',
brightCyan: '#0598bc',
brightGreen: '#14ce14',
brightMagenta: '#bc05bc',
brightRed: '#cd3131',
brightWhite: '#a5a5a5',
brightYellow: '#b5ba00',
cyan: '#0598bc',
green: '#00bc00',
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
yellow: '#949800'
};
break;
}
}
term.options[o] = value;
if (o === 'theme') {
updateTerminalContainerBackground();
}
});
});
}
function updateTerminalContainerBackground(): void {
const bg = term.options.theme?.background ?? '#000000';
terminalContainer.style.backgroundColor = bg;
@@ -631,9 +473,9 @@ function initAddons(term: Terminal): void {
function postInitWebgl(): void {
setTimeout(() => {
setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
gpuWindow.setTextureAtlas(addons.webgl.instance.textureAtlas);
addons.webgl.instance.onChangeTextureAtlas(e => gpuWindow.setTextureAtlas(e));
addons.webgl.instance.onAddTextureAtlasCanvas(e => gpuWindow.appendTextureAtlas(e));
}, 500);
}
function preDisposeWebgl(): void {
@@ -773,9 +615,9 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
}
function updateTerminalSize(): void {
const width = autoResize ? '100%'
const width = optionsWindow.autoResize ? '100%'
: (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = autoResize ? '100%'
const height = optionsWindow.autoResize ? '100%'
: (term._core._renderService.dimensions.css.canvas.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
@@ -808,21 +650,7 @@ function htmlSerializeButtonHandler(): void {
document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
}
function setTextureAtlas(e: HTMLCanvasElement): void {
styleAtlasPage(e);
document.querySelector('#texture-atlas').replaceChildren(e);
}
function appendTextureAtlas(e: HTMLCanvasElement): void {
styleAtlasPage(e);
document.querySelector('#texture-atlas').appendChild(e);
}
function removeTextureAtlas(e: HTMLCanvasElement): void {
e.remove();
}
function styleAtlasPage(e: HTMLCanvasElement): void {
e.style.width = `${e.width / window.devicePixelRatio}px`;
e.style.height = `${e.height / window.devicePixelRatio}px`;
}
function customGlyphAlignmentHandler(): void {
term.write('\n\r');
@@ -1456,65 +1284,6 @@ function decorationStressTest(): void {
console.groupEnd();
};
function addVtButtons(): void {
function csi(e: string): string {
return `\x1b[${e}`;
}
function createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement {
const inputs: HTMLInputElement[] = [];
for (let i = 0; i < paramCount; i++) {
const input = document.createElement('input');
input.type = 'number';
input.title = `Input #${i + 1}`;
inputs.push(input);
}
const element = document.createElement('button');
element.textContent = name;
const writeCsiSplit = writeCsi.split('|');
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
const desc = document.createElement('span');
desc.textContent = description;
const container = document.createElement('div');
container.classList.add('vt-button');
container.append(element, ...inputs, desc);
return container;
}
const vtFragment = document.createDocumentFragment();
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
G: { label: 'CHA', description: 'Cursor Character Absolute' },
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
J: { label: 'ED', description: 'Erase in Display' },
'?|J': { label: 'DECSED', description: 'Erase in Display' },
K: { label: 'EL', description: 'Erase in Line' },
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
L: { label: 'IL', description: 'Insert Ps Line(s)' },
M: { label: 'DL', description: 'Delete Ps Line(s)' },
P: { label: 'DCH', description: 'Delete Ps Character(s)' },
' q': { label: 'DECSCUSR', description: 'Set Cursor Style' },
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
vtFragment.appendChild(createButton(spec.label, spec.description, s, spec.paramCount));
}
document.querySelector('#vt-container').appendChild(vtFragment);
}
function ligaturesTest(): void {
term.write([
'',
+160 -29
View File
@@ -3,41 +3,172 @@
* @license MIT
*/
export function initControlBar(): void {
// Sidebar resize handling
const sidebar = document.getElementById('sidebar');
const resizeHandleH = document.getElementById('sidebar-resize-handle-horizontal');
const resizeHandleV = document.getElementById('sidebar-resize-handle-vertical');
const resizeHandleCorner = document.getElementById('sidebar-resize-handle-corner');
let resizeMode: 'none' | 'horizontal' | 'vertical' | 'corner' = 'none';
export interface ITabConfig {
id: string;
label: string;
}
function startResize(mode: 'horizontal' | 'vertical' | 'corner', e: MouseEvent): void {
resizeMode = mode;
export interface IControlWindow {
readonly id: string;
readonly label: string;
build(container: HTMLElement): void;
}
export class ControlBar {
private readonly _sidebar: HTMLElement;
private readonly _resizeHandleH: HTMLElement;
private readonly _resizeHandleV: HTMLElement;
private readonly _resizeHandleCorner: HTMLElement;
private _resizeMode: 'none' | 'horizontal' | 'vertical' | 'corner' = 'none';
private readonly _tabContainer: HTMLElement;
private readonly _tabs: Map<string, { button: HTMLButtonElement; content: HTMLElement }> = new Map();
private _activeTabId: string | null = null;
constructor(sidebar: HTMLElement, tabContainer: HTMLElement, tabs: ITabConfig[]) {
this._sidebar = sidebar;
this._tabContainer = tabContainer;
// Create resize handles
this._resizeHandleH = document.createElement('div');
this._resizeHandleH.id = 'sidebar-resize-handle-horizontal';
this._resizeHandleV = document.createElement('div');
this._resizeHandleV.id = 'sidebar-resize-handle-vertical';
this._resizeHandleCorner = document.createElement('div');
this._resizeHandleCorner.id = 'sidebar-resize-handle-corner';
// Insert handles at the beginning of the sidebar
this._sidebar.prepend(this._resizeHandleCorner);
this._sidebar.prepend(this._resizeHandleV);
this._sidebar.prepend(this._resizeHandleH);
this._initResizeListeners();
this._initTabs(tabs);
}
private _initResizeListeners(): void {
this._resizeHandleH.addEventListener('mousedown', (e: MouseEvent) => this._startResize('horizontal', e));
this._resizeHandleV.addEventListener('mousedown', (e: MouseEvent) => this._startResize('vertical', e));
this._resizeHandleCorner.addEventListener('mousedown', (e: MouseEvent) => this._startResize('corner', e));
document.addEventListener('mousemove', (e: MouseEvent) => {
if (this._resizeMode === 'none') return;
if (this._resizeMode === 'horizontal' || this._resizeMode === 'corner') {
const newWidth = window.innerWidth - e.clientX - 10;
this._sidebar.style.width = `${Math.max(200, newWidth)}px`;
}
if (this._resizeMode === 'vertical' || this._resizeMode === 'corner') {
const rect = this._sidebar.getBoundingClientRect();
const newHeight = e.clientY - rect.top;
this._sidebar.style.height = `${Math.max(100, newHeight)}px`;
}
});
document.addEventListener('mouseup', () => {
this._resizeMode = 'none';
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
private _startResize(mode: 'horizontal' | 'vertical' | 'corner', e: MouseEvent): void {
this._resizeMode = mode;
document.body.style.cursor = mode === 'horizontal' ? 'ew-resize' : mode === 'vertical' ? 'ns-resize' : 'nesw-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
}
resizeHandleH.addEventListener('mousedown', (e: MouseEvent) => startResize('horizontal', e));
resizeHandleV.addEventListener('mousedown', (e: MouseEvent) => startResize('vertical', e));
resizeHandleCorner.addEventListener('mousedown', (e: MouseEvent) => startResize('corner', e));
private _initTabs(tabs: ITabConfig[]): void {
// Clear existing buttons
this._tabContainer.innerHTML = '';
document.addEventListener('mousemove', (e: MouseEvent) => {
if (resizeMode === 'none') return;
if (resizeMode === 'horizontal' || resizeMode === 'corner') {
const newWidth = window.innerWidth - e.clientX - 10;
sidebar.style.width = `${Math.max(200, newWidth)}px`;
}
if (resizeMode === 'vertical' || resizeMode === 'corner') {
const rect = sidebar.getBoundingClientRect();
const newHeight = e.clientY - rect.top;
sidebar.style.height = `${Math.max(100, newHeight)}px`;
}
});
// Create tab buttons
for (const tab of tabs) {
const content = document.getElementById(tab.id);
if (!content) {
console.warn(`Tab content element not found: ${tab.id}`);
continue;
}
document.addEventListener('mouseup', () => {
resizeMode = 'none';
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
const button = document.createElement('button');
button.id = `${tab.id}button`;
button.className = 'tabLinks';
button.textContent = tab.label;
button.addEventListener('click', (e) => this._openSection(e, tab.id));
this._tabContainer.appendChild(button);
this._tabs.set(tab.id, { button, content });
// Hide content initially
content.style.display = 'none';
}
// Restore saved tab or default to first
const savedTab = localStorage.getItem('tab');
const tabId = savedTab && this._tabs.has(savedTab) ? savedTab : tabs[0]?.id;
if (tabId) {
this._activateTab(tabId);
}
}
private _openSection(event: MouseEvent, tabId: string): void {
const tab = this._tabs.get(tabId);
if (!tab) return;
// If clicking active tab, toggle sidebar visibility
if (tab.button.classList.contains('active')) {
this._sidebar.classList.toggle('sidebar-hidden');
return;
}
// Show sidebar if hidden
this._sidebar.classList.remove('sidebar-hidden');
this._activateTab(tabId);
}
private _activateTab(tabId: string): void {
// Deactivate all tabs
for (const [, { button, content }] of this._tabs) {
button.classList.remove('active');
content.style.display = 'none';
}
// Activate the selected tab
const tab = this._tabs.get(tabId);
if (tab) {
tab.button.classList.add('active');
tab.content.style.display = 'block';
this._activeTabId = tabId;
localStorage.setItem('tab', tabId);
}
}
public registerWindow(window: IControlWindow): void {
// Create button
const button = document.createElement('button');
button.id = `${window.id}button`;
button.className = 'tabLinks';
button.textContent = window.label;
button.addEventListener('click', (e) => this._openSection(e, window.id));
this._tabContainer.appendChild(button);
// Create content container
const content = document.createElement('div');
content.id = window.id;
content.className = 'tabContent';
content.style.display = 'none';
this._sidebar.appendChild(content);
// Let the window build its content
window.build(content);
this._tabs.set(window.id, { button, content });
}
public get activeTabId(): string | null {
return this._activeTabId;
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class GpuWindow implements IControlWindow {
public readonly id = 'gpu';
public readonly label = 'GPU';
private _textureAtlasContainer: HTMLElement;
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'GPU';
container.appendChild(heading);
const zoomCheckbox = document.createElement('input');
zoomCheckbox.type = 'checkbox';
zoomCheckbox.id = 'texture-atlas-zoom';
container.appendChild(zoomCheckbox);
const zoomLabel = document.createElement('label');
zoomLabel.htmlFor = 'texture-atlas-zoom';
zoomLabel.textContent = 'Zoom texture atlas';
container.appendChild(zoomLabel);
this._textureAtlasContainer = document.createElement('div');
this._textureAtlasContainer.id = 'texture-atlas';
container.appendChild(this._textureAtlasContainer);
}
public setTextureAtlas(canvas: HTMLCanvasElement): void {
this._styleAtlasPage(canvas);
this._textureAtlasContainer.replaceChildren(canvas);
}
public appendTextureAtlas(canvas: HTMLCanvasElement): void {
this._styleAtlasPage(canvas);
this._textureAtlasContainer.appendChild(canvas);
}
public removeTextureAtlas(canvas: HTMLCanvasElement): void {
canvas.remove();
}
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
canvas.style.width = `${canvas.width / window.devicePixelRatio}px`;
canvas.style.height = `${canvas.height / window.devicePixelRatio}px`;
}
}
+250
View File
@@ -0,0 +1,250 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { Terminal, ITheme } from '@xterm/xterm';
import type { IControlWindow } from '../controlBar';
const xtermjsTheme: ITheme = {
foreground: '#F8F8F8',
background: '#2D2E2C',
selectionBackground: '#5DA5D533',
selectionInactiveBackground: '#555555AA',
black: '#1E1E1D',
brightBlack: '#262625',
red: '#CE5C5C',
brightRed: '#FF7272',
green: '#5BCC5B',
brightGreen: '#72FF72',
yellow: '#CCCC5B',
brightYellow: '#FFFF72',
blue: '#5D5DD3',
brightBlue: '#7279FF',
magenta: '#BC5ED1',
brightMagenta: '#E572FF',
cyan: '#5DA5D5',
brightCyan: '#72F0FF',
white: '#F8F8F8',
brightWhite: '#FFFFFF'
};
export class OptionsWindow implements IControlWindow {
public readonly id = 'options';
public readonly label = 'Options';
private _container: HTMLElement;
private _optionsContainer: HTMLElement;
private _term: Terminal;
private _autoResize: boolean = true;
private _updateTerminalSize: () => void;
private _updateTerminalContainerBackground: () => void;
constructor(
updateTerminalSize: () => void,
updateTerminalContainerBackground: () => void
) {
this._updateTerminalSize = updateTerminalSize;
this._updateTerminalContainerBackground = updateTerminalContainerBackground;
}
public build(container: HTMLElement): void {
this._container = container;
const heading = document.createElement('h3');
heading.textContent = 'Options';
container.appendChild(heading);
const description = document.createElement('p');
description.innerHTML = 'These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.';
container.appendChild(description);
this._optionsContainer = document.createElement('div');
this._optionsContainer.id = 'options-container';
container.appendChild(this._optionsContainer);
}
public initOptions(term: Terminal, addDomListener: (el: HTMLElement, type: string, handler: (...args: any[]) => any) => void): void {
this._term = term;
const blacklistedOptions = [
'cancelEvents',
'convertEol',
'termName',
'cols', 'rows',
'documentOverride',
'linkHandler',
'logger',
'overviewRuler',
'theme',
'windowOptions',
'windowsPty',
];
const stringOptions: { [key: string]: string[] | null } = {
cursorStyle: ['block', 'underline', 'bar'],
cursorInactiveStyle: ['outline', 'block', 'bar', 'underline', 'none'],
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
logLevel: ['trace', 'debug', 'info', 'warn', 'error', 'off'],
theme: ['default', 'xtermjs', 'sapphire', 'light'],
wordSeparator: null,
colsRows: null
};
const options = Object.getOwnPropertyNames(term.options);
const booleanOptions: string[] = [];
const numberOptions: string[] = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.options[o]) {
case 'boolean':
booleanOptions.push(o);
break;
case 'number':
numberOptions.push(o);
break;
default:
if (Object.keys(stringOptions).indexOf(o) === -1 && numberOptions.indexOf(o) === -1 && booleanOptions.indexOf(o) === -1) {
console.warn(`Unrecognized option: "${o}"`);
}
}
});
let html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o] ?? ''}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (o === 'colsRows') {
html += `<div class="option"><label>size (<var>cols</var><code>x</code><var>rows</var> or <code>auto</code>) <input id="opt-${o}" type="text" value="auto"/></label></div>`;
} else if (stringOptions[o]) {
const selectedOption = o === 'theme' ? 'xtermjs' : term.options[o];
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o]!.map(v => `<option ${v === selectedOption ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
}
});
html += '</div>';
this._optionsContainer.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'lineHeight') {
term.options.lineHeight = parseFloat(input.value);
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
} else if (o === 'scrollback') {
term.options.scrollback = parseInt(input.value);
setTimeout(() => this._updateTerminalSize(), 5);
} else {
term.options[o] = parseInt(input.value);
}
this._updateTerminalSize();
});
});
Object.keys(stringOptions).forEach(o => {
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
let value: any = input.value;
if (o === 'colsRows') {
const m = input.value.match(/^([0-9]+)x([0-9]+)$/);
if (m) {
this._autoResize = false;
term.resize(parseInt(m[1]), parseInt(m[2]));
} else {
this._autoResize = true;
input.value = 'auto';
this._updateTerminalSize();
}
} else if (o === 'theme') {
switch (input.value) {
case 'default':
value = undefined;
break;
case 'xtermjs':
value = xtermjsTheme;
break;
case 'sapphire':
value = {
background: '#1c2431',
foreground: '#cccccc',
selectionBackground: '#399ef440',
black: '#666666',
blue: '#399ef4',
brightBlack: '#666666',
brightBlue: '#399ef4',
brightCyan: '#21c5c7',
brightGreen: '#4eb071',
brightMagenta: '#b168df',
brightRed: '#da6771',
brightWhite: '#efefef',
brightYellow: '#fff099',
cyan: '#21c5c7',
green: '#4eb071',
magenta: '#b168df',
red: '#da6771',
white: '#efefef',
yellow: '#fff099'
};
break;
case 'light':
value = {
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
cursorAccent: '#ffffff',
selectionBackground: '#add6ff',
overviewRulerBorder: '#aaaaaa',
black: '#000000',
blue: '#0451a5',
brightBlack: '#666666',
brightBlue: '#0451a5',
brightCyan: '#0598bc',
brightGreen: '#14ce14',
brightMagenta: '#bc05bc',
brightRed: '#cd3131',
brightWhite: '#a5a5a5',
brightYellow: '#b5ba00',
cyan: '#0598bc',
green: '#00bc00',
magenta: '#bc05bc',
red: '#cd3131',
white: '#555555',
yellow: '#949800'
};
break;
}
}
term.options[o] = value;
if (o === 'theme') {
this._updateTerminalContainerBackground();
}
});
});
}
public get autoResize(): boolean {
return this._autoResize;
}
public set autoResize(value: boolean) {
this._autoResize = value;
}
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class StyleWindow implements IControlWindow {
public readonly id = 'style';
public readonly label = 'Style';
private _paddingElement: HTMLInputElement;
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'Style';
container.appendChild(heading);
const wrapper = document.createElement('div');
wrapper.style.display = 'inline-block';
wrapper.style.marginRight = '16px';
const label = document.createElement('label');
label.htmlFor = 'padding';
label.textContent = 'Padding';
wrapper.appendChild(label);
this._paddingElement = document.createElement('input');
this._paddingElement.type = 'number';
this._paddingElement.id = 'padding';
wrapper.appendChild(this._paddingElement);
container.appendChild(wrapper);
}
public get paddingElement(): HTMLInputElement {
return this._paddingElement;
}
}
+177
View File
@@ -0,0 +1,177 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
export class TestWindow implements IControlWindow {
public readonly id = 'test';
public readonly label = 'Test';
public build(container: HTMLElement): void {
const heading = document.createElement('h3');
heading.textContent = 'Test';
container.appendChild(heading);
const wrapper = document.createElement('div');
wrapper.style.display = 'inline-block';
wrapper.style.marginRight = '16px';
const dl = document.createElement('dl');
// Lifecycle section
this._addDt(dl, 'Lifecycle');
this._addDdWithCheckbox(dl, 'use-real-terminal', 'Use real terminal', 'This is used to real vs fake terminals', true);
this._addDdWithButton(dl, 'dispose', 'Dispose terminal', 'This is used to testing memory leaks');
this._addDdWithButton(dl, 'create-new-window', 'Create terminal in new window', 'This is used to test rendering in other windows');
// Performance section
this._addDt(dl, 'Performance');
this._addDdWithButton(dl, 'load-test', 'Load test', 'Write several MB of data to simulate a lot of data coming from the process');
this._addDdWithButton(dl, 'load-test-long-lines', 'Load test (long lines)', 'Write several MB of data with long lines to simulate a lot of data coming from the process');
this._addDdWithButton(dl, 'print-cjk', 'CJK Unified Ideographs', 'Prints the 20977 characters from the CJK Unified Ideographs unicode block');
this._addDdWithButton(dl, 'print-cjk-sgr', 'CJK Unified Ideographs (random SGR)', 'Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes');
// Styles section
this._addDt(dl, 'Styles');
this._addDdWithButton(dl, 'custom-glyph-alignment', 'Custom glyph alignment test', 'Write custom glyph alignment tests to the terminal');
this._addDdWithButton(dl, 'custom-glyph-ranges', 'Custom glyph ranges', 'Write custom glyph unicode range to the terminal');
this._addDdWithButton(dl, 'powerline-symbol-test', 'Powerline symbol test', 'Write powerline symbol characters to the terminal (\\ue0a0+)');
this._addDdWithButton(dl, 'underline-test', 'Underline test', 'Write text with Kitty\'s extended underline sequences');
this._addDdWithButton(dl, 'sgr-test', 'SGR test', 'Write text with SGR attribute');
this._addDdWithButton(dl, 'ansi-colors', 'Ansi colors test', 'Write a wide range of ansi colors');
this._addDdWithButton(dl, 'osc-hyperlinks', 'Ansi hyperlinks test', 'Write some OSC 8 hyperlinks');
this._addDdWithButton(dl, 'bce', 'Colored Erase (BCE)', 'Test colored erase');
this._addDdWithButton(dl, 'add-grapheme-clusters', 'Grapheme clusters', 'Write grapheme cluster test strings');
// Decorations section
this._addDt(dl, 'Decorations');
this._addDdWithButton(dl, 'add-decoration', 'Decoration', 'Add a decoration to the terminal');
this._addDdWithButton(dl, 'add-overview-ruler', 'Add Overview Ruler', 'Add an overview ruler to the terminal');
this._addDdWithButton(dl, 'decoration-stress-test', 'Stress Test', 'Toggle between adding and removing a decoration to each line');
// Ligatures Addon section
this._addDt(dl, 'Ligatures Addon');
this._addDdWithButton(dl, 'ligatures-test', 'Common ligatures', 'Write common ligatures sequences');
// Weblinks Addon section
this._addDt(dl, 'Weblinks Addon');
this._addDdWithButton(dl, 'weblinks-test', 'Test URLs', 'Various url conditions from demo data, hover&click to test');
// Image Test section
this._addDt(dl, 'Image Test');
this._addDdWithButton(dl, 'image-demo1', 'snake (sixel)', '');
this._addDdWithButton(dl, 'image-demo2', 'oranges (sixel)', '');
this._addDdWithButton(dl, 'image-demo3', 'palette (iip)', '');
// Events Test section
this._addDt(dl, 'Events Test');
this._addDdWithButton(dl, 'event-focus', 'focus', '');
this._addDdWithButton(dl, 'event-blur', 'blur', '');
// Progress Addon section
this._addDt(dl, 'Progress Addon');
this._addDdWithButton(dl, 'progress-run', 'full set run', '');
this._addDdWithButton(dl, 'progress-0', 'state 0: remove', '');
this._addDdWithButton(dl, 'progress-1', 'state 1: set 20%', '');
this._addDdWithButton(dl, 'progress-2', 'state 2: error', '');
this._addDdWithButton(dl, 'progress-3', 'state 3: indeterminate', '');
this._addDdWithButton(dl, 'progress-4', 'state 4: pause', '');
// Progress bar
const progressDd = document.createElement('dd');
const progressDiv = document.createElement('div');
progressDiv.id = 'progress-progress';
const progressPercent = document.createElement('div');
progressPercent.id = 'progress-percent';
const progressIndeterminate = document.createElement('div');
progressIndeterminate.id = 'progress-indeterminate';
progressDiv.appendChild(progressPercent);
progressDiv.appendChild(progressIndeterminate);
progressDd.appendChild(progressDiv);
dl.appendChild(progressDd);
// Progress state
const stateDd = document.createElement('dd');
const stateDiv = document.createElement('div');
stateDiv.id = 'progress-state';
stateDiv.textContent = 'State:';
stateDd.appendChild(stateDiv);
dl.appendChild(stateDd);
wrapper.appendChild(dl);
container.appendChild(wrapper);
// Add progress bar styles
this._addProgressStyles(container);
}
private _addDt(dl: HTMLElement, text: string): void {
const dt = document.createElement('dt');
dt.textContent = text;
dl.appendChild(dt);
}
private _addDdWithButton(dl: HTMLElement, id: string, label: string, title: string): void {
const dd = document.createElement('dd');
const button = document.createElement('button');
button.id = id;
button.textContent = label;
if (title) {
button.title = title;
}
dd.appendChild(button);
dl.appendChild(dd);
}
private _addDdWithCheckbox(dl: HTMLElement, id: string, label: string, title: string, checked: boolean): void {
const dd = document.createElement('dd');
const labelElement = document.createElement('label');
labelElement.htmlFor = id;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = id;
checkbox.checked = checked;
if (title) {
checkbox.title = title;
}
labelElement.appendChild(checkbox);
labelElement.appendChild(document.createTextNode(label));
dd.appendChild(labelElement);
dl.appendChild(dd);
}
private _addProgressStyles(container: HTMLElement): void {
const style = document.createElement('style');
style.textContent = `
#progress-progress {
border: 1px solid black;
height: 10px;
}
#progress-percent {
height: 100%;
}
#progress-indeterminate {
display: none;
position: relative;
height: 100%;
}
#progress-indeterminate:before {
content: '';
position: absolute;
left: 0;
bottom: 0px;
width: 50px;
height: 10px;
background: blue;
animation: ballbns 1s ease-in-out infinite alternate;
}
@keyframes ballbns {
0% { left: 0; transform: translateX(0%); }
100% { left: 100%; transform: translateX(-100%); }
}
`;
container.appendChild(style);
}
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import type { IControlWindow } from '../controlBar';
import type { Terminal } from '@xterm/xterm';
export class VtWindow implements IControlWindow {
public readonly id = 'vt';
public readonly label = 'VT';
private _container: HTMLElement;
private _term: Terminal | undefined;
public build(container: HTMLElement): void {
this._container = container;
const heading = document.createElement('h3');
heading.textContent = 'VT';
container.appendChild(heading);
const vtContainer = document.createElement('div');
vtContainer.id = 'vt-container';
container.appendChild(vtContainer);
}
public initTerminal(term: Terminal): void {
this._term = term;
this._addVtButtons();
}
private _addVtButtons(): void {
const vtContainer = this._container.querySelector('#vt-container');
if (!vtContainer) return;
const vtFragment = document.createDocumentFragment();
const buttonSpecs: { [key: string]: { label: string; description: string; paramCount?: number } } = {
'A': { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
'B': { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
'C': { label: 'CUF →', description: 'Cursor Forward Ps Times' },
'D': { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
'E': { label: 'CNL', description: 'Cursor Next Line Ps Times' },
'F': { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
'G': { label: 'CHA', description: 'Cursor Character Absolute' },
'H': { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
'I': { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
'J': { label: 'ED', description: 'Erase in Display' },
'?|J': { label: 'DECSED', description: 'Erase in Display' },
'K': { label: 'EL', description: 'Erase in Line' },
'?|K': { label: 'DECSEL', description: 'Erase in Line' },
'L': { label: 'IL', description: 'Insert Ps Line(s)' },
'M': { label: 'DL', description: 'Delete Ps Line(s)' },
'P': { label: 'DCH', description: 'Delete Ps Character(s)' },
' q': { label: 'DECSCUSR', description: 'Set Cursor Style' },
'?2026h': { label: 'BSU', description: 'Begin synchronized update', paramCount: 0 },
'?2026l': { label: 'ESU', description: 'End synchronized update', paramCount: 0 }
};
for (const s of Object.keys(buttonSpecs)) {
const spec = buttonSpecs[s];
vtFragment.appendChild(this._createButton(spec.label, spec.description, s, spec.paramCount));
}
vtContainer.appendChild(vtFragment);
}
private _createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement {
const inputs: HTMLInputElement[] = [];
for (let i = 0; i < paramCount; i++) {
const input = document.createElement('input');
input.type = 'number';
input.title = `Input #${i + 1}`;
inputs.push(input);
}
const element = document.createElement('button');
element.textContent = name;
const writeCsiSplit = writeCsi.split('|');
const prefix = writeCsiSplit.length === 2 ? writeCsiSplit[0] : '';
const suffix = writeCsiSplit[writeCsiSplit.length - 1];
element.addEventListener('click', () => this._term?.write(this._csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
const desc = document.createElement('span');
desc.textContent = description;
const container = document.createElement('div');
container.classList.add('vt-button');
container.append(element, ...inputs, desc);
return container;
}
private _csi(e: string): string {
return `\x1b[${e}`;
}
}
+1 -165
View File
@@ -14,28 +14,13 @@
<body>
<div id="banner">
<span class="banner-title">xterm.js</span>
<div class="banner-tabs">
<button id="optionsbutton" class="tabLinks" onclick="openSection(event, 'options')">Options</button>
<button id="addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
<button id="stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
<button id="testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
<button id="vtbutton" class="tabLinks" onclick="openSection(event, 'vt')">VT</button>
<button id="gpubutton" class="tabLinks" onclick="openSection(event, 'gpu')">GPU</button>
</div>
<div class="banner-tabs"></div>
</div>
<div id="container">
<div class="grid">
<div id="terminal-container"></div>
</div>
<div id="sidebar" class="grid">
<div id="sidebar-resize-handle-horizontal"></div>
<div id="sidebar-resize-handle-vertical"></div>
<div id="sidebar-resize-handle-corner"></div>
<div id="options" class="tabContent">
<h3>Options</h3>
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.options</code> property.</p>
<div id="options-container"></div>
</div>
<div id="addons" class="tabContent">
<h3>Addons</h3>
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
@@ -75,157 +60,8 @@
</div>
</details>
</div>
<div id="style" class="tabContent">
<h3>Style</h3>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
<div id="test" class="tabContent">
<h3>Test</h3>
<div style="display: inline-block; margin-right: 16px;">
<dl>
<dt>Lifecycle</dt>
<dd><label for="use-real-terminal"><input type="checkbox" checked id="use-real-terminal" title="This is used to real vs fake terminals" />Use real terminal</label></dd>
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
<dd><button id="create-new-window" title="This is used to test rendering in other windows">Create terminal in new window</button></dd>
<dt>Performance</dt>
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
<dd><button id="load-test-long-lines" title="Write several MB of data with long lines to simulate a lot of data coming from the process">Load test (long lines)</button></dd>
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
<dt>Styles</dt>
<dd><button id="custom-glyph-alignment" title="Write custom glyph alignment tests to the terminal">Custom glyph alignment test</button></dd>
<dd><button id="custom-glyph-ranges" title="Write custom glyph unicode range to the terminal">Custom glyph ranges</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
<dd><button id="bce" title="Test colored erase">Colored Erase (BCE)</button></dd>
<dd><button id="add-grapheme-clusters" title="Write grapheme cluster test strings">Grapheme clusters</button></dd>
<dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
<dt>Ligatures Addon</dt>
<dd><button id="ligatures-test" title="Write common ligatures sequences">Common ligatures</button></dd>
<dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
<dt>Image Test</dt>
<dd><button id="image-demo1">snake (sixel)</button></dd>
<dd><button id="image-demo2">oranges (sixel)</button></dd>
<dd><button id="image-demo3">palette (iip)</button></dd>
<dt>Events Test</dt>
<dd><button id="event-focus">focus</button></dd>
<dd><button id="event-blur">blur</button></dd>
<dt>Progress Addon</dt>
<dd><button id="progress-run">full set run</button></dd>
<dd><button id="progress-0">state 0: remove</button></dd>
<dd><button id="progress-1">state 1: set 20%</button></dd>
<dd><button id="progress-2">state 2: error</button></dd>
<dd><button id="progress-3">state 3: indeterminate</button></dd>
<dd><button id="progress-4">state 4: pause</button></dd>
<style>
#progress-progress {
border: 1px solid black;
height: 10px;
}
#progress-percent {
height: 100%;
}
#progress-indeterminate {
display: none;
position: relative;
height: 100%;
}
#progress-indeterminate:before {
content: '';
position: absolute;
left: 0;
bottom: 0px;
width: 50px;
height: 10px;
background: blue;
animation: ballbns 1s ease-in-out infinite alternate;
}
@keyframes ballbns {
0% { left: 0; transform: translateX(0%); }
100% { left: 100%; transform: translateX(-100%); }
}
</style>
<dd><div id="progress-progress">
<div id="progress-percent"></div>
<div id="progress-indeterminate"></div>
</div></dd>
<dd><div id="progress-state">State:</div></dd>
</dl>
</div>
</div>
<div id="vt" class="tabContent">
<h3>VT</h3>
<div id="vt-container"></div>
</div>
<div id="gpu" class="tabContent">
<h3>GPU</h3>
<input type="checkbox" id="texture-atlas-zoom"/>
<label for="texture-atlas-zoom">Zoom texture atlas</label>
<div id="texture-atlas"></div>
</div>
</div>
</div>
<script src="dist/client-bundle.js" defer ></script>
<script>
var tab = localStorage.getItem("tab");
if(tab === null){
document.getElementById("options").style.display = "block";
document.getElementById("optionsbutton").classList.add("active");
}
else {
const tabContent = document.getElementsByClassName("tabContent");
let itr;
for (itr = 0; itr < tabContent.length; itr+=1) {
tabContent[itr].style.display = "none";
}
document.getElementById(""+tab+"").style.display = "block";
document.getElementById(""+tab+"button").classList.add("active");
}
function openSection(event, section) {
const sidebar = document.getElementById("sidebar");
const tabContent = document.getElementsByClassName("tabContent");
const tabLinks = document.getElementsByClassName("tabLinks");
let itr;
// If clicking active tab, toggle sidebar visibility
if (event.currentTarget.classList.contains("active")) {
sidebar.classList.toggle("sidebar-hidden");
return;
}
// Show sidebar if hidden
sidebar.classList.remove("sidebar-hidden");
for (itr = 0; itr < tabContent.length; itr+=1) {
tabContent[itr].style.display = "none";
}
for (itr = 0; itr < tabLinks.length; itr+=1) {
tabLinks[itr].className = tabLinks[itr].className.replace(" active", "");
}
document.getElementById(section).style.display = "block";
localStorage.setItem("tab", section);
event.currentTarget.className += " active";
}
</script>
</body>
</html>