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