1use std::cell::RefCell;
7use std::collections::HashMap;
8use std::sync::{Arc, Mutex};
9
10use rdesktop_core::config::{AppConfig, WindowConfig};
11use rdesktop_core::ipc::{IpcHandler, IpcMessage};
12use rdesktop_core::renderer::{Renderer, RendererKind, ResizeEdge};
13use rdesktop_core::window::WindowHandle;
14use rdesktop_core::Result;
15
16use chromiumoxide::browser::{Browser, BrowserConfig};
17use chromiumoxide::cdp::browser_protocol::input::{
18 DispatchKeyEventParams, DispatchKeyEventType, DispatchMouseEventParams, DispatchMouseEventType,
19 MouseButton,
20};
21use chromiumoxide::handler::viewport::Viewport;
22use chromiumoxide::page::Page;
23use futures::StreamExt;
24use tao::event::{ElementState, Event, StartCause, WindowEvent};
25use tao::event_loop::{ControlFlow, EventLoopBuilder};
26use tao::window::{Window, WindowBuilder, WindowId};
27use tokio::runtime::Runtime;
28
29#[cfg(target_os = "windows")]
31#[allow(non_snake_case)]
32mod gdi {
33 use std::ffi::c_void;
34
35 #[repr(C)]
36 pub struct BITMAPINFOHEADER {
37 pub biSize: u32,
38 pub biWidth: i32,
39 pub biHeight: i32,
40 pub biPlanes: u16,
41 pub biBitCount: u16,
42 pub biCompression: u32,
43 pub biSizeImage: u32,
44 pub biXPelsPerMeter: i32,
45 pub biYPelsPerMeter: i32,
46 pub biClrUsed: u32,
47 pub biClrImportant: u32,
48 }
49
50 #[repr(C)]
51 pub struct RECT {
52 pub left: i32,
53 pub top: i32,
54 pub right: i32,
55 pub bottom: i32,
56 }
57
58 pub const BI_RGB: u32 = 0;
59 pub const DIB_RGB_COLORS: u32 = 0;
60 pub const SRCCOPY: u32 = 0x00CC0020;
61
62 pub type HWND = *mut c_void;
63 pub type HDC = *mut c_void;
64
65 #[link(name = "user32")]
66 extern "system" {
67 pub fn GetDC(hwnd: HWND) -> HDC;
68 pub fn ReleaseDC(hwnd: HWND, hdc: HDC) -> i32;
69 pub fn GetClientRect(hwnd: HWND, rect: *mut RECT) -> i32;
70 }
71
72 #[link(name = "gdi32")]
73 extern "system" {
74 pub fn StretchDIBits(
75 hdc: HDC,
76 x_dest: i32,
77 y_dest: i32,
78 dest_width: i32,
79 dest_height: i32,
80 x_src: i32,
81 y_src: i32,
82 src_width: i32,
83 src_height: i32,
84 bits: *const c_void,
85 bits_info: *const c_void,
86 usage: u32,
87 rop: u32,
88 ) -> i32;
89 }
90}
91struct ChromePage {
94 page: Page,
95 width: u32,
96 height: u32,
97 pixels: Vec<u8>, mouse_pos: (f64, f64),
99}
100
101enum PendingOp {
102 LoadUrl(u64, String),
103 LoadHtml(u64, String),
104 EvalScript(u64, String),
105 SendToFrontend(u64, String),
106}
107
108pub struct CefRenderer {
109 _config: AppConfig,
110 pending_pages: RefCell<Vec<(u64, WindowConfig)>>,
111 pending_ops: RefCell<Vec<PendingOp>>,
112 next_id: RefCell<u64>,
113 ipc_handler: Option<Arc<dyn IpcHandler>>,
114 mod_shift: RefCell<bool>,
115 mod_caps: RefCell<bool>,
116 screenshot_sink: Option<Arc<dyn Fn(&[u8], u32, u32) + Send + Sync>>,
117 outbox: Arc<Mutex<Vec<String>>>,
121}
122
123impl CefRenderer {
124 pub fn new(config: &AppConfig) -> Result<Self> {
125 Ok(Self {
126 _config: config.clone(),
127 pending_pages: RefCell::new(Vec::new()),
128 pending_ops: RefCell::new(Vec::new()),
129 next_id: RefCell::new(1),
130 ipc_handler: None,
131 mod_shift: RefCell::new(false),
132 mod_caps: RefCell::new(false),
133 screenshot_sink: None,
134 outbox: Arc::new(Mutex::new(Vec::new())),
135 })
136 }
137
138 pub fn set_screenshot_sink(&mut self, sink: Arc<dyn Fn(&[u8], u32, u32) + Send + Sync>) {
141 self.screenshot_sink = Some(sink);
142 }
143
144 pub fn set_outbox(&mut self, outbox: Arc<Mutex<Vec<String>>>) {
149 self.outbox = outbox;
150 }
151
152 fn alloc_id(&self) -> u64 {
153 let mut id = self.next_id.borrow_mut();
154 let v = *id;
155 *id += 1;
156 v
157 }
158
159 fn find_chrome() -> Option<String> {
160 let lad = std::env::var("LOCALAPPDATA").unwrap_or_default();
161 let cands: Vec<String> = if cfg!(target_os = "windows") {
162 vec![
163 r"C:\Program Files\Google\Chrome\Application\chrome.exe".into(),
164 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe".into(),
165 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe".into(),
166 format!(r"{}\Google\Chrome\Application\chrome.exe", lad),
167 ]
168 } else if cfg!(target_os = "macos") {
169 vec![
170 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome".into(),
171 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge".into(),
172 ]
173 } else {
174 vec![
175 "/usr/bin/google-chrome".into(),
176 "/usr/bin/chromium".into(),
177 "/usr/bin/chromium-browser".into(),
178 ]
179 };
180 cands.into_iter().find(|p| std::path::Path::new(p).exists())
181 }
182
183 fn bridge_script() -> &'static str {
184 r#"
185 (function() {
186 if (window.__RDESKTOP_BRIDGE__) return;
187 window.__RDESKTOP_BRIDGE__ = true;
188 window.__RDESKTOP_RESOLVE__ = {};
189 window.__RDESKTOP_QUEUE__ = [];
190 window.__RDESKTOP_INVOKE__ = function(cmd, payload) {
191 return new Promise(function(resolve, reject) {
192 var id = Math.random().toString(36).slice(2);
193 window.__RDESKTOP_RESOLVE__[id] = resolve;
194 window.__RDESKTOP_QUEUE__.push({ id: id, cmd: cmd, payload: payload || {} });
195 setTimeout(function() {
196 if (window.__RDESKTOP_RESOLVE__[id]) {
197 delete window.__RDESKTOP_RESOLVE__[id];
198 reject(new Error('IPC timeout'));
199 }
200 }, 30000);
201 });
202 };
203 window.__rdesktop_take__ = function() {
204 var q = window.__RDESKTOP_QUEUE__ || [];
205 window.__RDESKTOP_QUEUE__ = [];
206 return JSON.stringify(q);
207 };
208 window.__RDESKTOP_IPC__ = function(message) {
209 try {
210 var data = typeof message === 'string' ? JSON.parse(message) : message;
211 if (data.id && window.__RDESKTOP_RESOLVE__[data.id]) {
212 window.__RDESKTOP_RESOLVE__[data.id](data);
213 delete window.__RDESKTOP_RESOLVE__[data.id];
214 } else if (window.__RDESKTOP_PUSH__) {
215 // Unnamed push (e.g. extension host → UI event, or a
216 // global hotkey / global input event).
217 window.__RDESKTOP_PUSH__(data);
218 }
219 } catch (e) { console.error('rdesktop IPC error:', e); }
220 };
221 window.__RDESKTOP_WINDOW__ = {
222 minimize: function() {},
223 maximize: function() {},
224 close: function() { window.close(); },
225 startDrag: function() {},
226 startResize: function() {},
227 setFullscreen: function() {},
228 isMaximized: false,
229 isFullscreen: false
230 };
231 })();
232 "#
233 }
234
235 fn decode_png(png_bytes: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
236 let dec = png::Decoder::new(std::io::Cursor::new(png_bytes));
237 let mut reader = dec.read_info().ok()?;
238 let mut buf = vec![0u8; reader.output_buffer_size().unwrap_or(0)];
239 let info = reader.next_frame(&mut buf).ok()?;
240 let (w, h) = (info.width, info.height);
241 let rgba = &buf[..info.buffer_size()];
242 let mut bgra = Vec::with_capacity((w * h * 4) as usize);
243 for c in rgba.chunks_exact(4) {
244 bgra.extend_from_slice(&[c[2], c[1], c[0], 255]);
245 }
246 Some((w, h, bgra))
247 }
248
249 #[cfg(target_os = "windows")]
250 fn blit(window: &Window, pixels: &[u8], w: u32, h: u32) {
251 use gdi::*;
252 use tao::platform::windows::WindowExtWindows;
253
254 let hwnd = window.hwnd() as HWND;
255 unsafe {
256 let hdc = GetDC(hwnd);
257 if hdc.is_null() {
258 return;
259 }
260
261 let bmi = BITMAPINFOHEADER {
262 biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
263 biWidth: w as i32,
264 biHeight: -(h as i32), biPlanes: 1,
266 biBitCount: 32,
267 biCompression: BI_RGB,
268 biSizeImage: 0,
269 biXPelsPerMeter: 0,
270 biYPelsPerMeter: 0,
271 biClrUsed: 0,
272 biClrImportant: 0,
273 };
274
275 let mut rect = RECT {
276 left: 0,
277 top: 0,
278 right: 0,
279 bottom: 0,
280 };
281 GetClientRect(hwnd, &mut rect);
282
283 StretchDIBits(
284 hdc,
285 0,
286 0,
287 rect.right,
288 rect.bottom,
289 0,
290 0,
291 w as i32,
292 h as i32,
293 pixels.as_ptr() as *const _,
294 &bmi as *const _ as *const _,
295 DIB_RGB_COLORS,
296 SRCCOPY,
297 );
298
299 ReleaseDC(hwnd, hdc);
300 }
301 }
302
303 #[cfg(not(target_os = "windows"))]
304 fn blit(_window: &Window, _pixels: &[u8], _w: u32, _h: u32) {
305 tracing::warn!("Chrome GDI rendering: Windows only. Other platforms pending.");
306 }
307}
308
309impl Renderer for CefRenderer {
310 fn init(&mut self) -> Result<()> {
311 Ok(())
312 }
313
314 fn create_window(&mut self, config: &WindowConfig) -> Result<WindowHandle> {
315 let id = self.alloc_id();
316 self.pending_pages.borrow_mut().push((id, config.clone()));
317 Ok(WindowHandle::new(id))
318 }
319
320 fn load_url(&self, w: WindowHandle, url: &str) -> Result<()> {
321 self.pending_ops
322 .borrow_mut()
323 .push(PendingOp::LoadUrl(w.id(), url.into()));
324 Ok(())
325 }
326 fn load_html(&self, w: WindowHandle, html: &str) -> Result<()> {
327 self.pending_ops
328 .borrow_mut()
329 .push(PendingOp::LoadHtml(w.id(), html.into()));
330 Ok(())
331 }
332 fn eval_script(&self, w: WindowHandle, script: &str) -> Result<()> {
333 self.pending_ops
334 .borrow_mut()
335 .push(PendingOp::EvalScript(w.id(), script.into()));
336 Ok(())
337 }
338 fn set_ipc_handler(&mut self, h: Box<dyn IpcHandler>) {
339 self.ipc_handler = Some(Arc::from(h));
340 }
341 fn send_to_frontend(&self, w: WindowHandle, msg: &str) -> Result<()> {
342 self.pending_ops
343 .borrow_mut()
344 .push(PendingOp::SendToFrontend(w.id(), msg.into()));
345 Ok(())
346 }
347 fn set_title(&self, _w: WindowHandle, _t: &str) -> Result<()> {
348 Ok(())
349 }
350 fn set_size(&self, _w: WindowHandle, _x: u32, _y: u32) -> Result<()> {
351 Ok(())
352 }
353 fn set_resizable(&self, _w: WindowHandle, _r: bool) -> Result<()> {
354 Ok(())
355 }
356 fn set_visible(&self, _w: WindowHandle, _v: bool) -> Result<()> {
357 Ok(())
358 }
359 fn close_window(&mut self, _w: WindowHandle) -> Result<()> {
360 Ok(())
361 }
362 fn minimize_window(&self, _w: WindowHandle) -> Result<()> {
363 Ok(())
364 }
365 fn maximize_window(&self, _w: WindowHandle) -> Result<()> {
366 Ok(())
367 }
368 fn is_maximized(&self, _w: WindowHandle) -> Result<bool> {
369 Ok(false)
370 }
371 fn set_fullscreen(&self, _w: WindowHandle, _f: bool) -> Result<()> {
372 Ok(())
373 }
374 fn is_fullscreen(&self, _w: WindowHandle) -> Result<bool> {
375 Ok(false)
376 }
377 fn start_drag(&self, _w: WindowHandle) -> Result<()> {
378 Ok(())
379 }
380 fn start_resize(&self, _w: WindowHandle, _e: ResizeEdge) -> Result<()> {
381 Ok(())
382 }
383 fn set_decorations(&self, _w: WindowHandle, _d: bool) -> Result<()> {
384 Ok(())
385 }
386 fn set_always_on_top(&self, _w: WindowHandle, _a: bool) -> Result<()> {
387 Ok(())
388 }
389
390 fn run(self: Box<Self>) -> Result<()> {
391 tracing::info!("Chrome renderer starting");
392
393 let chrome_path = Self::find_chrome()
394 .ok_or_else(|| rdesktop_core::RdesktopError::Cef("Chrome not found".into()))?;
395
396 let pending_pages = self
397 .pending_pages
398 .borrow_mut()
399 .drain(..)
400 .collect::<Vec<_>>();
401 let pending_ops = self.pending_ops.borrow_mut().drain(..).collect::<Vec<_>>();
402
403 let outbox_for_loop = self.outbox.clone();
405
406 let global_handler = rdesktop_core::PushHandler::new(self.outbox.clone());
411 let _hotkey_manager = {
412 let mgr = rdesktop_core::HotkeyManager::new(global_handler.clone());
413 for (i, hc) in self._config.hotkeys.iter().enumerate() {
414 if let Ok(hk) = hc.combo.parse::<rdesktop_core::Hotkey>() {
415 let id = i as u32 + 1;
416 if let Err(e) = mgr.register(id, &hk) {
417 tracing::warn!("failed to register hotkey {:?}: {}", hc.combo, e);
418 }
419 } else {
420 tracing::warn!("invalid hotkey combo: {:?}", hc.combo);
421 }
422 }
423 mgr
424 };
425 let _input_manager = if self._config.global_input.enabled {
426 let mut inp = rdesktop_core::GlobalInput::new(global_handler.clone());
427 if self._config.global_input.mouse_move {
428 inp = inp.with_mouse_move(true);
429 }
430 match inp.start() {
431 Ok(()) => Some(inp),
432 Err(e) => {
433 tracing::warn!("failed to start global input: {}", e);
434 None
435 }
436 }
437 } else {
438 None
439 };
440
441 let rt = Runtime::new().map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
442
443 let (viewport_width, viewport_height) = pending_pages
444 .first()
445 .map(|(_, config)| (config.width, config.height))
446 .unwrap_or((800, 600));
447 let cfg = BrowserConfig::builder()
448 .chrome_executable(&chrome_path)
449 .no_sandbox()
450 .new_headless_mode()
451 .window_size(viewport_width, viewport_height)
452 .viewport(Viewport {
453 width: viewport_width,
454 height: viewport_height,
455 device_scale_factor: Some(1.0),
456 emulating_mobile: false,
457 is_landscape: viewport_width >= viewport_height,
458 has_touch: false,
459 })
460 .build()
461 .map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
462
463 let (browser, mut handler) = rt
464 .block_on(async { Browser::launch(cfg).await })
465 .map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
466
467 rt.spawn(async move { while let Some(_) = handler.next().await {} });
468
469 let screenshot_sink = self.screenshot_sink.clone();
470
471 let mut pages: Vec<(u64, ChromePage)> = Vec::new();
473 for (rd_id, wc) in &pending_pages {
474 let page = rt
475 .block_on(async { browser.new_page("about:blank").await })
476 .map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
477
478 rt.block_on(async {
479 let _ = page.evaluate(Self::bridge_script()).await;
480 });
481
482 let screenshot = rt.block_on(async {
483 use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams;
484 page.screenshot(CaptureScreenshotParams::builder().build())
485 .await
486 .ok()
487 });
488 let (width, height, pixels) = screenshot
489 .as_deref()
490 .and_then(Self::decode_png)
491 .unwrap_or_else(|| {
492 (
493 wc.width,
494 wc.height,
495 vec![0u8; (wc.width * wc.height * 4) as usize],
496 )
497 });
498 if let (Some(sink), Some(bytes)) = (screenshot_sink.as_ref(), screenshot.as_deref()) {
499 sink(bytes, width, height);
500 }
501
502 pages.push((
503 *rd_id,
504 ChromePage {
505 page,
506 width,
507 height,
508 pixels,
509 mouse_pos: (0.0, 0.0),
510 },
511 ));
512 }
513
514 for op in &pending_ops {
516 let page = pages
517 .iter()
518 .find(|(id, _)| match op {
519 PendingOp::LoadUrl(i, _)
520 | PendingOp::LoadHtml(i, _)
521 | PendingOp::EvalScript(i, _)
522 | PendingOp::SendToFrontend(i, _) => id == i,
523 })
524 .map(|(_, p)| &p.page);
525 let Some(page) = page else { continue };
526 match op {
527 PendingOp::LoadUrl(_, url) => {
528 rt.block_on(async {
529 let _ = page.goto(url.as_str()).await;
530 });
531 }
532 PendingOp::LoadHtml(_, html) => {
533 rt.block_on(async {
534 let _ = page.set_content(html.as_str()).await;
535 });
536 }
537 PendingOp::EvalScript(_, script) => {
538 rt.block_on(async {
539 let _ = page.evaluate(script.as_str()).await;
540 });
541 }
542 PendingOp::SendToFrontend(_, msg) => {
543 if let Ok(js) = serde_json::to_string(msg) {
544 let s = format!("window.__RDESKTOP_IPC__({js})");
545 rt.block_on(async {
546 let _ = page.evaluate(s.as_str()).await;
547 });
548 }
549 }
550 }
551 }
552
553 let event_loop = EventLoopBuilder::new().build();
555 let mut tao_windows: HashMap<WindowId, (Window, u64)> = HashMap::new();
556
557 event_loop.run(move |event, el_target, cf| {
558 *cf = ControlFlow::WaitUntil(
559 std::time::Instant::now() + std::time::Duration::from_millis(33),
560 );
561
562 match event {
563 Event::NewEvents(StartCause::Init) => {
564 for (rd_id, wc) in &pending_pages {
565 let window = match WindowBuilder::new()
566 .with_title(&wc.title)
567 .with_inner_size(tao::dpi::LogicalSize::new(wc.width, wc.height))
568 .with_resizable(wc.resizable)
569 .with_decorations(wc.decorations)
570 .with_transparent(wc.transparent)
571 .with_always_on_top(wc.always_on_top)
572 .with_window_icon(rdesktop_core::window_icon(wc))
573 .build(el_target)
574 {
575 Ok(w) => w,
576 Err(e) => {
577 tracing::error!("Window: {}", e);
578 continue;
579 }
580 };
581
582 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
583 Self::blit(&window, &p.pixels, p.width, p.height);
584 }
585
586 let tao_id = window.id();
587
588 rdesktop_core::apply_window_attributes(&window, wc);
590
591 tao_windows.insert(tao_id, (window, *rd_id));
592 tracing::info!(rd_id = rd_id, ?tao_id, "Chrome window created");
593 }
594 }
595
596 Event::WindowEvent {
597 event: WindowEvent::CursorMoved { position, .. },
598 window_id,
599 ..
600 } => {
601 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
602 if let Some((_, p)) = pages.iter_mut().find(|(id, _)| id == rd_id) {
603 p.mouse_pos = (position.x, position.y);
604 let params = DispatchMouseEventParams::builder()
605 .r#type(DispatchMouseEventType::MouseMoved)
606 .x(position.x)
607 .y(position.y)
608 .build()
609 .unwrap();
610 rt.block_on(async {
611 let _ = p.page.execute(params).await;
612 });
613 }
614 }
615 }
616
617 Event::WindowEvent {
618 event:
619 WindowEvent::MouseInput {
620 state: bs, button, ..
621 },
622 window_id,
623 ..
624 } => {
625 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
626 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
627 let mt = match bs {
628 ElementState::Pressed => DispatchMouseEventType::MousePressed,
629 ElementState::Released => DispatchMouseEventType::MouseReleased,
630 _ => DispatchMouseEventType::MouseReleased,
631 };
632 let mb = match button {
633 tao::event::MouseButton::Left => MouseButton::Left,
634 tao::event::MouseButton::Right => MouseButton::Right,
635 tao::event::MouseButton::Middle => MouseButton::Middle,
636 _ => return,
637 };
638 let params = DispatchMouseEventParams::builder()
639 .r#type(mt)
640 .x(p.mouse_pos.0)
641 .y(p.mouse_pos.1)
642 .button(mb)
643 .build()
644 .unwrap();
645 rt.block_on(async {
646 let _ = p.page.execute(params).await;
647 });
648 }
649 }
650 }
651
652 Event::WindowEvent {
653 event: WindowEvent::MouseWheel { delta, .. },
654 window_id,
655 ..
656 } => {
657 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
658 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
659 let (dx, dy) = match delta {
660 tao::event::MouseScrollDelta::LineDelta(dx, dy) => {
661 (dx as f64 * 50.0, dy as f64 * 50.0)
662 }
663 tao::event::MouseScrollDelta::PixelDelta(pos) => (pos.x, pos.y),
664 _ => (0.0, 0.0),
665 };
666 let params = DispatchMouseEventParams::builder()
667 .r#type(DispatchMouseEventType::MouseWheel)
668 .x(p.mouse_pos.0)
669 .y(p.mouse_pos.1)
670 .delta_x(dx)
671 .delta_y(dy)
672 .build()
673 .unwrap();
674 rt.block_on(async {
675 let _ = p.page.execute(params).await;
676 });
677 }
678 }
679 }
680
681 Event::WindowEvent {
682 event:
683 WindowEvent::KeyboardInput {
684 event: key_event, ..
685 },
686 window_id,
687 ..
688 } => {
689 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
690 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
691 let ts = match key_event.state {
692 ElementState::Pressed => DispatchKeyEventType::KeyDown,
693 ElementState::Released => DispatchKeyEventType::KeyUp,
694 _ => DispatchKeyEventType::KeyUp,
695 };
696 let physical = format!("{:?}", key_event.physical_key);
697
698 if physical == "ShiftLeft" || physical == "ShiftRight" {
701 *self.mod_shift.borrow_mut() =
702 matches!(ts, DispatchKeyEventType::KeyDown);
703 } else if physical == "CapsLock"
704 && matches!(ts, DispatchKeyEventType::KeyDown)
705 {
706 let mut c = self.mod_caps.borrow_mut();
707 *c = !*c;
708 }
709 let shift = *self.mod_shift.borrow();
710 let caps = *self.mod_caps.borrow();
711
712 let (key_text, text_opt) = cdp_key_event(&physical, shift, caps);
713 let params_builder = DispatchKeyEventParams::builder()
714 .r#type(ts)
715 .key(key_text.clone())
716 .code(physical.clone());
717 let params = if let Some(t) = text_opt {
718 params_builder.text(t).build().unwrap()
719 } else {
720 params_builder.build().unwrap()
721 };
722 rt.block_on(async {
723 let _ = p.page.execute(params).await;
724 });
725 }
726 }
727 }
728
729 Event::WindowEvent {
730 event: WindowEvent::CloseRequested,
731 window_id,
732 ..
733 } => {
734 if let Some((_, rd_id)) = tao_windows.remove(&window_id) {
735 if let Some(idx) = pages.iter().position(|(id, _)| *id == rd_id) {
736 let (_, p) = pages.remove(idx);
737 rt.block_on(async {
738 let _ = p.page.close().await;
739 });
740 }
741 }
742 if tao_windows.is_empty() {
743 *cf = ControlFlow::Exit;
744 }
745 }
746
747 Event::MainEventsCleared => {
748 for (_, p) in pages.iter_mut() {
750 use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams;
751 if let Ok(bytes) = rt.block_on(async {
752 p.page
753 .screenshot(CaptureScreenshotParams::builder().build())
754 .await
755 }) {
756 if let Some((w, h, bgra)) = Self::decode_png(&bytes) {
757 if let Some(sink) = screenshot_sink.as_ref() {
758 sink(&bytes, w, h);
759 }
760 p.pixels = bgra;
761 p.width = w;
762 p.height = h;
763 }
764 }
765 }
766
767 if let Some(handler) = self.ipc_handler.as_ref() {
769 for (_, p) in pages.iter() {
770 if let Ok(value) =
771 rt.block_on(p.page.evaluate("window.__rdesktop_take__()"))
772 {
773 if let Some(raw) = value.value().and_then(|v| v.as_str()) {
774 if let Ok(messages) =
775 serde_json::from_str::<Vec<IpcMessage>>(raw)
776 {
777 for msg in messages {
778 let response = handler.handle(msg);
779 if let Ok(json) = serde_json::to_string(&response) {
780 let script =
781 format!("window.__RDESKTOP_IPC__({})", json);
782 let _ =
783 rt.block_on(p.page.evaluate(script.as_str()));
784 }
785 }
786 }
787 }
788 }
789 }
790 }
791
792 let live_ops = self.pending_ops.borrow_mut().drain(..).collect::<Vec<_>>();
794 for op in live_ops {
795 if let PendingOp::SendToFrontend(rd_id, msg) = op {
796 if let Some((_, p)) = pages.iter().find(|(id, _)| *id == rd_id) {
797 if let Ok(js) = serde_json::to_string(&msg) {
798 let script = format!("window.__RDESKTOP_IPC__({js})");
799 let _ = rt.block_on(p.page.evaluate(script.as_str()));
800 }
801 }
802 }
803 }
804
805 let outbox_msgs: Vec<String> = {
809 let mut queue = outbox_for_loop.lock().unwrap();
810 queue.drain(..).collect()
811 };
812 for json in outbox_msgs {
813 if let Ok(js) = serde_json::to_string(&json) {
814 let script = format!("window.__RDESKTOP_IPC__({js})");
815 for (_, p) in pages.iter() {
816 let _ = rt.block_on(p.page.evaluate(script.as_str()));
817 }
818 }
819 }
820
821 for (_, (window, rd_id)) in tao_windows.iter() {
822 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
823 Self::blit(window, &p.pixels, p.width, p.height);
824 }
825 }
826 }
827
828 Event::LoopDestroyed => {
829 tracing::info!("Chrome renderer destroyed");
830 }
831 _ => {}
832 }
833 });
834 }
835
836 fn kind(&self) -> RendererKind {
837 RendererKind::Chrome
838 }
839}
840
841fn cdp_key_event(code: &str, shift: bool, caps: bool) -> (String, Option<String>) {
848 if let Some(c) = code.strip_prefix("Key") {
849 let upper = shift ^ caps;
850 let ch = if upper {
851 c.to_uppercase()
852 } else {
853 c.to_lowercase()
854 };
855 return (ch.clone(), Some(ch));
856 }
857 if let Some(d) = code.strip_prefix("Digit") {
858 const SHIFTED: &[&str] = &[")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
859 if let Ok(idx) = d.parse::<usize>() {
860 if idx < SHIFTED.len() {
861 let s = if shift {
862 SHIFTED[idx].to_string()
863 } else {
864 d.to_string()
865 };
866 return (s.clone(), Some(s));
867 }
868 }
869 }
870 match code {
871 "Enter" => ("Enter".into(), None),
872 "Escape" => ("Escape".into(), None),
873 "Backspace" => ("Backspace".into(), None),
874 "Tab" => ("Tab".into(), None),
875 "Space" => (" ".into(), Some(" ".into())),
876 "ArrowLeft" => ("ArrowLeft".into(), None),
877 "ArrowRight" => ("ArrowRight".into(), None),
878 "ArrowUp" => ("ArrowUp".into(), None),
879 "ArrowDown" => ("ArrowDown".into(), None),
880 "Delete" => ("Delete".into(), None),
881 "Home" => ("Home".into(), None),
882 "End" => ("End".into(), None),
883 "PageUp" => ("PageUp".into(), None),
884 "PageDown" => ("PageDown".into(), None),
885 "ShiftLeft" | "ShiftRight" => ("Shift".into(), None),
886 "ControlLeft" | "ControlRight" => ("Control".into(), None),
887 "AltLeft" | "AltRight" => ("Alt".into(), None),
888 "MetaLeft" | "MetaRight" => ("Meta".into(), None),
889 _ => (code.to_string(), None),
890 }
891}