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 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}