com_croftsoft_lib_animation/web_sys/
mod.rs

1// =============================================================================
2//! - web-sys functions for the CroftSoft Animation Library
3//!
4//! # Metadata
5//! - Copyright: © 2023 [`CroftSoft Inc`]
6//! - Author: [`David Wallace Croft`]
7//! - Created: 2023-03-07
8//! - Updated: 2023-03-07
9//!
10//! [`CroftSoft Inc`]: https://www.croftsoft.com/
11//! [`David Wallace Croft`]: https://www.croftsoft.com/people/david/
12// =============================================================================
13
14// TODO: see https://github.com/rustwasm/gloo
15
16use anyhow::{anyhow, Result};
17use futures::channel::mpsc::{unbounded, UnboundedReceiver};
18use js_sys::Function;
19use std::{cell::RefCell, rc::Rc};
20use wasm_bindgen::prelude::Closure;
21use wasm_bindgen::{JsCast, JsValue};
22use web_sys::{
23  console, window, Document, DomRect, Element, Event, EventTarget,
24  HtmlCanvasElement, HtmlElement, MouseEvent, Window,
25};
26
27type LoopClosure = Closure<dyn FnMut(f64)>;
28
29// TODO: Move this to another crate and pull it back in as a dependency
30pub trait LoopUpdater {
31  fn update_loop(
32    &mut self,
33    update_time: f64,
34  );
35}
36
37pub fn add_change_handler(elem: HtmlElement) -> UnboundedReceiver<Event> {
38  let (mut change_sender, change_receiver) = unbounded();
39  let event_closure = move |event: Event| {
40    let _result: Result<(), futures::channel::mpsc::SendError> =
41      change_sender.start_send(event);
42  };
43  let event_closure_box: Box<dyn FnMut(Event)> = Box::new(event_closure);
44  let on_change_closure: Closure<dyn FnMut(Event)> =
45    Closure::wrap(event_closure_box);
46  let closure_as_js_value_ref: &JsValue = on_change_closure.as_ref();
47  let js_function_ref: &Function = closure_as_js_value_ref.unchecked_ref();
48  let js_function_ref_option: Option<&Function> = Some(js_function_ref);
49  elem.set_onchange(js_function_ref_option);
50  on_change_closure.forget();
51  change_receiver
52}
53
54pub fn add_change_handler_by_id(id: &str) -> Option<UnboundedReceiver<Event>> {
55  let html_element = get_html_element_by_id(id);
56  // TODO: return None if fails
57  Some(add_change_handler(html_element))
58}
59
60pub fn add_click_handler(elem: HtmlElement) -> UnboundedReceiver<()> {
61  let (mut click_sender, click_receiver) = unbounded();
62  let on_click = Closure::wrap(Box::new(move || {
63    let _result: Result<(), futures::channel::mpsc::SendError> =
64      click_sender.start_send(());
65  }) as Box<dyn FnMut()>);
66  elem.set_onclick(Some(on_click.as_ref().unchecked_ref()));
67  on_click.forget();
68  click_receiver
69}
70
71pub fn add_click_handler_by_id(id: &str) -> Option<UnboundedReceiver<()>> {
72  let html_element = get_html_element_by_id(id);
73  // TODO: return None if fails
74  Some(add_click_handler(html_element))
75}
76
77pub fn add_mouse_down_handler(
78  elem: HtmlElement
79) -> UnboundedReceiver<MouseEvent> {
80  let (mut mouse_down_sender, mouse_down_receiver) = unbounded();
81  let mouse_event_closure = move |mouse_event: MouseEvent| {
82    let _result: Result<(), futures::channel::mpsc::SendError> =
83      mouse_down_sender.start_send(mouse_event);
84  };
85  let mouse_event_closure_box: Box<dyn FnMut(MouseEvent)> =
86    Box::new(mouse_event_closure);
87  let on_mouse_down_closure: Closure<dyn FnMut(MouseEvent)> =
88    Closure::wrap(mouse_event_closure_box);
89  let closure_as_js_value_ref: &JsValue = on_mouse_down_closure.as_ref();
90  let js_function_ref: &Function = closure_as_js_value_ref.unchecked_ref();
91  let js_function_ref_option: Option<&Function> = Some(js_function_ref);
92  elem.set_onmousedown(js_function_ref_option);
93  on_mouse_down_closure.forget();
94  mouse_down_receiver
95}
96
97pub fn add_mouse_down_handler_by_id(
98  id: &str
99) -> Option<UnboundedReceiver<MouseEvent>> {
100  let html_element = get_html_element_by_id(id);
101  // TODO: return None if fails
102  Some(add_mouse_down_handler(html_element))
103}
104
105pub fn get_canvas_xy(mouse_event: &MouseEvent) -> (usize, usize) {
106  let client_x: f64 = mouse_event.client_x() as f64;
107  let client_y: f64 = mouse_event.client_y() as f64;
108  let event_target: EventTarget = mouse_event.target().unwrap();
109  let html_canvas_element: HtmlCanvasElement = event_target.dyn_into().unwrap();
110  let dom_rect: DomRect = html_canvas_element.get_bounding_client_rect();
111  let scale_x = html_canvas_element.width() as f64 / dom_rect.width();
112  let scale_y = html_canvas_element.height() as f64 / dom_rect.height();
113  let canvas_x: usize = ((client_x - dom_rect.left()) * scale_x) as usize;
114  let canvas_y: usize = ((client_y - dom_rect.top()) * scale_y) as usize;
115  (canvas_x, canvas_y)
116}
117
118pub fn get_html_canvas_element_by_id(
119  canvas_element_id: &str
120) -> HtmlCanvasElement {
121  let document: Document = window().unwrap().document().unwrap();
122  let element: Element = document.get_element_by_id(canvas_element_id).unwrap();
123  element.dyn_into().unwrap()
124}
125
126pub fn get_html_element_by_id(id: &str) -> HtmlElement {
127  let document: Document = window().unwrap().document().unwrap();
128  let element: Element = document.get_element_by_id(id).unwrap();
129  element.dyn_into().unwrap()
130}
131
132pub fn get_window() -> Result<Window> {
133  web_sys::window().ok_or_else(|| anyhow!("No Window Found"))
134}
135
136pub fn log(message: &str) {
137  console::log_1(&JsValue::from_str(message));
138}
139
140pub fn request_animation_frame(
141  callback: &Closure<dyn FnMut(f64)>
142) -> Result<i32> {
143  get_window()?
144    .request_animation_frame(callback.as_ref().unchecked_ref())
145    .map_err(|err| anyhow!("Cannot request animation frame {:#?}", err))
146}
147
148pub fn spawn_local_loop<L: LoopUpdater + 'static>(loop_updater: L) {
149  wasm_bindgen_futures::spawn_local(async move {
150    start_looping(loop_updater).await.expect("loop start failed");
151  });
152}
153
154pub async fn start_looping<L: LoopUpdater + 'static>(
155  mut loop_updater: L
156) -> Result<()> {
157  let f: Rc<RefCell<Option<LoopClosure>>> = Rc::new(RefCell::new(None));
158  let g = f.clone();
159  *g.borrow_mut() = Some(Closure::wrap(Box::new(move |update_time: f64| {
160    loop_updater.update_loop(update_time);
161    let _result: Result<i32, anyhow::Error> =
162      request_animation_frame(f.borrow().as_ref().unwrap());
163  })));
164  request_animation_frame(
165    g.borrow().as_ref().ok_or_else(|| anyhow!("loop failed"))?,
166  )?;
167  Ok(())
168}