Skip to main content

orinium_browser/engine/layouter/builder/
layout_fix.rs

1use crate::engine::layouter::shift::shift_box;
2use crate::engine::layouter::text_layouter::TextFlowLayouter;
3use crate::engine::layouter::types::{InfoNode, NodeKind, TextAlign, WhiteSpace};
4use ui_layout::{
5    AutoSizeBehavior, Display, GridTrack, InnerDisplay, LayoutChild, LayoutNode, Length,
6    LengthOrAuto, OuterDisplay,
7};
8
9// ---------------------------------------------------------------------------
10// orinium internal helpers
11// ---------------------------------------------------------------------------
12
13/// Returns `true` when `info` represents a whitespace-only text node that
14/// renders as a single collapsible space.
15///
16/// `white_space` is the computed `white-space` of the element that owns the
17/// text. Per CSS, `pre`, `pre-wrap`, and `break-spaces` preserve whitespace
18/// and must not be collapsed even at block boundaries.
19pub fn is_collapsible_whitespace_info(info: &InfoNode, white_space: WhiteSpace) -> bool {
20    // Preserved-whitespace modes keep all whitespace, including at boundaries.
21    if !matches!(white_space, WhiteSpace::Normal | WhiteSpace::Nowrap) {
22        return false;
23    }
24    matches!(&info.kind, NodeKind::Text { text, .. } if text.trim().is_empty())
25}
26
27/// Returns `true` when `child` is a block-level layout child — i.e. it has
28/// `outer: Block` display, or it is a shrink-to-fit `FlowRoot` (an anonymous
29/// flex/grid wrapper).
30pub fn is_block_layout_child(child: &LayoutChild) -> bool {
31    child.node().is_some_and(|node| {
32        node.style.display.outer() == Some(OuterDisplay::Block)
33            || (node.style.display.inner() == Some(InnerDisplay::FlowRoot)
34                && node.style.size.auto_behavior == AutoSizeBehavior::ShrinkToFit)
35    })
36}
37
38// ---------------------------------------------------------------------------
39// ui_layout bug fixes
40// ---------------------------------------------------------------------------
41
42/// **Bug fix:** `grid-template-columns`, `width`
43///
44/// During the intrinsic grid pass `ui_layout` measures block flex containers
45/// with their containing width. In a template such as `1fr auto 1fr`, that
46/// makes the auto track take the entire grid and leaves both fraction tracks
47/// at zero. The first layout still records the flex contents' actual extent in
48/// `children_box`, so use that intrinsic width and let a second layout resolve
49/// the tracks correctly.
50pub fn constrain_auto_grid_track_items(node: &mut LayoutNode) -> bool {
51    let mut changed = false;
52    for child in &mut node.children {
53        if let LayoutChild::Node(child) = child {
54            changed |= constrain_auto_grid_track_items(child);
55        }
56    }
57
58    if node.style.display.inner() != Some(InnerDisplay::Grid)
59        || node.style.grid_template_columns.is_empty()
60    {
61        return changed;
62    }
63
64    let mut item_index = 0usize;
65    for child in &mut node.children {
66        let LayoutChild::Node(child) = child else {
67            continue;
68        };
69        if child.style.display == Display::None || child.style.position.kind.is_out_of_flow() {
70            continue;
71        }
72
73        let auto_track = node
74            .style
75            .grid_template_columns
76            .get(item_index)
77            .is_some_and(|track| matches!(track, GridTrack::Breadth(LengthOrAuto::Auto)));
78        item_index += 1;
79        let self_aligned = child.style.spacing.margin_left == LengthOrAuto::Auto
80            || child.style.spacing.margin_right == LengthOrAuto::Auto;
81        if (!auto_track && !self_aligned) || child.style.size.width != LengthOrAuto::Auto {
82            continue;
83        }
84
85        let Some(model) = child.layout_box.iter().next() else {
86            continue;
87        };
88        let intrinsic_width = model.children_box.width.max(0.0);
89        if intrinsic_width > 0.0 && intrinsic_width + 0.5 < model.content_box.width {
90            child.style.size.width = LengthOrAuto::Length(Length::Px(intrinsic_width));
91            changed = true;
92        }
93    }
94
95    changed
96}
97
98/// **Bug fix:** `display`, `margin-left`, `margin-right`
99///
100/// `ui_layout` advances past an inline flow-root using its content width.
101/// CSS inline-blocks advance by their margin-box width instead. Adjacent
102/// atomic inline boxes end up overlapping their padding or horizontal margins.
103pub fn correct_atomic_inline_spacing(node: &mut LayoutNode) {
104    correct_atomic_inline_spacing_impl(node, None);
105}
106
107/// Like [`correct_atomic_inline_spacing`], but accepts an [`InfoNode`] so that
108/// per-child `text-align` information is available during correction.
109pub fn correct_atomic_inline_spacing_with_info(node: &mut LayoutNode, info: &InfoNode) {
110    correct_atomic_inline_spacing_impl(node, Some(info));
111}
112
113/// Core implementation of atomic-inline spacing correction. When `info` is
114/// supplied, `text-align` from the container is respected for the first item on
115/// each line.
116fn correct_atomic_inline_spacing_impl(node: &mut LayoutNode, info: Option<&InfoNode>) {
117    let containing_rect = node.layout_box.iter().next().map(|model| model.content_box);
118    let containing_width = containing_rect.map(|rect| rect.width);
119    let text_align = info
120        .and_then(|info| match &info.kind {
121            NodeKind::Container { style, .. } => Some(style.text_align),
122            _ => None,
123        })
124        .unwrap_or_default();
125    let wraps_inline_content = matches!(
126        node.style.display.inner(),
127        Some(InnerDisplay::Flow | InnerDisplay::FlowRoot)
128    );
129    let mut previous: Option<(f32, f32)> = None;
130    let mut line_y: Option<f32> = None;
131    let mut line_start_x = 0.0;
132    let mut line_bottom = 0.0;
133    let mut line_margin_bottom = 0.0;
134    let mut preceding_block_bottom: Option<(f32, f32)> = None;
135
136    for (child_index, child) in node.children.iter_mut().enumerate() {
137        let LayoutChild::Node(child) = child else {
138            continue;
139        };
140
141        let child_info = info.and_then(|info| info.children.get(child_index));
142        correct_atomic_inline_spacing_impl(child, child_info);
143
144        let is_atomic_inline = child.style.display.outer() == Some(OuterDisplay::Inline)
145            && child.style.display.inner() != Some(InnerDisplay::Flow);
146
147        if is_atomic_inline && let Some(model) = child.layout_box.iter().next() {
148            let rect = model.border_box;
149            let margin_left = fixed_nonnegative_px(&child.style.spacing.margin_left);
150            let margin_right = fixed_nonnegative_px(&child.style.spacing.margin_right);
151            let margin_top = fixed_nonnegative_px(&child.style.spacing.margin_top);
152            let margin_bottom = fixed_nonnegative_px(&child.style.spacing.margin_bottom);
153
154            if line_y.is_none_or(|y| (y - rect.y).abs() >= 0.5) {
155                previous = None;
156                line_y = Some(rect.y);
157                line_start_x = rect.x;
158                line_bottom = rect.bottom();
159                line_margin_bottom = margin_bottom;
160            }
161
162            let mut desired_x = match previous {
163                Some((right, previous_margin_right)) => right + previous_margin_right + margin_left,
164                None => {
165                    let margin_width = margin_left + rect.width + margin_right;
166                    let aligned_x = containing_rect.map_or(rect.x, |containing| {
167                        let free_space = (containing.width - margin_width).max(0.0);
168                        // Child coordinates are already relative to this content origin.
169                        match text_align {
170                            TextAlign::Left => 0.0,
171                            TextAlign::Center => free_space / 2.0,
172                            TextAlign::Right => free_space,
173                        }
174                    });
175                    aligned_x + margin_left
176                }
177            };
178            // ui_layout positions atomic inline boxes at the line origin but
179            // does not include their vertical margins in that position. The
180            // margin box, rather than the border box, is what participates in
181            // inline formatting (notably a full-width inline-block <main>
182            // placed below a fixed header).
183            let mut desired_y = rect.y + margin_top;
184            if previous.is_none()
185                && let Some((block_bottom, block_margin_bottom)) = preceding_block_bottom
186            {
187                desired_y = desired_y.max(block_bottom + block_margin_bottom + margin_top);
188            }
189            let exceeds_line = previous.is_some()
190                && wraps_inline_content
191                && containing_width
192                    .is_some_and(|width| desired_x + rect.width + margin_right > width + 0.5);
193            if exceeds_line {
194                desired_x = line_start_x + margin_left;
195                desired_y = line_bottom + line_margin_bottom + margin_top;
196                line_y = Some(desired_y);
197                line_bottom = desired_y + rect.height;
198                line_margin_bottom = margin_bottom;
199            }
200
201            let shift_x = desired_x - rect.x;
202            if shift_x.abs() >= 0.01 {
203                shift_box(&mut child.layout_box, shift_x, 0.0);
204            }
205            let shift_y = desired_y - rect.y;
206            if shift_y.abs() >= 0.01 {
207                shift_box(&mut child.layout_box, 0.0, shift_y);
208            }
209
210            line_bottom = line_bottom.max(desired_y + rect.height);
211            line_margin_bottom = line_margin_bottom.max(margin_bottom);
212            previous = Some((desired_x + rect.width, margin_right));
213        } else if matches!(child.layout_box, ui_layout::LayoutBox::BlockBox(_)) {
214            previous = None;
215            line_y = None;
216            if !child.style.position.kind.is_out_of_flow()
217                && let Some(model) = child.layout_box.iter().next()
218            {
219                preceding_block_bottom = Some((
220                    model.border_box.bottom(),
221                    fixed_nonnegative_px(&child.style.spacing.margin_bottom),
222                ));
223            }
224        }
225    }
226
227    expand_auto_inline_width_to_children(node);
228    correct_single_row_grid_inline_alignment(node);
229}
230
231/// **Supplement:** `width`
232///
233/// `ui_layout` does not handle `width: auto` for inline containers wrapping
234/// block children. Grow the inline container's width to accommodate the
235/// widest child's margin box.
236fn expand_auto_inline_width_to_children(node: &mut LayoutNode) {
237    if node.style.display.outer() != Some(OuterDisplay::Inline)
238        || node.style.size.width != LengthOrAuto::Auto
239    {
240        return;
241    }
242    // Use the margin-box width of the widest child.
243    let required_width = node
244        .children
245        .iter()
246        .filter_map(LayoutChild::node)
247        .filter_map(|child| {
248            child.layout_box.iter().next().map(|model| {
249                model.border_box.right() + fixed_nonnegative_px(&child.style.spacing.margin_right)
250            })
251        })
252        .fold(0.0, f32::max);
253    if required_width <= 0.0 {
254        return;
255    }
256    if let Some(model) = node.layout_box.iter().next() {
257        let extra = required_width - model.content_box.width;
258        if extra > 0.0 {
259            match &mut node.layout_box {
260                ui_layout::LayoutBox::BlockBox(model) => {
261                    model.content_box.width += extra;
262                    model.padding_box.width += extra;
263                    model.border_box.width += extra;
264                    model.children_box.width = model.children_box.width.max(required_width);
265                }
266                ui_layout::LayoutBox::InlineBox(inline) => {
267                    inline.box_model.content_box.width += extra;
268                    inline.box_model.padding_box.width += extra;
269                    inline.box_model.border_box.width += extra;
270                    inline.box_model.children_box.width =
271                        inline.box_model.children_box.width.max(required_width);
272                    if let Some(last) = inline.line_spans.last_mut() {
273                        last.x_range.end += extra;
274                    }
275                }
276                ui_layout::LayoutBox::None => {}
277            }
278        }
279    }
280}
281
282// ---------------------------------------------------------------------------
283// ui_layout feature supplements
284// ---------------------------------------------------------------------------
285
286/// **Supplement:** custom flex item `layout` method
287///
288/// `ui_layout` measures and positions direct custom flex items but does not
289/// call their `layout` method. Render-time text-flow lookup therefore finds no
290/// spans for text directly inside an `inline-flex` element. Walk the tree and
291/// invoke `layout` for any custom flex item whose result is missing.
292pub fn refresh_missing_text_layout_results(
293    layout: &mut LayoutNode,
294    info: &InfoNode,
295    viewport: (f32, f32),
296) {
297    let containing = layout
298        .layout_box
299        .iter()
300        .next()
301        .map(|model| (model.content_box.width, model.content_box.height))
302        .unwrap_or(viewport);
303
304    for (layout_child, info_child) in layout.children.iter_mut().zip(&info.children) {
305        match (layout_child, &info_child.kind) {
306            (LayoutChild::Node(child), _) => {
307                refresh_missing_text_layout_results(child, info_child, viewport);
308            }
309            (LayoutChild::Custom(custom), NodeKind::Text { text_id, .. })
310                if TextFlowLayouter::get_result(*text_id).is_none() =>
311            {
312                let Some(box_model) = custom.result().map(|result| result.box_model.clone()) else {
313                    continue;
314                };
315                let line_height = custom
316                    .style()
317                    .line_height
318                    .resolve_with(Some(containing.0), viewport.0, viewport.1)
319                    .unwrap_or(box_model.border_box.height);
320                let _ = custom.layouter_mut().layout(&ui_layout::LayoutContext {
321                    containing_block_width: Some(containing.0),
322                    containing_block_height: Some(containing.1),
323                    start_pos: (box_model.border_box.x, box_model.border_box.y),
324                    available_inline_size: box_model.border_box.width.max(1.0),
325                    float_space: None,
326                    line_height,
327                    viewport_width: viewport.0,
328                    viewport_height: viewport.1,
329                });
330            }
331            _ => {}
332        }
333    }
334}
335
336/// **Supplement:** `margin-left`, `margin-right`, `column-gap`
337///
338/// `ui_layout` does not implement `margin-inline: auto` for grid items. When
339/// all grid items land in a single row, resolve auto left/right margins within
340/// each track so that items are horizontally centered or right-aligned.
341fn correct_single_row_grid_inline_alignment(node: &mut LayoutNode) {
342    if node.style.display.inner() != Some(InnerDisplay::Grid) {
343        return;
344    }
345    let Some(content_box) = node.layout_box.iter().next().map(|model| model.content_box) else {
346        return;
347    };
348    let column_gap = fixed_nonnegative_px(&node.style.column_gap);
349    let item_indices: Vec<usize> = node
350        .children
351        .iter()
352        .enumerate()
353        .filter_map(|(index, child)| {
354            child.node().and_then(|child| {
355                (child.style.display != Display::None
356                    && !child.style.position.kind.is_out_of_flow())
357                .then_some(index)
358            })
359        })
360        .collect();
361    if item_indices.len() > node.style.grid_template_columns.len() {
362        return;
363    }
364
365    for (position, index) in item_indices.iter().copied().enumerate() {
366        let next_x = item_indices
367            .get(position + 1)
368            .and_then(|next| node.children[*next].node())
369            .and_then(|next| next.layout_box.iter().next())
370            .map(|model| model.border_box.x - column_gap)
371            .unwrap_or(content_box.width);
372        let child = node.children[index].node_mut().expect("grid item");
373        let left_auto = child.style.spacing.margin_left == LengthOrAuto::Auto;
374        let right_auto = child.style.spacing.margin_right == LengthOrAuto::Auto;
375        if !left_auto && !right_auto {
376            continue;
377        }
378        let Some(model) = child.layout_box.iter().next() else {
379            continue;
380        };
381        let track_start = model.border_box.x;
382        let free_space = (next_x - track_start - model.border_box.width).max(0.0);
383        let offset = match (left_auto, right_auto) {
384            (true, true) => free_space / 2.0,
385            (true, false) => free_space,
386            _ => 0.0,
387        };
388        shift_box(&mut child.layout_box, offset, 0.0);
389    }
390}
391
392// ---------------------------------------------------------------------------
393// utilities
394// ---------------------------------------------------------------------------
395
396/// Extract a non-negative pixel value from a `LengthOrAuto`, returning `0.0`
397/// for `Auto` or non-pixel lengths.
398fn fixed_nonnegative_px(value: &LengthOrAuto) -> f32 {
399    match value {
400        LengthOrAuto::Length(Length::Px(value)) => value.max(0.0),
401        _ => 0.0,
402    }
403}