Skip to main content

rdesktop_webview/
renderer.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4
5use rdesktop_core::config::{AppConfig, WindowConfig};
6use rdesktop_core::ipc::{IpcHandler, IpcMessage};
7use rdesktop_core::renderer::{Renderer, RendererKind, ResizeEdge};
8use rdesktop_core::window::WindowHandle;
9use rdesktop_core::Result;
10
11use tao::event::{Event, StartCause, WindowEvent};
12use tao::event_loop::{ControlFlow, EventLoopBuilder};
13use tao::window::{Window, WindowBuilder, WindowId};
14#[cfg(target_os = "windows")]
15use wry::WebViewBuilderExtWindows;
16use wry::{WebView, WebViewBuilder};
17
18struct WindowEntry {
19    window: Window,
20    webview: WebView,
21}
22
23/// Pending operation queued before the event loop starts.
24enum PendingOp {
25    LoadUrl(u64, String),
26    LoadHtml(u64, String),
27    EvalScript(u64, String),
28    SetTitle(u64, String),
29    SetSize(u64, u32, u32),
30    SetResizable(u64, bool),
31    SetVisible(u64, bool),
32    SendToFrontend(u64, String),
33    Close(u64),
34    // Frameless / window control
35    Minimize(u64),
36    Maximize(u64),
37    SetFullscreen(u64, bool),
38    StartDrag(u64),
39    StartResize(u64, tao::window::ResizeDirection),
40    SetDecorations(u64, bool),
41    SetAlwaysOnTop(u64, bool),
42}
43
44/// Shared IPC response queue.
45type IpcResponseQueue = Arc<Mutex<Vec<String>>>;
46
47/// Window control commands from the IPC thread, drained by the event loop.
48type WindowCommandQueue = Arc<Mutex<Vec<WindowCommand>>>;
49
50/// A window control command sent from the IPC handler to the event loop.
51struct WindowCommand {
52    rdesktop_id: u64,
53    action: WindowAction,
54}
55
56enum WindowAction {
57    Minimize,
58    Maximize,
59    Close,
60    StartDrag,
61    StartResize(tao::window::ResizeDirection),
62    SetFullscreen(bool),
63}
64
65/// Convert rdesktop ResizeEdge to tao's ResizeDirection.
66fn to_tao_resize(edge: ResizeEdge) -> tao::window::ResizeDirection {
67    match edge {
68        ResizeEdge::Top => tao::window::ResizeDirection::North,
69        ResizeEdge::Bottom => tao::window::ResizeDirection::South,
70        ResizeEdge::Left => tao::window::ResizeDirection::West,
71        ResizeEdge::Right => tao::window::ResizeDirection::East,
72        ResizeEdge::TopLeft => tao::window::ResizeDirection::NorthWest,
73        ResizeEdge::TopRight => tao::window::ResizeDirection::NorthEast,
74        ResizeEdge::BottomLeft => tao::window::ResizeDirection::SouthWest,
75        ResizeEdge::BottomRight => tao::window::ResizeDirection::SouthEast,
76    }
77}
78
79/// WebView-based renderer using wry + tao.
80///
81/// Platform backends:
82/// - Windows: WebView2 (Edge Chromium)
83/// - macOS: WKWebView (WebKit)
84/// - Linux: WebKitGTK
85///
86/// ## Frameless / Custom Title Bar
87///
88/// Set `decorations = false` in `WindowConfig` to create a frameless window.
89/// The frontend can use `window.__RDESKTOP_WINDOW__` to control the window:
90///
91/// ```javascript
92/// window.__RDESKTOP_WINDOW__.minimize()
93/// window.__RDESKTOP_WINDOW__.maximize()
94/// window.__RDESKTOP_WINDOW__.close()
95/// window.__RDESKTOP_WINDOW__.startDrag()       // drag from custom title bar
96/// window.__RDESKTOP_WINDOW__.startResize('bottom-right')  // resize from edge
97/// ```
98pub struct WebViewRenderer {
99    _config: AppConfig,
100    ipc_handler: Option<Arc<dyn IpcHandler>>,
101    pending_windows: RefCell<Vec<(u64, WindowConfig)>>,
102    pending_ops: RefCell<Vec<PendingOp>>,
103    next_window_id: RefCell<u64>,
104    /// External outbox for native → frontend pushes (e.g. a Node extension
105    /// host asking the UI to show a message or apply an editor edit). Drained
106    /// every frame by the event loop, same as `ipc_response_queue`.
107    outbox: Arc<Mutex<Vec<String>>>,
108}
109
110impl WebViewRenderer {
111    pub fn new(config: &AppConfig) -> Result<Self> {
112        Ok(Self {
113            _config: config.clone(),
114            ipc_handler: None,
115            pending_windows: RefCell::new(Vec::new()),
116            pending_ops: RefCell::new(Vec::new()),
117            next_window_id: RefCell::new(1),
118            outbox: Arc::new(Mutex::new(Vec::new())),
119        })
120    }
121
122    /// Attach an external outbox so other runtimes (e.g. a Node extension
123    /// host) can push messages to the frontend. Each entry is a JSON string
124    /// emitted as `window.__RDESKTOP_IPC__(<json>)`.
125    pub fn set_outbox(&mut self, outbox: Arc<Mutex<Vec<String>>>) {
126        self.outbox = outbox;
127    }
128
129    fn next_id(&self) -> u64 {
130        let mut id = self.next_window_id.borrow_mut();
131        let current = *id;
132        *id += 1;
133        current
134    }
135
136    /// JavaScript bridge injected into every WebView.
137    fn bridge_script() -> &'static str {
138        r#"
139        (function() {
140            if (window.__RDESKTOP_BRIDGE__) return;
141            window.__RDESKTOP_BRIDGE__ = true;
142            window.__RDESKTOP_RESOLVE__ = {};
143
144            // ── IPC Bridge ──────────────────────────────────────
145            window.__RDESKTOP_INVOKE__ = function(cmd, payload) {
146                return new Promise(function(resolve, reject) {
147                    var id = Math.random().toString(36).slice(2);
148                    window.__RDESKTOP_RESOLVE__[id] = resolve;
149                    if (window.ipc && window.ipc.postMessage) {
150                        window.ipc.postMessage(JSON.stringify({ id: id, cmd: cmd, payload: payload || {} }));
151                    }
152                    setTimeout(function() {
153                        if (window.__RDESKTOP_RESOLVE__[id]) {
154                            delete window.__RDESKTOP_RESOLVE__[id];
155                            reject(new Error('IPC timeout'));
156                        }
157                    }, 30000);
158                });
159            };
160
161            window.__RDESKTOP_IPC__ = function(message) {
162                try {
163                    var data = typeof message === 'string' ? JSON.parse(message) : message;
164                    if (data.id && window.__RDESKTOP_RESOLVE__[data.id]) {
165                        window.__RDESKTOP_RESOLVE__[data.id](data);
166                        delete window.__RDESKTOP_RESOLVE__[data.id];
167                    } else if (window.__RDESKTOP_PUSH__) {
168                        // Unnamed push (e.g. extension host → UI event).
169                        window.__RDESKTOP_PUSH__(data);
170                    }
171                } catch (e) {
172                    console.error('rdesktop IPC error:', e);
173                }
174            };
175
176            // ── Window Controls (frameless / custom title bar) ──
177            window.__RDESKTOP_WINDOW__ = {
178                minimize: function() {
179                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'minimize' }));
180                },
181                maximize: function() {
182                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'maximize' }));
183                },
184                close: function() {
185                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'close' }));
186                },
187                startDrag: function() {
188                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'start_drag' }));
189                },
190                startResize: function(edge) {
191                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'start_resize', edge: edge || 'bottom-right' }));
192                },
193                setFullscreen: function(fs) {
194                    window.ipc && window.ipc.postMessage(JSON.stringify({ __window__: true, action: 'set_fullscreen', value: !!fs }));
195                },
196                isMaximized: false,
197                isFullscreen: false
198            };
199        })();
200        "#
201    }
202
203    /// Parse a window control message from the IPC handler.
204    /// Returns Some(WindowAction) if it's a window command, None otherwise.
205    fn parse_window_command(msg: &IpcMessage, rdesktop_id: u64) -> Option<WindowCommand> {
206        // Check if the payload has __window__ flag
207        if msg
208            .payload
209            .get("__window__")
210            .and_then(|v| v.as_bool())
211            .unwrap_or(false)
212        {
213            let action = match msg.payload["action"].as_str()? {
214                "minimize" => WindowAction::Minimize,
215                "maximize" => WindowAction::Maximize,
216                "close" => WindowAction::Close,
217                "start_drag" => WindowAction::StartDrag,
218                "start_resize" => {
219                    let edge_str = msg.payload["edge"].as_str().unwrap_or("bottom-right");
220                    let dir = match edge_str {
221                        "top" => tao::window::ResizeDirection::North,
222                        "bottom" => tao::window::ResizeDirection::South,
223                        "left" => tao::window::ResizeDirection::West,
224                        "right" => tao::window::ResizeDirection::East,
225                        "top-left" => tao::window::ResizeDirection::NorthWest,
226                        "top-right" => tao::window::ResizeDirection::NorthEast,
227                        "bottom-left" => tao::window::ResizeDirection::SouthWest,
228                        _ => tao::window::ResizeDirection::SouthEast,
229                    };
230                    WindowAction::StartResize(dir)
231                }
232                "set_fullscreen" => {
233                    let val = msg.payload["value"].as_bool().unwrap_or(false);
234                    WindowAction::SetFullscreen(val)
235                }
236                _ => return None,
237            };
238            return Some(WindowCommand {
239                rdesktop_id,
240                action,
241            });
242        }
243        None
244    }
245}
246
247impl Renderer for WebViewRenderer {
248    fn init(&mut self) -> Result<()> {
249        tracing::info!("Initializing WebView renderer");
250        Ok(())
251    }
252
253    fn create_window(&mut self, config: &WindowConfig) -> Result<WindowHandle> {
254        let id = self.next_id();
255        self.pending_windows.borrow_mut().push((id, config.clone()));
256        tracing::info!(window_id = id, "Window queued for creation");
257        Ok(WindowHandle::new(id))
258    }
259
260    fn load_url(&self, window: WindowHandle, url: &str) -> Result<()> {
261        self.pending_ops
262            .borrow_mut()
263            .push(PendingOp::LoadUrl(window.id(), url.to_string()));
264        Ok(())
265    }
266
267    fn load_html(&self, window: WindowHandle, html: &str) -> Result<()> {
268        self.pending_ops
269            .borrow_mut()
270            .push(PendingOp::LoadHtml(window.id(), html.to_string()));
271        Ok(())
272    }
273
274    fn eval_script(&self, window: WindowHandle, script: &str) -> Result<()> {
275        self.pending_ops
276            .borrow_mut()
277            .push(PendingOp::EvalScript(window.id(), script.to_string()));
278        Ok(())
279    }
280
281    fn set_ipc_handler(&mut self, handler: Box<dyn IpcHandler>) {
282        self.ipc_handler = Some(Arc::from(handler));
283    }
284
285    fn send_to_frontend(&self, window: WindowHandle, message: &str) -> Result<()> {
286        self.pending_ops
287            .borrow_mut()
288            .push(PendingOp::SendToFrontend(window.id(), message.to_string()));
289        Ok(())
290    }
291
292    fn set_title(&self, window: WindowHandle, title: &str) -> Result<()> {
293        self.pending_ops
294            .borrow_mut()
295            .push(PendingOp::SetTitle(window.id(), title.to_string()));
296        Ok(())
297    }
298
299    fn set_size(&self, window: WindowHandle, width: u32, height: u32) -> Result<()> {
300        self.pending_ops
301            .borrow_mut()
302            .push(PendingOp::SetSize(window.id(), width, height));
303        Ok(())
304    }
305
306    fn set_resizable(&self, window: WindowHandle, resizable: bool) -> Result<()> {
307        self.pending_ops
308            .borrow_mut()
309            .push(PendingOp::SetResizable(window.id(), resizable));
310        Ok(())
311    }
312
313    fn set_visible(&self, window: WindowHandle, visible: bool) -> Result<()> {
314        self.pending_ops
315            .borrow_mut()
316            .push(PendingOp::SetVisible(window.id(), visible));
317        Ok(())
318    }
319
320    fn close_window(&mut self, window: WindowHandle) -> Result<()> {
321        self.pending_ops
322            .borrow_mut()
323            .push(PendingOp::Close(window.id()));
324        Ok(())
325    }
326
327    // ── Frameless / Window Controls ─────────────────────────────
328
329    fn minimize_window(&self, window: WindowHandle) -> Result<()> {
330        self.pending_ops
331            .borrow_mut()
332            .push(PendingOp::Minimize(window.id()));
333        Ok(())
334    }
335
336    fn maximize_window(&self, window: WindowHandle) -> Result<()> {
337        self.pending_ops
338            .borrow_mut()
339            .push(PendingOp::Maximize(window.id()));
340        Ok(())
341    }
342
343    fn is_maximized(&self, _window: WindowHandle) -> Result<bool> {
344        // This needs to be checked inside the event loop; return false for now.
345        // In practice, the frontend can track this via window state events.
346        Ok(false)
347    }
348
349    fn set_fullscreen(&self, window: WindowHandle, fullscreen: bool) -> Result<()> {
350        self.pending_ops
351            .borrow_mut()
352            .push(PendingOp::SetFullscreen(window.id(), fullscreen));
353        Ok(())
354    }
355
356    fn is_fullscreen(&self, _window: WindowHandle) -> Result<bool> {
357        Ok(false)
358    }
359
360    fn start_drag(&self, window: WindowHandle) -> Result<()> {
361        self.pending_ops
362            .borrow_mut()
363            .push(PendingOp::StartDrag(window.id()));
364        Ok(())
365    }
366
367    fn start_resize(&self, window: WindowHandle, edge: ResizeEdge) -> Result<()> {
368        self.pending_ops
369            .borrow_mut()
370            .push(PendingOp::StartResize(window.id(), to_tao_resize(edge)));
371        Ok(())
372    }
373
374    fn set_decorations(&self, window: WindowHandle, decorations: bool) -> Result<()> {
375        self.pending_ops
376            .borrow_mut()
377            .push(PendingOp::SetDecorations(window.id(), decorations));
378        Ok(())
379    }
380
381    fn set_always_on_top(&self, window: WindowHandle, always: bool) -> Result<()> {
382        self.pending_ops
383            .borrow_mut()
384            .push(PendingOp::SetAlwaysOnTop(window.id(), always));
385        Ok(())
386    }
387
388    // ── Event Loop ──────────────────────────────────────────────
389
390    fn run(mut self: Box<Self>) -> Result<()> {
391        tracing::info!("Starting WebView event loop");
392
393        let ipc_handler = self.ipc_handler.take();
394        let webgpu_enabled = self._config.renderer.webgpu;
395        let pending_windows: Vec<(u64, WindowConfig)> =
396            self.pending_windows.borrow_mut().drain(..).collect();
397        let pending_ops: Vec<PendingOp> = self.pending_ops.borrow_mut().drain(..).collect();
398
399        let ipc_response_queue: IpcResponseQueue = Arc::new(Mutex::new(Vec::new()));
400        let ipc_queue_for_handler = ipc_response_queue.clone();
401
402        // External outbox for native → frontend pushes (Node extension host, etc.)
403        let outbox_for_loop = self.outbox.clone();
404
405        // ── Phase 2: global hotkeys & input hooks ───────────────────────
406        // Wired through the shared outbox so the frontend receives them as
407        // `window.__RDESKTOP_PUSH__` events (`rdesktop.globalHotkey` /
408        // `rdesktop.globalInput`). Managers live for the whole event loop.
409        let global_handler = rdesktop_core::PushHandler::new(self.outbox.clone());
410        let _hotkey_manager = {
411            let mgr = rdesktop_core::HotkeyManager::new(global_handler.clone());
412            for (i, hc) in self._config.hotkeys.iter().enumerate() {
413                if let Ok(hk) = hc.combo.parse::<rdesktop_core::Hotkey>() {
414                    let id = i as u32 + 1;
415                    if let Err(e) = mgr.register(id, &hk) {
416                        tracing::warn!("failed to register hotkey {:?}: {}", hc.combo, e);
417                    }
418                } else {
419                    tracing::warn!("invalid hotkey combo: {:?}", hc.combo);
420                }
421            }
422            mgr
423        };
424        let _input_manager = if self._config.global_input.enabled {
425            let mut inp = rdesktop_core::GlobalInput::new(global_handler.clone());
426            if self._config.global_input.mouse_move {
427                inp = inp.with_mouse_move(true);
428            }
429            match inp.start() {
430                Ok(()) => Some(inp),
431                Err(e) => {
432                    tracing::warn!("failed to start global input: {}", e);
433                    None
434                }
435            }
436        } else {
437            None
438        };
439
440        // Window command queue for IPC-triggered window operations
441        let window_cmd_queue: WindowCommandQueue = Arc::new(Mutex::new(Vec::new()));
442        let window_cmd_queue_for_ipc = window_cmd_queue.clone();
443
444        // Build a map of rdesktop_id -> first tao_id for the IPC handler
445        // (the IPC handler needs to know which window to operate on)
446        let first_window_id: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
447
448        let event_loop = EventLoopBuilder::new().build();
449        let mut windows: HashMap<WindowId, WindowEntry> = HashMap::new();
450        let mut rdesktop_to_tao: HashMap<u64, WindowId> = HashMap::new();
451        let mut tao_to_rdesktop: HashMap<WindowId, u64> = HashMap::new();
452
453        event_loop.run(move |event, event_loop_target, control_flow| {
454            *control_flow = ControlFlow::Wait;
455
456            match event {
457                Event::NewEvents(StartCause::Init) => {
458                    // Create all pending windows
459                    for (rdesktop_id, window_config) in &pending_windows {
460                        let window = match WindowBuilder::new()
461                            .with_title(&window_config.title)
462                            .with_inner_size(tao::dpi::LogicalSize::new(
463                                window_config.width,
464                                window_config.height,
465                            ))
466                            .with_resizable(window_config.resizable)
467                            .with_decorations(window_config.decorations)
468                            .with_transparent(window_config.transparent)
469                            .with_always_on_top(window_config.always_on_top)
470                            .build(event_loop_target)
471                        {
472                            Ok(w) => w,
473                            Err(e) => {
474                                tracing::error!("Failed to create window {}: {}", rdesktop_id, e);
475                                continue;
476                            }
477                        };
478
479                        let tao_id = window.id();
480
481                        // Realize wallpaper/overlay/click-through window attributes.
482                        rdesktop_core::apply_window_attributes(&window, window_config);
483
484                        let mut builder = WebViewBuilder::new()
485                            .with_url("about:blank")
486                            .with_devtools(cfg!(debug_assertions))
487                            .with_initialization_script(Self::bridge_script());
488
489                        // Enable WebGPU in the web context when requested, so the
490                        // frontend can drive native shaders (wallpaper effects).
491                        if window_config.transparent {
492                            builder = builder.with_transparent(true);
493                        }
494                        // Enable WebGPU in the web context so the frontend can
495                        // drive native shaders (wallpaper effects). On Windows
496                        // WebView2/Edge needs the feature flag; on macOS WKWebView
497                        // exposes WebGPU natively and on Linux WebKitGTK enables it
498                        // via a different path, so the args are Windows-only.
499                        #[cfg(target_os = "windows")]
500                        if webgpu_enabled {
501                            builder = builder.with_additional_browser_args(
502                                "--enable-features=Vulkan,WebGPU --enable-unsafe-webgpu",
503                            );
504                        }
505
506                        // Wire up IPC handler
507                        if let Some(ref handler) = ipc_handler {
508                            let handler = handler.clone();
509                            let queue = ipc_queue_for_handler.clone();
510                            let win_queue = window_cmd_queue_for_ipc.clone();
511                            let _first_id = first_window_id.clone();
512                            let rd_id = *rdesktop_id;
513
514                            builder =
515                                builder.with_ipc_handler(move |req: wry::http::Request<String>| {
516                                    let body = req.body();
517
518                                    // Try parsing as window command first
519                                    if let Ok(msg) = serde_json::from_str::<IpcMessage>(body) {
520                                        if let Some(cmd) =
521                                            WebViewRenderer::parse_window_command(&msg, rd_id)
522                                        {
523                                            if let Ok(mut q) = win_queue.lock() {
524                                                q.push(cmd);
525                                            }
526                                            return;
527                                        }
528
529                                        // Regular IPC message
530                                        let response = handler.handle(msg);
531                                        if let Ok(json) = serde_json::to_string(&response) {
532                                            if let Ok(mut q) = queue.lock() {
533                                                q.push(json);
534                                            }
535                                        }
536                                    }
537                                });
538                        }
539
540                        let webview = match builder.build(&window) {
541                            Ok(wv) => wv,
542                            Err(e) => {
543                                tracing::error!("Failed to create webview {}: {}", rdesktop_id, e);
544                                continue;
545                            }
546                        };
547
548                        windows.insert(tao_id, WindowEntry { window, webview });
549                        rdesktop_to_tao.insert(*rdesktop_id, tao_id);
550                        tao_to_rdesktop.insert(tao_id, *rdesktop_id);
551
552                        if first_window_id.lock().unwrap().is_none() {
553                            *first_window_id.lock().unwrap() = Some(*rdesktop_id);
554                        }
555
556                        tracing::info!(rdesktop_id = rdesktop_id, ?tao_id, "Window created");
557                    }
558
559                    // Process pending operations
560                    for op in &pending_ops {
561                        Self::apply_op(op, &windows, &rdesktop_to_tao);
562                    }
563                }
564
565                Event::WindowEvent {
566                    event: WindowEvent::CloseRequested,
567                    window_id,
568                    ..
569                } => {
570                    if let Some(rd_id) = tao_to_rdesktop.remove(&window_id) {
571                        rdesktop_to_tao.remove(&rd_id);
572                    }
573                    windows.remove(&window_id);
574                    if windows.is_empty() {
575                        tracing::info!("All windows closed, exiting");
576                        *control_flow = ControlFlow::Exit;
577                    }
578                }
579
580                Event::WindowEvent {
581                    event: WindowEvent::Resized(size),
582                    window_id,
583                    ..
584                } => {
585                    if let Some(entry) = windows.get(&window_id) {
586                        let _ = entry.webview.set_bounds(wry::Rect {
587                            position: tao::dpi::LogicalPosition::<i32>::new(0, 0).into(),
588                            size: tao::dpi::LogicalSize::new(size.width, size.height).into(),
589                        });
590                    }
591                }
592
593                Event::WindowEvent {
594                    event: WindowEvent::ScaleFactorChanged { new_inner_size, .. },
595                    window_id,
596                    ..
597                } => {
598                    if let Some(entry) = windows.get(&window_id) {
599                        let _ = entry.webview.set_bounds(wry::Rect {
600                            position: tao::dpi::LogicalPosition::<i32>::new(0, 0).into(),
601                            size: tao::dpi::LogicalSize::new(
602                                new_inner_size.width,
603                                new_inner_size.height,
604                            )
605                            .into(),
606                        });
607                    }
608                }
609
610                Event::MainEventsCleared => {
611                    // Drain IPC response queue
612                    let responses: Vec<String> = {
613                        let mut queue = ipc_response_queue.lock().unwrap();
614                        queue.drain(..).collect()
615                    };
616                    for json in responses {
617                        if let Some(entry) = windows.values().next() {
618                            if let Ok(js) = serde_json::to_string(&json) {
619                                let script = format!("window.__RDESKTOP_IPC__({js})");
620                                let _ = entry.webview.evaluate_script(&script);
621                            }
622                        }
623                    }
624
625                    // Drain external outbox (native → frontend pushes)
626                    let outbox_msgs: Vec<String> = {
627                        let mut queue = outbox_for_loop.lock().unwrap();
628                        queue.drain(..).collect()
629                    };
630                    for json in outbox_msgs {
631                        if let Some(entry) = windows.values().next() {
632                            if let Ok(js) = serde_json::to_string(&json) {
633                                let script = format!("window.__RDESKTOP_IPC__({js})");
634                                let _ = entry.webview.evaluate_script(&script);
635                            }
636                        }
637                    }
638
639                    // Drain window command queue
640                    let commands: Vec<WindowCommand> = {
641                        let mut queue = window_cmd_queue.lock().unwrap();
642                        queue.drain(..).collect()
643                    };
644                    for cmd in commands {
645                        if let Some(tao_id) = rdesktop_to_tao.get(&cmd.rdesktop_id) {
646                            if let Some(entry) = windows.get(tao_id) {
647                                match cmd.action {
648                                    WindowAction::Minimize => {
649                                        entry.window.set_minimized(true);
650                                    }
651                                    WindowAction::Maximize => {
652                                        let is_max = entry.window.is_maximized();
653                                        entry.window.set_maximized(!is_max);
654                                    }
655                                    WindowAction::Close => {
656                                        // Will be handled by CloseRequested
657                                        // For now, just remove the window
658                                    }
659                                    WindowAction::StartDrag => {
660                                        let _ = entry.window.drag_window();
661                                    }
662                                    WindowAction::StartResize(dir) => {
663                                        let _ = entry.window.drag_resize_window(dir);
664                                    }
665                                    WindowAction::SetFullscreen(fs) => {
666                                        if fs {
667                                            entry.window.set_fullscreen(Some(
668                                                tao::window::Fullscreen::Borderless(None),
669                                            ));
670                                        } else {
671                                            entry.window.set_fullscreen(None);
672                                        }
673                                    }
674                                }
675                            }
676                        }
677                    }
678                }
679
680                Event::LoopDestroyed => {
681                    tracing::info!("WebView event loop destroyed");
682                }
683
684                _ => {}
685            }
686        });
687    }
688
689    fn kind(&self) -> RendererKind {
690        RendererKind::WebView
691    }
692}
693
694impl WebViewRenderer {
695    /// Apply a pending operation to a window.
696    fn apply_op(
697        op: &PendingOp,
698        windows: &HashMap<WindowId, WindowEntry>,
699        rdesktop_to_tao: &HashMap<u64, WindowId>,
700    ) {
701        match op {
702            PendingOp::LoadUrl(rd_id, url) => {
703                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
704                    let _ = entry.webview.load_url(url);
705                }
706            }
707            PendingOp::LoadHtml(rd_id, html) => {
708                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
709                    let _ = entry.webview.load_html(html);
710                }
711            }
712            PendingOp::EvalScript(rd_id, script) => {
713                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
714                    let _ = entry.webview.evaluate_script(script);
715                }
716            }
717            PendingOp::SetTitle(rd_id, title) => {
718                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
719                    entry.window.set_title(title);
720                }
721            }
722            PendingOp::SetSize(rd_id, w, h) => {
723                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
724                    entry
725                        .window
726                        .set_inner_size(tao::dpi::LogicalSize::new(*w, *h));
727                }
728            }
729            PendingOp::SetResizable(rd_id, resizable) => {
730                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
731                    entry.window.set_resizable(*resizable);
732                }
733            }
734            PendingOp::SetVisible(rd_id, visible) => {
735                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
736                    entry.window.set_visible(*visible);
737                }
738            }
739            PendingOp::SendToFrontend(rd_id, msg) => {
740                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
741                    if let Ok(js) = serde_json::to_string(msg) {
742                        let script = format!("window.__RDESKTOP_IPC__({js})");
743                        let _ = entry.webview.evaluate_script(&script);
744                    }
745                }
746            }
747            PendingOp::Close(_rd_id) => {
748                // Handled by the caller (removes from maps)
749            }
750            PendingOp::Minimize(rd_id) => {
751                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
752                    entry.window.set_minimized(true);
753                }
754            }
755            PendingOp::Maximize(rd_id) => {
756                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
757                    let is_max = entry.window.is_maximized();
758                    entry.window.set_maximized(!is_max);
759                }
760            }
761            PendingOp::SetFullscreen(rd_id, fs) => {
762                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
763                    if *fs {
764                        entry
765                            .window
766                            .set_fullscreen(Some(tao::window::Fullscreen::Borderless(None)));
767                    } else {
768                        entry.window.set_fullscreen(None);
769                    }
770                }
771            }
772            PendingOp::StartDrag(rd_id) => {
773                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
774                    let _ = entry.window.drag_window();
775                }
776            }
777            PendingOp::StartResize(rd_id, dir) => {
778                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
779                    let _ = entry.window.drag_resize_window(*dir);
780                }
781            }
782            PendingOp::SetDecorations(rd_id, decorations) => {
783                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
784                    entry.window.set_decorations(*decorations);
785                }
786            }
787            PendingOp::SetAlwaysOnTop(rd_id, always) => {
788                if let Some(entry) = rdesktop_to_tao.get(rd_id).and_then(|id| windows.get(id)) {
789                    entry.window.set_always_on_top(*always);
790                }
791            }
792        }
793    }
794}