1use crate::component::Component;
4
5#[cfg(not(feature = "web"))]
6use crate::platform::create_platform;
7
8#[cfg(feature = "web")]
10pub fn mount<C: Component>(selector: &str, component: C) -> Result<(), String> {
11 #[cfg(target_arch = "wasm32")]
12 {
13 let window = web_sys::window().ok_or("No window found")?;
15 let document = window.document().ok_or("No document found")?;
16
17 let target = document
19 .query_selector(selector)
20 .map_err(|_| format!("Invalid selector: {}", selector))?
21 .ok_or(format!("Element not found: {}", selector))?;
22
23 let vnode = component.render();
25
26 let renderer = WebRenderer::new();
28
29 let dom_node = renderer.create_element(&vnode)?;
31
32 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#[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 Ok(())
65}
66
67#[cfg(not(target_arch = "wasm32"))]
69pub trait Renderer: Send + Sync {
70 fn init(&mut self) -> Result<(), String>;
72
73 fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String>;
75
76 fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String>;
78}
79
80#[cfg(target_arch = "wasm32")]
82pub trait Renderer {
83 fn init(&mut self) -> Result<(), String>;
85
86 fn render(&mut self, vnode: &crate::vdom::VNode) -> Result<(), String>;
88
89 fn patch(&mut self, patches: &[crate::vdom::Patch]) -> Result<(), String>;
91}
92
93#[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 for (key, value) in &element.attrs {
142 if key.starts_with("on") {
144 let event_type = key.strip_prefix("on").unwrap_or(key);
145
146 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 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 callback.forget();
169 } else {
170 dom_element
172 .set_attribute(key, value)
173 .map_err(|_| format!("Failed to set attribute: {}", key))?;
174 }
175 }
176
177 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 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 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 while let Some(child) = root.first_child() {
250 root.remove_child(&child)
251 .map_err(|_| "Failed to remove child")?;
252 }
253
254 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 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 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 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 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 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#[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 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#[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 for (key, value) in &element.attrs {
410 html.push_str(&format!(" {}=\"{}\"", key, value));
411 }
412
413 html.push('>');
414
415 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 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 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 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 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#[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 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 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 Ok(())
622 }
623
624 #[cfg(feature = "mobile-android")]
625 fn create_android_views(&self) -> Result<(), String> {
626 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 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 self.view_hierarchy.clear();
657
658 self.root_view = self.vnode_to_native_view(vnode);
660
661 #[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 for patch in patches {
676 match patch {
677 Patch::Replace { .. } => {
678 }
680 Patch::UpdateText { .. } => {
681 }
683 Patch::SetAttribute { .. } => {
684 }
686 Patch::Append { .. } => {
687 }
689 Patch::Remove { .. } => {
690 }
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}