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:
@@ -96,7 +96,7 @@ const GuacamoleApp = React.forwardRef<GuacamoleAppHandle, GuacamoleAppProps>(
|
||||
|
||||
interface GuacamoleAppInnerProps {
|
||||
hostId: number;
|
||||
hostConfig: Pick<SSHHost, "connectionType">;
|
||||
hostConfig: Pick<SSHHost, "connectionType" | "guacamoleConfig">;
|
||||
hostName: string;
|
||||
tabId?: string;
|
||||
protocol?: "rdp" | "vnc" | "telnet";
|
||||
@@ -213,6 +213,7 @@ const GuacamoleAppInner = React.forwardRef<
|
||||
| "rdp"
|
||||
| "vnc"
|
||||
| "telnet";
|
||||
const configuredDpi = Number(hostConfig.guacamoleConfig?.dpi);
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
@@ -250,6 +251,10 @@ const GuacamoleAppInner = React.forwardRef<
|
||||
token,
|
||||
protocol: resolvedProtocol,
|
||||
type: resolvedProtocol,
|
||||
dpi:
|
||||
Number.isFinite(configuredDpi) && configuredDpi > 0
|
||||
? configuredDpi
|
||||
: undefined,
|
||||
}}
|
||||
isVisible={true}
|
||||
onError={(err) => setConnectionError(err)}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isPasteShortcut,
|
||||
pasteTextToRemote,
|
||||
} from "./guacamole-clipboard.ts";
|
||||
import { getGuacamoleDisplaySize } from "./guacamole-display-size.ts";
|
||||
|
||||
export type GuacamoleConnectionType = "rdp" | "vnc" | "telnet";
|
||||
|
||||
@@ -129,13 +130,14 @@ export const GuacamoleDisplay = forwardRef<
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
let token: string;
|
||||
const protocol = connectionConfig.protocol ?? connectionConfig.type;
|
||||
const connectionProtocol =
|
||||
connectionConfig.protocol ?? connectionConfig.type;
|
||||
|
||||
if (connectionConfig.token) {
|
||||
token = connectionConfig.token;
|
||||
} else {
|
||||
const data = await getGuacamoleToken({
|
||||
protocol: protocol ?? "rdp",
|
||||
protocol: connectionProtocol ?? "rdp",
|
||||
hostname: String(connectionConfig.hostname ?? ""),
|
||||
port: connectionConfig.port,
|
||||
username: connectionConfig.username,
|
||||
@@ -156,8 +158,13 @@ export const GuacamoleDisplay = forwardRef<
|
||||
token = data.token;
|
||||
}
|
||||
|
||||
const width = connectionConfig.width ?? containerWidth ?? 1280;
|
||||
const height = connectionConfig.height ?? containerHeight ?? 720;
|
||||
const displaySize = getGuacamoleDisplaySize(
|
||||
connectionConfig.width ?? containerWidth ?? 1280,
|
||||
connectionConfig.height ?? containerHeight ?? 720,
|
||||
connectionProtocol,
|
||||
window.devicePixelRatio,
|
||||
connectionConfig.dpi,
|
||||
);
|
||||
|
||||
const wsBase = buildGuacamoleWebSocketBaseUrl({
|
||||
isDev,
|
||||
@@ -171,9 +178,10 @@ export const GuacamoleDisplay = forwardRef<
|
||||
|
||||
const params = new URLSearchParams({
|
||||
token,
|
||||
width: String(width),
|
||||
height: String(height),
|
||||
width: String(displaySize.width),
|
||||
height: String(displaySize.height),
|
||||
});
|
||||
if (displaySize.dpi) params.set("dpi", String(displaySize.dpi));
|
||||
return `${wsBase}?${params.toString()}`;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
@@ -434,9 +442,14 @@ export const GuacamoleDisplay = forwardRef<
|
||||
onConnect?.();
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const w = Math.round(rect.width);
|
||||
const h = Math.round(rect.height);
|
||||
if (w > 0 && h > 0) client.sendSize(w, h);
|
||||
const size = getGuacamoleDisplaySize(
|
||||
rect.width,
|
||||
rect.height,
|
||||
protocol,
|
||||
window.devicePixelRatio,
|
||||
connectionConfig.dpi,
|
||||
);
|
||||
client.sendSize(size.width, size.height);
|
||||
}
|
||||
rescaleDisplay(false);
|
||||
break;
|
||||
@@ -515,6 +528,7 @@ export const GuacamoleDisplay = forwardRef<
|
||||
rescaleDisplay,
|
||||
connectionConfig.protocol,
|
||||
connectionConfig.type,
|
||||
connectionConfig.dpi,
|
||||
t,
|
||||
]);
|
||||
|
||||
@@ -588,10 +602,15 @@ export const GuacamoleDisplay = forwardRef<
|
||||
resizeTimeoutRef.current = setTimeout(() => {
|
||||
if (clientRef.current && containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const w = Math.round(rect.width);
|
||||
const h = Math.round(rect.height);
|
||||
if (w > 0 && h > 0) {
|
||||
clientRef.current.sendSize(w, h);
|
||||
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);
|
||||
rescaleDisplay(true);
|
||||
}
|
||||
}
|
||||
@@ -603,7 +622,12 @@ export const GuacamoleDisplay = forwardRef<
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [rescaleDisplay]);
|
||||
}, [
|
||||
connectionConfig.dpi,
|
||||
connectionConfig.protocol,
|
||||
connectionConfig.type,
|
||||
rescaleDisplay,
|
||||
]);
|
||||
|
||||
const syncClipboard = useCallback(() => {
|
||||
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