Skip to main content

lightweight_pdf_layout/layoutable/
row.rs

1//! Row: horizontal main axis, always bound by the incoming area width.
2//! Non-flex children measure at that bound (approximation, see
3//! `plan/03-builder-api-design.md`); flex children share the leftover
4//! space proportionally (taffy flex-grow analogy, ADR-004). No Row-level
5//! Split in V1 — only Column/Text split (phase-2 plan, step 1).
6
7use super::shared::{coerce_to_fit_and_warn, finish_fit, measure_at_width, resolve_auto_size, resolve_bound};
8use super::{LayoutCtx, LayoutResult, Layoutable};
9use crate::geometry::{Constraints, Rect, Size};
10use crate::render_node::align_offset;
11use crate::warnings::LayoutWarning;
12use lightweight_pdf_core::{Element, Row};
13
14impl Layoutable for Row {
15    fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
16        let (width, inner_width) = resolve_bound(self.common.width, constraints.max_width, self.common.padding);
17        let (natural_heights, used_width) = row_natural_layout(self, ctx, inner_width);
18        let height = resolve_auto_size(
19            self.common.height,
20            natural_heights.iter().cloned().fold(0.0f32, f32::max),
21            self.common.padding,
22        );
23        Size {
24            width: self.common.width.unwrap_or((used_width + 2.0 * self.common.padding).min(width)),
25            height,
26        }
27    }
28
29    fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
30        let inner = area.shrink(self.common.padding);
31        let resolved_widths = resolve_row_widths(self, ctx, inner.width);
32
33        let mut children_nodes = Vec::with_capacity(self.children.len());
34        let mut cursor_x = inner.x;
35        let mut max_child_height = 0.0f32;
36        let mut child_sizes = Vec::with_capacity(self.children.len());
37
38        for (child, w) in self.children.iter().zip(resolved_widths.iter()) {
39            if let Element::Spacer(s) = child {
40                cursor_x += s.size + self.gap;
41                continue;
42            }
43            let size = measure_at_width(ctx, child, *w);
44            max_child_height = max_child_height.max(size.height);
45            child_sizes.push((cursor_x, *w, size.height));
46            cursor_x += w + self.gap;
47        }
48
49        let row_height = self.common.height.unwrap_or(max_child_height).max(0.0);
50        let bounded_row_height = row_height.min(inner.height.max(row_height));
51
52        let mut idx = 0;
53        for child in self.children.iter() {
54            if matches!(child, Element::Spacer(_)) {
55                continue;
56            }
57            let (x, w, h) = child_sizes[idx];
58            idx += 1;
59            let y_offset = align_offset(self.align, bounded_row_height, h);
60            let child_area = Rect {
61                x,
62                y: inner.y + y_offset,
63                width: w,
64                height: h,
65            };
66            // Row does not support splitting across pages (V1 scope): keep
67            // what fits, clip the rest, warn.
68            let result = child.layout(ctx, child_area, warnings, page);
69            children_nodes.push(coerce_to_fit_and_warn(
70                result,
71                warnings,
72                page,
73                "Row child taller than available space",
74            ));
75        }
76
77        finish_fit(&self.common, area, bounded_row_height, children_nodes)
78    }
79}
80
81/// Natural (non-flex-adjusted) child heights plus the total width used by
82/// non-flex children, at a given bound width — used by `measure`.
83fn row_natural_layout(row: &Row, ctx: &LayoutCtx, bound_width: f32) -> (Vec<f32>, f32) {
84    let mut heights = Vec::new();
85    let mut used = 0.0f32;
86    let n = row.children.len();
87    for (i, child) in row.children.iter().enumerate() {
88        if let Element::Spacer(s) = child {
89            used += s.size;
90        } else {
91            let size = measure_at_width(ctx, child, bound_width);
92            heights.push(size.height);
93            used += size.width;
94        }
95        if i + 1 < n {
96            used += row.gap;
97        }
98    }
99    (heights, used)
100}
101
102/// Resolves each non-spacer child's width: fixed/natural for non-flex
103/// children, leftover space shared proportionally among flex children.
104fn resolve_row_widths(row: &Row, ctx: &LayoutCtx, bound_width: f32) -> Vec<f32> {
105    let n = row.children.len();
106    let gaps = if n > 0 { (n - 1) as f32 * row.gap } else { 0.0 };
107    let mut natural = vec![0.0f32; n];
108    let mut flex_sum = 0.0f32;
109    let mut fixed_total = 0.0f32;
110
111    for (i, child) in row.children.iter().enumerate() {
112        if let Element::Spacer(s) = child {
113            natural[i] = s.size;
114            fixed_total += s.size;
115            continue;
116        }
117        let common = child.common();
118        if let Some(f) = common.and_then(|c| c.flex) {
119            flex_sum += f;
120            continue;
121        }
122        let size = measure_at_width(ctx, child, bound_width);
123        natural[i] = size.width;
124        fixed_total += size.width;
125    }
126
127    let leftover = (bound_width - fixed_total - gaps).max(0.0);
128    if flex_sum > 0.0 {
129        for (i, child) in row.children.iter().enumerate() {
130            if let Some(f) = child.common().and_then(|c| c.flex) {
131                natural[i] = leftover * (f / flex_sum);
132            }
133        }
134    }
135    natural
136}