windjammer_ui/
app.rs

1//! App runtime for mounting and running Windjammer UI applications
2//!
3//! This module provides the `App` struct and runtime system for:
4//! - Mounting UI components to the DOM (WASM)
5//! - Running the reactive system
6//! - Handling the render loop
7//! - Managing application lifecycle
8
9use crate::simple_vnode::VNode;
10
11#[cfg(target_arch = "wasm32")]
12use std::cell::RefCell;
13#[cfg(target_arch = "wasm32")]
14use std::rc::Rc;
15#[cfg(target_arch = "wasm32")]
16use wasm_bindgen::prelude::*;
17#[cfg(target_arch = "wasm32")]
18use web_sys::{window, Element};
19
20#[cfg(target_arch = "wasm32")]
21thread_local! {
22    /// Global app state for re-rendering
23    static APP_STATE: RefCell<Option<AppState>> = RefCell::new(None);
24}
25
26#[cfg(target_arch = "wasm32")]
27struct AppState {
28    render_fn: Rc<dyn Fn() -> VNode>,
29    root_element: Element,
30    document: web_sys::Document,
31}
32
33/// Application runtime
34pub struct App {
35    /// Application title
36    pub title: String,
37    /// Root UI component (static VNode for now)
38    pub root: VNode,
39    /// Optional render function for reactive apps
40    pub render_fn: Option<Box<dyn Fn() -> VNode>>,
41}
42
43impl App {
44    /// Create a new application with a static VNode
45    pub fn new(title: impl Into<String>, root: VNode) -> Self {
46        Self {
47            title: title.into(),
48            root,
49            render_fn: None,
50        }
51    }
52
53    /// Create a new application with a render function (reactive)
54    pub fn new_reactive<F>(title: impl Into<String>, render_fn: F) -> Self
55    where
56        F: Fn() -> VNode + 'static,
57    {
58        let initial_vnode = render_fn();
59        Self {
60            title: title.into(),
61            root: initial_vnode,
62            render_fn: Some(Box::new(render_fn)),
63        }
64    }
65
66    /// Run the application (WASM only)
67    #[cfg(target_arch = "wasm32")]
68    pub fn run(self) {
69        use crate::simple_renderer;
70        use wasm_bindgen::JsCast;
71
72        // Get the root element from the DOM
73        let window = web_sys::window().expect("No window found");
74        let document = window.document().expect("No document found");
75        let root_el = document
76            .get_element_by_id("app")
77            .expect("No #app element found in HTML")
78            .dyn_into::<web_sys::HtmlElement>()
79            .expect("Root is not an HTMLElement");
80
81        // Render the initial UI
82        let render_fn = self.render_fn;
83        let mut current_vnode = self.root;
84
85        // Simple initial render
86        let html = simple_renderer::render_to_html(&current_vnode);
87        root_el.set_inner_html(&html);
88
89        web_sys::console::log_1(&format!("✅ {} mounted", self.title).into());
90
91        // TODO: Add reactive re-rendering support for WASM
92        // For now, just renders the initial state
93    }
94
95    /// Run the application (Desktop with egui/eframe)
96    #[cfg(all(not(target_arch = "wasm32"), feature = "desktop"))]
97    pub fn run(self) {
98        use crate::desktop_renderer::DesktopRenderer;
99
100        let title = self.title;
101        let render_fn = self.render_fn;
102        let mut current_vnode = self.root;
103
104        let options = eframe::NativeOptions {
105            viewport: eframe::egui::ViewportBuilder::default()
106                .with_inner_size([800.0, 600.0])
107                .with_title(title.clone()),
108            ..Default::default()
109        };
110
111        let _ = eframe::run_simple_native(&title, options, move |ctx, _frame| {
112            // Set up repaint callback for reactive updates
113            let ctx_clone = ctx.clone();
114            crate::desktop_app_context::set_repaint_callback(move || {
115                ctx_clone.request_repaint();
116            });
117
118            // Re-generate VNode if we have a render function (reactive mode)
119            if let Some(ref render) = render_fn {
120                current_vnode = render();
121            }
122
123            // Render the UI
124            let mut renderer = DesktopRenderer::new();
125            renderer.render(ctx, &current_vnode);
126        });
127
128        // Cleanup
129        crate::desktop_app_context::clear_repaint_callback();
130    }
131
132    /// Run the application (Non-desktop, non-WASM - error)
133    #[cfg(all(not(target_arch = "wasm32"), not(feature = "desktop")))]
134    pub fn run(self) {
135        eprintln!("❌ Error: App::run() requires either:");
136        eprintln!("   - WASM target (for browser)");
137        eprintln!("   - 'desktop' feature (for native)");
138        panic!("Cannot run app without a supported platform");
139    }
140
141    /// Internal run method that returns Result
142    #[cfg(target_arch = "wasm32")]
143    fn run_internal(self) -> Result<(), JsValue> {
144        // Set up panic hook for better error messages
145        console_error_panic_hook::set_once();
146
147        web_sys::console::log_1(&"🔧 Starting App::run_internal".into());
148
149        // Get the window and document
150        let window = window().ok_or("No window found")?;
151        let document = window.document().ok_or("No document found")?;
152
153        web_sys::console::log_1(&"✓ Got window and document".into());
154
155        // Set the document title
156        document.set_title(&self.title);
157
158        // Get or create the root element
159        let root_element = document
160            .get_element_by_id("app")
161            .or_else(|| document.body().map(|b| b.into()))
162            .ok_or("No root element found")?;
163
164        web_sys::console::log_1(&"✓ Got root element".into());
165
166        // Clear existing content
167        root_element.set_inner_html("");
168
169        web_sys::console::log_1(&"✓ Cleared root element".into());
170
171        // Render the root VNode
172        web_sys::console::log_1(&"🎨 Rendering VNode...".into());
173        let rendered = self.root.render(&document)?;
174
175        web_sys::console::log_1(&"✓ VNode rendered".into());
176
177        root_element.append_child(&rendered)?;
178
179        web_sys::console::log_1(&"✅ UI mounted successfully!".into());
180
181        Ok(())
182    }
183}
184
185/// Mount a component to the DOM (WASM only)
186#[cfg(target_arch = "wasm32")]
187pub fn mount(root: VNode) {
188    App::new("Windjammer App", root).run()
189}
190
191/// Mount a component with a custom title (WASM only)
192#[cfg(target_arch = "wasm32")]
193pub fn mount_with_title(title: impl Into<String>, root: VNode) {
194    App::new(title, root).run()
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn test_app_creation() {
203        let app = App::new("Test App", VNode::Text("Hello".to_string()));
204        assert_eq!(app.title, "Test App");
205    }
206}