Skip to content

Commit a0a8529

Browse files
committed
feat(ie-layout): implement CSS positioned layout #73
Positioned layout as post-pass after normal flow (positioned.rs): - position: relative — offset from normal position via top/left/bottom/right - position: absolute — relative to nearest positioned ancestor or viewport - position: fixed — relative to viewport - find_containing_block walks arena tree to find positioned ancestor - top/left take precedence over bottom/right per spec 3 new tests: relative offset, fixed to viewport, static default. 18 total ie-layout tests.
1 parent 61fdfc6 commit a0a8529

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

crates/ie-layout/src/lib.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
pub mod block;
77
pub mod box_generation;
88
pub mod inline;
9+
pub mod positioned;
910
pub mod text_measure;
1011

1112
use ie_css::resolve::ResolvedStyle;
@@ -65,6 +66,7 @@ pub fn layout(
6566
if let Some(root) = tree.root {
6667
block::layout_block(root, &mut tree, styles, viewport.width, 0.0, text_measure);
6768
}
69+
positioned::apply_positioned(&mut tree, styles, viewport);
6870
tree
6971
}
7072

@@ -308,4 +310,47 @@ mod tests {
308310
);
309311
}
310312
}
313+
314+
#[test]
315+
fn position_relative_offset() {
316+
let tree = layout_html_with_css(
317+
"<div id='box'>content</div>",
318+
"#box { position: relative; top: 10px; left: 20px; }",
319+
);
320+
// The box should be offset from its normal position
321+
let has_offset = tree
322+
.boxes
323+
.iter()
324+
.any(|b| b.content_rect.x >= 20.0 && b.content_rect.y >= 10.0);
325+
assert!(has_offset, "relative positioned box should be offset");
326+
}
327+
328+
#[test]
329+
fn position_fixed_to_viewport() {
330+
let tree = layout_html_with_css(
331+
"<div id='fixed'>fixed</div>",
332+
"#fixed { position: fixed; top: 0px; left: 0px; width: 100px; }",
333+
);
334+
let fixed = tree
335+
.boxes
336+
.iter()
337+
.find(|b| (b.content_rect.width - 100.0).abs() < 0.1);
338+
if let Some(b) = fixed {
339+
assert!(
340+
(b.content_rect.x).abs() < 1.0,
341+
"fixed box should be at viewport left"
342+
);
343+
assert!(
344+
(b.content_rect.y).abs() < 1.0,
345+
"fixed box should be at viewport top"
346+
);
347+
}
348+
}
349+
350+
#[test]
351+
fn position_static_is_default() {
352+
let tree = layout_html("<div>normal flow</div>");
353+
// Should just work normally, no special positioning
354+
assert!(tree.root.is_some());
355+
}
311356
}

crates/ie-layout/src/positioned.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
use ie_css::resolve::ResolvedStyle;
2+
use ie_css::values::{CssValue, LengthUnit, PropertyId};
3+
4+
use crate::{LayoutTree, Rect};
5+
6+
/// Apply CSS positioned layout as a post-pass after normal flow.
7+
pub fn apply_positioned(tree: &mut LayoutTree, styles: &[ResolvedStyle], viewport: Rect) {
8+
let count = tree.boxes.len();
9+
for i in 0..count {
10+
let node_id = match tree.boxes[i].node_id {
11+
Some(id) => id,
12+
None => continue,
13+
};
14+
let style = match styles.get(node_id) {
15+
Some(s) => s,
16+
None => continue,
17+
};
18+
let position = style
19+
.get(PropertyId::Position)
20+
.and_then(|v| match v {
21+
CssValue::Keyword(k) => Some(k.as_str()),
22+
_ => None,
23+
})
24+
.unwrap_or("static");
25+
26+
match position {
27+
"relative" => apply_relative(i, tree, style),
28+
"absolute" => apply_absolute(i, tree, styles, viewport),
29+
"fixed" => apply_fixed(i, tree, style, viewport),
30+
_ => {} // static: nothing to do
31+
}
32+
}
33+
}
34+
35+
fn apply_relative(box_idx: usize, tree: &mut LayoutTree, style: &ResolvedStyle) {
36+
let top = get_offset(style, PropertyId::Top);
37+
let left = get_offset(style, PropertyId::Left);
38+
let bottom = get_offset(style, PropertyId::Bottom);
39+
let right = get_offset(style, PropertyId::Right);
40+
41+
// top takes precedence over bottom, left over right
42+
let dx = if left != 0.0 { left } else { -right };
43+
let dy = if top != 0.0 { top } else { -bottom };
44+
45+
tree.boxes[box_idx].content_rect.x += dx;
46+
tree.boxes[box_idx].content_rect.y += dy;
47+
}
48+
49+
fn apply_absolute(box_idx: usize, tree: &mut LayoutTree, styles: &[ResolvedStyle], viewport: Rect) {
50+
let node_id = match tree.boxes[box_idx].node_id {
51+
Some(id) => id,
52+
None => return,
53+
};
54+
let style = match styles.get(node_id) {
55+
Some(s) => s,
56+
None => return,
57+
};
58+
59+
// Find containing block: nearest positioned ancestor, or viewport
60+
let containing = find_containing_block(box_idx, tree, styles).unwrap_or(viewport);
61+
62+
let top = get_offset_option(style, PropertyId::Top);
63+
let left = get_offset_option(style, PropertyId::Left);
64+
let bottom = get_offset_option(style, PropertyId::Bottom);
65+
let right = get_offset_option(style, PropertyId::Right);
66+
67+
if let Some(t) = top {
68+
tree.boxes[box_idx].content_rect.y = containing.y + t;
69+
} else if let Some(b) = bottom {
70+
tree.boxes[box_idx].content_rect.y =
71+
containing.y + containing.height - tree.boxes[box_idx].content_rect.height - b;
72+
}
73+
74+
if let Some(l) = left {
75+
tree.boxes[box_idx].content_rect.x = containing.x + l;
76+
} else if let Some(r) = right {
77+
tree.boxes[box_idx].content_rect.x =
78+
containing.x + containing.width - tree.boxes[box_idx].content_rect.width - r;
79+
}
80+
}
81+
82+
fn apply_fixed(box_idx: usize, tree: &mut LayoutTree, style: &ResolvedStyle, viewport: Rect) {
83+
let top = get_offset_option(style, PropertyId::Top);
84+
let left = get_offset_option(style, PropertyId::Left);
85+
let bottom = get_offset_option(style, PropertyId::Bottom);
86+
let right = get_offset_option(style, PropertyId::Right);
87+
88+
if let Some(t) = top {
89+
tree.boxes[box_idx].content_rect.y = viewport.y + t;
90+
} else if let Some(b) = bottom {
91+
tree.boxes[box_idx].content_rect.y =
92+
viewport.y + viewport.height - tree.boxes[box_idx].content_rect.height - b;
93+
}
94+
95+
if let Some(l) = left {
96+
tree.boxes[box_idx].content_rect.x = viewport.x + l;
97+
} else if let Some(r) = right {
98+
tree.boxes[box_idx].content_rect.x =
99+
viewport.x + viewport.width - tree.boxes[box_idx].content_rect.width - r;
100+
}
101+
}
102+
103+
fn find_containing_block(
104+
box_idx: usize,
105+
tree: &LayoutTree,
106+
styles: &[ResolvedStyle],
107+
) -> Option<Rect> {
108+
// Walk up the tree to find nearest positioned ancestor
109+
for i in (0..box_idx).rev() {
110+
if tree.boxes[i].children.contains(&box_idx) {
111+
if let Some(node_id) = tree.boxes[i].node_id
112+
&& let Some(style) = styles.get(node_id)
113+
{
114+
let pos = style
115+
.get(PropertyId::Position)
116+
.and_then(|v| match v {
117+
CssValue::Keyword(k) => Some(k.as_str()),
118+
_ => None,
119+
})
120+
.unwrap_or("static");
121+
if pos != "static" {
122+
return Some(tree.boxes[i].content_rect);
123+
}
124+
}
125+
// Continue searching upward from this parent
126+
return find_containing_block(i, tree, styles);
127+
}
128+
}
129+
None
130+
}
131+
132+
fn get_offset(style: &ResolvedStyle, prop: PropertyId) -> f32 {
133+
match style.get(prop) {
134+
Some(CssValue::Length(v, LengthUnit::Px)) => *v as f32,
135+
Some(CssValue::Number(v)) => *v as f32,
136+
_ => 0.0,
137+
}
138+
}
139+
140+
fn get_offset_option(style: &ResolvedStyle, prop: PropertyId) -> Option<f32> {
141+
match style.get(prop) {
142+
Some(CssValue::Length(v, LengthUnit::Px)) => Some(*v as f32),
143+
Some(CssValue::Number(v)) => Some(*v as f32),
144+
Some(CssValue::Auto) | None => None,
145+
_ => None,
146+
}
147+
}

0 commit comments

Comments
 (0)