diff --git a/src/ui/features/guacamole/GuacamoleApp.tsx b/src/ui/features/guacamole/GuacamoleApp.tsx index a978557d..4c9f80a6 100644 --- a/src/ui/features/guacamole/GuacamoleApp.tsx +++ b/src/ui/features/guacamole/GuacamoleApp.tsx @@ -17,6 +17,7 @@ import { logActivity, isElectron, } from "@/main-axios.ts"; +import { readConfiguredDimension } from "@/features/guacamole/guacamole-display-size.ts"; import { resolveConnectionOrigin } from "@/lib/connection-origin.ts"; import { useTranslation } from "react-i18next"; import { AlertCircle, RefreshCw } from "lucide-react"; @@ -362,7 +363,15 @@ const GuacamoleAppInner = React.forwardRef< } const resolvedProtocol = resolvedProtocolForConnect; - const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi); + const configuredDpi = readConfiguredDimension( + hostConfig.guacamoleConfig?.dpi, + ); + const configuredWidth = readConfiguredDimension( + hostConfig.guacamoleConfig?.width, + ); + const configuredHeight = readConfiguredDimension( + hostConfig.guacamoleConfig?.height, + ); return (
@@ -400,10 +409,9 @@ const GuacamoleAppInner = React.forwardRef< token, protocol: resolvedProtocol, type: resolvedProtocol, - dpi: - Number.isFinite(configuredDpi) && configuredDpi > 0 - ? configuredDpi - : undefined, + width: configuredWidth, + height: configuredHeight, + dpi: configuredDpi, }} isVisible={isVisible} touchMode={touchMode} diff --git a/src/ui/features/guacamole/GuacamoleDisplay.tsx b/src/ui/features/guacamole/GuacamoleDisplay.tsx index 671fa6f7..74e14856 100644 --- a/src/ui/features/guacamole/GuacamoleDisplay.tsx +++ b/src/ui/features/guacamole/GuacamoleDisplay.tsx @@ -69,6 +69,10 @@ export const GuacamoleDisplay = forwardRef< ref, ) { const { t } = useTranslation(); + // The host config pins the session resolution; without it the display follows + // the container. + const hasConfiguredSize = + connectionConfig.width != null && connectionConfig.height != null; const containerRef = useRef(null); const displayRef = useRef(null); const displayElementRef = useRef(null); @@ -493,7 +497,10 @@ export const GuacamoleDisplay = forwardRef< isConnectingRef.current = false; setIsReady(true); onConnect?.(); - if (containerRef.current) { + // A configured resolution is the size the session should render at; + // resizing it to the container would discard it. rescaleDisplay still + // fits that fixed display into whatever space is available. + if (!hasConfiguredSize && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); const size = getGuacamoleDisplaySize( rect.width, @@ -601,6 +608,7 @@ export const GuacamoleDisplay = forwardRef< connectionConfig.protocol, connectionConfig.type, connectionConfig.dpi, + hasConfiguredSize, touchMode, t, ]); @@ -675,15 +683,17 @@ export const GuacamoleDisplay = forwardRef< resizeTimeoutRef.current = setTimeout(() => { if (clientRef.current && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); - const size = getGuacamoleDisplaySize( - rect.width, - rect.height, - connectionConfig.protocol ?? connectionConfig.type, - window.devicePixelRatio, - connectionConfig.dpi, - ); if (rect.width > 0 && rect.height > 0) { - clientRef.current.sendSize(size.width, size.height); + if (!hasConfiguredSize) { + const size = getGuacamoleDisplaySize( + rect.width, + rect.height, + connectionConfig.protocol ?? connectionConfig.type, + window.devicePixelRatio, + connectionConfig.dpi, + ); + clientRef.current.sendSize(size.width, size.height); + } rescaleDisplay(true); } } @@ -699,6 +709,7 @@ export const GuacamoleDisplay = forwardRef< connectionConfig.dpi, connectionConfig.protocol, connectionConfig.type, + hasConfiguredSize, rescaleDisplay, ]); diff --git a/src/ui/features/guacamole/guacamole-display-size.ts b/src/ui/features/guacamole/guacamole-display-size.ts index a16a3475..27e34f3a 100644 --- a/src/ui/features/guacamole/guacamole-display-size.ts +++ b/src/ui/features/guacamole/guacamole-display-size.ts @@ -1,6 +1,15 @@ const DEFAULT_RDP_DPI = 96; const MAX_DEVICE_PIXEL_RATIO = 3; +/** + * Reads a guacamoleConfig display field. The UI stores these as strings, and + * leaves them empty when the size should follow the browser window. + */ +export function readConfiguredDimension(value: unknown): number | undefined { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined; +} + export interface GuacamoleDisplaySize { width: number; height: number; diff --git a/src/ui/tests/features/guacamole/guacamole-display-size.test.ts b/src/ui/tests/features/guacamole/guacamole-display-size.test.ts index b208acd7..71c336bf 100644 --- a/src/ui/tests/features/guacamole/guacamole-display-size.test.ts +++ b/src/ui/tests/features/guacamole/guacamole-display-size.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { getGuacamoleDisplaySize } from "../../../features/guacamole/guacamole-display-size"; +import { + getGuacamoleDisplaySize, + readConfiguredDimension, +} from "../../../features/guacamole/guacamole-display-size"; describe("getGuacamoleDisplaySize", () => { it("requests native pixels and matching DPI for HiDPI RDP", () => { @@ -36,4 +39,29 @@ describe("getGuacamoleDisplaySize", () => { pixelRatio: 3, }); }); + + it("uses a configured resolution in place of the container size", () => { + expect( + getGuacamoleDisplaySize(800, 600, "rdp", 1, undefined), + ).toMatchObject({ width: 800, height: 600 }); + expect( + getGuacamoleDisplaySize(1920, 1080, "rdp", 1, undefined), + ).toMatchObject({ width: 1920, height: 1080 }); + }); +}); + +describe("readConfiguredDimension", () => { + it("accepts the strings the host editor stores", () => { + expect(readConfiguredDimension("1920")).toBe(1920); + expect(readConfiguredDimension(1080)).toBe(1080); + }); + + it("treats an unset or unusable value as 'follow the container'", () => { + expect(readConfiguredDimension("")).toBeUndefined(); + expect(readConfiguredDimension(undefined)).toBeUndefined(); + expect(readConfiguredDimension(null)).toBeUndefined(); + expect(readConfiguredDimension("auto")).toBeUndefined(); + expect(readConfiguredDimension("0")).toBeUndefined(); + expect(readConfiguredDimension("-1080")).toBeUndefined(); + }); });