Skip to main content

orinium_browser/engine/layouter/builder/
layout_fix.rs

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