Skip to main content

lumen_core/
introspect.rs

1//! Typed, whitelisted component-introspection registry for the dynamic
2//! DOM `n.components()` / `n.component("LayoutBox")` reads (design 4.7).
3//!
4//! Each exposable Lumen component registers a reader that turns its public
5//! fields into a `(name, value)` string map. The read is bounded and typed;
6//! there is no raw transmute or arbitrary memory access, and a name that
7//! is not in the registry is an error rather than an empty read. The
8//! runtime runs the registry against every element each tick and publishes
9//! the resulting maps into the cross-thread snapshot the script hosts read.
10
11use crate::components::{
12    Fill, InlineStyle, LumenAttributes, LumenClasses, Opacity, Style, TextContent, Transform,
13    Visible, Visuals, ZIndex,
14};
15use bevy_ecs::world::EntityRef;
16
17/// A field map for one component instance: `(field, value)` pairs.
18pub type ComponentValueMap = Vec<(String, String)>;
19
20/// Reader for one exposable component: `Some(map)` when the entity carries
21/// it, `None` when absent.
22pub type ComponentReader = fn(EntityRef) -> Option<ComponentValueMap>;
23
24/// The whitelist of exposable components and their field readers. Built
25/// once by the runtime (see `with_defaults`) and consulted per element.
26pub struct ComponentIntrospection {
27    readers: Vec<(&'static str, ComponentReader)>,
28}
29
30impl ComponentIntrospection {
31    /// The starter whitelist: geometry, paint, text, identity, and the
32    /// generic attribute / inline-style maps.
33    pub fn with_defaults() -> Self {
34        Self {
35            readers: vec![
36                ("LayoutBox", read_layout_box),
37                ("Visuals", read_visuals),
38                ("Opacity", read_opacity),
39                ("ZIndex", read_z_index),
40                ("Visible", read_visible),
41                ("TextContent", read_text_content),
42                ("LumenClasses", read_classes),
43                ("LumenAttributes", read_attributes),
44                ("InlineStyle", read_inline_style),
45                ("Style", read_style),
46            ],
47        }
48    }
49
50    /// The names of every whitelisted component, in registry order.
51    pub fn names(&self) -> Vec<&'static str> {
52        self.readers.iter().map(|(n, _)| *n).collect()
53    }
54
55    /// Whether `name` is a whitelisted component.
56    pub fn is_known(&self, name: &str) -> bool {
57        self.readers.iter().any(|(n, _)| *n == name)
58    }
59
60    /// Every whitelisted component present on `entity`, with its field map.
61    pub fn read_all(&self, entity: EntityRef) -> Vec<(String, ComponentValueMap)> {
62        self.readers
63            .iter()
64            .filter_map(|(name, reader)| reader(entity).map(|map| (name.to_string(), map)))
65            .collect()
66    }
67}
68
69fn read_layout_box(e: EntityRef) -> Option<ComponentValueMap> {
70    let t = e.get::<Transform>()?;
71    let mut m = vec![
72        ("x".into(), t.absolute.x.to_string()),
73        ("y".into(), t.absolute.y.to_string()),
74        ("width".into(), t.size.x.to_string()),
75        ("height".into(), t.size.y.to_string()),
76    ];
77    if let Some(b) = t.baseline_y {
78        m.push(("baseline_y".into(), b.to_string()));
79    }
80    Some(m)
81}
82
83fn hex(c: &crate::components::Color) -> String {
84    let [r, g, b, a] = c.to_rgba8();
85    if a == 0xff {
86        format!("#{r:02x}{g:02x}{b:02x}")
87    } else {
88        format!("#{r:02x}{g:02x}{b:02x}{a:02x}")
89    }
90}
91
92fn read_visuals(e: EntityRef) -> Option<ComponentValueMap> {
93    let v = e.get::<Visuals>()?;
94    let mut m = vec![("radius".into(), v.radius.to_string())];
95    match &v.fill {
96        Some(Fill::Solid(c)) => m.push(("fill".into(), hex(c))),
97        Some(_) => m.push(("fill".into(), "gradient".into())),
98        None => {}
99    }
100    if let Some(border) = &v.border {
101        m.push(("border_color".into(), hex(&border.color)));
102        m.push(("border_width".into(), border.widths.top.to_string()));
103    }
104    m.push(("shadows".into(), v.shadows.len().to_string()));
105    Some(m)
106}
107
108fn read_opacity(e: EntityRef) -> Option<ComponentValueMap> {
109    let o = e.get::<Opacity>()?;
110    Some(vec![("value".into(), o.0.to_string())])
111}
112
113fn read_z_index(e: EntityRef) -> Option<ComponentValueMap> {
114    let z = e.get::<ZIndex>()?;
115    Some(vec![("value".into(), z.0.to_string())])
116}
117
118fn read_visible(e: EntityRef) -> Option<ComponentValueMap> {
119    let v = e.get::<Visible>()?;
120    Some(vec![("value".into(), v.0.to_string())])
121}
122
123fn read_text_content(e: EntityRef) -> Option<ComponentValueMap> {
124    let t = e.get::<TextContent>()?;
125    Some(vec![("text".into(), t.0.clone())])
126}
127
128fn read_classes(e: EntityRef) -> Option<ComponentValueMap> {
129    let c = e.get::<LumenClasses>()?;
130    Some(vec![(
131        "classes".into(),
132        c.0.iter()
133            .map(|s| s.to_string())
134            .collect::<Vec<_>>()
135            .join(" "),
136    )])
137}
138
139fn read_attributes(e: EntityRef) -> Option<ComponentValueMap> {
140    let a = e.get::<LumenAttributes>()?;
141    let mut m: ComponentValueMap = a.0.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
142    m.sort();
143    Some(m)
144}
145
146fn read_inline_style(e: EntityRef) -> Option<ComponentValueMap> {
147    let s = e.get::<InlineStyle>()?;
148    Some(s.0.clone())
149}
150
151fn read_style(e: EntityRef) -> Option<ComponentValueMap> {
152    let s = e.get::<Style>()?;
153    Some(vec![
154        ("display".into(), format!("{:?}", s.display)),
155        ("width".into(), format!("{:?}", s.width)),
156        ("height".into(), format!("{:?}", s.height)),
157    ])
158}