summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rw-r--r--src/web/app.mjs106
1 files changed, 92 insertions, 14 deletions
diff --git a/src/web/app.mjs b/src/web/app.mjs
index 37d07eb7..8e08c9d4 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -74,10 +74,71 @@ export class DomRenderer {
}
raw[index] = { text, fg, bg, attrs };
}
+ this.renderBodyLayers();
this.snapshot = { cols, rows, cells: raw, cursorX, cursorY, cursorBar };
return this.snapshot;
}
+ renderBodyLayers() {
+ if (!this.wasm.pardes_body_layer_limit || !this.cellWidth) return;
+ if (!this.layerRoot?.isConnected) {
+ this.layerRoot = this.root.ownerDocument.createElement("div");
+ this.layerRoot.style.cssText = "position:absolute;inset:0;pointer-events:none;overflow:hidden";
+ this.root.style.position = "relative";
+ this.root.append(this.layerRoot);
+ }
+ const nodes = [];
+ this.layerNodes ??= new Map();
+ const bw = this.cellWidth, bh = this.cellHeight;
+ const tw = this.taglineWidth, th = this.taglineHeight;
+ for (let i = 0; i < this.wasm.pardes_body_layer_limit(); i++) {
+ const val = (field) => this.wasm.pardes_body_layer_value(i, field);
+ const rows = val(0), cols = val(1), contexts = val(2);
+ if (!rows) continue;
+ const ptr = this.wasm.pardes_body_layer_cells(i);
+ if (!ptr) continue;
+ const view = new DataView(this.wasm.memory.buffer, ptr, rows * cols * CELL_SIZE);
+ const bytes = new Uint8Array(this.wasm.memory.buffer);
+ const geometry = [bw,bh,tw,th,...Array.from({length:11},(_,field)=>val(field))].join(":");
+ const encoded = bytes.subarray(ptr,ptr+rows*cols*CELL_SIZE);
+ const cached = this.layerNodes.get(i);
+ if (cached && cached.geometry===geometry && cached.bytes.length===encoded.length && cached.bytes.every((byte,index)=>byte===encoded[index])) {
+ nodes.push(cached.node);
+ continue;
+ }
+ const layer = this.root.ownerDocument.createElement("div");
+ layer.style.cssText = `position:absolute;overflow:hidden;left:${val(3)*bw}px;top:${val(4)*bh}px;width:${val(5)*bw}px;height:${val(6)*bh}px;background:${DEFAULT_BG}`;
+ for (let row = 0; row < rows; row++) {
+ const compact = row < contexts;
+ const height = compact ? th : bh, width = compact ? tw : bw;
+ const top = Math.min(row, contexts)*th + Math.max(0,row-contexts)*bh;
+ if (top >= val(6)*bh) break;
+ const line = this.root.ownerDocument.createElement("div");
+ line.style.cssText = `position:absolute;left:0;right:0;top:${top}px;height:${height}px;line-height:${height}px;white-space:pre;overflow:hidden`;
+ line.style.backgroundColor = color(view.getUint32(row*cols*CELL_SIZE+12,true),DEFAULT_BG);
+ for (let col = 0; col < cols; col++) {
+ const off = (row*cols+col)*CELL_SIZE;
+ const cell = this.root.ownerDocument.createElement("span");
+ cell.className = "pardes-cell";
+ cell.textContent = decoder.decode(bytes.subarray(ptr+off,ptr+off+view.getUint8(off+18))) || " ";
+ applyStyle(cell, view.getUint32(off+8,true), view.getUint32(off+12,true), view.getUint16(off+16,true), col===val(8)&&row===val(9), val(10)!==0);
+ cell.style.textAlign = "left";
+ cell.style.width = `${width}px`;
+ cell.style.height = `${height}px`;
+ cell.style.lineHeight = `${height}px`;
+ line.append(cell);
+ }
+ layer.append(line);
+ }
+ const border = this.root.ownerDocument.createElement("div");
+ border.style.cssText = `position:absolute;left:0;right:0;top:${contexts*th}px;height:1px;background:currentColor;opacity:.45`;
+ layer.append(border);
+ this.layerNodes.set(i,{geometry,bytes:encoded.slice(),node:layer});
+ nodes.push(layer);
+ }
+ if (nodes.length!==this.layerRoot.childNodes.length || nodes.some((node,index)=>node!==this.layerRoot.childNodes[index])) this.layerRoot.replaceChildren(...nodes);
+ }
+
ensureGrid(cols, rows) {
if (cols === this.cols && rows === this.rowCount) return;
const document = this.root.ownerDocument;
@@ -148,6 +209,10 @@ export class PardesRuntime {
frame(timestamp) {
if (!this.running) return;
+ if (this.wasm.pardes_gui_tagline_font_percent() !== this.measuredTaglinePercent) {
+ this.resize();
+ this.root.style.setProperty("--pardes-tagline-font-size", `${this.measuredTaglinePercent}%`);
+ }
// JS owns the animation clock; the pump that follows applies those ticks,
// runs the core's effects and presents one frame however fast the display
// is. Both halves are needed: an idle core still has input and effects.
@@ -169,6 +234,11 @@ export class PardesRuntime {
probe.remove();
this.cellWidth = Math.max(1, rect.width);
this.cellHeight = Math.max(1, rect.height);
+ const ratio = this.wasm.pardes_gui_tagline_font_percent() / 100;
+ this.measuredTaglinePercent = this.wasm.pardes_gui_tagline_font_percent();
+ this.taglineWidth = this.cellWidth * ratio;
+ this.taglineHeight = this.cellHeight * ratio;
+ Object.assign(this.renderer, { cellWidth:this.cellWidth, cellHeight:this.cellHeight, taglineWidth:this.taglineWidth, taglineHeight:this.taglineHeight });
}
gridSize() {
@@ -182,6 +252,7 @@ export class PardesRuntime {
this.measure();
const { cols, rows } = this.gridSize();
this.wasm.pardes_resize(cols, rows);
+ this.wasm.pardes_row_metrics?.(Math.max(1,Math.round(this.cellWidth)),Math.max(1,Math.round(this.cellHeight)),Math.max(1,Math.round(this.taglineWidth)),Math.max(1,Math.round(this.taglineHeight)));
}
writeInput(text) {
@@ -204,6 +275,7 @@ export class PardesRuntime {
point(event) {
const rect = this.root.getBoundingClientRect();
+ this.sampledPointer = { x:event.clientX-rect.left, y:event.clientY-rect.top };
return {
col: clamp(Math.floor((event.clientX - rect.left) / this.cellWidth), 0, Math.max(0, this.wasm.pardes_frame_cols() - 1)),
row: clamp(Math.floor((event.clientY - rect.top) / this.cellHeight), 0, Math.max(0, this.wasm.pardes_frame_rows() - 1)),
@@ -215,12 +287,18 @@ export class PardesRuntime {
return { x: rect.left + (col + 0.5) * this.cellWidth, y: rect.top + (row + 0.5) * this.cellHeight };
}
+ mouse(button, kind, col, row, mods) {
+ if (this.wasm.pardes_mouse_pixel && this.sampledPointer) {
+ this.wasm.pardes_mouse_pixel(button,kind,col,row,mods,this.sampledPointer.x*256,this.sampledPointer.y*256,Math.round(this.cellWidth*256),Math.round(this.cellHeight*256),Math.round(this.taglineWidth*256),Math.round(this.taglineHeight*256));
+ } else this.wasm.pardes_mouse(button,kind,col,row,mods);
+ }
+
mouseDown(event) {
event.preventDefault();
if (event.button !== 3 && event.button !== 4) return;
this.root.focus({ preventScroll: true });
const point = this.point(event);
- this.wasm.pardes_mouse(pointerButton(event.button), MOUSE.press, point.col, point.row, modifiers(event));
+ this.mouse(pointerButton(event.button), MOUSE.press, point.col, point.row, modifiers(event));
}
pointerDown(event) {
@@ -236,12 +314,12 @@ export class PardesRuntime {
const chrome = packed !== 0xffffffff;
const snapped = chrome ? { col: packed & 0xffff, row: packed >>> 16 } : point;
this.pointer = { id: event.pointerId, touch: true, chrome, startX: event.clientX, startY: event.clientY, lastX: event.clientX, lastY: event.clientY, point: snapped, scrolling: false, accum: 0 };
- if (chrome) this.wasm.pardes_mouse(BUTTON.left, MOUSE.press, snapped.col, snapped.row, modifiers(event));
+ if (chrome) this.mouse(BUTTON.left, MOUSE.press, snapped.col, snapped.row, modifiers(event));
return;
}
const button = pointerButton(event.button);
this.pointer = { id: event.pointerId, touch: false, button };
- this.wasm.pardes_mouse(button, MOUSE.press, point.col, point.row, modifiers(event));
+ this.mouse(button, MOUSE.press, point.col, point.row, modifiers(event));
}
pointerMove(event) {
@@ -249,16 +327,16 @@ export class PardesRuntime {
const point = this.point(event);
const active = this.pointer?.id === event.pointerId ? this.pointer : null;
if (!active) {
- if (event.pointerType !== "touch") this.wasm.pardes_mouse(BUTTON.none, MOUSE.motion, point.col, point.row, modifiers(event));
+ if (event.pointerType !== "touch") this.mouse(BUTTON.none, MOUSE.motion, point.col, point.row, modifiers(event));
return;
}
event.preventDefault();
if (!active.touch) {
- this.wasm.pardes_mouse(active.button, MOUSE.drag, point.col, point.row, modifiers(event));
+ this.mouse(active.button, MOUSE.drag, point.col, point.row, modifiers(event));
return;
}
if (active.chrome) {
- this.wasm.pardes_mouse(BUTTON.left, MOUSE.drag, point.col, point.row, modifiers(event));
+ this.mouse(BUTTON.left, MOUSE.drag, point.col, point.row, modifiers(event));
return;
}
const travel = Math.hypot(event.clientX - active.startX, event.clientY - active.startY);
@@ -267,11 +345,11 @@ export class PardesRuntime {
const dy = event.clientY - active.lastY;
active.accum += dy / this.cellHeight;
while (active.accum <= -1) {
- this.wasm.pardes_mouse(BUTTON.wheelDown, MOUSE.press, point.col, point.row, 0);
+ this.mouse(BUTTON.wheelDown, MOUSE.press, point.col, point.row, 0);
active.accum += 1;
}
while (active.accum >= 1) {
- this.wasm.pardes_mouse(BUTTON.wheelUp, MOUSE.press, point.col, point.row, 0);
+ this.mouse(BUTTON.wheelUp, MOUSE.press, point.col, point.row, 0);
active.accum -= 1;
}
this.wasm.pardes_touch_scroll(dy / this.cellHeight);
@@ -290,13 +368,13 @@ export class PardesRuntime {
event.preventDefault();
const point = this.point(event);
if (active.touch) {
- if (active.chrome) this.wasm.pardes_mouse(BUTTON.left, MOUSE.release, point.col, point.row, modifiers(event));
+ if (active.chrome) this.mouse(BUTTON.left, MOUSE.release, point.col, point.row, modifiers(event));
else if (!active.scrolling) {
- this.wasm.pardes_mouse(BUTTON.right, MOUSE.press, point.col, point.row, 0);
- this.wasm.pardes_mouse(BUTTON.right, MOUSE.release, point.col, point.row, 0);
+ this.mouse(BUTTON.right, MOUSE.press, point.col, point.row, 0);
+ this.mouse(BUTTON.right, MOUSE.release, point.col, point.row, 0);
}
} else {
- this.wasm.pardes_mouse(active.button, MOUSE.release, point.col, point.row, modifiers(event));
+ this.mouse(active.button, MOUSE.release, point.col, point.row, modifiers(event));
}
if (this.root.hasPointerCapture?.(event.pointerId)) this.root.releasePointerCapture(event.pointerId);
this.pointer = null;
@@ -305,8 +383,8 @@ export class PardesRuntime {
wheel(event) {
event.preventDefault();
const point = this.point(event);
- if (event.deltaY) this.wasm.pardes_mouse(event.deltaY < 0 ? BUTTON.wheelUp : BUTTON.wheelDown, MOUSE.press, point.col, point.row, modifiers(event));
- if (event.deltaX) this.wasm.pardes_mouse(event.deltaX < 0 ? BUTTON.wheelLeft : BUTTON.wheelRight, MOUSE.press, point.col, point.row, modifiers(event));
+ if (event.deltaY) this.mouse(event.deltaY < 0 ? BUTTON.wheelUp : BUTTON.wheelDown, MOUSE.press, point.col, point.row, modifiers(event));
+ if (event.deltaX) this.mouse(event.deltaX < 0 ? BUTTON.wheelLeft : BUTTON.wheelRight, MOUSE.press, point.col, point.row, modifiers(event));
}
paste(event) {