StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use crate::image::{Image, ImageFit, ImageSource}; |
| 2 | use crate::prelude::*; |
| 3 | use crate::widget::{Widget, WidgetContext, WidgetId}; |
| 4 | use std::collections::HashMap; |
| 5 | use std::sync::{Arc, Mutex}; |
| 6 | use strato_core::event::{Event, EventResult}; |
| 7 | use strato_core::layout::{Constraints, Layout, Size}; |
| 8 | use strato_core::types::Point; |
| 9 | use strato_core::ui_node::{PropValue, UiNode, WidgetNode}; |
| 10 | use strato_renderer::batch::RenderBatch; |
| 11 | |
| 12 | /// A builder function that creates a widget from properties. |
| 13 | type WidgetBuilder = Box< |
| 14 | dyn Fn(Vec<(String, PropValue)>, Vec<UiNode>, &WidgetRegistry) -> Box<dyn Widget> + Send + Sync, |
| 15 | >; |
| 16 | |
| 17 | /// Registry for mapping widget names to their constructors. |
| 18 | pub struct WidgetRegistry { |
| 19 | builders: HashMap<String, WidgetBuilder>, |
| 20 | } |
| 21 | |
| 22 | /// Wrapper to allow Box<dyn Widget> to satisfy impl Widget |
| 23 | #[derive(Debug)] |
| 24 | pub struct BoxedWidget(pub Box<dyn Widget>); |
| 25 | |
| 26 | impl Widget for BoxedWidget { |
| 27 | fn id(&self) -> WidgetId { |
| 28 | self.0.id() |
| 29 | } |
| 30 | |
| 31 | fn layout(&mut self, constraints: Constraints) -> Size { |
| 32 | self.0.layout(constraints) |
| 33 | } |
| 34 | |
| 35 | fn render(&self, batch: &mut RenderBatch, layout: Layout) { |
| 36 | self.0.render(batch, layout) |
| 37 | } |
| 38 | |
| 39 | fn handle_event(&mut self, event: &Event) -> EventResult { |
| 40 | self.0.handle_event(event) |
| 41 | } |
| 42 | |
| 43 | fn update(&mut self, ctx: &WidgetContext) { |
| 44 | self.0.update(ctx) |
| 45 | } |
| 46 | |
| 47 | fn children(&self) -> Vec<&(dyn Widget + '_)> { |
| 48 | self.0.children() |
| 49 | } |
| 50 | fn children_mut(&mut self) -> Vec<&mut (dyn Widget + '_)> { |
| 51 | self.0.children_mut() |
| 52 | } |
| 53 | |
| 54 | fn hit_test(&self, point: Point, layout: Layout) -> bool { |
| 55 | self.0.hit_test(point, layout) |
| 56 | } |
| 57 | |
| 58 | fn as_any(&self) -> &dyn std::any::Any { |
| 59 | self.0.as_any() |
| 60 | } |
| 61 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { |
| 62 | self.0.as_any_mut() |
| 63 | } |
| 64 | |
| 65 | fn clone_widget(&self) -> Box<dyn Widget> { |
| 66 | self.0.clone_widget() |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | impl WidgetRegistry { |
| 71 | pub fn new() -> Self { |
| 72 | let mut registry = Self { |
| 73 | builders: HashMap::new(), |
| 74 | }; |
| 75 | registry.register_defaults(); |
| 76 | registry |
| 77 | } |
| 78 | |
| 79 | /// Register a widget builder. |
| 80 | pub fn register<F>(&mut self, name: &str, builder: F) |
| 81 | where |
| 82 | F: Fn(Vec<(String, PropValue)>, Vec<UiNode>, &WidgetRegistry) -> Box<dyn Widget> |
| 83 | + Send |
| 84 | + Sync |
| 85 | + 'static, |
| 86 | { |
| 87 | self.builders.insert(name.to_string(), Box::new(builder)); |
| 88 | } |
| 89 | |
| 90 | /// Build a widget tree from a UiNode. |
| 91 | pub fn build(&self, node: UiNode) -> BoxedWidget { |
| 92 | BoxedWidget(match node { |
| 93 | UiNode::Widget(node) => self.build_widget(node), |
| 94 | UiNode::Text(text) => Box::new(Text::new(text)), |
| 95 | UiNode::Fragment(children) => { |
| 96 | let mut col = Column::new(); |
| 97 | for child in children { |
| 98 | col = col.child(self.build(child).0); |
| 99 | } |
| 100 | Box::new(col) |
| 101 | } |
| 102 | }) |
| 103 | } |
| 104 | |
| 105 | fn build_widget(&self, node: WidgetNode) -> Box<dyn Widget> { |
| 106 | if let Some(builder) = self.builders.get(&node.name) { |
| 107 | builder(node.props, node.children, self) |
| 108 | } else { |
| 109 | // Fallback for unknown widgets - maybe a red text? |
| 110 | Box::new(Text::new(format!("Unknown widget: {}", node.name))) |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | fn register_defaults(&mut self) { |
| 115 | // Container |
| 116 | self.register("Container", |props, children, registry| { |
| 117 | let mut widget = Container::new(); |
| 118 | for (name, value) in props { |
| 119 | match (name.as_str(), value) { |
| 120 | ("padding", PropValue::Float(v)) => widget = widget.padding(v as f32), |
| 121 | ("background", PropValue::Color(c)) => widget = widget.background(c), |
| 122 | ("width", PropValue::Float(v)) => widget = widget.width(v as f32), |
| 123 | ("height", PropValue::Float(v)) => widget = widget.height(v as f32), |
| 124 | ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), |
| 125 | ("margin", PropValue::Float(v)) => widget = widget.margin(v as f32), |
| 126 | _ => {} |
| 127 | } |
| 128 | } |
| 129 | // Handle "child" logic (Container takes 1 child usually, but our generic AST has list) |
| 130 | if let Some(first) = children.first() { |
| 131 | widget = widget.child(registry.build(first.clone())); |
| 132 | } |
| 133 | Box::new(widget) |
| 134 | }); |
| 135 | |
| 136 | // Column |
| 137 | self.register("Column", |props, children, registry| { |
| 138 | let mut widget = Column::new(); |
| 139 | for (name, value) in props { |
| 140 | if name == "spacing" { |
| 141 | if let PropValue::Float(v) = value { |
| 142 | widget = widget.spacing(v as f32); |
| 143 | } |
| 144 | } |
| 145 | } |
| 146 | // Column::children takes Vec<Box<dyn Widget>>. |
| 147 | // registry.build returns BoxedWidget. |
| 148 | // We need to unwrap or map. |
| 149 | let child_widgets: Vec<Box<dyn Widget>> = children |
| 150 | .into_iter() |
| 151 | .map(|child| registry.build(child).0) |
| 152 | .collect(); |
| 153 | |
| 154 | widget = widget.children(child_widgets); |
| 155 | Box::new(widget) |
| 156 | }); |
| 157 | |
| 158 | // Row |
| 159 | self.register("Row", |props, children, registry| { |
| 160 | let mut widget = Row::new(); |
| 161 | for (name, value) in props { |
| 162 | if name == "spacing" { |
| 163 | if let PropValue::Float(v) = value { |
| 164 | widget = widget.spacing(v as f32); |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | let child_widgets: Vec<Box<dyn Widget>> = children |
| 169 | .into_iter() |
| 170 | .map(|child| registry.build(child).0) |
| 171 | .collect(); |
| 172 | widget = widget.children(child_widgets); |
| 173 | Box::new(widget) |
| 174 | }); |
| 175 | |
| 176 | // Text |
| 177 | self.register("Text", |props, _children, _registry| { |
| 178 | let mut text = String::new(); |
| 179 | |
| 180 | // First pass: find text semantic prop |
| 181 | for (name, value) in &props { |
| 182 | if name == "text" { |
| 183 | if let PropValue::String(s) = value { |
| 184 | text = s.clone(); |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | let mut widget = Text::new(text); |
| 190 | |
| 191 | // Second pass: apply properties |
| 192 | for (name, value) in props { |
| 193 | match (name.as_str(), value) { |
| 194 | ("color", PropValue::Color(c)) => widget = widget.color(c), |
| 195 | ("size", PropValue::Float(v)) => widget = widget.size(v as f32), |
| 196 | _ => {} |
| 197 | } |
| 198 | } |
| 199 | |
| 200 | Box::new(widget) |
| 201 | }); |
| 202 | |
| 203 | // Button |
| 204 | self.register("Button", |props, _children, _registry| { |
| 205 | let mut label = String::new(); |
| 206 | for (name, value) in &props { |
| 207 | if name == "text" { |
| 208 | if let PropValue::String(s) = value { |
| 209 | label = s.clone(); |
| 210 | } |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | let widget = Button::new(label); |
| 215 | |
| 216 | // Button usually doesn't take children in this framework, just text in constructor? |
| 217 | // But macro might support `Button { child: Icon }`? |
| 218 | // Existing Button::new implementation takes string. |
| 219 | // If children present, ignored? or fallback? |
| 220 | |
| 221 | for (name, value) in props { |
| 222 | match (name.as_str(), value) { |
| 223 | // disabled? |
| 224 | |
| 225 | // events? |
| 226 | _ => {} |
| 227 | } |
| 228 | } |
| 229 | Box::new(widget) |
| 230 | }); |
| 231 | // Image |
| 232 | self.register("Image", |props, _children, _registry| { |
| 233 | let mut source = ImageSource::Placeholder { |
| 234 | width: 100, |
| 235 | height: 100, |
| 236 | color: Color::GRAY, |
| 237 | }; |
| 238 | |
| 239 | for (name, value) in &props { |
| 240 | if name == "source" { |
| 241 | if let PropValue::String(s) = value { |
| 242 | // Simple heuristic for source type |
| 243 | if s.starts_with("http") { |
| 244 | source = ImageSource::Url(s.clone()); |
| 245 | } else if s.starts_with("placeholder") { |
| 246 | // Format: placeholder:width:height:hex |
| 247 | // Simplified parsing for now: placeholder -> default |
| 248 | } else { |
| 249 | source = ImageSource::File(std::path::PathBuf::from(s)); |
| 250 | } |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | let mut widget = Image::new(source); |
| 256 | for (name, value) in props { |
| 257 | match (name.as_str(), value) { |
| 258 | ("fit", PropValue::String(s)) => { |
| 259 | let fit = match s.as_str() { |
| 260 | "cover" => ImageFit::Cover, |
| 261 | "contain" => ImageFit::Contain, |
| 262 | "fill" => ImageFit::Fill, |
| 263 | _ => ImageFit::None, |
| 264 | }; |
| 265 | widget = widget.fit(fit); |
| 266 | } |
| 267 | ("opacity", PropValue::Float(v)) => widget = widget.opacity(v as f32), |
| 268 | ("radius", PropValue::Float(v)) => widget = widget.border_radius(v as f32), |
| 269 | _ => {} |
| 270 | } |
| 271 | } |
| 272 | Box::new(widget) |
| 273 | }); |
| 274 | |
| 275 | // TopBar |
| 276 | self.register("TopBar", |props, _children, _registry| { |
| 277 | let mut title = "".to_string(); |
| 278 | // First pass for title |
| 279 | for (name, value) in &props { |
| 280 | if name == "title" { |
| 281 | if let PropValue::String(s) = value { |
| 282 | title = s.clone(); |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | let mut widget = crate::top_bar::TopBar::new(title); |
| 288 | |
| 289 | for (name, value) in props { |
| 290 | match (name.as_str(), value) { |
| 291 | ("background", PropValue::Color(c)) => widget = widget.with_background(c), |
| 292 | // height handles directly field access if needed, or via method if added |
| 293 | _ => {} |
| 294 | } |
| 295 | } |
| 296 | Box::new(widget) |
| 297 | }); |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | // Global registry instance (lazy static approach usually, but here we instantiate it) |
| 302 | pub fn create_default_registry() -> WidgetRegistry { |
| 303 | WidgetRegistry::new() |
| 304 | } |
| 305 |