diff --git a/src/pages/RemoteDesktop.tsx b/src/pages/RemoteDesktop.tsx index be67e8b..6a621a0 100644 --- a/src/pages/RemoteDesktop.tsx +++ b/src/pages/RemoteDesktop.tsx @@ -17,6 +17,19 @@ interface Session { interface SessionHandle { client: any container: HTMLDivElement + display: any +} + +// Scale the Guacamole display to fit its visible container. The display canvas +// renders at the remote resolution; without this it can paint to a 0-sized / +// unscaled area (blank screen) when the element wasn't in the live DOM at connect. +function fitDisplay(handle: SessionHandle | null | undefined, host: HTMLElement | null) { + if (!handle || !host) return + const w = handle.display.getWidth() + const h = handle.display.getHeight() + if (!w || !h) return + const scale = Math.min(host.clientWidth / w, host.clientHeight / h, 1) || 1 + handle.display.scale(scale) } export default function RemoteDesktop() { @@ -25,6 +38,8 @@ export default function RemoteDesktop() { const [activeSessionId, setActiveSessionId] = useState(null) const displayHostRef = useRef(null) const handlesRef = useRef>(new Map()) + const activeSessionIdRef = useRef(null) + activeSessionIdRef.current = activeSessionId useEffect(() => { api.listIntegrations().then(({ integrations }) => { @@ -57,7 +72,8 @@ export default function RemoteDesktop() { // so the tunnel URL itself must not already contain one. const tunnel = new Guacamole.WebSocketTunnel(`${proto}://${window.location.host}/api/guacamole`) const client = new Guacamole.Client(tunnel) - handlesRef.current.set(sessionId, { client, container }) + const display = client.getDisplay() + handlesRef.current.set(sessionId, { client, container, display }) client.onerror = (err: { message?: string }) => { patchSession(sessionId, { status: 'error', errorMessage: err?.message ?? 'Connection failed' }) @@ -65,8 +81,13 @@ export default function RemoteDesktop() { client.onstatechange = (state: number) => { if (state === 3) patchSession(sessionId, { status: 'connected' }) } + // Re-fit whenever the remote desktop reports a new size, but only while this + // session is the visible one. + display.onresize = () => { + if (activeSessionIdRef.current === sessionId) fitDisplay(handlesRef.current.get(sessionId), displayHostRef.current) + } - container.appendChild(client.getDisplay().getElement()) + container.appendChild(display.getElement()) client.connect(`token=${encodeURIComponent(token ?? '')}&integrationId=${host.id}`) } @@ -91,9 +112,24 @@ export default function RemoteDesktop() { if (!host) return host.innerHTML = '' const active = activeSessionId ? handlesRef.current.get(activeSessionId) : null - if (active) host.appendChild(active.container) + if (active) { + host.appendChild(active.container) + // The display may have received its size while off-DOM (zero-sized), which + // leaves the canvas unscaled / invisible — re-fit now that it's visible. + fitDisplay(active, host) + } }, [activeSessionId]) + // Keep the active session scaled to the panel as the window/panel resizes. + useEffect(() => { + const onResize = () => { + const active = activeSessionIdRef.current ? handlesRef.current.get(activeSessionIdRef.current) : null + fitDisplay(active, displayHostRef.current) + } + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, []) + const activeSession = sessions.find((s) => s.sessionId === activeSessionId) return (