azul-layout 0.0.7

Layout solver + font and image loader the Azul GUI framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//! solver3/positioning.rs
//! Pass 3: Final positioning of layout nodes

use std::collections::BTreeMap;

use azul_core::{
    dom::NodeId,
    geom::{LogicalPosition, LogicalRect, LogicalSize},
    hit_test::ScrollPosition,
    resources::RendererResources,
    styled_dom::StyledDom,
};
use azul_css::{
    corety::LayoutDebugMessage,
    css::CssPropertyValue,
    props::{
        basic::pixel::PixelValue,
        layout::{LayoutPosition, LayoutWritingMode},
        property::{CssProperty, CssPropertyType},
    },
};

use crate::{
    font_traits::{FontLoaderTrait, ParsedFontTrait},
    solver3::{
        fc::{layout_formatting_context, LayoutConstraints, TextAlign},
        getters::{
            get_direction_property, get_writing_mode, get_position, MultiValue,
            get_css_top, get_css_bottom, get_css_left, get_css_right,
        },
        layout_tree::LayoutTree,
        LayoutContext, LayoutError, Result,
    },
};

#[derive(Debug, Default)]
struct PositionOffsets {
    top: Option<f32>,
    right: Option<f32>,
    bottom: Option<f32>,
    left: Option<f32>,
}

/// Looks up the `position` property using the compact-cache-aware getter.
pub fn get_position_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutPosition {
    let Some(id) = dom_id else {
        return LayoutPosition::Static;
    };
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
    get_position(styled_dom, id, node_state).unwrap_or_default()
}

/// Correctly looks up the `position` property from the styled DOM.
fn get_position_property(styled_dom: &StyledDom, node_id: NodeId) -> LayoutPosition {
    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
    get_position(styled_dom, node_id, node_state).unwrap_or(LayoutPosition::Static)
}

/// **NEW API:** Correctly reads and resolves `top`, `right`, `bottom`, `left` properties,
/// including percentages relative to the containing block's size, and em/rem units.
/// Uses the modern resolve_with_context() API.
fn resolve_position_offsets(
    styled_dom: &StyledDom,
    dom_id: Option<NodeId>,
    cb_size: LogicalSize,
) -> PositionOffsets {
    use azul_css::props::basic::pixel::{PhysicalSize, PropertyContext, ResolutionContext};

    use crate::solver3::getters::{
        get_element_font_size, get_parent_font_size, get_root_font_size,
    };

    let Some(id) = dom_id else {
        return PositionOffsets::default();
    };
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;

    // Create resolution context with font sizes and containing block size
    let element_font_size = get_element_font_size(styled_dom, id, node_state);
    let parent_font_size = get_parent_font_size(styled_dom, id, node_state);
    let root_font_size = get_root_font_size(styled_dom, node_state);

    let containing_block_size = PhysicalSize::new(cb_size.width, cb_size.height);

    let resolution_context = ResolutionContext {
        element_font_size,
        parent_font_size,
        root_font_size,
        containing_block_size,
        element_size: None, // Not needed for position offsets
        viewport_size: PhysicalSize::new(0.0, 0.0),
    };

    let mut offsets = PositionOffsets::default();

    // Resolve offsets using compact-cache-aware getters
    // top/bottom use Height context (% refers to containing block height)
    offsets.top = match get_css_top(styled_dom, id, node_state) {
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
        _ => None,
    };

    offsets.bottom = match get_css_bottom(styled_dom, id, node_state) {
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Height)),
        _ => None,
    };

    // left/right use Width context (% refers to containing block width)
    offsets.left = match get_css_left(styled_dom, id, node_state) {
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
        _ => None,
    };

    offsets.right = match get_css_right(styled_dom, id, node_state) {
        MultiValue::Exact(pv) => Some(pv.resolve_with_context(&resolution_context, PropertyContext::Width)),
        _ => None,
    };

    offsets
}

/// After the main layout pass, this function iterates through the tree and correctly
/// calculates the final positions of out-of-flow elements (`absolute`, `fixed`).
pub fn position_out_of_flow_elements<T: ParsedFontTrait>(
    ctx: &mut LayoutContext<'_, T>,
    tree: &mut LayoutTree,
    calculated_positions: &mut super::PositionVec,
    viewport: LogicalRect,
) -> Result<()> {
    for node_index in 0..tree.nodes.len() {
        let node = &tree.nodes[node_index];
        let dom_id = match node.dom_node_id {
            Some(id) => id,
            None => continue,
        };

        let position_type = get_position_type(ctx.styled_dom, Some(dom_id));

        if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
            // Get parent info before any mutable borrows
            let parent_info: Option<(usize, LogicalPosition, f32, f32, f32, f32)> = {
                let node = &tree.nodes[node_index];
                node.parent.and_then(|parent_idx| {
                    let parent_node = tree.get(parent_idx)?;
                    let parent_dom_id = parent_node.dom_node_id?;
                    let parent_position = get_position_type(ctx.styled_dom, Some(parent_dom_id));
                    if parent_position == LayoutPosition::Absolute
                        || parent_position == LayoutPosition::Fixed
                    {
                        calculated_positions.get(parent_idx).map(|parent_pos| {
                            (
                                parent_idx,
                                *parent_pos,
                                parent_node.box_props.border.left,
                                parent_node.box_props.border.top,
                                parent_node.box_props.padding.left,
                                parent_node.box_props.padding.top,
                            )
                        })
                    } else {
                        None
                    }
                })
            };

            // Determine containing block FIRST (before calculating size)
            let containing_block_rect = if position_type == LayoutPosition::Fixed {
                viewport
            } else {
                find_absolute_containing_block_rect(
                    tree,
                    node_index,
                    ctx.styled_dom,
                    calculated_positions,
                    viewport,
                )?
            };

            // Get node again after containing block calculation
            let node = &tree.nodes[node_index];

            // Calculate used size for out-of-flow elements (they don't get sized during normal
            // layout)
            let element_size = if let Some(size) = node.used_size {
                size
            } else {
                // Element hasn't been sized yet - calculate it now using containing block
                let intrinsic = node.intrinsic_sizes.unwrap_or_default();
                let size = crate::solver3::sizing::calculate_used_size_for_node(
                    ctx.styled_dom,
                    Some(dom_id),
                    containing_block_rect.size,
                    intrinsic,
                    &node.box_props,
                    ctx.viewport_size,
                )?;

                // Store the calculated size in the tree node
                if let Some(node_mut) = tree.get_mut(node_index) {
                    node_mut.used_size = Some(size);
                }

                size
            };

            // Resolve offsets using the now-known containing block size.
            let offsets =
                resolve_position_offsets(ctx.styled_dom, Some(dom_id), containing_block_rect.size);

            let mut static_pos = calculated_positions
                .get(node_index)
                .copied()
                .unwrap_or_default();

            // Special case: If this is a fixed-position element and it has a positioned
            // parent, update static_pos to be relative to the parent's final absolute
            // position (content-box). The initial static_pos from process_out_of_flow_children
            // may include border/padding offsets, so we must always recalculate here.
            if position_type == LayoutPosition::Fixed {
                if let Some((_, parent_pos, border_left, border_top, padding_left, padding_top)) =
                    parent_info
                {
                    // Add parent's border and padding to get content-box position
                    static_pos = LogicalPosition::new(
                        parent_pos.x + border_left + padding_left,
                        parent_pos.y + border_top + padding_top,
                    );
                }
            }

            let mut final_pos = LogicalPosition::zero();

            // Vertical Positioning
            if let Some(top) = offsets.top {
                final_pos.y = containing_block_rect.origin.y + top;
            } else if let Some(bottom) = offsets.bottom {
                final_pos.y = containing_block_rect.origin.y + containing_block_rect.size.height
                    - element_size.height
                    - bottom;
            } else {
                final_pos.y = static_pos.y;
            }

            // Horizontal Positioning
            if let Some(left) = offsets.left {
                final_pos.x = containing_block_rect.origin.x + left;
            } else if let Some(right) = offsets.right {
                final_pos.x = containing_block_rect.origin.x + containing_block_rect.size.width
                    - element_size.width
                    - right;
            } else {
                final_pos.x = static_pos.x;
            }

            calculated_positions.insert(node_index, final_pos);
        }
    }
    Ok(())
}

/// Final pass to shift relatively positioned elements from their static flow position.
///
/// This function now correctly resolves percentage-based offsets for `top`, `left`, etc.
/// According to the CSS spec, for relatively positioned elements, these percentages are
/// relative to the dimensions of the parent element's content box.
pub fn adjust_relative_positions<T: ParsedFontTrait>(
    ctx: &mut LayoutContext<'_, T>,
    tree: &LayoutTree,
    calculated_positions: &mut super::PositionVec,
    viewport: LogicalRect, // The viewport is needed if the root element is relative.
) -> Result<()> {
    // Iterate through all nodes. We need the index to modify the position map.
    for node_index in 0..tree.nodes.len() {
        let node = &tree.nodes[node_index];
        let position_type = get_position_type(ctx.styled_dom, node.dom_node_id);

        // Early continue for non-relative positioning
        if position_type != LayoutPosition::Relative {
            continue;
        }

        // Determine the containing block size for resolving percentages.
        // For `position: relative`, this is the parent's content box size.
        let containing_block_size = node.parent
            .and_then(|parent_idx| tree.get(parent_idx))
            .map(|parent_node| {
                // Get parent's writing mode to correctly calculate its inner (content) size.
                let parent_dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
                let parent_node_state =
                    &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
                let parent_wm =
                    get_writing_mode(ctx.styled_dom, parent_dom_id, parent_node_state)
                    .unwrap_or_default();
                let parent_used_size = parent_node.used_size.unwrap_or_default();
                parent_node.box_props.inner_size(parent_used_size, parent_wm)
            })
            // The root element is relatively positioned. Its containing block is the viewport.
            .unwrap_or(viewport.size);

        // Resolve offsets using the calculated containing block size.
        let offsets =
            resolve_position_offsets(ctx.styled_dom, node.dom_node_id, containing_block_size);

        // Get a mutable reference to the position and apply the offsets.
        let Some(current_pos) = calculated_positions.get_mut(node_index) else {
            continue;
        };

        let initial_pos = *current_pos;

        // top/bottom/left/right offsets are applied relative to the static position.
        let mut delta_x = 0.0;
        let mut delta_y = 0.0;

        // According to CSS 2.1 Section 9.3.2:
        // - For `top` and `bottom`: if both are specified, `top` wins and `bottom` is ignored
        // - For `left` and `right`: depends on direction (ltr/rtl)
        //   - In LTR: if both specified, `left` wins and `right` is ignored
        //   - In RTL: if both specified, `right` wins and `left` is ignored

        // Vertical positioning: `top` takes precedence over `bottom`
        if let Some(top) = offsets.top {
            delta_y = top;
        } else if let Some(bottom) = offsets.bottom {
            delta_y = -bottom;
        }

        // Horizontal positioning: depends on direction
        // Get the direction for this element
        let node_dom_id = node.dom_node_id.unwrap_or(NodeId::ZERO);
        let node_state = &ctx.styled_dom.styled_nodes.as_container()[node_dom_id].styled_node_state;

        use azul_css::props::style::StyleDirection;
        let direction = match get_direction_property(ctx.styled_dom, node_dom_id, node_state) {
            MultiValue::Exact(v) => v,
            _ => StyleDirection::Ltr,
        };
        match direction {
            StyleDirection::Ltr => {
                // In LTR mode: `left` takes precedence over `right`
                if let Some(left) = offsets.left {
                    delta_x = left;
                } else if let Some(right) = offsets.right {
                    delta_x = -right;
                }
            }
            StyleDirection::Rtl => {
                // In RTL mode: `right` takes precedence over `left`
                if let Some(right) = offsets.right {
                    delta_x = -right;
                } else if let Some(left) = offsets.left {
                    delta_x = left;
                }
            }
        }

        // Only apply the shift if there is a non-zero delta.
        if delta_x != 0.0 || delta_y != 0.0 {
            current_pos.x += delta_x;
            current_pos.y += delta_y;

            ctx.debug_log(&format!(
                "Adjusted relative element #{} from {:?} to {:?} (delta: {}, {})",
                node_index, initial_pos, *current_pos, delta_x, delta_y
            ));
        }
    }
    Ok(())
}

/// Helper to find the containing block for an absolutely positioned element.
/// CSS 2.1 Section 10.1: The containing block for absolutely positioned elements
/// is the padding box of the nearest positioned ancestor.
fn find_absolute_containing_block_rect(
    tree: &LayoutTree,
    node_index: usize,
    styled_dom: &StyledDom,
    calculated_positions: &super::PositionVec,
    viewport: LogicalRect,
) -> Result<LogicalRect> {
    let mut current_parent_idx = tree.get(node_index).and_then(|n| n.parent);

    while let Some(parent_index) = current_parent_idx {
        let parent_node = tree.get(parent_index).ok_or(LayoutError::InvalidTree)?;

        if get_position_type(styled_dom, parent_node.dom_node_id) != LayoutPosition::Static {
            // calculated_positions stores margin-box positions
            let margin_box_pos = calculated_positions
                .get(parent_index)
                .copied()
                .unwrap_or_default();
            // used_size is the border-box size
            let border_box_size = parent_node.used_size.unwrap_or_default();

            // Calculate padding-box origin (margin-box + border)
            // CSS 2.1 ยง 10.1: containing block is the padding box
            let padding_box_pos = LogicalPosition::new(
                margin_box_pos.x + parent_node.box_props.border.left,
                margin_box_pos.y + parent_node.box_props.border.top,
            );

            // Calculate padding-box size (border-box - borders)
            let padding_box_size = LogicalSize::new(
                border_box_size.width
                    - parent_node.box_props.border.left
                    - parent_node.box_props.border.right,
                border_box_size.height
                    - parent_node.box_props.border.top
                    - parent_node.box_props.border.bottom,
            );

            return Ok(LogicalRect::new(padding_box_pos, padding_box_size));
        }
        current_parent_idx = parent_node.parent;
    }

    Ok(viewport) // Fallback to the initial containing block.
}