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