From b06a393577580c097d97fb419e2d85489e41e174 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 17 Sep 2024 12:21:00 -0400 Subject: [PATCH] feat(web): allow dynamic resize for web --- crates/ironrdp-web/Cargo.toml | 1 + crates/ironrdp-web/src/canvas.rs | 5 + crates/ironrdp-web/src/session.rs | 68 +++++++ .../src/interfaces/UserInteraction.ts | 3 + .../src/iron-remote-gui.svelte | 5 + .../iron-remote-gui/src/services/PublicAPI.ts | 7 + .../src/services/wasm-bridge.service.ts | 12 ++ web-client/iron-svelte-client/src/app.html | 15 +- .../src/lib/login/login.svelte | 30 +++ .../src/lib/popup-screen/popup-screen.svelte | 173 ++++++++++++++++++ .../src/routes/popup-session/+page.svelte | 8 + 11 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 web-client/iron-svelte-client/src/lib/popup-screen/popup-screen.svelte create mode 100644 web-client/iron-svelte-client/src/routes/popup-session/+page.svelte diff --git a/crates/ironrdp-web/Cargo.toml b/crates/ironrdp-web/Cargo.toml index 8159b8403..f5964a497 100644 --- a/crates/ironrdp-web/Cargo.toml +++ b/crates/ironrdp-web/Cargo.toml @@ -30,6 +30,7 @@ ironrdp = { workspace = true, features = [ "dvc", "cliprdr", "svc", + "displaycontrol" ] } ironrdp-core.workspace = true ironrdp-cliprdr-format = { workspace = true } diff --git a/crates/ironrdp-web/src/canvas.rs b/crates/ironrdp-web/src/canvas.rs index 963c594ad..f1b02877f 100644 --- a/crates/ironrdp-web/src/canvas.rs +++ b/crates/ironrdp-web/src/canvas.rs @@ -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(); diff --git a/crates/ironrdp-web/src/session.rs b/crates/ironrdp-web/src/session.rs index dca625a88..a2d0f9d52 100644 --- a/crates/ironrdp-web/src/session.rs +++ b/crates/ironrdp-web/src/session.rs @@ -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, remote_received_format_list_callback: Option, force_clipboard_update_callback: Option, + + 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 { let ( username, @@ -309,6 +322,7 @@ impl SessionBuilder { pcb, kdc_proxy_url, clipboard.as_ref().map(|clip| clip.backend()), + self.0.borrow().use_display_control, ) .await?; @@ -345,6 +359,12 @@ pub(crate) enum RdpInputEvent { Cliprdr(ClipboardMessage), ClipboardBackend(WasmClipboardBackendMessage), FastPath(FastPathInputEvents), + Resize { + width: u32, + height: u32, + scale_factor: Option, + physical_size: Option<(u32, u32)>, + }, TerminateSession, } @@ -489,6 +509,23 @@ impl Session { active_stage.process_fastpath_input(&mut image, &events) .context("fast path input events processing")? } + RdpInputEvent::Resize { width, height, scale_factor, physical_size } => { + info!(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) { + info!("Resize event processed"); + 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 { + info!("Resize event ignored"); + Vec::new() + } + }, RdpInputEvent::TerminateSession => { active_stage.graceful_shutdown() .context("graceful shutdown")? @@ -507,6 +544,7 @@ impl Session { ActiveStageOutput::GraphicsUpdate(region) => { // PERF: some copies and conversion could be optimized let (region, buffer) = extract_partial_image(&image, region); + info!(width=?image.width(),height=?image.height(),?region, gui_width = ?gui.width, "Graphic Update received"); gui.draw(&buffer, region).context("draw updated region")?; } ActiveStageOutput::PointerDefault => { @@ -757,6 +795,26 @@ impl Session { Ok(()) } + pub fn resize( + &self, + width: u32, + height: u32, + scale_factor: Option, + physical_width: Option, + physical_height: Option, + ) { + self.input_events_tx + .unbounded_send(RdpInputEvent::Resize { + width, + height, + scale_factor, + physical_size: physical_width + .map(|width| physical_height.map(|height| (width, height))) + .flatten(), + }) + .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 @@ -840,6 +898,7 @@ async fn connect( pcb: Option, kdc_proxy_url: Option, clipboard_backend: Option, + use_display_control: bool, ) -> Result<(connector::ConnectionResult, WebSocket), IronRdpError> { let mut framed = ironrdp_futures::LocalFuturesFramed::new(ws); @@ -849,6 +908,15 @@ 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?; diff --git a/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts b/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts index 83c8e07e4..8179e9264 100644 --- a/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts +++ b/web-client/iron-remote-gui/src/interfaces/UserInteraction.ts @@ -18,6 +18,7 @@ export interface UserInteraction { desktopSize?: DesktopSize, preConnectionBlob?: string, kdc_proxy_url?: string, + use_display_control?: boolean, ): Promise; setKeyboardUnicodeMode(use_unicode: boolean): void; @@ -31,4 +32,6 @@ export interface UserInteraction { setCursorStyleOverride(style: string | null): void; onSessionEvent(callback: (event: SessionEvent) => void): void; + + resizeDynamic(width: number, height: number, scale?: number): void; } diff --git a/web-client/iron-remote-gui/src/iron-remote-gui.svelte b/web-client/iron-remote-gui/src/iron-remote-gui.svelte index 2f78c61b1..40bd11ab1 100644 --- a/web-client/iron-remote-gui/src/iron-remote-gui.svelte +++ b/web-client/iron-remote-gui/src/iron-remote-gui.svelte @@ -445,6 +445,11 @@ scaleSession(s); }); + wasmService.dynamicResize.subscribe((evt) => { + loggingService.info('Dynamic resize!'); + setViewerStyle(evt.width.toString(), evt.height.toString(), true); + }); + wasmService.changeVisibilityObservable.subscribe((val) => { isVisible = val; if (val) { diff --git a/web-client/iron-remote-gui/src/services/PublicAPI.ts b/web-client/iron-remote-gui/src/services/PublicAPI.ts index c485cc090..53ac53490 100644 --- a/web-client/iron-remote-gui/src/services/PublicAPI.ts +++ b/web-client/iron-remote-gui/src/services/PublicAPI.ts @@ -23,6 +23,7 @@ export class PublicAPI { desktopSize?: DesktopSize, preConnectionBlob?: string, kdc_proxy_url?: string, + use_display_control = false, ): Promise { 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 resizeDynamic(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), + resizeDynamic: this.resizeDynamic.bind(this), }; } } diff --git a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts index 2d1d4cc03..69d06cc5f 100644 --- a/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts +++ b/web-client/iron-remote-gui/src/services/wasm-bridge.service.ts @@ -57,6 +57,11 @@ export class WasmBridgeService { sessionObserver: Observable = this.sessionEvent.asObservable(); scaleObserver: Observable = 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 = false, ): Observable { 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 { diff --git a/web-client/iron-svelte-client/src/app.html b/web-client/iron-svelte-client/src/app.html index 51f2a2200..5235875d8 100644 --- a/web-client/iron-svelte-client/src/app.html +++ b/web-client/iron-svelte-client/src/app.html @@ -1,5 +1,5 @@ - + @@ -13,7 +13,18 @@ %sveltekit.head% - +
%sveltekit.body%
+ + \ No newline at end of file diff --git a/web-client/iron-svelte-client/src/lib/login/login.svelte b/web-client/iron-svelte-client/src/lib/login/login.svelte index af37a1815..6ba8e6c17 100644 --- a/web-client/iron-svelte-client/src/lib/login/login.svelte +++ b/web-client/iron-svelte-client/src/lib/login/login.svelte @@ -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,12 @@ +
+
+ + +
+