summaryrefslogtreecommitdiff
path: root/src/web
diff options
context:
space:
mode:
Diffstat (limited to 'src/web')
-rw-r--r--src/web/app.mjs63
1 files changed, 59 insertions, 4 deletions
diff --git a/src/web/app.mjs b/src/web/app.mjs
index 477186a3..11d8f228 100644
--- a/src/web/app.mjs
+++ b/src/web/app.mjs
@@ -34,6 +34,9 @@ export class DomRenderer {
this.cols = 0;
this.rowCount = 0;
this.snapshot = null;
+ this.gripFonts = new Map();
+ this.gripMetrics = new Map();
+ this.root.ownerDocument.fonts?.addEventListener("loadingdone", () => this.resetGripMetrics());
const taglinePercent = this.wasm.pardes_gui_tagline_font_percent();
if (taglinePercent < 1 || taglinePercent > 100) throw new Error(`bad tagline font percentage ${taglinePercent}`);
this.root.style.setProperty("--pardes-tagline-font-size", `${taglinePercent}%`);
@@ -56,6 +59,7 @@ export class DomRenderer {
const raw = new Array(count);
const tagCovered = new Uint8Array(count);
const tagPrefixBands = new Map();
+ const paneIndicators = new Set();
if (this.wasm.pardes_tag_layer_limit) {
for (let i = 0; i < this.wasm.pardes_tag_layer_limit(); i++) {
const val = field => this.wasm.pardes_tag_layer_value(i, field);
@@ -65,6 +69,7 @@ export class DomRenderer {
if (val(1) !== 0 && val(4) >= textInset && this.cellHeight) {
const offset = this.wasm.pardes_tagline_band_offset(val(5),rows*this.cellHeight*256,Math.round(this.cellHeight*256),Math.round(this.taglineHeight*256))/256;
for (let x = val(4)-textInset; x < val(4); x++) tagPrefixBands.set(val(5)*cols+x,offset);
+ if (val(1) === 2) paneIndicators.add(val(5)*cols+val(4)-textInset);
}
for (let x = val(4); x < Math.min(cols, val(4) + val(6)); x++) {
if (val(5) < rows) tagCovered[val(5) * cols + x] = 1;
@@ -88,7 +93,8 @@ export class DomRenderer {
cell.style.height = gripOffset === undefined ? "" : `${this.taglineHeight}px`;
cell.style.lineHeight = gripOffset === undefined ? "" : `${this.taglineHeight}px`;
cell.style.marginTop = gripOffset === undefined ? "" : `${gripOffset}px`;
- if (cell.textContent !== text) cell.textContent = text;
+ const displayedText = paneIndicators.has(index) ? " " : text;
+ if (cell.textContent !== displayedText) cell.textContent = displayedText;
const styleKey = `${fg}:${bg}:${attrs}:${cursor ? 1 : 0}:${cursorBar ? 1 : 0}`;
if (cell.pardesStyle !== styleKey) {
applyStyle(cell, fg, bg, attrs, cursor, cursorBar);
@@ -124,8 +130,8 @@ export class DomRenderer {
const gripCols = this.wasm.pardes_grip_columns?.() ?? 2;
const gripX = values[4] - (this.wasm.pardes_tag_text_inset?.() ?? gripCols);
const gripCell = canonical[values[5]*this.cols+gripX];
- const grip = values[1] === 2 && gripX >= 0 && gripCell?.text === " " && th >= 6 && tw >= 3;
- const geometry = [bw,bh,tw,th,offset,...values,grip,gripCell?.fg].join(":");
+ const grip = values[1] === 2 && gripX >= 0 && gripCell && th >= 1 && tw >= 1;
+ const geometry = [bw,bh,tw,th,offset,...values,grip,gripCell?.fg,gripCell?.bg,gripCell?.attrs,gripCell?.text,globalThis.devicePixelRatio].join(":");
const cached = this.tagNodes.get(i);
if (cached && cached.geometry === geometry && cached.bytes.length === bytes.length && cached.bytes.every((byte,index)=>byte===bytes[index])) {
nodes.push(cached.node);
@@ -149,7 +155,13 @@ export class DomRenderer {
layer.append(cell);
}
let gripNode = null;
- if (grip) {
+ if (grip && gripCell.text.trim()) {
+ const cell = this.cells[values[5]*this.cols+gripX];
+ gripNode = this.drawGripGlyph(cell, gripCell, gripCols*bw, th);
+ gripNode.dataset.tagGrip = String(i);
+ gripNode.style.left = `${gripX*bw}px`;
+ gripNode.style.top = `${values[5]*bh+offset}px`;
+ } else if (grip && th >= 6 && tw >= 3) {
gripNode = this.root.ownerDocument.createElement("div");
gripNode.dataset.tagGrip = String(i);
const centerX = (gripX+gripCols/2)*bw, centerY = values[5]*bh+offset+th/2;
@@ -168,6 +180,43 @@ export class DomRenderer {
if (nodes.length !== this.tagRoot.childNodes.length || nodes.some((node,index)=>node!==this.tagRoot.childNodes[index])) this.tagRoot.replaceChildren(...nodes);
}
+ resetGripMetrics() {
+ this.gripFonts.clear();
+ this.gripMetrics.clear();
+ this.tagNodes?.clear();
+ }
+
+ drawGripGlyph(cell, glyph, width, height) {
+ const canvas = this.root.ownerDocument.createElement("canvas");
+ const ratio = globalThis.devicePixelRatio || 1;
+ canvas.width = Math.max(1, Math.ceil(width*ratio));
+ canvas.height = Math.max(1, Math.ceil(height*ratio));
+ canvas.style.cssText = `position:absolute;pointer-events:none;width:${width}px;height:${height}px`;
+ canvas.setAttribute("aria-label", glyph.text);
+ const ctx = canvas.getContext("2d");
+ // Resolve a face once per style, then retain its ink bearings for each
+ // symbol. Font loading and runtime metric changes clear both caches.
+ let font = this.gripFonts.get(glyph.attrs);
+ if (!font) {
+ const style = this.root.ownerDocument.defaultView.getComputedStyle(cell);
+ font = `${style.fontStyle} ${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
+ this.gripFonts.set(glyph.attrs, font);
+ }
+ ctx.scale(ratio, ratio);
+ ctx.font = font;
+ const key = `${font}:${ratio}:${glyph.text}`;
+ let ink = this.gripMetrics.get(key);
+ if (!ink) {
+ const measured = ctx.measureText(glyph.text);
+ ink = { left:measured.actualBoundingBoxLeft, right:measured.actualBoundingBoxRight,
+ ascent:measured.actualBoundingBoxAscent, descent:measured.actualBoundingBoxDescent };
+ this.gripMetrics.set(key, ink);
+ }
+ ctx.fillStyle = cell.style.color;
+ ctx.fillText(glyph.text, (width-ink.right+ink.left)/2, (height+ink.ascent-ink.descent)/2);
+ return canvas;
+ }
+
renderBodyLayers() {
if (!this.wasm.pardes_body_layer_limit || !this.cellWidth) return;
if (!this.layerRoot?.isConnected) {
@@ -332,6 +381,12 @@ export class PardesRuntime {
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 });
+ const style = this.root.ownerDocument.defaultView.getComputedStyle(this.root);
+ const fontKey = [style.fontFamily,style.fontSize,style.fontWeight,style.fontStyle,ratio,globalThis.devicePixelRatio].join(":");
+ if (fontKey !== this.gripFontKey) {
+ this.renderer.resetGripMetrics();
+ this.gripFontKey = fontKey;
+ }
}
gridSize() {