Skip to main content

euv_core/renderer/
fn.rs

1use crate::*;
2
3/// Converts a web_sys event into a euv event.
4///
5/// # Arguments
6///
7/// - `&Event` - The raw browser event.
8/// - `&str` - The name of the event for dispatching to the correct variant.
9///
10/// # Returns
11///
12/// - `NativeEvent` - The corresponding euv event variant.
13pub(crate) fn convert_web_event(event: &Event, event_name: &str) -> NativeEvent {
14    match event_name {
15        "click" | "mousedown" | "mouseup" | "mousemove" | "mouseenter" | "mouseleave"
16        | "mouseover" | "mouseout" | "dblclick" | "contextmenu" => {
17            if let Some(mouse_event) = event.dyn_ref::<MouseEvent>() {
18                NativeEvent::Mouse(NativeMouseEvent {
19                    client_x: mouse_event.client_x(),
20                    client_y: mouse_event.client_y(),
21                    screen_x: mouse_event.screen_x(),
22                    screen_y: mouse_event.screen_y(),
23                    button: mouse_event.button(),
24                    buttons: mouse_event.buttons(),
25                    ctrl_key: mouse_event.ctrl_key(),
26                    shift_key: mouse_event.shift_key(),
27                    alt_key: mouse_event.alt_key(),
28                    meta_key: mouse_event.meta_key(),
29                })
30            } else {
31                NativeEvent::Generic
32            }
33        }
34        "input" => {
35            if let Some(input_event) = event.dyn_ref::<InputEvent>() {
36                let value: String = get_input_value(event);
37                NativeEvent::Input(NativeInputEvent::new(value, input_event.input_type()))
38            } else {
39                NativeEvent::Input(NativeInputEvent::new(get_input_value(event), String::new()))
40            }
41        }
42        "keydown" | "keyup" | "keypress" => {
43            if let Some(key_event) = event.dyn_ref::<KeyboardEvent>() {
44                NativeEvent::Keyboard(NativeKeyboardEvent {
45                    key: key_event.key(),
46                    code: key_event.code(),
47                    location: key_event.location(),
48                    ctrl_key: key_event.ctrl_key(),
49                    shift_key: key_event.shift_key(),
50                    alt_key: key_event.alt_key(),
51                    meta_key: key_event.meta_key(),
52                    repeat: key_event.repeat(),
53                })
54            } else {
55                NativeEvent::Generic
56            }
57        }
58        "focus" | "blur" | "focusin" | "focusout" => {
59            let is_focus: bool = event_name == "focus" || event_name == "focusin";
60            NativeEvent::Focus(NativeFocusEvent::new(is_focus, !is_focus))
61        }
62        "submit" => {
63            if let Some(submit_event) = event.dyn_ref::<SubmitEvent>() {
64                let submitter: Option<String> = submit_event
65                    .submitter()
66                    .and_then(|s| s.dyn_into::<HtmlElement>().ok())
67                    .map(|el| el.id());
68                NativeEvent::Submit(NativeSubmitEvent::new(submitter))
69            } else {
70                NativeEvent::Generic
71            }
72        }
73        "change" => {
74            let (value, checked) = get_change_value(event);
75            NativeEvent::Change(NativeChangeEvent::new(value, checked))
76        }
77        "drag" | "dragstart" | "dragend" | "dragover" | "dragenter" | "dragleave" | "drop" => {
78            if let Some(drag_event) = event.dyn_ref::<DragEvent>() {
79                let types: Vec<String> = drag_event
80                    .data_transfer()
81                    .map(|dt| {
82                        let len: u32 = dt.types().length();
83                        (0..len)
84                            .filter_map(|i: u32| dt.types().get(i).as_string())
85                            .collect()
86                    })
87                    .unwrap_or_default();
88                NativeEvent::Drag(NativeDragEvent::new(
89                    drag_event.client_x(),
90                    drag_event.client_y(),
91                    types,
92                ))
93            } else {
94                NativeEvent::Generic
95            }
96        }
97        "touchstart" | "touchend" | "touchmove" | "touchcancel" => {
98            if let Some(touch_event) = event.dyn_ref::<TouchEvent>() {
99                let touches: TouchList = touch_event.touches();
100                let first: Option<Touch> = touches.get(0);
101                NativeEvent::Touch(NativeTouchEvent::new(
102                    touches.length(),
103                    first.as_ref().map(|t| t.client_x()).unwrap_or(0),
104                    first.as_ref().map(|t| t.client_y()).unwrap_or(0),
105                ))
106            } else {
107                NativeEvent::Generic
108            }
109        }
110        "wheel" => {
111            if let Some(wheel_event) = event.dyn_ref::<WheelEvent>() {
112                NativeEvent::Wheel(NativeWheelEvent::new(
113                    wheel_event.delta_x(),
114                    wheel_event.delta_y(),
115                    wheel_event.delta_mode(),
116                ))
117            } else {
118                NativeEvent::Generic
119            }
120        }
121        "copy" | "cut" | "paste" => {
122            if let Some(clipboard_event) = event.dyn_ref::<ClipboardEvent>() {
123                let data: Option<String> = clipboard_event
124                    .clipboard_data()
125                    .and_then(|cd| cd.get_data("text").ok());
126                NativeEvent::Clipboard(NativeClipboardEvent::new(data))
127            } else {
128                NativeEvent::Generic
129            }
130        }
131        "play" | "pause" | "ended" | "loadeddata" | "canplay" | "volumechange" | "timeupdate" => {
132            NativeEvent::Media(NativeMediaEvent::new(event_name.to_string()))
133        }
134        _ => NativeEvent::Generic,
135    }
136}
137
138/// Extracts the value from an input-like event target.
139///
140/// # Arguments
141///
142/// - `Event` - The event containing the target element.
143///
144/// # Returns
145///
146/// - `String` - The current value of the input, textarea, or select element.
147fn get_input_value(event: &Event) -> String {
148    if let Some(target) = event.target() {
149        if let Ok(input) = target.clone().dyn_into::<HtmlInputElement>() {
150            return input.value();
151        }
152        if let Ok(textarea) = target.clone().dyn_into::<HtmlTextAreaElement>() {
153            return textarea.value();
154        }
155        if let Ok(select) = target.clone().dyn_into::<HtmlSelectElement>() {
156            return select.value();
157        }
158    }
159    String::new()
160}
161
162/// Extracts value and checked state from a change event target.
163///
164/// # Arguments
165///
166/// - `&Event` - The change event containing the target element.
167///
168/// # Returns
169///
170/// - `(String, bool)` - A tuple of the element value and its checked state.
171fn get_change_value(event: &Event) -> (String, bool) {
172    if let Some(target) = event.target() {
173        if let Ok(input) = target.clone().dyn_into::<HtmlInputElement>() {
174            return (input.value(), input.checked());
175        }
176        if let Ok(textarea) = target.clone().dyn_into::<HtmlTextAreaElement>() {
177            return (textarea.value(), false);
178        }
179        if let Ok(select) = target.clone().dyn_into::<HtmlSelectElement>() {
180            return (select.value(), false);
181        }
182    }
183    (String::new(), false)
184}
185
186/// Mounts the given virtual DOM tree to the document body.
187///
188/// # Arguments
189///
190/// - `FnOnce() -> VirtualNode + 'static` - A closure that returns the virtual DOM tree to render.
191///
192/// # Panics
193///
194/// Panics if the document body cannot be found.
195pub fn mount_body<F>(render_fn: F)
196where
197    F: FnOnce() -> VirtualNode,
198{
199    mount("body", render_fn);
200}
201
202/// Mounts the given virtual DOM tree to a specific element matched by a CSS selector.
203///
204/// Supported selector syntax:
205/// - `"#id"` — select by element ID
206/// - `".class"` — select by class name (uses the first match)
207/// - `"tag"` — select by tag name (uses the first match)
208///
209/// # Arguments
210///
211/// - `&str` - A CSS selector string to locate the target element.
212/// - `FnOnce() -> VirtualNode + 'static` - A closure that returns the virtual DOM tree to render.
213///
214/// # Panics
215///
216/// Panics if no global `window` or `document` exists, or if the selector does not match any element.
217pub fn mount<F>(selector: &str, render_fn: F)
218where
219    F: FnOnce() -> VirtualNode,
220{
221    let window: Window = web_sys::window().expect("no global window exists");
222    let document: Document = window.document().expect("should have a document");
223    let target: Element = if selector == "body" {
224        document.body().expect("document should have a body").into()
225    } else if let Some(id) = selector.strip_prefix('#') {
226        document
227            .get_element_by_id(id)
228            .unwrap_or_else(|| panic!("no element found with id '{}'", id))
229    } else if let Some(class) = selector.strip_prefix('.') {
230        document
231            .get_elements_by_class_name(class)
232            .item(0)
233            .unwrap_or_else(|| panic!("no element found with class '{}'", class))
234    } else {
235        document
236            .get_elements_by_tag_name(selector)
237            .item(0)
238            .unwrap_or_else(|| panic!("no element found with tag '{}'", selector))
239    };
240    let mut renderer: Renderer = Renderer::new(target);
241    let vnode: VirtualNode = render_fn();
242    renderer.render(vnode);
243}
244
245/// Returns a mutable reference to the global handler registry.
246///
247/// Lazily initializes the registry on first access via `Box::leak`.
248/// The allocated memory lives for the remainder of the program.
249///
250/// # Returns
251///
252/// - `&'static mut HashMap<(usize, String), HandlerEntry>` - A mutable reference to the global handler registry.
253///
254/// # Panics
255///
256/// Panics if the registry pointer is invalid after lazy initialization.
257pub(crate) fn get_handler_registry() -> &'static mut HashMap<(usize, String), HandlerEntry> {
258    unsafe {
259        if HANDLER_REGISTRY.is_null() {
260            let registry: Box<HashMap<(usize, String), HandlerEntry>> = Box::default();
261            HANDLER_REGISTRY = Box::leak(registry) as *mut HashMap<(usize, String), HandlerEntry>;
262        }
263        &mut *HANDLER_REGISTRY
264    }
265}
266
267/// Returns a mutable reference to the global DynamicNode listener registry.
268///
269/// Lazily initializes the registry on first access via `Box::leak`.
270/// Maps `data-euv-dynamic-id` values to the `JsValue` reference of the
271/// corresponding `__euv_signal_update__` event listener closure.
272///
273/// # Returns
274///
275/// - `&'static mut HashMap<usize, JsValue>` - A mutable reference to the global DynamicNode listener registry.
276#[cfg(target_arch = "wasm32")]
277pub(crate) fn get_dynamic_listener_registry() -> &'static mut HashMap<usize, JsValue> {
278    unsafe {
279        if DYNAMIC_LISTENER_REGISTRY.is_null() {
280            let registry: Box<HashMap<usize, JsValue>> = Box::default();
281            DYNAMIC_LISTENER_REGISTRY = Box::leak(registry) as *mut HashMap<usize, JsValue>;
282        }
283        &mut *DYNAMIC_LISTENER_REGISTRY
284    }
285}
286
287/// Returns a mutable reference to the global attribute signal listener registry.
288///
289/// Lazily initializes the registry on first access via `Box::leak`.
290/// Maps `Signal<String>` inner pointer addresses to the `JsValue` reference
291/// of the corresponding `__euv_signal_update__` event listener closure.
292///
293/// # Returns
294///
295/// - `&'static mut HashMap<usize, JsValue>` - A mutable reference to the global attribute signal listener registry.
296#[cfg(target_arch = "wasm32")]
297pub(crate) fn get_attr_signal_listener_registry() -> &'static mut HashMap<usize, JsValue> {
298    unsafe {
299        if ATTR_SIGNAL_LISTENER_REGISTRY.is_null() {
300            let registry: Box<HashMap<usize, JsValue>> = Box::default();
301            ATTR_SIGNAL_LISTENER_REGISTRY = Box::leak(registry) as *mut HashMap<usize, JsValue>;
302        }
303        &mut *ATTR_SIGNAL_LISTENER_REGISTRY
304    }
305}
306
307/// Registers a `__euv_signal_update__` event listener for a DynamicNode placeholder.
308///
309/// If a previous listener was registered for the same `dynamic_id`, it is removed
310/// from the window before the new one is added. This prevents listener accumulation
311/// when match arms switch during route changes.
312///
313/// # Arguments
314///
315/// - `usize` - The `data-euv-dynamic-id` value identifying the placeholder element.
316/// - `Closure<dyn FnMut()>` - The re-render closure to register as the event listener.
317#[cfg(target_arch = "wasm32")]
318pub(crate) fn register_dynamic_listener(dynamic_id: usize, closure: Closure<dyn FnMut()>) {
319    let event_name: String = NativeEventName::EuvSignalUpdate.to_string();
320    let registry: &mut HashMap<usize, JsValue> = get_dynamic_listener_registry();
321    if let Some(old_js_value) = registry.remove(&dynamic_id) {
322        let window: Window = window().unwrap();
323        let _ =
324            window.remove_event_listener_with_callback(&event_name, old_js_value.unchecked_ref());
325    }
326    let js_value: JsValue = closure.as_ref().clone();
327    let window: Window = window().unwrap();
328    window
329        .add_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref())
330        .unwrap();
331    closure.forget();
332    registry.insert(dynamic_id, js_value);
333}
334
335/// Registers a `__euv_signal_update__` event listener for an attribute signal.
336///
337/// If a previous listener was registered for the same signal pointer, it is removed
338/// from the window before the new one is added. This prevents listener accumulation
339/// when the same signal slot is reused across match arm switches.
340///
341/// # Arguments
342///
343/// - `usize` - The inner pointer address of the `Signal<String>`.
344/// - `Closure<dyn FnMut()>` - The attribute recomputation closure to register.
345#[cfg(target_arch = "wasm32")]
346pub(crate) fn register_attr_signal_listener(signal_key: usize, closure: Closure<dyn FnMut()>) {
347    let event_name: String = NativeEventName::EuvSignalUpdate.to_string();
348    let registry: &mut HashMap<usize, JsValue> = get_attr_signal_listener_registry();
349    if let Some(old_js_value) = registry.remove(&signal_key) {
350        let window: Window = window().unwrap();
351        let _ =
352            window.remove_event_listener_with_callback(&event_name, old_js_value.unchecked_ref());
353    }
354    let js_value: JsValue = closure.as_ref().clone();
355    let window: Window = window().unwrap();
356    window
357        .add_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref())
358        .unwrap();
359    closure.forget();
360    registry.insert(signal_key, js_value);
361}
362
363#[cfg(not(target_arch = "wasm32"))]
364pub(crate) fn register_dynamic_listener(_dynamic_id: usize, _closure: Closure<dyn FnMut()>) {}
365
366#[cfg(not(target_arch = "wasm32"))]
367#[allow(dead_code)]
368pub(crate) fn register_attr_signal_listener(_signal_key: usize, _closure: Closure<dyn FnMut()>) {}