screen: add fit-to-window toggle
Adds a 'fit' toolbar toggle on the /screen VNC viewer. When on (the default), the canvas is CSS-scaled down to fit the browser viewport preserving aspect ratio — no more scrolling a desktop larger than the window. When off, the canvas renders at native resolution with scroll. The choice persists in localStorage. The canvas's intrinsic resolution is never touched — only its display size. sendPointer now rescales client coordinates by the canvas display-vs-intrinsic ratio so clicks stay accurate in fit mode (this also fixes a latent off-by-scale bug). Toolbar buttons share a .tbtn class for consistent styling. Issue #133 (fit toggle now; dynamic desktop resize tracked separately).
This commit is contained in:
parent
1f52746bd9
commit
305a32220b
1 changed files with 41 additions and 7 deletions
|
|
@ -30,6 +30,12 @@ html, body { height: 100%; background: var(--base); color: var(--text);
|
||||||
}
|
}
|
||||||
#toolbar a { color: var(--blue); text-decoration: none; font-size: 0.85rem; }
|
#toolbar a { color: var(--blue); text-decoration: none; font-size: 0.85rem; }
|
||||||
#toolbar a:hover { text-decoration: underline; }
|
#toolbar a:hover { text-decoration: underline; }
|
||||||
|
.tbtn {
|
||||||
|
padding: 0.15rem 0.5rem; font-size: 0.72rem; font-family: inherit;
|
||||||
|
background: var(--surface0); color: var(--subtext0);
|
||||||
|
border: 1px solid var(--surface1); border-radius: 4px; cursor: pointer;
|
||||||
|
}
|
||||||
|
.tbtn.active { color: var(--green); border-color: var(--green); }
|
||||||
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
#status { margin-left: auto; font-size: 0.75rem; color: var(--subtext0); }
|
||||||
#status.connected { color: var(--green); }
|
#status.connected { color: var(--green); }
|
||||||
#status.error { color: var(--red); }
|
#status.error { color: var(--red); }
|
||||||
|
|
@ -50,7 +56,12 @@ 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
|
||||||
|
viewport (intrinsic resolution unchanged — see canvas.fit). */
|
||||||
|
#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;
|
||||||
|
|
@ -64,10 +75,8 @@ canvas { display: block; cursor: default; }
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<strong>🖥 screen</strong>
|
<strong>🖥 screen</strong>
|
||||||
<a href="/" title="back to agent page">← agent</a>
|
<a href="/" title="back to agent page">← agent</a>
|
||||||
<button id="debug-toggle" title="Toggle RFB debug log"
|
<button id="fit-toggle" class="tbtn" title="Toggle fit-to-window scaling">⤢ fit</button>
|
||||||
style="margin-left:0.5rem;padding:0.15rem 0.5rem;font-size:0.72rem;
|
<button id="debug-toggle" class="tbtn" title="Toggle RFB debug log">debug</button>
|
||||||
background:var(--surface0);color:var(--subtext0);border:1px solid var(--surface1);
|
|
||||||
border-radius:4px;cursor:pointer;">debug</button>
|
|
||||||
<span id="status">connecting…</span>
|
<span id="status">connecting…</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="canvas-wrap"><canvas id="c"></canvas></div>
|
<div id="canvas-wrap"><canvas id="c"></canvas></div>
|
||||||
|
|
@ -92,15 +101,36 @@ canvas { display: block; cursor: default; }
|
||||||
const msg = document.getElementById('msg');
|
const msg = document.getElementById('msg');
|
||||||
const debugLog = document.getElementById('debug-log');
|
const debugLog = document.getElementById('debug-log');
|
||||||
const debugBtn = document.getElementById('debug-toggle');
|
const debugBtn = document.getElementById('debug-toggle');
|
||||||
|
const fitBtn = document.getElementById('fit-toggle');
|
||||||
|
const canvasWrap = document.getElementById('canvas-wrap');
|
||||||
|
|
||||||
// --- Debug log ---
|
// --- Debug log ---
|
||||||
let debugVisible = false;
|
let debugVisible = false;
|
||||||
debugBtn.addEventListener('click', () => {
|
debugBtn.addEventListener('click', () => {
|
||||||
debugVisible = !debugVisible;
|
debugVisible = !debugVisible;
|
||||||
debugLog.style.display = debugVisible ? 'block' : 'none';
|
debugLog.style.display = debugVisible ? 'block' : 'none';
|
||||||
debugBtn.style.color = debugVisible ? 'var(--green)' : 'var(--subtext0)';
|
debugBtn.classList.toggle('active', debugVisible);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- 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.
|
||||||
|
let fitMode = localStorage.getItem('screen-fit') !== 'off';
|
||||||
|
function applyFitMode() {
|
||||||
|
canvas.classList.toggle('fit', fitMode);
|
||||||
|
canvasWrap.classList.toggle('fit', fitMode);
|
||||||
|
fitBtn.classList.toggle('active', fitMode);
|
||||||
|
}
|
||||||
|
fitBtn.addEventListener('click', () => {
|
||||||
|
fitMode = !fitMode;
|
||||||
|
localStorage.setItem('screen-fit', fitMode ? 'on' : 'off');
|
||||||
|
applyFitMode();
|
||||||
|
});
|
||||||
|
applyFitMode();
|
||||||
|
|
||||||
function hex(bytes) {
|
function hex(bytes) {
|
||||||
return Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
return Array.from(bytes).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
||||||
}
|
}
|
||||||
|
|
@ -570,8 +600,12 @@ canvas { display: block; cursor: default; }
|
||||||
|
|
||||||
function sendPointer(ev) {
|
function sendPointer(ev) {
|
||||||
const r = canvas.getBoundingClientRect();
|
const r = canvas.getBoundingClientRect();
|
||||||
const x = Math.max(0, Math.min(fbW-1, ev.clientX - r.left));
|
// In fit mode the canvas is CSS-scaled, so the rendered rect differs
|
||||||
const y = Math.max(0, Math.min(fbH-1, ev.clientY - r.top));
|
// from the intrinsic resolution — map client coords back to fb pixels.
|
||||||
|
const sx = r.width ? canvas.width / r.width : 1;
|
||||||
|
const sy = r.height ? canvas.height / r.height : 1;
|
||||||
|
const x = Math.max(0, Math.min(fbW-1, Math.round((ev.clientX - r.left) * sx)));
|
||||||
|
const y = Math.max(0, Math.min(fbH-1, Math.round((ev.clientY - r.top) * sy)));
|
||||||
let mask = 0;
|
let mask = 0;
|
||||||
if (ev.buttons & 1) mask |= 1;
|
if (ev.buttons & 1) mask |= 1;
|
||||||
if (ev.buttons & 4) mask |= 2;
|
if (ev.buttons & 4) mask |= 2;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue