mittens_engine/engine/ecs/system/layout/
mod.rs1pub mod block;
2pub mod box_model_viz;
3pub mod flex;
4pub mod inline;
5pub mod measure;
6
7use crate::engine::ecs::ComponentId;
8use crate::engine::ecs::EventSignal;
9use crate::engine::ecs::SignalEmitter;
10use crate::engine::ecs::World;
11use crate::engine::ecs::component::LayoutComponent;
12use crate::engine::ecs::component::style::Display;
13use crate::engine::ecs::component::{HtmlElementComponent, StyleComponent};
14use measure::{MeasuredItem, measure_container_items};
15
16const CHAR_WIDTH_GLYPH: f32 = 0.55;
18const DEFAULT_PANEL_WIDTH_CHARS: usize = 40;
20
21pub(crate) const LAYER_DISTANCE: f32 = 0.05;
29
30pub(crate) const AUTO_TEXT_LIFT_Z: f32 = 0.4 * LAYER_DISTANCE;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub(crate) enum FormattingContext {
39 Block,
40 Inline,
41 Flex,
42}
43
44pub(crate) fn formatting_context_for_container(
45 world: &World,
46 container_id: ComponentId,
47 items: &[MeasuredItem],
48) -> FormattingContext {
49 if matches!(container_display(world, container_id), Some(Display::Flex)) {
50 return FormattingContext::Flex;
51 }
52
53 let all_inline_block = !items.is_empty()
54 && items
55 .iter()
56 .all(|it| matches!(it.display, Some(Display::InlineBlock | Display::Inline)));
57
58 if all_inline_block {
59 FormattingContext::Inline
60 } else {
61 FormattingContext::Block
62 }
63}
64
65fn container_display(world: &World, container_id: ComponentId) -> Option<Display> {
66 let children = world.children_of(container_id);
67 let style_display = children.iter().find_map(|&child| {
68 world
69 .get_component_by_id_as::<StyleComponent>(child)
70 .and_then(|style| style.display)
71 });
72 if style_display.is_some() {
73 return style_display;
74 }
75 children.iter().find_map(|&child| {
76 world
77 .get_component_by_id_as::<HtmlElementComponent>(child)
78 .and_then(|el| el.element_type.default_display())
79 })
80}
81
82pub(crate) fn layout_container_items(
83 world: &mut World,
84 emit: &mut dyn SignalEmitter,
85 container_id: ComponentId,
86 items: &[MeasuredItem],
87 avail_w_gu: f32,
88 avail_h_gu: Option<f32>,
89 unit_scale: f32,
90 axis_scales: (f32, f32),
91 depth: i32,
92 parent_depth: i32,
93 viz: bool,
94) -> (f32, f32) {
95 match formatting_context_for_container(world, container_id, items) {
96 FormattingContext::Block => {
97 block::layout_items_for(
98 world,
99 emit,
100 items,
101 unit_scale,
102 axis_scales,
103 depth,
104 parent_depth,
105 viz,
106 );
107 let height = items.iter().map(|i| i.margin_box_height_gu).sum();
108 (avail_w_gu, height)
109 }
110 FormattingContext::Inline => inline::layout_items(
111 world,
112 emit,
113 items,
114 avail_w_gu,
115 unit_scale,
116 axis_scales,
117 depth,
118 parent_depth,
119 viz,
120 ),
121 FormattingContext::Flex => flex::layout_items(
122 world,
123 emit,
124 container_id,
125 items,
126 avail_w_gu,
127 avail_h_gu,
128 unit_scale,
129 axis_scales,
130 depth,
131 parent_depth,
132 viz,
133 ),
134 }
135}
136
137#[derive(Debug, Default)]
144pub struct LayoutSystem;
145
146impl LayoutSystem {
147 pub fn new() -> Self {
148 Self
149 }
150
151 pub fn tick(&mut self, world: &mut World, emit: &mut dyn SignalEmitter) {
153 let dirty: Vec<ComponentId> = world
154 .all_components()
155 .filter(|&id| {
156 world
157 .get_component_by_id_as::<LayoutComponent>(id)
158 .map(|l| l.dirty)
159 .unwrap_or(false)
160 })
161 .collect();
162
163 for &layout_id in &dirty {
164 let (width_gu, height_gu) = Self::run_layout(world, emit, layout_id);
165 let unit_scale = world
166 .get_component_by_id_as::<LayoutComponent>(layout_id)
167 .map(|lc| lc.unit_scale)
168 .unwrap_or(1.0);
169 let size_wu = (width_gu * unit_scale, height_gu * unit_scale);
170
171 if let Some(lc) = world.get_component_by_id_as_mut::<LayoutComponent>(layout_id) {
172 lc.computed_size_wu = Some(size_wu);
173 }
174
175 emit.push_event(
176 layout_id,
177 EventSignal::LayoutRootSizeAvailable {
178 layout_id,
179 width_wu: size_wu.0,
180 height_wu: size_wu.1,
181 },
182 );
183 }
184
185 for layout_id in dirty {
186 if let Some(lc) = world.get_component_by_id_as_mut::<LayoutComponent>(layout_id) {
187 lc.dirty = false;
188 }
189 }
190 }
191
192 fn run_layout(
200 world: &mut World,
201 emit: &mut dyn SignalEmitter,
202 layout_id: ComponentId,
203 ) -> (f32, f32) {
204 if world
206 .get_component_by_id_as::<LayoutComponent>(layout_id)
207 .is_none()
208 {
209 return (0.0, 0.0);
210 }
211
212 let (avail_w, avail_h, unit_scale) =
213 measure::layout_root_available_bounds(world, layout_id);
214 let items = measure_container_items(world, layout_id, avail_w, avail_h, unit_scale);
215 let viz = block::layout_root_has_inspect(world, layout_id);
216 let axis_scales = measure::layout_root_axis_scales(world, layout_id);
217 layout_container_items(
218 world,
219 emit,
220 layout_id,
221 &items,
222 avail_w,
223 avail_h,
224 unit_scale,
225 axis_scales,
226 0,
227 0,
228 viz,
229 )
230 }
231
232 pub fn estimate_panel_width(max_chars: usize, text_scale: f32, indent_width: f32) -> f32 {
235 let panel_chars = if max_chars == 0 {
236 DEFAULT_PANEL_WIDTH_CHARS
237 } else {
238 max_chars
239 };
240 indent_width + panel_chars as f32 * CHAR_WIDTH_GLYPH * text_scale
241 }
242
243 pub fn default_panel_width_chars() -> usize {
244 DEFAULT_PANEL_WIDTH_CHARS
245 }
246}