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 @@