import { clampFontSize } from "./terminal.js"; // xterm.js's WebGL renderer clears the framebuffer when it reassigns // canvas.width/height on a font-size change, and during a touch gesture the // repaint is rAF-throttled, so the cleared canvas can flash blank before the // repaint lands. This overlay pins a 2d-canvas snapshot over the live canvas. export function createPinchController(term) { let pinchOverlay = null; let pinchOverlayAwaitingRender = false; let pinchOverlaySafetyTimer = null; const destroy = () => { if (pinchOverlay) { pinchOverlay.remove(); pinchOverlay = null; } pinchOverlayAwaitingRender = false; if (pinchOverlaySafetyTimer !== null) { clearTimeout(pinchOverlaySafetyTimer); pinchOverlaySafetyTimer = null; } }; const snapshot = () => { destroy(); if (!term.element) return; const sourceCanvases = term.element.querySelectorAll("canvas"); if (sourceCanvases.length === 0) return; const ref = sourceCanvases[0]; const rect = ref.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return; const overlay = document.createElement("canvas"); overlay.width = ref.width; overlay.height = ref.height; const outer = document.getElementById("terminal"); const bg = outer ? window.getComputedStyle(outer).backgroundColor : "transparent"; Object.assign(overlay.style, { position: "fixed", left: rect.left + "px", top: rect.top + "px", width: rect.width + "px", height: rect.height + "px", zIndex: "9999", pointerEvents: "none", background: bg, }); const ctx = overlay.getContext("2d"); if (ctx) { for (const c of sourceCanvases) { try { ctx.drawImage(c, 0, 0); } catch (e) { } } } document.body.appendChild(overlay); pinchOverlay = overlay; }; const refresh = () => { if (!pinchOverlay) return; if (!term.element) return; const sourceCanvases = term.element.querySelectorAll("canvas"); if (sourceCanvases.length === 0) return; const ref = sourceCanvases[0]; const rect = ref.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return; if (pinchOverlay.width !== ref.width) { pinchOverlay.width = ref.width; } if (pinchOverlay.height !== ref.height) { pinchOverlay.height = ref.height; } pinchOverlay.style.left = rect.left + "px"; pinchOverlay.style.top = rect.top + "px"; pinchOverlay.style.width = rect.width + "px"; pinchOverlay.style.height = rect.height + "px"; const ctx = pinchOverlay.getContext("2d"); if (ctx) { ctx.clearRect(0, 0, pinchOverlay.width, pinchOverlay.height); for (const c of sourceCanvases) { try { ctx.drawImage(c, 0, 0); } catch (e) { } } } }; const armRemoval = () => { if (!pinchOverlay) return; pinchOverlayAwaitingRender = true; if (pinchOverlaySafetyTimer !== null) { clearTimeout(pinchOverlaySafetyTimer); } pinchOverlaySafetyTimer = setTimeout(() => { pinchOverlaySafetyTimer = null; destroy(); }, 600); }; // Must not call fitAddon.fit() here: that would pre-sync term.rows/cols to the // new dims, making the resize handler short-circuit and never notify the server. const applyFontSize = (px) => { const clamped = clampFontSize(px); if (term.options.fontSize === clamped) { return; } term.options.fontSize = clamped; window.dispatchEvent(new CustomEvent("zellij:rendering-resize")); }; if (term && typeof term.onRender === "function") { term.onRender(() => { if (!pinchOverlay) return; refresh(); if (pinchOverlayAwaitingRender) { pinchOverlayAwaitingRender = false; requestAnimationFrame(() => { destroy(); }); } }); } return { snapshot, refresh, applyFontSize, armRemoval, destroy }; }