diff --git a/hive-ag3nt/assets/screen.html b/hive-ag3nt/assets/screen.html index 2752277..44f50d0 100644 --- a/hive-ag3nt/assets/screen.html +++ b/hive-ag3nt/assets/screen.html @@ -57,12 +57,10 @@ html, body { height: 100%; background: var(--base); color: var(--text); width: 100%; height: calc(100% - 36px); overflow: auto; background: var(--crust); } -/* Fit mode: centre the canvas and let CSS scale it down to the - viewport (intrinsic resolution unchanged — see canvas.fit). */ +/* Fit mode: centre the canvas (relayoutCanvas() scales it in JS to + fit the wrap) and clip any sub-pixel rounding overflow. */ #canvas-wrap.fit { align-items: center; overflow: hidden; } canvas { display: block; cursor: default; } -/* max-* on a replaced element shrinks it preserving aspect ratio. */ -canvas.fit { max-width: 100%; max-height: 100%; } #msg { position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); background: var(--surface0); color: var(--yellow); border-radius: 6px; @@ -114,22 +112,44 @@ canvas.fit { max-width: 100%; max-height: 100%; } }); // --- Fit-to-window toggle --- - // Scales the canvas down via CSS so the whole desktop is visible - // without scrolling. The canvas's intrinsic resolution is untouched; - // only its displayed size changes (see canvas.fit / #canvas-wrap.fit). - // Pointer coordinates are rescaled in sendPointer to stay accurate. - // The choice is persisted in localStorage; default is fit-on. + // Scales the canvas down so the whole desktop is visible without + // scrolling. The canvas's intrinsic resolution (width/height attrs) + // is untouched — only its CSS display size changes, set explicitly + // by relayoutCanvas(). Pointer coordinates are rescaled in + // sendPointer to stay accurate. Persisted in localStorage; default + // is fit-on. let fitMode = localStorage.getItem('screen-fit') !== 'off'; + // Size the canvas. In fit mode, scale down (never up) to the wrap, + // preserving aspect ratio. Explicit px sizing rather than CSS + // max-width/max-height: on a flex item those are overridden by the + // automatic minimum size, so fit mode was a silent no-op — the + // oversized canvas just got centred and clipped (issue #133). + function relayoutCanvas() { + if (fitMode && canvas.width && canvas.height + && canvasWrap.clientWidth && canvasWrap.clientHeight) { + const scale = Math.min( + canvasWrap.clientWidth / canvas.width, + canvasWrap.clientHeight / canvas.height, + 1, + ); + canvas.style.width = (canvas.width * scale) + 'px'; + canvas.style.height = (canvas.height * scale) + 'px'; + } else if (!fitMode) { + canvas.style.width = ''; + canvas.style.height = ''; + } + } function applyFitMode() { - canvas.classList.toggle('fit', fitMode); canvasWrap.classList.toggle('fit', fitMode); fitBtn.classList.toggle('active', fitMode); + relayoutCanvas(); } fitBtn.addEventListener('click', () => { fitMode = !fitMode; localStorage.setItem('screen-fit', fitMode ? 'on' : 'off'); applyFitMode(); }); + window.addEventListener('resize', relayoutCanvas); applyFitMode(); function hex(bytes) { @@ -514,6 +534,7 @@ canvas.fit { max-width: 100%; max-height: 100%; } dbg('← server-init: ' + fbW + 'x' + fbH + ' bpp=' + pixelFormat.bpp, 'ok'); canvas.width = fbW; canvas.height = fbH; + relayoutCanvas(); setStatus('connected', 'connected'); // Request full framebuffer update requestUpdate(0, 0, 0, fbW, fbH);