use super::{BrowserSoftwareRenderTarget, BrowserWebGlRenderTarget};
use crate::interop::canvas_helper::{RENDER_TARGET_KIND_SOFTWARE, RENDER_TARGET_KIND_WEB_GL};
use crate::interop::{non_null, JsObject};
use ferroui_base::platform::IPlatformGraphicsContext;
use ferroui_base::PixelSize;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(raw_module = "./ferroui.js")]
extern "C" {
pub type JsRenderTarget;
#[wasm_bindgen(method, getter, js_name = renderTargetType)]
fn render_target_type(this: &JsRenderTarget) -> String;
#[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = getRenderTarget)]
fn get_js_render_target(id: i32) -> JsObject;
#[wasm_bindgen(js_namespace = WebRenderTarget, js_name = setSize)]
fn set_js_size(target: &JsObject, w: i32, h: i32);
#[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = initializeWorker)]
fn initialize_js_worker();
#[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = workerStarted)]
fn js_worker_started(thread_id: i32);
#[wasm_bindgen(js_namespace = WebRenderTargetRegistry, js_name = unregister)]
fn js_unregister(id: i32);
}
pub fn initialize_worker() {
initialize_js_worker();
}
pub const PENDING_RENDER_THREAD: i32 = -1;
pub(crate) fn worker_started(thread_id: i32) {
js_worker_started(thread_id);
}
pub(crate) fn unregister_canvas(id: i32) {
let unregister = SCRIPT_UNREGISTER.with(|unregister| unregister.get());
unregister(id);
}
#[derive(Clone)]
pub enum BrowserRenderTarget {
WebGl(Rc<BrowserWebGlRenderTarget>),
Software(Rc<BrowserSoftwareRenderTarget>),
}
impl BrowserRenderTarget {
pub fn platform_graphics_context(&self) -> Option<Rc<dyn IPlatformGraphicsContext>> {
match self {
Self::WebGl(target) => Some(target.gl_context()),
Self::Software(_) => None,
}
}
pub fn kind(&self) -> i32 {
match self {
Self::WebGl(_) => RENDER_TARGET_KIND_WEB_GL,
Self::Software(_) => RENDER_TARGET_KIND_SOFTWARE,
}
}
}
type ScriptRenderTargets = fn(i32) -> Option<BrowserRenderTarget>;
ferroui_base::ferro_thread_local! {
static RENDER_TARGETS: RefCell<HashMap<i32, BrowserRenderTarget>> = RefCell::new(HashMap::new());
static SCRIPT_RENDER_TARGETS: Cell<ScriptRenderTargets> = Cell::new(wrap_script_render_target as ScriptRenderTargets);
static SCRIPT_UNREGISTER: Cell<fn(i32)> = Cell::new(js_unregister as fn(i32));
}
fn wrap_script_render_target(id: i32) -> Option<BrowserRenderTarget> {
let js = non_null(get_js_render_target(id))?;
let type_ = JsCast::unchecked_ref::<JsRenderTarget>(&js).render_target_type();
if type_ == "webgl" {
return Some(BrowserRenderTarget::WebGl(BrowserWebGlRenderTarget::new(js)));
}
if type_ == "software" {
return Some(BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(js)));
}
panic!("{type_}");
}
pub fn get_render_target(id: i32) -> Option<BrowserRenderTarget> {
if let Some(target) = RENDER_TARGETS.with(|targets| targets.borrow().get(&id).cloned()) {
return Some(target);
}
let script = SCRIPT_RENDER_TARGETS.with(|script| script.get());
let target = script(id)?;
RENDER_TARGETS.with(|targets| targets.borrow_mut().insert(id, target.clone()));
Some(target)
}
pub fn remove_render_target(id: i32) -> bool {
RENDER_TARGETS.with(|targets| targets.borrow_mut().remove(&id)).is_some()
}
#[cfg(test)]
pub(crate) fn set_script_render_targets_for_unit_tests(script: fn(i32) -> Option<BrowserRenderTarget>) {
SCRIPT_RENDER_TARGETS.with(|current| current.set(script));
}
#[cfg(test)]
pub(crate) fn set_script_unregister_for_unit_tests(unregister: fn(i32)) {
SCRIPT_UNREGISTER.with(|current| current.set(unregister));
}
pub(crate) fn update_size(js: &JsObject, size: PixelSize) {
set_js_size(js, size.width, size.height);
}
#[cfg(test)]
mod tests {
use super::*;
ferroui_base::ferro_thread_local! {
static ASKED: Cell<u32> = const { Cell::new(0) };
}
fn asked() -> u32 {
ASKED.with(|asked| asked.get())
}
fn script(id: i32) -> Option<BrowserRenderTarget> {
ASKED.with(|asked| asked.set(asked.get() + 1));
(id == 41 || id == 42).then(|| BrowserRenderTarget::Software(BrowserSoftwareRenderTarget::new(JsObject::NULL)))
}
fn empty_script(_id: i32) -> Option<BrowserRenderTarget> {
ASKED.with(|asked| asked.set(asked.get() + 1));
None
}
fn same(a: &BrowserRenderTarget, b: &BrowserRenderTarget) -> bool {
match (a, b) {
(BrowserRenderTarget::Software(a), BrowserRenderTarget::Software(b)) => Rc::ptr_eq(a, b),
(BrowserRenderTarget::WebGl(a), BrowserRenderTarget::WebGl(b)) => Rc::ptr_eq(a, b),
_ => false,
}
}
#[test]
fn a_target_is_wrapped_once_and_kept_for_its_thread() {
set_script_render_targets_for_unit_tests(script);
let first = get_render_target(41).expect("the script has the target");
let again = get_render_target(41).expect("the table has the target");
assert!(same(&first, &again));
assert_eq!(1, asked());
assert_eq!(RENDER_TARGET_KIND_SOFTWARE, first.kind());
assert!(first.platform_graphics_context().is_none());
let other = get_render_target(42).expect("the script has the target");
assert!(!same(&first, &other));
assert_eq!(2, asked());
}
#[test]
fn a_target_the_script_does_not_have_is_asked_for_each_time() {
set_script_render_targets_for_unit_tests(script);
assert!(get_render_target(7).is_none());
assert!(get_render_target(7).is_none());
assert_eq!(2, asked());
}
#[test]
fn each_thread_has_a_table_of_its_own() {
set_script_render_targets_for_unit_tests(script);
let here = get_render_target(41).expect("the script has the target");
let there = std::thread::spawn(|| {
set_script_render_targets_for_unit_tests(empty_script);
let found = get_render_target(41).is_some();
(found, asked())
})
.join()
.unwrap();
assert_eq!((false, 1), there);
let wrapped = std::thread::spawn(|| {
set_script_render_targets_for_unit_tests(script);
get_render_target(41).is_some() && get_render_target(41).is_some() && asked() == 1
})
.join()
.unwrap();
assert!(wrapped);
assert!(same(&here, &get_render_target(41).expect("the table has the target")));
assert_eq!(1, asked());
}
#[test]
fn a_removed_target_is_asked_for_again() {
set_script_render_targets_for_unit_tests(script);
let first = get_render_target(42).expect("the script has the target");
assert!(remove_render_target(42));
assert!(!remove_render_target(42));
let second = get_render_target(42).expect("the script has the target");
assert!(!same(&first, &second));
assert_eq!(2, asked());
}
}