Skip to main content

mittens_engine/engine/ecs/component/
layout.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::Component;
3use crate::engine::ecs::component::style::SizeDimension;
4
5/// The viewport of a self-contained layout subtree — analogous to the browser's
6/// initial containing block.
7///
8/// `LayoutComponent` does **not** participate in flow itself; it defines the space
9/// available to the first `HtmlElementComponent` child (usually `Body`).
10///
11/// Multiple `LayoutComponent` nodes can coexist — one per panel, one per HUD region,
12/// one per workspace.
13///
14/// `available_width` is in **glyph units** (1.0 = one monospace character cell).
15/// World-space scaling stays in `TransformComponent`.
16///
17/// Set `dirty = true` to signal the `LayoutSystem` to recompute the subtree on the next tick.
18#[derive(Debug, Clone)]
19pub struct LayoutComponent {
20    /// Available inline (X-axis) width for children, in glyph units.
21    pub available_width: f32,
22
23    /// Authored available width as entered in MMS.
24    pub authored_available_width: SizeDimension,
25
26    /// Optional block (Y-axis) constraint. Used for overflow/clip; `None` = unconstrained.
27    pub available_height: Option<f32>,
28
29    /// Authored available height as entered in MMS.
30    pub authored_available_height: Option<SizeDimension>,
31
32    /// When `true`, the layout system will recompute this subtree on the next tick.
33    pub dirty: bool,
34
35    /// Scale factor to convert glyph units → local coordinates of the nearest ancestor
36    /// `TransformComponent`.
37    ///
38    /// **When the parent `TransformComponent` already has `scale = TEXT_SCALE`** (i.e. the whole
39    /// subtree is in glyph-unit space), leave this at the default `1.0`.
40    ///
41    /// **When the parent `TransformComponent` has `scale = 1.0` (world units)** and the
42    /// `StyleComponent` heights are authored in glyph units, set `unit_scale = TEXT_SCALE`
43    /// (e.g. `0.08`) so the emitted `UpdateTransform` translations land in world space.
44    pub unit_scale: f32,
45
46    /// When `true`, the layout pass spawns box-model viz quads (padding, content,
47    /// margin) for each styled item in this subtree. Per-tree dynamic toggle —
48    /// flipped via `IntentValue::SetLayoutInspect` (MMS: `layout.set_inspect(bool)`).
49    /// Static MMS declarations can also enable viz by attaching an
50    /// [`InspectLayoutComponent`](crate::engine::ecs::component::InspectLayoutComponent)
51    /// child to the LayoutRoot.
52    pub inspect: bool,
53
54    /// Computed total extent of this layout root's direct children, in world units,
55    /// populated after each successful layout pass. `None` before the first layout.
56    pub computed_size_wu: Option<(f32, f32)>,
57
58    component: Option<ComponentId>,
59}
60
61impl LayoutComponent {
62    pub fn new(available_width: f32) -> Self {
63        Self {
64            available_width,
65            authored_available_width: SizeDimension::GlyphUnits(available_width),
66            available_height: None,
67            authored_available_height: None,
68            dirty: true,
69            unit_scale: 1.0,
70            inspect: false,
71            computed_size_wu: None,
72            component: None,
73        }
74    }
75
76    fn resolve_layout_length_gu(length: SizeDimension, unit_scale: f32) -> f32 {
77        match length {
78            SizeDimension::GlyphUnits(v) => v,
79            SizeDimension::WorldUnits(v) => {
80                if unit_scale.abs() > f32::EPSILON {
81                    v / unit_scale
82                } else {
83                    v
84                }
85            }
86            SizeDimension::Auto | SizeDimension::Percent(_) => {
87                debug_assert!(false, "LayoutRoot sizes only support gu or wu units");
88                0.0
89            }
90        }
91    }
92
93    fn refresh_available_bounds(&mut self) {
94        self.available_width =
95            Self::resolve_layout_length_gu(self.authored_available_width, self.unit_scale);
96        self.available_height = self
97            .authored_available_height
98            .map(|height| Self::resolve_layout_length_gu(height, self.unit_scale));
99    }
100
101    pub fn with_height(mut self, h: f32) -> Self {
102        self.set_available_height(h);
103        self
104    }
105
106    pub fn with_unit_scale(mut self, scale: f32) -> Self {
107        self.set_unit_scale(scale);
108        self
109    }
110
111    /// Mark this layout root as needing a recompute.
112    pub fn mark_dirty(&mut self) {
113        self.dirty = true;
114    }
115
116    /// Update `available_width` and flag this root for recompute on the next tick.
117    pub fn set_available_width(&mut self, w: f32) {
118        self.set_available_width_dimension(SizeDimension::GlyphUnits(w));
119    }
120
121    pub fn set_available_width_dimension(&mut self, width: SizeDimension) {
122        self.authored_available_width = width;
123        self.refresh_available_bounds();
124        self.dirty = true;
125    }
126
127    pub fn set_available_height(&mut self, h: f32) {
128        self.set_available_height_dimension(SizeDimension::GlyphUnits(h));
129    }
130
131    pub fn set_available_height_dimension(&mut self, height: SizeDimension) {
132        self.authored_available_height = Some(height);
133        self.refresh_available_bounds();
134        self.dirty = true;
135    }
136
137    pub fn set_unit_scale(&mut self, scale: f32) {
138        self.unit_scale = scale;
139        self.refresh_available_bounds();
140        self.dirty = true;
141    }
142}
143
144impl Component for LayoutComponent {
145    fn name(&self) -> &'static str {
146        "layout"
147    }
148
149    fn set_id(&mut self, id: ComponentId) {
150        self.component = Some(id);
151    }
152
153    fn as_any(&self) -> &dyn std::any::Any {
154        self
155    }
156    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
157        self
158    }
159
160    fn to_mms_ast(
161        &self,
162        _world: &crate::engine::ecs::World,
163    ) -> crate::scripting::ast::ComponentExpression {
164        use crate::engine::ecs::component::ce_helpers::*;
165        use crate::scripting::ast::Expression;
166        use crate::scripting::token::Unit;
167
168        fn layout_dim_expr(sd: SizeDimension) -> Expression {
169            match sd {
170                SizeDimension::GlyphUnits(v) => Expression::Dimension(v as f64, Unit::GlyphUnits),
171                SizeDimension::WorldUnits(v) => Expression::Dimension(v as f64, Unit::WorldUnits),
172                SizeDimension::Percent(v) => Expression::Dimension(v as f64, Unit::Percent),
173                SizeDimension::Auto => num(0.0),
174            }
175        }
176
177        let mut ce = ce_call(
178            "LayoutRoot",
179            "width",
180            vec![layout_dim_expr(self.authored_available_width)],
181        );
182        if let Some(h) = self.authored_available_height {
183            ce = ce.with_call("height", vec![layout_dim_expr(h)]);
184        }
185        if (self.unit_scale - 1.0).abs() > f32::EPSILON {
186            ce = ce.with_call("unit_scale", nums([self.unit_scale as f64]));
187        }
188        ce
189    }
190}