1use super::shared::{
6 clip_to_fixed_height, line_height_pt, measure_at_width, push_warning, resolve_auto_size, resolve_bound, shrink_and_bound_height,
7 wrap_children, EPS,
8};
9use super::{LayoutCtx, LayoutResult, Layoutable};
10use crate::geometry::{Constraints, Rect, Size};
11use crate::render_node::{align_offset, RenderNode};
12use crate::warnings::{LayoutWarning, LayoutWarningKind};
13use lightweight_pdf_core::{Align, Column, Common, Element};
14
15impl Layoutable for Column {
16 fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
17 let (bound_width, inner_width) = resolve_bound(self.common.width, constraints.max_width, self.common.padding);
24 let mut total_height = 0.0f32;
25 let mut max_child_width = 0.0f32;
26 let n = self.children.len();
27 for (i, child) in self.children.iter().enumerate() {
28 if let Element::PageBreak = child {
29 continue;
30 }
31 let (w, h) = if let Element::Spacer(s) = child {
32 (0.0, s.size)
33 } else {
34 let size = measure_at_width(ctx, child, inner_width);
35 (size.width, size.height)
36 };
37 max_child_width = max_child_width.max(w);
38 total_height += h;
39 if i + 1 < n {
40 total_height += self.gap;
41 }
42 }
43 Size {
44 width: self
45 .common
46 .width
47 .unwrap_or((max_child_width + 2.0 * self.common.padding).min(bound_width)),
48 height: resolve_auto_size(self.common.height, total_height, self.common.padding),
49 }
50 }
51
52 fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
53 let (inner, bound_height) = shrink_and_bound_height(area, self.common.height, self.common.padding);
54
55 let mut rendered = Vec::new();
56 let mut cursor_y = 0.0f32;
57
58 for (i, child) in self.children.iter().enumerate() {
59 if let Element::PageBreak = child {
60 let remainder_children: Vec<Element> = self.children[i + 1..].to_vec();
61 return finish_page(self, area, cursor_y, rendered, remainder_children, warnings, page);
62 }
63
64 let child_width = child
65 .common()
66 .and_then(|c| c.width)
67 .unwrap_or(inner.width - self.common.padding.min(inner.width));
68 let child_width = if child.common().and_then(|c| c.width).is_some() {
69 child_width
70 } else {
71 inner.width
72 };
73
74 if let Element::Spacer(s) = child {
75 cursor_y += s.size + self.gap;
76 continue;
77 }
78
79 let remaining_height = (bound_height - cursor_y).max(0.0);
80 let natural = measure_at_width(ctx, child, child_width);
81
82 let fits_fully = natural.height <= remaining_height + EPS;
83 let keep_ok = !fits_fully
84 || !child.common().map(|c| c.keep_with_next).unwrap_or(false)
85 || keep_with_next_satisfied(self, ctx, i, child_width, remaining_height - natural.height - self.gap);
86
87 if fits_fully && keep_ok {
88 let child_area = column_child_rect(inner, self.align, cursor_y, child_width, natural.height);
89 match child.layout(ctx, child_area, warnings, page) {
90 LayoutResult::Fit(node) => {
91 rendered.push(node);
92 cursor_y += natural.height + self.gap;
93 }
94 LayoutResult::Split { current, remainder } => {
97 let split = ChildSplit {
98 current,
99 remainder,
100 remaining_siblings: &self.children[i + 1..],
101 };
102 return split_child_and_finish_page(self, area, cursor_y, rendered, split, warnings, page);
103 }
104 }
105 continue;
106 }
107
108 let splittable = matches!(
110 child,
111 Element::Text(_) | Element::Column(_) | Element::Table(_) | Element::TableOfContents(_)
112 );
113 let min_unit = match child {
114 Element::Text(t) => line_height_pt(&t.style),
115 Element::Table(t) => crate::table::table_min_unit(ctx, t, child_width),
116 Element::TableOfContents(t) => line_height_pt(&t.style),
117 _ => natural.height,
118 };
119
120 if cursor_y > EPS && (remaining_height < min_unit - EPS || !keep_ok) {
121 let mut remainder_children = vec![child.clone()];
124 remainder_children.extend(self.children[i + 1..].to_vec());
125 return finish_page(self, area, cursor_y, rendered, remainder_children, warnings, page);
126 }
127
128 if !splittable {
129 let forced_height = if fits_fully { natural.height } else { remaining_height.max(0.0) };
135 let child_area = column_child_rect(inner, self.align, cursor_y, child_width, forced_height);
136 if let LayoutResult::Fit(node) = child.layout(ctx, child_area, warnings, page) {
137 rendered.push(node);
138 }
139 if !fits_fully {
140 push_warning(
141 warnings,
142 LayoutWarningKind::ForcedPageBreak,
143 page,
144 "atomic element larger than one page",
145 );
146 }
147 cursor_y += forced_height + self.gap;
148 continue;
149 }
150
151 let child_area = column_child_rect(inner, self.align, cursor_y, child_width, remaining_height);
154 match child.layout(ctx, child_area, warnings, page) {
155 LayoutResult::Fit(node) => {
156 rendered.push(node);
157 cursor_y += natural.height.min(remaining_height) + self.gap;
158 }
159 LayoutResult::Split { current, remainder } => {
164 let split = ChildSplit {
165 current,
166 remainder,
167 remaining_siblings: &self.children[i + 1..],
168 };
169 return split_child_and_finish_page(self, area, cursor_y, rendered, split, warnings, page);
170 }
171 }
172 }
173
174 let outer_height = self
175 .common
176 .height
177 .unwrap_or(cursor_y.max(0.0) - self.gap.min(cursor_y) + 2.0 * self.common.padding)
178 .max(0.0);
179 let outer_height = if cursor_y <= EPS { 0.0 } else { outer_height };
180 LayoutResult::Fit(wrap_children(area, outer_height, &self.common, rendered))
181 }
182}
183
184fn column_child_rect(inner: Rect, align: Align, cursor_y: f32, child_width: f32, height: f32) -> Rect {
190 let x_offset = align_offset(align, inner.width, child_width);
191 Rect {
192 x: inner.x + x_offset,
193 y: inner.y + cursor_y,
194 width: child_width,
195 height,
196 }
197}
198
199struct ChildSplit<'a> {
204 current: RenderNode,
205 remainder: Element,
206 remaining_siblings: &'a [Element],
207}
208
209fn split_child_and_finish_page(
217 col: &Column,
218 area: Rect,
219 cursor_y: f32,
220 mut rendered: Vec<RenderNode>,
221 split: ChildSplit,
222 warnings: &mut Vec<LayoutWarning>,
223 page: usize,
224) -> LayoutResult {
225 let consumed = split.current.height();
226 if !matches!(split.current, RenderNode::Empty) {
227 rendered.push(split.current);
228 }
229 let mut remainder_children = vec![split.remainder];
230 remainder_children.extend(split.remaining_siblings.to_vec());
231 finish_page(col, area, cursor_y + consumed, rendered, remainder_children, warnings, page)
232}
233
234fn keep_with_next_satisfied(col: &Column, ctx: &LayoutCtx, i: usize, width: f32, leftover: f32) -> bool {
239 let Some(next) = col.children.get(i + 1) else {
240 return true;
241 };
242 let min_needed = match next {
243 Element::Text(t) => line_height_pt(&t.style),
244 Element::Spacer(s) => s.size,
245 _ => measure_at_width(ctx, next, width).height,
246 };
247 leftover + EPS >= min_needed
248}
249
250fn finish_page(
257 col: &Column,
258 outer_area: Rect,
259 cursor_y: f32,
260 rendered: Vec<RenderNode>,
261 remainder_children: Vec<Element>,
262 warnings: &mut Vec<LayoutWarning>,
263 page: usize,
264) -> LayoutResult {
265 if let Some(fixed_height) = col.common.height {
266 let overflow_hint = (!remainder_children.is_empty()).then_some("Column content exceeds its fixed height");
267 return clip_to_fixed_height(outer_area, fixed_height, &col.common, rendered, warnings, page, overflow_hint);
268 }
269
270 let outer_height = if cursor_y <= EPS {
271 0.0
272 } else {
273 (cursor_y - col.gap.min(cursor_y) + 2.0 * col.common.padding).max(0.0)
274 };
275 let current = wrap_children(outer_area, outer_height, &col.common, rendered);
276 let remainder = Column {
277 children: remainder_children,
278 gap: col.gap,
279 align: col.align,
280 common: Common {
281 height: None,
282 ..col.common
283 },
284 };
285 LayoutResult::Split {
286 current: if cursor_y <= EPS { RenderNode::Empty } else { current },
287 remainder: Element::Column(remainder),
288 }
289}