kobalt_runtime/
app.rs

1//! High-level Kobalt application API
2//!
3//! Provides a declarative, Flutter-style API for building Kobalt apps
4
5use crate::desktop::DesktopApp;
6use kobalt_core::types::Color;
7use kobalt_core::widget::Widget;
8use kobalt_render::RealTextRenderer;
9use winit::event::{Event, WindowEvent};
10
11/// Kobalt application builder
12///
13/// Provides a declarative API for building apps, similar to Flutter's MaterialApp
14///
15/// # Example
16///
17/// ```no_run
18/// use kobalt_runtime::KobaltApp;
19/// use kobalt_widgets::Text;
20///
21/// KobaltApp::build()
22///     .title("My App")
23///     .size(800, 600)
24///     .home(Text::new("Hello, Kobalt!"))
25///     .run()
26///     .unwrap();
27/// ```
28pub struct KobaltApp {
29    title: Box<dyn Widget>,
30    width: u32,
31    height: u32,
32    background_color: Color,
33    home: Option<Box<dyn Widget>>,
34}
35
36impl KobaltApp {
37    /// Creates a new Kobalt application builder
38    pub fn build() -> Self {
39        // Default title
40        let default_title = kobalt_widgets::Text::new("Kobalt App");
41
42        Self {
43            title: Box::new(default_title),
44            width: 800,
45            height: 600,
46            background_color: Color::from_rgb8(20, 20, 30),
47            home: None,
48        }
49    }
50
51    /// Sets the window title (as a Widget)
52    pub fn title<W: Widget + 'static>(mut self, title: W) -> Self {
53        self.title = Box::new(title);
54        self
55    }
56
57    /// Sets the window size
58    pub fn size(mut self, width: u32, height: u32) -> Self {
59        self.width = width;
60        self.height = height;
61        self
62    }
63
64    /// Sets the background color
65    pub fn background(mut self, color: Color) -> Self {
66        self.background_color = color;
67        self
68    }
69
70    /// Sets the home widget (main content)
71    pub fn home<W: Widget + 'static>(mut self, widget: W) -> Self {
72        self.home = Some(Box::new(widget));
73        self
74    }
75
76    /// Runs the application
77    pub fn run(self) -> Result<(), Box<dyn std::error::Error>> {
78        let title_widget = self.title;
79        let width = self.width;
80        let height = self.height;
81        let bg_color = self.background_color;
82        let home_widget = self.home.expect("Home widget not set. Call .home() before .run()");
83
84        // Extract title text for window (temporary hack)
85        let window_title = if title_widget.widget_type() == "Text" {
86            let text_ptr = &*title_widget as *const dyn Widget as *const kobalt_widgets::Text;
87            let text = unsafe { &*text_ptr };
88            text.content().to_string()
89        } else {
90            "Kobalt App".to_string()
91        };
92
93        // Text renderer state
94        let mut text_renderer: Option<RealTextRenderer> = None;
95
96        let app = DesktopApp::new(&window_title, width, height, move |window, event| {
97            match event {
98                Event::WindowEvent {
99                    event: WindowEvent::RedrawRequested,
100                    ..
101                } => {
102                    // Initialize text renderer on first frame
103                    if text_renderer.is_none() {
104                        let renderer = window.renderer();
105                        text_renderer = Some(RealTextRenderer::new(
106                            renderer.device(),
107                            renderer.config(),
108                        ));
109                    }
110
111                    let screen_size = window.renderer().size();
112
113                    // Collect all widgets to render
114                    let mut widgets_to_render = Vec::new();
115                    collect_widgets(&*home_widget, &mut widgets_to_render);
116
117                    // Prepare text for each widget
118                    if let Some(ref mut text_renderer) = text_renderer {
119                        for widget in &widgets_to_render {
120                            if widget.widget_type() == "Text" {
121                                add_text_widget(
122                                    *widget,
123                                    text_renderer,
124                                    window.renderer().device(),
125                                    window.renderer().queue(),
126                                    screen_size.width,
127                                    screen_size.height,
128                                );
129                            }
130                        }
131                    }
132
133                    // Render frame
134                    match window.renderer().begin_frame() {
135                        Ok(frame) => {
136                            let mut encoder = frame.create_encoder();
137                            {
138                                let mut render_pass = frame.begin_render_pass(&mut encoder, bg_color);
139
140                                // Render text
141                                if let Some(ref text_renderer) = text_renderer {
142                                    text_renderer.render(&mut render_pass);
143                                }
144                            }
145                            frame.present(encoder);
146                        }
147                        Err(kobalt_render::SurfaceError::Lost) => {
148                            window.renderer_mut().resize(screen_size);
149                        }
150                        Err(kobalt_render::SurfaceError::OutOfMemory) => {
151                            eprintln!("Out of memory!");
152                        }
153                        Err(e) => {
154                            eprintln!("Surface error: {:?}", e);
155                        }
156                    }
157                }
158                _ => {}
159            }
160        });
161
162        app.run()
163    }
164}
165
166/// Recursively collect all widgets from the widget tree
167fn collect_widgets<'a>(widget: &'a dyn Widget, widgets: &mut Vec<&'a dyn Widget>) {
168    match widget.widget_type() {
169        "Text" => {
170            widgets.push(widget);
171        }
172        "Column" => {
173            // Downcast to Column to access children
174            let column_ptr = widget as *const dyn Widget as *const kobalt_widgets::Column;
175            let column = unsafe { &*column_ptr };
176
177            for child in column.children() {
178                collect_widgets(&**child, widgets);
179            }
180        }
181        _ => {
182            eprintln!("Warning: Unknown widget type: {}", widget.widget_type());
183        }
184    }
185}
186
187/// Add a text widget to the text renderer
188fn add_text_widget(
189    widget: &dyn Widget,
190    text_renderer: &mut RealTextRenderer,
191    device: &wgpu::Device,
192    queue: &wgpu::Queue,
193    screen_width: f32,
194    screen_height: f32,
195) {
196    use kobalt_widgets::Text;
197
198    let text_ptr = widget as *const dyn Widget as *const Text;
199    let text = unsafe { &*text_ptr };
200
201    text_renderer.prepare_text(
202        device,
203        queue,
204        text.content(),
205        text.position,
206        text.font_size,
207        text.color,
208        screen_width,
209        screen_height,
210    );
211}