Skip to main content

cssbox_core/
position.rs

1//! CSS positioning: relative, absolute, fixed, sticky.
2//!
3//! Implements CSS 2.1 §9.3 and css-position-3.
4
5use crate::box_model::BoxModel;
6use crate::fragment::Fragment;
7use crate::geometry::{Point, Size};
8use crate::style::{ComputedStyle, Position};
9use crate::tree::BoxTree;
10
11/// Resolve positioned elements after initial layout.
12///
13/// This walks the fragment tree and adjusts positions for:
14/// - `position: relative` — offset from normal flow position
15/// - `position: absolute` — positioned relative to containing block
16/// - `position: fixed` — positioned relative to viewport
17pub fn resolve_positioned(tree: &BoxTree, mut root: Fragment, viewport: Size) -> Fragment {
18    resolve_fragment(tree, &mut root, viewport, viewport);
19    root
20}
21
22fn resolve_fragment(
23    tree: &BoxTree,
24    fragment: &mut Fragment,
25    containing_block_size: Size,
26    viewport: Size,
27) {
28    let style = tree.style(fragment.node);
29
30    match style.position {
31        Position::Relative => {
32            apply_relative_offset(fragment, style, containing_block_size);
33        }
34        Position::Absolute => {
35            resolve_absolute_position(fragment, style, containing_block_size);
36        }
37        Position::Fixed => {
38            resolve_absolute_position(fragment, style, viewport);
39        }
40        Position::Sticky => {
41            // Sticky is treated like relative for initial layout
42            apply_relative_offset(fragment, style, containing_block_size);
43        }
44        Position::Static => {}
45    }
46
47    // Determine containing block for positioned descendants
48    let child_cb = if style.position.is_positioned() || style.position == Position::Static {
49        // If this element is positioned, it becomes the containing block for
50        // absolutely positioned descendants
51        Size::new(
52            fragment.size.width + fragment.padding.horizontal() + fragment.border.horizontal(),
53            fragment.size.height + fragment.padding.vertical() + fragment.border.vertical(),
54        )
55    } else {
56        containing_block_size
57    };
58
59    // Recurse into children
60    for child in &mut fragment.children {
61        resolve_fragment(tree, child, child_cb, viewport);
62    }
63}
64
65/// Apply relative positioning offsets.
66/// CSS 2.1 §9.4.3: Relative positioning offsets the box from its normal flow position.
67fn apply_relative_offset(fragment: &mut Fragment, style: &ComputedStyle, cb_size: Size) {
68    let dx = resolve_offset_pair(&style.left, &style.right, cb_size.width);
69    let dy = resolve_offset_pair(&style.top, &style.bottom, cb_size.height);
70
71    fragment.position.x += dx;
72    fragment.position.y += dy;
73}
74
75/// Resolve absolute positioning.
76/// CSS 2.1 §10.3.7 and §10.6.4: Absolutely positioned, non-replaced elements.
77fn resolve_absolute_position(fragment: &mut Fragment, style: &ComputedStyle, cb_size: Size) {
78    let border = BoxModel::resolve_border(style);
79    let padding = BoxModel::resolve_padding(style, cb_size.width);
80
81    // Resolve horizontal position and width
82    let (x, width) = resolve_absolute_axis(
83        &style.left,
84        &style.right,
85        &style.width,
86        &style.margin_left,
87        &style.margin_right,
88        border.left + padding.left,
89        border.right + padding.right,
90        cb_size.width,
91        fragment.size.width,
92    );
93
94    // Resolve vertical position and height
95    let (y, height) = resolve_absolute_axis(
96        &style.top,
97        &style.bottom,
98        &style.height,
99        &style.margin_top,
100        &style.margin_bottom,
101        border.top + padding.top,
102        border.bottom + padding.bottom,
103        cb_size.height,
104        fragment.size.height,
105    );
106
107    fragment.position = Point::new(x, y);
108    if width >= 0.0 {
109        fragment.size.width = width;
110    }
111    if height >= 0.0 {
112        fragment.size.height = height;
113    }
114    fragment.border = border;
115    fragment.padding = padding;
116}
117
118/// Resolve one axis of absolute positioning.
119///
120/// The constraint equation:
121/// start + margin_start + border_start + padding_start + width +
122/// padding_end + border_end + margin_end + end = containing_block_size
123///
124/// Returns (position, content_size). Content_size is -1 if unchanged.
125fn resolve_absolute_axis(
126    start: &crate::values::LengthPercentageAuto,
127    end: &crate::values::LengthPercentageAuto,
128    size: &crate::values::LengthPercentageAuto,
129    margin_start: &crate::values::LengthPercentageAuto,
130    margin_end: &crate::values::LengthPercentageAuto,
131    border_padding_start: f32,
132    border_padding_end: f32,
133    cb_size: f32,
134    intrinsic_size: f32,
135) -> (f32, f32) {
136    let start_val = start.resolve(cb_size);
137    let end_val = end.resolve(cb_size);
138    let size_val = size.resolve(cb_size);
139    let ms = margin_start.resolve(cb_size).unwrap_or(0.0);
140    let me = margin_end.resolve(cb_size).unwrap_or(0.0);
141
142    match (start_val, end_val, size_val) {
143        // All three specified: over-constrained, ignore end
144        (Some(s), Some(_e), Some(w)) => {
145            let pos = s + ms;
146            (pos, w)
147        }
148        // Start and size specified
149        (Some(s), _, Some(w)) => {
150            let pos = s + ms;
151            (pos, w)
152        }
153        // End and size specified
154        (_, Some(e), Some(w)) => {
155            let pos = cb_size - e - me - w - border_padding_start - border_padding_end;
156            (pos, w)
157        }
158        // Start and end specified (stretch)
159        (Some(s), Some(e), None) => {
160            let pos = s + ms;
161            let w = cb_size - s - e - ms - me - border_padding_start - border_padding_end;
162            (pos, w.max(0.0))
163        }
164        // Only start specified
165        (Some(s), None, None) => {
166            let pos = s + ms;
167            (pos, intrinsic_size)
168        }
169        // Only end specified
170        (None, Some(e), None) => {
171            let pos = cb_size - e - me - intrinsic_size - border_padding_start - border_padding_end;
172            (pos, intrinsic_size)
173        }
174        // Only size specified: use static position
175        (None, None, Some(w)) => (ms, w),
176        // Nothing specified: use static position and intrinsic size
177        (None, None, None) => (ms, intrinsic_size),
178    }
179}
180
181/// Resolve an offset pair (e.g., left/right or top/bottom) for relative positioning.
182/// If both are specified, the start value wins (per CSS 2.1).
183fn resolve_offset_pair(
184    start: &crate::values::LengthPercentageAuto,
185    end: &crate::values::LengthPercentageAuto,
186    reference: f32,
187) -> f32 {
188    let s = start.resolve(reference);
189    let e = end.resolve(reference);
190
191    match (s, e) {
192        (Some(sv), _) => sv,     // start wins
193        (None, Some(ev)) => -ev, // end is negated
194        (None, None) => 0.0,
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::values::LengthPercentageAuto;
202
203    #[test]
204    fn test_relative_offset_left() {
205        let left = LengthPercentageAuto::px(10.0);
206        let right = LengthPercentageAuto::Auto;
207        assert_eq!(resolve_offset_pair(&left, &right, 800.0), 10.0);
208    }
209
210    #[test]
211    fn test_relative_offset_right() {
212        let left = LengthPercentageAuto::Auto;
213        let right = LengthPercentageAuto::px(10.0);
214        assert_eq!(resolve_offset_pair(&left, &right, 800.0), -10.0);
215    }
216
217    #[test]
218    fn test_relative_offset_both_start_wins() {
219        let left = LengthPercentageAuto::px(20.0);
220        let right = LengthPercentageAuto::px(10.0);
221        assert_eq!(resolve_offset_pair(&left, &right, 800.0), 20.0);
222    }
223
224    #[test]
225    fn test_absolute_position_left_top() {
226        let (x, w) = resolve_absolute_axis(
227            &LengthPercentageAuto::px(10.0),
228            &LengthPercentageAuto::Auto,
229            &LengthPercentageAuto::px(200.0),
230            &LengthPercentageAuto::px(0.0),
231            &LengthPercentageAuto::px(0.0),
232            0.0,
233            0.0,
234            800.0,
235            0.0,
236        );
237        assert_eq!(x, 10.0);
238        assert_eq!(w, 200.0);
239    }
240
241    #[test]
242    fn test_absolute_position_stretch() {
243        let (x, w) = resolve_absolute_axis(
244            &LengthPercentageAuto::px(10.0),
245            &LengthPercentageAuto::px(10.0),
246            &LengthPercentageAuto::Auto,
247            &LengthPercentageAuto::px(0.0),
248            &LengthPercentageAuto::px(0.0),
249            0.0,
250            0.0,
251            800.0,
252            0.0,
253        );
254        assert_eq!(x, 10.0);
255        assert_eq!(w, 780.0); // 800 - 10 - 10
256    }
257}