windjammer_ui/
component_runtime.rs

1//! Component runtime for direct DOM manipulation
2//!
3//! This module provides the runtime support for compiled Windjammer UI components.
4//! Unlike traditional virtual DOM approaches, we generate direct DOM manipulation code
5//! for maximum performance and transparency.
6
7#[cfg(target_arch = "wasm32")]
8use wasm_bindgen::prelude::*;
9#[cfg(target_arch = "wasm32")]
10use web_sys::{Document, Element, Event, HtmlElement, Window};
11
12/// Get the browser window
13#[cfg(target_arch = "wasm32")]
14pub fn window() -> Result<Window, JsValue> {
15    web_sys::window().ok_or_else(|| JsValue::from_str("No window found"))
16}
17
18/// Get the document
19#[cfg(target_arch = "wasm32")]
20pub fn document() -> Result<Document, JsValue> {
21    window()?
22        .document()
23        .ok_or_else(|| JsValue::from_str("No document found"))
24}
25
26/// Create an element with a tag name
27#[cfg(target_arch = "wasm32")]
28pub fn create_element(tag: &str) -> Result<Element, JsValue> {
29    document()?.create_element(tag)
30}
31
32/// Set text content on an element
33#[cfg(target_arch = "wasm32")]
34pub fn set_text(element: &Element, text: &str) {
35    element.set_text_content(Some(text));
36}
37
38/// Set an attribute on an element
39#[cfg(target_arch = "wasm32")]
40pub fn set_attribute(element: &Element, name: &str, value: &str) -> Result<(), JsValue> {
41    element.set_attribute(name, value)
42}
43
44/// Add a class to an element
45#[cfg(target_arch = "wasm32")]
46pub fn add_class(element: &Element, class: &str) -> Result<(), JsValue> {
47    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
48        html_element.class_list().add_1(class)?;
49    }
50    Ok(())
51}
52
53/// Remove a class from an element
54#[cfg(target_arch = "wasm32")]
55pub fn remove_class(element: &Element, class: &str) -> Result<(), JsValue> {
56    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
57        html_element.class_list().remove_1(class)?;
58    }
59    Ok(())
60}
61
62/// Append a child element
63#[cfg(target_arch = "wasm32")]
64pub fn append_child(parent: &Element, child: &Element) -> Result<(), JsValue> {
65    parent.append_child(child)?;
66    Ok(())
67}
68
69/// Get element by ID
70#[cfg(target_arch = "wasm32")]
71pub fn get_element_by_id(id: &str) -> Result<Element, JsValue> {
72    document()?
73        .get_element_by_id(id)
74        .ok_or_else(|| JsValue::from_str(&format!("Element with id '{}' not found", id)))
75}
76
77/// Event handler type
78#[cfg(target_arch = "wasm32")]
79pub type EventCallback = Box<dyn FnMut(Event)>;
80
81/// Helper to create an event listener closure
82#[cfg(target_arch = "wasm32")]
83pub fn create_event_listener<F>(mut handler: F) -> Closure<dyn FnMut(Event)>
84where
85    F: FnMut(Event) + 'static,
86{
87    Closure::wrap(Box::new(move |event: Event| {
88        handler(event);
89    }) as Box<dyn FnMut(Event)>)
90}
91
92/// Add an event listener to an element
93#[cfg(target_arch = "wasm32")]
94pub fn add_event_listener(
95    element: &Element,
96    event_type: &str,
97    closure: &Closure<dyn FnMut(Event)>,
98) -> Result<(), JsValue> {
99    element.add_event_listener_with_callback(event_type, closure.as_ref().unchecked_ref())
100}
101
102/// Remove an element from its parent
103#[cfg(target_arch = "wasm32")]
104pub fn remove_element(element: &Element) -> Result<(), JsValue> {
105    if let Some(parent) = element.parent_element() {
106        parent.remove_child(element)?;
107    }
108    Ok(())
109}
110
111/// Toggle a class on an element
112#[cfg(target_arch = "wasm32")]
113pub fn toggle_class(element: &Element, class: &str) -> Result<(), JsValue> {
114    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
115        html_element.class_list().toggle(class)?;
116    }
117    Ok(())
118}
119
120/// Check if element has a class
121#[cfg(target_arch = "wasm32")]
122pub fn has_class(element: &Element, class: &str) -> bool {
123    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
124        html_element.class_list().contains(class)
125    } else {
126        false
127    }
128}
129
130/// Set multiple classes at once
131#[cfg(target_arch = "wasm32")]
132pub fn set_classes(element: &Element, classes: &[&str]) -> Result<(), JsValue> {
133    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
134        html_element.set_class_name(&classes.join(" "));
135    }
136    Ok(())
137}
138
139/// Get attribute value
140#[cfg(target_arch = "wasm32")]
141pub fn get_attribute(element: &Element, name: &str) -> Option<String> {
142    element.get_attribute(name)
143}
144
145/// Remove an attribute
146#[cfg(target_arch = "wasm32")]
147pub fn remove_attribute(element: &Element, name: &str) -> Result<(), JsValue> {
148    element.remove_attribute(name)
149}
150
151/// Set inner HTML (use with caution)
152#[cfg(target_arch = "wasm32")]
153pub fn set_inner_html(element: &Element, html: &str) {
154    element.set_inner_html(html);
155}
156
157/// Get inner HTML
158#[cfg(target_arch = "wasm32")]
159pub fn get_inner_html(element: &Element) -> String {
160    element.inner_html()
161}
162
163/// Set style property
164#[cfg(target_arch = "wasm32")]
165pub fn set_style(element: &Element, property: &str, value: &str) -> Result<(), JsValue> {
166    use wasm_bindgen::JsCast;
167    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
168        // Get the style attribute and set it
169        let current_style = html_element.get_attribute("style").unwrap_or_default();
170        let new_style = if current_style.is_empty() {
171            format!("{}: {};", property, value)
172        } else {
173            format!("{}; {}: {};", current_style, property, value)
174        };
175        html_element.set_attribute("style", &new_style)?;
176    }
177    Ok(())
178}
179
180/// Get style property
181#[cfg(target_arch = "wasm32")]
182pub fn get_style(element: &Element, property: &str) -> Result<String, JsValue> {
183    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
184        // Parse the style attribute
185        if let Some(style_attr) = html_element.get_attribute("style") {
186            for part in style_attr.split(';') {
187                let parts: Vec<&str> = part.split(':').collect();
188                if parts.len() == 2 && parts[0].trim() == property {
189                    return Ok(parts[1].trim().to_string());
190                }
191            }
192        }
193    }
194    Ok(String::new())
195}
196
197/// Focus an element
198#[cfg(target_arch = "wasm32")]
199pub fn focus(element: &Element) -> Result<(), JsValue> {
200    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
201        html_element.focus()?;
202    }
203    Ok(())
204}
205
206/// Blur an element
207#[cfg(target_arch = "wasm32")]
208pub fn blur(element: &Element) -> Result<(), JsValue> {
209    if let Some(html_element) = element.dyn_ref::<HtmlElement>() {
210        html_element.blur()?;
211    }
212    Ok(())
213}
214
215/// Query selector - find first matching element
216#[cfg(target_arch = "wasm32")]
217pub fn query_selector(selector: &str) -> Result<Option<Element>, JsValue> {
218    document()?.query_selector(selector)
219}
220
221/// Query selector all - find all matching elements
222#[cfg(target_arch = "wasm32")]
223pub fn query_selector_all(selector: &str) -> Result<web_sys::NodeList, JsValue> {
224    document()?.query_selector_all(selector)
225}
226
227/// Create a text node
228#[cfg(target_arch = "wasm32")]
229pub fn create_text_node(text: &str) -> Result<web_sys::Text, JsValue> {
230    Ok(document()?.create_text_node(text))
231}
232
233/// Request animation frame
234#[cfg(target_arch = "wasm32")]
235pub fn request_animation_frame<F>(f: F) -> Result<i32, JsValue>
236where
237    F: FnMut() + 'static,
238{
239    let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
240    let handle = window()?.request_animation_frame(closure.as_ref().unchecked_ref())?;
241    closure.forget();
242    Ok(handle)
243}
244
245/// Set timeout
246#[cfg(target_arch = "wasm32")]
247pub fn set_timeout<F>(f: F, millis: i32) -> Result<i32, JsValue>
248where
249    F: FnMut() + 'static,
250{
251    let closure = Closure::wrap(Box::new(f) as Box<dyn FnMut()>);
252    let handle = window()?.set_timeout_with_callback_and_timeout_and_arguments_0(
253        closure.as_ref().unchecked_ref(),
254        millis,
255    )?;
256    closure.forget();
257    Ok(handle)
258}
259
260/// Console log helper
261#[cfg(target_arch = "wasm32")]
262pub fn console_log(message: &str) {
263    web_sys::console::log_1(&JsValue::from_str(message));
264}
265
266/// Console error helper
267#[cfg(target_arch = "wasm32")]
268pub fn console_error(message: &str) {
269    web_sys::console::error_1(&JsValue::from_str(message));
270}
271
272/// Console warn helper
273#[cfg(target_arch = "wasm32")]
274pub fn console_warn(message: &str) {
275    web_sys::console::warn_1(&JsValue::from_str(message));
276}
277
278#[cfg(not(target_arch = "wasm32"))]
279pub fn window() -> Result<(), String> {
280    Err("Window is only available on WASM target".to_string())
281}
282
283#[cfg(not(target_arch = "wasm32"))]
284pub fn document() -> Result<(), String> {
285    Err("Document is only available on WASM target".to_string())
286}