1use std::borrow::Cow;
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Component, Path, PathBuf};
6use std::sync::{Arc, Mutex};
7
8use rdesktop_core::config::{AppConfig, WindowConfig};
9use rdesktop_core::ipc::{IpcHandler, IpcMessage, IpcResponseSender};
10use rdesktop_core::renderer::{Renderer, RendererKind, ResizeEdge};
11use rdesktop_core::window::WindowHandle;
12use rdesktop_core::{RdesktopError, Result};
13
14use tao::event::{Event, StartCause, WindowEvent};
15use tao::event_loop::{ControlFlow, EventLoopBuilder};
16use tao::window::{Window, WindowBuilder, WindowId};
17use wry::http::{Request, Response};
18#[cfg(target_os = "windows")]
19use wry::WebViewBuilderExtWindows;
20use wry::{WebContext, WebView, WebViewBuilder};
21
22struct WindowEntry {
23 window: Window,
24 webview: WebView,
25}
26
27fn serve_asset(root: &Path, request: Request<Vec<u8>>) -> Response<Cow<'static, [u8]>> {
28 let request_path = request.uri().path().trim_start_matches('/');
29 let request_path = percent_encoding::percent_decode_str(request_path).decode_utf8_lossy();
30 let relative = Path::new(request_path.as_ref());
31
32 let invalid_path = relative.components().any(|component| {
33 matches!(
34 component,
35 Component::ParentDir | Component::RootDir | Component::Prefix(_)
36 )
37 });
38 if invalid_path {
39 return asset_response(403, "text/plain; charset=utf-8", b"forbidden".to_vec());
40 }
41
42 let relative = if request_path.is_empty() {
43 Path::new("index.html")
44 } else {
45 relative
46 };
47 let path = root.join(relative);
48 match fs::read(&path) {
49 Ok(bytes) => asset_response(200, content_type(&path), bytes),
50 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
51 asset_response(404, "text/plain; charset=utf-8", b"not found".to_vec())
52 }
53 Err(error) => {
54 tracing::error!(path = %path.display(), %error, "Failed to serve native asset");
55 asset_response(
56 500,
57 "text/plain; charset=utf-8",
58 b"asset read failed".to_vec(),
59 )
60 }
61 }
62}
63
64fn asset_response(status: u16, content_type: &str, body: Vec<u8>) -> Response<Cow<'static, [u8]>> {
65 Response::builder()
66 .status(status)
67 .header("Content-Type", content_type)
68 .header("Cache-Control", "no-cache")
69 .body(Cow::Owned(body))
70 .expect("valid native asset response")
71}
72
73fn content_type(path: &Path) -> &'static str {
74 match path
75 .extension()
76 .and_then(|ext| ext.to_str())
77 .unwrap_or_default()
78 {
79 "html" => "text/html; charset=utf-8",
80 "js" | "mjs" => "text/javascript; charset=utf-8",
81 "css" => "text/css; charset=utf-8",
82 "json" => "application/json; charset=utf-8",
83 "png" => "image/png",
84 "jpg" | "jpeg" => "image/jpeg",
85 "svg" => "image/svg+xml",
86 "wav" => "audio/wav",
87 "mp3" => "audio/mpeg",
88 "woff" => "font/woff",
89 "woff2" => "font/woff2",
90 _ => "application/octet-stream",
91 }
92}
93
94fn native_asset_url(url: &str, has_asset_root: bool) -> String {
99 #[cfg(target_os = "windows")]
100 if has_asset_root {
101 if let Some(rest) = url.strip_prefix("rdesktop://") {
102 return format!("http://rdesktop.{rest}");
103 }
104 }
105
106 url.to_string()
107}
108
109enum PendingOp {
111 LoadUrl(u64, String),
112 LoadHtml(u64, String),
113 EvalScript(u64, String),
114 SetTitle(u64, String),
115 SetSize(u64, u32, u32),
116 SetResizable(u64, bool),
117 SetVisible(u64, bool),
118 SendToFrontend(u64, String),
119 Close(u64),
120 Minimize(u64),
122 Maximize(u64),
123 SetFullscreen(u64, bool),
124 StartDrag(u64),
125 StartResize(u64, tao::window::ResizeDirection),
126 SetDecorations(u64, bool),
127 SetAlwaysOnTop(u64, bool),
128}
129
130type IpcResponseQueue = Arc<Mutex<Vec<(u64, String)>>>;
132
133type WindowCommandQueue = Arc<Mutex<Vec<WindowCommand>>>;
135
136struct WindowCommand {
138 rdesktop_id: u64,
139 action: WindowAction,
140}
141
142enum WindowAction {
143 Minimize,
144 Maximize,
145 Close,
146 StartDrag,
147 StartResize(tao::window::ResizeDirection),
148 SetFullscreen(bool),
149 SetDecorations(bool),
150}
151
152fn to_tao_resize(edge: ResizeEdge) -> tao::window::ResizeDirection {
154 match edge {
155 ResizeEdge::Top => tao::window::ResizeDirection::North,
156 ResizeEdge::Bottom => tao::window::ResizeDirection::South,
157 ResizeEdge::Left => tao::window::ResizeDirection::West,
158 ResizeEdge::Right => tao::window::ResizeDirection::East,
159 ResizeEdge::TopLeft => tao::window::ResizeDirection::NorthWest,
160 ResizeEdge::TopRight => tao::window::ResizeDirection::NorthEast,
161 ResizeEdge::BottomLeft => tao::window::ResizeDirection::SouthWest,
162 ResizeEdge::BottomRight => tao::window::ResizeDirection::SouthEast,
163 }
164}
165
166pub struct WebViewRenderer {
186 _config: AppConfig,
187 ipc_handler: Option<Arc<dyn IpcHandler>>,
188 pending_windows: RefCell<Vec<(u64, WindowConfig)>>,
189 pending_ops: RefCell<Vec<PendingOp>>,
190 next_window_id: RefCell<u64>,
191 asset_root: Option<PathBuf>,
192 data_directory: Option<PathBuf>,
193 outbox: Arc<Mutex<Vec<String>>>,
197}
198
199impl WebViewRenderer {
200 pub fn new(config: &AppConfig) -> Result<Self> {
201 Ok(Self {
202 _config: config.clone(),
203 ipc_handler: None,
204 pending_windows: RefCell::new(Vec::new()),
205 pending_ops: RefCell::new(Vec::new()),
206 next_window_id: RefCell::new(1),
207 asset_root: None,
208 data_directory: None,
209 outbox: Arc::new(Mutex::new(Vec::new())),
210 })
211 }
212
213 pub fn set_asset_root(&mut self, root: impl Into<PathBuf>) -> Result<()> {
220 let requested_root = root.into();
221 let root = std::fs::canonicalize(&requested_root).map_err(|error| {
222 RdesktopError::Config(format!(
223 "asset root is not accessible ({}): {error}",
224 requested_root.display()
225 ))
226 })?;
227 if !root.is_dir() {
228 return Err(RdesktopError::Config(format!(
229 "asset root is not a directory: {}",
230 root.display()
231 )));
232 }
233 self.asset_root = Some(root);
234 Ok(())
235 }
236
237 pub fn set_data_directory(&mut self, directory: impl Into<PathBuf>) -> Result<()> {
246 let directory = directory.into();
247 if !directory.is_absolute() {
248 return Err(RdesktopError::Config(
249 "webview data directory must be absolute".to_string(),
250 ));
251 }
252 if directory.exists() && !directory.is_dir() {
253 return Err(RdesktopError::Config(format!(
254 "webview data directory is not a directory: {}",
255 directory.display()
256 )));
257 }
258 self.data_directory = Some(directory);
259 Ok(())
260 }
261
262 pub fn set_outbox(&mut self, outbox: Arc<Mutex<Vec<String>>>) {
266 self.outbox = outbox;
267 }
268
269 fn next_id(&self) -> u64 {
270 let mut id = self.next_window_id.borrow_mut();
271 let current = *id;
272 *id += 1;
273 current
274 }
275
276 fn bridge_script() -> &'static str {
278 r#"
279 (function() {
280 if (window.__RDESKTOP_BRIDGE__) return;
281 window.__RDESKTOP_BRIDGE__ = true;
282 window.__RDESKTOP_RESOLVE__ = {};
283
284 // ── IPC Bridge ──────────────────────────────────────
285 window.__RDESKTOP_INVOKE__ = function(cmd, payload) {
286 return new Promise(function(resolve, reject) {
287 var id = Math.random().toString(36).slice(2);
288 window.__RDESKTOP_RESOLVE__[id] = resolve;
289 if (window.ipc && window.ipc.postMessage) {
290 window.ipc.postMessage(JSON.stringify({ id: id, cmd: cmd, payload: payload || {} }));
291 }
292 setTimeout(function() {
293 if (window.__RDESKTOP_RESOLVE__[id]) {
294 delete window.__RDESKTOP_RESOLVE__[id];
295 reject(new Error('IPC timeout'));
296 }
297 }, 120000);
298 });
299 };
300
301 window.__RDESKTOP_IPC__ = function(message) {
302 try {
303 var data = typeof message === 'string' ? JSON.parse(message) : message;
304 if (data.id && window.__RDESKTOP_RESOLVE__[data.id]) {
305 window.__RDESKTOP_RESOLVE__[data.id](data);
306 delete window.__RDESKTOP_RESOLVE__[data.id];
307 } else if (window.__RDESKTOP_PUSH__) {
308 // Unnamed push (e.g. extension host → UI event).
309 window.__RDESKTOP_PUSH__(data);
310 }
311 } catch (e) {
312 console.error('rdesktop IPC error:', e);
313 }
314 };
315
316 // ── Window Controls (frameless / custom title bar) ──
317 var postWindowCommand = function(action, extra) {
318 if (!window.ipc || !window.ipc.postMessage) return;
319 var payload = extra || {};
320 payload.__window__ = true;
321 payload.action = action;
322 window.ipc.postMessage(JSON.stringify({
323 id: 'window-' + Math.random().toString(36).slice(2),
324 cmd: 'rdesktop.window',
325 payload: payload
326 }));
327 };
328
329 window.__RDESKTOP_WINDOW__ = {
330 minimize: function() {
331 postWindowCommand('minimize');
332 },
333 maximize: function() {
334 postWindowCommand('maximize');
335 },
336 close: function() {
337 postWindowCommand('close');
338 },
339 startDrag: function() {
340 postWindowCommand('start_drag');
341 },
342 startResize: function(edge) {
343 postWindowCommand('start_resize', { edge: edge || 'bottom-right' });
344 },
345 setFullscreen: function(fs) {
346 postWindowCommand('set_fullscreen', { value: !!fs });
347 },
348 setDecorations: function(decorations) {
349 postWindowCommand('set_decorations', { value: !!decorations });
350 },
351 isMaximized: false,
352 isFullscreen: false
353 };
354 })();
355 "#
356 }
357
358 fn parse_window_payload(
361 payload: &serde_json::Value,
362 rdesktop_id: u64,
363 ) -> Option<WindowCommand> {
364 if payload
366 .get("__window__")
367 .and_then(|v| v.as_bool())
368 .unwrap_or(false)
369 {
370 let action = match payload["action"].as_str()? {
371 "minimize" => WindowAction::Minimize,
372 "maximize" => WindowAction::Maximize,
373 "close" => WindowAction::Close,
374 "start_drag" => WindowAction::StartDrag,
375 "start_resize" => {
376 let edge_str = payload["edge"].as_str().unwrap_or("bottom-right");
377 let dir = match edge_str {
378 "top" => tao::window::ResizeDirection::North,
379 "bottom" => tao::window::ResizeDirection::South,
380 "left" => tao::window::ResizeDirection::West,
381 "right" => tao::window::ResizeDirection::East,
382 "top-left" => tao::window::ResizeDirection::NorthWest,
383 "top-right" => tao::window::ResizeDirection::NorthEast,
384 "bottom-left" => tao::window::ResizeDirection::SouthWest,
385 _ => tao::window::ResizeDirection::SouthEast,
386 };
387 WindowAction::StartResize(dir)
388 }
389 "set_fullscreen" => {
390 let val = payload["value"].as_bool().unwrap_or(false);
391 WindowAction::SetFullscreen(val)
392 }
393 "set_decorations" => {
394 let val = payload["value"].as_bool().unwrap_or(true);
395 WindowAction::SetDecorations(val)
396 }
397 _ => return None,
398 };
399 return Some(WindowCommand {
400 rdesktop_id,
401 action,
402 });
403 }
404 None
405 }
406
407 fn parse_window_command(msg: &IpcMessage, rdesktop_id: u64) -> Option<WindowCommand> {
408 Self::parse_window_payload(&msg.payload, rdesktop_id)
409 }
410
411 fn parse_legacy_window_command(
412 raw: &serde_json::Value,
413 rdesktop_id: u64,
414 ) -> Option<WindowCommand> {
415 Self::parse_window_payload(raw, rdesktop_id)
416 }
417}
418
419fn physical_webview_bounds(width: u32, height: u32) -> wry::Rect {
420 wry::Rect {
421 position: tao::dpi::PhysicalPosition::<i32>::new(0, 0).into(),
422 size: tao::dpi::PhysicalSize::new(width, height).into(),
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn parses_formal_window_command_envelope() {
432 let message = IpcMessage {
433 id: "window-test".to_string(),
434 cmd: "rdesktop.window".to_string(),
435 payload: serde_json::json!({
436 "__window__": true,
437 "action": "close"
438 }),
439 };
440
441 assert!(WebViewRenderer::parse_window_command(&message, 1).is_some());
442 }
443
444 #[test]
445 fn parses_legacy_top_level_window_command() {
446 let raw = serde_json::json!({
447 "__window__": true,
448 "action": "minimize"
449 });
450
451 assert!(WebViewRenderer::parse_legacy_window_command(&raw, 1).is_some());
452 }
453
454 #[test]
455 fn parses_runtime_system_window_decoration_toggle() {
456 let raw = serde_json::json!({
457 "__window__": true,
458 "action": "set_decorations",
459 "value": true
460 });
461
462 assert!(matches!(
463 WebViewRenderer::parse_legacy_window_command(&raw, 1),
464 Some(WindowCommand {
465 action: WindowAction::SetDecorations(true),
466 ..
467 })
468 ));
469 }
470
471 #[test]
472 fn normalizes_runtime_asset_navigation_for_the_native_backend() {
473 assert_eq!(
474 native_asset_url("rdesktop://localhost/index.html", true),
475 if cfg!(target_os = "windows") {
476 "http://rdesktop.localhost/index.html"
477 } else {
478 "rdesktop://localhost/index.html"
479 }
480 );
481 assert_eq!(
482 native_asset_url("https://example.com", true),
483 "https://example.com"
484 );
485 assert_eq!(
486 native_asset_url("rdesktop://localhost/index.html", false),
487 "rdesktop://localhost/index.html"
488 );
489 }
490
491 #[test]
492 fn resize_bounds_preserve_physical_pixels_at_high_dpi() {
493 let bounds = physical_webview_bounds(2560, 1369);
494
495 assert!(matches!(
496 bounds.size,
497 tao::dpi::Size::Physical(size) if size.width == 2560 && size.height == 1369
498 ));
499 assert!(matches!(
500 bounds.position,
501 tao::dpi::Position::Physical(position) if position.x == 0 && position.y == 0
502 ));
503 }
504
505 #[test]
506 fn persistent_webview_data_directory_requires_an_absolute_path() {
507 let mut renderer = WebViewRenderer::new(&AppConfig::default()).unwrap();
508
509 assert!(renderer
510 .set_data_directory("relative/webview-data")
511 .is_err());
512 assert!(renderer.data_directory.is_none());
513 }
514
515 #[test]
516 fn persistent_webview_data_directory_accepts_a_missing_per_user_directory() {
517 let requested = std::env::temp_dir().join("rdesktop-webview-persistent-profile");
518 let mut renderer = WebViewRenderer::new(&AppConfig::default()).unwrap();
519
520 renderer.set_data_directory(&requested).unwrap();
521
522 assert_eq!(
523 renderer.data_directory.as_deref(),
524 Some(requested.as_path())
525 );
526 }
527}
528
529impl Renderer for WebViewRenderer {
530 fn init(&mut self) -> Result<()> {
531 tracing::info!("Initializing WebView renderer");
532 Ok(())
533 }
534
535 fn create_window(&mut self, config: &WindowConfig) -> Result<WindowHandle> {
536 let id = self.next_id();
537 self.pending_windows.borrow_mut().push((id, config.clone()));
538 tracing::info!(window_id = id, "Window queued for creation");
539 Ok(WindowHandle::new(id))
540 }
541
542 fn load_url(&self, window: WindowHandle, url: &str) -> Result<()> {
543 self.pending_ops
544 .borrow_mut()
545 .push(PendingOp::LoadUrl(window.id(), url.to_string()));
546 Ok(())
547 }
548
549 fn load_html(&self, window: WindowHandle, html: &str) -> Result<()> {
550 self.pending_ops
551 .borrow_mut()
552 .push(PendingOp::LoadHtml(window.id(), html.to_string()));
553 Ok(())
554 }
555
556 fn eval_script(&self, window: WindowHandle, script: &str) -> Result<()> {
557 self.pending_ops
558 .borrow_mut()
559 .push(PendingOp::EvalScript(window.id(), script.to_string()));
560 Ok(())
561 }
562
563 fn set_ipc_handler(&mut self, handler: Box<dyn IpcHandler>) {
564 self.ipc_handler = Some(Arc::from(handler));
565 }
566
567 fn send_to_frontend(&self, window: WindowHandle, message: &str) -> Result<()> {
568 self.pending_ops
569 .borrow_mut()
570 .push(PendingOp::SendToFrontend(window.id(), message.to_string()));
571 Ok(())
572 }
573
574 fn set_title(&self, window: WindowHandle, title: &str) -> Result<()> {
575 self.pending_ops
576 .borrow_mut()
577 .push(PendingOp::SetTitle(window.id(), title.to_string()));
578 Ok(())
579 }
580
581 fn set_size(&self, window: WindowHandle, width: u32, height: u32) -> Result<()> {
582 self.pending_ops
583 .borrow_mut()
584 .push(PendingOp::SetSize(window.id(), width, height));
585 Ok(())
586 }
587
588 fn set_resizable(&self, window: WindowHandle, resizable: bool) -> Result<()> {
589 self.pending_ops
590 .borrow_mut()
591 .push(PendingOp::SetResizable(window.id(), resizable));
592 Ok(())
593 }
594
595 fn set_visible(&self, window: WindowHandle, visible: bool) -> Result<()> {
596 self.pending_ops
597 .borrow_mut()
598 .push(PendingOp::SetVisible(window.id(), visible));
599 Ok(())
600 }
601
602 fn close_window(&mut self, window: WindowHandle) -> Result<()> {
603 self.pending_ops
604 .borrow_mut()
605 .push(PendingOp::Close(window.id()));
606 Ok(())
607 }
608
609 fn minimize_window(&self, window: WindowHandle) -> Result<()> {
612 self.pending_ops
613 .borrow_mut()
614 .push(PendingOp::Minimize(window.id()));
615 Ok(())
616 }
617
618 fn maximize_window(&self, window: WindowHandle) -> Result<()> {
619 self.pending_ops
620 .borrow_mut()
621 .push(PendingOp::Maximize(window.id()));
622 Ok(())
623 }
624
625 fn is_maximized(&self, _window: WindowHandle) -> Result<bool> {
626 Ok(false)
629 }
630
631 fn set_fullscreen(&self, window: WindowHandle, fullscreen: bool) -> Result<()> {
632 self.pending_ops
633 .borrow_mut()
634 .push(PendingOp::SetFullscreen(window.id(), fullscreen));
635 Ok(())
636 }
637
638 fn is_fullscreen(&self, _window: WindowHandle) -> Result<bool> {
639 Ok(false)
640 }
641
642 fn start_drag(&self, window: WindowHandle) -> Result<()> {
643 self.pending_ops
644 .borrow_mut()
645 .push(PendingOp::StartDrag(window.id()));
646 Ok(())
647 }
648
649 fn start_resize(&self, window: WindowHandle, edge: ResizeEdge) -> Result<()> {
650 self.pending_ops
651 .borrow_mut()
652 .push(PendingOp::StartResize(window.id(), to_tao_resize(edge)));
653 Ok(())
654 }
655
656 fn set_decorations(&self, window: WindowHandle, decorations: bool) -> Result<()> {
657 self.pending_ops
658 .borrow_mut()
659 .push(PendingOp::SetDecorations(window.id(), decorations));
660 Ok(())
661 }
662
663 fn set_always_on_top(&self, window: WindowHandle, always: bool) -> Result<()> {
664 self.pending_ops
665 .borrow_mut()
666 .push(PendingOp::SetAlwaysOnTop(window.id(), always));
667 Ok(())
668 }
669
670 fn run(mut self: Box<Self>) -> Result<()> {
673 tracing::info!("Starting WebView event loop");
674
675 let ipc_handler = self.ipc_handler.take();
676 let webgpu_enabled = self._config.renderer.webgpu;
677 let asset_root = self.asset_root.clone();
678 let data_directory = self.data_directory.clone();
679 let pending_windows: Vec<(u64, WindowConfig)> =
680 self.pending_windows.borrow_mut().drain(..).collect();
681 let pending_ops: Vec<PendingOp> = self.pending_ops.borrow_mut().drain(..).collect();
682
683 let ipc_response_queue: IpcResponseQueue = Arc::new(Mutex::new(Vec::new()));
684 let ipc_queue_for_handler = ipc_response_queue.clone();
685
686 let outbox_for_loop = self.outbox.clone();
688
689 let global_handler = rdesktop_core::PushHandler::new(self.outbox.clone());
694 let _hotkey_manager = {
695 let mgr = rdesktop_core::HotkeyManager::new(global_handler.clone());
696 for (i, hc) in self._config.hotkeys.iter().enumerate() {
697 if let Ok(hk) = hc.combo.parse::<rdesktop_core::Hotkey>() {
698 let id = i as u32 + 1;
699 if let Err(e) = mgr.register(id, &hk) {
700 tracing::warn!("failed to register hotkey {:?}: {}", hc.combo, e);
701 }
702 } else {
703 tracing::warn!("invalid hotkey combo: {:?}", hc.combo);
704 }
705 }
706 mgr
707 };
708 let _input_manager = if self._config.global_input.enabled {
709 let mut inp = rdesktop_core::GlobalInput::new(global_handler.clone());
710 if self._config.global_input.mouse_move {
711 inp = inp.with_mouse_move(true);
712 }
713 match inp.start() {
714 Ok(()) => Some(inp),
715 Err(e) => {
716 tracing::warn!("failed to start global input: {}", e);
717 None
718 }
719 }
720 } else {
721 None
722 };
723
724 let window_cmd_queue: WindowCommandQueue = Arc::new(Mutex::new(Vec::new()));
726 let window_cmd_queue_for_ipc = window_cmd_queue.clone();
727
728 let first_window_id: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
731
732 let event_loop = EventLoopBuilder::new().build();
733 let event_loop_proxy = event_loop.create_proxy();
734 let mut windows: HashMap<WindowId, WindowEntry> = HashMap::new();
735 let mut rdesktop_to_tao: HashMap<u64, WindowId> = HashMap::new();
736 let mut tao_to_rdesktop: HashMap<WindowId, u64> = HashMap::new();
737 let mut web_context = data_directory.map(|path| WebContext::new(Some(path)));
741
742 event_loop.run(move |event, event_loop_target, control_flow| {
743 *control_flow = ControlFlow::Wait;
744
745 match event {
746 Event::NewEvents(StartCause::Init) => {
747 let event_loop_proxy = event_loop_proxy.clone();
748 for (rdesktop_id, window_config) in &pending_windows {
750 let window = match WindowBuilder::new()
751 .with_title(&window_config.title)
752 .with_inner_size(tao::dpi::LogicalSize::new(
753 window_config.width,
754 window_config.height,
755 ))
756 .with_resizable(window_config.resizable)
757 .with_decorations(window_config.decorations)
758 .with_transparent(window_config.transparent)
759 .with_always_on_top(window_config.always_on_top)
760 .with_window_icon(rdesktop_core::window_icon(window_config))
761 .build(event_loop_target)
762 {
763 Ok(w) => w,
764 Err(e) => {
765 tracing::error!("Failed to create window {}: {}", rdesktop_id, e);
766 continue;
767 }
768 };
769
770 let tao_id = window.id();
771
772 rdesktop_core::apply_window_attributes(&window, window_config);
774
775 let mut builder = if let Some(context) = web_context.as_mut() {
776 WebViewBuilder::new_with_web_context(context)
777 } else {
778 WebViewBuilder::new()
779 }
780 .with_url("about:blank")
781 .with_devtools(cfg!(debug_assertions))
782 .with_initialization_script(Self::bridge_script());
783
784 if let Some(root) = asset_root.clone() {
785 builder = builder.with_custom_protocol(
786 "rdesktop".to_string(),
787 move |_webview_id, request| serve_asset(&root, request),
788 );
789 }
790
791 if window_config.transparent {
794 builder = builder.with_transparent(true);
795 }
796 #[cfg(target_os = "windows")]
802 if webgpu_enabled {
803 builder = builder.with_additional_browser_args(
804 "--enable-features=Vulkan,WebGPU --enable-unsafe-webgpu",
805 );
806 }
807
808 if let Some(ref handler) = ipc_handler {
810 let handler = handler.clone();
811 let queue = ipc_queue_for_handler.clone();
812 let win_queue = window_cmd_queue_for_ipc.clone();
813 let wake_proxy = event_loop_proxy.clone();
814 let _first_id = first_window_id.clone();
815 let rd_id = *rdesktop_id;
816
817 builder =
818 builder.with_ipc_handler(move |req: wry::http::Request<String>| {
819 let body = req.body();
820
821 if let Ok(raw) = serde_json::from_str::<serde_json::Value>(body)
824 {
825 if let Some(cmd) =
826 WebViewRenderer::parse_legacy_window_command(
827 &raw, rd_id,
828 )
829 {
830 if let Ok(mut q) = win_queue.lock() {
831 q.push(cmd);
832 }
833 let _ = wake_proxy.send_event(());
834 return;
835 }
836
837 if let Ok(msg) = serde_json::from_value::<IpcMessage>(raw) {
838 if let Some(cmd) =
840 WebViewRenderer::parse_window_command(&msg, rd_id)
841 {
842 if let Ok(mut q) = win_queue.lock() {
843 q.push(cmd);
844 }
845 } else {
846 let request_handler = handler.clone();
851 let response_queue = queue.clone();
852 let response_wake = wake_proxy.clone();
853 let response_sink: IpcResponseSender =
854 Arc::new(move |response| {
855 if let Ok(json) =
856 serde_json::to_string(&response)
857 {
858 if let Ok(mut q) = response_queue.lock()
859 {
860 q.push((rd_id, json));
861 }
862 }
863 let _ = response_wake.send_event(());
864 });
865 let thread_name =
866 format!("rdesktop-ipc-{rd_id}-{}", msg.id);
867 let _ = std::thread::Builder::new()
868 .name(thread_name)
869 .spawn(move || {
870 request_handler
871 .handle_async(msg, response_sink)
872 });
873 }
874 let _ = wake_proxy.send_event(());
875 }
876 }
877 });
878 }
879
880 let webview = match builder.build(&window) {
881 Ok(wv) => wv,
882 Err(e) => {
883 tracing::error!("Failed to create webview {}: {}", rdesktop_id, e);
884 continue;
885 }
886 };
887
888 windows.insert(tao_id, WindowEntry { window, webview });
889 rdesktop_to_tao.insert(*rdesktop_id, tao_id);
890 tao_to_rdesktop.insert(tao_id, *rdesktop_id);
891
892 if first_window_id.lock().unwrap().is_none() {
893 *first_window_id.lock().unwrap() = Some(*rdesktop_id);
894 }
895
896 tracing::info!(rdesktop_id = rdesktop_id, ?tao_id, "Window created");
897 }
898
899 for op in &pending_ops {
901 Self::apply_op(op, &windows, &rdesktop_to_tao, asset_root.as_deref());
902 }
903 }
904
905 Event::WindowEvent {
906 event: WindowEvent::CloseRequested,
907 window_id,
908 ..
909 } => {
910 if let Some(rd_id) = tao_to_rdesktop.remove(&window_id) {
911 rdesktop_to_tao.remove(&rd_id);
912 }
913 windows.remove(&window_id);
914 if windows.is_empty() {
915 tracing::info!("All windows closed, exiting");
916 *control_flow = ControlFlow::Exit;
917 }
918 }
919
920 Event::WindowEvent {
921 event: WindowEvent::Resized(size),
922 window_id,
923 ..
924 } => {
925 if let Some(entry) = windows.get(&window_id) {
926 let _ = entry
931 .webview
932 .set_bounds(physical_webview_bounds(size.width, size.height));
933 }
934 }
935
936 Event::WindowEvent {
937 event: WindowEvent::ScaleFactorChanged { new_inner_size, .. },
938 window_id,
939 ..
940 } => {
941 if let Some(entry) = windows.get(&window_id) {
942 let _ = entry.webview.set_bounds(physical_webview_bounds(
943 new_inner_size.width,
944 new_inner_size.height,
945 ));
946 }
947 }
948
949 Event::MainEventsCleared => {
950 let responses: Vec<(u64, String)> = {
952 let mut queue = ipc_response_queue.lock().unwrap();
953 queue.drain(..).collect()
954 };
955 for (rdesktop_id, json) in responses {
956 if let Some(tao_id) = rdesktop_to_tao.get(&rdesktop_id) {
957 if let Some(entry) = windows.get(tao_id) {
958 if let Ok(js) = serde_json::to_string(&json) {
959 let script = format!("window.__RDESKTOP_IPC__({js})");
960 let _ = entry.webview.evaluate_script(&script);
961 }
962 }
963 }
964 }
965
966 let outbox_msgs: Vec<String> = {
968 let mut queue = outbox_for_loop.lock().unwrap();
969 queue.drain(..).collect()
970 };
971 for json in outbox_msgs {
972 if let Some(entry) = windows.values().next() {
973 if let Ok(js) = serde_json::to_string(&json) {
974 let script = format!("window.__RDESKTOP_IPC__({js})");
975 let _ = entry.webview.evaluate_script(&script);
976 }
977 }
978 }
979
980 let commands: Vec<WindowCommand> = {
982 let mut queue = window_cmd_queue.lock().unwrap();
983 queue.drain(..).collect()
984 };
985 for cmd in commands {
986 if let Some(tao_id) = rdesktop_to_tao.get(&cmd.rdesktop_id) {
987 if let Some(entry) = windows.get(tao_id) {
988 match cmd.action {
989 WindowAction::Minimize => {
990 entry.window.set_minimized(true);
991 }
992 WindowAction::Maximize => {
993 let is_max = entry.window.is_maximized();
994 entry.window.set_maximized(!is_max);
995 }
996 WindowAction::Close => {
997 *control_flow = ControlFlow::Exit;
998 }
999 WindowAction::StartDrag => {
1000 let _ = entry.window.drag_window();
1001 }
1002 WindowAction::StartResize(dir) => {
1003 let _ = entry.window.drag_resize_window(dir);
1004 }
1005 WindowAction::SetFullscreen(fs) => {
1006 if fs {
1007 entry.window.set_fullscreen(Some(
1008 tao::window::Fullscreen::Borderless(None),
1009 ));
1010 } else {
1011 entry.window.set_fullscreen(None);
1012 }
1013 }
1014 WindowAction::SetDecorations(decorations) => {
1015 entry.window.set_decorations(decorations);
1016 }
1017 }
1018 }
1019 }
1020 }
1021 }
1022
1023 Event::LoopDestroyed => {
1024 tracing::info!("WebView event loop destroyed");
1025 }
1026
1027 _ => {}
1028 }
1029 });
1030 }
1031
1032 fn kind(&self) -> RendererKind {
1033 RendererKind::WebView
1034 }
1035}
1036
1037impl WebViewRenderer {
1038 fn apply_op(
1040 op: &PendingOp,
1041 windows: &HashMap<WindowId, WindowEntry>,
1042 rdesktop_to_tao: &HashMap<u64, WindowId>,
1043 asset_root: Option<&Path>,
1044 ) {
1045 match op {
1046 PendingOp::LoadUrl(rd_id, url) => {
1047 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1048 let native_url = native_asset_url(url, asset_root.is_some());
1049 let _ = entry.webview.load_url(&native_url);
1050 }
1051 }
1052 PendingOp::LoadHtml(rd_id, html) => {
1053 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1054 let _ = entry.webview.load_html(html);
1055 }
1056 }
1057 PendingOp::EvalScript(rd_id, script) => {
1058 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1059 let _ = entry.webview.evaluate_script(script);
1060 }
1061 }
1062 PendingOp::SetTitle(rd_id, title) => {
1063 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1064 entry.window.set_title(title);
1065 }
1066 }
1067 PendingOp::SetSize(rd_id, w, h) => {
1068 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1069 entry
1070 .window
1071 .set_inner_size(tao::dpi::LogicalSize::new(*w, *h));
1072 }
1073 }
1074 PendingOp::SetResizable(rd_id, resizable) => {
1075 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1076 entry.window.set_resizable(*resizable);
1077 }
1078 }
1079 PendingOp::SetVisible(rd_id, visible) => {
1080 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1081 entry.window.set_visible(*visible);
1082 }
1083 }
1084 PendingOp::SendToFrontend(rd_id, msg) => {
1085 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1086 if let Ok(js) = serde_json::to_string(msg) {
1087 let script = format!("window.__RDESKTOP_IPC__({js})");
1088 let _ = entry.webview.evaluate_script(&script);
1089 }
1090 }
1091 }
1092 PendingOp::Close(_rd_id) => {
1093 }
1095 PendingOp::Minimize(rd_id) => {
1096 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1097 entry.window.set_minimized(true);
1098 }
1099 }
1100 PendingOp::Maximize(rd_id) => {
1101 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1102 let is_max = entry.window.is_maximized();
1103 entry.window.set_maximized(!is_max);
1104 }
1105 }
1106 PendingOp::SetFullscreen(rd_id, fs) => {
1107 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1108 if *fs {
1109 entry
1110 .window
1111 .set_fullscreen(Some(tao::window::Fullscreen::Borderless(None)));
1112 } else {
1113 entry.window.set_fullscreen(None);
1114 }
1115 }
1116 }
1117 PendingOp::StartDrag(rd_id) => {
1118 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1119 let _ = entry.window.drag_window();
1120 }
1121 }
1122 PendingOp::StartResize(rd_id, dir) => {
1123 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1124 let _ = entry.window.drag_resize_window(*dir);
1125 }
1126 }
1127 PendingOp::SetDecorations(rd_id, decorations) => {
1128 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1129 entry.window.set_decorations(*decorations);
1130 }
1131 }
1132 PendingOp::SetAlwaysOnTop(rd_id, always) => {
1133 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1134 entry.window.set_always_on_top(*always);
1135 }
1136 }
1137 }
1138 }
1139}