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