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