mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
fix: render RDP sessions at native pixel density (#1059)
This commit is contained in:
@@ -102,7 +102,7 @@ const clientOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
allowedUnencryptedConnectionSettings: {
|
allowedUnencryptedConnectionSettings: {
|
||||||
rdp: ["width", "height"],
|
rdp: ["width", "height", "dpi"],
|
||||||
vnc: ["width", "height"],
|
vnc: ["width", "height"],
|
||||||
telnet: ["width", "height"],
|
telnet: ["width", "height"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
|||||||
|
|
||||||
interface GuacamoleAppInnerProps {
|
interface GuacamoleAppInnerProps {
|
||||||
hostId: number;
|
hostId: number;
|
||||||
hostConfig: Pick<SSHHost, "connectionType">;
|
hostConfig: Pick<SSHHost, "connectionType" | "guacamoleConfig">;
|
||||||
hostName: string;
|
hostName: string;
|
||||||
tabId?: string;
|
tabId?: string;
|
||||||
protocol?: "rdp" | "vnc" | "telnet";
|
protocol?: "rdp" | "vnc" | "telnet";
|
||||||
@@ -213,6 +213,7 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
| "rdp"
|
| "rdp"
|
||||||
| "vnc"
|
| "vnc"
|
||||||
| "telnet";
|
| "telnet";
|
||||||
|
const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full">
|
<div className="relative w-full h-full">
|
||||||
@@ -250,6 +251,10 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
token,
|
token,
|
||||||
protocol: resolvedProtocol,
|
protocol: resolvedProtocol,
|
||||||
type: resolvedProtocol,
|
type: resolvedProtocol,
|
||||||
|
dpi:
|
||||||
|
Number.isFinite(configuredDpi) && configuredDpi > 0
|
||||||
|
? configuredDpi
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
isVisible={true}
|
isVisible={true}
|
||||||
onError={(err) => setConnectionError(err)}
|
onError={(err) => setConnectionError(err)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
isPasteShortcut,
|
isPasteShortcut,
|
||||||
pasteTextToRemote,
|
pasteTextToRemote,
|
||||||
} from "./guacamole-clipboard.ts";
|
} from "./guacamole-clipboard.ts";
|
||||||
|
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
|
||||||
|
|
||||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||||
|
|
||||||
@@ -129,13 +130,14 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
let token: string;
|
let token: string;
|
||||||
const protocol = connectionConfig.protocol ?? connectionConfig.type;
|
const connectionProtocol =
|
||||||
|
connectionConfig.protocol ?? connectionConfig.type;
|
||||||
|
|
||||||
if (connectionConfig.token) {
|
if (connectionConfig.token) {
|
||||||
token = connectionConfig.token;
|
token = connectionConfig.token;
|
||||||
} else {
|
} else {
|
||||||
const data = await getGuacamoleToken({
|
const data = await getGuacamoleToken({
|
||||||
protocol: protocol ?? "rdp",
|
protocol: connectionProtocol ?? "rdp",
|
||||||
hostname: String(connectionConfig.hostname ?? ""),
|
hostname: String(connectionConfig.hostname ?? ""),
|
||||||
port: connectionConfig.port,
|
port: connectionConfig.port,
|
||||||
username: connectionConfig.username,
|
username: connectionConfig.username,
|
||||||
@@ -156,8 +158,13 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
token = data.token;
|
token = data.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
const width = connectionConfig.width ?? containerWidth ?? 1280;
|
const displaySize = getGuacamoleDisplaySize(
|
||||||
const height = connectionConfig.height ?? containerHeight ?? 720;
|
connectionConfig.width ?? containerWidth ?? 1280,
|
||||||
|
connectionConfig.height ?? containerHeight ?? 720,
|
||||||
|
connectionProtocol,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
connectionConfig.dpi,
|
||||||
|
);
|
||||||
|
|
||||||
const wsBase = buildGuacamoleWebSocketBaseUrl({
|
const wsBase = buildGuacamoleWebSocketBaseUrl({
|
||||||
isDev,
|
isDev,
|
||||||
@@ -171,9 +178,10 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
token,
|
token,
|
||||||
width: String(width),
|
width: String(displaySize.width),
|
||||||
height: String(height),
|
height: String(displaySize.height),
|
||||||
});
|
});
|
||||||
|
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
|
||||||
return `${wsBase}?${params.toString()}`;
|
return `${wsBase}?${params.toString()}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
@@ -434,9 +442,14 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
onConnect?.();
|
onConnect?.();
|
||||||
if (containerRef.current) {
|
if (containerRef.current) {
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const w = Math.round(rect.width);
|
const size = getGuacamoleDisplaySize(
|
||||||
const h = Math.round(rect.height);
|
rect.width,
|
||||||
if (w > 0 && h > 0) client.sendSize(w, h);
|
rect.height,
|
||||||
|
protocol,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
connectionConfig.dpi,
|
||||||
|
);
|
||||||
|
client.sendSize(size.width, size.height);
|
||||||
}
|
}
|
||||||
rescaleDisplay(false);
|
rescaleDisplay(false);
|
||||||
break;
|
break;
|
||||||
@@ -515,6 +528,7 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
rescaleDisplay,
|
rescaleDisplay,
|
||||||
connectionConfig.protocol,
|
connectionConfig.protocol,
|
||||||
connectionConfig.type,
|
connectionConfig.type,
|
||||||
|
connectionConfig.dpi,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -588,10 +602,15 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
resizeTimeoutRef.current = setTimeout(() => {
|
resizeTimeoutRef.current = setTimeout(() => {
|
||||||
if (clientRef.current && containerRef.current) {
|
if (clientRef.current && containerRef.current) {
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
const rect = containerRef.current.getBoundingClientRect();
|
||||||
const w = Math.round(rect.width);
|
const size = getGuacamoleDisplaySize(
|
||||||
const h = Math.round(rect.height);
|
rect.width,
|
||||||
if (w > 0 && h > 0) {
|
rect.height,
|
||||||
clientRef.current.sendSize(w, h);
|
connectionConfig.protocol ?? connectionConfig.type,
|
||||||
|
window.devicePixelRatio,
|
||||||
|
connectionConfig.dpi,
|
||||||
|
);
|
||||||
|
if (rect.width > 0 && rect.height > 0) {
|
||||||
|
clientRef.current.sendSize(size.width, size.height);
|
||||||
rescaleDisplay(true);
|
rescaleDisplay(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -603,7 +622,12 @@ export const GuacamoleDisplay = forwardRef<
|
|||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [rescaleDisplay]);
|
}, [
|
||||||
|
connectionConfig.dpi,
|
||||||
|
connectionConfig.protocol,
|
||||||
|
connectionConfig.type,
|
||||||
|
rescaleDisplay,
|
||||||
|
]);
|
||||||
|
|
||||||
const syncClipboard = useCallback(() => {
|
const syncClipboard = useCallback(() => {
|
||||||
const client = clientRef.current;
|
const client = clientRef.current;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { getGuacamoleDisplaySize } from "./guacamole-display-size";
|
||||||
|
|
||||||
|
describe("getGuacamoleDisplaySize", () => {
|
||||||
|
it("requests native pixels and matching DPI for HiDPI RDP", () => {
|
||||||
|
expect(getGuacamoleDisplaySize(1280, 720, "rdp", 2)).toEqual({
|
||||||
|
width: 2560,
|
||||||
|
height: 1440,
|
||||||
|
dpi: 192,
|
||||||
|
pixelRatio: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("scales a configured RDP DPI with the device pixel ratio", () => {
|
||||||
|
expect(getGuacamoleDisplaySize(1000, 600, "rdp", 1.5, 120)).toEqual({
|
||||||
|
width: 1500,
|
||||||
|
height: 900,
|
||||||
|
dpi: 180,
|
||||||
|
pixelRatio: 1.5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves non-RDP protocols at CSS-pixel dimensions", () => {
|
||||||
|
expect(getGuacamoleDisplaySize(1280, 720, "vnc", 2)).toEqual({
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
pixelRatio: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps pathological pixel ratios to protect remote session size", () => {
|
||||||
|
expect(getGuacamoleDisplaySize(400, 800, "rdp", 4)).toEqual({
|
||||||
|
width: 1200,
|
||||||
|
height: 2400,
|
||||||
|
dpi: 288,
|
||||||
|
pixelRatio: 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
const DEFAULT_RDP_DPI = 96;
|
||||||
|
const MAX_DEVICE_PIXEL_RATIO = 3;
|
||||||
|
|
||||||
|
export interface GuacamoleDisplaySize {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
dpi?: number;
|
||||||
|
pixelRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGuacamoleDisplaySize(
|
||||||
|
cssWidth: number,
|
||||||
|
cssHeight: number,
|
||||||
|
protocol: string | undefined,
|
||||||
|
devicePixelRatio: number,
|
||||||
|
configuredDpi?: number,
|
||||||
|
): GuacamoleDisplaySize {
|
||||||
|
const isRdp = protocol === "rdp";
|
||||||
|
const pixelRatio = isRdp
|
||||||
|
? Math.min(
|
||||||
|
MAX_DEVICE_PIXEL_RATIO,
|
||||||
|
Math.max(1, Number.isFinite(devicePixelRatio) ? devicePixelRatio : 1),
|
||||||
|
)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const size = {
|
||||||
|
width: Math.max(1, Math.round(cssWidth * pixelRatio)),
|
||||||
|
height: Math.max(1, Math.round(cssHeight * pixelRatio)),
|
||||||
|
pixelRatio,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isRdp) return size;
|
||||||
|
|
||||||
|
const baseDpi =
|
||||||
|
configuredDpi && Number.isFinite(configuredDpi) && configuredDpi > 0
|
||||||
|
? configuredDpi
|
||||||
|
: DEFAULT_RDP_DPI;
|
||||||
|
return { ...size, dpi: Math.round(baseDpi * pixelRatio) };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user