lightweight_pdf_layout/layoutable/
leaf.rs1use super::shared::{line_height_pt, push_warning, size_with_defaults, EPS};
5use super::{LayoutCtx, LayoutResult, Layoutable};
6use crate::geometry::{Constraints, Rect, Size};
7use crate::render_node::RenderNode;
8use crate::text::{text_width_pt, wrap_text};
9use crate::warnings::{LayoutWarning, LayoutWarningKind};
10use lightweight_pdf_core::{Element, Line, Overflow, Rect as RectElement, Spacer, Text, TextStyle};
11
12const WIDOW_ORPHAN_N: usize = 2;
15
16fn max_lines_fitting(area_height: f32, lh: f32, available: usize) -> usize {
20 (((area_height + EPS) / lh).floor().max(0.0) as usize).min(available)
21}
22
23fn text_lines_node(area: Rect, style: TextStyle, lines: Vec<String>, lh: f32) -> RenderNode {
24 let height = lines.len() as f32 * lh;
25 RenderNode::clipped(
26 area,
27 RenderNode::TextLines {
28 area: Rect { height, ..area },
29 style,
30 lines,
31 line_height_pt: lh,
32 },
33 )
34}
35
36impl Layoutable for Text {
37 fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
38 let width = self.common.width.unwrap_or(constraints.max_width);
39 let lines = wrap_text(ctx.resolver, &self.style, &self.content, width);
40 let lh = line_height_pt(&self.style);
41 let actual_width = lines
42 .iter()
43 .map(|l| text_width_pt(ctx.resolver, self.style.font, self.style.size, l))
44 .fold(0.0f32, f32::max);
45 Size {
46 width: self.common.width.unwrap_or(actual_width.min(width)),
47 height: self.common.height.unwrap_or(lines.len() as f32 * lh),
48 }
49 }
50
51 fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
52 let lines = wrap_text(ctx.resolver, &self.style, &self.content, area.width);
53 let lh = line_height_pt(&self.style);
54 let total_height = lines.len() as f32 * lh;
55
56 if total_height <= area.height + EPS || lines.len() <= 1 {
57 if total_height > area.height + EPS {
58 push_warning(warnings, LayoutWarningKind::TextClipped, page, text_clipped_hint(&self.content));
59 }
60 return LayoutResult::Fit(text_lines_node(area, self.style, lines, lh));
61 }
62
63 if self.common.height.is_some() {
69 return LayoutResult::Fit(layout_text_fixed_overflow(self, ctx, area, lines, lh, warnings, page));
70 }
71
72 let max_lines_by_height = max_lines_fitting(area.height, lh, lines.len());
73
74 let mut split_at = max_lines_by_height;
75 if lines.len() < 2 * WIDOW_ORPHAN_N {
76 split_at = 0;
78 } else if split_at < WIDOW_ORPHAN_N {
79 split_at = 0;
81 } else if lines.len() - split_at < WIDOW_ORPHAN_N {
82 let adjusted = lines.len().saturating_sub(WIDOW_ORPHAN_N);
84 split_at = if adjusted >= WIDOW_ORPHAN_N { adjusted } else { 0 };
85 }
86
87 if split_at == 0 {
88 return LayoutResult::Split {
89 current: RenderNode::Empty,
90 remainder: Element::Text(self.clone()),
91 };
92 }
93
94 let (current_lines, remainder_lines) = lines.split_at(split_at);
95 let current = text_lines_node(
96 Rect {
97 height: current_lines.len() as f32 * lh,
98 ..area
99 },
100 self.style,
101 current_lines.to_vec(),
102 lh,
103 );
104 let remainder_text = remainder_lines.join(" ");
105 let mut remainder = self.clone();
106 remainder.content = remainder_text;
107 LayoutResult::Split {
108 current,
109 remainder: Element::Text(remainder),
110 }
111 }
112}
113
114fn layout_text_fixed_overflow(
120 text: &Text,
121 ctx: &LayoutCtx,
122 area: Rect,
123 lines: Vec<String>,
124 lh: f32,
125 warnings: &mut Vec<LayoutWarning>,
126 page: usize,
127) -> RenderNode {
128 let max_lines = max_lines_fitting(area.height, lh, lines.len());
129 if max_lines >= lines.len() {
130 return text_lines_node(area, text.style, lines, lh);
131 }
132 push_warning(warnings, LayoutWarningKind::TextClipped, page, text_clipped_hint(&text.content));
133 let take = if text.common.overflow == Overflow::Ellipsis {
134 max_lines.max(1).min(lines.len())
135 } else {
136 max_lines
137 };
138 let mut kept: Vec<String> = lines.into_iter().take(take).collect();
139 if text.common.overflow == Overflow::Ellipsis {
140 if let Some(last) = kept.last_mut() {
141 *last = fit_with_ellipsis(ctx, &text.style, last, area.width);
142 }
143 }
144 text_lines_node(area, text.style, kept, lh)
145}
146
147fn fit_with_ellipsis(ctx: &LayoutCtx, style: &TextStyle, line: &str, max_width: f32) -> String {
150 let mut chars: Vec<char> = line.chars().collect();
151 loop {
152 let candidate: String = chars.iter().collect::<String>() + "…";
153 if text_width_pt(ctx.resolver, style.font, style.size, &candidate) <= max_width || chars.is_empty() {
154 return candidate;
155 }
156 chars.pop();
157 }
158}
159
160fn text_clipped_hint(content: &str) -> String {
163 format!("Text \"{}\"", truncate_hint(content))
164}
165
166fn truncate_hint(s: &str) -> String {
167 if s.len() > 24 {
168 format!("{}…", &s[..24])
169 } else {
170 s.to_string()
171 }
172}
173
174impl Layoutable for Spacer {
181 fn measure(&self, _ctx: &LayoutCtx, _constraints: Constraints) -> Size {
182 Size {
183 width: self.size,
184 height: self.size,
185 }
186 }
187
188 fn layout(&self, _ctx: &LayoutCtx, _area: Rect, _warnings: &mut Vec<LayoutWarning>, _page: usize) -> LayoutResult {
189 LayoutResult::Fit(RenderNode::Empty)
190 }
191}
192
193impl Layoutable for Line {
198 fn measure(&self, _ctx: &LayoutCtx, constraints: Constraints) -> Size {
199 size_with_defaults(&self.common, constraints, self.thickness)
200 }
201
202 fn layout(&self, _ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
203 if self.thickness > area.height + EPS {
204 push_warning(warnings, LayoutWarningKind::ContentOverflow, page, "Line");
205 }
206 let y_mid = area.y + (self.thickness / 2.0).min(area.height);
207 let node = RenderNode::Line {
208 x1: area.x,
209 y1: y_mid,
210 x2: area.x + area.width,
211 y2: y_mid,
212 thickness: self.thickness,
213 color: self.color,
214 };
215 LayoutResult::Fit(RenderNode::clipped(area, node))
216 }
217}
218
219impl Layoutable for RectElement {
224 fn measure(&self, _ctx: &LayoutCtx, constraints: Constraints) -> Size {
225 size_with_defaults(&self.common, constraints, 0.0)
226 }
227
228 fn layout(&self, _ctx: &LayoutCtx, area: Rect, _warnings: &mut Vec<LayoutWarning>, _page: usize) -> LayoutResult {
229 let node = RenderNode::Rect {
230 area,
231 background: self.common.background,
232 border: self.common.border,
233 };
234 LayoutResult::Fit(RenderNode::clipped(area, node))
235 }
236}