From 0ddc42c00f35253c5a7aa8c878cf0602d1b5c521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Fri, 27 Jun 2025 23:24:09 +0200 Subject: [PATCH] sidebar layout switch the app from floating panels to a static sidebar on the right with resizable tools expect more layout bugs from now on --- static/base.css | 26 ++++- static/brush-box.js | 2 - static/brush-editor.js | 63 +++++++--- static/brush-preview.js | 12 +- static/code-editor.js | 6 +- static/framework.js | 2 +- static/index.css | 246 +++++++++++++++++++++++----------------- static/index.js | 31 ++--- static/resize-handle.js | 34 ++++-- template/index.hbs.html | 28 ++--- 10 files changed, 272 insertions(+), 178 deletions(-) diff --git a/static/base.css b/static/base.css index 6a9ede5..6fc9acd 100644 --- a/static/base.css +++ b/static/base.css @@ -18,6 +18,9 @@ --font-monospace: "Iosevka Hyperlegible", monospace; --line-height: 1.5; --line-height-em: 1.5em; + + --z-resize-handle: 50; + --z-modal: 100; } /* Reset */ @@ -92,6 +95,10 @@ button.icon { input { border: none; border-bottom: 1px solid var(--color-panel-border); + + &:hover:not(:focus) { + background-color: var(--color-shaded-background); + } } *:focus-visible { @@ -101,14 +108,23 @@ input { /* Modal dialogs */ +dialog { + &:not([open]) { + /* Default to dialogs being invisible. + Otherwise dialogs placed on the page via HTML display on top of everything. */ + display: none; + } + + &::backdrop { + background-color: var(--dialog-backdrop); + backdrop-filter: blur(8px); + } +} + dialog:not([open]) { - /* Weird this doesn't seem to work by default. */ - display: none; } dialog::backdrop { - background-color: var(--dialog-backdrop); - backdrop-filter: blur(8px); } /* Details */ @@ -217,8 +233,6 @@ abbr { .icon { vertical-align: middle; - width: 16px; - height: 16px; background-repeat: no-repeat; background-position: 50% 50%; diff --git a/static/brush-box.js b/static/brush-box.js index 1c2bc92..3470c94 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -132,8 +132,6 @@ export class BrushBox extends HTMLElement { connectedCallback() { this.saveData.attachToElement(this); - this.classList.add("rkgk-panel"); - this.brushes = []; this.brushesContainer = this.appendChild(document.createElement("div")); diff --git a/static/brush-editor.js b/static/brush-editor.js index 8e4f611..ad97df1 100644 --- a/static/brush-editor.js +++ b/static/brush-editor.js @@ -1,6 +1,9 @@ import { CodeEditor, Selection } from "rkgk/code-editor.js"; import { SaveData } from "rkgk/framework.js"; import { builtInPresets } from "rkgk/brush-box.js"; +import { ResizeHandle } from "rkgk/resize-handle.js"; +import { BrushPreview } from "rkgk/brush-preview.js"; +import { BrushCostGauges } from "rkgk/brush-cost.js"; export class BrushEditor extends HTMLElement { saveData = new SaveData("brushEditor"); @@ -10,13 +13,14 @@ export class BrushEditor extends HTMLElement { } connectedCallback() { - this.classList.add("rkgk-panel"); - this.saveData.attachToElement(this); const defaultBrush = builtInPresets[0]; - this.nameEditor = this.appendChild(document.createElement("input")); + this.editorContainer = this.appendChild(document.createElement("div")); + this.editorContainer.classList.add("editor"); + + this.nameEditor = this.editorContainer.appendChild(document.createElement("input")); this.nameEditor.value = this.saveData.get("name", defaultBrush.name); this.nameEditor.classList.add("name"); this.nameEditor.addEventListener("input", () => { @@ -31,7 +35,7 @@ export class BrushEditor extends HTMLElement { ); }); - this.codeEditor = this.appendChild( + this.codeEditor = this.editorContainer.appendChild( new CodeEditor([ { className: "layer-syntax", @@ -59,11 +63,28 @@ export class BrushEditor extends HTMLElement { ); }); - this.errorHeader = this.appendChild(document.createElement("h1")); - this.errorHeader.classList.add("error-header"); + this.output = document.createElement("output"); - this.errorArea = this.appendChild(document.createElement("pre")); - this.errorArea.classList.add("errors"); + this.appendChild( + new ResizeHandle({ + direction: "horizontal", + inverse: true, + targetElement: this, + targetProperty: "--brush-preview-height", + initSize: 192, + minSize: 64, + }), + ).classList.add("always-visible"); + + this.appendChild(this.output); + + this.ok = this.output.appendChild(document.createElement("div")); + this.ok.classList.add("ok"); + this.brushPreview = this.ok.appendChild(new BrushPreview()); + this.brushCostGauges = this.ok.appendChild(new BrushCostGauges()); + + this.errors = this.output.appendChild(document.createElement("pre")); + this.errors.classList.add("errors"); // NOTE(localStorage): Migration from old storage key. localStorage.removeItem("rkgk.brushEditor.code"); @@ -87,11 +108,19 @@ export class BrushEditor extends HTMLElement { } resetErrors() { - this.errorHeader.textContent = ""; - this.errorArea.textContent = ""; + this.output.dataset.state = "ok"; + this.errors.textContent = ""; } - renderHakuResult(phase, result) { + async updatePreview(haku, { getStats }) { + let previewResult = await this.brushPreview.renderBrush(haku); + this.brushCostGauges.update(getStats()); + if (previewResult.status == "error") { + this.renderHakuResult(previewResult.result); + } + } + + renderHakuResult(result) { this.resetErrors(); this.errorSquiggles = null; @@ -102,7 +131,7 @@ export class BrushEditor extends HTMLElement { return; } - this.errorHeader.textContent = `${phase} failed`; + this.output.dataset.state = "error"; if (result.errorKind == "diagnostics") { this.codeEditor.rebuildLineMap(); @@ -112,13 +141,13 @@ export class BrushEditor extends HTMLElement { ); this.codeEditor.renderLayer("layer-error-squiggles"); - this.errorArea.textContent = result.diagnostics + this.errors.textContent = result.diagnostics .map( (diagnostic) => `${diagnostic.start}..${diagnostic.end}: ${diagnostic.message}`, ) .join("\n"); } else if (result.errorKind == "plain") { - this.errorHeader.textContent = result.message; + this.errors.textContent = result.message; } else if (result.errorKind == "exception") { let renderer = new ErrorException(result); let squiggles = renderer.prepareSquiggles(); @@ -144,11 +173,11 @@ export class BrushEditor extends HTMLElement { this.codeEditor.setSelection(new Selection(span.start, span.end)); }); - this.errorArea.replaceChildren(); - this.errorArea.appendChild(renderer); + this.errors.replaceChildren(); + this.errors.appendChild(renderer); } else { console.warn(`unknown error kind: ${result.errorKind}`); - this.errorHeader.textContent = "(unknown error kind)"; + this.errors.textContent = "(unknown error kind)"; } } diff --git a/static/brush-preview.js b/static/brush-preview.js index 1697415..97a2015 100644 --- a/static/brush-preview.js +++ b/static/brush-preview.js @@ -13,16 +13,24 @@ export class BrushPreview extends HTMLElement { this.ctx = this.canvas.getContext("2d"); this.#resizeCanvas(); + if (this.width == null || this.height == null) { + new ResizeObserver(() => this.#resizeCanvas()).observe(this); + } } #resizeCanvas() { this.canvas.width = this.width ?? this.clientWidth; this.canvas.height = this.height ?? this.clientHeight; + // This can happen if the element's `display: none`. + if (this.canvas.width == 0 || this.canvas.height == 0) return; + if (this.pixmap != null) { this.pixmap.destroy(); } this.pixmap = new Pixmap(this.canvas.width, this.canvas.height); + + this.dispatchEvent(new Event(".pixmapLost")); } async #renderBrushInner(haku) { @@ -31,9 +39,9 @@ export class BrushPreview extends HTMLElement { runDotter: async () => { return { fromX: this.canvas.width / 2, - fromY: this.canvas.width / 2, + fromY: this.canvas.height / 2, toX: this.canvas.width / 2, - toY: this.canvas.width / 2, + toY: this.canvas.height / 2, num: 0, }; }, diff --git a/static/code-editor.js b/static/code-editor.js index b0197f1..e71d614 100644 --- a/static/code-editor.js +++ b/static/code-editor.js @@ -44,6 +44,9 @@ export class CodeEditor extends HTMLElement { this.undoHistory = []; this.undoHistoryTop = 0; + this.textArea.addEventListener("input", () => { + this.#codeChanged(); + }); this.#textAreaAutoSizingBehaviour(); this.#keyShortcutBehaviours(); @@ -76,9 +79,6 @@ export class CodeEditor extends HTMLElement { // Resizing the text area #textAreaAutoSizingBehaviour() { - this.textArea.addEventListener("input", () => { - this.#codeChanged(); - }); this.#resizeTextArea(); document.fonts.addEventListener("loadingdone", () => this.#resizeTextArea()); new ResizeObserver(() => this.#resizeTextArea()).observe(this.textArea); diff --git a/static/framework.js b/static/framework.js index cf18f65..ed382e1 100644 --- a/static/framework.js +++ b/static/framework.js @@ -65,7 +65,7 @@ export class SaveData { } attachToElement(element) { - this.elementId = element.dataset.storageId; + this.elementId = element.id; } getRaw(key) { diff --git a/static/index.css b/static/index.css index 4ff0072..40fc17d 100644 --- a/static/index.css +++ b/static/index.css @@ -44,9 +44,6 @@ main { & > .panels { --right-width: 384px; /* Overridden by JavaScript */ - box-sizing: border-box; - padding: 16px; - display: grid; grid-template-columns: [left] 1fr [right-resize] auto [right] minmax( 0, @@ -63,9 +60,9 @@ main { & > .left { display: flex; flex-direction: column; + padding: 16px; pointer-events: none; - & > * { pointer-events: auto; } @@ -80,8 +77,8 @@ main { min-height: 0; display: grid; - grid-template-rows: minmax(0, min-content); - grid-template-columns: [floating] max-content [resize] min-content [docked] auto; + grid-template-rows: 100%; + grid-template-columns: [resize] min-content [docked] auto; padding-left: 16px; @@ -92,29 +89,14 @@ main { min-height: 0; } - & > rkgk-resize-handle { - pointer-events: auto; - } - - & > .docked { - display: flex; - flex-direction: column; - - & > * { - pointer-events: auto; - } - } - - & > .docked > rkgk-brush-editor { - max-height: 100%; - } - & > .floating { display: flex; flex-direction: column; gap: 12px; + padding: 16px; + & > rkgk-brush-preview { width: 128px; height: 128px; @@ -126,6 +108,35 @@ main { pointer-events: auto; } } + + & > rkgk-resize-handle { + pointer-events: auto; + } + + & > .docked { + display: flex; + flex-direction: column; + height: 100%; + max-height: 100%; + + background-color: var(--color-panel-background); + box-shadow: 0 0 0 1px var(--color-panel-border); + + pointer-events: auto; + + & > * { + flex-shrink: 0; + } + + & > rkgk-brush-editor { + flex-grow: 1; + flex-shrink: 1; + } + + & > .menu-bar { + border-bottom: 1px solid var(--color-panel-border); + } + } } } @@ -148,6 +159,7 @@ main { & > #js-loading { background-color: var(--color-panel-background); + z-index: var(--z-modal); display: flex; align-items: center; @@ -158,47 +170,54 @@ main { /* Resize handle */ rkgk-resize-handle { - --width: 16px; - --line-width: 2px; + --width: 8px; + --line: none; display: flex; justify-content: center; flex-shrink: 0; + z-index: var(--z-resize-handle); + + &.always-visible { + --line: 1px solid var(--color-panel-border); + } + &[data-direction="horizontal"] { width: 100%; height: var(--width); + margin: calc(var(--width) / -2) 0; + flex-direction: column; cursor: row-resize; & > .visual { width: 100%; - height: var(--line-width); + height: 0; + border-bottom: var(--line); } } &[data-direction="vertical"] { width: var(--width); height: 100%; + margin: 0 calc(var(--width) / -2); + flex-direction: row; cursor: col-resize; & > .visual { - width: var(--line-width); + width: 0; height: 100%; + border-left: var(--line); } } - & > .visual { - background-color: var(--color-brand-blue); - opacity: 0%; - } - &:hover > .visual, &.dragging > .visual { - opacity: 100%; + --line: 2px solid var(--color-brand-blue); } } @@ -261,12 +280,15 @@ rkgk-reticle-cursor { /* Brush box */ -rkgk-brush-box.rkgk-panel { +rkgk-brush-box { --button-size: 56px; height: var(--height); padding: 12px; + overflow-x: hidden; + overflow-y: auto; + & > .brushes { display: grid; grid-template-columns: repeat( @@ -275,8 +297,6 @@ rkgk-brush-box.rkgk-panel { ); max-height: 100%; - overflow-x: hidden; - overflow-y: auto; & > button { padding: 0; @@ -337,20 +357,20 @@ rkgk-brush-box.rkgk-panel { /* Code editor */ rkgk-code-editor { - --gutter-width: 2.75em; + --gutter-width: 3.5em; + --vertical-padding: 12px; display: block; position: relative; - width: 100%; + padding: var(--vertical-padding) 0; overflow: auto; & > .layer { position: absolute; left: 0; - top: 0; + top: var(--vertical-padding); width: 100%; - height: 100%; box-sizing: border-box; margin: 0; @@ -468,58 +488,70 @@ rkgk-code-editor { &:has(textarea:focus) { outline: 1px solid var(--color-brand-blue); - outline-offset: 4px; + outline-offset: -1px; } } /* Brush editor */ -rkgk-brush-editor.rkgk-panel { - padding: 12px; - +rkgk-brush-editor { display: flex; flex-direction: column; - gap: 4px; + + min-height: 0; position: relative; - & > .name { - margin-bottom: 4px; - font-weight: bold; + & > .editor { + display: flex; + flex-direction: column; + + height: calc(100% - var(--brush-preview-height)); + + & > .name { + margin: 12px; + margin-bottom: 0; + font-weight: bold; + } + + & > rkgk-code-editor { + height: 100%; + flex-shrink: 1; + } } - & > .text-area { - display: block; - width: 100%; - margin: 0; - resize: none; - white-space: pre-wrap; - border: none; - overflow: hidden; - box-sizing: border-box; - } - - & > .errors:empty, - & > .error-header:empty { - display: none; - } - - & > .error-header { - margin: 0; - margin-top: 0.5em; - font-size: 1rem; - color: var(--color-error); - } - - & > .errors { - margin: 0; - color: var(--color-error); - white-space: pre-wrap; + & > output { + height: 64px; + flex-grow: 1; user-select: text; - max-height: 20em; - overflow-y: auto; + &[data-state="ok"] > .errors { + display: none; + } + &[data-state="error"] > .ok { + display: none; + } + + & > .ok { + display: flex; + flex-direction: row; + + height: 100%; + + & > rkgk-brush-preview { + flex-grow: 1; + } + } + + & > .errors { + margin: 0; + color: var(--color-error); + white-space: pre-wrap; + + max-height: 20em; + overflow-y: auto; + } } } @@ -594,7 +626,6 @@ rkgk-brush-preview { var(--checkerboard-dark) 0% 50% ) 50% 50% / var(--checkerboard-size) var(--checkerboard-size); - border-radius: 4px; & > canvas { display: block; @@ -625,26 +656,20 @@ rkgk-brush-preview { /* Brush cost gauges */ -rkgk-brush-cost-gauges, -rkgk-brush-cost-gauges.rkgk-panel { +rkgk-brush-cost-gauges { --gauge-size: 20px; - height: var(--gauge-size); - border-radius: 4px; - display: flex; flex-direction: row; - overflow: clip; /* clip corners */ - &.hidden { display: none; } & > rkgk-brush-cost-gauge { display: block; - height: var(--gauge-size); - flex-grow: 1; + width: var(--gauge-size); + height: 100%; &.hidden { display: none; @@ -654,7 +679,7 @@ rkgk-brush-cost-gauges.rkgk-panel { width: 100%; height: 100%; - clip-path: xywh(0 0 var(--progress) 100%); + clip-path: inset(calc(100% - var(--progress)) 0 0 0); background-color: var(--gauge-color); } @@ -672,6 +697,10 @@ rkgk-brush-cost-gauges.rkgk-panel { --gauge-color: #5aca40; } } + + & .icon { + background-position: 50% calc(100% - 4px); + } } /* Zoom indicator */ @@ -789,21 +818,38 @@ rkgk-context-menu.rkgk-panel { /* Menu bar */ .menu-bar { - --border-radius: 4px; - display: flex; align-items: center; box-sizing: border-box; - width: fit-content; - height: 24px; - border-radius: var(--border-radius); + width: 100%; + height: 28px; - & > a { + margin: 0; + padding: 0; + list-style: none; + + & > li { + display: flex; + flex-direction: row; + align-items: center; + height: 100%; + } + + & > li.icon { display: block; + width: 28px; + height: 28px; + } + + & > li > a { + display: flex; + flex-direction: row; + align-items: center; color: var(--color-text); - padding: 4px 8px; + height: 100%; + padding: 0 8px; text-decoration: none; line-height: 1; @@ -816,16 +862,6 @@ rkgk-context-menu.rkgk-panel { width: 24px; height: 24px; } - - &:first-child { - border-top-left-radius: var(--border-radius); - border-bottom-left-radius: var(--border-radius); - } - - &:last-child { - border-top-right-radius: var(--border-radius); - border-bottom-right-radius: var(--border-radius); - } } & > hr { diff --git a/static/index.js b/static/index.js index 4fb9f78..3bcb378 100644 --- a/static/index.js +++ b/static/index.js @@ -18,8 +18,6 @@ let canvasRenderer = main.querySelector("rkgk-canvas-renderer"); let reticleRenderer = main.querySelector("rkgk-reticle-renderer"); let brushBox = main.querySelector("rkgk-brush-box"); let brushEditor = main.querySelector("rkgk-brush-editor"); -let brushPreview = main.querySelector("rkgk-brush-preview"); -let brushCostGauges = main.querySelector("rkgk-brush-cost-gauges"); let zoomIndicator = main.querySelector("rkgk-zoom-indicator"); let welcome = main.querySelector("rkgk-welcome"); let connectionStatus = main.querySelector("rkgk-connection-status"); @@ -253,7 +251,7 @@ function readUrl(urlString) { let result = await currentUser.haku.evalBrush( selfController(interactionQueue, wall, event), ); - brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result); + brushEditor.renderHakuResult(result); }); canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render()); @@ -263,26 +261,19 @@ function readUrl(urlString) { // Brush editor + function updateBrushPreview() { + brushEditor.updatePreview(currentUser.haku, { + getStats: () => currentUser.getStats(session.wallInfo), + }); + } + function compileBrush() { let compileResult = currentUser.setBrush(brushEditor.code); - brushEditor.renderHakuResult("Compilation", compileResult); + brushEditor.renderHakuResult(compileResult); - brushCostGauges.update(currentUser.getStats(session.wallInfo)); - - if (compileResult.status != "ok") { - brushPreview.setErrorFlag(); - return; + if (compileResult.status == "ok") { + updateBrushPreview(); } - - brushPreview.renderBrush(currentUser.haku).then((previewResult) => { - brushCostGauges.update(currentUser.getStats(session.wallInfo)); - if (previewResult.status == "error") { - brushEditor.renderHakuResult( - previewResult.phase == "eval" ? "Evaluation" : "Rendering", - previewResult.result, - ); - } - }); } compileBrush(); @@ -294,6 +285,8 @@ function readUrl(urlString) { }); }); + brushEditor.brushPreview.addEventListener(".pixmapLost", updateBrushPreview); + // Brush box function updateBrushBoxDirtyState() { diff --git a/static/resize-handle.js b/static/resize-handle.js index b441be5..9a15fda 100644 --- a/static/resize-handle.js +++ b/static/resize-handle.js @@ -3,13 +3,31 @@ import { listen, SaveData } from "rkgk/framework.js"; export class ResizeHandle extends HTMLElement { saveData = new SaveData("resizeHandle"); + constructor(props) { + super(); + this.props = props; + } + connectedCallback() { - this.direction = this.getAttribute("data-direction"); - this.targetId = this.getAttribute("data-target"); - this.target = document.getElementById(this.targetId); - this.targetProperty = this.getAttribute("data-target-property"); - this.initSize = parseInt(this.getAttribute("data-init-size")); - this.minSize = parseInt(this.getAttribute("data-min-size")); + let props = this.props ?? this.dataset; + + this.direction = this.dataset.direction = props.direction; + this.targetProperty = props.targetProperty; + this.initSize = parseInt(props.initSize); + this.minSize = parseInt(props.minSize); + this.inverse = props.inverse != null; + + if (props.targetElement != null) { + // In case you want to construct the resize handle programatically: + // pass in the target element via targetElement. + // Don't forget to set its id. + this.target = props.targetElement; + this.targetId = this.target.id; + } else { + // Else use data-target. + this.targetId = props.target; + this.target = document.getElementById(this.targetId); + } this.saveData.elementId = this.targetId; @@ -58,8 +76,10 @@ export class ResizeHandle extends HTMLElement { if (event.type == "mousemove") { let delta = this.direction == "vertical" - ? mouseDown.clientX - event.clientX + ? event.clientX - mouseDown.clientX : event.clientY - mouseDown.clientY; + if (this.inverse) delta = -delta; + this.#setSize(startingSize + delta); this.#updateTargetProperty(); } else if (event.type == "mouseup") { diff --git a/template/index.hbs.html b/template/index.hbs.html index 75d88a5..9ff2603 100644 --- a/template/index.hbs.html +++ b/template/index.hbs.html @@ -70,39 +70,35 @@
- -
-
- - -
+ data-min-size="256">
- + +
  • +
    +
  • Manual
  • +
    + + + - +