com_croftsoft_lib_animation/web_sys/
mod.rs

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