feat(web): allow dynamic resize for web (#550)

This commit is contained in:
irvingouj @ Devolutions
2024-09-19 19:08:45 +00:00
committed by GitHub
parent 3d3d9f2c56
commit c04bc2d29c
11 changed files with 354 additions and 7 deletions
+1
View File
@@ -30,6 +30,7 @@ ironrdp = { workspace = true, features = [
"dvc",
"cliprdr",
"svc",
"displaycontrol"
] }
ironrdp-core.workspace = true
ironrdp-cliprdr-format = { workspace = true }
+5
View File
@@ -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();
+80 -5
View File
@@ -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> {
+12 -2
View File
@@ -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>