Skip to main content

azul_layout/solver3/
positioning.rs

1//! solver3/positioning.rs
2//! Pass 3: Final positioning of layout nodes
3
4use std::collections::BTreeMap;
5
6use azul_core::{
7    dom::NodeId,
8    geom::{LogicalPosition, LogicalRect, LogicalSize},
9    hit_test::ScrollPosition,
10    resources::RendererResources,
11    styled_dom::StyledDom,
12};
13use azul_css::{
14    corety::LayoutDebugMessage,
15    css::CssPropertyValue,
16    props::{
17        basic::pixel::PixelValue,
18        layout::{LayoutPosition, LayoutWritingMode},
19        property::{CssProperty, CssPropertyType},
20    },
21};
22
23use crate::{
24    font_traits::{FontLoaderTrait, ParsedFontTrait},
25    solver3::{
26        fc::{layout_formatting_context, LayoutConstraints, TextAlign},
27        getters::{
28            get_direction_property, get_writing_mode, get_position, MultiValue,
29            get_css_top, get_css_bottom, get_css_left, get_css_right,
30        },
31        layout_tree::LayoutTree,
32        LayoutContext, LayoutError, Result,
33    },
34};
35
36#[derive(Debug, Default)]
37struct PositionOffsets {
38    top: Option<f32>,
39    right: Option<f32>,
40    bottom: Option<f32>,
41    left: Option<f32>,
42}
43
44/// Looks up the `position` property using the compact-cache-aware getter.
45pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
46    let Some(id) = dom_id else {
47        return LayoutPosition::Static;
48    };
49    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
50    get_position(styled_dom, id, node_state).unwrap_or_default()
51}
52
53/// Correctly looks up the `position` property from the styled DOM.
54fn get_position_property(styled_dom: &StyledDom, node_id: NodeId) -> LayoutPosition {
55    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
56    get_position(styled_dom, node_id, node_state).unwrap_or(LayoutPosition::Static)
57}
58
59/// **NEW API:** Correctly reads and resolves `top`, `right`, `bottom`, `left` properties,
60/// including percentages relative to the containing block's size, and em/rem units.
61/// Uses the modern resolve_with_context() API.
62fn resolve_position_offsets(
63    styled_dom: &StyledDom,
64    dom_id: Option<NodeId>,
65    cb_size: LogicalSize,
66) -> PositionOffsets {
67    use azul_css::props::basic::pixel::{PhysicalSize, PropertyContext, ResolutionContext};
68
69    use crate::solver3::getters::{
70        get_element_font_size, get_parent_font_size, get_root_font_size,
71    };
72
73    let Some(id) = dom_id else {
74        return PositionOffsets::default();
75    };
76    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
77
78    // Create resolution context with font sizes and containing block size
79    let element_font_size = get_element_font_size(styled_dom, id, node_state);
80    let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
81    let root_font_size = get_root_font_size(styled_dom, node_state);
82
83    let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);
84
85    let resolution_context = ResolutionContext {
86        element_font_size,
87        parent_font_size,
88        root_font_size,
89        containing_block_size,
90        element_size: None, // Not needed for position offsets
91        viewport_size: PhysicalSize::new(0.0, 0.0),
92    };
93
94    let mut offsets = PositionOffsets::default();
95
96    // Resolve offsets using compact-cache-aware getters
97    // top/bottom use Height context (% refers to containing block height)
98    offsets.top = match get_css_top(styled_dom, id, node_state) {
99        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
100        _ => None,
101    };
102
103    offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
104        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
105        _ => None,
106    };
107
108    // left/right use Width context (% refers to containing block width)
109    offsets.left = match get_css_left(styled_dom, id, node_state) {
110        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
111        _ => None,
112    };
113
114    offsets.right = match get_css_right(styled_dom, id, node_state) {
115        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
116        _ => None,
117    };
118
119    offsets
120}
121
122/// After the main layout pass, this function iterates through the tree and correctly
123/// calculates the final positions of out-of-flow elements (`absolute`, `fixed`).
124pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
125    ctx: &mut LayoutContext<'_, T>,
126    tree: &mut LayoutTree,
127    calculated_positions: &mut super::PositionVec,
128    viewport: LogicalRect,
129) -> Result<()> {
130    for node_index in 0..tree.nodes.len() {
131        let node = &tree.nodes[node_index];
132        let dom_id = match node.dom_node_id {
133            Some(id) => id,
134            None => continue,
135        };
136
137        let position_type = get_position_type(ctx.styled_dom, Some(dom_id));
138
139        if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
140            // Get parent info before any mutable borrows
141            let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
142                let node = &tree.nodes[node_index];
143                node.parent.and_then(|parent_idx| {
144                    let parent_node = tree.get(parent_idx)?;
145                    let parent_dom_id = parent_node.dom_node_id?;
146                    let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
147                    if parent_position == LayoutPosition::Absolute
148                        || parent_position == LayoutPosition::Fixed
149                    {
150                        calculated_positions.get(parent_idx).map(|parent_pos| {
151                            (
152                                parent_idx,
153                                *parent_pos,
154                                parent_node.box_props.border.left,
155                                parent_node.box_props.border.top,
156                                parent_node.box_props.padding.left,
157                                parent_node.box_props.padding.top,
158                            )
159                        })
160                    } else {
161                        None
162                    }
163                })
164            };
165
166            // Determine containing block FIRST (before calculating size)
167            let containing_block_rect = if position_type == LayoutPosition::Fixed {
168                viewport
169            } else {
170                find_absolute_containing_block_rect(
171                    tree,
172                    node_index,
173                    ctx.styled_dom,
174                    calculated_positions,
175                    viewport,
176                )?
177            };
178
179            // Get node again after containing block calculation
180            let node = &tree.nodes[node_index];
181
182            // Calculate used size for out-of-flow elements (they don't get sized during normal
183            // layout)
184            let element_size = if let Some(size) = node.used_size {
185                size
186            } else {
187                // Element hasn't been sized yet - calculate it now using containing block
188                let intrinsic = node.intrinsic_sizes.unwrap_or_default();
189                let size = crate::solver3::sizing::calculate_used_size_for_node(
190                    ctx.styled_dom,
191                    Some(dom_id),
192                    containing_block_rect.size,
193                    intrinsic,
194                    &node.box_props,
195                    ctx.viewport_size,
196                )?;
197
198                // Store the calculated size in the tree node
199                if let Some(node_mut) = tree.get_mut(node_index) {
200                    node_mut.used_size = Some(size);
201                }
202
203                size
204            };
205
206            // Resolve offsets using the now-known containing block size.
207            let offsets =
208                resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size);
209
210            let mut static_pos = calculated_positions
211                .get(node_index)
212                .copied()
213                .unwrap_or_default();
214
215            // Special case: If this is a fixed-position element and it has a positioned
216            // parent, update static_pos to be relative to the parent's final absolute
217            // position (content-box). The initial static_pos from process_out_of_flow_children
218            // may include border/padding offsets, so we must always recalculate here.
219            if position_type == LayoutPosition::Fixed {
220                if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
221                    parent_info
222                {
223                    // Add parent's border and padding to get content-box position
224                    static_pos = LogicalPosition::new(
225                        parent_pos.x + border_left + padding_left,
226                        parent_pos.y + border_top + padding_top,
227                    );
228                }
229            }
230
231            let mut final_pos = LogicalPosition::zero();
232
233            // Vertical Positioning
234            if let Some(top) = offsets.top {
235                final_pos.y = containing_block_rect.origin.y + top;
236            } else if let Some(bottom) = offsets.bottom {
237                final_pos.y = containing_block_rect.origin.y + containing_block_rect.size.height
238                    - element_size.height
239                    - bottom;
240            } else {
241                final_pos.y = static_pos.y;
242            }
243
244            // Horizontal Positioning
245            if let Some(left) = offsets.left {
246                final_pos.x = containing_block_rect.origin.x + left;
247            } else if let Some(right) = offsets.right {
248                final_pos.x = containing_block_rect.origin.x + containing_block_rect.size.width
249                    - element_size.width
250                    - right;
251            } else {
252                final_pos.x = static_pos.x;
253            }
254
255            calculated_positions.insert(node_index, final_pos);
256        }
257    }
258    Ok(())
259}
260
261/// Final pass to shift relatively positioned elements from their static flow position.
262///
263/// This function now correctly resolves percentage-based offsets for `top`, `left`, etc.
264/// According to the CSS spec, for relatively positioned elements, these percentages are
265/// relative to the dimensions of the parent element's content box.
266pub fn adjust_relative_positions<T: ParsedFontTrait>(
267    ctx: &mut LayoutContext<'_, T>,
268    tree: &LayoutTree,
269    calculated_positions: &mut super::PositionVec,
270    viewport: LogicalRect, // The viewport is needed if the root element is relative.
271) -> Result<()> {
272    // Iterate through all nodes. We need the index to modify the position map.
273    for node_index in 0..tree.nodes.len() {
274        let node = &tree.nodes[node_index];
275        let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);
276
277        // Early continue for non-relative positioning
278        if position_type != LayoutPosition::Relative {
279            continue;
280        }
281
282        // Determine the containing block size for resolving percentages.
283        // For `position: relative`, this is the parent's content box size.
284        let containing_block_size = node.parent
285            .and_then(|parent_idx| tree.get(parent_idx))
286            .map(|parent_node| {
287                // Get parent's writing mode to correctly calculate its inner (content) size.
288                let parent_dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
289                let parent_node_state =
290                    &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
291                let parent_wm =
292                    get_writing_mode(ctx.styled_dom, parent_dom_id, parent_node_state)
293                    .unwrap_or_default();
294                let parent_used_size = parent_node.used_size.unwrap_or_default();
295                parent_node.box_props.inner_size(parent_used_size, parent_wm)
296            })
297            // The root element is relatively positioned. Its containing block is the viewport.
298            .unwrap_or(viewport.size);
299
300        // Resolve offsets using the calculated containing block size.
301        let offsets =
302            resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size);
303
304        // Get a mutable reference to the position and apply the offsets.
305        let Some(current_pos) = calculated_positions.get_mut(node_index) else {
306            continue;
307        };
308
309        let initial_pos = *current_pos;
310
311        // top/bottom/left/right offsets are applied relative to the static position.
312        let mut delta_x = 0.0;
313        let mut delta_y = 0.0;
314
315        // According to CSS 2.1 Section 9.3.2:
316        // - For `top` and `bottom`: if both are specified, `top` wins and `bottom` is ignored
317        // - For `left` and `right`: depends on direction (ltr/rtl)
318        //   - In LTR: if both specified, `left` wins and `right` is ignored
319        //   - In RTL: if both specified, `right` wins and `left` is ignored
320
321        // Vertical positioning: `top` takes precedence over `bottom`
322        if let Some(top) = offsets.top {
323            delta_y = top;
324        } else if let Some(bottom) = offsets.bottom {
325            delta_y = -bottom;
326        }
327
328        // Horizontal positioning: depends on direction
329        // Get the direction for this element
330        let node_dom_id = node.dom_node_id.unwrap_or(NodeId::ZERO);
331        let node_state = &ctx.styled_dom.styled_nodes.as_container()[node_dom_id].styled_node_state;
332
333        use azul_css::props::style::StyleDirection;
334        let direction = match get_direction_property(ctx.styled_dom, node_dom_id, node_state) {
335            MultiValue::Exact(v) => v,
336            _ => StyleDirection::Ltr,
337        };
338        match direction {
339            StyleDirection::Ltr => {
340                // In LTR mode: `left` takes precedence over `right`
341                if let Some(left) = offsets.left {
342                    delta_x = left;
343                } else if let Some(right) = offsets.right {
344                    delta_x = -right;
345                }
346            }
347            StyleDirection::Rtl => {
348                // In RTL mode: `right` takes precedence over `left`
349                if let Some(right) = offsets.right {
350                    delta_x = -right;
351                } else if let Some(left) = offsets.left {
352                    delta_x = left;
353                }
354            }
355        }
356
357        // Only apply the shift if there is a non-zero delta.
358        if delta_x != 0.0 || delta_y != 0.0 {
359            current_pos.x += delta_x;
360            current_pos.y += delta_y;
361
362            ctx.debug_log(&format!(
363                "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
364                node_index, initial_pos, *current_pos, delta_x, delta_y
365            ));
366        }
367    }
368    Ok(())
369}
370
371/// Helper to find the containing block for an absolutely positioned element.
372/// CSS 2.1 Section 10.1: The containing block for absolutely positioned elements
373/// is the padding box of the nearest positioned ancestor.
374fn find_absolute_containing_block_rect(
375    tree: &LayoutTree,
376    node_index: usize,
377    styled_dom: &StyledDom,
378    calculated_positions: &super::PositionVec,
379    viewport: LogicalRect,
380) -> Result<LogicalRect> {
381    let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);
382
383    while let Some(parent_index) = current_parent_idx {
384        let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;
385
386        if get_position_type(styled_dom, parent_node.dom_node_id) != LayoutPosition::Static {
387            // calculated_positions stores margin-box positions
388            let margin_box_pos = calculated_positions
389                .get(parent_index)
390                .copied()
391                .unwrap_or_default();
392            // used_size is the border-box size
393            let border_box_size = parent_node.used_size.unwrap_or_default();
394
395            // Calculate padding-box origin (margin-box + border)
396            // CSS 2.1 ยง 10.1: containing block is the padding box
397            let padding_box_pos = LogicalPosition::new(
398                margin_box_pos.x + parent_node.box_props.border.left,
399                margin_box_pos.y + parent_node.box_props.border.top,
400            );
401
402            // Calculate padding-box size (border-box - borders)
403            let padding_box_size = LogicalSize::new(
404                border_box_size.width
405                    - parent_node.box_props.border.left
406                    - parent_node.box_props.border.right,
407                border_box_size.height
408                    - parent_node.box_props.border.top
409                    - parent_node.box_props.border.bottom,
410            );
411
412            return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
413        }
414        current_parent_idx = parent_node.parent;
415    }
416
417    Ok(viewport) // Fallback to the initial containing block.
418}