Skip to content

Commit a06d21b

Browse files
committed
feat(ie-layout): implement box generation and block layout #71
Box generation (box_generation.rs): - Walk styled DOM, create LayoutBox for each visible element/text - BoxType: Block, Inline, InlineBlock, Anonymous, Text - Skip display:none elements - Anonymous block insertion for mixed block+inline children - Resolve padding/border/margin from ResolvedStyle to EdgeSizes Block layout (block.rs): - CSS 2.1 width algorithm: auto width fills container, explicit width, auto margin centering (margin: 0 auto) - box-sizing: content-box vs border-box - min-width/max-width clamping - Height: auto (sum of children) or explicit with min/max - Margin collapsing between adjacent siblings (larger wins) - Recursive child layout with text measurement Infrastructure: - TextMeasure trait + MockTextMeasure (0.5em/char, height=font_size) - LayoutTree arena (Vec<LayoutBox> indexed by usize) - layout() entry point: Document + styles + viewport + TextMeasure → LayoutTree 10 tests: viewport fill, explicit width, auto margins centering, nested blocks, display none, height auto, tree generation, margin collapsing, text measurement, box-sizing.
1 parent 46f5381 commit a06d21b

6 files changed

Lines changed: 648 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ie-layout/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ thiserror.workspace = true
1313
tracing.workspace = true
1414
ie-dom.workspace = true
1515
ie-css.workspace = true
16+
17+
[dev-dependencies]
18+
ie-html.workspace = true

crates/ie-layout/src/block.rs

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
use ie_css::CssValue;
2+
use ie_css::resolve::ResolvedStyle;
3+
use ie_css::values::PropertyId;
4+
5+
use crate::box_generation::{get_px, get_px_or, is_auto};
6+
use crate::text_measure::TextMeasure;
7+
use crate::{BoxType, LayoutTree, Rect};
8+
9+
/// Lay out a block-level box and all its descendants.
10+
pub fn layout_block(
11+
box_idx: usize,
12+
tree: &mut LayoutTree,
13+
styles: &[ResolvedStyle],
14+
containing_width: f32,
15+
offset_y: f32,
16+
text_measure: &dyn TextMeasure,
17+
) {
18+
let (node_id, _box_type) = {
19+
let b = &tree.boxes[box_idx];
20+
(b.node_id, b.box_type.clone())
21+
};
22+
23+
let style = node_id.and_then(|id| styles.get(id));
24+
25+
// --- 1. Compute width ---
26+
let margin_left;
27+
let margin_right;
28+
let padding_left;
29+
let padding_right;
30+
let border_left;
31+
let border_right;
32+
let content_width;
33+
34+
if let Some(style) = style {
35+
padding_left = get_px(style, PropertyId::PaddingLeft);
36+
padding_right = get_px(style, PropertyId::PaddingRight);
37+
border_left = get_px(style, PropertyId::BorderLeftWidth);
38+
border_right = get_px(style, PropertyId::BorderRightWidth);
39+
40+
let is_border_box = style
41+
.get(PropertyId::BoxSizing)
42+
.is_some_and(|v| matches!(v, CssValue::Keyword(k) if k == "border-box"));
43+
44+
let specified_width = if is_auto(style, PropertyId::Width) {
45+
None
46+
} else {
47+
Some(get_px(style, PropertyId::Width))
48+
};
49+
50+
let auto_margin_left = is_auto(style, PropertyId::MarginLeft);
51+
let auto_margin_right = is_auto(style, PropertyId::MarginRight);
52+
53+
let non_content = padding_left + padding_right + border_left + border_right;
54+
55+
match specified_width {
56+
Some(w) => {
57+
let box_width = if is_border_box { w } else { w + non_content };
58+
let remaining = containing_width - box_width;
59+
if auto_margin_left && auto_margin_right {
60+
let m = (remaining / 2.0).max(0.0);
61+
margin_left = m;
62+
margin_right = m;
63+
} else if auto_margin_left {
64+
margin_right = get_px(style, PropertyId::MarginRight);
65+
margin_left = (remaining - margin_right).max(0.0);
66+
} else if auto_margin_right {
67+
margin_left = get_px(style, PropertyId::MarginLeft);
68+
margin_right = (remaining - margin_left).max(0.0);
69+
} else {
70+
margin_left = get_px(style, PropertyId::MarginLeft);
71+
margin_right = get_px(style, PropertyId::MarginRight);
72+
}
73+
content_width = if is_border_box {
74+
(w - padding_left - padding_right - border_left - border_right).max(0.0)
75+
} else {
76+
w
77+
};
78+
}
79+
None => {
80+
margin_left = if auto_margin_left {
81+
0.0
82+
} else {
83+
get_px(style, PropertyId::MarginLeft)
84+
};
85+
margin_right = if auto_margin_right {
86+
0.0
87+
} else {
88+
get_px(style, PropertyId::MarginRight)
89+
};
90+
content_width =
91+
(containing_width - margin_left - margin_right - non_content).max(0.0);
92+
}
93+
}
94+
} else {
95+
// Anonymous box — takes full containing width
96+
margin_left = 0.0;
97+
margin_right = 0.0;
98+
padding_left = 0.0;
99+
padding_right = 0.0;
100+
border_left = 0.0;
101+
border_right = 0.0;
102+
content_width = containing_width;
103+
}
104+
105+
// Apply min/max width constraints
106+
let content_width = if let Some(style) = style {
107+
let min_w = get_px(style, PropertyId::MinWidth);
108+
let max_w = get_px_or(style, PropertyId::MaxWidth, f32::MAX);
109+
content_width.max(min_w).min(max_w)
110+
} else {
111+
content_width
112+
};
113+
114+
// Set box position and dimensions
115+
let x = margin_left + padding_left + border_left;
116+
let y = offset_y
117+
+ tree.boxes[box_idx].margin.top
118+
+ tree.boxes[box_idx].padding.top
119+
+ tree.boxes[box_idx].border.top;
120+
121+
tree.boxes[box_idx].content_rect.x = x;
122+
tree.boxes[box_idx].content_rect.y = y;
123+
tree.boxes[box_idx].content_rect.width = content_width;
124+
tree.boxes[box_idx].margin.left = margin_left;
125+
tree.boxes[box_idx].margin.right = margin_right;
126+
tree.boxes[box_idx].padding.left = padding_left;
127+
tree.boxes[box_idx].padding.right = padding_right;
128+
tree.boxes[box_idx].border.left = border_left;
129+
tree.boxes[box_idx].border.right = border_right;
130+
131+
// --- 2. Layout children ---
132+
let children = tree.boxes[box_idx].children.clone();
133+
let content_y = tree.boxes[box_idx].content_rect.y;
134+
let mut child_y = content_y;
135+
let mut prev_margin_bottom: f32 = 0.0;
136+
137+
for (i, &child_idx) in children.iter().enumerate() {
138+
// Margin collapsing between siblings
139+
let child_margin_top = tree.boxes[child_idx].margin.top;
140+
if i > 0 {
141+
let collapsed = prev_margin_bottom.max(child_margin_top);
142+
child_y -= prev_margin_bottom; // undo previous margin
143+
child_y += collapsed; // apply collapsed margin
144+
} else {
145+
child_y += child_margin_top;
146+
}
147+
148+
match &tree.boxes[child_idx].box_type {
149+
BoxType::Block | BoxType::Anonymous => {
150+
layout_block(
151+
child_idx,
152+
tree,
153+
styles,
154+
content_width,
155+
child_y,
156+
text_measure,
157+
);
158+
}
159+
BoxType::Text(text) => {
160+
// Text nodes don't have resolved styles; inherit from parent
161+
let font_size = node_id
162+
.and_then(|id| styles.get(id))
163+
.map(|s| get_px(s, PropertyId::FontSize))
164+
.filter(|&v| v > 0.0)
165+
.unwrap_or(16.0);
166+
let text = text.clone();
167+
let metrics = text_measure.measure(&text, font_size);
168+
tree.boxes[child_idx].content_rect = Rect {
169+
x: tree.boxes[box_idx].content_rect.x,
170+
y: child_y,
171+
width: metrics.width,
172+
height: metrics.height,
173+
};
174+
}
175+
BoxType::Inline | BoxType::InlineBlock => {
176+
// Simplified: treat as block for now
177+
layout_block(
178+
child_idx,
179+
tree,
180+
styles,
181+
content_width,
182+
child_y,
183+
text_measure,
184+
);
185+
}
186+
}
187+
188+
let child_height = tree.boxes[child_idx].content_rect.height
189+
+ tree.boxes[child_idx].padding.top
190+
+ tree.boxes[child_idx].padding.bottom
191+
+ tree.boxes[child_idx].border.top
192+
+ tree.boxes[child_idx].border.bottom;
193+
child_y += child_height;
194+
prev_margin_bottom = tree.boxes[child_idx].margin.bottom;
195+
}
196+
197+
child_y += prev_margin_bottom; // final child's bottom margin
198+
199+
// --- 3. Compute height ---
200+
let auto_height = child_y - content_y;
201+
let content_height = if let Some(style) = style {
202+
if is_auto(style, PropertyId::Height) {
203+
auto_height
204+
} else {
205+
let h = get_px(style, PropertyId::Height);
206+
let min_h = get_px(style, PropertyId::MinHeight);
207+
let max_h = get_px_or(style, PropertyId::MaxHeight, f32::MAX);
208+
h.max(min_h).min(max_h)
209+
}
210+
} else {
211+
auto_height
212+
};
213+
214+
tree.boxes[box_idx].content_rect.height = content_height;
215+
}

0 commit comments

Comments
 (0)