lightweight_pdf_layout/
list.rs1use crate::geometry::{Constraints, Rect, Size};
8use crate::layoutable::{LayoutCtx, LayoutResult, Layoutable};
9use crate::warnings::{LayoutWarning, LayoutWarningKind};
10use lightweight_pdf_core::{Align, Column, List, Marker, Row, Text};
11
12fn marker_text(marker: &Marker) -> String {
13 match marker {
14 Marker::Bullet => "\u{2022}".to_string(),
15 Marker::Number(n) => format!("{n}."),
16 }
17}
18
19fn to_column(list: &List) -> Column {
24 let mut col = Column::new().gap(list.gap);
25 for item in &list.items {
26 let mut content = item.content.clone();
27 if let Some(common) = content.common_mut() {
28 if common.width.is_none() && common.flex.is_none() {
29 common.flex = Some(1.0);
30 }
31 }
32 let row = Row::new()
33 .gap(list.gap)
34 .child(Text::new(marker_text(&item.marker)).width(list.marker_width).align(Align::End))
35 .child(content);
36 col = col.child(row);
37 }
38 col.common = list.common;
39 col
40}
41
42impl Layoutable for List {
43 fn measure(&self, ctx: &LayoutCtx, constraints: Constraints) -> Size {
44 to_column(self).measure(ctx, constraints)
45 }
46
47 fn layout(&self, ctx: &LayoutCtx, area: Rect, warnings: &mut Vec<LayoutWarning>, page: usize) -> LayoutResult {
48 match to_column(self).layout(ctx, area, warnings, page) {
52 LayoutResult::Fit(node) => LayoutResult::Fit(node),
53 LayoutResult::Split { current, .. } => {
54 warnings.push(LayoutWarning {
55 kind: LayoutWarningKind::ContentOverflow,
56 page,
57 element_hint: "List content exceeds available space".to_string(),
58 });
59 LayoutResult::Fit(current)
60 }
61 }
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::font_resolver::{FontMetrics, FontResolver};
69 use crate::render_node::RenderNode;
70 use lightweight_pdf_core::{FontKey, Text as TextEl};
71
72 struct FixedMetrics;
73 impl FontMetrics for FixedMetrics {
74 fn advance(&self, ch: char) -> f32 {
75 if ch == ' ' {
76 300.0
77 } else {
78 600.0
79 }
80 }
81 fn ascent(&self) -> f32 {
82 800.0
83 }
84 fn descent(&self) -> f32 {
85 -200.0
86 }
87 }
88 struct FixedResolver;
89 impl FontResolver for FixedResolver {
90 fn metrics(&self, _key: FontKey) -> &dyn FontMetrics {
91 &FixedMetrics
92 }
93 }
94 fn ctx() -> LayoutCtx<'static> {
95 LayoutCtx { resolver: &FixedResolver }
96 }
97
98 #[test]
99 fn bullet_and_numbered_items_get_distinct_markers() {
100 let list = List::new()
101 .bullet(TextEl::new("Erstens"))
102 .numbered(TextEl::new("Zweitens"))
103 .numbered(TextEl::new("Drittens"));
104 let c = ctx();
105 let mut warnings = Vec::new();
106 let area = Rect {
107 x: 0.0,
108 y: 0.0,
109 width: 300.0,
110 height: 300.0,
111 };
112 let LayoutResult::Fit(RenderNode::Group { children: rows, .. }) = list.layout(&c, area, &mut warnings, 1) else {
113 panic!("expected Fit");
114 };
115 assert_eq!(rows.len(), 3);
116
117 fn marker_of(row: &RenderNode) -> String {
118 let RenderNode::Group { children: cells, .. } = row else {
119 panic!("expected row group")
120 };
121 let RenderNode::Group { children: wrap, .. } = &cells[0] else {
122 panic!("expected clip wrapper")
123 };
124 let RenderNode::TextLines { lines, .. } = &wrap[0] else {
125 panic!("expected TextLines")
126 };
127 lines.join("")
128 }
129 assert_eq!(marker_of(&rows[0]), "\u{2022}");
130 assert_eq!(marker_of(&rows[1]), "1.");
131 assert_eq!(marker_of(&rows[2]), "2.");
132 }
133
134 #[test]
135 fn item_content_fills_remaining_row_width() {
136 let list = List::new().bullet(TextEl::new("x"));
137 let c = ctx();
138 let mut warnings = Vec::new();
139 let area = Rect {
140 x: 0.0,
141 y: 0.0,
142 width: 200.0,
143 height: 100.0,
144 };
145 let LayoutResult::Fit(RenderNode::Group { children: rows, .. }) = list.layout(&c, area, &mut warnings, 1) else {
146 panic!("expected Fit");
147 };
148 let RenderNode::Group { children: cells, .. } = &rows[0] else {
149 panic!("expected row group")
150 };
151 let RenderNode::Group { area: content_area, .. } = &cells[1] else {
152 panic!("expected clip wrapper for content cell")
153 };
154 assert!(
157 content_area.width > 150.0,
158 "expected content to fill remaining width, got {}",
159 content_area.width
160 );
161 }
162}