diff --git a/static/base.css b/static/base.css index 6fc9acd..6a9ede5 100644 --- a/static/base.css +++ b/static/base.css @@ -18,9 +18,6 @@ --font-monospace: "Iosevka Hyperlegible", monospace; --line-height: 1.5; --line-height-em: 1.5em; - - --z-resize-handle: 50; - --z-modal: 100; } /* Reset */ @@ -95,10 +92,6 @@ button.icon { input { border: none; border-bottom: 1px solid var(--color-panel-border); - - &:hover:not(:focus) { - background-color: var(--color-shaded-background); - } } *:focus-visible { @@ -108,23 +101,14 @@ 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 */ @@ -233,6 +217,8 @@ 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 3470c94..1c2bc92 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -132,6 +132,8 @@ 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 ad97df1..8e4f611 100644 --- a/static/brush-editor.js +++ b/static/brush-editor.js @@ -1,9 +1,6 @@ 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"); @@ -13,14 +10,13 @@ export class BrushEditor extends HTMLElement { } connectedCallback() { + this.classList.add("rkgk-panel"); + this.saveData.attachToElement(this); const defaultBrush = builtInPresets[0]; - this.editorContainer = this.appendChild(document.createElement("div")); - this.editorContainer.classList.add("editor"); - - this.nameEditor = this.editorContainer.appendChild(document.createElement("input")); + this.nameEditor = this.appendChild(document.createElement("input")); this.nameEditor.value = this.saveData.get("name", defaultBrush.name); this.nameEditor.classList.add("name"); this.nameEditor.addEventListener("input", () => { @@ -35,7 +31,7 @@ export class BrushEditor extends HTMLElement { ); }); - this.codeEditor = this.editorContainer.appendChild( + this.codeEditor = this.appendChild( new CodeEditor([ { className: "layer-syntax", @@ -63,28 +59,11 @@ export class BrushEditor extends HTMLElement { ); }); - this.output = document.createElement("output"); + this.errorHeader = this.appendChild(document.createElement("h1")); + this.errorHeader.classList.add("error-header"); - 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"); + this.errorArea = this.appendChild(document.createElement("pre")); + this.errorArea.classList.add("errors"); // NOTE(localStorage): Migration from old storage key. localStorage.removeItem("rkgk.brushEditor.code"); @@ -108,19 +87,11 @@ export class BrushEditor extends HTMLElement { } resetErrors() { - this.output.dataset.state = "ok"; - this.errors.textContent = ""; + this.errorHeader.textContent = ""; + this.errorArea.textContent = ""; } - 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) { + renderHakuResult(phase, result) { this.resetErrors(); this.errorSquiggles = null; @@ -131,7 +102,7 @@ export class BrushEditor extends HTMLElement { return; } - this.output.dataset.state = "error"; + this.errorHeader.textContent = `${phase} failed`; if (result.errorKind == "diagnostics") { this.codeEditor.rebuildLineMap(); @@ -141,13 +112,13 @@ export class BrushEditor extends HTMLElement { ); this.codeEditor.renderLayer("layer-error-squiggles"); - this.errors.textContent = result.diagnostics + this.errorArea.textContent = result.diagnostics .map( (diagnostic) => `${diagnostic.start}..${diagnostic.end}: ${diagnostic.message}`, ) .join("\n"); } else if (result.errorKind == "plain") { - this.errors.textContent = result.message; + this.errorHeader.textContent = result.message; } else if (result.errorKind == "exception") { let renderer = new ErrorException(result); let squiggles = renderer.prepareSquiggles(); @@ -173,11 +144,11 @@ export class BrushEditor extends HTMLElement { this.codeEditor.setSelection(new Selection(span.start, span.end)); }); - this.errors.replaceChildren(); - this.errors.appendChild(renderer); + this.errorArea.replaceChildren(); + this.errorArea.appendChild(renderer); } else { console.warn(`unknown error kind: ${result.errorKind}`); - this.errors.textContent = "(unknown error kind)"; + this.errorHeader.textContent = "(unknown error kind)"; } } diff --git a/static/brush-preview.js b/static/brush-preview.js index 97a2015..1697415 100644 --- a/static/brush-preview.js +++ b/static/brush-preview.js @@ -13,24 +13,16 @@ 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) { @@ -39,9 +31,9 @@ export class BrushPreview extends HTMLElement { runDotter: async () => { return { fromX: this.canvas.width / 2, - fromY: this.canvas.height / 2, + fromY: this.canvas.width / 2, toX: this.canvas.width / 2, - toY: this.canvas.height / 2, + toY: this.canvas.width / 2, num: 0, }; }, diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 6b5821b..95d07e7 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -451,10 +451,10 @@ class CanvasRenderer extends HTMLElement { async #cursorReportingBehaviour() { while (true) { - let event = await listen([window, "mousemove"]); + let event = await listen([this, "mousemove"]); let [x, y] = this.viewport.toViewportSpace( event.clientX - this.clientLeft, - event.clientY - this.clientTop, + event.offsetY - this.clientTop, this.getWindowSize(), ); this.dispatchEvent(Object.assign(new Event(".cursor"), { x, y })); @@ -493,7 +493,10 @@ class InteractEvent extends Event { continueAsDotter() { (async () => { - let event = await listen([window, "mousemove"], [window, "mouseup"]); + let event = await listen( + [this.canvasRenderer, "mousemove"], + [this.canvasRenderer, "mouseup"], + ); if (event.type == "mousemove") { let [mouseX, mouseY] = this.canvasRenderer.viewport.toViewportSpace( diff --git a/static/code-editor.js b/static/code-editor.js index e71d614..b0197f1 100644 --- a/static/code-editor.js +++ b/static/code-editor.js @@ -44,9 +44,6 @@ export class CodeEditor extends HTMLElement { this.undoHistory = []; this.undoHistoryTop = 0; - this.textArea.addEventListener("input", () => { - this.#codeChanged(); - }); this.#textAreaAutoSizingBehaviour(); this.#keyShortcutBehaviours(); @@ -79,6 +76,9 @@ 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 ed382e1..cf18f65 100644 --- a/static/framework.js +++ b/static/framework.js @@ -65,7 +65,7 @@ export class SaveData { } attachToElement(element) { - this.elementId = element.id; + this.elementId = element.dataset.storageId; } getRaw(key) { diff --git a/static/index.css b/static/index.css index 40fc17d..4ff0072 100644 --- a/static/index.css +++ b/static/index.css @@ -44,6 +44,9 @@ 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, @@ -60,9 +63,9 @@ main { & > .left { display: flex; flex-direction: column; - padding: 16px; pointer-events: none; + & > * { pointer-events: auto; } @@ -77,8 +80,8 @@ main { min-height: 0; display: grid; - grid-template-rows: 100%; - grid-template-columns: [resize] min-content [docked] auto; + grid-template-rows: minmax(0, min-content); + grid-template-columns: [floating] max-content [resize] min-content [docked] auto; padding-left: 16px; @@ -89,14 +92,29 @@ 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; @@ -108,35 +126,6 @@ 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); - } - } } } @@ -159,7 +148,6 @@ main { & > #js-loading { background-color: var(--color-panel-background); - z-index: var(--z-modal); display: flex; align-items: center; @@ -170,54 +158,47 @@ main { /* Resize handle */ rkgk-resize-handle { - --width: 8px; - --line: none; + --width: 16px; + --line-width: 2px; 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: 0; - border-bottom: var(--line); + height: var(--line-width); } } &[data-direction="vertical"] { width: var(--width); height: 100%; - margin: 0 calc(var(--width) / -2); - flex-direction: row; cursor: col-resize; & > .visual { - width: 0; + width: var(--line-width); height: 100%; - border-left: var(--line); } } + & > .visual { + background-color: var(--color-brand-blue); + opacity: 0%; + } + &:hover > .visual, &.dragging > .visual { - --line: 2px solid var(--color-brand-blue); + opacity: 100%; } } @@ -280,15 +261,12 @@ rkgk-reticle-cursor { /* Brush box */ -rkgk-brush-box { +rkgk-brush-box.rkgk-panel { --button-size: 56px; height: var(--height); padding: 12px; - overflow-x: hidden; - overflow-y: auto; - & > .brushes { display: grid; grid-template-columns: repeat( @@ -297,6 +275,8 @@ rkgk-brush-box { ); max-height: 100%; + overflow-x: hidden; + overflow-y: auto; & > button { padding: 0; @@ -357,20 +337,20 @@ rkgk-brush-box { /* Code editor */ rkgk-code-editor { - --gutter-width: 3.5em; - --vertical-padding: 12px; + --gutter-width: 2.75em; display: block; position: relative; + width: 100%; - padding: var(--vertical-padding) 0; overflow: auto; & > .layer { position: absolute; left: 0; - top: var(--vertical-padding); + top: 0; width: 100%; + height: 100%; box-sizing: border-box; margin: 0; @@ -488,70 +468,58 @@ rkgk-code-editor { &:has(textarea:focus) { outline: 1px solid var(--color-brand-blue); - outline-offset: -1px; + outline-offset: 4px; } } /* Brush editor */ -rkgk-brush-editor { +rkgk-brush-editor.rkgk-panel { + padding: 12px; + display: flex; flex-direction: column; - - min-height: 0; + gap: 4px; position: relative; - & > .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; - } + & > .name { + margin-bottom: 4px; + font-weight: bold; } - & > output { - height: 64px; - flex-grow: 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; user-select: text; - &[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; - } + max-height: 20em; + overflow-y: auto; } } @@ -626,6 +594,7 @@ rkgk-brush-preview { var(--checkerboard-dark) 0% 50% ) 50% 50% / var(--checkerboard-size) var(--checkerboard-size); + border-radius: 4px; & > canvas { display: block; @@ -656,20 +625,26 @@ rkgk-brush-preview { /* Brush cost gauges */ -rkgk-brush-cost-gauges { +rkgk-brush-cost-gauges, +rkgk-brush-cost-gauges.rkgk-panel { --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; - width: var(--gauge-size); - height: 100%; + height: var(--gauge-size); + flex-grow: 1; &.hidden { display: none; @@ -679,7 +654,7 @@ rkgk-brush-cost-gauges { width: 100%; height: 100%; - clip-path: inset(calc(100% - var(--progress)) 0 0 0); + clip-path: xywh(0 0 var(--progress) 100%); background-color: var(--gauge-color); } @@ -697,10 +672,6 @@ rkgk-brush-cost-gauges { --gauge-color: #5aca40; } } - - & .icon { - background-position: 50% calc(100% - 4px); - } } /* Zoom indicator */ @@ -818,38 +789,21 @@ rkgk-context-menu.rkgk-panel { /* Menu bar */ .menu-bar { + --border-radius: 4px; + display: flex; align-items: center; box-sizing: border-box; - width: 100%; - height: 28px; + width: fit-content; + height: 24px; + border-radius: var(--border-radius); - margin: 0; - padding: 0; - list-style: none; - - & > li { - display: flex; - flex-direction: row; - align-items: center; - height: 100%; - } - - & > li.icon { + & > a { display: block; - width: 28px; - height: 28px; - } - - & > li > a { - display: flex; - flex-direction: row; - align-items: center; color: var(--color-text); - height: 100%; - padding: 0 8px; + padding: 4px 8px; text-decoration: none; line-height: 1; @@ -862,6 +816,16 @@ 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 3bcb378..4fb9f78 100644 --- a/static/index.js +++ b/static/index.js @@ -18,6 +18,8 @@ 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"); @@ -251,7 +253,7 @@ function readUrl(urlString) { let result = await currentUser.haku.evalBrush( selfController(interactionQueue, wall, event), ); - brushEditor.renderHakuResult(result); + brushEditor.renderHakuResult(result.phase == "eval" ? "Evaluation" : "Rendering", result); }); canvasRenderer.addEventListener(".viewportUpdate", () => reticleRenderer.render()); @@ -261,19 +263,26 @@ 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(compileResult); + brushEditor.renderHakuResult("Compilation", compileResult); - if (compileResult.status == "ok") { - updateBrushPreview(); + brushCostGauges.update(currentUser.getStats(session.wallInfo)); + + if (compileResult.status != "ok") { + brushPreview.setErrorFlag(); + return; } + + 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(); @@ -285,8 +294,6 @@ 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 9a15fda..b441be5 100644 --- a/static/resize-handle.js +++ b/static/resize-handle.js @@ -3,31 +3,13 @@ import { listen, SaveData } from "rkgk/framework.js"; export class ResizeHandle extends HTMLElement { saveData = new SaveData("resizeHandle"); - constructor(props) { - super(); - this.props = props; - } - connectedCallback() { - 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.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")); this.saveData.elementId = this.targetId; @@ -76,10 +58,8 @@ export class ResizeHandle extends HTMLElement { if (event.type == "mousemove") { let delta = this.direction == "vertical" - ? event.clientX - mouseDown.clientX + ? mouseDown.clientX - event.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 9ff2603..fa55672 100644 --- a/template/index.hbs.html +++ b/template/index.hbs.html @@ -7,6 +7,8 @@