ferroui_browser/rendering/
web_render_target.rs1use super::{BrowserSoftwareRenderTarget, BrowserWebGlRenderTarget};
2use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
3use crate::interop::{non_null, JsObject};
4use ferroui_base::platform::IPlatformGraphicsContext;
5use ferroui_base::PixelSize;
6use std::cell::{Cell, RefCell};
7use std::collections::HashMap;
8use std::rc::Rc;
9use wasm_bindgen::prelude::*;
10
11#[wasm_bindgen(raw_module = "./ferroui.js")]
12extern "C" {
13 pub type JsRenderTarget;
15
16 #[wasm_bindgen(method, getter, js_name = renderTargetType)]
17 fn render_target_type(this: &JsRenderTarget) -> String;
18
19 #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = getRenderTarget)]
20 fn get_js_render_target(id: i32) -> JsObject;
21
22 #[wasm_bindgen(js_namespace = WebRenderTarget, js_name = setSize)]
23 fn set_js_size(target: &JsObject, w: i32, h: i32);
24
25 #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = initializeWorker)]
26 fn initialize_js_worker();
27
28 #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = workerStarted)]
29 fn js_worker_started(thread_id: i32);
30
31 #[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = unregister)]
32 fn js_unregister(id: i32);
33}
34
35pub fn initialize_worker() {
45 initialize_js_worker();
46}
47
48pub const PENDING_RENDER_THREAD: i32 = -1;
57
58pub(crate) fn worker_started(thread_id: i32) {
64 js_worker_started(thread_id);
65}
66
67pub(crate) fn unregister_canvas(id: i32) {
78 let unregister = SCRIPT_UNREGISTER.with(|unregister| unregister.get());
79 unregister(id);
80}
81
82#[derive(Clone)]
89pub enum BrowserRenderTarget {
90 WebGl(Rc<BrowserWebGlRenderTarget>),
92 Software(Rc<BrowserSoftwareRenderTarget>),
94}
95
96impl BrowserRenderTarget {
97 pub fn platform_graphics_context(&self) -> Option<Rc<dyn IPlatformGraphicsContext>> {
100 match self {
101 Self::WebGl(target) => Some(target.gl_context()),
102 Self::Software(_) => None,
103 }
104 }
105
106 pub fn kind(&self) -> i32 {
110 match self {
111 Self::WebGl(_) => RENDER_TARGET_KIND_WEB_GL,
112 Self::Software(_) => RENDER_TARGET_KIND_SOFTWARE,
113 }
114 }
115}
116
117type ScriptRenderTargets = fn(i32) -> Option<BrowserRenderTarget>;
119
120ferroui_base::ferro_thread_local! {
121 static RENDER_TARGETS: RefCell<HashMap<i32, BrowserRenderTarget>> = RefCell::new(HashMap::new());
126 static SCRIPT_RENDER_TARGETS: Cell<ScriptRenderTargets> = Cell::new(wrap_script_render_target as ScriptRenderTargets);
129 static SCRIPT_UNREGISTER: Cell<fn(i32)> = Cell::new(js_unregister as fn(i32));
132}
133
134fn wrap_script_render_target(id: i32) -> Option<BrowserRenderTarget> {
140 let js = non_null(get_js_render_target(id))?;
141 let type_ = JsCast::unchecked_ref::<JsRenderTarget>(&js).render_target_type();
142 if type_ == "webgl" {
143 return Some(BrowserRenderTarget::WebGl(BrowserWebGlRenderTarget::new(js)));
144 }
145 if type_ == "software" {
146 return Some(BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(js)));
147 }
148 panic!("{type_}");
149}
150
151pub fn get_render_target(id: i32) -> Option<BrowserRenderTarget> {
162 if let Some(target) = RENDER_TARGETS.with(|targets| targets.borrow().get(&id).cloned()) {
163 return Some(target);
164 }
165 let script = SCRIPT_RENDER_TARGETS.with(|script| script.get());
168 let target = script(id)?;
169 RENDER_TARGETS.with(|targets| targets.borrow_mut().insert(id, target.clone()));
170 Some(target)
171}
172
173pub fn remove_render_target(id: i32) -> bool {
180 RENDER_TARGETS.with(|targets| targets.borrow_mut().remove(&id)).is_some()
181}
182
183#[cfg(test)]
185pub(crate) fn set_script_render_targets_for_unit_tests(script: fn(i32) -> Option<BrowserRenderTarget>) {
186 SCRIPT_RENDER_TARGETS.with(|current| current.set(script));
187}
188
189#[cfg(test)]
192pub(crate) fn set_script_unregister_for_unit_tests(unregister: fn(i32)) {
193 SCRIPT_UNREGISTER.with(|current| current.set(unregister));
194}
195
196pub(crate) fn update_size(js: &JsObject, size: PixelSize) {
198 set_js_size(js, size.width, size.height);
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 ferroui_base::ferro_thread_local! {
206 static ASKED: Cell<u32> = const { Cell::new(0) };
208 }
209
210 fn asked() -> u32 {
211 ASKED.with(|asked| asked.get())
212 }
213
214 fn script(id: i32) -> Option<BrowserRenderTarget> {
216 ASKED.with(|asked| asked.set(asked.get() + 1));
217 (id == 41 || id == 42).then(|| BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(JsObject::NULL)))
218 }
219
220 fn empty_script(_id: i32) -> Option<BrowserRenderTarget> {
223 ASKED.with(|asked| asked.set(asked.get() + 1));
224 None
225 }
226
227 fn same(a: &BrowserRenderTarget, b: &BrowserRenderTarget) -> bool {
228 match (a, b) {
229 (BrowserRenderTarget::Software(a), BrowserRenderTarget::Software(b)) => Rc::ptr_eq(a, b),
230 (BrowserRenderTarget::WebGl(a), BrowserRenderTarget::WebGl(b)) => Rc::ptr_eq(a, b),
231 _ => false,
232 }
233 }
234
235 #[test]
236 fn a_target_is_wrapped_once_and_kept_for_its_thread() {
237 set_script_render_targets_for_unit_tests(script);
238
239 let first = get_render_target(41).expect("the script has the target");
240 let again = get_render_target(41).expect("the table has the target");
241 assert!(same(&first, &again));
242 assert_eq!(1, asked());
243 assert_eq!(RENDER_TARGET_KIND_SOFTWARE, first.kind());
244 assert!(first.platform_graphics_context().is_none());
245
246 let other = get_render_target(42).expect("the script has the target");
248 assert!(!same(&first, &other));
249 assert_eq!(2, asked());
250 }
251
252 #[test]
253 fn a_target_the_script_does_not_have_is_asked_for_each_time() {
254 set_script_render_targets_for_unit_tests(script);
255
256 assert!(get_render_target(7).is_none());
257 assert!(get_render_target(7).is_none());
258 assert_eq!(2, asked());
259 }
260
261 #[test]
262 fn each_thread_has_a_table_of_its_own() {
263 set_script_render_targets_for_unit_tests(script);
264 let here = get_render_target(41).expect("the script has the target");
265
266 let there = std::thread::spawn(|| {
269 set_script_render_targets_for_unit_tests(empty_script);
270 let found = get_render_target(41).is_some();
271 (found, asked())
272 })
273 .join()
274 .unwrap();
275 assert_eq!((false, 1), there);
276
277 let wrapped = std::thread::spawn(|| {
279 set_script_render_targets_for_unit_tests(script);
280 get_render_target(41).is_some() && get_render_target(41).is_some() && asked() == 1
281 })
282 .join()
283 .unwrap();
284 assert!(wrapped);
285
286 assert!(same(&here, &get_render_target(41).expect("the table has the target")));
287 assert_eq!(1, asked());
288 }
289
290 #[test]
291 fn a_removed_target_is_asked_for_again() {
292 set_script_render_targets_for_unit_tests(script);
293 let first = get_render_target(42).expect("the script has the target");
294
295 assert!(remove_render_target(42));
296 assert!(!remove_render_target(42));
297
298 let second = get_render_target(42).expect("the script has the target");
299 assert!(!same(&first, &second));
300 assert_eq!(2, asked());
301 }
302}