app_ui/
screen_preview_canvas.rs1use leptos::prelude::*;
15
16#[cfg(target_arch = "wasm32")]
17use leptos::task::spawn_local;
18#[cfg(target_arch = "wasm32")]
19use wasm_bindgen::JsCast;
20#[cfg(target_arch = "wasm32")]
21use wasm_bindgen::prelude::*;
22
23pub const SCREEN_PREVIEW_CANVAS_ID: &str = "screen-preview-canvas";
25
26pub const SCREEN_PREVIEW_WIDTH: u32 = 1280;
30pub const SCREEN_PREVIEW_HEIGHT: u32 = 720;
32pub const SCREEN_PREVIEW_POLL_MS: i32 = 66;
34
35#[cfg(target_arch = "wasm32")]
41pub fn install_screen_preview(active: Memo<bool>, source: RwSignal<Option<String>>) {
42 Effect::new(move |_| {
47 let on = active.get();
48 let src = source.get();
49 spawn_local(async move {
50 if on {
51 let _ = crate::screen_ipc::start_screen_capture(src).await;
52 } else {
53 crate::screen_ipc::stop_screen_capture().await;
54 }
55 });
56 });
57 on_cleanup(|| {
58 spawn_local(async {
59 crate::screen_ipc::stop_screen_capture().await;
60 });
61 });
62
63 let closure = Closure::wrap(Box::new(move || {
64 spawn_local(async move {
65 paint_one_screen_frame().await;
66 });
67 }) as Box<dyn FnMut()>);
68 if let Some(window) = web_sys::window() {
69 let _ = window.set_interval_with_callback_and_timeout_and_arguments_0(
70 closure.as_ref().unchecked_ref(),
71 SCREEN_PREVIEW_POLL_MS,
72 );
73 }
74 closure.forget();
75}
76
77#[cfg(not(target_arch = "wasm32"))]
79pub fn install_screen_preview(_active: Memo<bool>, _source: RwSignal<Option<String>>) {}
80
81#[cfg(target_arch = "wasm32")]
85async fn paint_one_screen_frame() {
86 use js_sys::{Reflect, Uint8ClampedArray};
87 use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, ImageData};
88
89 let bytes_len = (SCREEN_PREVIEW_WIDTH as usize) * (SCREEN_PREVIEW_HEIGHT as usize) * 4;
90
91 let Some(window) = web_sys::window() else {
92 return;
93 };
94 let Some(document) = window.document() else {
95 return;
96 };
97 let Some(canvas_el) = document.get_element_by_id(SCREEN_PREVIEW_CANVAS_ID) else {
100 return;
101 };
102 let Ok(canvas) = canvas_el.dyn_into::<HtmlCanvasElement>() else {
103 return;
104 };
105
106 let Ok(invoke_fn) = Reflect::get(&window, &JsValue::from_str("__screenLatestScreenFrameBgra"))
107 else {
108 return;
109 };
110 if !invoke_fn.is_function() {
111 return;
112 }
113 let invoke: js_sys::Function = invoke_fn.unchecked_into();
114 let Ok(promise) = invoke.call0(&JsValue::NULL) else {
115 return;
116 };
117 let promise: js_sys::Promise = match promise.dyn_into() {
118 Ok(p) => p,
119 Err(_) => return,
120 };
121 let Ok(buf) = wasm_bindgen_futures::JsFuture::from(promise).await else {
122 return;
123 };
124
125 let bytes = if let Ok(array_buffer) = buf.clone().dyn_into::<js_sys::ArrayBuffer>() {
126 Uint8ClampedArray::new(&array_buffer)
127 } else if let Ok(typed_array) = buf.dyn_into::<Uint8ClampedArray>() {
128 typed_array
129 } else {
130 return;
131 };
132 let len = bytes.length() as usize;
134 if len < bytes_len {
135 return;
136 }
137
138 let mut rgba = vec![0u8; bytes_len];
139 bytes.copy_to(&mut rgba[..bytes_len]);
140 for px in rgba.chunks_exact_mut(4) {
141 px.swap(0, 2);
143 }
144
145 let Ok(Some(ctx)) = canvas.get_context("2d") else {
146 return;
147 };
148 let Ok(ctx) = ctx.dyn_into::<CanvasRenderingContext2d>() else {
149 return;
150 };
151 let Ok(image_data) = ImageData::new_with_u8_clamped_array_and_sh(
152 wasm_bindgen::Clamped(&rgba[..]),
153 SCREEN_PREVIEW_WIDTH,
154 SCREEN_PREVIEW_HEIGHT,
155 ) else {
156 return;
157 };
158 let _ = ctx.put_image_data(&image_data, 0.0, 0.0);
159}