mirror of
https://github.com/netbirdio/IronRDP.git
synced 2026-05-22 18:43:12 -07:00
feat(web): allow dynamic resize for web (#550)
This commit is contained in:
@@ -30,6 +30,7 @@ ironrdp = { workspace = true, features = [
|
||||
"dvc",
|
||||
"cliprdr",
|
||||
"svc",
|
||||
"displaycontrol"
|
||||
] }
|
||||
ironrdp-core.workspace = true
|
||||
ironrdp-cliprdr-format = { workspace = true }
|
||||
|
||||
@@ -36,6 +36,11 @@ impl Canvas {
|
||||
Ok(Self { width, surface })
|
||||
}
|
||||
|
||||
pub(crate) fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) {
|
||||
self.surface.resize(width, height).expect("surface resize");
|
||||
self.width = width.get();
|
||||
}
|
||||
|
||||
pub(crate) fn draw(&mut self, buffer: &[u8], region: InclusiveRectangle) -> anyhow::Result<()> {
|
||||
let region_width = region.width();
|
||||
let region_height = region.height();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use core::cell::RefCell;
|
||||
use std::borrow::Cow;
|
||||
use std::num::NonZeroU32;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -18,6 +19,8 @@ use ironrdp::cliprdr::CliprdrClient;
|
||||
use ironrdp::connector::connection_activation::ConnectionActivationState;
|
||||
use ironrdp::connector::credssp::KerberosConfig;
|
||||
use ironrdp::connector::{self, ClientConnector, Credentials};
|
||||
use ironrdp::displaycontrol::client::DisplayControlClient;
|
||||
use ironrdp::dvc::DrdynvcClient;
|
||||
use ironrdp::graphics::image_processing::PixelFormat;
|
||||
use ironrdp::pdu::input::fast_path::FastPathInputEvent;
|
||||
use ironrdp::pdu::rdp::client_info::PerformanceFlags;
|
||||
@@ -64,6 +67,8 @@ struct SessionBuilderInner {
|
||||
remote_clipboard_changed_callback: Option<js_sys::Function>,
|
||||
remote_received_format_list_callback: Option<js_sys::Function>,
|
||||
force_clipboard_update_callback: Option<js_sys::Function>,
|
||||
|
||||
use_display_control: bool,
|
||||
}
|
||||
|
||||
impl Default for SessionBuilderInner {
|
||||
@@ -89,6 +94,8 @@ impl Default for SessionBuilderInner {
|
||||
remote_clipboard_changed_callback: None,
|
||||
remote_received_format_list_callback: None,
|
||||
force_clipboard_update_callback: None,
|
||||
|
||||
use_display_control: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,6 +216,12 @@ impl SessionBuilder {
|
||||
self.clone()
|
||||
}
|
||||
|
||||
/// Optional
|
||||
pub fn use_display_control(&self) -> SessionBuilder {
|
||||
self.0.borrow_mut().use_display_control = true;
|
||||
self.clone()
|
||||
}
|
||||
|
||||
pub async fn connect(&self) -> Result<Session, IronRdpError> {
|
||||
let (
|
||||
username,
|
||||
@@ -301,15 +314,18 @@ impl SessionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
let (connection_result, ws) = connect(
|
||||
let use_display_control = self.0.borrow().use_display_control;
|
||||
|
||||
let (connection_result, ws) = connect(ConnectParams {
|
||||
ws,
|
||||
config,
|
||||
auth_token,
|
||||
proxy_auth_token: auth_token,
|
||||
destination,
|
||||
pcb,
|
||||
kdc_proxy_url,
|
||||
clipboard.as_ref().map(|clip| clip.backend()),
|
||||
)
|
||||
clipboard_backend: clipboard.as_ref().map(|clip| clip.backend()),
|
||||
use_display_control,
|
||||
})
|
||||
.await?;
|
||||
|
||||
info!("Connected!");
|
||||
@@ -345,6 +361,12 @@ pub(crate) enum RdpInputEvent {
|
||||
Cliprdr(ClipboardMessage),
|
||||
ClipboardBackend(WasmClipboardBackendMessage),
|
||||
FastPath(FastPathInputEvents),
|
||||
Resize {
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_size: Option<(u32, u32)>,
|
||||
},
|
||||
TerminateSession,
|
||||
}
|
||||
|
||||
@@ -489,6 +511,21 @@ impl Session {
|
||||
active_stage.process_fastpath_input(&mut image, &events)
|
||||
.context("fast path input events processing")?
|
||||
}
|
||||
RdpInputEvent::Resize { width, height, scale_factor, physical_size } => {
|
||||
debug!(width, height, scale_factor, "Resize event received");
|
||||
if width == 0 || height == 0 {
|
||||
warn!("Resize event ignored: width or height is zero");
|
||||
Vec::new()
|
||||
} else if let Some(response_frame) = active_stage.encode_resize(width, height, scale_factor, physical_size) {
|
||||
self.render_canvas.set_width(width);
|
||||
self.render_canvas.set_height(height);
|
||||
gui.resize(NonZeroU32::new(width).unwrap(), NonZeroU32::new(height).unwrap());
|
||||
vec![ActiveStageOutput::ResponseFrame(response_frame?)]
|
||||
} else {
|
||||
debug!("Resize event ignored");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
RdpInputEvent::TerminateSession => {
|
||||
active_stage.graceful_shutdown()
|
||||
.context("graceful shutdown")?
|
||||
@@ -757,6 +794,24 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resize(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
scale_factor: Option<u32>,
|
||||
physical_width: Option<u32>,
|
||||
physical_height: Option<u32>,
|
||||
) {
|
||||
self.input_events_tx
|
||||
.unbounded_send(RdpInputEvent::Resize {
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
physical_size: physical_width.and_then(|width| physical_height.map(|height| (width, height))),
|
||||
})
|
||||
.expect("send resize event to writer task");
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn supports_unicode_keyboard_shortcuts(&self) -> bool {
|
||||
// RDP does not support Unicode keyboard shortcuts (When key combinations are executed, only
|
||||
@@ -832,7 +887,7 @@ async fn writer_task(rx: mpsc::UnboundedReceiver<Vec<u8>>, rdp_writer: WriteHalf
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
struct ConnectParams {
|
||||
ws: WebSocket,
|
||||
config: connector::Config,
|
||||
proxy_auth_token: String,
|
||||
@@ -840,6 +895,20 @@ async fn connect(
|
||||
pcb: Option<String>,
|
||||
kdc_proxy_url: Option<String>,
|
||||
clipboard_backend: Option<WasmClipboardBackend>,
|
||||
use_display_control: bool,
|
||||
}
|
||||
|
||||
async fn connect(
|
||||
ConnectParams {
|
||||
ws,
|
||||
config,
|
||||
proxy_auth_token,
|
||||
destination,
|
||||
pcb,
|
||||
kdc_proxy_url,
|
||||
clipboard_backend,
|
||||
use_display_control,
|
||||
}: ConnectParams,
|
||||
) -> Result<(connector::ConnectionResult, WebSocket), IronRdpError> {
|
||||
let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws);
|
||||
|
||||
@@ -849,6 +918,12 @@ async fn connect(
|
||||
connector.attach_static_channel(CliprdrClient::new(Box::new(clipboard_backend)));
|
||||
}
|
||||
|
||||
if use_display_control {
|
||||
connector.attach_static_channel(
|
||||
DrdynvcClient::new().with_dynamic_channel(DisplayControlClient::new(|_| Ok(Vec::new()))),
|
||||
);
|
||||
}
|
||||
|
||||
let (upgraded, server_public_key) =
|
||||
connect_rdcleanpath(&mut framed, &mut connector, destination.clone(), proxy_auth_token, pcb).await?;
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface UserInteraction {
|
||||
desktopSize?: DesktopSize,
|
||||
preConnectionBlob?: string,
|
||||
kdc_proxy_url?: string,
|
||||
use_display_control?: boolean,
|
||||
): Promise<NewSessionInfo>;
|
||||
|
||||
setKeyboardUnicodeMode(use_unicode: boolean): void;
|
||||
@@ -31,4 +32,6 @@ export interface UserInteraction {
|
||||
setCursorStyleOverride(style: string | null): void;
|
||||
|
||||
onSessionEvent(callback: (event: SessionEvent) => void): void;
|
||||
|
||||
resize(width: number, height: number, scale?: number): void;
|
||||
}
|
||||
|
||||
@@ -445,6 +445,11 @@
|
||||
scaleSession(s);
|
||||
});
|
||||
|
||||
wasmService.dynamicResize.subscribe((evt) => {
|
||||
loggingService.info(`Dynamic resize!, width: ${evt.width}, height: ${evt.height}`);
|
||||
setViewerStyle(evt.width.toString(), evt.height.toString(), true);
|
||||
});
|
||||
|
||||
wasmService.changeVisibilityObservable.subscribe((val) => {
|
||||
isVisible = val;
|
||||
if (val) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export class PublicAPI {
|
||||
desktopSize?: DesktopSize,
|
||||
preConnectionBlob?: string,
|
||||
kdc_proxy_url?: string,
|
||||
use_display_control = false,
|
||||
): Promise<NewSessionInfo> {
|
||||
loggingService.info('Initializing connection.');
|
||||
const resultObservable = this.wasmService.connect(
|
||||
@@ -35,6 +36,7 @@ export class PublicAPI {
|
||||
desktopSize,
|
||||
preConnectionBlob,
|
||||
kdc_proxy_url,
|
||||
use_display_control,
|
||||
);
|
||||
|
||||
return resultObservable.toPromise();
|
||||
@@ -69,6 +71,10 @@ export class PublicAPI {
|
||||
this.wasmService.setCursorStyleOverride(style);
|
||||
}
|
||||
|
||||
private resize(width: number, height: number, scale?: number) {
|
||||
this.wasmService.resizeDynamic(width, height, scale);
|
||||
}
|
||||
|
||||
getExposedFunctions(): UserInteraction {
|
||||
return {
|
||||
setVisibility: this.setVisibility.bind(this),
|
||||
@@ -82,6 +88,7 @@ export class PublicAPI {
|
||||
shutdown: this.shutdown.bind(this),
|
||||
setKeyboardUnicodeMode: this.setKeyboardUnicodeMode.bind(this),
|
||||
setCursorStyleOverride: this.setCursorStyleOverride.bind(this),
|
||||
resize: this.resize.bind(this),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ export class WasmBridgeService {
|
||||
sessionObserver: Observable<SessionEvent> = this.sessionEvent.asObservable();
|
||||
scaleObserver: Observable<ScreenScale> = this.scale.asObservable();
|
||||
|
||||
dynamicResize = new Subject<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
|
||||
constructor() {
|
||||
this.resize = this._resize.asObservable();
|
||||
loggingService.info('Web bridge initialized.');
|
||||
@@ -131,6 +136,7 @@ export class WasmBridgeService {
|
||||
desktopSize?: IDesktopSize,
|
||||
preConnectionBlob?: string,
|
||||
kdc_proxy_url?: string,
|
||||
use_display_control = true,
|
||||
): Observable<NewSessionInfo> {
|
||||
const sessionBuilder = SessionBuilder.new();
|
||||
sessionBuilder.proxy_address(proxyAddress);
|
||||
@@ -143,6 +149,7 @@ export class WasmBridgeService {
|
||||
sessionBuilder.set_cursor_style_callback_context(this);
|
||||
sessionBuilder.set_cursor_style_callback(this.setCursorStyleCallback);
|
||||
sessionBuilder.kdc_proxy_url(kdc_proxy_url);
|
||||
use_display_control && sessionBuilder.use_display_control();
|
||||
|
||||
if (preConnectionBlob != null) {
|
||||
sessionBuilder.pcb(preConnectionBlob);
|
||||
@@ -253,6 +260,11 @@ export class WasmBridgeService {
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
resizeDynamic(width: number, height: number, scale?: number) {
|
||||
this.dynamicResize.next({ width, height });
|
||||
this.session?.resize(width, height, scale);
|
||||
}
|
||||
|
||||
/// Triggered by the browser when local clipboard is updated. Clipboard backend should
|
||||
/// cache the content and send it to the server when it is requested.
|
||||
onClipboardChanged(transaction: ClipboardTransaction): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" style="height: 100%; margin: 0; padding: 0">
|
||||
<html lang="en" style="height: 100%; margin: 0; padding: 0; overflow: hidden">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
@@ -13,7 +13,17 @@
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body class="light" style="height: 100%; margin: 0; padding: 0">
|
||||
<body class="light no-scroll-bar" style="height: 100%; margin: 0; padding: 0">
|
||||
<div style="display: contents" class="mdc-typography--font-family">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.no-scroll-bar {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scroll-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
height: 768,
|
||||
};
|
||||
let pcb: string;
|
||||
let pop_up = false;
|
||||
|
||||
let userInteraction: UserInteraction;
|
||||
|
||||
@@ -53,6 +54,28 @@
|
||||
type: 'info',
|
||||
message: 'Connection in progress...',
|
||||
});
|
||||
|
||||
if (pop_up) {
|
||||
const data = JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
hostname,
|
||||
gatewayAddress,
|
||||
domain,
|
||||
authtoken,
|
||||
desktopSize,
|
||||
pcb,
|
||||
kdc_proxy_url,
|
||||
});
|
||||
const base64Data = btoa(data);
|
||||
window.open(
|
||||
`/popup-session?data=${base64Data}`,
|
||||
'_blank',
|
||||
`width=${desktopSize.width},height=${desktopSize.height},resizable=yes,scrollbars=yes,status=yes`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
from(
|
||||
userInteraction.connect(
|
||||
username,
|
||||
@@ -64,6 +87,7 @@
|
||||
desktopSize,
|
||||
pcb,
|
||||
kdc_proxy_url,
|
||||
true,
|
||||
),
|
||||
)
|
||||
.pipe(
|
||||
@@ -149,6 +173,17 @@
|
||||
<input id="kdc_proxy_url" type="text" bind:value={kdc_proxy_url} />
|
||||
<label for="kdc_proxy_url">KDC Proxy URL</label>
|
||||
</div>
|
||||
<div class="field label border">
|
||||
<div style="display: flex; height: 100%; align-items: center; font-size: 1.5em;">
|
||||
<input
|
||||
id="use_pop_up"
|
||||
type="checkbox"
|
||||
bind:value={pop_up}
|
||||
style="width: 1.5em; height: 1.5em; margin-right: 0.5em;"
|
||||
/>
|
||||
<label for="use_pop_up">Use Pop Up</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="center-align">
|
||||
<button on:click={StartSession}>Login</button>
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { setCurrentSessionActive, userInteractionService } from '../../services/session.service';
|
||||
import type { UserInteraction } from '../../../static/iron-remote-gui';
|
||||
|
||||
let uiService: UserInteraction;
|
||||
let cursorOverrideActive = false;
|
||||
let showUtilityBar = false;
|
||||
|
||||
userInteractionService.subscribe((uis) => {
|
||||
if (uis != null) {
|
||||
uiService = uis;
|
||||
uiService.onSessionEvent((event) => {
|
||||
if (event.type === 0) {
|
||||
uiService.setVisibility(true);
|
||||
} else if (event.type === 1) {
|
||||
setCurrentSessionActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
userInteractionService.subscribe((uis) => {
|
||||
if (uis != null) {
|
||||
uiService = uis;
|
||||
//read query params named data
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const data = urlParams.get('data');
|
||||
if (data == null) {
|
||||
console.warn('No data found in query params');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedData = JSON.parse(atob(data));
|
||||
const { hostname, gatewayAddress, domain, username, password, authtoken, kdc_proxy_url, pcb, desktopSize } =
|
||||
parsedData;
|
||||
uiService
|
||||
.connect(
|
||||
username,
|
||||
password,
|
||||
hostname,
|
||||
gatewayAddress,
|
||||
domain,
|
||||
authtoken,
|
||||
desktopSize,
|
||||
pcb,
|
||||
kdc_proxy_url,
|
||||
true,
|
||||
)
|
||||
.then(() => {
|
||||
uiService.setVisibility(true);
|
||||
window.onresize = onWindowResize;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function onWindowResize() {
|
||||
const innerWidth = window.innerWidth;
|
||||
const innerHeight = window.innerHeight;
|
||||
uiService.resize(innerWidth, innerHeight);
|
||||
}
|
||||
|
||||
function onUnicodeModeChange(e: MouseEvent) {
|
||||
if (e.target == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = e.target as HTMLInputElement;
|
||||
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiService.setKeyboardUnicodeMode(element.checked);
|
||||
}
|
||||
|
||||
function toggleCursorKind() {
|
||||
if (cursorOverrideActive) {
|
||||
uiService.setCursorStyleOverride(null);
|
||||
} else {
|
||||
uiService.setCursorStyleOverride('url("crosshair.png") 7 7, default');
|
||||
}
|
||||
|
||||
cursorOverrideActive = !cursorOverrideActive;
|
||||
}
|
||||
|
||||
function toggleFullScreen() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
} else {
|
||||
document.documentElement.requestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const el = document.querySelector('iron-remote-gui');
|
||||
|
||||
if (el == null) {
|
||||
throw '`iron-remote-gui` element not found';
|
||||
}
|
||||
|
||||
el.addEventListener('ready', (e) => {
|
||||
const event = e as CustomEvent;
|
||||
userInteractionService.set(event.detail.irgUserInteraction);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="popup-screen"
|
||||
style="display: flex; height: 100%; flex-direction: column; background-color: #2e2e2e; position: relative"
|
||||
on:mousemove={(event) => {
|
||||
if (event.clientY < 100) {
|
||||
showUtilityBar = true;
|
||||
} else {
|
||||
showUtilityBar = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="tool-bar" class:hidden={!showUtilityBar}>
|
||||
<div class="toolbar-container">
|
||||
<button on:click={() => toggleFullScreen()}>Full Screen</button>
|
||||
<button on:click={() => uiService.ctrlAltDel()}>Ctrl+Alt+Del</button>
|
||||
<button on:click={() => uiService.metaKey()}>
|
||||
Meta
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 512 512">
|
||||
<title>ionicons-v5_logos</title>
|
||||
<path d="M480,265H232V444l248,36V265Z" />
|
||||
<path d="M216,265H32V415l184,26.7V265Z" />
|
||||
<path d="M480,32,232,67.4V249H480V32Z" />
|
||||
<path d="M216,69.7,32,96V249H216V69.7Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button on:click={() => toggleCursorKind()}>Toggle cursor kind</button>
|
||||
<button on:click={() => uiService.shutdown()}>Terminate Session</button>
|
||||
<label style="color: white;">
|
||||
<input on:click={(e) => onUnicodeModeChange(e)} type="checkbox" />
|
||||
Unicode keyboard mode
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<iron-remote-gui debugwasm="INFO" verbose="true" scale="fit" flexcenter="true" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tool-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 50%;
|
||||
background: rgba(0, 0, 0, 0.7); /* 70% opacity */
|
||||
color: white;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
gap: 10px; /* Spacing between buttons */
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #444;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em; /* Smaller button size */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Message from '$lib/messages/message.svelte';
|
||||
import PopupScreen from '$lib/popup-screen/popup-screen.svelte';
|
||||
</script>
|
||||
|
||||
<PopupScreen />
|
||||
<Message></Message>
|
||||
Reference in New Issue
Block a user