apply the configured RDP resolution to the session (#1120)

The host editor stores width and height in guacamoleConfig, and the backend
passes them to guacd in the connection token. The renderer then appends its own
width and height query parameters measured from the container, which take
precedence, so a configured resolution never reached the session — only dpi did,
because that was the one display field GuacamoleApp read back.

Pass the configured width and height alongside dpi, and skip the container-driven
sendSize on connect and on resize when a resolution is pinned. rescaleDisplay
still fits the fixed display into the available space.

Closes Termix-SSH/Support#1039
This commit is contained in:
ZacharyZcR
2026-07-28 01:48:57 +08:00
committed by GitHub
parent 6bdd38159c
commit 94a072b76d
4 changed files with 71 additions and 15 deletions
+13 -5
View File
@@ -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 (
<div className="relative w-full h-full">
@@ -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}
+20 -9
View File
@@ -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<HTMLDivElement>(null);
const displayRef = useRef<HTMLDivElement>(null);
const displayElementRef = useRef<HTMLElement | null>(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,
]);
@@ -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;
@@ -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();
});
});