windjammer_ui/
renderer.rs

1//! Cross-platform renderer
2
3use crate::component::Component;
4
5#[cfg(not(feature = "web"))]
6use crate::platform::create_platform;
7
8/// Mount a component to the target selector
9#[cfg(feature = "web")]
10pub fn mount<C: Component>(selector: &str, component: C) -> Result<(), String> {
11    #[cfg(target_arch = "wasm32")]
12    {
13        // Get the window and document
14        let window = web_sys::window().ok_or("No window found")?;
15        let document = window.document().ok_or("No document found")?;
16
17        // Find the target element
18        let target = document
19            .query_selector(selector)
20            .map_err(|_| format!("Invalid selector: {}", selector))?
21            .ok_or(format!("Element not found: {}", selector))?;
22
23        // Render the component to a VNode
24        let vnode = component.render();
25
26        // Create a WebRenderer
27        let renderer = WebRenderer::new();
28
29        // Create the DOM element from VNode
30        let dom_node = renderer.create_element(&vnode)?;
31
32        // Clear the target and append the rendered content
33        while let Some(child) = target.first_child() {
34            target
35                .remove_child(&child)
36                .map_err(|_| "Failed to clear target")?;
37        }
38
39        target
40            .append_child(&dom_node)
41            .map_err(|_| "Failed to mount component")?;
42
43        Ok(())
44    }
45    #[cfg(not(target_arch = "wasm32"))]
46    {
47        let _ = (selector, component);
48        Err("mount() is only available on WASM target".to_string())
49    }
50}
51
52/// Mount a component to the target selector (non-web platforms)
53#[cfg(not(feature = "web"))]
54pub fn mount<C: Component>(_selector: &str, _component: C) -> Result<(), String> {
55    let mut platform = create_platform();
56    platform.init()?;
57
58    // In a full implementation, this would:
59    // 1. Render the component to a VNode tree
60    // 2. Convert VNode to platform-specific representation
61    // 3. Attach to the DOM/native view
62
63    // For now, just return success
64    Ok(())
65}
66
67/// Renderer trait for different platforms
68#[cfg(not(target_arch = "wasm32"))]
69pub trait Renderer: Send + Sync {
70    /// Initialize the renderer
71    fn init(&mut self) -> Result<(), String>;
72
73    /// Render a virtual DOM tree
74    fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String>;
75
76    /// Apply patches to the rendered tree
77    fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String>;
78}
79
80/// Renderer trait for WASM (no Send + Sync required)
81#[cfg(target_arch = "wasm32")]
82pub trait Renderer {
83    /// Initialize the renderer
84    fn init(&mut self) -> Result<(), String>;
85
86    /// Render a virtual DOM tree
87    fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String>;
88
89    /// Apply patches to the rendered tree
90    fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String>;
91}
92
93/// Web renderer (JavaScript/WASM)
94#[cfg(feature = "web")]
95pub struct WebRenderer {
96    #[cfg(target_arch = "wasm32")]
97    document: web_sys::Document,
98    #[cfg(target_arch = "wasm32")]
99    root: Option<web_sys::Element>,
100    #[cfg(not(target_arch = "wasm32"))]
101    _dummy: (),
102}
103
104#[cfg(not(feature = "web"))]
105pub struct WebRenderer {
106    initialized: bool,
107}
108
109#[cfg(feature = "web")]
110impl WebRenderer {
111    pub fn new() -> Self {
112        #[cfg(target_arch = "wasm32")]
113        {
114            let window = web_sys::window().expect("no window");
115            let document = window.document().expect("no document");
116            Self {
117                document,
118                root: None,
119            }
120        }
121        #[cfg(not(target_arch = "wasm32"))]
122        {
123            Self { _dummy: () }
124        }
125    }
126
127    #[cfg(target_arch = "wasm32")]
128    pub fn create_element(&self, vnode: &crate::vdom::VNode) -> Result<web_sys::Node, String> {
129        use crate::vdom::VNode;
130        use wasm_bindgen::closure::Closure;
131        use wasm_bindgen::JsCast;
132
133        match vnode {
134            VNode::Element(element) => {
135                let dom_element = self
136                    .document
137                    .create_element(&element.tag)
138                    .map_err(|_| format!("Failed to create element: {}", element.tag))?;
139
140                // Set attributes and event handlers
141                for (key, value) in &element.attrs {
142                    // Check if this is an event handler (starts with "on")
143                    if key.starts_with("on") {
144                        let event_type = key.strip_prefix("on").unwrap_or(key);
145
146                        // For now, we'll just log events
147                        // In a full implementation, this would dispatch to component methods
148                        let callback = Closure::wrap(Box::new(move |event: web_sys::Event| {
149                            web_sys::console::log_1(
150                                &format!("Event triggered: {}", event_type).into(),
151                            );
152                            // Prevent default behavior for some events
153                            if event_type == "submit" {
154                                event.prevent_default();
155                            }
156                        })
157                            as Box<dyn FnMut(_)>);
158
159                        dom_element
160                            .add_event_listener_with_callback(
161                                event_type,
162                                callback.as_ref().unchecked_ref(),
163                            )
164                            .map_err(|_| format!("Failed to add event listener: {}", key))?;
165
166                        // Leak the closure to keep it alive
167                        // In production, we'd store these and clean them up properly
168                        callback.forget();
169                    } else {
170                        // Regular attribute
171                        dom_element
172                            .set_attribute(key, value)
173                            .map_err(|_| format!("Failed to set attribute: {}", key))?;
174                    }
175                }
176
177                // Append children
178                for child in &element.children {
179                    let child_node = self.create_element(child)?;
180                    dom_element
181                        .append_child(&child_node)
182                        .map_err(|_| "Failed to append child".to_string())?;
183                }
184
185                Ok(dom_element.into())
186            }
187            VNode::Text(text) => {
188                let text_node = self.document.create_text_node(&text.content);
189                Ok(text_node.into())
190            }
191            VNode::Component(_) => {
192                // Components should be expanded to elements before rendering
193                Err("Cannot render component directly".to_string())
194            }
195            VNode::Empty => {
196                let text_node = self.document.create_text_node("");
197                Ok(text_node.into())
198            }
199        }
200    }
201}
202
203#[cfg(not(feature = "web"))]
204impl WebRenderer {
205    pub fn new() -> Self {
206        Self { initialized: false }
207    }
208}
209
210impl Default for WebRenderer {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216#[cfg(feature = "web")]
217impl Renderer for WebRenderer {
218    fn init(&mut self) -> Result<(), String> {
219        #[cfg(target_arch = "wasm32")]
220        {
221            // Get or create root element
222            let root = self
223                .document
224                .get_element_by_id("app")
225                .or_else(|| {
226                    let body = self.document.body()?;
227                    let div = self.document.create_element("div").ok()?;
228                    div.set_id("app");
229                    body.append_child(&div).ok()?;
230                    Some(div)
231                })
232                .ok_or("Failed to create root element")?;
233
234            self.root = Some(root);
235            Ok(())
236        }
237        #[cfg(not(target_arch = "wasm32"))]
238        {
239            Ok(())
240        }
241    }
242
243    fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String> {
244        #[cfg(target_arch = "wasm32")]
245        {
246            let root = self.root.as_ref().ok_or("Renderer not initialized")?;
247
248            // Clear existing content
249            while let Some(child) = root.first_child() {
250                root.remove_child(&child)
251                    .map_err(|_| "Failed to remove child")?;
252            }
253
254            // Render new content
255            let node = self.create_element(vnode)?;
256            root.append_child(&node)
257                .map_err(|_| "Failed to append root node")?;
258
259            Ok(())
260        }
261        #[cfg(not(target_arch = "wasm32"))]
262        {
263            let _ = vnode;
264            Ok(())
265        }
266    }
267
268    fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String> {
269        #[cfg(target_arch = "wasm32")]
270        {
271            use crate::vdom::Patch;
272            use wasm_bindgen::JsCast;
273
274            let root = self.root.as_ref().ok_or("No root element")?;
275
276            for patch in patches {
277                match patch {
278                    Patch::Replace { path, node } => {
279                        // Find the node at path and replace it
280                        let target = self.find_node_at_path(root, path)?;
281                        let new_node = self.create_element(node)?;
282
283                        if let Some(parent) = target.parent_node() {
284                            parent
285                                .replace_child(&new_node, &target)
286                                .map_err(|_| "Failed to replace node")?;
287                        }
288                    }
289                    Patch::UpdateText { path, content } => {
290                        // Update text node content
291                        let target = self.find_node_at_path(root, path)?;
292                        if let Some(text_node) = target.dyn_ref::<web_sys::Text>() {
293                            text_node.set_data(content);
294                        }
295                    }
296                    Patch::SetAttribute { path, key, value } => {
297                        // Set attribute on element
298                        let target = self.find_node_at_path(root, path)?;
299                        if let Some(element) = target.dyn_ref::<web_sys::Element>() {
300                            if value.is_empty() {
301                                element
302                                    .remove_attribute(key)
303                                    .map_err(|_| format!("Failed to remove attribute: {}", key))?;
304                            } else {
305                                element
306                                    .set_attribute(key, value)
307                                    .map_err(|_| format!("Failed to set attribute: {}", key))?;
308                            }
309                        }
310                    }
311                    Patch::Append { path, node } => {
312                        // Append child node
313                        let target = self.find_node_at_path(root, path)?;
314                        let new_node = self.create_element(node)?;
315                        target
316                            .append_child(&new_node)
317                            .map_err(|_| "Failed to append child")?;
318                    }
319                    Patch::Remove { path } => {
320                        // Remove child node
321                        let target = self.find_node_at_path(root, path)?;
322                        if let Some(parent) = target.parent_node() {
323                            parent
324                                .remove_child(&target)
325                                .map_err(|_| "Failed to remove child")?;
326                        }
327                    }
328                }
329            }
330            Ok(())
331        }
332        #[cfg(not(target_arch = "wasm32"))]
333        {
334            let _ = patches;
335            Ok(())
336        }
337    }
338}
339
340// Helper methods for WebRenderer
341#[cfg(target_arch = "wasm32")]
342impl WebRenderer {
343    fn find_node_at_path(
344        &self,
345        root: &web_sys::Element,
346        path: &[usize],
347    ) -> Result<web_sys::Node, String> {
348        let mut current: web_sys::Node = root.clone().into();
349
350        for &index in path.iter().skip(1) {
351            // Skip first index (root)
352            // Node has child_nodes() method, but we need to access it correctly
353            let children = current.child_nodes();
354            current = children
355                .get(index as u32)
356                .ok_or(format!("Child not found at index {}", index))?;
357        }
358
359        Ok(current)
360    }
361}
362
363#[cfg(not(feature = "web"))]
364impl Renderer for WebRenderer {
365    fn init(&mut self) -> Result<(), String> {
366        self.initialized = true;
367        Ok(())
368    }
369
370    fn render(&mut self, _vnode: &crate::vdom::VNode) -> Result<(), String> {
371        Ok(())
372    }
373
374    fn patch(&mut self, _patches: &[crate::vdom::Patch]) -> Result<(), String> {
375        Ok(())
376    }
377}
378
379/// Desktop renderer (Tauri)
380#[cfg(feature = "desktop")]
381pub struct DesktopRenderer {
382    html_content: String,
383    pending_updates: Vec<String>,
384}
385
386#[cfg(not(feature = "desktop"))]
387pub struct DesktopRenderer {
388    initialized: bool,
389}
390
391#[cfg(feature = "desktop")]
392impl DesktopRenderer {
393    pub fn new() -> Self {
394        Self {
395            html_content: String::new(),
396            pending_updates: Vec::new(),
397        }
398    }
399
400    #[allow(clippy::only_used_in_recursion)]
401    fn vnode_to_html(&self, vnode: &crate::vdom::VNode) -> String {
402        use crate::vdom::VNode;
403
404        match vnode {
405            VNode::Element(element) => {
406                let mut html = format!("<{}", element.tag);
407
408                // Add attributes
409                for (key, value) in &element.attrs {
410                    html.push_str(&format!(" {}=\"{}\"", key, value));
411                }
412
413                html.push('>');
414
415                // Add children
416                for child in &element.children {
417                    html.push_str(&self.vnode_to_html(child));
418                }
419
420                html.push_str(&format!("</{}>", element.tag));
421                html
422            }
423            VNode::Text(text) => text.content.clone(),
424            VNode::Component(_) => String::new(),
425            VNode::Empty => String::new(),
426        }
427    }
428
429    #[cfg(feature = "desktop")]
430    fn send_to_webview(&mut self, html: &str) -> Result<(), String> {
431        // In a full Tauri implementation, this would use:
432        // tauri::Manager::emit() to send updates to the webview
433        // For now, store for testing
434        self.html_content = html.to_string();
435        Ok(())
436    }
437}
438
439#[cfg(not(feature = "desktop"))]
440impl DesktopRenderer {
441    pub fn new() -> Self {
442        Self { initialized: false }
443    }
444}
445
446impl Default for DesktopRenderer {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452#[cfg(feature = "desktop")]
453impl Renderer for DesktopRenderer {
454    fn init(&mut self) -> Result<(), String> {
455        // Initialize Tauri webview
456        self.html_content = r#"
457            <!DOCTYPE html>
458            <html>
459            <head>
460                <meta charset="UTF-8">
461                <title>Windjammer App</title>
462                <style>
463                    body { margin: 0; padding: 0; font-family: system-ui; }
464                    #app { width: 100vw; height: 100vh; }
465                </style>
466            </head>
467            <body>
468                <div id="app"></div>
469            </body>
470            </html>
471        "#
472        .to_string();
473        Ok(())
474    }
475
476    fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String> {
477        let html = self.vnode_to_html(vnode);
478        self.send_to_webview(&html)?;
479        Ok(())
480    }
481
482    fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String> {
483        use crate::vdom::Patch;
484
485        // Convert patches to JavaScript commands
486        for patch in patches {
487            let js_command = match patch {
488                Patch::Replace { .. } => "document.getElementById('app').innerHTML = ...;",
489                Patch::UpdateText { .. } => "element.textContent = ...;",
490                Patch::SetAttribute { .. } => "element.setAttribute(...);",
491                // RemoveAttribute is not a separate variant, it's handled by SetAttribute with empty value
492                Patch::Append { .. } => "element.appendChild(...);",
493                Patch::Remove { .. } => "element.removeChild(...);",
494            };
495            self.pending_updates.push(js_command.to_string());
496        }
497
498        Ok(())
499    }
500}
501
502#[cfg(not(feature = "desktop"))]
503impl Renderer for DesktopRenderer {
504    fn init(&mut self) -> Result<(), String> {
505        self.initialized = true;
506        Ok(())
507    }
508
509    fn render(&mut self, _vnode: &crate::vdom::VNode) -> Result<(), String> {
510        Ok(())
511    }
512
513    fn patch(&mut self, _patches: &[crate::vdom::Patch]) -> Result<(), String> {
514        Ok(())
515    }
516}
517
518/// Mobile renderer (iOS/Android)
519#[cfg(any(feature = "mobile-ios", feature = "mobile-android"))]
520pub struct MobileRenderer {
521    view_hierarchy: Vec<NativeView>,
522    root_view: Option<usize>,
523}
524
525#[cfg(not(any(feature = "mobile-ios", feature = "mobile-android")))]
526pub struct MobileRenderer {
527    initialized: bool,
528}
529
530#[cfg(any(feature = "mobile-ios", feature = "mobile-android"))]
531#[derive(Debug, Clone)]
532#[allow(dead_code)]
533struct NativeView {
534    id: usize,
535    view_type: String,
536    properties: std::collections::HashMap<String, String>,
537    children: Vec<usize>,
538}
539
540#[cfg(any(feature = "mobile-ios", feature = "mobile-android"))]
541impl MobileRenderer {
542    pub fn new() -> Self {
543        Self {
544            view_hierarchy: Vec::new(),
545            root_view: None,
546        }
547    }
548
549    fn vnode_to_native_view(&mut self, vnode: &crate::vdom::VNode) -> Option<usize> {
550        use crate::vdom::VNode;
551
552        match vnode {
553            VNode::Element(element) => {
554                let view_type = self.map_html_to_native(&element.tag);
555                let id = self.view_hierarchy.len();
556
557                let mut properties = std::collections::HashMap::new();
558                for (key, value) in &element.attrs {
559                    properties.insert(key.clone(), value.clone());
560                }
561
562                let mut view = NativeView {
563                    id,
564                    view_type,
565                    properties,
566                    children: Vec::new(),
567                };
568
569                // Process children
570                for child in &element.children {
571                    if let Some(child_id) = self.vnode_to_native_view(child) {
572                        view.children.push(child_id);
573                    }
574                }
575
576                self.view_hierarchy.push(view);
577                Some(id)
578            }
579            VNode::Text(text) => {
580                let id = self.view_hierarchy.len();
581                let mut properties = std::collections::HashMap::new();
582                properties.insert("text".to_string(), text.content.clone());
583
584                let view = NativeView {
585                    id,
586                    view_type: "TextView".to_string(),
587                    properties,
588                    children: Vec::new(),
589                };
590
591                self.view_hierarchy.push(view);
592                Some(id)
593            }
594            VNode::Component(_) => None,
595            VNode::Empty => None,
596        }
597    }
598
599    fn map_html_to_native(&self, tag: &str) -> String {
600        // Map HTML tags to native view types
601        match tag {
602            "div" => "ContainerView",
603            "span" => "TextView",
604            "button" => "Button",
605            "input" => "TextField",
606            "img" => "ImageView",
607            "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => "HeaderView",
608            "p" => "TextView",
609            "a" => "LinkView",
610            "ul" | "ol" => "ListView",
611            "li" => "ListItemView",
612            _ => "View",
613        }
614        .to_string()
615    }
616
617    #[cfg(feature = "mobile-ios")]
618    fn create_uikit_views(&self) -> Result<(), String> {
619        // In a full implementation, would use objc/cocoa to create UIViews
620        // For now, just validate the hierarchy
621        Ok(())
622    }
623
624    #[cfg(feature = "mobile-android")]
625    fn create_android_views(&self) -> Result<(), String> {
626        // In a full implementation, would use jni to create Android Views
627        // For now, just validate the hierarchy
628        Ok(())
629    }
630}
631
632#[cfg(not(any(feature = "mobile-ios", feature = "mobile-android")))]
633impl MobileRenderer {
634    pub fn new() -> Self {
635        Self { initialized: false }
636    }
637}
638
639impl Default for MobileRenderer {
640    fn default() -> Self {
641        Self::new()
642    }
643}
644
645#[cfg(any(feature = "mobile-ios", feature = "mobile-android"))]
646impl Renderer for MobileRenderer {
647    fn init(&mut self) -> Result<(), String> {
648        // Initialize native view system
649        self.view_hierarchy.clear();
650        self.root_view = None;
651        Ok(())
652    }
653
654    fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String> {
655        // Clear existing views
656        self.view_hierarchy.clear();
657
658        // Build native view hierarchy
659        self.root_view = self.vnode_to_native_view(vnode);
660
661        // Create platform-specific views
662        #[cfg(feature = "mobile-ios")]
663        self.create_uikit_views()?;
664
665        #[cfg(feature = "mobile-android")]
666        self.create_android_views()?;
667
668        Ok(())
669    }
670
671    fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String> {
672        use crate::vdom::Patch;
673
674        // Apply patches to native views
675        for patch in patches {
676            match patch {
677                Patch::Replace { .. } => {
678                    // Replace native view
679                }
680                Patch::UpdateText { .. } => {
681                    // Update native text view
682                }
683                Patch::SetAttribute { .. } => {
684                    // Update view property (or remove if value is empty)
685                }
686                Patch::Append { .. } => {
687                    // Add subview
688                }
689                Patch::Remove { .. } => {
690                    // Remove subview
691                }
692            }
693        }
694
695        Ok(())
696    }
697}
698
699#[cfg(not(any(feature = "mobile-ios", feature = "mobile-android")))]
700impl Renderer for MobileRenderer {
701    fn init(&mut self) -> Result<(), String> {
702        self.initialized = true;
703        Ok(())
704    }
705
706    fn render(&mut self, _vnode: &crate::vdom::VNode) -> Result<(), String> {
707        Ok(())
708    }
709
710    fn patch(&mut self, _patches: &[crate::vdom::Patch]) -> Result<(), String> {
711        Ok(())
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_web_renderer_creation() {
721        let mut renderer = WebRenderer::new();
722        assert!(renderer.init().is_ok());
723    }
724
725    #[test]
726    fn test_desktop_renderer_creation() {
727        let mut renderer = DesktopRenderer::new();
728        assert!(renderer.init().is_ok());
729    }
730
731    #[test]
732    fn test_mobile_renderer_creation() {
733        let mut renderer = MobileRenderer::new();
734        assert!(renderer.init().is_ok());
735    }
736
737    #[test]
738    fn test_web_renderer_render() {
739        let mut renderer = WebRenderer::new();
740        renderer.init().unwrap();
741
742        use crate::vdom::{VElement, VNode, VText};
743        let vnode: VNode = VElement::new("div")
744            .child(VNode::Text(VText::new("Hello World")))
745            .into();
746
747        assert!(renderer.render(&vnode).is_ok());
748    }
749
750    #[test]
751    fn test_desktop_renderer_render() {
752        let mut renderer = DesktopRenderer::new();
753        renderer.init().unwrap();
754
755        use crate::vdom::{VElement, VNode, VText};
756        let vnode: VNode = VElement::new("div")
757            .child(VNode::Text(VText::new("Hello Desktop")))
758            .into();
759
760        assert!(renderer.render(&vnode).is_ok());
761    }
762
763    #[test]
764    fn test_mobile_renderer_render() {
765        let mut renderer = MobileRenderer::new();
766        renderer.init().unwrap();
767
768        use crate::vdom::{VElement, VNode, VText};
769        let vnode: VNode = VElement::new("div")
770            .child(VNode::Text(VText::new("Hello Mobile")))
771            .into();
772
773        assert!(renderer.render(&vnode).is_ok());
774    }
775}