1use std::any::Any;
30use std::collections::HashMap;
31use std::sync::{Arc, OnceLock, RwLock};
32
33use super::{StaticValue, PropValue, Template, TemplateNode};
34use crate::tree::{Button, Column, Row, Text, Widget};
35
36pub type Handler = Arc<dyn Fn() + Send + Sync>;
40
41pub enum PropInput<'a> {
44 Static(&'a StaticValue),
45 Hole(&'a dyn Any),
46}
47
48#[macro_export]
65macro_rules! inflatable {
66 ($name:literal, $ctor:expr, children, { $($prop:literal => $setter:ident : $ty:ty),* $(,)? }) => {
69 $crate::template::register_widget($name, |_args: &[$crate::template::PropInput], props, children| {
70 let mut w = $ctor;
71 for (k, v) in props {
72 let _ = &v;
74 match k.as_str() {
75 $( $prop => w = w.$setter(<$ty as $crate::template::FromProp>::from_prop(v, $name, $prop)?), )*
76 _ => return ::core::result::Result::Err(
77 $crate::template::InflateError::UnknownProp { widget: $name.into(), prop: k.clone() }
78 ),
79 }
80 }
81 for kid in children { w = w.child(kid); }
82 let _ = &mut w; ::core::result::Result::Ok(::std::boxed::Box::new(w) as ::std::boxed::Box<dyn $crate::tree::Widget>)
84 });
85 };
86 ($name:literal, $ctor:expr, leaf, { $($prop:literal => $setter:ident : $ty:ty),* $(,)? }) => {
88 $crate::template::register_widget($name, |_args: &[$crate::template::PropInput], props, children: ::std::vec::Vec<::std::boxed::Box<dyn $crate::tree::Widget>>| {
89 if !children.is_empty() {
90 return ::core::result::Result::Err(
91 $crate::template::InflateError::UnexpectedChildren { widget: $name.into() }
92 );
93 }
94 let mut w = $ctor;
95 for (k, v) in props {
96 let _ = &v;
97 match k.as_str() {
98 $( $prop => w = w.$setter(<$ty as $crate::template::FromProp>::from_prop(v, $name, $prop)?), )*
99 _ => return ::core::result::Result::Err(
100 $crate::template::InflateError::UnknownProp { widget: $name.into(), prop: k.clone() }
101 ),
102 }
103 }
104 let _ = &mut w;
105 ::core::result::Result::Ok(::std::boxed::Box::new(w) as ::std::boxed::Box<dyn $crate::tree::Widget>)
106 });
107 };
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum InflateError {
113 UnknownWidget(String),
115 UnknownProp { widget: String, prop: String },
117 PropType { widget: String, prop: String, expected: &'static str },
120 UnexpectedChildren { widget: String },
122 HoleOutOfRange { index: usize, len: usize },
124}
125
126impl std::fmt::Display for InflateError {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 match self {
129 InflateError::UnknownWidget(w) => write!(f, "unknown widget `{w}`"),
130 InflateError::UnknownProp { widget, prop } => write!(f, "`{widget}` has no prop `{prop}`"),
131 InflateError::PropType { widget, prop, expected } => {
132 write!(f, "`{widget}.{prop}` expected {expected}")
133 }
134 InflateError::UnexpectedChildren { widget } => write!(f, "`{widget}` cannot have children"),
135 InflateError::HoleOutOfRange { index, len } => {
136 write!(f, "hole #{index} out of range (only {len} supplied)")
137 }
138 }
139 }
140}
141impl std::error::Error for InflateError {}
142
143pub type BuildFn = Box<
147 dyn for<'a> Fn(&'a [PropInput<'a>], &'a [(String, PropInput<'a>)], Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError>
148 + Send
149 + Sync,
150>;
151
152fn registry() -> &'static RwLock<HashMap<String, BuildFn>> {
153 static REG: OnceLock<RwLock<HashMap<String, BuildFn>>> = OnceLock::new();
154 REG.get_or_init(|| RwLock::new(builtin_widgets()))
155}
156
157pub fn register_widget<F>(name: impl Into<String>, build: F)
160where
161 F: for<'a> Fn(&'a [PropInput<'a>], &'a [(String, PropInput<'a>)], Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError>
162 + Send
163 + Sync
164 + 'static,
165{
166 registry().write().unwrap_or_else(|e| e.into_inner()).insert(name.into(), Box::new(build));
167}
168
169pub fn is_registered(name: &str) -> bool {
171 registry().read().unwrap_or_else(|e| e.into_inner()).contains_key(name)
172}
173
174pub fn inflate(template: &Template, holes: &[Box<dyn Any>]) -> Result<Box<dyn Widget>, InflateError> {
176 inflate_node(&template.root, holes)
177}
178
179fn resolve<'a>(value: &'a PropValue, holes: &'a [Box<dyn Any>]) -> Result<PropInput<'a>, InflateError> {
182 match value {
183 PropValue::Static(s) => Ok(PropInput::Static(s)),
184 PropValue::Hole(i) => holes
185 .get(*i)
186 .map(|h| PropInput::Hole(h.as_ref()))
187 .ok_or(InflateError::HoleOutOfRange { index: *i, len: holes.len() }),
188 }
189}
190
191fn inflate_node(node: &TemplateNode, holes: &[Box<dyn Any>]) -> Result<Box<dyn Widget>, InflateError> {
192 let mut args: Vec<PropInput<'_>> = Vec::with_capacity(node.args.len());
194 for value in &node.args {
195 args.push(resolve(value, holes)?);
196 }
197 let mut props: Vec<(String, PropInput<'_>)> = Vec::with_capacity(node.props.len());
198 for (key, value) in &node.props {
199 props.push((key.clone(), resolve(value, holes)?));
200 }
201
202 let mut children: Vec<Box<dyn Widget>> = Vec::with_capacity(node.children.len());
204 for child in &node.children {
205 children.push(inflate_node(child, holes)?);
206 }
207
208 let reg = registry().read().unwrap_or_else(|e| e.into_inner());
209 let build = reg
210 .get(&node.widget)
211 .ok_or_else(|| InflateError::UnknownWidget(node.widget.clone()))?;
212 build(&args, &props, children)
213}
214
215pub trait FromProp: Sized {
225 const TYPE_NAME: &'static str;
227 fn from_prop(pi: &PropInput, widget: &str, prop: &str) -> Result<Self, InflateError>;
228}
229
230fn prop_type_err<T: FromProp>(widget: &str, prop: &str) -> InflateError {
232 InflateError::PropType { widget: widget.to_string(), prop: prop.to_string(), expected: T::TYPE_NAME }
233}
234
235macro_rules! impl_from_prop_number {
236 ($($t:ty),+) => {$(
237 impl FromProp for $t {
238 const TYPE_NAME: &'static str = stringify!($t);
239 fn from_prop(pi: &PropInput, widget: &str, prop: &str) -> Result<Self, InflateError> {
240 match pi {
241 PropInput::Static(StaticValue::Float(f)) => Ok(*f as $t),
242 PropInput::Static(StaticValue::Int(i)) => Ok(*i as $t),
243 PropInput::Static(StaticValue::Bool(b)) => Ok(*b as i64 as $t),
244 PropInput::Hole(any) => any
245 .downcast_ref::<$t>()
246 .copied()
247 .or_else(|| any.downcast_ref::<f32>().map(|v| *v as $t))
248 .or_else(|| any.downcast_ref::<f64>().map(|v| *v as $t))
249 .or_else(|| any.downcast_ref::<i64>().map(|v| *v as $t))
250 .ok_or_else(|| prop_type_err::<$t>(widget, prop)),
251 _ => Err(prop_type_err::<$t>(widget, prop)),
252 }
253 }
254 }
255 )+};
256}
257impl_from_prop_number!(f32, f64, i64);
258
259impl FromProp for bool {
260 const TYPE_NAME: &'static str = "bool";
261 fn from_prop(pi: &PropInput, widget: &str, prop: &str) -> Result<Self, InflateError> {
262 match pi {
263 PropInput::Static(StaticValue::Bool(b)) => Ok(*b),
264 PropInput::Hole(any) => any.downcast_ref::<bool>().copied().ok_or_else(|| prop_type_err::<bool>(widget, prop)),
265 _ => Err(prop_type_err::<bool>(widget, prop)),
266 }
267 }
268}
269
270impl FromProp for String {
271 const TYPE_NAME: &'static str = "string";
272 fn from_prop(pi: &PropInput, widget: &str, prop: &str) -> Result<Self, InflateError> {
273 match pi {
274 PropInput::Static(StaticValue::Str(s)) => Ok(s.clone()),
275 PropInput::Hole(any) => any
276 .downcast_ref::<String>()
277 .cloned()
278 .or_else(|| any.downcast_ref::<&str>().map(|s| s.to_string()))
279 .ok_or_else(|| prop_type_err::<String>(widget, prop)),
280 _ => Err(prop_type_err::<String>(widget, prop)),
281 }
282 }
283}
284
285impl FromProp for Handler {
289 const TYPE_NAME: &'static str = "handler";
290 fn from_prop(pi: &PropInput, widget: &str, prop: &str) -> Result<Self, InflateError> {
291 match pi {
292 PropInput::Hole(any) => any
293 .downcast_ref::<Handler>()
294 .cloned()
295 .ok_or_else(|| prop_type_err::<Handler>(widget, prop)),
296 _ => Err(prop_type_err::<Handler>(widget, prop)),
297 }
298 }
299}
300
301fn builtin_widgets() -> HashMap<String, BuildFn> {
304 let mut m: HashMap<String, BuildFn> = HashMap::new();
305 m.insert("Column".into(), Box::new(build_column));
306 m.insert("Row".into(), Box::new(build_row));
307 m.insert("Text".into(), Box::new(build_text));
308 m.insert("Button".into(), Box::new(build_button));
309 m
310}
311
312fn build_button(args: &[PropInput], props: &[(String, PropInput)], children: Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError> {
315 if !children.is_empty() {
316 return Err(InflateError::UnexpectedChildren { widget: "Button".into() });
317 }
318 let label = match args.first() {
319 Some(a) => String::from_prop(a, "Button", "label")?,
320 None => String::new(),
321 };
322 let mut button = Button::new(label);
323 for (k, v) in props {
324 match k.as_str() {
325 "on_press" => {
326 let handler = Handler::from_prop(v, "Button", "on_press")?;
327 button = button.on_press(move || (*handler)());
328 }
329 _ => return Err(InflateError::UnknownProp { widget: "Button".into(), prop: k.clone() }),
330 }
331 }
332 Ok(Box::new(button))
333}
334
335fn build_column(_args: &[PropInput], props: &[(String, PropInput)], children: Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError> {
338 let mut col = Column::new();
339 for (k, v) in props {
340 match k.as_str() {
341 "spacing" => col = col.spacing(f32::from_prop(v, "Column", "spacing")?),
342 _ => return Err(InflateError::UnknownProp { widget: "Column".into(), prop: k.clone() }),
343 }
344 }
345 for kid in children {
346 col = col.child(kid);
347 }
348 Ok(Box::new(col))
349}
350
351fn build_row(_args: &[PropInput], props: &[(String, PropInput)], children: Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError> {
352 let mut row = Row::new();
353 for (k, v) in props {
354 match k.as_str() {
355 "spacing" => row = row.spacing(f32::from_prop(v, "Row", "spacing")?),
356 _ => return Err(InflateError::UnknownProp { widget: "Row".into(), prop: k.clone() }),
357 }
358 }
359 for kid in children {
360 row = row.child(kid);
361 }
362 Ok(Box::new(row))
363}
364
365fn build_text(args: &[PropInput], props: &[(String, PropInput)], children: Vec<Box<dyn Widget>>) -> Result<Box<dyn Widget>, InflateError> {
367 if !children.is_empty() {
368 return Err(InflateError::UnexpectedChildren { widget: "Text".into() });
369 }
370 let content = match args.first() {
371 Some(a) => String::from_prop(a, "Text", "text")?,
372 None => String::new(),
373 };
374 if let Some((k, _)) = props.first() {
377 return Err(InflateError::UnknownProp { widget: "Text".into(), prop: k.clone() });
378 }
379 Ok(Box::new(Text::new(content)))
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::template::{Template, TemplateKey, TemplateNode};
386 use crate::tree::{LayoutCtx, Widget};
387 use rosace_layout::Constraints;
388
389 fn tmpl(root: TemplateNode) -> Template {
390 Template::new(TemplateKey::new("src/inflate_test.rs", 1, 1), root)
391 }
392
393 fn measure(w: &dyn Widget) -> rosace_core::types::Size {
395 let font = rosace_render::FontCache::embedded();
396 let theme = rosace_theme::built_in::dark_theme();
397 let ctx = LayoutCtx::new(Constraints::loose(400.0, 400.0), &font, &theme);
398 w.layout(&ctx)
399 }
400
401 #[test]
402 fn button_inflates_with_a_handler_hole_and_matches_the_builder() {
403 let handler: Handler = Arc::new(|| {});
405 let t = tmpl(
406 TemplateNode::new("Button")
407 .with_arg_static(StaticValue::Str("Save".into()))
408 .with_hole("on_press", 0),
409 );
410 let holes: Vec<Box<dyn Any>> = vec![Box::new(handler)];
411 let inflated = inflate(&t, &holes).expect("button with a handler hole inflates");
412 assert_eq!(measure(&*inflated), measure(&Button::new("Save").on_press(|| {})));
413 }
414
415 #[test]
416 fn a_non_handler_value_in_a_handler_slot_escalates() {
417 let t = tmpl(
419 TemplateNode::new("Button")
420 .with_arg_static(StaticValue::Str("x".into()))
421 .with_hole("on_press", 0),
422 );
423 let holes: Vec<Box<dyn Any>> = vec![Box::new(42i64)];
424 assert!(matches!(inflate(&t, &holes).err(), Some(InflateError::PropType { .. })));
425 }
426
427 #[test]
428 fn positional_arg_constructs_a_text_like_the_builder() {
429 let t = tmpl(TemplateNode::new("Text").with_arg_static(StaticValue::Str("Hi".into())));
431 let inflated = inflate(&t, &[]).expect("inflate Text(\"Hi\")");
432 assert_eq!(measure(&*inflated), measure(&Text::new("Hi")));
433 }
434
435 #[test]
436 fn positional_arg_binds_a_hole() {
437 let t = tmpl(TemplateNode::new("Text").with_arg_hole(0));
439 let holes: Vec<Box<dyn Any>> = vec![Box::new(String::from("live"))];
440 let inflated = inflate(&t, &holes).expect("inflate Text(hole)");
441 assert_eq!(measure(&*inflated), measure(&Text::new("live")));
442 }
443
444 #[test]
445 fn unknown_widget_escalates() {
446 let t = tmpl(TemplateNode::new("NoSuchWidget"));
447 assert_eq!(inflate(&t, &[]).err(), Some(InflateError::UnknownWidget("NoSuchWidget".into())));
448 }
449
450 #[test]
451 fn inflates_children_and_matches_the_builder_for_a_multi_child_tree() {
452 let t = tmpl(
457 TemplateNode::new("Column")
458 .with_static("spacing", StaticValue::Float(8.0))
459 .with_child(TemplateNode::new("Text").with_arg_static(StaticValue::Str("A".into())))
460 .with_child(TemplateNode::new("Text").with_arg_static(StaticValue::Str("B".into()))),
461 );
462 let inflated = inflate(&t, &[]).expect("inflate");
463 let built = Column::new().spacing(8.0).child(Text::new("A")).child(Text::new("B"));
464 assert_eq!(measure(&*inflated), measure(&built), "two-child inflate must match builder");
465
466 let empty = inflate(&tmpl(TemplateNode::new("Column")), &[]).expect("inflate empty");
467 assert!(measure(&*inflated).height > measure(&*empty).height, "children should add height");
468 }
469
470 #[test]
471 fn inflated_static_tree_lays_out_identically_to_the_builder() {
472 let t = tmpl(
473 TemplateNode::new("Column")
474 .with_static("spacing", StaticValue::Float(8.0))
475 .with_child(TemplateNode::new("Text").with_arg_static(StaticValue::Str("Hi".into()))),
476 );
477 let inflated = inflate(&t, &[]).expect("inflate");
478 let built = Column::new().spacing(8.0).child(Text::new("Hi"));
479 assert_eq!(measure(&*inflated), measure(&built), "inflater must match builder output");
480 }
481
482 #[test]
483 fn binds_value_holes_by_index_matching_the_builder() {
484 let t = tmpl(
486 TemplateNode::new("Column")
487 .with_hole("spacing", 0)
488 .with_child(TemplateNode::new("Text").with_arg_hole(1)),
489 );
490 let holes: Vec<Box<dyn Any>> = vec![Box::new(8.0f32), Box::new(String::from("Hi"))];
491 let inflated = inflate(&t, &holes).expect("inflate");
492 let built = Column::new().spacing(8.0).child(Text::new("Hi"));
493 assert_eq!(measure(&*inflated), measure(&built), "hole binding must match builder output");
494 }
495
496 #[test]
497 fn hole_out_of_range_escalates() {
498 let t = tmpl(TemplateNode::new("Column").with_hole("spacing", 5));
499 assert_eq!(inflate(&t, &[]).err(), Some(InflateError::HoleOutOfRange { index: 5, len: 0 }));
500 }
501
502 #[test]
503 fn wrong_hole_type_escalates() {
504 let t = tmpl(TemplateNode::new("Column").with_hole("spacing", 0));
506 let holes: Vec<Box<dyn Any>> = vec![Box::new(String::from("not a number"))];
507 assert_eq!(
508 inflate(&t, &holes).err(),
509 Some(InflateError::PropType { widget: "Column".into(), prop: "spacing".into(), expected: "f32" })
510 );
511 }
512
513 #[test]
514 fn unknown_prop_escalates() {
515 let t = tmpl(TemplateNode::new("Column").with_static("bogus", StaticValue::Int(1)));
516 assert_eq!(
517 inflate(&t, &[]).err(),
518 Some(InflateError::UnknownProp { widget: "Column".into(), prop: "bogus".into() })
519 );
520 }
521
522 #[test]
523 fn third_party_widget_registers_and_inflates() {
524 register_widget("MyBadge", |_args, _props, _children| Ok(Box::new(Text::new("badge")) as Box<dyn Widget>));
526 assert!(is_registered("MyBadge"));
527 let t = tmpl(TemplateNode::new("MyBadge"));
528 let w = inflate(&t, &[]).expect("custom widget inflates");
529 assert_eq!(measure(&*w), measure(&Text::new("badge")));
531 }
532
533 #[test]
536 fn inflatable_macro_registers_a_container_with_props_and_children() {
537 crate::inflatable!("MacroCol", Column::new(), children, {
539 "spacing" => spacing: f32,
540 });
541 assert!(is_registered("MacroCol"));
542
543 let t = tmpl(
544 TemplateNode::new("MacroCol")
545 .with_static("spacing", StaticValue::Float(8.0))
546 .with_child(TemplateNode::new("Text").with_arg_static(StaticValue::Str("A".into())))
547 .with_child(TemplateNode::new("Text").with_arg_static(StaticValue::Str("B".into()))),
548 );
549 let inflated = inflate(&t, &[]).expect("macro-registered widget inflates");
550 let built = Column::new().spacing(8.0).child(Text::new("A")).child(Text::new("B"));
551 assert_eq!(measure(&*inflated), measure(&built), "macro closure must match builder");
552 }
553
554 #[test]
555 fn inflatable_macro_binds_a_hole_and_reports_unknown_props() {
556 crate::inflatable!("MacroCol2", Column::new(), children, {
557 "spacing" => spacing: f32,
558 });
559 let t = tmpl(TemplateNode::new("MacroCol2").with_hole("spacing", 0));
561 let holes: Vec<Box<dyn Any>> = vec![Box::new(6.0f32)];
562 let inflated = inflate(&t, &holes).expect("hole binds");
563 assert_eq!(measure(&*inflated), measure(&Column::new().spacing(6.0)));
564 let bad = tmpl(TemplateNode::new("MacroCol2").with_static("nope", StaticValue::Int(1)));
566 assert_eq!(
567 inflate(&bad, &[]).err(),
568 Some(InflateError::UnknownProp { widget: "MacroCol2".into(), prop: "nope".into() })
569 );
570 }
571
572 #[test]
573 fn inflatable_macro_leaf_rejects_children() {
574 crate::inflatable!("MacroLeaf", Text::new("leaf"), leaf, {});
575 assert!(is_registered("MacroLeaf"));
576 assert_eq!(
578 measure(&*inflate(&tmpl(TemplateNode::new("MacroLeaf")), &[]).unwrap()),
579 measure(&Text::new("leaf"))
580 );
581 let with_kids = tmpl(TemplateNode::new("MacroLeaf").with_child(TemplateNode::new("Text")));
583 assert_eq!(
584 inflate(&with_kids, &[]).err(),
585 Some(InflateError::UnexpectedChildren { widget: "MacroLeaf".into() })
586 );
587 }
588}