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