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