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, DispatchMouseEventParams, MouseButton,
19 DispatchMouseEventType, DispatchKeyEventType,
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)]
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, y_dest: i32,
76 dest_width: i32, dest_height: i32,
77 x_src: i32, y_src: i32,
78 src_width: i32, src_height: i32,
79 bits: *const c_void,
80 bits_info: *const c_void,
81 usage: u32,
82 rop: u32,
83 ) -> i32;
84 }
85}
86struct ChromePage {
89 page: Page,
90 width: u32,
91 height: u32,
92 pixels: Vec<u8>, mouse_pos: (f64, f64),
94}
95
96enum PendingOp {
97 LoadUrl(u64, String),
98 LoadHtml(u64, String),
99 EvalScript(u64, String),
100 SendToFrontend(u64, String),
101}
102
103pub struct CefRenderer {
104 _config: AppConfig,
105 pending_pages: RefCell<Vec<(u64, WindowConfig)>>,
106 pending_ops: RefCell<Vec<PendingOp>>,
107 next_id: RefCell<u64>,
108 ipc_handler: Option<Arc<dyn IpcHandler>>,
109 mod_shift: RefCell<bool>,
110 mod_caps: RefCell<bool>,
111 outbox: Arc<Mutex<Vec<String>>>,
115}
116
117impl CefRenderer {
118 pub fn new(config: &AppConfig) -> Result<Self> {
119 Ok(Self {
120 _config: config.clone(),
121 pending_pages: RefCell::new(Vec::new()),
122 pending_ops: RefCell::new(Vec::new()),
123 next_id: RefCell::new(1),
124 ipc_handler: None,
125 mod_shift: RefCell::new(false),
126 mod_caps: RefCell::new(false),
127 outbox: Arc::new(Mutex::new(Vec::new())),
128 })
129 }
130
131 pub fn set_outbox(&mut self, outbox: Arc<Mutex<Vec<String>>>) {
136 self.outbox = outbox;
137 }
138
139 fn alloc_id(&self) -> u64 {
140 let mut id = self.next_id.borrow_mut();
141 let v = *id;
142 *id += 1;
143 v
144 }
145
146 fn find_chrome() -> Option<String> {
147 let lad = std::env::var("LOCALAPPDATA").unwrap_or_default();
148 let cands: Vec<String> = if cfg!(target_os = "windows") {
149 vec![
150 r"C:\Program Files\Google\Chrome\Application\chrome.exe".into(),
151 r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe".into(),
152 r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe".into(),
153 format!(r"{}\Google\Chrome\Application\chrome.exe", lad),
154 ]
155 } else if cfg!(target_os = "macos") {
156 vec![
157 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome".into(),
158 "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge".into(),
159 ]
160 } else {
161 vec![
162 "/usr/bin/google-chrome".into(),
163 "/usr/bin/chromium".into(),
164 "/usr/bin/chromium-browser".into(),
165 ]
166 };
167 cands.into_iter().find(|p| std::path::Path::new(p).exists())
168 }
169
170 fn bridge_script() -> &'static str {
171 r#"
172 (function() {
173 if (window.__RDESKTOP_BRIDGE__) return;
174 window.__RDESKTOP_BRIDGE__ = true;
175 window.__RDESKTOP_RESOLVE__ = {};
176 window.__RDESKTOP_QUEUE__ = [];
177 window.__RDESKTOP_INVOKE__ = function(cmd, payload) {
178 return new Promise(function(resolve, reject) {
179 var id = Math.random().toString(36).slice(2);
180 window.__RDESKTOP_RESOLVE__[id] = resolve;
181 window.__RDESKTOP_QUEUE__.push({ id: id, cmd: cmd, payload: payload || {} });
182 setTimeout(function() {
183 if (window.__RDESKTOP_RESOLVE__[id]) {
184 delete window.__RDESKTOP_RESOLVE__[id];
185 reject(new Error('IPC timeout'));
186 }
187 }, 30000);
188 });
189 };
190 window.__rdesktop_take__ = function() {
191 var q = window.__RDESKTOP_QUEUE__ || [];
192 window.__RDESKTOP_QUEUE__ = [];
193 return JSON.stringify(q);
194 };
195 window.__RDESKTOP_IPC__ = function(message) {
196 try {
197 var data = typeof message === 'string' ? JSON.parse(message) : message;
198 if (data.id && window.__RDESKTOP_RESOLVE__[data.id]) {
199 window.__RDESKTOP_RESOLVE__[data.id](data);
200 delete window.__RDESKTOP_RESOLVE__[data.id];
201 } else if (window.__RDESKTOP_PUSH__) {
202 // Unnamed push (e.g. extension host → UI event, or a
203 // global hotkey / global input event).
204 window.__RDESKTOP_PUSH__(data);
205 }
206 } catch (e) { console.error('rdesktop IPC error:', e); }
207 };
208 window.__RDESKTOP_WINDOW__ = {
209 minimize: function() {},
210 maximize: function() {},
211 close: function() { window.close(); },
212 startDrag: function() {},
213 startResize: function() {},
214 setFullscreen: function() {},
215 isMaximized: false,
216 isFullscreen: false
217 };
218 })();
219 "#
220 }
221
222 fn decode_png(png_bytes: &[u8]) -> Option<(u32, u32, Vec<u8>)> {
223 let dec = png::Decoder::new(std::io::Cursor::new(png_bytes));
224 let mut reader = dec.read_info().ok()?;
225 let mut buf = vec![0u8; reader.output_buffer_size().unwrap_or(0)];
226 let info = reader.next_frame(&mut buf).ok()?;
227 let (w, h) = (info.width, info.height);
228 let rgba = &buf[..info.buffer_size()];
229 let mut bgra = Vec::with_capacity((w * h * 4) as usize);
230 for c in rgba.chunks_exact(4) {
231 bgra.extend_from_slice(&[c[2], c[1], c[0], 255]);
232 }
233 Some((w, h, bgra))
234 }
235
236 #[cfg(target_os = "windows")]
237 fn blit(window: &Window, pixels: &[u8], w: u32, h: u32) {
238 use tao::platform::windows::WindowExtWindows;
239 use gdi::*;
240
241 let hwnd = window.hwnd() as HWND;
242 unsafe {
243 let hdc = GetDC(hwnd);
244 if hdc.is_null() { return; }
245
246 let bmi = BITMAPINFOHEADER {
247 biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
248 biWidth: w as i32,
249 biHeight: -(h as i32), biPlanes: 1,
251 biBitCount: 32,
252 biCompression: BI_RGB,
253 biSizeImage: 0,
254 biXPelsPerMeter: 0,
255 biYPelsPerMeter: 0,
256 biClrUsed: 0,
257 biClrImportant: 0,
258 };
259
260 let mut rect = RECT { left: 0, top: 0, right: 0, bottom: 0 };
261 GetClientRect(hwnd, &mut rect);
262
263 StretchDIBits(
264 hdc,
265 0, 0,
266 rect.right, rect.bottom,
267 0, 0,
268 w as i32, h as i32,
269 pixels.as_ptr() as *const _,
270 &bmi as *const _ as *const _,
271 DIB_RGB_COLORS,
272 SRCCOPY,
273 );
274
275 ReleaseDC(hwnd, hdc);
276 }
277 }
278
279 #[cfg(not(target_os = "windows"))]
280 fn blit(_window: &Window, _pixels: &[u8], _w: u32, _h: u32) {
281 tracing::warn!("Chrome GDI rendering: Windows only. Other platforms pending.");
282 }
283}
284
285impl Renderer for CefRenderer {
286 fn init(&mut self) -> Result<()> { Ok(()) }
287
288 fn create_window(&mut self, config: &WindowConfig) -> Result<WindowHandle> {
289 let id = self.alloc_id();
290 self.pending_pages.borrow_mut().push((id, config.clone()));
291 Ok(WindowHandle::new(id))
292 }
293
294 fn load_url(&self, w: WindowHandle, url: &str) -> Result<()> {
295 self.pending_ops.borrow_mut().push(PendingOp::LoadUrl(w.id(), url.into())); Ok(())
296 }
297 fn load_html(&self, w: WindowHandle, html: &str) -> Result<()> {
298 self.pending_ops.borrow_mut().push(PendingOp::LoadHtml(w.id(), html.into())); Ok(())
299 }
300 fn eval_script(&self, w: WindowHandle, script: &str) -> Result<()> {
301 self.pending_ops.borrow_mut().push(PendingOp::EvalScript(w.id(), script.into())); Ok(())
302 }
303 fn set_ipc_handler(&mut self, h: Box<dyn IpcHandler>) {
304 self.ipc_handler = Some(Arc::from(h));
305 }
306 fn send_to_frontend(&self, w: WindowHandle, msg: &str) -> Result<()> {
307 self.pending_ops.borrow_mut().push(PendingOp::SendToFrontend(w.id(), msg.into())); Ok(())
308 }
309 fn set_title(&self, _w: WindowHandle, _t: &str) -> Result<()> { Ok(()) }
310 fn set_size(&self, _w: WindowHandle, _x: u32, _y: u32) -> Result<()> { Ok(()) }
311 fn set_resizable(&self, _w: WindowHandle, _r: bool) -> Result<()> { Ok(()) }
312 fn set_visible(&self, _w: WindowHandle, _v: bool) -> Result<()> { Ok(()) }
313 fn close_window(&mut self, _w: WindowHandle) -> Result<()> { Ok(()) }
314 fn minimize_window(&self, _w: WindowHandle) -> Result<()> { Ok(()) }
315 fn maximize_window(&self, _w: WindowHandle) -> Result<()> { Ok(()) }
316 fn is_maximized(&self, _w: WindowHandle) -> Result<bool> { Ok(false) }
317 fn set_fullscreen(&self, _w: WindowHandle, _f: bool) -> Result<()> { Ok(()) }
318 fn is_fullscreen(&self, _w: WindowHandle) -> Result<bool> { Ok(false) }
319 fn start_drag(&self, _w: WindowHandle) -> Result<()> { Ok(()) }
320 fn start_resize(&self, _w: WindowHandle, _e: ResizeEdge) -> Result<()> { Ok(()) }
321 fn set_decorations(&self, _w: WindowHandle, _d: bool) -> Result<()> { Ok(()) }
322 fn set_always_on_top(&self, _w: WindowHandle, _a: bool) -> Result<()> { Ok(()) }
323
324 fn run(self: Box<Self>) -> Result<()> {
325 tracing::info!("Chrome renderer starting");
326
327 let chrome_path = Self::find_chrome().ok_or_else(|| {
328 rdesktop_core::RdesktopError::Cef("Chrome not found".into())
329 })?;
330
331 let pending_pages = self.pending_pages.borrow_mut().drain(..).collect::<Vec<_>>();
332 let pending_ops = self.pending_ops.borrow_mut().drain(..).collect::<Vec<_>>();
333
334 let outbox_for_loop = self.outbox.clone();
336
337 let global_handler = rdesktop_core::PushHandler::new(self.outbox.clone());
342 let _hotkey_manager = {
343 let mgr = rdesktop_core::HotkeyManager::new(global_handler.clone());
344 for (i, hc) in self._config.hotkeys.iter().enumerate() {
345 if let Ok(hk) = hc.combo.parse::<rdesktop_core::Hotkey>() {
346 let id = i as u32 + 1;
347 if let Err(e) = mgr.register(id, &hk) {
348 tracing::warn!("failed to register hotkey {:?}: {}", hc.combo, e);
349 }
350 } else {
351 tracing::warn!("invalid hotkey combo: {:?}", hc.combo);
352 }
353 }
354 mgr
355 };
356 let _input_manager = if self._config.global_input.enabled {
357 let mut inp = rdesktop_core::GlobalInput::new(global_handler.clone());
358 if self._config.global_input.mouse_move {
359 inp = inp.with_mouse_move(true);
360 }
361 match inp.start() {
362 Ok(()) => Some(inp),
363 Err(e) => {
364 tracing::warn!("failed to start global input: {}", e);
365 None
366 }
367 }
368 } else {
369 None
370 };
371
372 let rt = Runtime::new().map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
373
374 let cfg = BrowserConfig::builder()
375 .chrome_executable(&chrome_path)
376 .no_sandbox()
377 .new_headless_mode()
378 .build()
379 .map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
380
381 let (browser, mut handler) = rt.block_on(async {
382 Browser::launch(cfg).await
383 }).map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
384
385 rt.spawn(async move { while let Some(_) = handler.next().await {} });
386
387 let mut pages: Vec<(u64, ChromePage)> = Vec::new();
389 for (rd_id, wc) in &pending_pages {
390 let page = rt.block_on(async {
391 browser.new_page("about:blank").await
392 }).map_err(|e| rdesktop_core::RdesktopError::Cef(format!("{}", e)))?;
393
394 rt.block_on(async { let _ = page.evaluate(Self::bridge_script()).await; });
395
396 let pixels = rt.block_on(async {
397 use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams;
398 let bytes = page.screenshot(CaptureScreenshotParams::builder().build()).await.ok()?;
399 let (_, _, bgra) = Self::decode_png(&bytes)?;
400 Some(bgra)
401 }).unwrap_or_else(|| vec![0u8; (wc.width * wc.height * 4) as usize]);
402
403 pages.push((*rd_id, ChromePage {
404 page, width: wc.width, height: wc.height, pixels, mouse_pos: (0.0, 0.0),
405 }));
406 }
407
408 for op in &pending_ops {
410 let page = pages.iter().find(|(id, _)| match op {
411 PendingOp::LoadUrl(i, _) | PendingOp::LoadHtml(i, _)
412 | PendingOp::EvalScript(i, _) | PendingOp::SendToFrontend(i, _) => id == i,
413 }).map(|(_, p)| &p.page);
414 let Some(page) = page else { continue };
415 match op {
416 PendingOp::LoadUrl(_, url) => { rt.block_on(async { let _ = page.goto(url.as_str()).await; }); }
417 PendingOp::LoadHtml(_, html) => { rt.block_on(async { let _ = page.set_content(html.as_str()).await; }); }
418 PendingOp::EvalScript(_, script) => { rt.block_on(async { let _ = page.evaluate(script.as_str()).await; }); }
419 PendingOp::SendToFrontend(_, msg) => {
420 if let Ok(js) = serde_json::to_string(msg) {
421 let s = format!("window.__RDESKTOP_IPC__({js})");
422 rt.block_on(async { let _ = page.evaluate(s.as_str()).await; });
423 }
424 }
425 }
426 }
427
428 let event_loop = EventLoopBuilder::new().build();
430 let mut tao_windows: HashMap<WindowId, (Window, u64)> = HashMap::new();
431
432 event_loop.run(move |event, el_target, cf| {
433 *cf = ControlFlow::WaitUntil(
434 std::time::Instant::now() + std::time::Duration::from_millis(33),
435 );
436
437 match event {
438 Event::NewEvents(StartCause::Init) => {
439 for (rd_id, wc) in &pending_pages {
440 let window = match WindowBuilder::new()
441 .with_title(&wc.title)
442 .with_inner_size(tao::dpi::LogicalSize::new(wc.width, wc.height))
443 .with_resizable(wc.resizable)
444 .with_decorations(wc.decorations)
445 .with_transparent(wc.transparent)
446 .with_always_on_top(wc.always_on_top)
447 .with_window_icon(rdesktop_core::window_icon(wc))
448 .build(el_target)
449 {
450 Ok(w) => w,
451 Err(e) => { tracing::error!("Window: {}", e); continue; }
452 };
453
454 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
455 Self::blit(&window, &p.pixels, p.width, p.height);
456 }
457
458 let tao_id = window.id();
459
460 rdesktop_core::apply_window_attributes(&window, wc);
462
463 tao_windows.insert(tao_id, (window, *rd_id));
464 tracing::info!(rd_id = rd_id, ?tao_id, "Chrome window created");
465 }
466 }
467
468 Event::WindowEvent { event: WindowEvent::CursorMoved { position, .. }, window_id, .. } => {
469 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
470 if let Some((_, p)) = pages.iter_mut().find(|(id, _)| id == rd_id) {
471 p.mouse_pos = (position.x, position.y);
472 let params = DispatchMouseEventParams::builder()
473 .r#type(DispatchMouseEventType::MouseMoved)
474 .x(position.x).y(position.y).build().unwrap();
475 rt.block_on(async { let _ = p.page.execute(params).await; });
476 }
477 }
478 }
479
480 Event::WindowEvent { event: WindowEvent::MouseInput { state: bs, button, .. }, window_id, .. } => {
481 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
482 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
483 let mt = match bs {
484 ElementState::Pressed => DispatchMouseEventType::MousePressed,
485 ElementState::Released => DispatchMouseEventType::MouseReleased,
486 _ => DispatchMouseEventType::MouseReleased,
487 };
488 let mb = match button {
489 tao::event::MouseButton::Left => MouseButton::Left,
490 tao::event::MouseButton::Right => MouseButton::Right,
491 tao::event::MouseButton::Middle => MouseButton::Middle,
492 _ => return,
493 };
494 let params = DispatchMouseEventParams::builder()
495 .r#type(mt).x(p.mouse_pos.0).y(p.mouse_pos.1).button(mb).build().unwrap();
496 rt.block_on(async { let _ = p.page.execute(params).await; });
497 }
498 }
499 }
500
501 Event::WindowEvent { event: WindowEvent::MouseWheel { delta, .. }, window_id, .. } => {
502 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
503 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
504 let (dx, dy) = match delta {
505 tao::event::MouseScrollDelta::LineDelta(dx, dy) => (dx as f64 * 50.0, dy as f64 * 50.0),
506 tao::event::MouseScrollDelta::PixelDelta(pos) => (pos.x, pos.y),
507 _ => (0.0, 0.0),
508 };
509 let params = DispatchMouseEventParams::builder()
510 .r#type(DispatchMouseEventType::MouseWheel).x(p.mouse_pos.0).y(p.mouse_pos.1)
511 .delta_x(dx).delta_y(dy).build().unwrap();
512 rt.block_on(async { let _ = p.page.execute(params).await; });
513 }
514 }
515 }
516
517 Event::WindowEvent { event: WindowEvent::KeyboardInput { event: key_event, .. }, window_id, .. } => {
518 if let Some((_, rd_id)) = tao_windows.get(&window_id) {
519 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
520 let ts = match key_event.state {
521 ElementState::Pressed => DispatchKeyEventType::KeyDown,
522 ElementState::Released => DispatchKeyEventType::KeyUp,
523 _ => DispatchKeyEventType::KeyUp,
524 };
525 let physical = format!("{:?}", key_event.physical_key);
526
527 if physical == "ShiftLeft" || physical == "ShiftRight" {
530 *self.mod_shift.borrow_mut() = matches!(ts, DispatchKeyEventType::KeyDown);
531 } else if physical == "CapsLock" && matches!(ts, DispatchKeyEventType::KeyDown) {
532 let mut c = self.mod_caps.borrow_mut();
533 *c = !*c;
534 }
535 let shift = *self.mod_shift.borrow();
536 let caps = *self.mod_caps.borrow();
537
538 let (key_text, text_opt) = cdp_key_event(&physical, shift, caps);
539 let params_builder = DispatchKeyEventParams::builder()
540 .r#type(ts)
541 .key(key_text.clone())
542 .code(physical.clone());
543 let params = if let Some(t) = text_opt {
544 params_builder.text(t).build().unwrap()
545 } else {
546 params_builder.build().unwrap()
547 };
548 rt.block_on(async { let _ = p.page.execute(params).await; });
549 }
550 }
551 }
552
553 Event::WindowEvent { event: WindowEvent::CloseRequested, window_id, .. } => {
554 if let Some((_, rd_id)) = tao_windows.remove(&window_id) {
555 if let Some(idx) = pages.iter().position(|(id, _)| *id == rd_id) {
556 let (_, p) = pages.remove(idx);
557 rt.block_on(async { let _ = p.page.close().await; });
558 }
559 }
560 if tao_windows.is_empty() { *cf = ControlFlow::Exit; }
561 }
562
563 Event::MainEventsCleared => {
564 for (_, p) in pages.iter_mut() {
566 use chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams;
567 if let Ok(bytes) = rt.block_on(async {
568 p.page.screenshot(CaptureScreenshotParams::builder().build()).await
569 }) {
570 if let Some((w, h, bgra)) = Self::decode_png(&bytes) {
571 p.pixels = bgra;
572 p.width = w;
573 p.height = h;
574 }
575 }
576 }
577
578 if let Some(handler) = self.ipc_handler.as_ref() {
580 for (_, p) in pages.iter() {
581 if let Ok(value) = rt.block_on(p.page.evaluate("window.__rdesktop_take__()")) {
582 if let Some(raw) = value.value().and_then(|v| v.as_str()) {
583 if let Ok(messages) = serde_json::from_str::<Vec<IpcMessage>>(raw) {
584 for msg in messages {
585 let response = handler.handle(msg);
586 if let Ok(json) = serde_json::to_string(&response) {
587 let script = format!("window.__RDESKTOP_IPC__({})", json);
588 let _ = rt.block_on(p.page.evaluate(script.as_str()));
589 }
590 }
591 }
592 }
593 }
594 }
595 }
596
597 let live_ops = self.pending_ops.borrow_mut().drain(..).collect::<Vec<_>>();
599 for op in live_ops {
600 if let PendingOp::SendToFrontend(rd_id, msg) = op {
601 if let Some((_, p)) = pages.iter().find(|(id, _)| *id == rd_id) {
602 if let Ok(js) = serde_json::to_string(&msg) {
603 let script = format!("window.__RDESKTOP_IPC__({js})");
604 let _ = rt.block_on(p.page.evaluate(script.as_str()));
605 }
606 }
607 }
608 }
609
610 let outbox_msgs: Vec<String> = {
614 let mut queue = outbox_for_loop.lock().unwrap();
615 queue.drain(..).collect()
616 };
617 for json in outbox_msgs {
618 if let Ok(js) = serde_json::to_string(&json) {
619 let script = format!("window.__RDESKTOP_IPC__({js})");
620 for (_, p) in pages.iter() {
621 let _ = rt.block_on(p.page.evaluate(script.as_str()));
622 }
623 }
624 }
625
626 for (_, (window, rd_id)) in tao_windows.iter() {
627 if let Some((_, p)) = pages.iter().find(|(id, _)| id == rd_id) {
628 Self::blit(window, &p.pixels, p.width, p.height);
629 }
630 }
631 }
632
633 Event::LoopDestroyed => { tracing::info!("Chrome renderer destroyed"); }
634 _ => {}
635 }
636 });
637 }
638
639 fn kind(&self) -> RendererKind { RendererKind::Chrome }
640}
641
642fn cdp_key_event(code: &str, shift: bool, caps: bool) -> (String, Option<String>) {
649 if let Some(c) = code.strip_prefix("Key") {
650 let upper = shift ^ caps;
651 let ch = if upper { c.to_uppercase() } else { c.to_lowercase() };
652 return (ch.clone(), Some(ch));
653 }
654 if let Some(d) = code.strip_prefix("Digit") {
655 const SHIFTED: &[&str] = &[")", "!", "@", "#", "$", "%", "^", "&", "*", "("];
656 if let Ok(idx) = d.parse::<usize>() {
657 if idx < SHIFTED.len() {
658 let s = if shift { SHIFTED[idx].to_string() } else { d.to_string() };
659 return (s.clone(), Some(s));
660 }
661 }
662 }
663 match code {
664 "Enter" => ("Enter".into(), None),
665 "Escape" => ("Escape".into(), None),
666 "Backspace" => ("Backspace".into(), None),
667 "Tab" => ("Tab".into(), None),
668 "Space" => (" ".into(), Some(" ".into())),
669 "ArrowLeft" => ("ArrowLeft".into(), None),
670 "ArrowRight" => ("ArrowRight".into(), None),
671 "ArrowUp" => ("ArrowUp".into(), None),
672 "ArrowDown" => ("ArrowDown".into(), None),
673 "Delete" => ("Delete".into(), None),
674 "Home" => ("Home".into(), None),
675 "End" => ("End".into(), None),
676 "PageUp" => ("PageUp".into(), None),
677 "PageDown" => ("PageDown".into(), None),
678 "ShiftLeft" | "ShiftRight" => ("Shift".into(), None),
679 "ControlLeft" | "ControlRight" => ("Control".into(), None),
680 "AltLeft" | "AltRight" => ("Alt".into(), None),
681 "MetaLeft" | "MetaRight" => ("Meta".into(), None),
682 _ => (code.to_string(), None),
683 }
684}