fix: screen fit-to-window actually scales the canvas (#133)

The fit toggle relied on canvas.fit { max-width/max-height: 100% }.
The canvas is a flex item, and a flex item's automatic minimum size
(min-width/min-height: auto → the replaced element's intrinsic size)
overrides max-*, so an oversized desktop never shrank — it just got
centred and clipped, looking like the toggle did nothing.

Replace the CSS approach with explicit JS sizing: relayoutCanvas()
computes a scale factor (min of width/height ratios, capped at 1 so
it never upscales) and sets canvas.style.width/height in px,
preserving aspect ratio. Recomputed on fit toggle, window resize,
and framebuffer-size (ServerInit). Pointer mapping is unaffected —
sendPointer already derives scale from getBoundingClientRect().
This commit is contained in:
iris 2026-05-21 18:45:13 +02:00
parent 00281730bb
commit dfee0574a5

View file

@ -57,12 +57,10 @@ html, body { height: 100%; background: var(--base); color: var(--text);
width: 100%; height: calc(100% - 36px); overflow: auto; width: 100%; height: calc(100% - 36px); overflow: auto;
background: var(--crust); background: var(--crust);
} }
/* Fit mode: centre the canvas and let CSS scale it down to the /* Fit mode: centre the canvas (relayoutCanvas() scales it in JS to
viewport (intrinsic resolution unchanged — see canvas.fit). */ fit the wrap) and clip any sub-pixel rounding overflow. */
#canvas-wrap.fit { align-items: center; overflow: hidden; } #canvas-wrap.fit { align-items: center; overflow: hidden; }
canvas { display: block; cursor: default; } canvas { display: block; cursor: default; }
/* max-* on a replaced element shrinks it preserving aspect ratio. */
canvas.fit { max-width: 100%; max-height: 100%; }
#msg { #msg {
position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%); position: fixed; bottom: 1rem; left: 50%; transform: translateX(-50%);
background: var(--surface0); color: var(--yellow); border-radius: 6px; 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 --- // --- Fit-to-window toggle ---
// Scales the canvas down via CSS so the whole desktop is visible // Scales the canvas down so the whole desktop is visible without
// without scrolling. The canvas's intrinsic resolution is untouched; // scrolling. The canvas's intrinsic resolution (width/height attrs)
// only its displayed size changes (see canvas.fit / #canvas-wrap.fit). // is untouched — only its CSS display size changes, set explicitly
// Pointer coordinates are rescaled in sendPointer to stay accurate. // by relayoutCanvas(). Pointer coordinates are rescaled in
// The choice is persisted in localStorage; default is fit-on. // sendPointer to stay accurate. Persisted in localStorage; default
// is fit-on.
let fitMode = localStorage.getItem('screen-fit') !== 'off'; 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() { function applyFitMode() {
canvas.classList.toggle('fit', fitMode);
canvasWrap.classList.toggle('fit', fitMode); canvasWrap.classList.toggle('fit', fitMode);
fitBtn.classList.toggle('active', fitMode); fitBtn.classList.toggle('active', fitMode);
relayoutCanvas();
} }
fitBtn.addEventListener('click', () => { fitBtn.addEventListener('click', () => {
fitMode = !fitMode; fitMode = !fitMode;
localStorage.setItem('screen-fit', fitMode ? 'on' : 'off'); localStorage.setItem('screen-fit', fitMode ? 'on' : 'off');
applyFitMode(); applyFitMode();
}); });
window.addEventListener('resize', relayoutCanvas);
applyFitMode(); applyFitMode();
function hex(bytes) { 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'); dbg('← server-init: ' + fbW + 'x' + fbH + ' bpp=' + pixelFormat.bpp, 'ok');
canvas.width = fbW; canvas.width = fbW;
canvas.height = fbH; canvas.height = fbH;
relayoutCanvas();
setStatus('connected', 'connected'); setStatus('connected', 'connected');
// Request full framebuffer update // Request full framebuffer update
requestUpdate(0, 0, 0, fbW, fbH); requestUpdate(0, 0, 0, fbW, fbH);