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::{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 outbox: Arc<Mutex<Vec<String>>>,
196}
197
198impl WebViewRenderer {
199 pub fn new(config: &AppConfig) -> Result<Self> {
200 Ok(Self {
201 _config: config.clone(),
202 ipc_handler: None,
203 pending_windows: RefCell::new(Vec::new()),
204 pending_ops: RefCell::new(Vec::new()),
205 next_window_id: RefCell::new(1),
206 asset_root: None,
207 outbox: Arc::new(Mutex::new(Vec::new())),
208 })
209 }
210
211 pub fn set_asset_root(&mut self, root: impl Into<PathBuf>) -> Result<()> {
218 let requested_root = root.into();
219 let root = std::fs::canonicalize(&requested_root).map_err(|error| {
220 RdesktopError::Config(format!(
221 "asset root is not accessible ({}): {error}",
222 requested_root.display()
223 ))
224 })?;
225 if !root.is_dir() {
226 return Err(RdesktopError::Config(format!(
227 "asset root is not a directory: {}",
228 root.display()
229 )));
230 }
231 self.asset_root = Some(root);
232 Ok(())
233 }
234
235 pub fn set_outbox(&mut self, outbox: Arc<Mutex<Vec<String>>>) {
239 self.outbox = outbox;
240 }
241
242 fn next_id(&self) -> u64 {
243 let mut id = self.next_window_id.borrow_mut();
244 let current = *id;
245 *id += 1;
246 current
247 }
248
249 fn bridge_script() -> &'static str {
251 r#"
252 (function() {
253 if (window.__RDESKTOP_BRIDGE__) return;
254 window.__RDESKTOP_BRIDGE__ = true;
255 window.__RDESKTOP_RESOLVE__ = {};
256
257 // ── IPC Bridge ──────────────────────────────────────
258 window.__RDESKTOP_INVOKE__ = function(cmd, payload) {
259 return new Promise(function(resolve, reject) {
260 var id = Math.random().toString(36).slice(2);
261 window.__RDESKTOP_RESOLVE__[id] = resolve;
262 if (window.ipc && window.ipc.postMessage) {
263 window.ipc.postMessage(JSON.stringify({ id: id, cmd: cmd, payload: payload || {} }));
264 }
265 setTimeout(function() {
266 if (window.__RDESKTOP_RESOLVE__[id]) {
267 delete window.__RDESKTOP_RESOLVE__[id];
268 reject(new Error('IPC timeout'));
269 }
270 }, 120000);
271 });
272 };
273
274 window.__RDESKTOP_IPC__ = function(message) {
275 try {
276 var data = typeof message === 'string' ? JSON.parse(message) : message;
277 if (data.id && window.__RDESKTOP_RESOLVE__[data.id]) {
278 window.__RDESKTOP_RESOLVE__[data.id](data);
279 delete window.__RDESKTOP_RESOLVE__[data.id];
280 } else if (window.__RDESKTOP_PUSH__) {
281 // Unnamed push (e.g. extension host → UI event).
282 window.__RDESKTOP_PUSH__(data);
283 }
284 } catch (e) {
285 console.error('rdesktop IPC error:', e);
286 }
287 };
288
289 // ── Window Controls (frameless / custom title bar) ──
290 var postWindowCommand = function(action, extra) {
291 if (!window.ipc || !window.ipc.postMessage) return;
292 var payload = extra || {};
293 payload.__window__ = true;
294 payload.action = action;
295 window.ipc.postMessage(JSON.stringify({
296 id: 'window-' + Math.random().toString(36).slice(2),
297 cmd: 'rdesktop.window',
298 payload: payload
299 }));
300 };
301
302 window.__RDESKTOP_WINDOW__ = {
303 minimize: function() {
304 postWindowCommand('minimize');
305 },
306 maximize: function() {
307 postWindowCommand('maximize');
308 },
309 close: function() {
310 postWindowCommand('close');
311 },
312 startDrag: function() {
313 postWindowCommand('start_drag');
314 },
315 startResize: function(edge) {
316 postWindowCommand('start_resize', { edge: edge || 'bottom-right' });
317 },
318 setFullscreen: function(fs) {
319 postWindowCommand('set_fullscreen', { value: !!fs });
320 },
321 setDecorations: function(decorations) {
322 postWindowCommand('set_decorations', { value: !!decorations });
323 },
324 isMaximized: false,
325 isFullscreen: false
326 };
327 })();
328 "#
329 }
330
331 fn parse_window_payload(
334 payload: &serde_json::Value,
335 rdesktop_id: u64,
336 ) -> Option<WindowCommand> {
337 if payload
339 .get("__window__")
340 .and_then(|v| v.as_bool())
341 .unwrap_or(false)
342 {
343 let action = match payload["action"].as_str()? {
344 "minimize" => WindowAction::Minimize,
345 "maximize" => WindowAction::Maximize,
346 "close" => WindowAction::Close,
347 "start_drag" => WindowAction::StartDrag,
348 "start_resize" => {
349 let edge_str = payload["edge"].as_str().unwrap_or("bottom-right");
350 let dir = match edge_str {
351 "top" => tao::window::ResizeDirection::North,
352 "bottom" => tao::window::ResizeDirection::South,
353 "left" => tao::window::ResizeDirection::West,
354 "right" => tao::window::ResizeDirection::East,
355 "top-left" => tao::window::ResizeDirection::NorthWest,
356 "top-right" => tao::window::ResizeDirection::NorthEast,
357 "bottom-left" => tao::window::ResizeDirection::SouthWest,
358 _ => tao::window::ResizeDirection::SouthEast,
359 };
360 WindowAction::StartResize(dir)
361 }
362 "set_fullscreen" => {
363 let val = payload["value"].as_bool().unwrap_or(false);
364 WindowAction::SetFullscreen(val)
365 }
366 "set_decorations" => {
367 let val = payload["value"].as_bool().unwrap_or(true);
368 WindowAction::SetDecorations(val)
369 }
370 _ => return None,
371 };
372 return Some(WindowCommand {
373 rdesktop_id,
374 action,
375 });
376 }
377 None
378 }
379
380 fn parse_window_command(msg: &IpcMessage, rdesktop_id: u64) -> Option<WindowCommand> {
381 Self::parse_window_payload(&msg.payload, rdesktop_id)
382 }
383
384 fn parse_legacy_window_command(
385 raw: &serde_json::Value,
386 rdesktop_id: u64,
387 ) -> Option<WindowCommand> {
388 Self::parse_window_payload(raw, rdesktop_id)
389 }
390}
391
392fn physical_webview_bounds(width: u32, height: u32) -> wry::Rect {
393 wry::Rect {
394 position: tao::dpi::PhysicalPosition::<i32>::new(0, 0).into(),
395 size: tao::dpi::PhysicalSize::new(width, height).into(),
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402
403 #[test]
404 fn parses_formal_window_command_envelope() {
405 let message = IpcMessage {
406 id: "window-test".to_string(),
407 cmd: "rdesktop.window".to_string(),
408 payload: serde_json::json!({
409 "__window__": true,
410 "action": "close"
411 }),
412 };
413
414 assert!(WebViewRenderer::parse_window_command(&message, 1).is_some());
415 }
416
417 #[test]
418 fn parses_legacy_top_level_window_command() {
419 let raw = serde_json::json!({
420 "__window__": true,
421 "action": "minimize"
422 });
423
424 assert!(WebViewRenderer::parse_legacy_window_command(&raw, 1).is_some());
425 }
426
427 #[test]
428 fn parses_runtime_system_window_decoration_toggle() {
429 let raw = serde_json::json!({
430 "__window__": true,
431 "action": "set_decorations",
432 "value": true
433 });
434
435 assert!(matches!(
436 WebViewRenderer::parse_legacy_window_command(&raw, 1),
437 Some(WindowCommand {
438 action: WindowAction::SetDecorations(true),
439 ..
440 })
441 ));
442 }
443
444 #[test]
445 fn normalizes_runtime_asset_navigation_for_the_native_backend() {
446 assert_eq!(
447 native_asset_url("rdesktop://localhost/index.html", true),
448 if cfg!(target_os = "windows") {
449 "http://rdesktop.localhost/index.html"
450 } else {
451 "rdesktop://localhost/index.html"
452 }
453 );
454 assert_eq!(
455 native_asset_url("https://example.com", true),
456 "https://example.com"
457 );
458 assert_eq!(
459 native_asset_url("rdesktop://localhost/index.html", false),
460 "rdesktop://localhost/index.html"
461 );
462 }
463
464 #[test]
465 fn resize_bounds_preserve_physical_pixels_at_high_dpi() {
466 let bounds = physical_webview_bounds(2560, 1369);
467
468 assert!(matches!(
469 bounds.size,
470 tao::dpi::Size::Physical(size) if size.width == 2560 && size.height == 1369
471 ));
472 assert!(matches!(
473 bounds.position,
474 tao::dpi::Position::Physical(position) if position.x == 0 && position.y == 0
475 ));
476 }
477}
478
479impl Renderer for WebViewRenderer {
480 fn init(&mut self) -> Result<()> {
481 tracing::info!("Initializing WebView renderer");
482 Ok(())
483 }
484
485 fn create_window(&mut self, config: &WindowConfig) -> Result<WindowHandle> {
486 let id = self.next_id();
487 self.pending_windows.borrow_mut().push((id, config.clone()));
488 tracing::info!(window_id = id, "Window queued for creation");
489 Ok(WindowHandle::new(id))
490 }
491
492 fn load_url(&self, window: WindowHandle, url: &str) -> Result<()> {
493 self.pending_ops
494 .borrow_mut()
495 .push(PendingOp::LoadUrl(window.id(), url.to_string()));
496 Ok(())
497 }
498
499 fn load_html(&self, window: WindowHandle, html: &str) -> Result<()> {
500 self.pending_ops
501 .borrow_mut()
502 .push(PendingOp::LoadHtml(window.id(), html.to_string()));
503 Ok(())
504 }
505
506 fn eval_script(&self, window: WindowHandle, script: &str) -> Result<()> {
507 self.pending_ops
508 .borrow_mut()
509 .push(PendingOp::EvalScript(window.id(), script.to_string()));
510 Ok(())
511 }
512
513 fn set_ipc_handler(&mut self, handler: Box<dyn IpcHandler>) {
514 self.ipc_handler = Some(Arc::from(handler));
515 }
516
517 fn send_to_frontend(&self, window: WindowHandle, message: &str) -> Result<()> {
518 self.pending_ops
519 .borrow_mut()
520 .push(PendingOp::SendToFrontend(window.id(), message.to_string()));
521 Ok(())
522 }
523
524 fn set_title(&self, window: WindowHandle, title: &str) -> Result<()> {
525 self.pending_ops
526 .borrow_mut()
527 .push(PendingOp::SetTitle(window.id(), title.to_string()));
528 Ok(())
529 }
530
531 fn set_size(&self, window: WindowHandle, width: u32, height: u32) -> Result<()> {
532 self.pending_ops
533 .borrow_mut()
534 .push(PendingOp::SetSize(window.id(), width, height));
535 Ok(())
536 }
537
538 fn set_resizable(&self, window: WindowHandle, resizable: bool) -> Result<()> {
539 self.pending_ops
540 .borrow_mut()
541 .push(PendingOp::SetResizable(window.id(), resizable));
542 Ok(())
543 }
544
545 fn set_visible(&self, window: WindowHandle, visible: bool) -> Result<()> {
546 self.pending_ops
547 .borrow_mut()
548 .push(PendingOp::SetVisible(window.id(), visible));
549 Ok(())
550 }
551
552 fn close_window(&mut self, window: WindowHandle) -> Result<()> {
553 self.pending_ops
554 .borrow_mut()
555 .push(PendingOp::Close(window.id()));
556 Ok(())
557 }
558
559 fn minimize_window(&self, window: WindowHandle) -> Result<()> {
562 self.pending_ops
563 .borrow_mut()
564 .push(PendingOp::Minimize(window.id()));
565 Ok(())
566 }
567
568 fn maximize_window(&self, window: WindowHandle) -> Result<()> {
569 self.pending_ops
570 .borrow_mut()
571 .push(PendingOp::Maximize(window.id()));
572 Ok(())
573 }
574
575 fn is_maximized(&self, _window: WindowHandle) -> Result<bool> {
576 Ok(false)
579 }
580
581 fn set_fullscreen(&self, window: WindowHandle, fullscreen: bool) -> Result<()> {
582 self.pending_ops
583 .borrow_mut()
584 .push(PendingOp::SetFullscreen(window.id(), fullscreen));
585 Ok(())
586 }
587
588 fn is_fullscreen(&self, _window: WindowHandle) -> Result<bool> {
589 Ok(false)
590 }
591
592 fn start_drag(&self, window: WindowHandle) -> Result<()> {
593 self.pending_ops
594 .borrow_mut()
595 .push(PendingOp::StartDrag(window.id()));
596 Ok(())
597 }
598
599 fn start_resize(&self, window: WindowHandle, edge: ResizeEdge) -> Result<()> {
600 self.pending_ops
601 .borrow_mut()
602 .push(PendingOp::StartResize(window.id(), to_tao_resize(edge)));
603 Ok(())
604 }
605
606 fn set_decorations(&self, window: WindowHandle, decorations: bool) -> Result<()> {
607 self.pending_ops
608 .borrow_mut()
609 .push(PendingOp::SetDecorations(window.id(), decorations));
610 Ok(())
611 }
612
613 fn set_always_on_top(&self, window: WindowHandle, always: bool) -> Result<()> {
614 self.pending_ops
615 .borrow_mut()
616 .push(PendingOp::SetAlwaysOnTop(window.id(), always));
617 Ok(())
618 }
619
620 fn run(mut self: Box<Self>) -> Result<()> {
623 tracing::info!("Starting WebView event loop");
624
625 let ipc_handler = self.ipc_handler.take();
626 let webgpu_enabled = self._config.renderer.webgpu;
627 let asset_root = self.asset_root.clone();
628 let pending_windows: Vec<(u64, WindowConfig)> =
629 self.pending_windows.borrow_mut().drain(..).collect();
630 let pending_ops: Vec<PendingOp> = self.pending_ops.borrow_mut().drain(..).collect();
631
632 let ipc_response_queue: IpcResponseQueue = Arc::new(Mutex::new(Vec::new()));
633 let ipc_queue_for_handler = ipc_response_queue.clone();
634
635 let outbox_for_loop = self.outbox.clone();
637
638 let global_handler = rdesktop_core::PushHandler::new(self.outbox.clone());
643 let _hotkey_manager = {
644 let mgr = rdesktop_core::HotkeyManager::new(global_handler.clone());
645 for (i, hc) in self._config.hotkeys.iter().enumerate() {
646 if let Ok(hk) = hc.combo.parse::<rdesktop_core::Hotkey>() {
647 let id = i as u32 + 1;
648 if let Err(e) = mgr.register(id, &hk) {
649 tracing::warn!("failed to register hotkey {:?}: {}", hc.combo, e);
650 }
651 } else {
652 tracing::warn!("invalid hotkey combo: {:?}", hc.combo);
653 }
654 }
655 mgr
656 };
657 let _input_manager = if self._config.global_input.enabled {
658 let mut inp = rdesktop_core::GlobalInput::new(global_handler.clone());
659 if self._config.global_input.mouse_move {
660 inp = inp.with_mouse_move(true);
661 }
662 match inp.start() {
663 Ok(()) => Some(inp),
664 Err(e) => {
665 tracing::warn!("failed to start global input: {}", e);
666 None
667 }
668 }
669 } else {
670 None
671 };
672
673 let window_cmd_queue: WindowCommandQueue = Arc::new(Mutex::new(Vec::new()));
675 let window_cmd_queue_for_ipc = window_cmd_queue.clone();
676
677 let first_window_id: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
680
681 let event_loop = EventLoopBuilder::new().build();
682 let event_loop_proxy = event_loop.create_proxy();
683 let mut windows: HashMap<WindowId, WindowEntry> = HashMap::new();
684 let mut rdesktop_to_tao: HashMap<u64, WindowId> = HashMap::new();
685 let mut tao_to_rdesktop: HashMap<WindowId, u64> = HashMap::new();
686
687 event_loop.run(move |event, event_loop_target, control_flow| {
688 *control_flow = ControlFlow::Wait;
689
690 match event {
691 Event::NewEvents(StartCause::Init) => {
692 let event_loop_proxy = event_loop_proxy.clone();
693 for (rdesktop_id, window_config) in &pending_windows {
695 let window = match WindowBuilder::new()
696 .with_title(&window_config.title)
697 .with_inner_size(tao::dpi::LogicalSize::new(
698 window_config.width,
699 window_config.height,
700 ))
701 .with_resizable(window_config.resizable)
702 .with_decorations(window_config.decorations)
703 .with_transparent(window_config.transparent)
704 .with_always_on_top(window_config.always_on_top)
705 .with_window_icon(rdesktop_core::window_icon(window_config))
706 .build(event_loop_target)
707 {
708 Ok(w) => w,
709 Err(e) => {
710 tracing::error!("Failed to create window {}: {}", rdesktop_id, e);
711 continue;
712 }
713 };
714
715 let tao_id = window.id();
716
717 rdesktop_core::apply_window_attributes(&window, window_config);
719
720 let mut builder = WebViewBuilder::new()
721 .with_url("about:blank")
722 .with_devtools(cfg!(debug_assertions))
723 .with_initialization_script(Self::bridge_script());
724
725 if let Some(root) = asset_root.clone() {
726 builder = builder.with_custom_protocol(
727 "rdesktop".to_string(),
728 move |_webview_id, request| serve_asset(&root, request),
729 );
730 }
731
732 if window_config.transparent {
735 builder = builder.with_transparent(true);
736 }
737 #[cfg(target_os = "windows")]
743 if webgpu_enabled {
744 builder = builder.with_additional_browser_args(
745 "--enable-features=Vulkan,WebGPU --enable-unsafe-webgpu",
746 );
747 }
748
749 if let Some(ref handler) = ipc_handler {
751 let handler = handler.clone();
752 let queue = ipc_queue_for_handler.clone();
753 let win_queue = window_cmd_queue_for_ipc.clone();
754 let wake_proxy = event_loop_proxy.clone();
755 let _first_id = first_window_id.clone();
756 let rd_id = *rdesktop_id;
757
758 builder =
759 builder.with_ipc_handler(move |req: wry::http::Request<String>| {
760 let body = req.body();
761
762 if let Ok(raw) = serde_json::from_str::<serde_json::Value>(body)
765 {
766 if let Some(cmd) =
767 WebViewRenderer::parse_legacy_window_command(
768 &raw, rd_id,
769 )
770 {
771 if let Ok(mut q) = win_queue.lock() {
772 q.push(cmd);
773 }
774 let _ = wake_proxy.send_event(());
775 return;
776 }
777
778 if let Ok(msg) = serde_json::from_value::<IpcMessage>(raw) {
779 if let Some(cmd) =
781 WebViewRenderer::parse_window_command(&msg, rd_id)
782 {
783 if let Ok(mut q) = win_queue.lock() {
784 q.push(cmd);
785 }
786 } else {
787 let request_handler = handler.clone();
792 let response_queue = queue.clone();
793 let response_wake = wake_proxy.clone();
794 let response_sink: IpcResponseSender =
795 Arc::new(move |response| {
796 if let Ok(json) =
797 serde_json::to_string(&response)
798 {
799 if let Ok(mut q) = response_queue.lock()
800 {
801 q.push((rd_id, json));
802 }
803 }
804 let _ = response_wake.send_event(());
805 });
806 let thread_name =
807 format!("rdesktop-ipc-{rd_id}-{}", msg.id);
808 let _ = std::thread::Builder::new()
809 .name(thread_name)
810 .spawn(move || {
811 request_handler
812 .handle_async(msg, response_sink)
813 });
814 }
815 let _ = wake_proxy.send_event(());
816 }
817 }
818 });
819 }
820
821 let webview = match builder.build(&window) {
822 Ok(wv) => wv,
823 Err(e) => {
824 tracing::error!("Failed to create webview {}: {}", rdesktop_id, e);
825 continue;
826 }
827 };
828
829 windows.insert(tao_id, WindowEntry { window, webview });
830 rdesktop_to_tao.insert(*rdesktop_id, tao_id);
831 tao_to_rdesktop.insert(tao_id, *rdesktop_id);
832
833 if first_window_id.lock().unwrap().is_none() {
834 *first_window_id.lock().unwrap() = Some(*rdesktop_id);
835 }
836
837 tracing::info!(rdesktop_id = rdesktop_id, ?tao_id, "Window created");
838 }
839
840 for op in &pending_ops {
842 Self::apply_op(op, &windows, &rdesktop_to_tao, asset_root.as_deref());
843 }
844 }
845
846 Event::WindowEvent {
847 event: WindowEvent::CloseRequested,
848 window_id,
849 ..
850 } => {
851 if let Some(rd_id) = tao_to_rdesktop.remove(&window_id) {
852 rdesktop_to_tao.remove(&rd_id);
853 }
854 windows.remove(&window_id);
855 if windows.is_empty() {
856 tracing::info!("All windows closed, exiting");
857 *control_flow = ControlFlow::Exit;
858 }
859 }
860
861 Event::WindowEvent {
862 event: WindowEvent::Resized(size),
863 window_id,
864 ..
865 } => {
866 if let Some(entry) = windows.get(&window_id) {
867 let _ = entry
872 .webview
873 .set_bounds(physical_webview_bounds(size.width, size.height));
874 }
875 }
876
877 Event::WindowEvent {
878 event: WindowEvent::ScaleFactorChanged { new_inner_size, .. },
879 window_id,
880 ..
881 } => {
882 if let Some(entry) = windows.get(&window_id) {
883 let _ = entry.webview.set_bounds(physical_webview_bounds(
884 new_inner_size.width,
885 new_inner_size.height,
886 ));
887 }
888 }
889
890 Event::MainEventsCleared => {
891 let responses: Vec<(u64, String)> = {
893 let mut queue = ipc_response_queue.lock().unwrap();
894 queue.drain(..).collect()
895 };
896 for (rdesktop_id, json) in responses {
897 if let Some(tao_id) = rdesktop_to_tao.get(&rdesktop_id) {
898 if let Some(entry) = windows.get(tao_id) {
899 if let Ok(js) = serde_json::to_string(&json) {
900 let script = format!("window.__RDESKTOP_IPC__({js})");
901 let _ = entry.webview.evaluate_script(&script);
902 }
903 }
904 }
905 }
906
907 let outbox_msgs: Vec<String> = {
909 let mut queue = outbox_for_loop.lock().unwrap();
910 queue.drain(..).collect()
911 };
912 for json in outbox_msgs {
913 if let Some(entry) = windows.values().next() {
914 if let Ok(js) = serde_json::to_string(&json) {
915 let script = format!("window.__RDESKTOP_IPC__({js})");
916 let _ = entry.webview.evaluate_script(&script);
917 }
918 }
919 }
920
921 let commands: Vec<WindowCommand> = {
923 let mut queue = window_cmd_queue.lock().unwrap();
924 queue.drain(..).collect()
925 };
926 for cmd in commands {
927 if let Some(tao_id) = rdesktop_to_tao.get(&cmd.rdesktop_id) {
928 if let Some(entry) = windows.get(tao_id) {
929 match cmd.action {
930 WindowAction::Minimize => {
931 entry.window.set_minimized(true);
932 }
933 WindowAction::Maximize => {
934 let is_max = entry.window.is_maximized();
935 entry.window.set_maximized(!is_max);
936 }
937 WindowAction::Close => {
938 *control_flow = ControlFlow::Exit;
939 }
940 WindowAction::StartDrag => {
941 let _ = entry.window.drag_window();
942 }
943 WindowAction::StartResize(dir) => {
944 let _ = entry.window.drag_resize_window(dir);
945 }
946 WindowAction::SetFullscreen(fs) => {
947 if fs {
948 entry.window.set_fullscreen(Some(
949 tao::window::Fullscreen::Borderless(None),
950 ));
951 } else {
952 entry.window.set_fullscreen(None);
953 }
954 }
955 WindowAction::SetDecorations(decorations) => {
956 entry.window.set_decorations(decorations);
957 }
958 }
959 }
960 }
961 }
962 }
963
964 Event::LoopDestroyed => {
965 tracing::info!("WebView event loop destroyed");
966 }
967
968 _ => {}
969 }
970 });
971 }
972
973 fn kind(&self) -> RendererKind {
974 RendererKind::WebView
975 }
976}
977
978impl WebViewRenderer {
979 fn apply_op(
981 op: &PendingOp,
982 windows: &HashMap<WindowId, WindowEntry>,
983 rdesktop_to_tao: &HashMap<u64, WindowId>,
984 asset_root: Option<&Path>,
985 ) {
986 match op {
987 PendingOp::LoadUrl(rd_id, url) => {
988 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
989 let native_url = native_asset_url(url, asset_root.is_some());
990 let _ = entry.webview.load_url(&native_url);
991 }
992 }
993 PendingOp::LoadHtml(rd_id, html) => {
994 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
995 let _ = entry.webview.load_html(html);
996 }
997 }
998 PendingOp::EvalScript(rd_id, script) => {
999 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1000 let _ = entry.webview.evaluate_script(script);
1001 }
1002 }
1003 PendingOp::SetTitle(rd_id, title) => {
1004 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1005 entry.window.set_title(title);
1006 }
1007 }
1008 PendingOp::SetSize(rd_id, w, h) => {
1009 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1010 entry
1011 .window
1012 .set_inner_size(tao::dpi::LogicalSize::new(*w, *h));
1013 }
1014 }
1015 PendingOp::SetResizable(rd_id, resizable) => {
1016 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1017 entry.window.set_resizable(*resizable);
1018 }
1019 }
1020 PendingOp::SetVisible(rd_id, visible) => {
1021 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1022 entry.window.set_visible(*visible);
1023 }
1024 }
1025 PendingOp::SendToFrontend(rd_id, msg) => {
1026 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1027 if let Ok(js) = serde_json::to_string(msg) {
1028 let script = format!("window.__RDESKTOP_IPC__({js})");
1029 let _ = entry.webview.evaluate_script(&script);
1030 }
1031 }
1032 }
1033 PendingOp::Close(_rd_id) => {
1034 }
1036 PendingOp::Minimize(rd_id) => {
1037 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1038 entry.window.set_minimized(true);
1039 }
1040 }
1041 PendingOp::Maximize(rd_id) => {
1042 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1043 let is_max = entry.window.is_maximized();
1044 entry.window.set_maximized(!is_max);
1045 }
1046 }
1047 PendingOp::SetFullscreen(rd_id, fs) => {
1048 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1049 if *fs {
1050 entry
1051 .window
1052 .set_fullscreen(Some(tao::window::Fullscreen::Borderless(None)));
1053 } else {
1054 entry.window.set_fullscreen(None);
1055 }
1056 }
1057 }
1058 PendingOp::StartDrag(rd_id) => {
1059 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1060 let _ = entry.window.drag_window();
1061 }
1062 }
1063 PendingOp::StartResize(rd_id, dir) => {
1064 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1065 let _ = entry.window.drag_resize_window(*dir);
1066 }
1067 }
1068 PendingOp::SetDecorations(rd_id, decorations) => {
1069 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1070 entry.window.set_decorations(*decorations);
1071 }
1072 }
1073 PendingOp::SetAlwaysOnTop(rd_id, always) => {
1074 if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
1075 entry.window.set_always_on_top(*always);
1076 }
1077 }
1078 }
1079 }
1080}