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);