diff options
Diffstat (limited to 'src/web/app.mjs')
| -rw-r--r-- | src/web/app.mjs | 106 |
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) { |
