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