1pub mod audit;
35pub mod flex;
36pub mod grid;
37pub mod page_break;
38
39use std::cell::RefCell;
40use std::collections::HashMap;
41use std::sync::Arc;
42
43use serde::Serialize;
44
45use crate::font::FontContext;
46use crate::model::*;
47use crate::style::*;
48use crate::text::bidi;
49use crate::text::shaping;
50use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
51
52#[derive(Debug, Clone, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookmarkEntry {
56 pub title: String,
57 pub page_index: usize,
58 pub y: f64,
59}
60
61#[derive(Debug, Clone, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct LayoutInfo {
67 pub pages: Vec<PageInfo>,
68}
69
70#[derive(Debug, Clone, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct PageInfo {
74 pub width: f64,
75 pub height: f64,
76 pub content_x: f64,
77 pub content_y: f64,
78 pub content_width: f64,
79 pub content_height: f64,
80 pub elements: Vec<ElementInfo>,
81}
82
83#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct ElementStyleInfo {
87 pub margin: Edges,
89 pub padding: Edges,
90 pub border_width: Edges,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub width: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub height: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_width: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub min_height: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_width: Option<f64>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub max_height: Option<f64>,
103 pub flex_direction: FlexDirection,
105 pub justify_content: JustifyContent,
106 pub align_items: AlignItems,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub align_self: Option<AlignItems>,
109 pub flex_wrap: FlexWrap,
110 pub align_content: AlignContent,
111 pub flex_grow: f64,
112 pub flex_shrink: f64,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub flex_basis: Option<String>,
115 pub gap: f64,
116 pub row_gap: f64,
117 pub column_gap: f64,
118 pub font_family: String,
120 pub font_size: f64,
121 pub font_weight: u32,
122 pub font_style: FontStyle,
123 pub line_height: f64,
124 pub text_align: TextAlign,
125 pub letter_spacing: f64,
126 pub word_spacing: f64,
127 pub text_decoration: TextDecoration,
128 pub text_transform: TextTransform,
129 pub color: Color,
131 pub background_color: Option<Color>,
132 pub border_color: EdgeValues<Color>,
133 pub border_style: EdgeValues<crate::style::BorderStyle>,
134 pub border_radius: CornerValues,
135 pub opacity: f64,
136 pub position: Position,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub top: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub right: Option<f64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub bottom: Option<f64>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub left: Option<f64>,
146 pub overflow: Overflow,
148 pub breakable: bool,
150 pub break_before: bool,
151 pub min_widow_lines: u32,
152 pub min_orphan_lines: u32,
153}
154
155fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
156 match sc {
157 SizeConstraint::Auto => None,
158 SizeConstraint::Fixed(v) => Some(format!("{v}")),
159 }
160}
161
162impl ElementStyleInfo {
163 fn from_resolved(style: &ResolvedStyle) -> Self {
164 ElementStyleInfo {
165 margin: style.margin.to_edges(),
166 padding: style.padding,
167 border_width: style.border_width,
168 width: size_constraint_to_str(&style.width),
169 height: size_constraint_to_str(&style.height),
170 min_width: if style.min_width > 0.0 {
171 Some(style.min_width)
172 } else {
173 None
174 },
175 min_height: if style.min_height > 0.0 {
176 Some(style.min_height)
177 } else {
178 None
179 },
180 max_width: if style.max_width.is_finite() {
181 Some(style.max_width)
182 } else {
183 None
184 },
185 max_height: if style.max_height.is_finite() {
186 Some(style.max_height)
187 } else {
188 None
189 },
190 flex_direction: style.flex_direction,
191 justify_content: style.justify_content,
192 align_items: style.align_items,
193 align_self: style.align_self,
194 flex_wrap: style.flex_wrap,
195 align_content: style.align_content,
196 flex_grow: style.flex_grow,
197 flex_shrink: style.flex_shrink,
198 flex_basis: size_constraint_to_str(&style.flex_basis),
199 gap: style.gap,
200 row_gap: style.row_gap,
201 column_gap: style.column_gap,
202 font_family: style.font_family.clone(),
203 font_size: style.font_size,
204 font_weight: style.font_weight,
205 font_style: style.font_style,
206 line_height: style.line_height,
207 text_align: style.text_align,
208 letter_spacing: style.letter_spacing,
209 word_spacing: style.word_spacing,
210 text_decoration: style.text_decoration,
211 text_transform: style.text_transform,
212 color: style.color,
213 background_color: style.background_color,
214 border_color: style.border_color,
215 border_style: style.border_style,
216 border_radius: style.border_radius,
217 opacity: style.opacity,
218 position: style.position,
219 top: style.top,
220 right: style.right,
221 bottom: style.bottom,
222 left: style.left,
223 overflow: style.overflow,
224 breakable: style.breakable,
225 break_before: style.break_before,
226 min_widow_lines: style.min_widow_lines,
227 min_orphan_lines: style.min_orphan_lines,
228 }
229 }
230}
231
232impl Default for ElementStyleInfo {
233 fn default() -> Self {
234 ElementStyleInfo {
235 margin: Edges::default(),
236 padding: Edges::default(),
237 border_width: Edges::default(),
238 width: None,
239 height: None,
240 min_width: None,
241 min_height: None,
242 max_width: None,
243 max_height: None,
244 flex_direction: FlexDirection::default(),
245 justify_content: JustifyContent::default(),
246 align_items: AlignItems::default(),
247 align_self: None,
248 flex_wrap: FlexWrap::default(),
249 align_content: AlignContent::default(),
250 flex_grow: 0.0,
251 flex_shrink: 1.0,
252 flex_basis: None,
253 gap: 0.0,
254 row_gap: 0.0,
255 column_gap: 0.0,
256 font_family: "Helvetica".to_string(),
257 font_size: 12.0,
258 font_weight: 400,
259 font_style: FontStyle::default(),
260 line_height: 1.4,
261 text_align: TextAlign::default(),
262 letter_spacing: 0.0,
263 word_spacing: 0.0,
264 text_decoration: TextDecoration::None,
265 text_transform: TextTransform::None,
266 color: Color::BLACK,
267 background_color: None,
268 border_color: EdgeValues::uniform(Color::BLACK),
269 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
270 border_radius: CornerValues::uniform(0.0),
271 opacity: 1.0,
272 position: Position::default(),
273 top: None,
274 right: None,
275 bottom: None,
276 left: None,
277 overflow: Overflow::default(),
278 breakable: false,
279 break_before: false,
280 min_widow_lines: 2,
281 min_orphan_lines: 2,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct ElementInfo {
290 pub x: f64,
291 pub y: f64,
292 pub width: f64,
293 pub height: f64,
294 pub kind: String,
296 pub node_type: String,
298 pub style: ElementStyleInfo,
300 pub children: Vec<ElementInfo>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub source_location: Option<SourceLocation>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub text_content: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub href: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub bookmark: Option<String>,
314}
315
316impl LayoutInfo {
317 pub fn from_pages(pages: &[LayoutPage]) -> Self {
319 LayoutInfo {
320 pages: pages
321 .iter()
322 .map(|page| {
323 let (page_w, page_h) = page.config.size.dimensions();
324 let content_x = page.config.margin.left;
325 let content_y = page.config.margin.top;
326 let content_width = page_w - page.config.margin.horizontal();
327 let content_height = page_h - page.config.margin.vertical();
328
329 let elements = Self::build_element_tree(&page.elements);
330
331 PageInfo {
332 width: page_w,
333 height: page_h,
334 content_x,
335 content_y,
336 content_width,
337 content_height,
338 elements,
339 }
340 })
341 .collect(),
342 }
343 }
344
345 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
346 elems
347 .iter()
348 .map(|elem| {
349 let kind = match &elem.draw {
350 DrawCommand::None => "None",
351 DrawCommand::Rect { .. } => "Rect",
352 DrawCommand::Text { .. } => "Text",
353 DrawCommand::Image { .. } => "Image",
354 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
355 DrawCommand::Svg { .. } => "Svg",
356 DrawCommand::Barcode { .. } => "Barcode",
357 DrawCommand::QrCode { .. } => "QrCode",
358 DrawCommand::Chart { .. } => "Chart",
359 DrawCommand::Watermark { .. } => "Watermark",
360 DrawCommand::FormField { .. } => "FormField",
361 };
362 let text_content = match &elem.draw {
363 DrawCommand::Text { lines, .. } => {
364 let text: String = lines
365 .iter()
366 .flat_map(|line| {
367 line.glyphs.iter().flat_map(|g| {
368 g.cluster_text.as_deref().unwrap_or("").chars().chain(
370 if g.cluster_text.is_none() {
371 Some(g.char_value)
372 } else {
373 None
374 },
375 )
376 })
377 })
378 .collect();
379 if text.is_empty() {
380 None
381 } else {
382 Some(text)
383 }
384 }
385 _ => None,
386 };
387 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
388 let style = elem
389 .resolved_style
390 .as_ref()
391 .map(ElementStyleInfo::from_resolved)
392 .unwrap_or_default();
393 ElementInfo {
394 x: elem.x,
395 y: elem.y,
396 width: elem.width,
397 height: elem.height,
398 kind: kind.to_string(),
399 node_type,
400 style,
401 children: Self::build_element_tree(&elem.children),
402 source_location: elem.source_location.clone(),
403 text_content,
404 href: elem.href.clone(),
405 bookmark: elem.bookmark.clone(),
406 }
407 })
408 .collect()
409 }
410}
411
412pub(crate) type FixedEntry = (Node, f64, Option<ResolvedStyle>);
422
423pub(crate) type WatermarkEntry = (Node, Option<ResolvedStyle>);
426
427#[derive(Debug, Clone)]
429pub struct LayoutPage {
430 pub width: f64,
431 pub height: f64,
432 pub elements: Vec<LayoutElement>,
433 pub(crate) fixed_header: Vec<FixedEntry>,
435 pub(crate) fixed_footer: Vec<FixedEntry>,
437 pub(crate) watermarks: Vec<WatermarkEntry>,
439 pub(crate) config: PageConfig,
441 pub(crate) page_name: Option<String>,
443}
444
445#[derive(Debug, Clone)]
447pub struct LayoutElement {
448 pub x: f64,
450 pub y: f64,
451 pub width: f64,
453 pub height: f64,
454 pub draw: DrawCommand,
456 pub children: Vec<LayoutElement>,
458 pub node_type: Option<String>,
460 pub resolved_style: Option<ResolvedStyle>,
462 pub source_location: Option<SourceLocation>,
464 pub href: Option<String>,
466 pub bookmark: Option<String>,
468 pub alt: Option<String>,
470 pub is_header_row: bool,
472 pub list_numbering: Option<&'static str>,
477 pub actual_text: Option<String>,
482 pub col_span: u32,
485 pub overflow: Overflow,
487 pub opacity: f64,
492}
493
494fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
497 match &node.kind {
498 NodeKind::Fixed {
499 pages,
500 page_name: only,
501 exclude_page_names,
502 ..
503 } => {
504 pages.applies(page_index)
505 && match only {
506 Some(n) => page_name == Some(n.as_str()),
507 None => true,
508 }
509 && !exclude_page_names
510 .iter()
511 .any(|n| page_name == Some(n.as_str()))
512 }
513 _ => true,
514 }
515}
516
517fn node_kind_name(kind: &NodeKind) -> &'static str {
519 match kind {
520 NodeKind::View => "View",
521 NodeKind::Text { .. } => "Text",
522 NodeKind::Heading { level: 1, .. } => "H1",
523 NodeKind::Heading { level: 2, .. } => "H2",
524 NodeKind::Heading { level: 3, .. } => "H3",
525 NodeKind::Heading { level: 4, .. } => "H4",
526 NodeKind::Heading { level: 5, .. } => "H5",
527 NodeKind::Heading { .. } => "H6",
531 NodeKind::List { .. } => "List",
532 NodeKind::ListItem => "ListItem",
533 NodeKind::Image { .. } => "Image",
534 NodeKind::Table { .. } => "Table",
535 NodeKind::TableRow { .. } => "TableRow",
536 NodeKind::TableCell { .. } => "TableCell",
537 NodeKind::Fixed {
538 position: FixedPosition::Header,
539 ..
540 } => "FixedHeader",
541 NodeKind::Fixed {
542 position: FixedPosition::Footer,
543 ..
544 } => "FixedFooter",
545 NodeKind::Page { .. } => "Page",
546 NodeKind::PageBreak => "PageBreak",
547 NodeKind::PageName { .. } => "PageName",
548 NodeKind::Svg { .. } => "Svg",
549 NodeKind::Canvas { .. } => "Canvas",
550 NodeKind::Barcode { .. } => "Barcode",
551 NodeKind::QrCode { .. } => "QrCode",
552 NodeKind::BarChart { .. } => "BarChart",
553 NodeKind::LineChart { .. } => "LineChart",
554 NodeKind::PieChart { .. } => "PieChart",
555 NodeKind::AreaChart { .. } => "AreaChart",
556 NodeKind::DotPlot { .. } => "DotPlot",
557 NodeKind::Watermark { .. } => "Watermark",
558 NodeKind::TextField { .. } => "TextField",
559 NodeKind::Checkbox { .. } => "Checkbox",
560 NodeKind::Dropdown { .. } => "Dropdown",
561 NodeKind::RadioButton { .. } => "RadioButton",
562 }
563}
564
565fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
584 node.bookmark.as_ref().map(|title| LayoutElement {
585 x,
586 y,
587 width: 0.0,
588 height: 0.0,
589 draw: DrawCommand::None,
590 children: vec![],
591 node_type: Some("Bookmark".to_string()),
592 resolved_style: None,
593 source_location: None,
594 href: None,
595 bookmark: Some(title.clone()),
596 alt: None,
597 is_header_row: false,
598 actual_text: None,
599 list_numbering: None,
600 col_span: 1,
601 overflow: Overflow::default(),
602 opacity: 1.0,
603 })
604}
605
606fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
613 if !ordered {
614 return match marker_type {
618 ListMarkerType::None => String::new(),
619 _ => "•".to_string(),
620 };
621 }
622 let body = match marker_type {
623 ListMarkerType::LowerAlpha => to_alpha(idx, false),
624 ListMarkerType::UpperAlpha => to_alpha(idx, true),
625 ListMarkerType::LowerRoman => to_roman(idx, false),
626 ListMarkerType::UpperRoman => to_roman(idx, true),
627 ListMarkerType::None => return String::new(),
628 _ => idx.to_string(),
631 };
632 format!("{body}.")
633}
634
635fn to_alpha(mut n: u32, upper: bool) -> String {
638 if n == 0 {
639 return String::new();
640 }
641 let base = if upper { b'A' } else { b'a' };
642 let mut bytes: Vec<u8> = Vec::new();
643 while n > 0 {
644 let rem = ((n - 1) % 26) as u8;
645 bytes.push(base + rem);
646 n = (n - 1) / 26;
647 }
648 bytes.reverse();
649 String::from_utf8(bytes).unwrap_or_default()
650}
651
652fn to_roman(n: u32, upper: bool) -> String {
656 if n == 0 || n > 3999 {
657 return n.to_string();
658 }
659 const UPPER: &[(&str, u32)] = &[
660 ("M", 1000),
661 ("CM", 900),
662 ("D", 500),
663 ("CD", 400),
664 ("C", 100),
665 ("XC", 90),
666 ("L", 50),
667 ("XL", 40),
668 ("X", 10),
669 ("IX", 9),
670 ("V", 5),
671 ("IV", 4),
672 ("I", 1),
673 ];
674 const LOWER: &[(&str, u32)] = &[
675 ("m", 1000),
676 ("cm", 900),
677 ("d", 500),
678 ("cd", 400),
679 ("c", 100),
680 ("xc", 90),
681 ("l", 50),
682 ("xl", 40),
683 ("x", 10),
684 ("ix", 9),
685 ("v", 5),
686 ("iv", 4),
687 ("i", 1),
688 ];
689 let table = if upper { UPPER } else { LOWER };
690 let mut out = String::new();
691 let mut remaining = n;
692 for &(sym, val) in table {
693 while remaining >= val {
694 out.push_str(sym);
695 remaining -= val;
696 }
697 }
698 out
699}
700
701fn compute_marker_gutter_width(
708 ordered: bool,
709 marker_type: ListMarkerType,
710 start: u32,
711 n_items: u32,
712 style: &ResolvedStyle,
713) -> f64 {
714 if matches!(marker_type, ListMarkerType::None) {
715 return 0.0;
716 }
717 let approx_char_w = style.font_size * 0.6;
719 let gap = 6.0_f64;
720 if !ordered {
721 return approx_char_w + gap;
723 }
724 let last_idx = start + n_items.saturating_sub(1);
726 let widest = match marker_type {
727 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
728 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
729 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
730 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
731 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
732 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
733 };
734 widest.chars().count() as f64 * approx_char_w + gap
735}
736
737#[derive(Debug, Clone)]
739pub enum FormFieldType {
740 TextField {
741 value: Option<String>,
742 placeholder: Option<String>,
743 multiline: bool,
744 password: bool,
745 read_only: bool,
746 max_length: Option<u32>,
747 font_size: f64,
748 },
749 Checkbox {
750 checked: bool,
751 read_only: bool,
752 },
753 Dropdown {
754 options: Vec<String>,
755 value: Option<String>,
756 read_only: bool,
757 font_size: f64,
758 },
759 RadioButton {
760 value: String,
761 checked: bool,
762 read_only: bool,
763 },
764}
765
766#[derive(Debug, Clone)]
768pub enum DrawCommand {
769 None,
771 Rect {
773 background: Option<Color>,
774 border_width: Edges,
775 border_color: EdgeValues<Color>,
776 border_style: EdgeValues<crate::style::BorderStyle>,
777 border_radius: CornerValues,
778 opacity: f64,
779 box_shadow: Option<Box<crate::style::BoxShadow>>,
782 background_gradient: Option<Box<crate::style::Background>>,
786 },
787 Text {
789 lines: Vec<TextLine>,
790 color: Color,
791 text_decoration: TextDecoration,
792 opacity: f64,
793 },
794 Image {
796 image_data: crate::image_loader::LoadedImage,
797 },
798 ImagePlaceholder,
800 Svg {
802 commands: Vec<crate::svg::SvgCommand>,
803 width: f64,
805 height: f64,
807 viewbox_min_x: f64,
811 viewbox_min_y: f64,
812 viewbox_width: f64,
813 viewbox_height: f64,
814 clip: bool,
816 },
817 Barcode {
819 bars: Vec<u8>,
820 bar_width: f64,
821 height: f64,
822 color: Color,
823 },
824 QrCode {
826 modules: Vec<Vec<bool>>,
827 module_size: f64,
828 color: Color,
829 },
830 Chart {
832 primitives: Vec<crate::chart::ChartPrimitive>,
833 },
834 Watermark {
836 lines: Vec<TextLine>,
837 color: Color,
838 opacity: f64,
839 angle_rad: f64,
840 font_family: String,
842 },
843 FormField {
845 field_type: FormFieldType,
846 name: String,
847 },
848}
849
850#[derive(Debug, Clone)]
851pub struct TextLine {
852 pub x: f64,
853 pub y: f64,
854 pub glyphs: Vec<PositionedGlyph>,
855 pub width: f64,
856 pub height: f64,
857 pub word_spacing: f64,
859}
860
861#[derive(Debug, Clone)]
862pub struct PositionedGlyph {
863 pub glyph_id: u16,
866 pub x_offset: f64,
868 pub y_offset: f64,
870 pub x_advance: f64,
872 pub font_size: f64,
873 pub font_family: Arc<str>,
879 pub font_weight: u32,
880 pub font_style: FontStyle,
881 pub char_value: char,
883 pub color: Option<Color>,
885 pub href: Option<String>,
887 pub text_decoration: TextDecoration,
889 pub letter_spacing: f64,
891 pub cluster_text: Option<String>,
894 pub ligature: bool,
905}
906
907fn shaped_glyph_x(
914 char_positions: &[f64],
915 cluster: usize,
916 pen: f64,
917 cluster_pen: f64,
918 sg: &shaping::ShapedGlyph,
919 scale: f64,
920) -> f64 {
921 let base = char_positions
922 .get(cluster)
923 .map_or(pen, |&p| p + (pen - cluster_pen));
924 base + sg.x_offset as f64 * scale
925}
926
927fn cluster_texts(shaped: &[shaping::ShapedGlyph], chars: &[char]) -> Vec<(Option<String>, bool)> {
943 let num_chars = chars.len();
944 let fewer_glyphs_than_chars = shaped.len() < num_chars;
945 let mut starts: Vec<u32> = shaped.iter().map(|g| g.cluster).collect();
946 starts.sort_unstable();
947
948 shaped
949 .iter()
950 .map(|sg| {
951 let first = starts.partition_point(|&c| c < sg.cluster);
952 let past = starts.partition_point(|&c| c <= sg.cluster);
953 let glyphs_in_cluster = past - first;
954 let start = sg.cluster as usize;
955 let end = starts
956 .get(past)
957 .map_or(num_chars, |&c| c as usize)
958 .min(num_chars);
959 if end <= start + 1 {
960 return (None, false);
961 }
962 let ligature = glyphs_in_cluster == 1;
963 if ligature || fewer_glyphs_than_chars {
964 (Some(chars[start..end].iter().collect()), ligature)
965 } else {
966 (None, false)
967 }
968 })
969 .collect()
970}
971
972fn offset_element_y(el: &mut LayoutElement, dy: f64) {
975 el.y += dy;
976 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
977 for line in lines.iter_mut() {
978 line.y += dy;
979 }
980 }
981 for child in &mut el.children {
982 offset_element_y(child, dy);
983 }
984}
985
986fn offset_element_x(el: &mut LayoutElement, dx: f64) {
988 el.x += dx;
989 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
990 for line in lines.iter_mut() {
991 line.x += dx;
992 }
993 }
994 for child in &mut el.children {
995 offset_element_x(child, dx);
996 }
997}
998
999fn reapply_justify_content(elem: &mut LayoutElement) {
1003 let style = match elem.resolved_style {
1004 Some(ref s) => s,
1005 None => return,
1006 };
1007 if matches!(style.justify_content, JustifyContent::FlexStart) {
1008 return;
1009 }
1010 if elem.children.is_empty() {
1011 return;
1012 }
1013
1014 let padding_top = style.padding.top + style.border_width.top;
1015 let padding_bottom = style.padding.bottom + style.border_width.bottom;
1016 let inner_h = elem.height - padding_top - padding_bottom;
1017 let content_top = elem.y + padding_top;
1018
1019 let last_child = &elem.children[elem.children.len() - 1];
1021 let children_bottom = last_child.y + last_child.height;
1022 let children_span = children_bottom - content_top;
1023 let slack = inner_h - children_span;
1024 if slack < 0.001 {
1025 return;
1026 }
1027
1028 let n = elem.children.len();
1029 let offsets: Vec<f64> = match style.justify_content {
1030 JustifyContent::FlexEnd => vec![slack; n],
1031 JustifyContent::Center => vec![slack / 2.0; n],
1032 JustifyContent::SpaceBetween => {
1033 if n <= 1 {
1034 vec![0.0; n]
1035 } else {
1036 let per_gap = slack / (n - 1) as f64;
1037 (0..n).map(|i| i as f64 * per_gap).collect()
1038 }
1039 }
1040 JustifyContent::SpaceAround => {
1041 let space = slack / n as f64;
1042 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
1043 }
1044 JustifyContent::SpaceEvenly => {
1045 let space = slack / (n + 1) as f64;
1046 (0..n).map(|i| (i + 1) as f64 * space).collect()
1047 }
1048 JustifyContent::FlexStart => unreachable!(),
1049 };
1050
1051 for (i, child) in elem.children.iter_mut().enumerate() {
1052 let dy = offsets[i];
1053 if dy.abs() > 0.001 {
1054 offset_element_y(child, dy);
1055 }
1056 }
1057}
1058
1059fn apply_text_transform(text: &str, transform: TextTransform) -> String {
1061 match transform {
1062 TextTransform::None => text.to_string(),
1063 TextTransform::Uppercase => text.to_uppercase(),
1064 TextTransform::Lowercase => text.to_lowercase(),
1065 TextTransform::Capitalize => {
1066 let mut result = String::with_capacity(text.len());
1067 let mut prev_is_whitespace = true;
1068 for ch in text.chars() {
1069 if prev_is_whitespace && ch.is_alphabetic() {
1070 for upper in ch.to_uppercase() {
1071 result.push(upper);
1072 }
1073 } else {
1074 result.push(ch);
1075 }
1076 prev_is_whitespace = ch.is_whitespace();
1077 }
1078 result
1079 }
1080 }
1081}
1082
1083pub const PAGE_NUMBER_SENTINEL: char = '\x02';
1087
1088pub const TOTAL_PAGES_SENTINEL: char = '\x03';
1090
1091fn substitute_page_placeholders(text: &str) -> String {
1096 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
1097 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
1098 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
1099 } else {
1100 text.to_string()
1101 }
1102}
1103
1104fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1107 match transform {
1108 TextTransform::None => ch,
1109 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1110 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1111 TextTransform::Capitalize => {
1112 if is_word_start && ch.is_alphabetic() {
1113 ch.to_uppercase().next().unwrap_or(ch)
1114 } else {
1115 ch
1116 }
1117 }
1118 }
1119}
1120
1121pub struct LayoutEngine {
1123 text_layout: TextLayout,
1124 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1125 warnings: RefCell<Vec<String>>,
1137 measure_check: bool,
1151}
1152
1153#[derive(Debug, Clone)]
1155struct PageCursor {
1156 config: PageConfig,
1157 base_config: PageConfig,
1160 page_index: usize,
1165 content_width: f64,
1166 content_height: f64,
1167 y: f64,
1168 elements: Vec<LayoutElement>,
1169 fixed_header: Vec<FixedEntry>,
1170 fixed_footer: Vec<FixedEntry>,
1171 watermarks: Vec<WatermarkEntry>,
1173 content_x: f64,
1174 content_y: f64,
1175 continuation_top_offset: f64,
1177 containing_block: (f64, f64, f64, f64),
1182 left_config: Option<PageConfig>,
1190 right_config: Option<PageConfig>,
1191 display_config: Option<PageConfig>,
1194 flow_anchor_left: f64,
1199 page_name: Option<String>,
1203 base_display: Option<PageConfig>,
1207 first_display: Option<PageConfig>,
1211 doc_base: PageConfig,
1214 doc_left: Option<PageConfig>,
1215 doc_right: Option<PageConfig>,
1216 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1218}
1219
1220impl PageCursor {
1221 fn new(config: &PageConfig) -> Self {
1222 let (page_w, page_h) = config.size.dimensions();
1223 let content_width = page_w - config.margin.horizontal();
1224 let content_height = page_h - config.margin.vertical();
1225
1226 Self {
1227 config: config.clone(),
1228 base_config: config.clone(),
1229 page_index: 0,
1230 content_width,
1231 content_height,
1232 y: 0.0,
1233 elements: Vec::new(),
1234 fixed_header: Vec::new(),
1235 fixed_footer: Vec::new(),
1236 watermarks: Vec::new(),
1237 content_x: config.margin.left,
1238 content_y: config.margin.top,
1239 continuation_top_offset: 0.0,
1240 containing_block: (
1241 config.margin.left,
1242 config.margin.top,
1243 content_width,
1244 content_height,
1245 ),
1246 left_config: None,
1247 right_config: None,
1248 display_config: None,
1249 flow_anchor_left: config.margin.left,
1250 page_name: None,
1251 base_display: None,
1252 first_display: None,
1253 doc_base: config.clone(),
1254 doc_left: None,
1255 doc_right: None,
1256 named_sets: std::collections::HashMap::new(),
1257 }
1258 }
1259
1260 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1267 if page_number == 1 {
1268 if let Some(first) = &self.first_display {
1269 return Some(first.clone());
1270 }
1271 }
1272 let parity = if page_number % 2 == 1 {
1273 self.right_config.clone()
1274 } else {
1275 self.left_config.clone()
1276 };
1277 parity.or_else(|| self.base_display.clone())
1278 }
1279
1280 fn fixed_applies(&self, node: &Node) -> bool {
1283 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1284 }
1285
1286 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1293 let set = name
1294 .as_deref()
1295 .and_then(|n| self.named_sets.get(n))
1296 .cloned();
1297 let real = set
1298 .as_ref()
1299 .map(|s| s.base.clone())
1300 .unwrap_or_else(|| self.doc_base.clone());
1301 let mut cursor = PageCursor::new(&real);
1302 cursor.page_index = if in_place {
1303 self.page_index
1304 } else {
1305 self.page_index + 1
1306 };
1307 cursor.page_name = name;
1308 cursor.doc_base = self.doc_base.clone();
1309 cursor.doc_left = self.doc_left.clone();
1310 cursor.doc_right = self.doc_right.clone();
1311 cursor.named_sets = self.named_sets.clone();
1312 match &set {
1313 Some(s) => {
1314 cursor.left_config = s.display_left.clone();
1315 cursor.right_config = s.display_right.clone();
1316 cursor.base_display = s.display.clone();
1317 cursor.first_display = s.display_first.clone();
1318 }
1319 None => {
1320 cursor.left_config = self.doc_left.clone();
1324 cursor.right_config = self.doc_right.clone();
1325 }
1326 }
1327 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1328 cursor.flow_anchor_left = self.flow_anchor_left;
1329 cursor.fixed_header = self.fixed_header.clone();
1330 cursor.fixed_footer = self.fixed_footer.clone();
1331 cursor.watermarks = self.watermarks.clone();
1332 cursor.continuation_top_offset = self.continuation_top_offset;
1333
1334 let header_height: f64 = cursor
1335 .fixed_header
1336 .iter()
1337 .filter(|(n, _, _)| cursor.fixed_applies(n))
1338 .map(|(_, h, _)| *h)
1339 .sum();
1340 cursor.y = header_height + cursor.continuation_top_offset;
1341 cursor
1342 }
1343
1344 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1347 let mut cursor = PageCursor::new(first);
1348 cursor.base_config = base.clone();
1349 cursor
1350 }
1351
1352 fn remaining_height(&self) -> f64 {
1353 let footer_height: f64 = self
1354 .fixed_footer
1355 .iter()
1356 .filter(|(n, _, _)| self.fixed_applies(n))
1357 .map(|(_, h, _)| *h)
1358 .sum();
1359 (self.content_height - self.y - footer_height).max(0.0)
1360 }
1361
1362 fn finalize(&self) -> LayoutPage {
1363 let (page_w, page_h) = self.config.size.dimensions();
1364
1365 let (elements, config) = match &self.display_config {
1371 Some(display) => {
1372 let dx = display.margin.left - self.flow_anchor_left;
1373 let mut elements = self.elements.clone();
1374 if dx != 0.0 {
1375 fn shift(els: &mut [LayoutElement], dx: f64) {
1376 for el in els {
1377 el.x += dx;
1378 shift(&mut el.children, dx);
1379 }
1380 }
1381 shift(&mut elements, dx);
1382 }
1383 (elements, display.clone())
1384 }
1385 None => (self.elements.clone(), self.config.clone()),
1386 };
1387
1388 LayoutPage {
1389 width: page_w,
1390 height: page_h,
1391 elements,
1392 fixed_header: self.fixed_header.clone(),
1393 fixed_footer: self.fixed_footer.clone(),
1394 watermarks: self.watermarks.clone(),
1395 config,
1396 page_name: self.page_name.clone(),
1397 }
1398 }
1399
1400 fn new_page(&self) -> Self {
1401 let mut cursor = PageCursor::new(&self.base_config);
1404 cursor.page_index = self.page_index + 1;
1405 cursor.page_name = self.page_name.clone();
1406 cursor.left_config = self.left_config.clone();
1407 cursor.right_config = self.right_config.clone();
1408 cursor.base_display = self.base_display.clone();
1409 cursor.first_display = self.first_display.clone();
1410 cursor.doc_base = self.doc_base.clone();
1411 cursor.doc_left = self.doc_left.clone();
1412 cursor.doc_right = self.doc_right.clone();
1413 cursor.named_sets = self.named_sets.clone();
1414 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1415 cursor.flow_anchor_left = self.flow_anchor_left;
1416 cursor.fixed_header = self.fixed_header.clone();
1417 cursor.fixed_footer = self.fixed_footer.clone();
1418 cursor.watermarks = self.watermarks.clone();
1419 cursor.continuation_top_offset = self.continuation_top_offset;
1420
1421 let header_height: f64 = cursor
1422 .fixed_header
1423 .iter()
1424 .filter(|(n, _, _)| cursor.fixed_applies(n))
1425 .map(|(_, h, _)| *h)
1426 .sum();
1427 cursor.y = header_height + cursor.continuation_top_offset;
1428
1429 cursor
1430 }
1431}
1432
1433impl Default for LayoutEngine {
1434 fn default() -> Self {
1435 Self::new()
1436 }
1437}
1438
1439impl LayoutEngine {
1440 pub fn new() -> Self {
1441 Self {
1442 text_layout: TextLayout::new(),
1443 image_dim_cache: RefCell::new(HashMap::new()),
1444 warnings: RefCell::new(Vec::new()),
1445 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1446 }
1447 }
1448
1449 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1451 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1452 return Some(*dims);
1453 }
1454 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1455 self.image_dim_cache
1456 .borrow_mut()
1457 .insert(src.to_string(), dims);
1458 Some(dims)
1459 } else {
1460 None
1461 }
1462 }
1463
1464 fn defect(&self, msg: String) {
1467 let mut w = self.warnings.borrow_mut();
1468 if !w.contains(&msg) {
1471 w.push(msg);
1472 }
1473 }
1474
1475 pub fn take_warnings(&self) -> Vec<String> {
1479 std::mem::take(&mut *self.warnings.borrow_mut())
1480 }
1481
1482 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1484 self.warnings.borrow_mut().clear();
1485 let mut pages: Vec<LayoutPage> = Vec::new();
1486 let mut cursor = match &document.first_page {
1487 Some(first) => PageCursor::new_first(first, &document.default_page),
1488 None => PageCursor::new(&document.default_page),
1489 };
1490 cursor.left_config = document.left_page.clone();
1496 cursor.right_config = document.right_page.clone();
1497 cursor.doc_base = document.default_page.clone();
1499 cursor.doc_left = document.left_page.clone();
1500 cursor.doc_right = document.right_page.clone();
1501 cursor.named_sets = document.named_pages.clone();
1502 if document.first_page.is_none() {
1503 cursor.display_config = cursor.display_for(1);
1504 }
1505
1506 let base = document.default_style.clone().unwrap_or_default();
1508 let root_style = Style {
1509 lang: base.lang.clone().or(document.metadata.lang.clone()),
1510 ..base
1511 }
1512 .resolve(None, cursor.content_width);
1513
1514 for node in &document.children {
1515 match &node.kind {
1516 NodeKind::Page { config } => {
1517 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1518 pages.push(cursor.finalize());
1519 }
1520 cursor = PageCursor::new(config);
1521 cursor.page_index = pages.len();
1522
1523 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1530 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1531
1532 let cx = cursor.content_x;
1533 let cw = cursor.content_width;
1534 self.layout_children(
1535 &node.children,
1536 &node.style,
1537 &mut cursor,
1538 &mut pages,
1539 cx,
1540 cw,
1541 Some(&page_root),
1542 font_context,
1543 );
1544 }
1545 NodeKind::PageBreak => {
1546 pages.push(cursor.finalize());
1547 cursor = cursor.new_page();
1548 }
1549 _ => {
1550 let cx = cursor.content_x;
1551 let cw = cursor.content_width;
1552 self.layout_node(
1553 node,
1554 &mut cursor,
1555 &mut pages,
1556 cx,
1557 cw,
1558 Some(&root_style),
1559 font_context,
1560 None,
1561 None,
1562 );
1563 }
1564 }
1565 }
1566
1567 if !cursor.elements.is_empty() {
1573 pages.push(cursor.finalize());
1574 }
1575
1576 fn has_visible_ink(els: &[LayoutElement]) -> bool {
1583 els.iter().any(|e| {
1584 let paints = match &e.draw {
1585 DrawCommand::None => false,
1586 DrawCommand::Rect {
1590 background,
1591 border_width,
1592 ..
1593 } => {
1594 background.is_some()
1595 || border_width.top > 0.0
1596 || border_width.right > 0.0
1597 || border_width.bottom > 0.0
1598 || border_width.left > 0.0
1599 }
1600 _ => true,
1601 };
1602 paints || has_visible_ink(&e.children)
1603 })
1604 }
1605 while pages.len() > 1 && !has_visible_ink(&pages.last().unwrap().elements) {
1606 pages.pop();
1607 }
1608
1609 self.inject_fixed_elements(&mut pages, font_context);
1610
1611 pages
1612 }
1613
1614 #[allow(clippy::too_many_arguments)]
1615 fn layout_node(
1616 &self,
1617 node: &Node,
1618 cursor: &mut PageCursor,
1619 pages: &mut Vec<LayoutPage>,
1620 x: f64,
1621 available_width: f64,
1622 parent_style: Option<&ResolvedStyle>,
1623 font_context: &FontContext,
1624 cross_axis_height: Option<f64>,
1625 forced_outer_width: Option<f64>,
1626 ) {
1627 let mut style = node.style.resolve(parent_style, available_width);
1628
1629 if let Some(h) = cross_axis_height {
1632 if matches!(style.height, SizeConstraint::Auto) {
1633 style.height = SizeConstraint::Fixed(h);
1634 }
1635 }
1636
1637 if let Some(w) = forced_outer_width {
1642 style.width = SizeConstraint::Fixed(w);
1643 }
1644
1645 if style.break_before && !cursor.elements.is_empty() {
1656 pages.push(cursor.finalize());
1657 *cursor = cursor.new_page();
1658 }
1659
1660 let elem_start = cursor.elements.len();
1663
1664 match &node.kind {
1665 NodeKind::PageBreak => {
1666 pages.push(cursor.finalize());
1667 *cursor = cursor.new_page();
1668 }
1669
1670 NodeKind::PageName { name } => {
1671 if cursor.page_name.as_deref() != name.as_deref() {
1677 let has_content = !cursor.elements.is_empty();
1684 if has_content {
1685 pages.push(cursor.finalize());
1686 }
1687 *cursor = cursor.renamed_page(name.clone(), !has_content);
1688 }
1689 }
1690
1691 NodeKind::Fixed {
1692 position, pages, ..
1693 } => {
1694 let height = self.measure_node_height(node, available_width, &style, font_context);
1695 match position {
1696 FixedPosition::Header => {
1697 cursor
1698 .fixed_header
1699 .push((node.clone(), height, parent_style.cloned()));
1700 let _ = pages;
1704 if cursor.fixed_applies(node) {
1705 cursor.y += height;
1706 }
1707 }
1708 FixedPosition::Footer => {
1709 cursor
1710 .fixed_footer
1711 .push((node.clone(), height, parent_style.cloned()));
1712 }
1713 }
1714 }
1715
1716 NodeKind::Watermark { .. } => {
1717 cursor
1719 .watermarks
1720 .push((node.clone(), parent_style.cloned()));
1721 }
1722
1723 NodeKind::TextField {
1724 name,
1725 value,
1726 placeholder,
1727 width: field_w,
1728 height: field_h,
1729 multiline,
1730 password,
1731 read_only,
1732 max_length,
1733 font_size,
1734 } => {
1735 self.layout_form_field(
1736 node,
1737 &style,
1738 cursor,
1739 pages,
1740 x,
1741 *field_w,
1742 *field_h,
1743 DrawCommand::FormField {
1744 field_type: FormFieldType::TextField {
1745 value: value.clone(),
1746 placeholder: placeholder.clone(),
1747 multiline: *multiline,
1748 password: *password,
1749 read_only: *read_only,
1750 max_length: *max_length,
1751 font_size: *font_size,
1752 },
1753 name: name.clone(),
1754 },
1755 "TextField",
1756 );
1757 }
1758
1759 NodeKind::Checkbox {
1760 name,
1761 checked,
1762 width: field_w,
1763 height: field_h,
1764 read_only,
1765 } => {
1766 self.layout_form_field(
1767 node,
1768 &style,
1769 cursor,
1770 pages,
1771 x,
1772 *field_w,
1773 *field_h,
1774 DrawCommand::FormField {
1775 field_type: FormFieldType::Checkbox {
1776 checked: *checked,
1777 read_only: *read_only,
1778 },
1779 name: name.clone(),
1780 },
1781 "Checkbox",
1782 );
1783 }
1784
1785 NodeKind::Dropdown {
1786 name,
1787 options,
1788 value,
1789 width: field_w,
1790 height: field_h,
1791 read_only,
1792 font_size,
1793 } => {
1794 self.layout_form_field(
1795 node,
1796 &style,
1797 cursor,
1798 pages,
1799 x,
1800 *field_w,
1801 *field_h,
1802 DrawCommand::FormField {
1803 field_type: FormFieldType::Dropdown {
1804 options: options.clone(),
1805 value: value.clone(),
1806 read_only: *read_only,
1807 font_size: *font_size,
1808 },
1809 name: name.clone(),
1810 },
1811 "Dropdown",
1812 );
1813 }
1814
1815 NodeKind::RadioButton {
1816 name,
1817 value,
1818 checked,
1819 width: field_w,
1820 height: field_h,
1821 read_only,
1822 } => {
1823 self.layout_form_field(
1824 node,
1825 &style,
1826 cursor,
1827 pages,
1828 x,
1829 *field_w,
1830 *field_h,
1831 DrawCommand::FormField {
1832 field_type: FormFieldType::RadioButton {
1833 value: value.clone(),
1834 checked: *checked,
1835 read_only: *read_only,
1836 },
1837 name: name.clone(),
1838 },
1839 "RadioButton",
1840 );
1841 }
1842
1843 NodeKind::Text {
1844 content,
1845 href,
1846 runs,
1847 } => {
1848 self.layout_text(
1849 content,
1850 href.as_deref(),
1851 runs,
1852 &style,
1853 cursor,
1854 pages,
1855 x,
1856 available_width,
1857 font_context,
1858 node.source_location.as_ref(),
1859 node.bookmark.as_deref(),
1860 None,
1861 );
1862 }
1863
1864 NodeKind::Heading {
1865 content,
1866 href,
1867 runs,
1868 ..
1869 } => {
1870 let heading_role = node_kind_name(&node.kind); self.layout_text(
1876 content,
1877 href.as_deref(),
1878 runs,
1879 &style,
1880 cursor,
1881 pages,
1882 x,
1883 available_width,
1884 font_context,
1885 node.source_location.as_ref(),
1886 node.bookmark.as_deref(),
1887 Some(heading_role),
1888 );
1889 }
1890
1891 NodeKind::List {
1892 ordered,
1893 marker_type,
1894 start,
1895 } => {
1896 self.layout_list(
1897 node,
1898 *ordered,
1899 *marker_type,
1900 *start,
1901 &style,
1902 cursor,
1903 pages,
1904 x,
1905 available_width,
1906 font_context,
1907 );
1908 }
1909
1910 NodeKind::ListItem => {
1911 self.layout_view(
1916 node,
1917 &style,
1918 cursor,
1919 pages,
1920 x,
1921 available_width,
1922 font_context,
1923 );
1924 }
1925
1926 NodeKind::Image { width, height, .. } => {
1927 self.layout_image(
1928 node,
1929 &style,
1930 cursor,
1931 pages,
1932 x,
1933 available_width,
1934 *width,
1935 *height,
1936 );
1937 }
1938
1939 NodeKind::Table { columns } => {
1940 self.layout_table(
1941 node,
1942 &style,
1943 columns,
1944 cursor,
1945 pages,
1946 x,
1947 available_width,
1948 font_context,
1949 );
1950 }
1951
1952 NodeKind::View | NodeKind::Page { .. } => {
1953 self.layout_view(
1954 node,
1955 &style,
1956 cursor,
1957 pages,
1958 x,
1959 available_width,
1960 font_context,
1961 );
1962 }
1963
1964 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1965 self.layout_view(
1966 node,
1967 &style,
1968 cursor,
1969 pages,
1970 x,
1971 available_width,
1972 font_context,
1973 );
1974 }
1975
1976 NodeKind::Svg {
1977 width: svg_w,
1978 height: svg_h,
1979 view_box,
1980 content,
1981 } => {
1982 self.layout_svg(
1983 node,
1984 &style,
1985 cursor,
1986 pages,
1987 x,
1988 available_width,
1989 *svg_w,
1990 *svg_h,
1991 view_box.as_deref(),
1992 content,
1993 );
1994 }
1995
1996 NodeKind::Barcode {
1997 data,
1998 format,
1999 width: explicit_width,
2000 height: bar_height,
2001 } => {
2002 self.layout_barcode(
2003 node,
2004 &style,
2005 cursor,
2006 pages,
2007 x,
2008 available_width,
2009 data,
2010 *format,
2011 *explicit_width,
2012 *bar_height,
2013 );
2014 }
2015
2016 NodeKind::QrCode {
2017 data,
2018 size: explicit_size,
2019 } => {
2020 self.layout_qrcode(
2021 node,
2022 &style,
2023 cursor,
2024 pages,
2025 x,
2026 available_width,
2027 data,
2028 *explicit_size,
2029 );
2030 }
2031
2032 NodeKind::Canvas {
2033 width: canvas_w,
2034 height: canvas_h,
2035 operations,
2036 } => {
2037 self.layout_canvas(
2038 node,
2039 &style,
2040 cursor,
2041 pages,
2042 x,
2043 available_width,
2044 *canvas_w,
2045 *canvas_h,
2046 operations,
2047 );
2048 }
2049
2050 NodeKind::BarChart {
2051 data,
2052 width: chart_w,
2053 height: chart_h,
2054 color,
2055 show_labels,
2056 show_values,
2057 show_grid,
2058 title,
2059 } => {
2060 let config = crate::chart::bar::BarChartConfig {
2061 color: color.clone(),
2062 show_labels: *show_labels,
2063 show_values: *show_values,
2064 show_grid: *show_grid,
2065 title: title.clone(),
2066 };
2067 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
2068 self.layout_chart(
2069 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
2070 );
2071 }
2072
2073 NodeKind::LineChart {
2074 series,
2075 labels,
2076 width: chart_w,
2077 height: chart_h,
2078 show_points,
2079 show_grid,
2080 title,
2081 } => {
2082 let config = crate::chart::line::LineChartConfig {
2083 show_points: *show_points,
2084 show_grid: *show_grid,
2085 title: title.clone(),
2086 };
2087 let primitives =
2088 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
2089 self.layout_chart(
2090 node,
2091 &style,
2092 cursor,
2093 pages,
2094 x,
2095 *chart_w,
2096 *chart_h,
2097 primitives,
2098 "LineChart",
2099 );
2100 }
2101
2102 NodeKind::PieChart {
2103 data,
2104 width: chart_w,
2105 height: chart_h,
2106 donut,
2107 show_legend,
2108 title,
2109 } => {
2110 let config = crate::chart::pie::PieChartConfig {
2111 donut: *donut,
2112 show_legend: *show_legend,
2113 title: title.clone(),
2114 };
2115 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
2116 self.layout_chart(
2117 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
2118 );
2119 }
2120
2121 NodeKind::AreaChart {
2122 series,
2123 labels,
2124 width: chart_w,
2125 height: chart_h,
2126 show_grid,
2127 title,
2128 } => {
2129 let config = crate::chart::area::AreaChartConfig {
2130 show_grid: *show_grid,
2131 title: title.clone(),
2132 };
2133 let primitives =
2134 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
2135 self.layout_chart(
2136 node,
2137 &style,
2138 cursor,
2139 pages,
2140 x,
2141 *chart_w,
2142 *chart_h,
2143 primitives,
2144 "AreaChart",
2145 );
2146 }
2147
2148 NodeKind::DotPlot {
2149 groups,
2150 width: chart_w,
2151 height: chart_h,
2152 x_min,
2153 x_max,
2154 y_min,
2155 y_max,
2156 x_label,
2157 y_label,
2158 show_legend,
2159 dot_size,
2160 } => {
2161 let config = crate::chart::dot::DotPlotConfig {
2162 x_min: *x_min,
2163 x_max: *x_max,
2164 y_min: *y_min,
2165 y_max: *y_max,
2166 x_label: x_label.clone(),
2167 y_label: y_label.clone(),
2168 show_legend: *show_legend,
2169 dot_size: *dot_size,
2170 };
2171 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2172 self.layout_chart(
2173 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2174 );
2175 }
2176 }
2177
2178 if matches!(style.position, Position::Relative)
2186 && (style.top.is_some()
2187 || style.left.is_some()
2188 || style.right.is_some()
2189 || style.bottom.is_some())
2190 {
2191 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2192 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2193 for el in &mut cursor.elements[elem_start..] {
2194 if dx != 0.0 {
2195 offset_element_x(el, dx);
2196 }
2197 if dy != 0.0 {
2198 offset_element_y(el, dy);
2199 }
2200 }
2201 }
2202 }
2203
2204 #[allow(clippy::too_many_arguments)]
2205 fn layout_view(
2206 &self,
2207 node: &Node,
2208 style: &ResolvedStyle,
2209 cursor: &mut PageCursor,
2210 pages: &mut Vec<LayoutPage>,
2211 x: f64,
2212 available_width: f64,
2213 font_context: &FontContext,
2214 ) {
2215 let padding = &style.padding;
2216 let margin = &style.margin.to_edges();
2217 let border = &style.border_width;
2218
2219 let outer_width = match style.width {
2220 SizeConstraint::Fixed(w) => w,
2221 SizeConstraint::Auto => available_width - margin.horizontal(),
2222 }
2223 .min(style.max_width)
2225 .max(style.min_width);
2226 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2227
2228 let children_height =
2229 self.measure_children_height(&node.children, inner_width, style, font_context);
2230 let total_height = match style.height {
2231 SizeConstraint::Fixed(h) => h,
2232 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2233 }
2234 .max(style.min_height);
2235
2236 let node_x = x + margin.left;
2237
2238 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2239
2240 if fits || !style.breakable {
2241 if !fits && !style.breakable {
2242 pages.push(cursor.finalize());
2243 *cursor = cursor.new_page();
2244 }
2245
2246 let rect_y = cursor.content_y + cursor.y + margin.top;
2248 let snapshot = cursor.elements.len();
2249
2250 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2255 cursor.elements.push(marker);
2256 }
2257
2258 let saved_y = cursor.y;
2259 cursor.y += margin.top + padding.top + border.top;
2260
2261 let pages_before = pages.len();
2262 let children_x = node_x + padding.left + border.left;
2263 let is_grid =
2264 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2265 if is_grid {
2266 self.layout_grid_children(
2267 &node.children,
2268 style,
2269 cursor,
2270 pages,
2271 children_x,
2272 inner_width,
2273 font_context,
2274 );
2275 } else {
2276 self.layout_children(
2277 &node.children,
2278 &node.style,
2279 cursor,
2280 pages,
2281 children_x,
2282 inner_width,
2283 Some(style),
2284 font_context,
2285 );
2286 }
2287
2288 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2290
2291 if self.measure_check
2298 && matches!(style.height, SizeConstraint::Auto)
2299 && pages.len() == pages_before
2300 && !child_elements.is_empty()
2301 {
2302 let content_top = rect_y + padding.top + border.top;
2303 let extent = child_elements
2304 .iter()
2305 .map(|el| el.y + el.height)
2306 .fold(f64::NEG_INFINITY, f64::max)
2307 - content_top;
2308 if extent > 0.0 && children_height - extent > 2.0 {
2309 self.defect(format!(
2310 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2311 node_kind_name(&node.kind),
2312 children_height - extent,
2313 ));
2314 }
2315 }
2316
2317 let rect_element = LayoutElement {
2318 x: node_x,
2319 y: rect_y,
2320 width: outer_width,
2321 height: total_height,
2322 draw: DrawCommand::Rect {
2323 background: style.background_color,
2324 border_width: style.border_width,
2325 border_color: style.border_color,
2326 border_style: style.border_style,
2327 border_radius: style.border_radius,
2328 opacity: 1.0,
2329 box_shadow: style.box_shadow.map(Box::new),
2330 background_gradient: style.background.clone().map(Box::new),
2331 },
2332 children: child_elements,
2333 node_type: Some(node_kind_name(&node.kind).to_string()),
2334 resolved_style: Some(style.clone()),
2335 source_location: node.source_location.clone(),
2336 href: node.href.clone(),
2337 bookmark: None,
2340 alt: None,
2341 is_header_row: false,
2342 actual_text: None,
2343 list_numbering: None,
2344 col_span: 1,
2345 overflow: style.overflow,
2346 opacity: style.opacity,
2347 };
2348 cursor.elements.push(rect_element);
2349
2350 cursor.y = saved_y + total_height + margin.vertical();
2351 } else {
2352 self.layout_breakable_view(
2353 node,
2354 style,
2355 cursor,
2356 pages,
2357 node_x,
2358 outer_width,
2359 inner_width,
2360 font_context,
2361 );
2362 }
2363 }
2364
2365 #[allow(clippy::too_many_arguments)]
2366 fn layout_breakable_view(
2367 &self,
2368 node: &Node,
2369 style: &ResolvedStyle,
2370 cursor: &mut PageCursor,
2371 pages: &mut Vec<LayoutPage>,
2372 node_x: f64,
2373 outer_width: f64,
2374 inner_width: f64,
2375 font_context: &FontContext,
2376 ) {
2377 let padding = &style.padding;
2378 let border = &style.border_width;
2379 let margin = &style.margin.to_edges();
2380
2381 let initial_page_count = pages.len();
2383 let snapshot = cursor.elements.len();
2384 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2385
2386 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2394 cursor.elements.push(marker);
2395 }
2396
2397 cursor.y += margin.top + padding.top + border.top;
2398 let prev_continuation_offset = cursor.continuation_top_offset;
2399 cursor.continuation_top_offset = padding.top + border.top;
2400
2401 let children_x = node_x + padding.left + border.left;
2402 let is_grid =
2403 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2404 if is_grid {
2405 self.layout_grid_children(
2406 &node.children,
2407 style,
2408 cursor,
2409 pages,
2410 children_x,
2411 inner_width,
2412 font_context,
2413 );
2414 } else {
2415 self.layout_children(
2416 &node.children,
2417 &node.style,
2418 cursor,
2419 pages,
2420 children_x,
2421 inner_width,
2422 Some(style),
2423 font_context,
2424 );
2425 }
2426
2427 cursor.continuation_top_offset = prev_continuation_offset;
2428
2429 let mut spans_by_height = false;
2458 if pages.len() == initial_page_count {
2459 if let SizeConstraint::Fixed(h) = style.height {
2460 let consumed =
2461 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2462 let mut owed = h - consumed;
2463 while owed > 0.0 {
2464 let room = cursor.remaining_height();
2465 if owed <= room {
2466 cursor.y += owed;
2467 break;
2468 }
2469 owed -= room;
2470 cursor.y += room;
2471 pages.push(cursor.finalize());
2472 *cursor = cursor.new_page();
2473 spans_by_height = true;
2474 }
2475 }
2476 }
2477
2478 let has_visual = style.background_color.is_some()
2480 || style.border_width.top > 0.0
2481 || style.border_width.right > 0.0
2482 || style.border_width.bottom > 0.0
2483 || style.border_width.left > 0.0;
2484 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2486
2487 if !needs_wrapper {
2488 cursor.y += padding.bottom + border.bottom + margin.bottom;
2490 return;
2491 }
2492
2493 let draw_cmd = DrawCommand::Rect {
2494 background: style.background_color,
2495 border_width: style.border_width,
2496 border_color: style.border_color,
2497 border_style: style.border_style,
2498 border_radius: style.border_radius,
2499 opacity: 1.0,
2500 box_shadow: style.box_shadow.map(Box::new),
2501 background_gradient: style.background.clone().map(Box::new),
2502 };
2503
2504 if pages.len() == initial_page_count {
2505 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2507 let rect_height =
2508 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2509 cursor.elements.push(LayoutElement {
2510 x: node_x,
2511 y: rect_start_y,
2512 width: outer_width,
2513 height: rect_height,
2514 draw: draw_cmd,
2515 children: child_elements,
2516 node_type: Some(node_kind_name(&node.kind).to_string()),
2517 resolved_style: Some(style.clone()),
2518 source_location: node.source_location.clone(),
2519 href: node.href.clone(),
2520 bookmark: None,
2525 alt: None,
2526 is_header_row: false,
2527 actual_text: None,
2528 list_numbering: None,
2529 col_span: 1,
2530 overflow: style.overflow,
2531 opacity: style.opacity,
2532 });
2533 } else {
2534 let page = &mut pages[initial_page_count];
2538 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
2539 let page_content_bottom =
2540 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2541 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
2542 if !our_elements.is_empty() {
2543 let rect_height = page_content_bottom - rect_start_y;
2544 page.elements.push(LayoutElement {
2545 x: node_x,
2546 y: rect_start_y,
2547 width: outer_width,
2548 height: rect_height,
2549 draw: draw_cmd.clone(),
2550 children: our_elements,
2551 node_type: Some(node_kind_name(&node.kind).to_string()),
2552 resolved_style: Some(style.clone()),
2553 source_location: node.source_location.clone(),
2554 href: node.href.clone(),
2555 bookmark: None,
2557 alt: None,
2558 is_header_row: false,
2559 actual_text: None,
2560 list_numbering: None,
2561 col_span: 1,
2562 overflow: Overflow::default(),
2563 opacity: 1.0,
2564 });
2565 }
2566
2567 for page in &mut pages[initial_page_count + 1..] {
2569 let header_h: f64 = page.fixed_header.iter().map(|(_, h, _)| *h).sum();
2570 let content_top = page.config.margin.top + header_h;
2571 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
2572 let content_bottom = page.config.margin.top
2573 + (page.height - page.config.margin.vertical())
2574 - footer_h;
2575 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2576 if !all_elements.is_empty() || spans_by_height {
2577 page.elements.push(LayoutElement {
2578 x: node_x,
2579 y: content_top,
2580 width: outer_width,
2581 height: content_bottom - content_top,
2582 draw: draw_cmd.clone(),
2583 children: all_elements,
2584 node_type: Some(node_kind_name(&node.kind).to_string()),
2585 resolved_style: Some(style.clone()),
2586 source_location: node.source_location.clone(),
2587 href: None,
2588 bookmark: None,
2589 alt: None,
2590 is_header_row: false,
2591 actual_text: None,
2592 list_numbering: None,
2593 col_span: 1,
2594 overflow: Overflow::default(),
2595 opacity: 1.0,
2596 });
2597 }
2598 }
2599
2600 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2602 if !all_elements.is_empty() || spans_by_height {
2603 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h, _)| *h).sum();
2604 let content_top = cursor.content_y + header_h;
2605 let rect_height =
2606 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2607 cursor.elements.push(LayoutElement {
2608 x: node_x,
2609 y: content_top,
2610 width: outer_width,
2611 height: rect_height,
2612 draw: draw_cmd,
2613 children: all_elements,
2614 node_type: Some(node_kind_name(&node.kind).to_string()),
2615 resolved_style: Some(style.clone()),
2616 source_location: node.source_location.clone(),
2617 href: None,
2618 bookmark: None,
2619 alt: None,
2620 is_header_row: false,
2621 actual_text: None,
2622 list_numbering: None,
2623 col_span: 1,
2624 overflow: Overflow::default(),
2625 opacity: 1.0,
2626 });
2627 }
2628 }
2629
2630 cursor.y += padding.bottom + border.bottom + margin.bottom;
2631 }
2632
2633 #[allow(clippy::too_many_arguments)]
2634 fn layout_children(
2635 &self,
2636 children: &[Node],
2637 _parent_raw_style: &Style,
2638 cursor: &mut PageCursor,
2639 pages: &mut Vec<LayoutPage>,
2640 content_x: f64,
2641 available_width: f64,
2642 parent_style: Option<&ResolvedStyle>,
2643 font_context: &FontContext,
2644 ) {
2645 let parent_box_y = cursor.content_y + cursor.y;
2647 let parent_box_x = content_x;
2648 let entry_page_index = pages.len();
2652
2653 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2659 let saved_cb = cursor.containing_block;
2660 if parent_positioned {
2661 let cb_height = parent_style
2662 .and_then(|ps| match ps.height {
2663 SizeConstraint::Fixed(h) => {
2664 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2665 }
2666 SizeConstraint::Auto => None,
2667 })
2668 .unwrap_or(saved_cb.3);
2669 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2670 }
2671
2672 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2674 .iter()
2675 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2676
2677 let direction = parent_style
2678 .map(|s| s.flex_direction)
2679 .unwrap_or(FlexDirection::Column);
2680
2681 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2682 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2683
2684 match direction {
2686 FlexDirection::Column | FlexDirection::ColumnReverse => {
2687 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2688 flow_children.into_iter().rev().collect()
2689 } else {
2690 flow_children
2691 };
2692
2693 let justify = parent_style
2694 .map(|s| s.justify_content)
2695 .unwrap_or(JustifyContent::FlexStart);
2696 let align = parent_style
2697 .map(|s| s.align_items)
2698 .unwrap_or(AlignItems::Stretch);
2699
2700 let start_y = cursor.y;
2701 let initial_pages = pages.len();
2702
2703 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2705
2706 for (i, child) in items.iter().enumerate() {
2707 if i > 0 {
2708 cursor.y += row_gap;
2709 }
2710 let child_start = cursor.elements.len();
2711
2712 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2715 let has_auto_h = child_margin.has_auto_horizontal();
2716
2717 let (child_x, layout_w) = if has_auto_h {
2724 let child_style = child.style.resolve(parent_style, available_width);
2725 let has_explicit_width =
2726 matches!(child_style.width, SizeConstraint::Fixed(_));
2727 let intrinsic = self
2728 .measure_intrinsic_width(child, &child_style, font_context)
2729 .min(available_width);
2730 let w = match child_style.width {
2731 SizeConstraint::Fixed(fw) => fw,
2732 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2737 (available_width - child_margin.horizontal())
2738 .min(child_style.max_width)
2739 }
2740 SizeConstraint::Auto => intrinsic,
2741 }
2742 .min(child_style.max_width)
2743 .max(child_style.min_width);
2744 let lw = if has_explicit_width {
2745 available_width
2746 } else {
2747 w
2748 };
2749 let fixed_h = child_margin.horizontal();
2750 let slack = (available_width - w - fixed_h).max(0.0);
2751 let auto_left = child_margin.left.is_auto();
2752 let auto_right = child_margin.right.is_auto();
2753 let ml = match (auto_left, auto_right) {
2754 (true, true) => slack / 2.0,
2755 (true, false) => slack,
2756 (false, true) => 0.0,
2757 (false, false) => 0.0,
2758 };
2759 (content_x + child_margin.left.resolve() + ml, lw)
2760 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2761 let child_style = child.style.resolve(parent_style, available_width);
2762 let has_explicit_width =
2763 matches!(child_style.width, SizeConstraint::Fixed(_));
2764 let intrinsic = self
2765 .measure_intrinsic_width(child, &child_style, font_context)
2766 .min(available_width);
2767 let w = match child_style.width {
2776 SizeConstraint::Fixed(fw) => fw,
2777 SizeConstraint::Auto => intrinsic,
2778 }
2779 .min(child_style.max_width)
2780 .max(child_style.min_width);
2781 let lw = if has_explicit_width {
2782 available_width
2783 } else {
2784 w
2785 };
2786 match align {
2787 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2788 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2789 _ => (content_x, available_width),
2790 }
2791 } else {
2792 (content_x, available_width)
2793 };
2794
2795 self.layout_node(
2796 child,
2797 cursor,
2798 pages,
2799 child_x,
2800 layout_w,
2801 parent_style,
2802 font_context,
2803 None,
2804 None,
2805 );
2806
2807 child_ranges.push((child_start, cursor.elements.len()));
2808 }
2809
2810 let container_inner_h: Option<f64> = parent_style
2813 .and_then(|ps| match ps.height {
2814 SizeConstraint::Fixed(h) => {
2815 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2816 }
2817 SizeConstraint::Auto => None,
2818 })
2819 .or_else(|| {
2820 if parent_style.is_none() {
2822 Some(cursor.content_height - start_y)
2823 } else {
2824 None
2825 }
2826 });
2827
2828 if let Some(inner_h) = container_inner_h {
2829 if pages.len() == initial_pages {
2830 let child_styles: Vec<ResolvedStyle> = items
2831 .iter()
2832 .map(|child| child.style.resolve(parent_style, available_width))
2833 .collect();
2834 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2835 if total_grow > 0.0 {
2836 let children_total = cursor.y - start_y;
2837 let slack = (inner_h - children_total).max(0.0);
2838 if slack > 0.0 {
2839 let mut cumulative_shift = 0.0_f64;
2840 for (i, cs) in child_styles.iter().enumerate() {
2841 let (start, end) = child_ranges[i];
2842 if cumulative_shift > 0.001 {
2843 for j in start..end {
2844 offset_element_y(
2845 &mut cursor.elements[j],
2846 cumulative_shift,
2847 );
2848 }
2849 }
2850 if cs.flex_grow > 0.0 {
2851 let extra = slack * (cs.flex_grow / total_grow);
2852 if start < end {
2854 let elem = &mut cursor.elements[end - 1];
2855 elem.height += extra;
2856 reapply_justify_content(elem);
2857 }
2858 cumulative_shift += extra;
2859 }
2860 }
2861 cursor.y += cumulative_shift;
2862 }
2863 }
2864 }
2865 }
2866
2867 if let Some(inner_h) = container_inner_h {
2874 if pages.len() == initial_pages {
2875 let auto_styles: Vec<ResolvedStyle> = items
2876 .iter()
2877 .map(|child| child.style.resolve(parent_style, available_width))
2878 .collect();
2879 let total_autos: usize = auto_styles
2880 .iter()
2881 .map(|s| {
2882 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2883 })
2884 .sum();
2885 if total_autos > 0 {
2886 let children_total = cursor.y - start_y;
2887 let total_slack = (inner_h - children_total).max(0.0);
2888 if total_slack > 0.0 {
2889 let per_auto = total_slack / total_autos as f64;
2890 let mut cumulative_shift = 0.0_f64;
2891 for (i, cs) in auto_styles.iter().enumerate() {
2892 let (start, end) = child_ranges[i];
2893 let mt_auto = cs.margin.top.is_auto();
2894 let mb_auto = cs.margin.bottom.is_auto();
2895 let this_child_shift =
2899 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2900 if this_child_shift > 0.001 {
2901 for j in start..end {
2902 offset_element_y(
2903 &mut cursor.elements[j],
2904 this_child_shift,
2905 );
2906 }
2907 }
2908 cumulative_shift =
2911 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2912 }
2913 cursor.y += cumulative_shift;
2914 }
2915 }
2916 }
2917 }
2918
2919 let needs_justify =
2921 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2922 if needs_justify {
2923 let justify_inner_h = container_inner_h.or_else(|| {
2925 parent_style.and_then(|ps| match ps.height {
2926 SizeConstraint::Fixed(h) => {
2927 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2928 }
2929 SizeConstraint::Auto => None,
2930 })
2931 });
2932 if let Some(inner_h) = justify_inner_h {
2933 let children_total = cursor.y - start_y;
2934 let slack = inner_h - children_total;
2935 if slack > 0.0 {
2936 let n = child_ranges.len();
2937 let offsets: Vec<f64> = match justify {
2938 JustifyContent::FlexEnd => vec![slack; n],
2939 JustifyContent::Center => vec![slack / 2.0; n],
2940 JustifyContent::SpaceBetween => {
2941 if n <= 1 {
2942 vec![0.0; n]
2943 } else {
2944 let per_gap = slack / (n - 1) as f64;
2945 (0..n).map(|i| i as f64 * per_gap).collect()
2946 }
2947 }
2948 JustifyContent::SpaceAround => {
2949 let space = slack / n as f64;
2950 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2951 }
2952 JustifyContent::SpaceEvenly => {
2953 let space = slack / (n + 1) as f64;
2954 (0..n).map(|i| (i + 1) as f64 * space).collect()
2955 }
2956 JustifyContent::FlexStart => vec![0.0; n],
2957 };
2958 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2959 let dy = offsets[i];
2960 if dy.abs() > 0.001 {
2961 for j in start..end {
2962 offset_element_y(&mut cursor.elements[j], dy);
2963 }
2964 }
2965 }
2966 cursor.y += *offsets.last().unwrap_or(&0.0);
2967 }
2968 }
2969 }
2970 }
2971
2972 FlexDirection::Row | FlexDirection::RowReverse => {
2973 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2974 self.layout_flex_row(
2975 &flow_owned,
2976 cursor,
2977 pages,
2978 content_x,
2979 available_width,
2980 parent_style,
2981 column_gap,
2982 row_gap,
2983 font_context,
2984 );
2985 }
2986 }
2987
2988 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
3002 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
3003 let ph = parent_style
3004 .and_then(|ps| match ps.height {
3005 SizeConstraint::Fixed(h) => {
3006 Some(h - ps.padding.vertical() - ps.border_width.vertical())
3007 }
3008 SizeConstraint::Auto => None,
3009 })
3010 .unwrap_or_else(|| {
3011 if parent_fragmented {
3012 let first = &pages[entry_page_index];
3013 (first.height - first.config.margin.bottom) - parent_box_y
3014 } else {
3015 cursor.content_y + cursor.y - parent_box_y
3016 }
3017 });
3018 (parent_box_x, parent_box_y, available_width, ph)
3019 } else {
3020 cursor.containing_block
3021 };
3022
3023 for abs_child in &abs_children {
3025 let abs_style = abs_child.style.resolve(parent_style, cb_w);
3026
3027 let child_width = match abs_style.width {
3029 SizeConstraint::Fixed(w) => w,
3030 SizeConstraint::Auto => {
3031 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
3033 (cb_w - l - r).max(0.0)
3034 } else {
3035 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
3036 }
3037 }
3038 };
3039
3040 let child_height = match abs_style.height {
3041 SizeConstraint::Fixed(h) => h,
3042 SizeConstraint::Auto => {
3043 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
3044 }
3045 };
3046
3047 let abs_margin = abs_style.margin.to_edges();
3055 let abs_x = if let Some(l) = abs_style.left {
3056 cb_x + l
3057 } else if let Some(r) = abs_style.right {
3058 cb_x + cb_w - r - child_width - abs_margin.horizontal()
3059 } else {
3060 cb_x
3061 };
3062
3063 let abs_y = if let Some(t) = abs_style.top {
3064 cb_y + t
3065 } else if let Some(b) = abs_style.bottom {
3066 cb_y + cb_h - b - child_height - abs_margin.vertical()
3067 } else {
3068 cb_y
3069 };
3070
3071 let mut abs_cursor = PageCursor::new(&cursor.config);
3073 abs_cursor.y = 0.0;
3074 abs_cursor.content_x = abs_x;
3075 abs_cursor.content_y = abs_y;
3076
3077 self.layout_node(
3078 abs_child,
3079 &mut abs_cursor,
3080 &mut Vec::new(),
3081 abs_x,
3082 child_width,
3083 parent_style,
3084 font_context,
3085 None,
3086 None,
3087 );
3088
3089 if parent_fragmented {
3093 pages[entry_page_index].elements.extend(abs_cursor.elements);
3094 } else {
3095 cursor.elements.extend(abs_cursor.elements);
3096 }
3097 }
3098
3099 cursor.containing_block = saved_cb;
3101 }
3102
3103 #[allow(clippy::too_many_arguments)]
3104 fn layout_flex_row(
3105 &self,
3106 children: &[Node],
3107 cursor: &mut PageCursor,
3108 pages: &mut Vec<LayoutPage>,
3109 content_x: f64,
3110 available_width: f64,
3111 parent_style: Option<&ResolvedStyle>,
3112 column_gap: f64,
3113 row_gap: f64,
3114 font_context: &FontContext,
3115 ) {
3116 if children.is_empty() {
3117 return;
3118 }
3119
3120 let flex_wrap = parent_style
3121 .map(|s| s.flex_wrap)
3122 .unwrap_or(FlexWrap::NoWrap);
3123
3124 let items: Vec<FlexItem> = children
3127 .iter()
3128 .map(|child| {
3129 let style = child.style.resolve(parent_style, available_width);
3130 let base_width = match style.flex_basis {
3131 SizeConstraint::Fixed(w) => w,
3132 SizeConstraint::Auto => match style.width {
3133 SizeConstraint::Fixed(w) => w,
3134 SizeConstraint::Auto => {
3135 self.measure_intrinsic_width(child, &style, font_context)
3136 }
3137 },
3138 };
3139 let min_content_width = self.measure_min_content_width(child, &style, font_context);
3140 FlexItem {
3141 node: child,
3142 style,
3143 base_width,
3144 min_content_width,
3145 }
3146 })
3147 .collect();
3148
3149 let outer_widths: Vec<f64> = items
3158 .iter()
3159 .map(|i| i.base_width + i.style.margin.horizontal())
3160 .collect();
3161 let lines = match flex_wrap {
3162 FlexWrap::NoWrap => {
3163 vec![flex::WrapLine {
3164 start: 0,
3165 end: items.len(),
3166 }]
3167 }
3168 FlexWrap::Wrap => {
3175 flex::partition_into_lines(&outer_widths, column_gap, available_width)
3176 }
3177 FlexWrap::WrapReverse => {
3178 let mut l = flex::partition_into_lines(&outer_widths, column_gap, available_width);
3179 l.reverse();
3180 l
3181 }
3182 };
3183
3184 if lines.is_empty() {
3185 return;
3186 }
3187
3188 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3190
3191 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3193
3194 let initial_pages_count = pages.len();
3195 let flex_start_y = cursor.y;
3196 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3197
3198 for (line_idx, line) in lines.iter().enumerate() {
3199 let parallel = true;
3214 let line_items = &items[line.start..line.end];
3215 let line_count = line.end - line.start;
3216 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3217 let distributable = available_width - line_gap;
3218
3219 let total_base: f64 = line_items
3233 .iter()
3234 .map(|i| i.base_width + i.style.margin.horizontal())
3235 .sum();
3236 let remaining = distributable - total_base;
3237
3238 if remaining > 0.0 {
3239 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3240 if total_grow > 0.0 {
3241 for (j, item) in line_items.iter().enumerate() {
3242 final_widths[line.start + j] =
3243 item.base_width + remaining * (item.style.flex_grow / total_grow);
3244 }
3245 }
3246 } else if remaining < 0.0 {
3247 let total_shrink: f64 = line_items
3248 .iter()
3249 .map(|i| i.style.flex_shrink * i.base_width)
3250 .sum();
3251 if total_shrink > 0.0 {
3252 for (j, item) in line_items.iter().enumerate() {
3253 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3254 let w = item.base_width + remaining * factor;
3255 let floor = item.style.min_width.max(item.min_content_width);
3256 final_widths[line.start + j] = w.max(floor);
3257 }
3258 }
3259 }
3260
3261 let mut line_height: f64 = line_items
3263 .iter()
3264 .enumerate()
3265 .map(|(j, item)| {
3266 let fw = final_widths[line.start + j];
3267 self.measure_node_height(item.node, fw, &item.style, font_context)
3268 + item.style.margin.vertical()
3269 })
3270 .fold(0.0f64, f64::max);
3271
3272 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3278 let baseline_ds: Vec<Option<f64>> = line_items
3279 .iter()
3280 .enumerate()
3281 .map(|(j, item)| {
3282 let align = item.style.align_self.unwrap_or(parent_align);
3283 if matches!(align, AlignItems::Baseline) {
3284 let fw = final_widths[line.start + j];
3285 Some(self.flex_item_baseline_distance(
3286 item.node,
3287 &item.style,
3288 fw,
3289 font_context,
3290 ))
3291 } else {
3292 None
3293 }
3294 })
3295 .collect();
3296 let line_baseline = baseline_ds
3297 .iter()
3298 .flatten()
3299 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3300 if let Some(bl) = line_baseline {
3301 for (j, item) in line_items.iter().enumerate() {
3302 if let Some(d) = baseline_ds[j] {
3303 let fw = final_widths[line.start + j];
3304 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3305 + item.style.margin.vertical();
3306 line_height = line_height.max(bl - d + h);
3307 }
3308 }
3309 }
3310
3311 if let Some(ps) = parent_style {
3329 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3330 if let SizeConstraint::Fixed(h) = ps.height {
3331 line_height = h - ps.padding.vertical() - ps.border_width.vertical();
3332 }
3333 }
3334 }
3335
3336 if !parallel && line_height > cursor.remaining_height() && cursor.y > 0.0 {
3358 pages.push(cursor.finalize());
3359 *cursor = cursor.new_page();
3360 }
3361
3362 if line_idx > 0 {
3364 cursor.y += row_gap;
3365 }
3366
3367 let row_start_y = cursor.y;
3368
3369 let actual_total: f64 = (line.start..line.end)
3378 .map(|i| final_widths[i] + items[i].style.margin.horizontal())
3379 .sum();
3380 let slack = available_width - actual_total - line_gap;
3381
3382 let (start_offset, between_extra) = match justify {
3383 JustifyContent::FlexStart => (0.0, 0.0),
3384 JustifyContent::FlexEnd => (slack, 0.0),
3385 JustifyContent::Center => (slack / 2.0, 0.0),
3386 JustifyContent::SpaceBetween => {
3387 if line_count > 1 {
3388 (0.0, slack / (line_count as f64 - 1.0))
3389 } else {
3390 (0.0, 0.0)
3391 }
3392 }
3393 JustifyContent::SpaceAround => {
3394 let s = slack / line_count as f64;
3395 (s / 2.0, s)
3396 }
3397 JustifyContent::SpaceEvenly => {
3398 let s = slack / (line_count as f64 + 1.0);
3399 (s, s)
3400 }
3401 };
3402
3403 let line_elem_start = cursor.elements.len();
3404 let mut x = content_x + start_offset;
3405
3406 let base_page_elements = if parallel {
3413 std::mem::take(&mut cursor.elements)
3414 } else {
3415 Vec::new()
3416 };
3417 let mut item_frags: Vec<ItemFragments> = Vec::with_capacity(line_items.len());
3418
3419 let line_start_pages = pages.len();
3430
3431 for (j, item) in line_items.iter().enumerate() {
3432 if j > 0 {
3433 x += column_gap + between_extra;
3434 }
3435
3436 let fw = final_widths[line.start + j];
3437
3438 let align = item
3439 .style
3440 .align_self
3441 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3442
3443 let item_height =
3444 self.measure_node_height(item.node, fw, &item.style, font_context);
3445
3446 let has_auto_v = item.style.margin.has_auto_vertical();
3448 let y_offset = if has_auto_v {
3449 let fixed_v = item.style.margin.vertical();
3450 let slack = (line_height - item_height - fixed_v).max(0.0);
3451 let auto_top = item.style.margin.top.is_auto();
3452 let auto_bottom = item.style.margin.bottom.is_auto();
3453 match (auto_top, auto_bottom) {
3454 (true, true) => slack / 2.0,
3455 (true, false) => slack,
3456 (false, true) => 0.0,
3457 (false, false) => 0.0,
3458 }
3459 } else {
3460 match align {
3461 AlignItems::FlexStart => 0.0,
3462 AlignItems::FlexEnd => {
3463 line_height - item_height - item.style.margin.vertical()
3464 }
3465 AlignItems::Center => {
3466 (line_height - item_height - item.style.margin.vertical()) / 2.0
3467 }
3468 AlignItems::Stretch => 0.0,
3469 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3470 (Some(bl), Some(d)) => bl - d,
3471 _ => 0.0,
3472 },
3473 }
3474 };
3475
3476 let cross_h = if matches!(align, AlignItems::Stretch)
3480 && matches!(item.style.height, SizeConstraint::Auto)
3481 && !has_auto_v
3482 {
3483 let stretch_h = line_height - item.style.margin.vertical();
3484 if stretch_h > item_height {
3485 Some(stretch_h)
3486 } else {
3487 None
3488 }
3489 } else {
3490 None
3491 };
3492
3493 let saved_y = cursor.y;
3494 cursor.y = row_start_y + y_offset;
3495
3496 let mut item_cursor = cursor.clone();
3529 let mut item_pages: Vec<LayoutPage> = Vec::new();
3530
3531 self.layout_node(
3532 item.node,
3533 &mut item_cursor,
3534 &mut item_pages,
3535 x,
3536 available_width,
3537 parent_style,
3538 font_context,
3539 cross_h,
3540 Some(fw),
3541 );
3542
3543 if parallel {
3544 let final_y = item_cursor.y;
3547 let tail = std::mem::take(&mut item_cursor.elements);
3548 item_frags.push(ItemFragments {
3549 pages: item_pages,
3550 tail,
3551 cursor: item_cursor,
3552 final_y,
3553 stretches: matches!(align, AlignItems::Stretch)
3554 && matches!(item.style.height, SizeConstraint::Auto)
3555 && !has_auto_v,
3556 });
3557 } else {
3558 pages.extend(item_pages);
3560 *cursor = item_cursor;
3561 }
3562
3563 cursor.y = saved_y;
3564 x += fw + item.style.margin.horizontal();
3571 }
3572
3573 if !parallel {
3580 if line_items.len() > 1 && pages.len() > line_start_pages {
3585 let near = line_items
3586 .iter()
3587 .find_map(|it| first_text_snippet(it.node))
3588 .map(|t| format!(" (row beginning \"{t}\")"))
3589 .unwrap_or_default();
3590 self.defect(format!(
3591 "render defect: a wrapped flex row crossing a page boundary lays its children sequentially — a column taller than the page does not continue side by side{near}"
3592 ));
3593 }
3594 cursor.y = row_start_y + line_height;
3595 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3596 continue;
3597 }
3598
3599 let frag_count = item_frags
3600 .iter()
3601 .map(|f| f.pages.len() + 1)
3602 .max()
3603 .unwrap_or(1);
3604
3605 let mut empty_bands: Vec<(usize, LayoutElement)> = Vec::new();
3617 if frag_count > 1 {
3618 let page_bottom = cursor.content_y + cursor.content_height;
3619 let last = frag_count - 1;
3620 let row_end = cursor.content_y + end_y_of(&item_frags, last);
3621 for f in item_frags.iter_mut() {
3622 if !f.stretches {
3623 continue;
3624 }
3625 let own_last = f.pages.len();
3626 for (k, page) in f.pages.iter_mut().enumerate() {
3627 let bottom = if k == last { row_end } else { page_bottom };
3628 stretch_fragment(&mut page.elements, bottom);
3629 }
3630 let bottom = if own_last == last {
3631 row_end
3632 } else {
3633 page_bottom
3634 };
3635 stretch_fragment(&mut f.tail, bottom);
3636
3637 if own_last < last {
3650 let template = f
3651 .tail
3652 .iter()
3653 .chain(f.pages.iter().flat_map(|p| p.elements.iter()))
3654 .find(|el| paints_a_box(el))
3655 .cloned();
3656 if let Some(t) = template {
3657 for k in (own_last + 1)..=last {
3658 let bottom = if k == last { row_end } else { page_bottom };
3659 let mut band = t.clone();
3660 band.children.clear();
3661 band.y = cursor.content_y;
3662 band.height = (bottom - cursor.content_y).max(0.0);
3663 empty_bands.push((k, band));
3667 }
3668 }
3669 }
3670 }
3671 }
3672
3673 let mut merged: Vec<Vec<LayoutElement>> = vec![Vec::new(); frag_count];
3674 merged[0] = base_page_elements;
3676 for (k, band) in empty_bands {
3679 merged[k].push(band);
3680 }
3681 for f in item_frags.iter_mut() {
3682 for (k, page) in f.pages.iter_mut().enumerate() {
3683 merged[k].append(&mut page.elements);
3684 }
3685 let tail_index = f.pages.len();
3688 let mut tail = std::mem::take(&mut f.tail);
3689 merged[tail_index].append(&mut tail);
3690 }
3691
3692 if frag_count == 1 {
3693 cursor.elements = std::mem::take(&mut merged[0]);
3697 cursor.y = row_start_y + line_height;
3698 } else {
3699 let carrier_idx = item_frags
3710 .iter()
3711 .enumerate()
3712 .max_by_key(|(_, f)| f.pages.len())
3713 .map(|(i, _)| i)
3714 .expect("a flex line has at least one item");
3715 let mut carrier = item_frags.swap_remove(carrier_idx);
3716
3717 for (k, page) in carrier.pages.iter_mut().enumerate() {
3718 page.elements = std::mem::take(&mut merged[k]);
3719 }
3720 pages.append(&mut carrier.pages);
3721
3722 let last = frag_count - 1;
3725 let end_y = item_frags
3726 .iter()
3727 .chain(std::iter::once(&carrier))
3728 .filter(|f| f.pages.len() == last)
3729 .map(|f| f.final_y)
3730 .fold(f64::MIN, f64::max);
3731
3732 let mut merged_tail = std::mem::take(&mut merged[last]);
3733 *cursor = carrier.cursor;
3734 cursor.elements.clear();
3735 cursor.elements.append(&mut merged_tail);
3736 cursor.y = end_y;
3737 }
3738
3739 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3740 }
3741
3742 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3744 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3745 if !matches!(align_content, AlignContent::FlexStart)
3746 && !matches!(flex_wrap, FlexWrap::NoWrap)
3747 {
3748 if let Some(parent) = parent_style {
3749 if let SizeConstraint::Fixed(container_h) = parent.height {
3750 let inner_h = container_h
3751 - parent.padding.vertical()
3752 - parent.border_width.vertical();
3753 let total_used = cursor.y - flex_start_y;
3754 let slack = inner_h - total_used;
3755 if slack > 0.0 {
3756 let n = line_infos.len();
3757 let offsets: Vec<f64> = match align_content {
3758 AlignContent::FlexEnd => vec![slack; n],
3759 AlignContent::Center => vec![slack / 2.0; n],
3760 AlignContent::SpaceBetween => {
3761 if n <= 1 {
3762 vec![0.0; n]
3763 } else {
3764 let per_gap = slack / (n - 1) as f64;
3765 (0..n).map(|i| i as f64 * per_gap).collect()
3766 }
3767 }
3768 AlignContent::SpaceAround => {
3769 let space = slack / n as f64;
3770 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3771 }
3772 AlignContent::SpaceEvenly => {
3773 let space = slack / (n + 1) as f64;
3774 (0..n).map(|i| (i + 1) as f64 * space).collect()
3775 }
3776 AlignContent::Stretch => {
3777 let extra = slack / n as f64;
3778 (0..n).map(|i| i as f64 * extra).collect()
3779 }
3780 AlignContent::FlexStart => vec![0.0; n],
3781 };
3782 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3783 let dy = offsets[i];
3784 if dy.abs() > 0.001 {
3785 for j in start..end {
3786 offset_element_y(&mut cursor.elements[j], dy);
3787 }
3788 }
3789 }
3790 cursor.y += *offsets.last().unwrap_or(&0.0);
3791 }
3792 }
3793 }
3794 }
3795 }
3796 }
3797
3798 #[allow(clippy::too_many_arguments)]
3801 fn layout_list(
3802 &self,
3803 node: &Node,
3804 ordered: bool,
3805 marker_type: ListMarkerType,
3806 start: u32,
3807 style: &ResolvedStyle,
3808 cursor: &mut PageCursor,
3809 pages: &mut Vec<LayoutPage>,
3810 x: f64,
3811 available_width: f64,
3812 font_context: &FontContext,
3813 ) {
3814 let margin = &style.margin.to_edges();
3815 let padding = &style.padding;
3816
3817 cursor.y += margin.top;
3818
3819 let list_x = x + margin.left;
3820 let outer_width = available_width - margin.horizontal();
3821 let inner_width = outer_width - padding.horizontal();
3822
3823 let n_items = node
3826 .children
3827 .iter()
3828 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3829 .count() as u32;
3830
3831 let marker_gutter =
3832 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3833
3834 let list_inner_x = list_x + padding.left;
3835 let content_x = list_inner_x + marker_gutter;
3836 let content_width = (inner_width - marker_gutter).max(0.0);
3837
3838 let snapshot = cursor.elements.len();
3841 let list_start_y = cursor.content_y + cursor.y;
3842 cursor.y += padding.top;
3843
3844 let mut item_index: u32 = 0;
3845 for child in &node.children {
3846 if !matches!(child.kind, NodeKind::ListItem) {
3847 continue;
3848 }
3849 let marker_idx = start + item_index;
3850 self.layout_list_item(
3851 child,
3852 marker_idx,
3853 ordered,
3854 marker_type,
3855 marker_gutter,
3856 style,
3857 cursor,
3858 pages,
3859 list_inner_x,
3860 content_x,
3861 content_width,
3862 font_context,
3863 );
3864 item_index += 1;
3865 }
3866
3867 cursor.y += padding.bottom;
3868
3869 let item_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
3871 let list_height = cursor.content_y + cursor.y - list_start_y;
3872 cursor.elements.push(LayoutElement {
3873 x: list_x,
3874 y: list_start_y,
3875 width: outer_width,
3876 height: list_height,
3877 draw: DrawCommand::None,
3878 children: item_elements,
3879 node_type: Some("List".to_string()),
3880 resolved_style: Some(style.clone()),
3881 source_location: node.source_location.clone(),
3882 href: None,
3883 bookmark: node.bookmark.clone(),
3884 alt: None,
3885 is_header_row: false,
3886 actual_text: None,
3890 list_numbering: match marker_type {
3891 ListMarkerType::None => None,
3892 ListMarkerType::Disc => Some("Disc"),
3893 ListMarkerType::Circle => Some("Circle"),
3894 ListMarkerType::Square => Some("Square"),
3895 ListMarkerType::Decimal => Some("Decimal"),
3896 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3897 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3898 ListMarkerType::LowerRoman => Some("LowerRoman"),
3899 ListMarkerType::UpperRoman => Some("UpperRoman"),
3900 },
3901 col_span: 1,
3902 overflow: style.overflow,
3903 opacity: style.opacity,
3904 });
3905
3906 cursor.y += margin.bottom;
3907 }
3908
3909 #[allow(clippy::too_many_arguments)]
3910 fn layout_list_item(
3911 &self,
3912 item: &Node,
3913 marker_idx: u32,
3914 ordered: bool,
3915 marker_type: ListMarkerType,
3916 marker_gutter: f64,
3917 parent_style: &ResolvedStyle,
3918 cursor: &mut PageCursor,
3919 pages: &mut Vec<LayoutPage>,
3920 list_inner_x: f64,
3921 content_x: f64,
3922 content_width: f64,
3923 font_context: &FontContext,
3924 ) {
3925 let item_style = item.style.resolve(Some(parent_style), content_width);
3926 let item_margin = item_style.margin.to_edges();
3927
3928 cursor.y += item_margin.top;
3929 let item_start_y = cursor.content_y + cursor.y;
3930 let item_snapshot = cursor.elements.len();
3931
3932 let marker_str = format_marker(marker_idx, ordered, marker_type);
3936 if !marker_str.is_empty() {
3937 let saved_y = cursor.y;
3938 self.layout_text(
3939 &marker_str,
3940 None,
3941 &[],
3942 &item_style,
3943 cursor,
3944 pages,
3945 list_inner_x,
3946 marker_gutter,
3947 font_context,
3948 None,
3949 None,
3950 Some("Lbl"),
3951 );
3952 cursor.y = saved_y;
3953 }
3954
3955 self.layout_children(
3961 &item.children,
3962 &item.style,
3963 cursor,
3964 pages,
3965 content_x,
3966 content_width,
3967 Some(&item_style),
3968 font_context,
3969 );
3970
3971 let item_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, item_snapshot);
3974 let item_height = cursor.content_y + cursor.y - item_start_y;
3975 let item_width = content_x + content_width - list_inner_x;
3976 cursor.elements.push(LayoutElement {
3977 x: list_inner_x,
3978 y: item_start_y,
3979 width: item_width,
3980 height: item_height,
3981 draw: DrawCommand::None,
3982 children: item_children,
3983 node_type: Some("ListItem".to_string()),
3984 resolved_style: Some(item_style.clone()),
3985 source_location: item.source_location.clone(),
3986 href: None,
3987 bookmark: item.bookmark.clone(),
3988 alt: None,
3989 is_header_row: false,
3990 actual_text: None,
3991 list_numbering: None,
3992 col_span: 1,
3993 overflow: item_style.overflow,
3994 opacity: item_style.opacity,
3995 });
3996
3997 cursor.y += item_margin.bottom;
3998 }
3999
4000 #[allow(clippy::too_many_arguments)]
4001 fn layout_table(
4002 &self,
4003 node: &Node,
4004 style: &ResolvedStyle,
4005 column_defs: &[ColumnDef],
4006 cursor: &mut PageCursor,
4007 pages: &mut Vec<LayoutPage>,
4008 x: f64,
4009 available_width: f64,
4010 font_context: &FontContext,
4011 ) {
4012 let padding = &style.padding;
4013 let margin = &style.margin.to_edges();
4014 let border = &style.border_width;
4015
4016 let table_x = x + margin.left;
4017 let table_width = match style.width {
4018 SizeConstraint::Fixed(w) => w,
4019 SizeConstraint::Auto => available_width - margin.horizontal(),
4020 };
4021 let inner_width = table_width - padding.horizontal() - border.horizontal();
4022
4023 let col_widths = self.resolve_column_widths(
4024 column_defs,
4025 inner_width,
4026 &node.children,
4027 style,
4028 font_context,
4029 );
4030
4031 let all_offsets = Self::table_column_offsets(&node.children);
4035 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
4036 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
4037
4038 for (child, offs) in node.children.iter().zip(&all_offsets) {
4039 match &child.kind {
4040 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
4041 _ => body_rows.push((child, offs)),
4042 }
4043 }
4044
4045 if !style.breakable {
4051 let total_height: f64 = node
4052 .children
4053 .iter()
4054 .zip(&all_offsets)
4055 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4056 .sum::<f64>()
4057 + padding.vertical()
4058 + border.vertical();
4059 let fresh_page_available = cursor.content_height
4060 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4061 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4062 if total_height > cursor.remaining_height()
4063 && total_height <= fresh_page_available
4064 && cursor.y > 0.0
4065 {
4066 pages.push(cursor.finalize());
4067 *cursor = cursor.new_page();
4068 }
4069 }
4070
4071 let initial_page_count = pages.len();
4078 let snapshot = cursor.elements.len();
4079 let rect_start_y = cursor.content_y + cursor.y + margin.top;
4080
4081 cursor.y += margin.top + padding.top + border.top;
4082
4083 let cell_x_start = table_x + padding.left + border.left;
4084
4085 if !header_rows.is_empty() {
4110 let total_header_h: f64 = header_rows
4111 .iter()
4112 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4113 .sum();
4114 let first_body_h = body_rows
4115 .first()
4116 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4117 .unwrap_or(0.0);
4118
4119 let needed = total_header_h + first_body_h;
4120 let fresh_page_available = cursor.content_height
4121 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4122 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4123
4124 if needed > cursor.remaining_height() && needed <= fresh_page_available {
4125 pages.push(cursor.finalize());
4126 *cursor = cursor.new_page();
4127 cursor.y += padding.top + border.top;
4128 }
4129 }
4130
4131 for (header_row, offs) in &header_rows {
4132 self.layout_table_row(
4133 header_row,
4134 &col_widths,
4135 offs,
4136 style,
4137 cursor,
4138 cell_x_start,
4139 font_context,
4140 pages,
4141 );
4142 }
4143
4144 for (body_row, offs) in &body_rows {
4145 let row_height =
4146 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
4147
4148 let fresh_page_available = cursor.content_height
4156 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4157 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4158 if row_height > fresh_page_available {
4159 self.defect(format!(
4160 "render defect: table row needs {row_height:.0}pt but a page holds {fresh_page_available:.0}pt — rows are atomic, so it is placed whole and overflows",
4161 ));
4162 }
4163 if row_height > cursor.remaining_height()
4164 && cursor.remaining_height() < fresh_page_available - 0.5
4165 {
4166 pages.push(cursor.finalize());
4167 *cursor = cursor.new_page();
4168
4169 cursor.y += padding.top + border.top;
4170 for (header_row, h_offs) in &header_rows {
4171 self.layout_table_row(
4172 header_row,
4173 &col_widths,
4174 h_offs,
4175 style,
4176 cursor,
4177 cell_x_start,
4178 font_context,
4179 pages,
4180 );
4181 }
4182 }
4183
4184 self.layout_table_row(
4185 body_row,
4186 &col_widths,
4187 offs,
4188 style,
4189 cursor,
4190 cell_x_start,
4191 font_context,
4192 pages,
4193 );
4194 }
4195
4196 let has_visual = style.background_color.is_some()
4200 || style.background.is_some()
4201 || style.border_width.top > 0.0
4202 || style.border_width.right > 0.0
4203 || style.border_width.bottom > 0.0
4204 || style.border_width.left > 0.0;
4205 let draw_cmd = if has_visual {
4206 DrawCommand::Rect {
4207 background: style.background_color,
4208 border_width: style.border_width,
4209 border_color: style.border_color,
4210 border_style: style.border_style,
4211 border_radius: style.border_radius,
4212 opacity: 1.0,
4213 box_shadow: style.box_shadow.map(Box::new),
4214 background_gradient: style.background.clone().map(Box::new),
4215 }
4216 } else {
4217 DrawCommand::None
4218 };
4219 let make_wrapper =
4220 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
4221 x: table_x,
4222 y,
4223 width: table_width,
4224 height,
4225 draw,
4226 children,
4227 node_type: Some(node_kind_name(&node.kind).to_string()),
4228 resolved_style: Some(style.clone()),
4229 source_location: node.source_location.clone(),
4230 href: node.href.clone(),
4231 bookmark: None,
4232 alt: None,
4233 is_header_row: false,
4234 actual_text: None,
4235 list_numbering: None,
4236 col_span: 1,
4237 overflow: Overflow::default(),
4238 opacity: style.opacity,
4239 };
4240
4241 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
4242
4243 if pages.len() == initial_page_count {
4244 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4246 cursor.elements.push(make_wrapper(
4247 rect_start_y,
4248 table_bottom_y - rect_start_y,
4249 child_elements,
4250 draw_cmd,
4251 ));
4252 } else {
4253 let page = &mut pages[initial_page_count];
4272 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
4273 let page_content_bottom =
4274 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
4275 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
4276 if !our_elements.is_empty() {
4277 page.elements.push(make_wrapper(
4278 rect_start_y,
4279 page_content_bottom - rect_start_y,
4280 our_elements,
4281 draw_cmd.clone(),
4282 ));
4283 }
4284
4285 for page in &mut pages[initial_page_count + 1..] {
4287 let header_h: f64 = page.fixed_header.iter().map(|(_, h, _)| *h).sum();
4288 let content_top = page.config.margin.top + header_h;
4289 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
4290 let content_bottom = page.config.margin.top
4291 + (page.height - page.config.margin.vertical())
4292 - footer_h;
4293 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
4294 if !all_elements.is_empty() {
4295 page.elements.push(make_wrapper(
4296 content_top,
4297 content_bottom - content_top,
4298 all_elements,
4299 draw_cmd.clone(),
4300 ));
4301 }
4302 }
4303
4304 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
4306 if !all_elements.is_empty() {
4307 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h, _)| *h).sum();
4308 let content_top = cursor.content_y + header_h;
4309 cursor.elements.push(make_wrapper(
4310 content_top,
4311 table_bottom_y - content_top,
4312 all_elements,
4313 draw_cmd,
4314 ));
4315 }
4316 }
4317
4318 cursor.y += padding.bottom + border.bottom + margin.bottom;
4319 }
4320
4321 fn subtree_forces_break(node: &Node) -> bool {
4331 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
4332 return true;
4333 }
4334 if node.style.break_before == Some(true) {
4335 return true;
4336 }
4337 node.children.iter().any(Self::subtree_forces_break)
4338 }
4339
4340 fn occupancy_column_count(children: &[Node]) -> usize {
4346 let offsets = Self::table_column_offsets(children);
4347 children
4348 .iter()
4349 .zip(&offsets)
4350 .map(|(row, offs)| {
4351 row.children
4352 .iter()
4353 .zip(offs)
4354 .map(|(cell, &start)| start + Self::cell_col_span(cell))
4355 .max()
4356 .unwrap_or(0)
4357 })
4358 .max()
4359 .unwrap_or(1)
4360 .max(1)
4361 }
4362
4363 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
4373 fn spans(cell: &Node) -> (usize, u32) {
4374 match &cell.kind {
4375 NodeKind::TableCell { col_span, row_span } => {
4376 ((*col_span).max(1) as usize, (*row_span).max(1))
4377 }
4378 _ => (1, 1),
4379 }
4380 }
4381 let mut pending: Vec<u32> = Vec::new();
4382 let mut out = Vec::with_capacity(rows.len());
4383 for row in rows {
4384 let mut offsets = Vec::with_capacity(row.children.len());
4385 let mut col = 0usize;
4386 for cell in &row.children {
4387 let (span, rspan) = spans(cell);
4388 while pending.get(col).copied().unwrap_or(0) > 0 {
4389 col += 1;
4390 }
4391 offsets.push(col);
4392 if rspan > 1 {
4393 if pending.len() < col + span {
4394 pending.resize(col + span, 0);
4395 }
4396 for slot in pending.iter_mut().take(col + span).skip(col) {
4397 *slot = (*slot).max(rspan);
4398 }
4399 }
4400 col += span;
4401 }
4402 out.push(offsets);
4403 for p in pending.iter_mut() {
4404 *p = p.saturating_sub(1);
4405 }
4406 }
4407 out
4408 }
4409
4410 #[allow(clippy::too_many_arguments)]
4411 fn layout_table_row(
4412 &self,
4413 row: &Node,
4414 col_widths: &[f64],
4415 col_offsets: &[usize],
4416 parent_style: &ResolvedStyle,
4417 cursor: &mut PageCursor,
4418 start_x: f64,
4419 font_context: &FontContext,
4420 pages: &mut Vec<LayoutPage>,
4421 ) {
4422 let row_style = row
4423 .style
4424 .resolve(Some(parent_style), col_widths.iter().sum());
4425
4426 let row_height =
4427 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
4428 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
4429 let row_y = cursor.content_y + cursor.y;
4430 let total_width: f64 = col_widths.iter().sum();
4431
4432 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
4433
4434 let row_snapshot = cursor.elements.len();
4436
4437 let row_may_break =
4449 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
4450
4451 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
4452 for (cell_i, cell) in row.children.iter().enumerate() {
4456 let span = match &cell.kind {
4457 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
4458 _ => 1,
4459 };
4460 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
4461 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
4462 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
4463
4464 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4465
4466 let cell_snapshot = cursor.elements.len();
4468
4469 let inner_width =
4470 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4471
4472 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4473 let saved_y = cursor.y;
4474 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4475
4476 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4480 let content_h: f64 = cell
4481 .children
4482 .iter()
4483 .map(|ch| {
4484 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4485 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4486 })
4487 .sum();
4488 let inner_row =
4489 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4490 let slack = (inner_row - content_h).max(0.0);
4491 cursor.y += match cell_style.vertical_align {
4492 crate::style::VerticalAlign::Middle => slack / 2.0,
4493 crate::style::VerticalAlign::Bottom => slack,
4494 crate::style::VerticalAlign::Baseline => row_bl
4499 .map(|b| {
4500 let d = self.cell_baseline_distance(
4501 cell,
4502 &cell_style,
4503 inner_width,
4504 font_context,
4505 );
4506 (b - d).max(0.0)
4507 })
4508 .unwrap_or(0.0),
4509 crate::style::VerticalAlign::Top => 0.0,
4510 };
4511 }
4512
4513 let cursor_before_cell = if row_may_break {
4518 Some(cursor.clone())
4519 } else {
4520 None
4521 };
4522 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4523 for child in &cell.children {
4524 self.layout_node(
4525 child,
4526 cursor,
4527 &mut cell_pages,
4528 content_x,
4529 inner_width,
4530 Some(&cell_style),
4531 font_context,
4532 None,
4533 None,
4534 );
4535 }
4536
4537 if !cell_pages.is_empty() {
4539 let post_break_elements = std::mem::take(&mut cursor.elements);
4540 if let Some(last_page) = cell_pages.last_mut() {
4541 last_page.elements.extend(post_break_elements);
4542 }
4543 if !is_header {
4549 all_overflow_pages.extend(cell_pages);
4550 }
4551 *cursor = cursor_before_cell
4557 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4558 }
4559
4560 cursor.y = saved_y;
4561
4562 let cell_children: Vec<LayoutElement> =
4564 drain_since(&mut cursor.elements, cell_snapshot);
4565
4566 cursor.elements.push(LayoutElement {
4568 x: cell_x,
4569 y: row_y,
4570 width: col_width,
4571 height: row_height,
4572 draw: if cell_style.background_color.is_some()
4573 || cell_style.border_width.horizontal() > 0.0
4574 || cell_style.border_width.vertical() > 0.0
4575 {
4576 DrawCommand::Rect {
4577 background: cell_style.background_color,
4578 border_width: cell_style.border_width,
4579 border_color: cell_style.border_color,
4580 border_style: cell_style.border_style,
4581 border_radius: cell_style.border_radius,
4582 opacity: 1.0,
4583 box_shadow: cell_style.box_shadow.map(Box::new),
4584 background_gradient: cell_style.background.clone().map(Box::new),
4585 }
4586 } else {
4587 DrawCommand::None
4588 },
4589 children: cell_children,
4590 node_type: Some("TableCell".to_string()),
4591 resolved_style: Some(cell_style.clone()),
4592 source_location: cell.source_location.clone(),
4593 href: None,
4594 bookmark: cell.bookmark.clone(),
4595 alt: None,
4596 is_header_row: is_header,
4597 actual_text: None,
4598 list_numbering: None,
4599 col_span: span as u32,
4600 overflow: Overflow::default(),
4601 opacity: 1.0,
4602 });
4603 }
4604
4605 let row_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, row_snapshot);
4607 let row_element = LayoutElement {
4608 x: start_x,
4609 y: row_y,
4610 width: total_width,
4611 height: row_height,
4612 draw: if let Some(bg) = row_style.background_color {
4613 DrawCommand::Rect {
4614 background: Some(bg),
4615 border_width: Edges::default(),
4616 border_color: EdgeValues::uniform(Color::BLACK),
4617 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4618 border_radius: CornerValues::uniform(0.0),
4619 opacity: 1.0,
4620 box_shadow: row_style.box_shadow.map(Box::new),
4621 background_gradient: row_style.background.clone().map(Box::new),
4622 }
4623 } else {
4624 DrawCommand::None
4625 },
4626 children: row_children,
4627 node_type: Some("TableRow".to_string()),
4628 resolved_style: Some(row_style.clone()),
4629 source_location: row.source_location.clone(),
4630 href: None,
4631 bookmark: row.bookmark.clone(),
4632 alt: None,
4633 is_header_row: is_header,
4634 actual_text: None,
4635 list_numbering: None,
4636 col_span: 1,
4637 overflow: row_style.overflow,
4638 opacity: row_style.opacity,
4639 };
4640
4641 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4642 first_overflow.elements.push(row_element);
4652 } else {
4653 cursor.elements.push(row_element);
4654 cursor.y += row_height;
4655 }
4656
4657 pages.extend(all_overflow_pages);
4659 }
4660
4661 #[allow(clippy::too_many_arguments)]
4662 #[allow(clippy::too_many_arguments)]
4663 fn layout_text(
4664 &self,
4665 content: &str,
4666 href: Option<&str>,
4667 runs: &[TextRun],
4668 style: &ResolvedStyle,
4669 cursor: &mut PageCursor,
4670 pages: &mut Vec<LayoutPage>,
4671 x: f64,
4672 available_width: f64,
4673 font_context: &FontContext,
4674 source_location: Option<&SourceLocation>,
4675 bookmark: Option<&str>,
4676 node_type_override: Option<&str>,
4680 ) {
4681 if style.border_width.top > 0.0
4688 || style.border_width.right > 0.0
4689 || style.border_width.bottom > 0.0
4690 || style.border_width.left > 0.0
4691 {
4692 self.defect(
4693 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4694 );
4695 }
4696 if style.background_color.is_some() {
4697 self.defect(
4698 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4699 );
4700 }
4701 let margin = &style.margin.to_edges();
4702 let text_x = x + margin.left;
4703 let text_width = match style.width {
4710 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4711 SizeConstraint::Auto => available_width - margin.horizontal(),
4712 };
4713 let text_width = text_width.min(style.max_width).max(style.min_width);
4724
4725 cursor.y += margin.top;
4726
4727 if !runs.is_empty() {
4729 self.layout_text_runs(
4730 runs,
4731 href,
4732 style,
4733 cursor,
4734 pages,
4735 text_x,
4736 text_width,
4737 font_context,
4738 source_location,
4739 bookmark,
4740 node_type_override,
4741 );
4742 cursor.y += margin.bottom;
4743 return;
4744 }
4745
4746 let content = substitute_page_placeholders(content);
4747 let transformed = apply_text_transform(&content, style.text_transform);
4748 let justify = matches!(style.text_align, TextAlign::Justify);
4749 let lines = match style.line_breaking {
4750 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4751 font_context,
4752 &transformed,
4753 text_width,
4754 style.font_size,
4755 &style.font_family,
4756 style.font_weight,
4757 style.font_style,
4758 style.letter_spacing,
4759 style.word_spacing,
4760 style.hyphens,
4761 style.lang.as_deref(),
4762 justify,
4763 ),
4764 LineBreaking::Greedy => self.text_layout.break_into_lines(
4765 font_context,
4766 &transformed,
4767 text_width,
4768 style.font_size,
4769 &style.font_family,
4770 style.font_weight,
4771 style.font_style,
4772 style.letter_spacing,
4773 style.word_spacing,
4774 style.hyphens,
4775 style.lang.as_deref(),
4776 ),
4777 };
4778
4779 let lines = match style.text_overflow {
4781 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4782 font_context,
4783 lines,
4784 text_width,
4785 style.font_size,
4786 &style.font_family,
4787 style.font_weight,
4788 style.font_style,
4789 style.letter_spacing,
4790 style.word_spacing,
4791 ),
4792 TextOverflow::Clip => self.text_layout.truncate_clip(
4793 font_context,
4794 lines,
4795 text_width,
4796 style.font_size,
4797 &style.font_family,
4798 style.font_weight,
4799 style.font_style,
4800 style.letter_spacing,
4801 style.word_spacing,
4802 ),
4803 TextOverflow::Wrap => lines,
4804 };
4805
4806 let line_height = style.font_size * style.line_height;
4807
4808 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4810 let decision = page_break::decide_break(
4811 cursor.remaining_height(),
4812 &line_heights,
4813 true,
4814 style.min_orphan_lines as usize,
4815 style.min_widow_lines as usize,
4816 );
4817
4818 let mut snapshot = cursor.elements.len();
4820 let mut container_start_y = cursor.content_y + cursor.y;
4821 let mut is_first_element = true;
4822
4823 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4825 pages.push(cursor.finalize());
4826 *cursor = cursor.new_page();
4827 snapshot = cursor.elements.len();
4828 container_start_y = cursor.content_y + cursor.y;
4829 }
4830
4831 let forced_break_at = match decision {
4833 page_break::BreakDecision::Split {
4834 items_on_current_page,
4835 } => Some(items_on_current_page),
4836 _ => None,
4837 };
4838 let mut first_break_done = false;
4839
4840 for (line_idx, line) in lines.iter().enumerate() {
4841 let needs_break = if let Some(break_at) = forced_break_at {
4843 if !first_break_done && line_idx == break_at {
4844 true
4845 } else {
4846 line_height > cursor.remaining_height()
4847 }
4848 } else {
4849 line_height > cursor.remaining_height()
4850 };
4851
4852 if needs_break {
4853 first_break_done = true;
4854 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4856 if !line_elements.is_empty() {
4857 let container_height = cursor.content_y + cursor.y - container_start_y;
4858 cursor.elements.push(LayoutElement {
4859 x: text_x,
4860 y: container_start_y,
4861 width: text_width,
4862 height: container_height,
4863 draw: DrawCommand::None,
4864 children: line_elements,
4865 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4866 resolved_style: Some(style.clone()),
4867 source_location: source_location.cloned(),
4868 href: href.map(|s| s.to_string()),
4869 bookmark: if is_first_element {
4870 bookmark.map(|s| s.to_string())
4871 } else {
4872 None
4873 },
4874 alt: None,
4875 is_header_row: false,
4876 actual_text: None,
4877 list_numbering: None,
4878 col_span: 1,
4879 overflow: Overflow::default(),
4880 opacity: 1.0,
4881 });
4882 is_first_element = false;
4883 }
4884
4885 pages.push(cursor.finalize());
4886 *cursor = cursor.new_page();
4887
4888 snapshot = cursor.elements.len();
4890 container_start_y = cursor.content_y + cursor.y;
4891 }
4892
4893 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4894
4895 let rendered_width = if glyphs.is_empty() {
4899 line.width
4900 } else {
4901 let last = &glyphs[glyphs.len() - 1];
4902 (last.x_offset + last.x_advance).max(line.width * 0.5)
4903 };
4904
4905 let line_x = match style.text_align {
4906 TextAlign::Left => text_x,
4907 TextAlign::Right => text_x + text_width - rendered_width,
4908 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4909 TextAlign::Justify => text_x,
4910 };
4911
4912 let is_last_line = line_idx == lines.len() - 1;
4920 let user_ws = style.word_spacing;
4921 let (justified_width, word_spacing) =
4922 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4923 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4924 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4925 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4926 let s = glyphs[..=idx]
4927 .iter()
4928 .filter(|g| g.char_value == ' ')
4929 .count();
4930 (w, s)
4931 } else {
4932 (0.0, 0)
4933 };
4934 let slack = text_width - natural_width;
4935 let ws = if space_count > 0 && slack.abs() > 0.01 {
4936 slack / space_count as f64
4937 } else {
4938 0.0
4939 };
4940 (text_width, user_ws + ws)
4941 } else {
4942 (rendered_width, user_ws)
4943 };
4944
4945 let text_line = TextLine {
4946 x: line_x,
4947 y: cursor.content_y
4953 + cursor.y
4954 + baseline_in_line(
4955 line_height,
4956 style.font_size,
4957 font_context.baseline_metrics(
4958 &style.font_family,
4959 style.font_weight,
4960 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4961 ),
4962 ),
4963 glyphs,
4964 width: justified_width,
4965 height: line_height,
4966 word_spacing,
4967 };
4968
4969 cursor.elements.push(LayoutElement {
4970 x: line_x,
4971 y: cursor.content_y + cursor.y,
4972 width: justified_width,
4973 height: line_height,
4974 draw: DrawCommand::Text {
4975 lines: vec![text_line],
4976 color: style.color,
4977 text_decoration: style.text_decoration,
4978 opacity: 1.0,
4979 },
4980 children: vec![],
4981 node_type: Some("TextLine".to_string()),
4982 resolved_style: Some(style.clone()),
4983 source_location: None,
4984 href: href.map(|s| s.to_string()),
4985 bookmark: None,
4986 alt: None,
4987 is_header_row: false,
4988 actual_text: None,
4989 list_numbering: None,
4990 col_span: 1,
4991 overflow: Overflow::default(),
4992 opacity: 1.0,
4993 });
4994
4995 cursor.y += line_height;
4996 }
4997
4998 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5000 if !line_elements.is_empty() {
5001 let container_height = cursor.content_y + cursor.y - container_start_y;
5002 cursor.elements.push(LayoutElement {
5003 x: text_x,
5004 y: container_start_y,
5005 width: text_width,
5006 height: container_height,
5007 draw: DrawCommand::None,
5008 children: line_elements,
5009 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5010 resolved_style: Some(style.clone()),
5011 source_location: source_location.cloned(),
5012 href: href.map(|s| s.to_string()),
5013 bookmark: if is_first_element {
5014 bookmark.map(|s| s.to_string())
5015 } else {
5016 None
5017 },
5018 alt: None,
5019 is_header_row: false,
5020 actual_text: None,
5021 list_numbering: None,
5022 col_span: 1,
5023 overflow: Overflow::default(),
5024 opacity: 1.0,
5025 });
5026 }
5027
5028 cursor.y += margin.bottom;
5029 }
5030
5031 #[allow(clippy::too_many_arguments)]
5033 #[allow(clippy::too_many_arguments)]
5034 fn layout_text_runs(
5035 &self,
5036 runs: &[TextRun],
5037 parent_href: Option<&str>,
5038 style: &ResolvedStyle,
5039 cursor: &mut PageCursor,
5040 pages: &mut Vec<LayoutPage>,
5041 text_x: f64,
5042 text_width: f64,
5043 font_context: &FontContext,
5044 source_location: Option<&SourceLocation>,
5045 bookmark: Option<&str>,
5046 node_type_override: Option<&str>,
5048 ) {
5049 let mut styled_chars: Vec<StyledChar> = Vec::new();
5051 for run in runs {
5052 let run_style = run.style.resolve(Some(style), text_width);
5053 let run_href = run.href.as_deref().or(parent_href);
5054 let transform = run_style.text_transform;
5055 let run_content = substitute_page_placeholders(&run.content);
5056 let mut prev_is_whitespace = true;
5057 for ch in run_content.chars() {
5058 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
5059 prev_is_whitespace = ch.is_whitespace();
5060 styled_chars.push(StyledChar {
5061 ch: transformed_ch,
5062 font_family: run_style.font_family.clone(),
5063 font_size: run_style.font_size,
5064 font_weight: run_style.font_weight,
5065 font_style: run_style.font_style,
5066 color: run_style.color,
5067 href: run_href.map(|s| s.to_string()),
5068 text_decoration: run_style.text_decoration,
5069 letter_spacing: run_style.letter_spacing,
5070 word_spacing: run_style.word_spacing,
5071 });
5072 }
5073 }
5074
5075 let justify = matches!(style.text_align, TextAlign::Justify);
5077 let broken_lines = match style.line_breaking {
5078 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
5079 font_context,
5080 &styled_chars,
5081 text_width,
5082 style.hyphens,
5083 style.lang.as_deref(),
5084 justify,
5085 ),
5086 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
5087 font_context,
5088 &styled_chars,
5089 text_width,
5090 style.hyphens,
5091 style.lang.as_deref(),
5092 ),
5093 };
5094
5095 let broken_lines = match style.text_overflow {
5097 TextOverflow::Ellipsis => {
5098 self.text_layout
5099 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
5100 }
5101 TextOverflow::Clip => {
5102 self.text_layout
5103 .truncate_runs_clip(font_context, broken_lines, text_width)
5104 }
5105 TextOverflow::Wrap => broken_lines,
5106 };
5107
5108 let line_height = style.font_size * style.line_height;
5109
5110 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
5112 let decision = page_break::decide_break(
5113 cursor.remaining_height(),
5114 &line_heights,
5115 true,
5116 style.min_orphan_lines as usize,
5117 style.min_widow_lines as usize,
5118 );
5119
5120 let mut snapshot = cursor.elements.len();
5121 let mut container_start_y = cursor.content_y + cursor.y;
5122 let mut is_first_element = true;
5123
5124 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
5125 pages.push(cursor.finalize());
5126 *cursor = cursor.new_page();
5127 snapshot = cursor.elements.len();
5128 container_start_y = cursor.content_y + cursor.y;
5129 }
5130
5131 let forced_break_at = match decision {
5132 page_break::BreakDecision::Split {
5133 items_on_current_page,
5134 } => Some(items_on_current_page),
5135 _ => None,
5136 };
5137 let mut first_break_done = false;
5138
5139 for (line_idx, run_line) in broken_lines.iter().enumerate() {
5140 let needs_break = if let Some(break_at) = forced_break_at {
5141 if !first_break_done && line_idx == break_at {
5142 true
5143 } else {
5144 line_height > cursor.remaining_height()
5145 }
5146 } else {
5147 line_height > cursor.remaining_height()
5148 };
5149
5150 if needs_break {
5151 first_break_done = true;
5152 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5153 if !line_elements.is_empty() {
5154 let container_height = cursor.content_y + cursor.y - container_start_y;
5155 cursor.elements.push(LayoutElement {
5156 x: text_x,
5157 y: container_start_y,
5158 width: text_width,
5159 height: container_height,
5160 draw: DrawCommand::None,
5161 children: line_elements,
5162 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5163 resolved_style: Some(style.clone()),
5164 source_location: source_location.cloned(),
5165 href: parent_href.map(|s| s.to_string()),
5166 bookmark: if is_first_element {
5167 bookmark.map(|s| s.to_string())
5168 } else {
5169 None
5170 },
5171 alt: None,
5172 is_header_row: false,
5173 actual_text: None,
5174 list_numbering: None,
5175 col_span: 1,
5176 overflow: Overflow::default(),
5177 opacity: 1.0,
5178 });
5179 is_first_element = false;
5180 }
5181
5182 pages.push(cursor.finalize());
5183 *cursor = cursor.new_page();
5184
5185 snapshot = cursor.elements.len();
5186 container_start_y = cursor.content_y + cursor.y;
5187 }
5188
5189 let line_x = match style.text_align {
5190 TextAlign::Left => text_x,
5191 TextAlign::Right => text_x + text_width - run_line.width,
5192 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
5193 TextAlign::Justify => text_x,
5194 };
5195
5196 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
5197
5198 let is_last_line = line_idx == broken_lines.len() - 1;
5205 let user_ws = style.word_spacing;
5206 let (justified_width, word_spacing) =
5207 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5208 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
5209 let (natural_width, space_count) = if let Some(idx) = last_non_space {
5210 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
5211 let s = glyphs[..=idx]
5212 .iter()
5213 .filter(|g| g.char_value == ' ')
5214 .count();
5215 (w, s)
5216 } else {
5217 (0.0, 0)
5218 };
5219 let slack = text_width - natural_width;
5220 let ws = if space_count > 0 && slack.abs() > 0.01 {
5221 slack / space_count as f64
5222 } else {
5223 0.0
5224 };
5225 (text_width, user_ws + ws)
5226 } else {
5227 (run_line.width, user_ws)
5228 };
5229
5230 let text_line = TextLine {
5231 x: line_x,
5232 y: cursor.content_y
5238 + cursor.y
5239 + baseline_in_line(
5240 line_height,
5241 style.font_size,
5242 font_context.baseline_metrics(
5243 &style.font_family,
5244 style.font_weight,
5245 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5246 ),
5247 ),
5248 glyphs,
5249 width: justified_width,
5250 height: line_height,
5251 word_spacing,
5252 };
5253
5254 let text_dec = run_line
5256 .chars
5257 .iter()
5258 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
5259 .map(|sc| sc.text_decoration)
5260 .unwrap_or(style.text_decoration);
5261
5262 cursor.elements.push(LayoutElement {
5263 x: line_x,
5264 y: cursor.content_y + cursor.y,
5265 width: justified_width,
5266 height: line_height,
5267 draw: DrawCommand::Text {
5268 lines: vec![text_line],
5269 color: style.color,
5270 text_decoration: text_dec,
5271 opacity: 1.0,
5272 },
5273 children: vec![],
5274 node_type: Some("TextLine".to_string()),
5275 resolved_style: Some(style.clone()),
5276 source_location: None,
5277 href: parent_href.map(|s| s.to_string()),
5278 bookmark: None,
5279 alt: None,
5280 is_header_row: false,
5281 actual_text: None,
5282 list_numbering: None,
5283 col_span: 1,
5284 overflow: Overflow::default(),
5285 opacity: 1.0,
5286 });
5287
5288 cursor.y += line_height;
5289 }
5290
5291 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5292 if !line_elements.is_empty() {
5293 let container_height = cursor.content_y + cursor.y - container_start_y;
5294 cursor.elements.push(LayoutElement {
5295 x: text_x,
5296 y: container_start_y,
5297 width: text_width,
5298 height: container_height,
5299 draw: DrawCommand::None,
5300 children: line_elements,
5301 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5302 resolved_style: Some(style.clone()),
5303 source_location: source_location.cloned(),
5304 href: parent_href.map(|s| s.to_string()),
5305 bookmark: if is_first_element {
5306 bookmark.map(|s| s.to_string())
5307 } else {
5308 None
5309 },
5310 alt: None,
5311 is_header_row: false,
5312 actual_text: None,
5313 list_numbering: None,
5314 col_span: 1,
5315 overflow: Overflow::default(),
5316 opacity: 1.0,
5317 });
5318 }
5319 }
5320
5321 fn build_positioned_glyphs_single_style(
5325 &self,
5326 line: &BrokenLine,
5327 style: &ResolvedStyle,
5328 href: Option<&str>,
5329 font_context: &FontContext,
5330 ) -> Vec<PositionedGlyph> {
5331 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5332 let line_text: String = line.chars.iter().collect();
5333 let direction = style.direction;
5334 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5336
5337 let font_runs = crate::font::fallback::segment_by_font(
5340 &line.chars,
5341 &style.font_family,
5342 style.font_weight,
5343 italic,
5344 font_context.registry(),
5345 );
5346 let needs_per_char_fallback = font_runs.len() > 1
5347 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
5348
5349 if needs_per_char_fallback {
5351 let bidi_runs = if has_bidi {
5352 bidi::analyze_bidi(&line_text, direction)
5353 } else {
5354 vec![crate::text::bidi::BidiRun {
5355 char_start: 0,
5356 char_end: line.chars.len(),
5357 level: unicode_bidi::Level::ltr(),
5358 is_rtl: false,
5359 }]
5360 };
5361
5362 let mut all_glyphs = Vec::new();
5363 let mut bidi_levels = Vec::new();
5364 let mut x = 0.0_f64;
5365
5366 for bidi_run in &bidi_runs {
5368 for font_run in &font_runs {
5370 let start = font_run.start.max(bidi_run.char_start);
5372 let end = font_run.end.min(bidi_run.char_end);
5373 if start >= end {
5374 continue;
5375 }
5376
5377 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
5378 let sub_text: String = sub_chars.iter().collect();
5379 let resolved_family = &font_run.family;
5380
5381 if let Some(font_data) =
5382 font_context.font_data(resolved_family, style.font_weight, italic)
5383 {
5384 if let Some(shaped) = shaping::shape_text_with_direction(
5385 &sub_text,
5386 font_data,
5387 bidi_run.is_rtl,
5388 ) {
5389 let units_per_em = font_context.units_per_em(
5390 resolved_family,
5391 style.font_weight,
5392 italic,
5393 );
5394 let scale = style.font_size / units_per_em as f64;
5395
5396 let clusters = cluster_texts(&shaped, &sub_chars);
5397 for (sg, (cluster_text, ligature)) in shaped.iter().zip(clusters) {
5398 let cluster = sg.cluster as usize;
5399 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
5400
5401 let glyph_x = x + sg.x_offset as f64 * scale;
5402 let glyph_y = sg.y_offset as f64 * scale;
5403 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5404
5405 all_glyphs.push(PositionedGlyph {
5406 glyph_id: sg.glyph_id,
5407 x_offset: glyph_x,
5408 y_offset: glyph_y,
5409 x_advance: advance,
5410 font_size: style.font_size,
5411 font_family: Arc::from(resolved_family.as_str()),
5412 font_weight: style.font_weight,
5413 font_style: style.font_style,
5414 char_value,
5415 color: Some(style.color),
5416 href: href.map(|s| s.to_string()),
5417 text_decoration: style.text_decoration,
5418 letter_spacing: style.letter_spacing,
5419 cluster_text,
5420 ligature,
5421 });
5422 bidi_levels.push(bidi_run.level);
5423 x += advance;
5424 }
5425 continue;
5426 }
5427 }
5428
5429 for i in start..end {
5431 let ch = line.chars[i];
5432 let glyph_x = x;
5433 let char_width = font_context.char_width(
5434 ch,
5435 resolved_family,
5436 style.font_weight,
5437 italic,
5438 style.font_size,
5439 );
5440 let advance = char_width + style.letter_spacing;
5441 all_glyphs.push(PositionedGlyph {
5442 glyph_id: ch as u16,
5443 x_offset: glyph_x,
5444 y_offset: 0.0,
5445 x_advance: advance,
5446 font_size: style.font_size,
5447 font_family: Arc::from(resolved_family.as_str()),
5448 font_weight: style.font_weight,
5449 font_style: style.font_style,
5450 char_value: ch,
5451 color: Some(style.color),
5452 href: href.map(|s| s.to_string()),
5453 text_decoration: style.text_decoration,
5454 letter_spacing: style.letter_spacing,
5455 cluster_text: None,
5456 ligature: false,
5457 });
5458 bidi_levels.push(bidi_run.level);
5459 x += advance;
5460 }
5461 }
5462 }
5463
5464 if has_bidi && !all_glyphs.is_empty() {
5466 all_glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5467 }
5468 return all_glyphs;
5469 }
5470
5471 if let Some(font_data) =
5474 font_context.font_data(&style.font_family, style.font_weight, italic)
5475 {
5476 if has_bidi {
5477 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5479 let units_per_em =
5480 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5481 let scale = style.font_size / units_per_em as f64;
5482
5483 let mut all_glyphs = Vec::new();
5484 let mut bidi_levels = Vec::new();
5485 let mut x = 0.0_f64;
5486
5487 for run in &bidi_runs {
5488 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5489 let run_text: String = run_chars.iter().collect();
5490
5491 if let Some(shaped) =
5492 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5493 {
5494 let clusters = cluster_texts(&shaped, &run_chars);
5495 for (sg, (cluster_text, ligature)) in shaped.iter().zip(clusters) {
5496 let cluster = sg.cluster as usize;
5497 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5498
5499 let glyph_x = x + sg.x_offset as f64 * scale;
5500 let glyph_y = sg.y_offset as f64 * scale;
5501 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5502
5503 all_glyphs.push(PositionedGlyph {
5504 glyph_id: sg.glyph_id,
5505 x_offset: glyph_x,
5506 y_offset: glyph_y,
5507 x_advance: advance,
5508 font_size: style.font_size,
5509 font_family: Arc::from(style.font_family.as_str()),
5510 font_weight: style.font_weight,
5511 font_style: style.font_style,
5512 char_value,
5513 color: Some(style.color),
5514 href: href.map(|s| s.to_string()),
5515 text_decoration: style.text_decoration,
5516 letter_spacing: style.letter_spacing,
5517 cluster_text,
5518 ligature,
5519 });
5520 bidi_levels.push(run.level);
5521
5522 x += advance;
5523 }
5524 }
5525 }
5526
5527 let glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5529 return glyphs;
5530 }
5531
5532 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5534 let units_per_em =
5535 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5536 let scale = style.font_size / units_per_em as f64;
5537
5538 return self.shaped_glyphs_to_positioned(
5539 &shaped,
5540 &line.chars,
5541 &line.char_positions,
5542 scale,
5543 style.font_size,
5544 &style.font_family,
5545 style.font_weight,
5546 style.font_style,
5547 Some(style.color),
5548 href,
5549 style.text_decoration,
5550 style.letter_spacing,
5551 );
5552 }
5553 }
5554
5555 let mut glyphs: Vec<PositionedGlyph> = line
5557 .chars
5558 .iter()
5559 .enumerate()
5560 .map(|(j, ch)| {
5561 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5562 let char_width = font_context.char_width(
5563 *ch,
5564 &style.font_family,
5565 style.font_weight,
5566 italic,
5567 style.font_size,
5568 );
5569 PositionedGlyph {
5570 glyph_id: *ch as u16,
5571 x_offset: glyph_x,
5572 y_offset: 0.0,
5573 x_advance: char_width,
5574 font_size: style.font_size,
5575 font_family: Arc::from(style.font_family.as_str()),
5576 font_weight: style.font_weight,
5577 font_style: style.font_style,
5578 char_value: *ch,
5579 color: Some(style.color),
5580 href: href.map(|s| s.to_string()),
5581 text_decoration: style.text_decoration,
5582 letter_spacing: style.letter_spacing,
5583 cluster_text: None,
5584 ligature: false,
5585 }
5586 })
5587 .collect();
5588
5589 if has_bidi && !glyphs.is_empty() {
5591 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5592 let mut levels = Vec::with_capacity(glyphs.len());
5593 let mut char_idx = 0;
5594 for run in &bidi_runs {
5595 for _ in run.char_start..run.char_end {
5596 if char_idx < glyphs.len() {
5597 levels.push(run.level);
5598 char_idx += 1;
5599 }
5600 }
5601 }
5602 while levels.len() < glyphs.len() {
5604 levels.push(unicode_bidi::Level::ltr());
5605 }
5606 glyphs = bidi::reorder_and_position(glyphs, &levels);
5607 }
5608
5609 glyphs
5610 }
5611
5612 fn build_positioned_glyphs_runs(
5617 &self,
5618 run_line: &RunBrokenLine,
5619 font_context: &FontContext,
5620 direction: Direction,
5621 ) -> Vec<PositionedGlyph> {
5622 let chars = &run_line.chars;
5623 if chars.is_empty() {
5624 return vec![];
5625 }
5626
5627 let resolved_families: Vec<String> = chars
5636 .iter()
5637 .map(|sc| {
5638 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5639 if !sc.font_family.contains(',') {
5640 let primary =
5641 font_context
5642 .registry()
5643 .resolve(&sc.font_family, sc.font_weight, italic);
5644 if sc.ch.is_whitespace()
5645 || sc.ch == PAGE_NUMBER_SENTINEL
5646 || sc.ch == TOTAL_PAGES_SENTINEL
5647 || primary.has_char(sc.ch)
5648 {
5649 return sc.font_family.clone();
5650 }
5651 }
5652 let (_, family) = font_context.registry().resolve_for_char(
5653 &sc.font_family,
5654 sc.ch,
5655 sc.font_weight,
5656 italic,
5657 );
5658 family
5659 })
5660 .collect();
5661
5662 let line_text: String = chars.iter().map(|c| c.ch).collect();
5663 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5664 let bidi_runs = if has_bidi {
5665 Some(bidi::analyze_bidi(&line_text, direction))
5666 } else {
5667 None
5668 };
5669
5670 let mut glyphs = Vec::new();
5671 let mut bidi_levels = Vec::new();
5672 let mut i = 0;
5673
5674 while i < chars.len() {
5675 let sc = &chars[i];
5676 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5677 let resolved_family = &resolved_families[i];
5678
5679 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5681 runs.iter()
5682 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5683 });
5684
5685 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5687 {
5688 let run_start = i;
5690 let mut run_end = i + 1;
5691 while run_end < chars.len() {
5692 let next = &chars[run_end];
5693 let next_italic =
5694 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5695 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5696 runs.iter()
5697 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5698 });
5699 if resolved_families[run_end] == *resolved_family
5701 && next.font_weight == sc.font_weight
5702 && next_italic == italic
5703 && (next.font_size - sc.font_size).abs() < 0.001
5704 && next_is_rtl == is_rtl
5705 {
5706 run_end += 1;
5707 } else {
5708 break;
5709 }
5710 }
5711
5712 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5713 if let Some(shaped) =
5714 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5715 {
5716 let units_per_em =
5717 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5718 let scale = sc.font_size / units_per_em as f64;
5719
5720 let run_chars: Vec<char> =
5722 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5723 let run_positions: Vec<f64> = (run_start..run_end)
5724 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5725 .collect();
5726
5727 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5729 &shaped,
5730 &chars[run_start..run_end],
5731 &run_chars,
5732 &run_positions,
5733 scale,
5734 );
5735 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5737 for g in &mut run_glyphs {
5738 g.font_family = resolved_family_arc.clone();
5739 }
5740 let run_level = if is_rtl {
5742 unicode_bidi::Level::rtl()
5743 } else {
5744 unicode_bidi::Level::ltr()
5745 };
5746 for _ in &run_glyphs {
5747 bidi_levels.push(run_level);
5748 }
5749 glyphs.extend(run_glyphs);
5750 i = run_end;
5751 continue;
5752 }
5753 }
5754
5755 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5757 let char_width = font_context.char_width(
5758 sc.ch,
5759 resolved_family,
5760 sc.font_weight,
5761 italic,
5762 sc.font_size,
5763 );
5764 glyphs.push(PositionedGlyph {
5765 glyph_id: sc.ch as u16,
5766 x_offset: glyph_x,
5767 y_offset: 0.0,
5768 x_advance: char_width,
5769 font_size: sc.font_size,
5770 font_family: Arc::from(resolved_family.as_str()),
5771 font_weight: sc.font_weight,
5772 font_style: sc.font_style,
5773 char_value: sc.ch,
5774 color: Some(sc.color),
5775 href: sc.href.clone(),
5776 text_decoration: sc.text_decoration,
5777 letter_spacing: sc.letter_spacing,
5778 cluster_text: None,
5779 ligature: false,
5780 });
5781 bidi_levels.push(if is_rtl {
5782 unicode_bidi::Level::rtl()
5783 } else {
5784 unicode_bidi::Level::ltr()
5785 });
5786 i += 1;
5787 }
5788
5789 if has_bidi && !glyphs.is_empty() {
5791 glyphs = bidi::reorder_and_position(glyphs, &bidi_levels);
5792 }
5793
5794 glyphs
5795 }
5796
5797 #[allow(clippy::too_many_arguments)]
5799 fn shaped_glyphs_to_positioned(
5800 &self,
5801 shaped: &[shaping::ShapedGlyph],
5802 chars: &[char],
5803 char_positions: &[f64],
5804 scale: f64,
5805 font_size: f64,
5806 font_family: &str,
5807 font_weight: u32,
5808 font_style: FontStyle,
5809 color: Option<Color>,
5810 href: Option<&str>,
5811 text_decoration: TextDecoration,
5812 letter_spacing: f64,
5813 ) -> Vec<PositionedGlyph> {
5814 let mut result = Vec::with_capacity(shaped.len());
5815 let mut x = 0.0_f64;
5816 let mut cluster_pen = 0.0_f64;
5817 let mut prev_cluster: Option<usize> = None;
5818
5819 let clusters = cluster_texts(shaped, chars);
5820 for (sg, (cluster_text, ligature)) in shaped.iter().zip(clusters) {
5821 let cluster = sg.cluster as usize;
5822 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5823
5824 if prev_cluster != Some(cluster) {
5825 cluster_pen = x;
5826 prev_cluster = Some(cluster);
5827 }
5828 let glyph_x = shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale);
5829 let glyph_y = sg.y_offset as f64 * scale;
5830 let advance = sg.x_advance as f64 * scale + letter_spacing;
5831
5832 result.push(PositionedGlyph {
5833 glyph_id: sg.glyph_id,
5834 x_offset: glyph_x,
5835 y_offset: glyph_y,
5836 x_advance: advance,
5837 font_size,
5838 font_family: Arc::from(font_family),
5839 font_weight,
5840 font_style,
5841 char_value,
5842 color,
5843 href: href.map(|s| s.to_string()),
5844 text_decoration,
5845 letter_spacing,
5846 cluster_text,
5847 ligature,
5848 });
5849
5850 x += advance;
5851 }
5852
5853 result
5854 }
5855
5856 fn shaped_glyphs_to_positioned_runs(
5858 &self,
5859 shaped: &[shaping::ShapedGlyph],
5860 styled_chars: &[StyledChar],
5861 chars: &[char],
5862 char_positions: &[f64],
5863 scale: f64,
5864 ) -> Vec<PositionedGlyph> {
5865 let mut result = Vec::with_capacity(shaped.len());
5866 let base_x = char_positions.first().copied().unwrap_or(0.0);
5869 let mut x = 0.0_f64;
5870 let mut cluster_pen = 0.0_f64;
5871 let mut prev_cluster: Option<usize> = None;
5872
5873 let clusters = cluster_texts(shaped, chars);
5874 for (sg, (cluster_text, ligature)) in shaped.iter().zip(clusters) {
5875 let cluster = sg.cluster as usize;
5876 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5877 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5878
5879 if prev_cluster != Some(cluster) {
5880 cluster_pen = x;
5881 prev_cluster = Some(cluster);
5882 }
5883 let glyph_x = if char_positions.get(cluster).is_some() {
5884 shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale)
5885 } else {
5886 base_x + x + sg.x_offset as f64 * scale
5887 };
5888 let glyph_y = sg.y_offset as f64 * scale;
5889 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5890
5891 result.push(PositionedGlyph {
5892 glyph_id: sg.glyph_id,
5893 x_offset: glyph_x,
5894 y_offset: glyph_y,
5895 x_advance: advance,
5896 font_size: sc.font_size,
5897 font_family: Arc::from(sc.font_family.as_str()),
5898 font_weight: sc.font_weight,
5899 font_style: sc.font_style,
5900 char_value,
5901 color: Some(sc.color),
5902 href: sc.href.clone(),
5903 text_decoration: sc.text_decoration,
5904 letter_spacing: sc.letter_spacing,
5905 cluster_text,
5906 ligature,
5907 });
5908
5909 x += advance;
5910 }
5911
5912 result
5913 }
5914
5915 fn image_display_size(
5925 &self,
5926 src: &str,
5927 style: &ResolvedStyle,
5928 explicit_width: Option<f64>,
5929 explicit_height: Option<f64>,
5930 available_width: f64,
5931 ) -> (f64, f64) {
5932 let dims = if src.is_empty() {
5933 None
5934 } else {
5935 self.get_image_dimensions(src)
5936 };
5937 let aspect = dims
5938 .map(|(w, h)| {
5939 if w > 0 {
5940 f64::from(h) / f64::from(w)
5941 } else {
5942 0.75
5943 }
5944 })
5945 .unwrap_or(0.75);
5946
5947 let style_w = match style.width {
5948 SizeConstraint::Fixed(w) => Some(w),
5949 SizeConstraint::Auto => None,
5950 };
5951 let style_h = match style.height {
5952 SizeConstraint::Fixed(h) => Some(h),
5953 SizeConstraint::Auto => None,
5954 };
5955 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5956
5957 let width_source = style_w.or(explicit_width);
5958 let height_source = style_h.or(explicit_height);
5959 match (width_source, height_source) {
5960 (Some(w), Some(h)) => (clamp(w), h),
5961 (Some(w), None) => {
5962 let w = clamp(w);
5963 (w, w * aspect)
5964 }
5965 (None, Some(h)) => (clamp(h / aspect), h),
5966 (None, None) => {
5967 let w = clamp(
5970 dims.map(|(w, _)| f64::from(w))
5971 .unwrap_or(available_width)
5972 .min(available_width),
5973 );
5974 (w, w * aspect)
5975 }
5976 }
5977 }
5978
5979 #[allow(clippy::too_many_arguments)]
5980 fn layout_image(
5981 &self,
5982 node: &Node,
5983 style: &ResolvedStyle,
5984 cursor: &mut PageCursor,
5985 pages: &mut Vec<LayoutPage>,
5986 x: f64,
5987 available_width: f64,
5988 explicit_width: Option<f64>,
5989 explicit_height: Option<f64>,
5990 ) {
5991 let margin = &style.margin.to_edges();
5992
5993 let src = match &node.kind {
5995 NodeKind::Image { src, .. } => src.as_str(),
5996 _ => "",
5997 };
5998
5999 let loaded = if !src.is_empty() {
6000 crate::image_loader::load_image(src).ok()
6001 } else {
6002 None
6003 };
6004
6005 let (img_width, img_height) = self.image_display_size(
6006 src,
6007 style,
6008 explicit_width,
6009 explicit_height,
6010 available_width - margin.horizontal(),
6011 );
6012
6013 let total_height = img_height + margin.vertical();
6014
6015 if total_height > cursor.remaining_height() {
6016 pages.push(cursor.finalize());
6017 *cursor = cursor.new_page();
6018 }
6019
6020 cursor.y += margin.top;
6021
6022 let draw = if let Some(image_data) = loaded {
6023 DrawCommand::Image { image_data }
6024 } else {
6025 DrawCommand::ImagePlaceholder
6026 };
6027
6028 cursor.elements.push(LayoutElement {
6029 x: x + margin.left,
6030 y: cursor.content_y + cursor.y,
6031 width: img_width,
6032 height: img_height,
6033 draw,
6034 children: vec![],
6035 node_type: Some(node_kind_name(&node.kind).to_string()),
6036 resolved_style: Some(style.clone()),
6037 source_location: node.source_location.clone(),
6038 href: node.href.clone(),
6039 bookmark: node.bookmark.clone(),
6040 alt: node.alt.clone(),
6041 is_header_row: false,
6042 actual_text: None,
6043 list_numbering: None,
6044 col_span: 1,
6045 overflow: style.overflow,
6046 opacity: style.opacity,
6047 });
6048
6049 cursor.y += img_height + margin.bottom;
6050 }
6051
6052 #[allow(clippy::too_many_arguments)]
6054 fn layout_svg(
6055 &self,
6056 node: &Node,
6057 style: &ResolvedStyle,
6058 cursor: &mut PageCursor,
6059 pages: &mut Vec<LayoutPage>,
6060 x: f64,
6061 _available_width: f64,
6062 svg_width: f64,
6063 svg_height: f64,
6064 view_box: Option<&str>,
6065 content: &str,
6066 ) {
6067 let margin = &style.margin.to_edges();
6068 let total_height = svg_height + margin.vertical();
6069
6070 if total_height > cursor.remaining_height() {
6071 pages.push(cursor.finalize());
6072 *cursor = cursor.new_page();
6073 }
6074
6075 cursor.y += margin.top;
6076
6077 let vb = view_box
6078 .and_then(crate::svg::parse_view_box)
6079 .unwrap_or(crate::svg::ViewBox {
6080 min_x: 0.0,
6081 min_y: 0.0,
6082 width: svg_width,
6083 height: svg_height,
6084 });
6085
6086 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
6087
6088 cursor.elements.push(LayoutElement {
6089 x: x + margin.left,
6090 y: cursor.content_y + cursor.y,
6091 width: svg_width,
6092 height: svg_height,
6093 draw: DrawCommand::Svg {
6094 commands,
6095 width: svg_width,
6096 height: svg_height,
6097 viewbox_min_x: vb.min_x,
6098 viewbox_min_y: vb.min_y,
6099 viewbox_width: vb.width,
6100 viewbox_height: vb.height,
6101 clip: false,
6102 },
6103 children: vec![],
6104 node_type: Some("Svg".to_string()),
6105 resolved_style: Some(style.clone()),
6106 source_location: node.source_location.clone(),
6107 href: node.href.clone(),
6108 bookmark: node.bookmark.clone(),
6109 alt: node.alt.clone(),
6110 is_header_row: false,
6111 actual_text: None,
6112 list_numbering: None,
6113 col_span: 1,
6114 overflow: style.overflow,
6115 opacity: style.opacity,
6116 });
6117
6118 cursor.y += svg_height + margin.bottom;
6119 }
6120
6121 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
6123 use crate::svg::SvgCommand;
6124
6125 let mut commands = Vec::new();
6126 let mut cur_x = 0.0_f64;
6127 let mut cur_y = 0.0_f64;
6128
6129 for op in operations {
6130 match op {
6131 CanvasOp::MoveTo { x, y } => {
6132 commands.push(SvgCommand::MoveTo(*x, *y));
6133 cur_x = *x;
6134 cur_y = *y;
6135 }
6136 CanvasOp::LineTo { x, y } => {
6137 commands.push(SvgCommand::LineTo(*x, *y));
6138 cur_x = *x;
6139 cur_y = *y;
6140 }
6141 CanvasOp::BezierCurveTo {
6142 cp1x,
6143 cp1y,
6144 cp2x,
6145 cp2y,
6146 x,
6147 y,
6148 } => {
6149 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
6150 cur_x = *x;
6151 cur_y = *y;
6152 }
6153 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
6154 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
6156 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
6157 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
6158 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
6159 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
6160 cur_x = *x;
6161 cur_y = *y;
6162 }
6163 CanvasOp::ClosePath => {
6164 commands.push(SvgCommand::ClosePath);
6165 }
6166 CanvasOp::Rect {
6167 x,
6168 y,
6169 width,
6170 height,
6171 } => {
6172 commands.push(SvgCommand::MoveTo(*x, *y));
6173 commands.push(SvgCommand::LineTo(*x + *width, *y));
6174 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
6175 commands.push(SvgCommand::LineTo(*x, *y + *height));
6176 commands.push(SvgCommand::ClosePath);
6177 cur_x = *x;
6178 cur_y = *y;
6179 }
6180 CanvasOp::Circle { cx, cy, r } => {
6181 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
6182 }
6183 CanvasOp::Ellipse { cx, cy, rx, ry } => {
6184 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
6185 }
6186 CanvasOp::Arc {
6187 cx,
6188 cy,
6189 r,
6190 start_angle,
6191 end_angle,
6192 counterclockwise,
6193 } => {
6194 let steps = 32;
6198 let mut sweep = end_angle - start_angle;
6199 if !counterclockwise && sweep < 0.0 {
6200 sweep += 2.0 * std::f64::consts::PI;
6201 }
6202 if *counterclockwise && sweep > 0.0 {
6203 sweep -= 2.0 * std::f64::consts::PI;
6204 }
6205 for i in 0..=steps {
6206 let t = *start_angle + sweep * (i as f64 / steps as f64);
6207 let px = cx + r * t.cos();
6208 let py = cy + r * t.sin();
6209 if i == 0 {
6210 commands.push(SvgCommand::MoveTo(px, py));
6211 } else {
6212 commands.push(SvgCommand::LineTo(px, py));
6213 }
6214 }
6215 }
6216 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
6217 CanvasOp::Fill => commands.push(SvgCommand::Fill),
6218 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
6219 CanvasOp::SetFillColor { r, g, b } => {
6220 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
6222 }
6223 CanvasOp::SetStrokeColor { r, g, b } => {
6224 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
6225 }
6226 CanvasOp::SetLineWidth { width } => {
6227 commands.push(SvgCommand::SetStrokeWidth(*width));
6228 }
6229 CanvasOp::SetLineCap { cap } => {
6230 commands.push(SvgCommand::SetLineCap(*cap));
6231 }
6232 CanvasOp::SetLineJoin { join } => {
6233 commands.push(SvgCommand::SetLineJoin(*join));
6234 }
6235 CanvasOp::Save => commands.push(SvgCommand::SaveState),
6236 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
6237 }
6238 }
6239
6240 commands
6241 }
6242
6243 #[allow(clippy::too_many_arguments)]
6245 fn layout_canvas(
6246 &self,
6247 node: &Node,
6248 style: &ResolvedStyle,
6249 cursor: &mut PageCursor,
6250 pages: &mut Vec<LayoutPage>,
6251 x: f64,
6252 _available_width: f64,
6253 canvas_width: f64,
6254 canvas_height: f64,
6255 operations: &[CanvasOp],
6256 ) {
6257 let margin = style.margin.to_edges();
6258 let total_height = canvas_height + margin.top + margin.bottom;
6259
6260 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
6262 pages.push(cursor.finalize());
6263 *cursor = cursor.new_page();
6264 }
6265
6266 cursor.y += margin.top;
6267
6268 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
6269
6270 cursor.elements.push(LayoutElement {
6271 x: x + margin.left,
6272 y: cursor.content_y + cursor.y,
6273 width: canvas_width,
6274 height: canvas_height,
6275 draw: DrawCommand::Svg {
6276 commands: svg_commands,
6277 width: canvas_width,
6278 height: canvas_height,
6279 viewbox_min_x: 0.0,
6282 viewbox_min_y: 0.0,
6283 viewbox_width: canvas_width,
6284 viewbox_height: canvas_height,
6285 clip: true,
6286 },
6287 children: vec![],
6288 node_type: Some("Canvas".to_string()),
6289 resolved_style: Some(style.clone()),
6290 source_location: node.source_location.clone(),
6291 href: node.href.clone(),
6292 bookmark: node.bookmark.clone(),
6293 alt: node.alt.clone(),
6294 is_header_row: false,
6295 actual_text: None,
6296 list_numbering: None,
6297 col_span: 1,
6298 overflow: style.overflow,
6299 opacity: style.opacity,
6300 });
6301
6302 cursor.y += canvas_height + margin.bottom;
6303 }
6304
6305 #[allow(clippy::too_many_arguments)]
6307 #[allow(clippy::too_many_arguments)]
6309 fn layout_chart(
6310 &self,
6311 node: &Node,
6312 style: &ResolvedStyle,
6313 cursor: &mut PageCursor,
6314 pages: &mut Vec<LayoutPage>,
6315 x: f64,
6316 chart_width: f64,
6317 chart_height: f64,
6318 primitives: Vec<crate::chart::ChartPrimitive>,
6319 node_type_name: &str,
6320 ) {
6321 let margin = &style.margin.to_edges();
6322 let total_height = chart_height + margin.vertical();
6323
6324 if total_height > cursor.remaining_height() {
6325 pages.push(cursor.finalize());
6326 *cursor = cursor.new_page();
6327 }
6328
6329 cursor.y += margin.top;
6330
6331 let draw = DrawCommand::Chart { primitives };
6332
6333 cursor.elements.push(LayoutElement {
6334 x: x + margin.left,
6335 y: cursor.content_y + cursor.y,
6336 width: chart_width,
6337 height: chart_height,
6338 draw,
6339 children: vec![],
6340 node_type: Some(node_type_name.to_string()),
6341 resolved_style: Some(style.clone()),
6342 source_location: node.source_location.clone(),
6343 href: node.href.clone(),
6344 bookmark: node.bookmark.clone(),
6345 alt: node.alt.clone(),
6346 is_header_row: false,
6347 actual_text: None,
6348 list_numbering: None,
6349 col_span: 1,
6350 overflow: style.overflow,
6351 opacity: style.opacity,
6352 });
6353
6354 cursor.y += chart_height + margin.bottom;
6355 }
6356
6357 #[allow(clippy::too_many_arguments)]
6359 fn layout_form_field(
6360 &self,
6361 node: &Node,
6362 style: &ResolvedStyle,
6363 cursor: &mut PageCursor,
6364 pages: &mut Vec<LayoutPage>,
6365 x: f64,
6366 field_width: f64,
6367 field_height: f64,
6368 draw: DrawCommand,
6369 node_type_name: &str,
6370 ) {
6371 let margin = &style.margin.to_edges();
6372 let total_height = field_height + margin.vertical();
6373
6374 if total_height > cursor.remaining_height() {
6375 pages.push(cursor.finalize());
6376 *cursor = cursor.new_page();
6377 }
6378
6379 cursor.y += margin.top;
6380
6381 cursor.elements.push(LayoutElement {
6382 x: x + margin.left,
6383 y: cursor.content_y + cursor.y,
6384 width: field_width,
6385 height: field_height,
6386 draw,
6387 children: vec![],
6388 node_type: Some(node_type_name.to_string()),
6389 resolved_style: Some(style.clone()),
6390 source_location: node.source_location.clone(),
6391 href: node.href.clone(),
6392 bookmark: node.bookmark.clone(),
6393 alt: node.alt.clone(),
6394 is_header_row: false,
6395 actual_text: None,
6396 list_numbering: None,
6397 col_span: 1,
6398 overflow: style.overflow,
6399 opacity: style.opacity,
6400 });
6401
6402 cursor.y += field_height + margin.bottom;
6403 }
6404
6405 #[allow(clippy::too_many_arguments)]
6406 fn layout_barcode(
6407 &self,
6408 node: &Node,
6409 style: &ResolvedStyle,
6410 cursor: &mut PageCursor,
6411 pages: &mut Vec<LayoutPage>,
6412 x: f64,
6413 available_width: f64,
6414 data: &str,
6415 format: crate::barcode::BarcodeFormat,
6416 explicit_width: Option<f64>,
6417 bar_height: f64,
6418 ) {
6419 let margin = &style.margin.to_edges();
6420 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
6421 let total_height = bar_height + margin.vertical();
6422
6423 if total_height > cursor.remaining_height() {
6424 pages.push(cursor.finalize());
6425 *cursor = cursor.new_page();
6426 }
6427
6428 cursor.y += margin.top;
6429
6430 let draw = match crate::barcode::generate_barcode(data, format) {
6431 Ok(barcode_data) => {
6432 let bar_width = if barcode_data.bars.is_empty() {
6433 0.0
6434 } else {
6435 display_width / barcode_data.bars.len() as f64
6436 };
6437 DrawCommand::Barcode {
6438 bars: barcode_data.bars,
6439 bar_width,
6440 height: bar_height,
6441 color: style.color,
6442 }
6443 }
6444 Err(_) => DrawCommand::None,
6445 };
6446
6447 cursor.elements.push(LayoutElement {
6448 x: x + margin.left,
6449 y: cursor.content_y + cursor.y,
6450 width: display_width,
6451 height: bar_height,
6452 draw,
6453 children: vec![],
6454 node_type: Some("Barcode".to_string()),
6455 resolved_style: Some(style.clone()),
6456 source_location: node.source_location.clone(),
6457 href: node.href.clone(),
6458 bookmark: node.bookmark.clone(),
6459 alt: node.alt.clone(),
6460 is_header_row: false,
6461 actual_text: Some(data.to_string()),
6462 list_numbering: None,
6463 col_span: 1,
6464 overflow: style.overflow,
6465 opacity: style.opacity,
6466 });
6467
6468 cursor.y += bar_height + margin.bottom;
6469 }
6470
6471 #[allow(clippy::too_many_arguments)]
6473 fn layout_qrcode(
6474 &self,
6475 node: &Node,
6476 style: &ResolvedStyle,
6477 cursor: &mut PageCursor,
6478 pages: &mut Vec<LayoutPage>,
6479 x: f64,
6480 available_width: f64,
6481 data: &str,
6482 explicit_size: Option<f64>,
6483 ) {
6484 let margin = &style.margin.to_edges();
6485 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6486 let total_height = display_size + margin.vertical();
6487
6488 if total_height > cursor.remaining_height() {
6489 pages.push(cursor.finalize());
6490 *cursor = cursor.new_page();
6491 }
6492
6493 cursor.y += margin.top;
6494
6495 let draw = match crate::qrcode::generate_qr(data) {
6496 Ok(matrix) => {
6497 let module_size = display_size / matrix.size as f64;
6498 DrawCommand::QrCode {
6499 modules: matrix.modules,
6500 module_size,
6501 color: style.color,
6502 }
6503 }
6504 Err(_) => DrawCommand::None,
6505 };
6506
6507 cursor.elements.push(LayoutElement {
6508 x: x + margin.left,
6509 y: cursor.content_y + cursor.y,
6510 width: display_size,
6511 height: display_size,
6512 draw,
6513 children: vec![],
6514 node_type: Some("QrCode".to_string()),
6515 resolved_style: Some(style.clone()),
6516 source_location: node.source_location.clone(),
6517 href: node.href.clone(),
6518 bookmark: node.bookmark.clone(),
6519 alt: node.alt.clone(),
6520 is_header_row: false,
6521 actual_text: Some(data.to_string()),
6522 list_numbering: None,
6523 col_span: 1,
6524 overflow: style.overflow,
6525 opacity: style.opacity,
6526 });
6527
6528 cursor.y += display_size + margin.bottom;
6529 }
6530
6531 fn measure_node_height(
6534 &self,
6535 node: &Node,
6536 available_width: f64,
6537 style: &ResolvedStyle,
6538 font_context: &FontContext,
6539 ) -> f64 {
6540 match &node.kind {
6541 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6546 let measure_width = match style.width {
6549 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6550 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6551 };
6552 if !runs.is_empty() {
6559 let mut styled_chars: Vec<StyledChar> = Vec::new();
6560 for run in runs {
6561 let run_style = run.style.resolve(Some(style), measure_width);
6562 let transform = run_style.text_transform;
6563 let run_content = substitute_page_placeholders(&run.content);
6564 let mut prev_is_whitespace = true;
6565 for ch in run_content.chars() {
6566 let transformed_ch =
6567 apply_char_transform(ch, transform, prev_is_whitespace);
6568 prev_is_whitespace = ch.is_whitespace();
6569 styled_chars.push(StyledChar {
6570 ch: transformed_ch,
6571 font_family: run_style.font_family.clone(),
6572 font_size: run_style.font_size,
6573 font_weight: run_style.font_weight,
6574 font_style: run_style.font_style,
6575 color: run_style.color,
6576 href: None,
6577 text_decoration: run_style.text_decoration,
6578 letter_spacing: run_style.letter_spacing,
6579 word_spacing: run_style.word_spacing,
6580 });
6581 }
6582 }
6583 let justify = matches!(style.text_align, TextAlign::Justify);
6584 let broken_lines = match style.line_breaking {
6585 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6586 font_context,
6587 &styled_chars,
6588 measure_width,
6589 style.hyphens,
6590 style.lang.as_deref(),
6591 justify,
6592 ),
6593 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6594 font_context,
6595 &styled_chars,
6596 measure_width,
6597 style.hyphens,
6598 style.lang.as_deref(),
6599 ),
6600 };
6601 let line_height = style.font_size * style.line_height;
6602 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6603 } else {
6604 let content = substitute_page_placeholders(content);
6605 let transformed = apply_text_transform(&content, style.text_transform);
6606 let justify = matches!(style.text_align, TextAlign::Justify);
6607 let lines = match style.line_breaking {
6608 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6609 font_context,
6610 &transformed,
6611 measure_width,
6612 style.font_size,
6613 &style.font_family,
6614 style.font_weight,
6615 style.font_style,
6616 style.letter_spacing,
6617 style.word_spacing,
6618 style.hyphens,
6619 style.lang.as_deref(),
6620 justify,
6621 ),
6622 LineBreaking::Greedy => self.text_layout.break_into_lines(
6623 font_context,
6624 &transformed,
6625 measure_width,
6626 style.font_size,
6627 &style.font_family,
6628 style.font_weight,
6629 style.font_style,
6630 style.letter_spacing,
6631 style.word_spacing,
6632 style.hyphens,
6633 style.lang.as_deref(),
6634 ),
6635 };
6636 let line_height = style.font_size * style.line_height;
6637 (lines.len() as f64) * line_height + style.padding.vertical()
6638 }
6639 }
6640 NodeKind::Image {
6641 src,
6642 width: explicit_w,
6643 height: explicit_h,
6644 } => {
6645 let (_, h) = self.image_display_size(
6647 src,
6648 style,
6649 *explicit_w,
6650 *explicit_h,
6651 available_width - style.margin.horizontal(),
6652 );
6653 h + style.padding.vertical()
6654 }
6655 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6656 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6657 NodeKind::QrCode { size, .. } => {
6658 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6659 display_size + style.margin.vertical()
6660 }
6661 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6662 NodeKind::BarChart { height, .. }
6663 | NodeKind::LineChart { height, .. }
6664 | NodeKind::PieChart { height, .. }
6665 | NodeKind::AreaChart { height, .. }
6666 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6667 NodeKind::TextField { height, .. }
6668 | NodeKind::Checkbox { height, .. }
6669 | NodeKind::Dropdown { height, .. }
6670 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6671 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6673 if let SizeConstraint::Fixed(h) = style.height {
6680 return h;
6681 }
6682 let outer_width = match style.width {
6683 SizeConstraint::Fixed(w) => w,
6684 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6685 };
6686 let inner_width =
6687 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6688 let col_widths = self.resolve_column_widths(
6689 columns,
6690 inner_width,
6691 &node.children,
6692 style,
6693 font_context,
6694 );
6695 let row_gap = style.row_gap;
6696 let offsets = Self::table_column_offsets(&node.children);
6697 let mut total = 0.0;
6698 for (i, row) in node.children.iter().enumerate() {
6699 if i > 0 {
6700 total += row_gap;
6701 }
6702 total += self.measure_table_row_height(
6703 row,
6704 &col_widths,
6705 &offsets[i],
6706 style,
6707 font_context,
6708 );
6709 }
6710 total + style.padding.vertical() + style.border_width.vertical()
6711 }
6712 NodeKind::TableRow { .. } => {
6713 let n = node.children.len().max(1);
6718 let usable = (available_width - style.margin.horizontal()).max(0.0);
6719 let col_w = usable / n as f64;
6720 let col_widths = vec![col_w; n];
6721 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6722 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6723 }
6724 _ => {
6725 if let SizeConstraint::Fixed(h) = style.height {
6727 return h;
6728 }
6729 let outer_width = match style.width {
6733 SizeConstraint::Fixed(w) => w,
6734 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6735 }
6736 .min(style.max_width)
6737 .max(style.min_width);
6738 let inner_width =
6739 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6740 let children_height =
6741 self.measure_children_height(&node.children, inner_width, style, font_context);
6742 (children_height + style.padding.vertical() + style.border_width.vertical())
6743 .max(style.min_height)
6744 }
6745 }
6746 }
6747
6748 fn measure_children_height(
6749 &self,
6750 children: &[Node],
6751 available_width: f64,
6752 parent_style: &ResolvedStyle,
6753 font_context: &FontContext,
6754 ) -> f64 {
6755 if children
6761 .iter()
6762 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6763 {
6764 let flow: Vec<Node> = children
6765 .iter()
6766 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6767 .cloned()
6768 .collect();
6769 return self.measure_children_height(
6770 &flow,
6771 available_width,
6772 parent_style,
6773 font_context,
6774 );
6775 }
6776 if matches!(parent_style.display, Display::Grid) {
6778 if let Some(template_cols) = &parent_style.grid_template_columns {
6779 let num_columns = template_cols.len();
6780 if num_columns > 0 && !children.is_empty() {
6781 let col_gap = parent_style.column_gap;
6782 let row_gap = parent_style.row_gap;
6783
6784 let content_sizes: Vec<f64> = template_cols
6785 .iter()
6786 .map(|track| {
6787 if matches!(track, GridTrackSize::Auto) {
6788 available_width / num_columns as f64
6789 } else {
6790 0.0
6791 }
6792 })
6793 .collect();
6794
6795 let col_widths = grid::resolve_tracks(
6796 template_cols,
6797 available_width,
6798 col_gap,
6799 &content_sizes,
6800 );
6801
6802 let placements: Vec<Option<&GridPlacement>> = children
6803 .iter()
6804 .map(|child| child.style.grid_placement.as_ref())
6805 .collect();
6806
6807 let item_placements = grid::place_items(&placements, num_columns);
6808 let num_rows = grid::compute_num_rows(&item_placements);
6809
6810 if num_rows == 0 {
6811 return 0.0;
6812 }
6813
6814 let mut row_heights = vec![0.0_f64; num_rows];
6815 for placement in &item_placements {
6816 let cell_width = grid::span_width(
6817 placement.col_start,
6818 placement.col_end,
6819 &col_widths,
6820 col_gap,
6821 );
6822 let child = &children[placement.child_index];
6823 let child_style = child.style.resolve(Some(parent_style), cell_width);
6824 let h =
6825 self.measure_node_height(child, cell_width, &child_style, font_context);
6826 let span = placement.row_end - placement.row_start;
6827 let per_row = h / span as f64;
6828 for rh in row_heights
6829 .iter_mut()
6830 .take(placement.row_end.min(num_rows))
6831 .skip(placement.row_start)
6832 {
6833 if per_row > *rh {
6834 *rh = per_row;
6835 }
6836 }
6837 }
6838
6839 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6840 return row_heights.iter().sum::<f64>() + total_row_gap;
6841 }
6842 }
6843 }
6844
6845 let direction = parent_style.flex_direction;
6846 let row_gap = parent_style.row_gap;
6847 let column_gap = parent_style.column_gap;
6848
6849 match direction {
6850 FlexDirection::Row | FlexDirection::RowReverse => {
6851 let styles: Vec<ResolvedStyle> = children
6854 .iter()
6855 .map(|child| child.style.resolve(Some(parent_style), available_width))
6856 .collect();
6857
6858 let base_widths: Vec<f64> = children
6859 .iter()
6860 .zip(&styles)
6861 .map(|(child, style)| match style.flex_basis {
6862 SizeConstraint::Fixed(w) => w,
6863 SizeConstraint::Auto => match style.width {
6864 SizeConstraint::Fixed(w) => w,
6865 SizeConstraint::Auto => self
6866 .measure_intrinsic_width(child, style, font_context)
6867 .min(available_width),
6868 },
6869 })
6870 .collect();
6871
6872 let lines = match parent_style.flex_wrap {
6873 FlexWrap::NoWrap => {
6874 vec![flex::WrapLine {
6875 start: 0,
6876 end: children.len(),
6877 }]
6878 }
6879 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6880 flex::partition_into_lines(&base_widths, column_gap, available_width)
6881 }
6882 };
6883
6884 let mut final_widths = base_widths.clone();
6886 for line in &lines {
6887 let line_count = line.end - line.start;
6888 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6889 let distributable = available_width - line_gap;
6890 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6891 let remaining = distributable - total_base;
6892
6893 if remaining > 0.0 {
6894 let total_grow: f64 = styles[line.start..line.end]
6895 .iter()
6896 .map(|s| s.flex_grow)
6897 .sum();
6898 if total_grow > 0.0 {
6899 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6900 final_widths[line.start + j] = base_widths[line.start + j]
6901 + remaining * (s.flex_grow / total_grow);
6902 }
6903 }
6904 } else if remaining < 0.0 {
6905 let total_shrink: f64 = styles[line.start..line.end]
6906 .iter()
6907 .enumerate()
6908 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6909 .sum();
6910 if total_shrink > 0.0 {
6911 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6912 let factor =
6913 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6914 let w = base_widths[line.start + j] + remaining * factor;
6915 final_widths[line.start + j] = w.max(s.min_width);
6916 }
6917 }
6918 }
6919 }
6920
6921 let mut total = 0.0;
6922 for (i, line) in lines.iter().enumerate() {
6923 let line_height: f64 = children[line.start..line.end]
6924 .iter()
6925 .enumerate()
6926 .map(|(j, child)| {
6927 let fw = final_widths[line.start + j];
6928 let child_style =
6937 child.style.resolve(Some(parent_style), available_width);
6938 self.measure_node_height(child, fw, &child_style, font_context)
6939 + child_style.margin.vertical()
6940 })
6941 .fold(0.0f64, f64::max);
6942 total += line_height;
6943 if i > 0 {
6944 total += row_gap;
6945 }
6946 }
6947 total
6948 }
6949 FlexDirection::Column | FlexDirection::ColumnReverse => {
6950 let mut total = 0.0;
6951 for (i, child) in children.iter().enumerate() {
6952 let child_style = child.style.resolve(Some(parent_style), available_width);
6953 let child_height = self.measure_node_height(
6954 child,
6955 available_width,
6956 &child_style,
6957 font_context,
6958 );
6959 total += child_height + child_style.margin.vertical();
6960 if i > 0 {
6961 total += row_gap;
6962 }
6963 }
6964 total
6965 }
6966 }
6967 }
6968
6969 fn measure_intrinsic_width(
6971 &self,
6972 node: &Node,
6973 style: &ResolvedStyle,
6974 font_context: &FontContext,
6975 ) -> f64 {
6976 match &node.kind {
6977 NodeKind::Svg { width, .. } => {
6978 *width + style.padding.horizontal() + style.margin.horizontal()
6979 }
6980 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6981 let text_width = if !runs.is_empty() {
6987 runs.iter()
6988 .map(|run| {
6989 let run_style = run.style.resolve(Some(style), 0.0);
6990 let run_content = substitute_page_placeholders(&run.content);
6991 let transformed =
6992 apply_text_transform(&run_content, run_style.text_transform);
6993 let italic = matches!(
6994 run_style.font_style,
6995 FontStyle::Italic | FontStyle::Oblique
6996 );
6997 transformed
7001 .split('\n')
7002 .map(|segment| {
7003 font_context.measure_string(
7004 segment,
7005 &run_style.font_family,
7006 run_style.font_weight,
7007 italic,
7008 run_style.font_size,
7009 run_style.letter_spacing,
7010 )
7011 })
7012 .fold(0.0f64, f64::max)
7013 })
7014 .sum()
7015 } else {
7016 let content = substitute_page_placeholders(content);
7017 let transformed = apply_text_transform(&content, style.text_transform);
7018 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7019 transformed
7020 .split('\n')
7021 .map(|segment| {
7022 font_context.measure_string(
7023 segment,
7024 &style.font_family,
7025 style.font_weight,
7026 italic,
7027 style.font_size,
7028 style.letter_spacing,
7029 )
7030 })
7031 .fold(0.0f64, f64::max)
7032 };
7033 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
7036 }
7037 NodeKind::Image {
7038 src, width, height, ..
7039 } => {
7040 let w = if let SizeConstraint::Fixed(w) = style.width {
7041 w
7042 } else if let Some(w) = width {
7043 *w
7044 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
7045 let pixel_w = iw as f64;
7046 let pixel_h = ih as f64;
7047 let aspect = if pixel_w > 0.0 {
7048 pixel_h / pixel_w
7049 } else {
7050 0.75
7051 };
7052 let constrained_h = match style.height {
7054 SizeConstraint::Fixed(h) => Some(h),
7055 SizeConstraint::Auto => *height,
7056 };
7057 if let Some(h) = constrained_h {
7058 h / aspect
7059 } else {
7060 pixel_w
7061 }
7062 } else {
7063 100.0
7064 };
7065 w + style.padding.horizontal() + style.margin.horizontal()
7066 }
7067 NodeKind::Barcode { width, .. } => {
7068 let w = width.unwrap_or(0.0);
7069 w + style.padding.horizontal() + style.margin.horizontal()
7070 }
7071 NodeKind::QrCode { size, .. } => {
7072 let display_size = size.unwrap_or(0.0);
7073 display_size + style.padding.horizontal() + style.margin.horizontal()
7074 }
7075 NodeKind::Canvas { width, .. } => {
7076 *width + style.padding.horizontal() + style.margin.horizontal()
7077 }
7078 NodeKind::BarChart { width, .. }
7079 | NodeKind::LineChart { width, .. }
7080 | NodeKind::PieChart { width, .. }
7081 | NodeKind::AreaChart { width, .. }
7082 | NodeKind::DotPlot { width, .. } => {
7083 *width + style.padding.horizontal() + style.margin.horizontal()
7084 }
7085 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
7086 *width + style.padding.horizontal() + style.margin.horizontal()
7087 }
7088 NodeKind::Table { columns } => {
7089 let num_cols = node
7094 .children
7095 .iter()
7096 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7097 .max()
7098 .unwrap_or(1)
7099 .max(columns.len().max(1));
7100 let (_, col_max) =
7101 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7102 col_max.iter().sum::<f64>()
7103 + style.padding.horizontal()
7104 + style.margin.horizontal()
7105 + style.border_width.horizontal()
7106 }
7107 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
7108 *width + style.padding.horizontal() + style.margin.horizontal()
7109 }
7110 NodeKind::Watermark { .. } => 0.0, _ => {
7112 if let SizeConstraint::Fixed(w) = style.width {
7120 return w
7121 + style.padding.horizontal()
7122 + style.border_width.horizontal()
7123 + style.margin.horizontal();
7124 }
7125 if node.children.is_empty() {
7127 style.padding.horizontal() + style.margin.horizontal()
7128 } else {
7129 let direction = style.flex_direction;
7130 let gap = style.column_gap;
7142 let mut total = 0.0f64;
7143 for (i, child) in node.children.iter().enumerate() {
7144 let child_style = child.style.resolve(Some(style), 0.0);
7145 let child_width =
7146 self.measure_intrinsic_width(child, &child_style, font_context);
7147 match direction {
7148 FlexDirection::Row | FlexDirection::RowReverse => {
7149 total += child_width;
7150 if i > 0 {
7151 total += gap;
7152 }
7153 }
7154 _ => {
7155 total = total.max(child_width);
7156 }
7157 }
7158 }
7159 total
7160 + style.padding.horizontal()
7161 + style.margin.horizontal()
7162 + style.border_width.horizontal()
7163 }
7164 }
7165 }
7166 }
7167
7168 pub fn measure_min_content_width(
7172 &self,
7173 node: &Node,
7174 style: &ResolvedStyle,
7175 font_context: &FontContext,
7176 ) -> f64 {
7177 match &node.kind {
7178 NodeKind::Table { columns } => {
7179 let num_cols = node
7182 .children
7183 .iter()
7184 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7185 .max()
7186 .unwrap_or(1)
7187 .max(columns.len().max(1));
7188 let (col_min, _) =
7189 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7190 col_min.iter().sum::<f64>()
7191 + style.padding.horizontal()
7192 + style.margin.horizontal()
7193 + style.border_width.horizontal()
7194 }
7195 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7196 let word_width = if !runs.is_empty() {
7197 runs.iter()
7199 .map(|run| {
7200 let run_style = run.style.resolve(Some(style), 0.0);
7201 let run_content = substitute_page_placeholders(&run.content);
7202 let transformed =
7203 apply_text_transform(&run_content, run_style.text_transform);
7204 self.text_layout.measure_widest_word(
7205 font_context,
7206 &transformed,
7207 run_style.font_size,
7208 &run_style.font_family,
7209 run_style.font_weight,
7210 run_style.font_style,
7211 run_style.letter_spacing,
7212 run_style.word_spacing,
7213 style.hyphens,
7214 style.lang.as_deref(),
7215 )
7216 })
7217 .fold(0.0f64, f64::max)
7218 } else {
7219 let content = substitute_page_placeholders(content);
7220 let transformed = apply_text_transform(&content, style.text_transform);
7221 self.text_layout.measure_widest_word(
7222 font_context,
7223 &transformed,
7224 style.font_size,
7225 &style.font_family,
7226 style.font_weight,
7227 style.font_style,
7228 style.letter_spacing,
7229 style.word_spacing,
7230 style.hyphens,
7231 style.lang.as_deref(),
7232 )
7233 };
7234 word_width + style.padding.horizontal() + style.margin.horizontal()
7235 }
7236 NodeKind::Image { width, .. } => {
7237 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
7238 }
7239 NodeKind::Svg { width, .. } => {
7240 *width + style.padding.horizontal() + style.margin.horizontal()
7241 }
7242 _ => {
7243 if node.children.is_empty() {
7244 style.padding.horizontal()
7245 + style.margin.horizontal()
7246 + style.border_width.horizontal()
7247 } else {
7248 let mut max_child_min = 0.0f64;
7249 for child in &node.children {
7250 let child_style = child.style.resolve(Some(style), 0.0);
7251 let child_min =
7252 self.measure_min_content_width(child, &child_style, font_context);
7253 max_child_min = max_child_min.max(child_min);
7254 }
7255 max_child_min
7256 + style.padding.horizontal()
7257 + style.margin.horizontal()
7258 + style.border_width.horizontal()
7259 }
7260 }
7261 }
7262 }
7263
7264 fn flex_item_baseline_distance(
7271 &self,
7272 item: &Node,
7273 style: &ResolvedStyle,
7274 w: f64,
7275 font_context: &FontContext,
7276 ) -> f64 {
7277 let first_line = match &item.kind {
7278 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
7279 let metrics = font_context.baseline_metrics(
7280 &style.font_family,
7281 style.font_weight,
7282 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
7283 );
7284 baseline_in_line(
7285 style.font_size * style.line_height,
7286 style.font_size,
7287 metrics,
7288 )
7289 }
7290 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
7291 };
7292 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
7293 }
7294
7295 fn cell_first_baseline_in_line(
7302 &self,
7303 cell: &Node,
7304 cell_style: &ResolvedStyle,
7305 w: f64,
7306 font_context: &FontContext,
7307 ) -> f64 {
7308 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
7309 for ch in &node.children {
7310 let s = ch.style.resolve(Some(parent), w);
7311 match &ch.kind {
7312 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
7313 _ => {
7314 if let Some(f) = first(ch, &s, w) {
7315 return Some(f);
7316 }
7317 }
7318 }
7319 }
7320 None
7321 }
7322 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
7323 let metrics = font_context.baseline_metrics(
7324 &s.font_family,
7325 s.font_weight,
7326 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
7327 );
7328 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
7329 }
7330
7331 fn cell_baseline_distance(
7336 &self,
7337 cell: &Node,
7338 cell_style: &ResolvedStyle,
7339 inner_width: f64,
7340 font_context: &FontContext,
7341 ) -> f64 {
7342 cell_style.padding.top
7343 + cell_style.border_width.top
7344 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
7345 }
7346
7347 fn row_baseline(
7350 &self,
7351 row: &Node,
7352 row_style: &ResolvedStyle,
7353 col_widths: &[f64],
7354 col_offsets: &[usize],
7355 font_context: &FontContext,
7356 ) -> Option<f64> {
7357 let mut b: Option<f64> = None;
7358 for (cell_i, cell) in row.children.iter().enumerate() {
7359 let span = match &cell.kind {
7360 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7361 _ => 1,
7362 };
7363 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7364 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7365 let cell_style = cell.style.resolve(Some(row_style), col_width);
7366 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7367 let iw = col_width
7368 - cell_style.padding.horizontal()
7369 - cell_style.border_width.horizontal();
7370 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
7371 b = Some(b.map_or(d, |m: f64| m.max(d)));
7372 }
7373 }
7374 b
7375 }
7376
7377 fn measure_table_row_height(
7378 &self,
7379 row: &Node,
7380 col_widths: &[f64],
7381 col_offsets: &[usize],
7382 parent_style: &ResolvedStyle,
7383 font_context: &FontContext,
7384 ) -> f64 {
7385 let row_style = row
7386 .style
7387 .resolve(Some(parent_style), col_widths.iter().sum());
7388 let mut max_height: f64 = 0.0;
7389 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
7392
7393 for (cell_i, cell) in row.children.iter().enumerate() {
7394 let span = match &cell.kind {
7395 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7396 _ => 1,
7397 };
7398 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7399 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7400 let cell_style = cell.style.resolve(Some(&row_style), col_width);
7401 let inner_width =
7402 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
7403
7404 let mut cell_content_height = 0.0;
7405 for child in &cell.children {
7406 let child_style = child.style.resolve(Some(&cell_style), inner_width);
7407 cell_content_height +=
7408 self.measure_node_height(child, inner_width, &child_style, font_context);
7409 }
7410
7411 let mut total = cell_content_height
7412 + cell_style.padding.vertical()
7413 + cell_style.border_width.vertical();
7414 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7418 if let Some(b) = row_bl {
7419 let d =
7420 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
7421 total += (b - d).max(0.0);
7422 }
7423 }
7424 if let SizeConstraint::Fixed(h) = cell_style.height {
7431 total = total.max(h);
7432 }
7433 max_height = max_height.max(total);
7434 }
7435
7436 max_height.max(row_style.min_height)
7437 }
7438
7439 fn cell_col_span(cell: &Node) -> usize {
7441 match &cell.kind {
7442 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7443 _ => 1,
7444 }
7445 }
7446
7447 fn measure_column_content(
7452 &self,
7453 children: &[Node],
7454 num_cols: usize,
7455 available_width: f64,
7456 table_style: &ResolvedStyle,
7457 font_context: &FontContext,
7458 ) -> (Vec<f64>, Vec<f64>) {
7459 let mut col_min = vec![0.0f64; num_cols];
7460 let mut col_max = vec![0.0f64; num_cols];
7461 let offsets = Self::table_column_offsets(children);
7462 for (row_i, row_node) in children.iter().enumerate() {
7463 for (cell_i, cell) in row_node.children.iter().enumerate() {
7464 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7465 let span = Self::cell_col_span(cell);
7466 let cell_style = cell.style.resolve(Some(table_style), available_width);
7467 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7468 let mut cmin = 0.0f64;
7469 let mut cmax = 0.0f64;
7470 for child in &cell.children {
7471 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7472 cmin =
7473 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7474 cmax =
7475 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7476 }
7477 cmin += chrome;
7478 let mut cmax = cmax.max(cmin) + chrome;
7479 if let SizeConstraint::Fixed(w) = cell_style.width {
7480 cmax = w.max(cmin);
7481 }
7482 let per_min = cmin / span as f64;
7483 let per_max = cmax / span as f64;
7484 for k in col..(col + span).min(num_cols) {
7485 col_min[k] = col_min[k].max(per_min);
7486 col_max[k] = col_max[k].max(per_max);
7487 }
7488 }
7489 }
7490 (col_min, col_max)
7491 }
7492
7493 fn resolve_column_widths(
7503 &self,
7504 defs: &[ColumnDef],
7505 available_width: f64,
7506 children: &[Node],
7507 table_style: &ResolvedStyle,
7508 font_context: &FontContext,
7509 ) -> Vec<f64> {
7510 if defs.is_empty() {
7511 let num_cols = Self::occupancy_column_count(children);
7512
7513 let (col_min, col_max) = self.measure_column_content(
7514 children,
7515 num_cols,
7516 available_width,
7517 table_style,
7518 font_context,
7519 );
7520
7521 let sum_min: f64 = col_min.iter().sum();
7522 let sum_max: f64 = col_max.iter().sum();
7523 let w = available_width;
7524 return if sum_max <= w {
7525 if sum_max <= f64::EPSILON {
7529 vec![w / num_cols as f64; num_cols]
7530 } else {
7531 col_max
7532 .iter()
7533 .map(|m| m + (w - sum_max) * (m / sum_max))
7534 .collect()
7535 }
7536 } else if sum_min <= w {
7537 let denom = (sum_max - sum_min).max(f64::EPSILON);
7540 col_min
7541 .iter()
7542 .zip(&col_max)
7543 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7544 .collect()
7545 } else {
7546 self.defect(format!(
7549 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7550 sum_min, w
7551 ));
7552 let scale = w / sum_min.max(f64::EPSILON);
7553 col_min.iter().map(|m| m * scale).collect()
7554 };
7555 }
7556
7557 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7563 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7564 while defs_vec.len() < num_cols {
7565 defs_vec.push(ColumnDef {
7566 width: ColumnWidth::Auto,
7567 });
7568 }
7569 let defs = &defs_vec[..];
7570
7571 let mut widths = Vec::new();
7572 let mut remaining = available_width;
7573 let mut auto_count = 0;
7574
7575 for def in defs {
7576 match def.width {
7577 ColumnWidth::Fixed(w) => {
7578 widths.push(w);
7579 remaining -= w;
7580 }
7581 ColumnWidth::Fraction(f) => {
7582 let w = available_width * f;
7583 widths.push(w);
7584 remaining -= w;
7585 }
7586 ColumnWidth::Auto => {
7587 widths.push(0.0);
7588 auto_count += 1;
7589 }
7590 }
7591 }
7592
7593 const OVERFULL_EPS: f64 = 0.01;
7602 if remaining < -OVERFULL_EPS {
7603 self.defect(format!(
7606 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7607 available_width - remaining,
7608 available_width
7609 ));
7610 remaining = 0.0;
7611 }
7612 remaining = remaining.max(0.0);
7615
7616 if auto_count > 0 {
7617 let auto_width = remaining / auto_count as f64;
7618 for (i, def) in defs.iter().enumerate() {
7619 if matches!(def.width, ColumnWidth::Auto) {
7620 widths[i] = auto_width;
7621 }
7622 }
7623 }
7624
7625 let (col_min, _) = self.measure_column_content(
7632 children,
7633 widths.len(),
7634 available_width,
7635 table_style,
7636 font_context,
7637 );
7638 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7639 if needs_floor {
7640 let sum_min: f64 = col_min.iter().sum();
7641 if sum_min > available_width {
7642 self.defect(format!(
7643 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7644 sum_min, available_width
7645 ));
7646 let scale = available_width / sum_min.max(f64::EPSILON);
7647 return col_min.iter().map(|m| m * scale).collect();
7648 }
7649 let deficit: f64 = widths
7650 .iter()
7651 .zip(&col_min)
7652 .map(|(w, m)| (m - w).max(0.0))
7653 .sum();
7654 let surplus: f64 = widths
7655 .iter()
7656 .zip(&col_min)
7657 .map(|(w, m)| (w - m).max(0.0))
7658 .sum();
7659 let take = if surplus > 0.0 {
7660 deficit / surplus
7661 } else {
7662 0.0
7663 };
7664 widths = widths
7665 .iter()
7666 .zip(&col_min)
7667 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7668 .collect();
7669 }
7670
7671 widths
7672 }
7673
7674 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7675 for (page_index, page) in pages.iter_mut().enumerate() {
7676 if !page.watermarks.is_empty() {
7678 let (page_w, page_h) = page.config.size.dimensions();
7679 let cx = page_w / 2.0;
7680 let cy = page_h / 2.0;
7681
7682 let mut watermark_elements = Vec::new();
7683 for (wm_node, wm_parent) in &page.watermarks {
7684 if let NodeKind::Watermark {
7685 text,
7686 font_size,
7687 angle,
7688 } = &wm_node.kind
7689 {
7690 let style = wm_node.style.resolve(wm_parent.as_ref(), page_w);
7691 let color = style.color;
7692 let opacity = style.opacity;
7693 let angle_rad = angle.to_radians();
7694
7695 let italic =
7697 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7698
7699 let shaped = self.text_layout.shape_text(
7701 font_context,
7702 text,
7703 &style.font_family,
7704 style.font_weight,
7705 style.font_style,
7706 );
7707
7708 let mut glyphs = Vec::new();
7709 let mut x_pos = 0.0;
7710 let text_chars: Vec<char> = text.chars().collect();
7711
7712 if let Some(shaped_glyphs) = shaped {
7713 let units_per_em = font_context.units_per_em(
7715 &style.font_family,
7716 style.font_weight,
7717 italic,
7718 ) as f64;
7719
7720 let clusters = cluster_texts(&shaped_glyphs, &text_chars);
7721 for (sg, (cluster_text, ligature)) in shaped_glyphs.iter().zip(clusters)
7722 {
7723 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7724 let cluster_idx = sg.cluster as usize;
7725 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7726 glyphs.push(PositionedGlyph {
7727 glyph_id: sg.glyph_id,
7728 char_value: ch,
7729 x_offset: x_pos,
7730 y_offset: 0.0,
7731 x_advance: advance,
7732 font_size: *font_size,
7733 font_family: Arc::from(style.font_family.as_str()),
7734 font_weight: style.font_weight,
7735 font_style: style.font_style,
7736 color: Some(color),
7737 href: None,
7738 text_decoration: TextDecoration::None,
7739 letter_spacing: style.letter_spacing,
7740 cluster_text,
7741 ligature,
7742 });
7743 x_pos += advance + style.letter_spacing;
7744 }
7745 } else {
7746 for &ch in &text_chars {
7748 let w = font_context.char_width(
7749 ch,
7750 &style.font_family,
7751 style.font_weight,
7752 italic,
7753 *font_size,
7754 );
7755 glyphs.push(PositionedGlyph {
7756 glyph_id: ch as u16,
7757 char_value: ch,
7758 x_offset: x_pos,
7759 y_offset: 0.0,
7760 x_advance: w,
7761 font_size: *font_size,
7762 font_family: Arc::from(style.font_family.as_str()),
7763 font_weight: style.font_weight,
7764 font_style: style.font_style,
7765 color: Some(color),
7766 href: None,
7767 text_decoration: TextDecoration::None,
7768 letter_spacing: style.letter_spacing,
7769 cluster_text: None,
7770 ligature: false,
7771 });
7772 x_pos += w + style.letter_spacing;
7773 }
7774 }
7775
7776 let text_width = x_pos;
7777
7778 let line = TextLine {
7779 x: 0.0,
7780 y: 0.0,
7781 glyphs,
7782 width: text_width,
7783 height: *font_size,
7784 word_spacing: 0.0,
7785 };
7786
7787 watermark_elements.push(LayoutElement {
7788 x: cx,
7789 y: cy,
7790 width: text_width,
7791 height: *font_size,
7792 draw: DrawCommand::Watermark {
7793 lines: vec![line],
7794 color,
7795 opacity,
7796 angle_rad,
7797 font_family: style.font_family.clone(),
7798 },
7799 children: vec![],
7800 node_type: Some("Watermark".to_string()),
7801 resolved_style: None,
7802 source_location: None,
7803 href: None,
7804 bookmark: None,
7805 alt: None,
7806 is_header_row: false,
7807 actual_text: None,
7808 list_numbering: None,
7809 col_span: 1,
7810 overflow: Overflow::default(),
7811 opacity: 1.0,
7812 });
7813 }
7814 }
7815
7816 watermark_elements.append(&mut page.elements);
7818 page.elements = watermark_elements;
7819 page.watermarks.clear();
7820 }
7821
7822 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7823 continue;
7824 }
7825
7826 if !page.fixed_header.is_empty() {
7828 let mut hdr_cursor = PageCursor::new(&page.config);
7829 for (node, _h, parent) in &page.fixed_header {
7830 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7833 continue;
7834 }
7835 let cw = hdr_cursor.content_width;
7836 let cx = hdr_cursor.content_x;
7837 let style = node.style.resolve(parent.as_ref(), cw);
7838 self.layout_view(
7839 node,
7840 &style,
7841 &mut hdr_cursor,
7842 &mut Vec::new(),
7843 cx,
7844 cw,
7845 font_context,
7846 );
7847 }
7848 let mut combined = hdr_cursor.elements;
7850 combined.append(&mut page.elements);
7851 page.elements = combined;
7852 }
7853
7854 if !page.fixed_footer.is_empty() {
7859 let mut ftr_cursor = PageCursor::new(&page.config);
7860 let total_ftr: f64 = page
7861 .fixed_footer
7862 .iter()
7863 .filter(|(n, _, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7864 .map(|(_, h, _)| *h)
7865 .sum();
7866 let target_y = ftr_cursor.content_height - total_ftr;
7867 for (node, _h, parent) in &page.fixed_footer {
7869 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7870 continue;
7871 }
7872 let cw = ftr_cursor.content_width;
7873 let cx = ftr_cursor.content_x;
7874 let style = node.style.resolve(parent.as_ref(), cw);
7875 self.layout_view(
7876 node,
7877 &style,
7878 &mut ftr_cursor,
7879 &mut Vec::new(),
7880 cx,
7881 cw,
7882 font_context,
7883 );
7884 }
7885 for el in &mut ftr_cursor.elements {
7889 offset_element_y(el, target_y);
7890 }
7891 page.elements.extend(ftr_cursor.elements);
7892 }
7893
7894 page.fixed_header.clear();
7896 page.fixed_footer.clear();
7897 }
7898 }
7899
7900 #[allow(clippy::too_many_arguments)]
7905 fn layout_grid_children(
7906 &self,
7907 children: &[Node],
7908 parent_style: &ResolvedStyle,
7909 cursor: &mut PageCursor,
7910 pages: &mut Vec<LayoutPage>,
7911 x: f64,
7912 available_width: f64,
7913 font_context: &FontContext,
7914 ) {
7915 let template_cols = match &parent_style.grid_template_columns {
7916 Some(cols) => cols,
7917 None => return, };
7919
7920 let num_columns = template_cols.len();
7921 if num_columns == 0 || children.is_empty() {
7922 return;
7923 }
7924
7925 let col_gap = parent_style.column_gap;
7926 let row_gap = parent_style.row_gap;
7927
7928 let content_sizes: Vec<f64> = template_cols
7931 .iter()
7932 .map(|track| {
7933 if matches!(track, GridTrackSize::Auto) {
7934 available_width / num_columns as f64
7937 } else {
7938 0.0
7939 }
7940 })
7941 .collect();
7942
7943 let col_widths =
7944 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7945
7946 let placements: Vec<Option<&GridPlacement>> = children
7948 .iter()
7949 .map(|child| child.style.grid_placement.as_ref())
7950 .collect();
7951
7952 let item_placements = grid::place_items(&placements, num_columns);
7954 let num_rows = grid::compute_num_rows(&item_placements);
7955
7956 if num_rows == 0 {
7957 return;
7958 }
7959
7960 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7962 for placement in &item_placements {
7963 let cell_width =
7964 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7965 let child = &children[placement.child_index];
7966 let child_style = child.style.resolve(Some(parent_style), cell_width);
7967 item_heights[placement.child_index] =
7968 self.measure_node_height(child, cell_width, &child_style, font_context);
7969 }
7970
7971 let template_rows = parent_style.grid_template_rows.as_deref();
7973 let mut row_heights = vec![0.0_f64; num_rows];
7974 for placement in &item_placements {
7975 let h = item_heights[placement.child_index];
7976 let span = placement.row_end - placement.row_start;
7977 let per_row = h / span as f64;
7978 for rh in row_heights
7979 .iter_mut()
7980 .take(placement.row_end.min(num_rows))
7981 .skip(placement.row_start)
7982 {
7983 if per_row > *rh {
7984 *rh = per_row;
7985 }
7986 }
7987 }
7988
7989 if let Some(template) = template_rows {
7991 let auto_row = parent_style.grid_auto_rows.as_ref();
7992 for (r, rh) in row_heights.iter_mut().enumerate() {
7993 let track = template.get(r).or(auto_row);
7994 if let Some(track) = track {
7995 match track {
7996 GridTrackSize::Pt(pts) => *rh = *pts,
7997 GridTrackSize::Auto => {} _ => {} }
8000 }
8001 }
8002 }
8003
8004 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
8006 if row_height > cursor.remaining_height() {
8011 pages.push(cursor.finalize());
8012 *cursor = cursor.new_page();
8013 }
8014
8015 let row_start_y = cursor.y;
8016
8017 for placement in &item_placements {
8019 if placement.row_start != row {
8020 continue; }
8022
8023 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
8024 let cell_width =
8025 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
8026
8027 let child = &children[placement.child_index];
8028
8029 self.layout_node(
8030 child,
8031 cursor,
8032 pages,
8033 cell_x,
8034 cell_width,
8035 Some(parent_style),
8036 font_context,
8037 None,
8038 None,
8039 );
8040 cursor.y = row_start_y;
8042 }
8043
8044 cursor.y = row_start_y + row_height + row_gap;
8045 }
8046
8047 if num_rows > 0 {
8049 cursor.y -= row_gap;
8050 }
8051 }
8052}
8053
8054fn end_y_of(frags: &[ItemFragments], last: usize) -> f64 {
8058 frags
8059 .iter()
8060 .filter(|f| f.pages.len() == last)
8061 .map(|f| f.final_y)
8062 .fold(f64::MIN, f64::max)
8063}
8064
8065fn paints_a_box(el: &LayoutElement) -> bool {
8073 match &el.draw {
8074 DrawCommand::Rect {
8075 background,
8076 border_width,
8077 ..
8078 } => {
8079 background.is_some()
8080 || border_width.top > 0.0
8081 || border_width.right > 0.0
8082 || border_width.bottom > 0.0
8083 || border_width.left > 0.0
8084 }
8085 _ => false,
8086 }
8087}
8088
8089fn drain_since(elements: &mut Vec<LayoutElement>, snapshot: usize) -> Vec<LayoutElement> {
8106 let start = snapshot.min(elements.len());
8107 elements.drain(start..).collect()
8108}
8109
8110fn stretch_fragment(elements: &mut [LayoutElement], bottom: f64) {
8111 for el in elements.iter_mut() {
8112 let grown = bottom - el.y;
8113 if grown > el.height {
8114 el.height = grown;
8115 }
8116 }
8117}
8118
8119struct ItemFragments {
8125 pages: Vec<LayoutPage>,
8126 tail: Vec<LayoutElement>,
8127 cursor: PageCursor,
8128 final_y: f64,
8129 stretches: bool,
8134}
8135
8136struct FlexItem<'a> {
8137 node: &'a Node,
8138 style: ResolvedStyle,
8139 base_width: f64,
8140 min_content_width: f64,
8141}
8142
8143fn first_text_snippet(node: &Node) -> Option<String> {
8146 fn walk(n: &Node) -> Option<&str> {
8147 match &n.kind {
8148 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
8149 if !content.trim().is_empty() {
8150 return Some(content.trim());
8151 }
8152 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
8153 return Some(r.content.trim());
8154 }
8155 None
8156 }
8157 _ => n.children.iter().find_map(walk),
8158 }
8159 }
8160 walk(node).map(|t| {
8161 let mut s: String = t.chars().take(32).collect();
8162 if t.chars().count() > 32 {
8163 s.push('…');
8164 }
8165 s
8166 })
8167}
8168
8169fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
8179 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
8180}
8181
8182#[cfg(test)]
8183mod tests {
8184 use super::*;
8185 use crate::font::FontContext;
8186
8187 fn sg(glyph_id: u16, cluster: u32) -> shaping::ShapedGlyph {
8188 shaping::ShapedGlyph {
8189 glyph_id,
8190 cluster,
8191 x_advance: 500,
8192 y_advance: 0,
8193 x_offset: 0,
8194 y_offset: 0,
8195 }
8196 }
8197
8198 #[test]
8199 fn test_cluster_texts_ltr_ligature() {
8200 let chars: Vec<char> = "office".chars().collect();
8202 let shaped = [sg(1, 0), sg(2, 1), sg(3, 4), sg(4, 5)];
8203 let got = cluster_texts(&shaped, &chars);
8204 assert_eq!(
8205 got,
8206 vec![
8207 (None, false),
8208 (Some("ffi".to_string()), true),
8209 (None, false),
8210 (None, false)
8211 ]
8212 );
8213 }
8214
8215 #[test]
8220 fn test_cluster_texts_rtl_ligature_uses_next_larger_cluster() {
8221 let chars: Vec<char> = "abcd".chars().collect();
8222 let shaped = [sg(1, 3), sg(2, 1), sg(3, 0)];
8223 let got = cluster_texts(&shaped, &chars);
8224 assert_eq!(
8225 got,
8226 vec![(None, false), (Some("bc".to_string()), true), (None, false)]
8227 );
8228 }
8229
8230 #[test]
8234 fn test_cluster_texts_ligature_found_when_counts_balance() {
8235 let chars: Vec<char> = "abcd".chars().collect();
8237 let shaped = [sg(1, 0), sg(2, 2), sg(3, 3), sg(4, 3)];
8238 let got = cluster_texts(&shaped, &chars);
8239 assert_eq!(got[0], (Some("ab".to_string()), true));
8240 assert_eq!(got[1], (None, false));
8241 assert_eq!(got[2], (None, false));
8242 assert_eq!(got[3], (None, false));
8243 }
8244
8245 #[test]
8249 fn test_cluster_texts_multi_glyph_cluster_is_not_a_ligature() {
8250 let chars: Vec<char> = "kixy".chars().collect();
8252 let shaped = [sg(1, 0), sg(2, 0), sg(3, 3)];
8253 let got = cluster_texts(&shaped, &chars);
8254 assert_eq!(got[0], (Some("kix".to_string()), false));
8255 assert_eq!(got[1], (Some("kix".to_string()), false));
8256 assert_eq!(got[2], (None, false));
8257 }
8258
8259 fn make_text(content: &str, font_size: f64) -> Node {
8260 Node {
8261 kind: NodeKind::Text {
8262 content: content.to_string(),
8263 href: None,
8264 runs: vec![],
8265 },
8266 style: Style {
8267 font_size: Some(font_size),
8268 ..Default::default()
8269 },
8270 children: vec![],
8271 id: None,
8272 source_location: None,
8273 bookmark: None,
8274 href: None,
8275 alt: None,
8276 }
8277 }
8278
8279 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
8280 Node {
8281 kind: NodeKind::View,
8282 style,
8283 children,
8284 id: None,
8285 source_location: None,
8286 bookmark: None,
8287 href: None,
8288 alt: None,
8289 }
8290 }
8291
8292 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
8293 Node {
8294 kind: NodeKind::Text {
8295 content: String::new(),
8296 href: None,
8297 runs,
8298 },
8299 style: Style::default(),
8300 children: vec![],
8301 id: None,
8302 source_location: None,
8303 bookmark: None,
8304 href: None,
8305 alt: None,
8306 }
8307 }
8308
8309 #[test]
8310 fn intrinsic_width_measures_runs_not_just_content() {
8311 let engine = LayoutEngine::new();
8315 let font_context = FontContext::new();
8316
8317 let runs_node = make_runs_text(vec![
8318 crate::model::TextRun {
8319 content: "Hello ".to_string(),
8320 style: Style::default(),
8321 href: None,
8322 },
8323 crate::model::TextRun {
8324 content: "World".to_string(),
8325 style: Style {
8326 font_weight: Some(700),
8327 ..Default::default()
8328 },
8329 href: None,
8330 },
8331 ]);
8332 let plain_node = make_text("Hello World", 12.0);
8333
8334 let runs_style = runs_node.style.resolve(None, 0.0);
8335 let plain_style = plain_node.style.resolve(None, 0.0);
8336 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
8337 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
8338
8339 assert!(
8342 runs_w >= plain_w,
8343 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
8344 );
8345 assert!(
8346 runs_w < plain_w * 1.5,
8347 "runs width ({runs_w}) should be close to plain width ({plain_w})"
8348 );
8349 }
8350
8351 #[test]
8352 fn intrinsic_width_of_multiline_text_is_widest_line() {
8353 let engine = LayoutEngine::new();
8354 let font_context = FontContext::new();
8355
8356 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
8357 let widest = make_text("Springfield, IL 62704", 12.0);
8358
8359 let m_style = multiline.style.resolve(None, 0.0);
8360 let w_style = widest.style.resolve(None, 0.0);
8361 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
8362 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
8363
8364 assert!(
8365 (m_w - w_w).abs() < 0.01,
8366 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
8367 );
8368 }
8369
8370 #[test]
8371 fn intrinsic_width_of_heading_measures_its_text() {
8372 let engine = LayoutEngine::new();
8375 let font_context = FontContext::new();
8376
8377 let heading = Node {
8378 kind: NodeKind::Heading {
8379 level: 1,
8380 content: "Invoice #2024-001".to_string(),
8381 href: None,
8382 runs: vec![],
8383 },
8384 style: Style {
8385 font_size: Some(24.0),
8386 ..Default::default()
8387 },
8388 children: vec![],
8389 id: None,
8390 source_location: None,
8391 bookmark: None,
8392 href: None,
8393 alt: None,
8394 };
8395 let style = heading.style.resolve(None, 0.0);
8396 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
8397 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
8398 }
8399
8400 #[test]
8401 fn measure_node_height_of_wrapping_heading_matches_text() {
8402 let engine = LayoutEngine::new();
8409 let font_context = FontContext::new();
8410
8411 let content = "Annual Performance Review";
8412 let heading = Node {
8413 kind: NodeKind::Heading {
8414 level: 1,
8415 content: content.to_string(),
8416 href: None,
8417 runs: vec![],
8418 },
8419 style: Style {
8420 font_size: Some(32.0),
8421 ..Default::default()
8422 },
8423 children: vec![],
8424 id: None,
8425 source_location: None,
8426 bookmark: None,
8427 href: None,
8428 alt: None,
8429 };
8430 let text = make_text(content, 32.0);
8431
8432 let width = 200.0;
8434 let h_style = heading.style.resolve(None, width);
8435 let t_style = text.style.resolve(None, width);
8436 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
8437 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
8438
8439 assert!(
8440 h_height > 32.0,
8441 "a wrapping 32pt heading must measure more than one line, got {h_height}"
8442 );
8443 assert!(
8444 (h_height - t_height).abs() < 0.01,
8445 "heading height ({h_height}) must equal the same text's height ({t_height})"
8446 );
8447 }
8448
8449 #[test]
8450 fn intrinsic_width_flex_row_sums_children() {
8451 let engine = LayoutEngine::new();
8452 let font_context = FontContext::new();
8453
8454 let child1 = make_text("Hello", 14.0);
8455 let child2 = make_text("World", 14.0);
8456
8457 let child1_style = child1.style.resolve(None, 0.0);
8458 let child2_style = child2.style.resolve(None, 0.0);
8459 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
8460 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
8461
8462 let row = make_styled_view(
8463 Style {
8464 flex_direction: Some(FlexDirection::Row),
8465 ..Default::default()
8466 },
8467 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
8468 );
8469 let row_style = row.style.resolve(None, 0.0);
8470 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
8471
8472 assert!(
8473 (row_w - (child1_w + child2_w)).abs() < 0.01,
8474 "Row intrinsic width ({}) should equal sum of children ({} + {})",
8475 row_w,
8476 child1_w,
8477 child2_w
8478 );
8479 }
8480
8481 #[test]
8482 fn intrinsic_width_flex_column_takes_max() {
8483 let engine = LayoutEngine::new();
8484 let font_context = FontContext::new();
8485
8486 let short = make_text("Hi", 14.0);
8487 let long = make_text("Hello World", 14.0);
8488
8489 let short_style = short.style.resolve(None, 0.0);
8490 let long_style = long.style.resolve(None, 0.0);
8491 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
8492 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
8493
8494 let col = make_styled_view(
8495 Style {
8496 flex_direction: Some(FlexDirection::Column),
8497 ..Default::default()
8498 },
8499 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
8500 );
8501 let col_style = col.style.resolve(None, 0.0);
8502 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
8503
8504 assert!(
8505 (col_w - long_w).abs() < 0.01,
8506 "Column intrinsic width ({}) should equal max child ({}, short was {})",
8507 col_w,
8508 long_w,
8509 short_w
8510 );
8511 }
8512
8513 #[test]
8514 fn intrinsic_width_nested_containers() {
8515 let engine = LayoutEngine::new();
8516 let font_context = FontContext::new();
8517
8518 let inner = make_styled_view(
8519 Style {
8520 flex_direction: Some(FlexDirection::Row),
8521 ..Default::default()
8522 },
8523 vec![make_text("A", 12.0), make_text("B", 12.0)],
8524 );
8525 let inner_style = inner.style.resolve(None, 0.0);
8526 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
8527
8528 let outer = make_styled_view(
8529 Style::default(),
8530 vec![make_styled_view(
8531 Style {
8532 flex_direction: Some(FlexDirection::Row),
8533 ..Default::default()
8534 },
8535 vec![make_text("A", 12.0), make_text("B", 12.0)],
8536 )],
8537 );
8538 let outer_style = outer.style.resolve(None, 0.0);
8539 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
8540
8541 assert!(
8542 (outer_w - inner_w).abs() < 0.01,
8543 "Nested container ({}) should match inner container ({})",
8544 outer_w,
8545 inner_w
8546 );
8547 }
8548
8549 #[test]
8550 fn intrinsic_width_row_with_gap() {
8551 let engine = LayoutEngine::new();
8552 let font_context = FontContext::new();
8553
8554 let no_gap = make_styled_view(
8555 Style {
8556 flex_direction: Some(FlexDirection::Row),
8557 ..Default::default()
8558 },
8559 vec![make_text("A", 12.0), make_text("B", 12.0)],
8560 );
8561 let with_gap = make_styled_view(
8562 Style {
8563 flex_direction: Some(FlexDirection::Row),
8564 gap: Some(10.0),
8565 ..Default::default()
8566 },
8567 vec![make_text("A", 12.0), make_text("B", 12.0)],
8568 );
8569
8570 let no_gap_style = no_gap.style.resolve(None, 0.0);
8571 let with_gap_style = with_gap.style.resolve(None, 0.0);
8572 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
8573 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
8574
8575 assert!(
8576 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
8577 "Gap should add 10pt: with_gap={}, no_gap={}",
8578 with_gap_w,
8579 no_gap_w
8580 );
8581 }
8582
8583 #[test]
8584 fn intrinsic_width_empty_container() {
8585 let engine = LayoutEngine::new();
8586 let font_context = FontContext::new();
8587
8588 let padding = 8.0;
8589 let empty = make_styled_view(
8590 Style {
8591 padding: Some(Edges::uniform(padding)),
8592 ..Default::default()
8593 },
8594 vec![],
8595 );
8596 let style = empty.style.resolve(None, 0.0);
8597 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
8598
8599 assert!(
8600 (w - padding * 2.0).abs() < 0.01,
8601 "Empty container width ({}) should equal horizontal padding ({})",
8602 w,
8603 padding * 2.0
8604 );
8605 }
8606
8607 #[test]
8610 fn flex_shrink_respects_min_content_width() {
8611 let engine = LayoutEngine::new();
8615 let font_context = FontContext::new();
8616
8617 let sale_text = make_text("SALE", 12.0);
8618 let sale_style = sale_text.style.resolve(None, 0.0);
8619 let sale_word_width =
8620 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8621 assert!(
8622 sale_word_width > 0.0,
8623 "SALE should have non-zero min-content width"
8624 );
8625
8626 let container = make_styled_view(
8629 Style {
8630 flex_direction: Some(FlexDirection::Row),
8631 width: Some(Dimension::Pt(100.0)),
8632 ..Default::default()
8633 },
8634 vec![
8635 make_styled_view(
8636 Style {
8637 width: Some(Dimension::Pt(80.0)),
8638 flex_shrink: Some(1.0),
8639 ..Default::default()
8640 },
8641 vec![],
8642 ),
8643 make_styled_view(
8644 Style {
8645 width: Some(Dimension::Pt(60.0)),
8646 flex_shrink: Some(1.0),
8647 ..Default::default()
8648 },
8649 vec![make_text("SALE", 12.0)],
8650 ),
8651 ],
8652 );
8653
8654 let doc = Document {
8655 children: vec![Node::page(
8656 PageConfig::default(),
8657 Style::default(),
8658 vec![container],
8659 )],
8660 metadata: Default::default(),
8661 default_page: PageConfig::default(),
8662 first_page: None,
8663 left_page: None,
8664 right_page: None,
8665 named_pages: Default::default(),
8666 attachments: vec![],
8667 zugferd: None,
8668 fonts: vec![],
8669 tagged: false,
8670 pdfa: None,
8671 default_style: None,
8672 embedded_data: None,
8673 flatten_forms: false,
8674 pdf_ua: false,
8675 certification: None,
8676 pdf_version: Default::default(),
8677 pdf_ua2: false,
8678 };
8679
8680 let pages = engine.layout(&doc, &font_context);
8681 assert!(!pages.is_empty());
8682
8683 let page = &pages[0];
8686 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8688 assert!(
8689 container_el.is_some(),
8690 "Should find container with 2 children"
8691 );
8692 let sale_child = &container_el.unwrap().children[1];
8693 assert!(
8694 sale_child.width >= sale_word_width - 0.01,
8695 "SALE child width ({}) should be >= min-content width ({})",
8696 sale_child.width,
8697 sale_word_width
8698 );
8699 }
8700
8701 #[test]
8704 fn column_justify_content_center() {
8705 let engine = LayoutEngine::new();
8708 let font_context = FontContext::new();
8709
8710 let container = make_styled_view(
8711 Style {
8712 flex_direction: Some(FlexDirection::Column),
8713 height: Some(Dimension::Pt(200.0)),
8714 justify_content: Some(JustifyContent::Center),
8715 ..Default::default()
8716 },
8717 vec![make_text("Centered", 12.0)],
8718 );
8719
8720 let doc = Document {
8721 children: vec![Node::page(
8722 PageConfig::default(),
8723 Style::default(),
8724 vec![container],
8725 )],
8726 metadata: Default::default(),
8727 default_page: PageConfig::default(),
8728 first_page: None,
8729 left_page: None,
8730 right_page: None,
8731 named_pages: Default::default(),
8732 attachments: vec![],
8733 zugferd: None,
8734 fonts: vec![],
8735 tagged: false,
8736 pdfa: None,
8737 default_style: None,
8738 embedded_data: None,
8739 flatten_forms: false,
8740 pdf_ua: false,
8741 certification: None,
8742 pdf_version: Default::default(),
8743 pdf_ua2: false,
8744 };
8745
8746 let pages = engine.layout(&doc, &font_context);
8747 let page = &pages[0];
8748
8749 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8752 assert!(
8753 container_el.is_some(),
8754 "Should find container with children"
8755 );
8756 let container_el = container_el.unwrap();
8757 let child = &container_el.children[0];
8758
8759 let child_offset = child.y - container_el.y;
8761 let expected_offset = (200.0 - child.height) / 2.0;
8762 assert!(
8763 (child_offset - expected_offset).abs() < 2.0,
8764 "Child offset ({}) should be near center ({})",
8765 child_offset,
8766 expected_offset
8767 );
8768 }
8769
8770 #[test]
8771 fn column_align_items_center() {
8772 let engine = LayoutEngine::new();
8775 let font_context = FontContext::new();
8776
8777 let container = make_styled_view(
8778 Style {
8779 flex_direction: Some(FlexDirection::Column),
8780 width: Some(Dimension::Pt(300.0)),
8781 align_items: Some(AlignItems::Center),
8782 ..Default::default()
8783 },
8784 vec![make_text("Hi", 12.0)],
8785 );
8786
8787 let doc = Document {
8788 children: vec![Node::page(
8789 PageConfig::default(),
8790 Style::default(),
8791 vec![container],
8792 )],
8793 metadata: Default::default(),
8794 default_page: PageConfig::default(),
8795 first_page: None,
8796 left_page: None,
8797 right_page: None,
8798 named_pages: Default::default(),
8799 attachments: vec![],
8800 zugferd: None,
8801 fonts: vec![],
8802 tagged: false,
8803 pdfa: None,
8804 default_style: None,
8805 embedded_data: None,
8806 flatten_forms: false,
8807 pdf_ua: false,
8808 certification: None,
8809 pdf_version: Default::default(),
8810 pdf_ua2: false,
8811 };
8812
8813 let pages = engine.layout(&doc, &font_context);
8814 let page = &pages[0];
8815
8816 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8817 assert!(container_el.is_some());
8818 let container_el = container_el.unwrap();
8819 let child = &container_el.children[0];
8820
8821 let child_center = child.x + child.width / 2.0;
8823 let container_center = container_el.x + container_el.width / 2.0;
8824 assert!(
8825 (child_center - container_center).abs() < 2.0,
8826 "Child center ({}) should be near container center ({})",
8827 child_center,
8828 container_center
8829 );
8830 }
8831
8832 #[test]
8835 fn absolute_child_positioned_relative_to_parent() {
8836 let engine = LayoutEngine::new();
8840 let font_context = FontContext::new();
8841
8842 let parent = make_styled_view(
8843 Style {
8844 position: Some(crate::model::Position::Relative),
8845 margin: Some(MarginEdges::from_edges(Edges {
8846 top: 50.0,
8847 left: 50.0,
8848 ..Default::default()
8849 })),
8850 width: Some(Dimension::Pt(200.0)),
8851 height: Some(Dimension::Pt(200.0)),
8852 ..Default::default()
8853 },
8854 vec![make_styled_view(
8855 Style {
8856 position: Some(crate::model::Position::Absolute),
8857 top: Some(10.0),
8858 left: Some(10.0),
8859 width: Some(Dimension::Pt(50.0)),
8860 height: Some(Dimension::Pt(50.0)),
8861 ..Default::default()
8862 },
8863 vec![],
8864 )],
8865 );
8866
8867 let doc = Document {
8868 children: vec![Node::page(
8869 PageConfig::default(),
8870 Style::default(),
8871 vec![parent],
8872 )],
8873 metadata: Default::default(),
8874 default_page: PageConfig::default(),
8875 first_page: None,
8876 left_page: None,
8877 right_page: None,
8878 named_pages: Default::default(),
8879 attachments: vec![],
8880 zugferd: None,
8881 fonts: vec![],
8882 tagged: false,
8883 pdfa: None,
8884 default_style: None,
8885 embedded_data: None,
8886 flatten_forms: false,
8887 pdf_ua: false,
8888 certification: None,
8889 pdf_version: Default::default(),
8890 pdf_ua2: false,
8891 };
8892
8893 let pages = engine.layout(&doc, &font_context);
8894 let page = &pages[0];
8895
8896 let parent_el = page
8899 .elements
8900 .iter()
8901 .find(|e| e.width > 190.0 && e.width < 210.0);
8902 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8903 let parent_el = parent_el.unwrap();
8904
8905 let abs_child = parent_el
8907 .children
8908 .iter()
8909 .find(|e| e.width > 45.0 && e.width < 55.0);
8910 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8911 let abs_child = abs_child.unwrap();
8912
8913 let expected_x = parent_el.x + 10.0;
8914 let expected_y = parent_el.y + 10.0;
8915 assert!(
8916 (abs_child.x - expected_x).abs() < 1.0,
8917 "Absolute child x ({}) should be parent.x + 10 ({})",
8918 abs_child.x,
8919 expected_x
8920 );
8921 assert!(
8922 (abs_child.y - expected_y).abs() < 1.0,
8923 "Absolute child y ({}) should be parent.y + 10 ({})",
8924 abs_child.y,
8925 expected_y
8926 );
8927 }
8928
8929 #[test]
8930 fn absolute_escapes_unpositioned_parent_to_page() {
8931 let engine = LayoutEngine::new();
8936 let font_context = FontContext::new();
8937 let parent = make_styled_view(
8938 Style {
8939 margin: Some(MarginEdges::from_edges(Edges {
8940 top: 50.0,
8941 left: 50.0,
8942 ..Default::default()
8943 })),
8944 width: Some(Dimension::Pt(200.0)),
8945 height: Some(Dimension::Pt(200.0)),
8946 ..Default::default()
8947 },
8948 vec![make_styled_view(
8949 Style {
8950 position: Some(crate::model::Position::Absolute),
8951 top: Some(10.0),
8952 left: Some(10.0),
8953 width: Some(Dimension::Pt(50.0)),
8954 height: Some(Dimension::Pt(50.0)),
8955 ..Default::default()
8956 },
8957 vec![],
8958 )],
8959 );
8960 let doc = Document {
8961 children: vec![Node::page(
8962 PageConfig::default(),
8963 Style::default(),
8964 vec![parent],
8965 )],
8966 metadata: Default::default(),
8967 default_page: PageConfig::default(),
8968 first_page: None,
8969 left_page: None,
8970 right_page: None,
8971 named_pages: Default::default(),
8972 attachments: vec![],
8973 zugferd: None,
8974 fonts: vec![],
8975 tagged: false,
8976 pdfa: None,
8977 default_style: None,
8978 embedded_data: None,
8979 flatten_forms: false,
8980 pdf_ua: false,
8981 certification: None,
8982 pdf_version: Default::default(),
8983 pdf_ua2: false,
8984 };
8985 let pages = engine.layout(&doc, &font_context);
8986 let page = &pages[0];
8987 let parent_el = page
8988 .elements
8989 .iter()
8990 .find(|e| e.width > 190.0 && e.width < 210.0)
8991 .expect("parent");
8992 let abs_child = parent_el
8993 .children
8994 .iter()
8995 .find(|e| e.width > 45.0 && e.width < 55.0)
8996 .expect("abs child");
8997 let page_left = PageConfig::default().margin.left;
8998 let page_top = PageConfig::default().margin.top;
8999 assert!(
9000 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
9001 "absolute escapes to the page: x {} should be page_left + 10 ({})",
9002 abs_child.x,
9003 page_left + 10.0
9004 );
9005 assert!(
9006 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
9007 "absolute escapes to the page: y {} should be page_top + 10 ({})",
9008 abs_child.y,
9009 page_top + 10.0
9010 );
9011 assert!(
9012 abs_child.x < parent_el.x,
9013 "child must no longer be parent-relative (parent is 50pt further in)"
9014 );
9015 }
9016
9017 #[test]
9018 fn text_transform_none_passthrough() {
9019 assert_eq!(
9020 apply_text_transform("Hello World", TextTransform::None),
9021 "Hello World"
9022 );
9023 }
9024
9025 #[test]
9026 fn text_transform_uppercase() {
9027 assert_eq!(
9028 apply_text_transform("hello world", TextTransform::Uppercase),
9029 "HELLO WORLD"
9030 );
9031 }
9032
9033 #[test]
9034 fn text_transform_lowercase() {
9035 assert_eq!(
9036 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
9037 "hello world"
9038 );
9039 }
9040
9041 #[test]
9042 fn text_transform_capitalize() {
9043 assert_eq!(
9044 apply_text_transform("hello world", TextTransform::Capitalize),
9045 "Hello World"
9046 );
9047 assert_eq!(
9048 apply_text_transform(" hello world ", TextTransform::Capitalize),
9049 " Hello World "
9050 );
9051 assert_eq!(
9052 apply_text_transform("already Capitalized", TextTransform::Capitalize),
9053 "Already Capitalized"
9054 );
9055 }
9056
9057 #[test]
9058 fn text_transform_capitalize_empty() {
9059 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
9060 }
9061
9062 #[test]
9063 fn apply_char_transform_uppercase() {
9064 assert_eq!(
9065 apply_char_transform('a', TextTransform::Uppercase, false),
9066 'A'
9067 );
9068 assert_eq!(
9069 apply_char_transform('A', TextTransform::Uppercase, false),
9070 'A'
9071 );
9072 }
9073
9074 #[test]
9075 fn apply_char_transform_capitalize_word_start() {
9076 assert_eq!(
9077 apply_char_transform('h', TextTransform::Capitalize, true),
9078 'H'
9079 );
9080 assert_eq!(
9081 apply_char_transform('h', TextTransform::Capitalize, false),
9082 'h'
9083 );
9084 }
9085
9086 #[test]
9089 fn column_flex_grow_single_child_fills_container() {
9090 let engine = LayoutEngine::new();
9093 let font_context = FontContext::new();
9094
9095 let child = make_styled_view(
9096 Style {
9097 flex_grow: Some(1.0),
9098 ..Default::default()
9099 },
9100 vec![make_text("Short", 12.0)],
9101 );
9102
9103 let container = make_styled_view(
9104 Style {
9105 flex_direction: Some(FlexDirection::Column),
9106 height: Some(Dimension::Pt(300.0)),
9107 ..Default::default()
9108 },
9109 vec![child],
9110 );
9111
9112 let doc = Document {
9113 children: vec![Node::page(
9114 PageConfig::default(),
9115 Style::default(),
9116 vec![container],
9117 )],
9118 metadata: Default::default(),
9119 default_page: PageConfig::default(),
9120 first_page: None,
9121 left_page: None,
9122 right_page: None,
9123 named_pages: Default::default(),
9124 attachments: vec![],
9125 zugferd: None,
9126 fonts: vec![],
9127 tagged: false,
9128 pdfa: None,
9129 default_style: None,
9130 embedded_data: None,
9131 flatten_forms: false,
9132 pdf_ua: false,
9133 certification: None,
9134 pdf_version: Default::default(),
9135 pdf_ua2: false,
9136 };
9137
9138 let pages = engine.layout(&doc, &font_context);
9139 let page = &pages[0];
9140
9141 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
9142 assert!(container_el.is_some());
9143 let container_el = container_el.unwrap();
9144 assert!(
9145 (container_el.height - 300.0).abs() < 1.0,
9146 "Container should be 300pt, got {}",
9147 container_el.height
9148 );
9149
9150 let child_el = &container_el.children[0];
9151 assert!(
9152 (child_el.height - 300.0).abs() < 1.0,
9153 "flex-grow child should expand to 300pt, got {}",
9154 child_el.height
9155 );
9156 }
9157
9158 #[test]
9159 fn column_flex_grow_two_children_proportional() {
9160 let engine = LayoutEngine::new();
9163 let font_context = FontContext::new();
9164
9165 let child1 = make_styled_view(
9166 Style {
9167 flex_grow: Some(1.0),
9168 ..Default::default()
9169 },
9170 vec![make_text("A", 12.0)],
9171 );
9172 let child2 = make_styled_view(
9173 Style {
9174 flex_grow: Some(2.0),
9175 ..Default::default()
9176 },
9177 vec![make_text("B", 12.0)],
9178 );
9179
9180 let container = make_styled_view(
9181 Style {
9182 flex_direction: Some(FlexDirection::Column),
9183 height: Some(Dimension::Pt(300.0)),
9184 ..Default::default()
9185 },
9186 vec![child1, child2],
9187 );
9188
9189 let doc = Document {
9190 children: vec![Node::page(
9191 PageConfig::default(),
9192 Style::default(),
9193 vec![container],
9194 )],
9195 metadata: Default::default(),
9196 default_page: PageConfig::default(),
9197 first_page: None,
9198 left_page: None,
9199 right_page: None,
9200 named_pages: Default::default(),
9201 attachments: vec![],
9202 zugferd: None,
9203 fonts: vec![],
9204 tagged: false,
9205 pdfa: None,
9206 default_style: None,
9207 embedded_data: None,
9208 flatten_forms: false,
9209 pdf_ua: false,
9210 certification: None,
9211 pdf_version: Default::default(),
9212 pdf_ua2: false,
9213 };
9214
9215 let pages = engine.layout(&doc, &font_context);
9216 let page = &pages[0];
9217
9218 let container_el = page
9219 .elements
9220 .iter()
9221 .find(|e| e.children.len() == 2)
9222 .expect("Should find container with two children");
9223
9224 let c1 = &container_el.children[0];
9225 let c2 = &container_el.children[1];
9226
9227 let total = c1.height + c2.height;
9231 assert!(
9232 (total - 300.0).abs() < 2.0,
9233 "Children should sum to ~300pt, got {}",
9234 total
9235 );
9236
9237 let ratio = c2.height / c1.height;
9240 assert!(
9241 ratio > 1.3 && ratio < 2.5,
9242 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
9243 ratio
9244 );
9245 }
9246
9247 #[test]
9248 fn column_flex_grow_mixed_grow_and_fixed() {
9249 let engine = LayoutEngine::new();
9252 let font_context = FontContext::new();
9253
9254 let fixed_child = make_styled_view(
9255 Style {
9256 height: Some(Dimension::Pt(50.0)),
9257 ..Default::default()
9258 },
9259 vec![make_text("Fixed", 12.0)],
9260 );
9261 let grow_child = make_styled_view(
9262 Style {
9263 flex_grow: Some(1.0),
9264 ..Default::default()
9265 },
9266 vec![make_text("Grow", 12.0)],
9267 );
9268
9269 let container = make_styled_view(
9270 Style {
9271 flex_direction: Some(FlexDirection::Column),
9272 height: Some(Dimension::Pt(300.0)),
9273 ..Default::default()
9274 },
9275 vec![fixed_child, grow_child],
9276 );
9277
9278 let doc = Document {
9279 children: vec![Node::page(
9280 PageConfig::default(),
9281 Style::default(),
9282 vec![container],
9283 )],
9284 metadata: Default::default(),
9285 default_page: PageConfig::default(),
9286 first_page: None,
9287 left_page: None,
9288 right_page: None,
9289 named_pages: Default::default(),
9290 attachments: vec![],
9291 zugferd: None,
9292 fonts: vec![],
9293 tagged: false,
9294 pdfa: None,
9295 default_style: None,
9296 embedded_data: None,
9297 flatten_forms: false,
9298 pdf_ua: false,
9299 certification: None,
9300 pdf_version: Default::default(),
9301 pdf_ua2: false,
9302 };
9303
9304 let pages = engine.layout(&doc, &font_context);
9305 let page = &pages[0];
9306
9307 let container_el = page
9308 .elements
9309 .iter()
9310 .find(|e| e.children.len() == 2)
9311 .expect("Should find container with two children");
9312
9313 let fixed_el = &container_el.children[0];
9314 let grow_el = &container_el.children[1];
9315
9316 assert!(
9318 (fixed_el.height - 50.0).abs() < 1.0,
9319 "Fixed child should stay at 50pt, got {}",
9320 fixed_el.height
9321 );
9322
9323 assert!(
9325 (grow_el.height - 250.0).abs() < 2.0,
9326 "Grow child should expand to ~250pt, got {}",
9327 grow_el.height
9328 );
9329 }
9330
9331 #[test]
9332 fn column_flex_grow_page_level() {
9333 let engine = LayoutEngine::new();
9335 let font_context = FontContext::new();
9336
9337 let grow_child = make_styled_view(
9338 Style {
9339 flex_grow: Some(1.0),
9340 ..Default::default()
9341 },
9342 vec![make_text("Fill page", 12.0)],
9343 );
9344
9345 let doc = Document {
9346 children: vec![Node::page(
9347 PageConfig::default(),
9348 Style::default(),
9349 vec![grow_child],
9350 )],
9351 metadata: Default::default(),
9352 default_page: PageConfig::default(),
9353 first_page: None,
9354 left_page: None,
9355 right_page: None,
9356 named_pages: Default::default(),
9357 attachments: vec![],
9358 zugferd: None,
9359 fonts: vec![],
9360 tagged: false,
9361 pdfa: None,
9362 default_style: None,
9363 embedded_data: None,
9364 flatten_forms: false,
9365 pdf_ua: false,
9366 certification: None,
9367 pdf_version: Default::default(),
9368 pdf_ua2: false,
9369 };
9370
9371 let pages = engine.layout(&doc, &font_context);
9372 let page = &pages[0];
9373
9374 assert!(
9376 !page.elements.is_empty(),
9377 "Page should have at least one element"
9378 );
9379
9380 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
9381 let el = &page.elements[0];
9382 assert!(
9383 (el.height - content_height).abs() < 2.0,
9384 "Page-level flex-grow child should fill content height ({}), got {}",
9385 content_height,
9386 el.height
9387 );
9388 }
9389
9390 #[test]
9391 fn column_flex_grow_with_justify_content() {
9392 let engine = LayoutEngine::new();
9396 let font_context = FontContext::new();
9397
9398 let fixed_child = make_styled_view(
9399 Style {
9400 height: Some(Dimension::Pt(50.0)),
9401 ..Default::default()
9402 },
9403 vec![make_text("Top", 12.0)],
9404 );
9405 let grow_child = make_styled_view(
9406 Style {
9407 flex_grow: Some(1.0),
9408 ..Default::default()
9409 },
9410 vec![make_text("Fill", 12.0)],
9411 );
9412
9413 let container = make_styled_view(
9414 Style {
9415 flex_direction: Some(FlexDirection::Column),
9416 height: Some(Dimension::Pt(300.0)),
9417 justify_content: Some(JustifyContent::Center),
9418 ..Default::default()
9419 },
9420 vec![fixed_child, grow_child],
9421 );
9422
9423 let doc = Document {
9424 children: vec![Node::page(
9425 PageConfig::default(),
9426 Style::default(),
9427 vec![container],
9428 )],
9429 metadata: Default::default(),
9430 default_page: PageConfig::default(),
9431 first_page: None,
9432 left_page: None,
9433 right_page: None,
9434 named_pages: Default::default(),
9435 attachments: vec![],
9436 zugferd: None,
9437 fonts: vec![],
9438 tagged: false,
9439 pdfa: None,
9440 default_style: None,
9441 embedded_data: None,
9442 flatten_forms: false,
9443 pdf_ua: false,
9444 certification: None,
9445 pdf_version: Default::default(),
9446 pdf_ua2: false,
9447 };
9448
9449 let pages = engine.layout(&doc, &font_context);
9450 let page = &pages[0];
9451
9452 let container_el = page
9453 .elements
9454 .iter()
9455 .find(|e| e.children.len() == 2)
9456 .expect("Should find container");
9457
9458 let first_child = &container_el.children[0];
9461 assert!(
9462 (first_child.y - container_el.y).abs() < 1.0,
9463 "First child should be at top of container"
9464 );
9465
9466 let total = container_el.children[0].height + container_el.children[1].height;
9468 assert!(
9469 (total - 300.0).abs() < 2.0,
9470 "Children should fill container, got {}",
9471 total
9472 );
9473 }
9474
9475 #[test]
9476 fn column_flex_grow_child_justify_content_center() {
9477 let engine = LayoutEngine::new();
9480 let font_context = FontContext::new();
9481
9482 let inner_box = make_styled_view(
9484 Style {
9485 height: Some(Dimension::Pt(40.0)),
9486 ..Default::default()
9487 },
9488 vec![make_text("Centered", 12.0)],
9489 );
9490
9491 let grow_child = make_styled_view(
9493 Style {
9494 flex_grow: Some(1.0),
9495 flex_direction: Some(FlexDirection::Column),
9496 justify_content: Some(JustifyContent::Center),
9497 ..Default::default()
9498 },
9499 vec![inner_box],
9500 );
9501
9502 let container = make_styled_view(
9504 Style {
9505 flex_direction: Some(FlexDirection::Column),
9506 height: Some(Dimension::Pt(400.0)),
9507 ..Default::default()
9508 },
9509 vec![grow_child],
9510 );
9511
9512 let doc = Document {
9513 children: vec![Node::page(
9514 PageConfig::default(),
9515 Style::default(),
9516 vec![container],
9517 )],
9518 metadata: Default::default(),
9519 default_page: PageConfig::default(),
9520 first_page: None,
9521 left_page: None,
9522 right_page: None,
9523 named_pages: Default::default(),
9524 attachments: vec![],
9525 zugferd: None,
9526 fonts: vec![],
9527 tagged: false,
9528 pdfa: None,
9529 default_style: None,
9530 embedded_data: None,
9531 flatten_forms: false,
9532 pdf_ua: false,
9533 certification: None,
9534 pdf_version: Default::default(),
9535 pdf_ua2: false,
9536 };
9537
9538 let pages = engine.layout(&doc, &font_context);
9539 let page = &pages[0];
9540
9541 let container_el = page
9543 .elements
9544 .iter()
9545 .find(|e| e.height > 350.0 && e.children.len() == 1)
9546 .expect("Should find outer container");
9547
9548 let grow_el = &container_el.children[0];
9549 assert!(
9550 (grow_el.height - 400.0).abs() < 2.0,
9551 "Grow child should expand to 400, got {}",
9552 grow_el.height
9553 );
9554
9555 let inner_el = &grow_el.children[0];
9557 let expected_center = grow_el.y + grow_el.height / 2.0;
9558 let actual_center = inner_el.y + inner_el.height / 2.0;
9559 assert!(
9560 (actual_center - expected_center).abs() < 2.0,
9561 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
9562 expected_center,
9563 actual_center
9564 );
9565 }
9566
9567 #[test]
9568 fn column_flex_grow_child_justify_content_flex_end() {
9569 let engine = LayoutEngine::new();
9571 let font_context = FontContext::new();
9572
9573 let inner_box = make_styled_view(
9574 Style {
9575 height: Some(Dimension::Pt(30.0)),
9576 ..Default::default()
9577 },
9578 vec![make_text("Bottom", 12.0)],
9579 );
9580
9581 let grow_child = make_styled_view(
9582 Style {
9583 flex_grow: Some(1.0),
9584 flex_direction: Some(FlexDirection::Column),
9585 justify_content: Some(JustifyContent::FlexEnd),
9586 ..Default::default()
9587 },
9588 vec![inner_box],
9589 );
9590
9591 let container = make_styled_view(
9592 Style {
9593 flex_direction: Some(FlexDirection::Column),
9594 height: Some(Dimension::Pt(300.0)),
9595 ..Default::default()
9596 },
9597 vec![grow_child],
9598 );
9599
9600 let doc = Document {
9601 children: vec![Node::page(
9602 PageConfig::default(),
9603 Style::default(),
9604 vec![container],
9605 )],
9606 metadata: Default::default(),
9607 default_page: PageConfig::default(),
9608 first_page: None,
9609 left_page: None,
9610 right_page: None,
9611 named_pages: Default::default(),
9612 attachments: vec![],
9613 zugferd: None,
9614 fonts: vec![],
9615 tagged: false,
9616 pdfa: None,
9617 default_style: None,
9618 embedded_data: None,
9619 flatten_forms: false,
9620 pdf_ua: false,
9621 certification: None,
9622 pdf_version: Default::default(),
9623 pdf_ua2: false,
9624 };
9625
9626 let pages = engine.layout(&doc, &font_context);
9627 let page = &pages[0];
9628
9629 let container_el = page
9630 .elements
9631 .iter()
9632 .find(|e| e.height > 250.0 && e.children.len() == 1)
9633 .expect("Should find outer container");
9634
9635 let grow_el = &container_el.children[0];
9636 let inner_el = &grow_el.children[0];
9637
9638 let inner_bottom = inner_el.y + inner_el.height;
9640 let grow_bottom = grow_el.y + grow_el.height;
9641 assert!(
9642 (inner_bottom - grow_bottom).abs() < 2.0,
9643 "Inner box bottom ({}) should align with grow child bottom ({})",
9644 inner_bottom,
9645 grow_bottom
9646 );
9647 }
9648
9649 #[test]
9650 fn column_flex_grow_child_no_justify_unchanged() {
9651 let engine = LayoutEngine::new();
9653 let font_context = FontContext::new();
9654
9655 let inner_box = make_styled_view(
9656 Style {
9657 height: Some(Dimension::Pt(50.0)),
9658 ..Default::default()
9659 },
9660 vec![make_text("Top", 12.0)],
9661 );
9662
9663 let grow_child = make_styled_view(
9664 Style {
9665 flex_grow: Some(1.0),
9666 flex_direction: Some(FlexDirection::Column),
9667 ..Default::default()
9669 },
9670 vec![inner_box],
9671 );
9672
9673 let container = make_styled_view(
9674 Style {
9675 flex_direction: Some(FlexDirection::Column),
9676 height: Some(Dimension::Pt(300.0)),
9677 ..Default::default()
9678 },
9679 vec![grow_child],
9680 );
9681
9682 let doc = Document {
9683 children: vec![Node::page(
9684 PageConfig::default(),
9685 Style::default(),
9686 vec![container],
9687 )],
9688 metadata: Default::default(),
9689 default_page: PageConfig::default(),
9690 first_page: None,
9691 left_page: None,
9692 right_page: None,
9693 named_pages: Default::default(),
9694 attachments: vec![],
9695 zugferd: None,
9696 fonts: vec![],
9697 tagged: false,
9698 pdfa: None,
9699 default_style: None,
9700 embedded_data: None,
9701 flatten_forms: false,
9702 pdf_ua: false,
9703 certification: None,
9704 pdf_version: Default::default(),
9705 pdf_ua2: false,
9706 };
9707
9708 let pages = engine.layout(&doc, &font_context);
9709 let page = &pages[0];
9710
9711 let container_el = page
9712 .elements
9713 .iter()
9714 .find(|e| e.height > 250.0 && e.children.len() == 1)
9715 .expect("Should find outer container");
9716
9717 let grow_el = &container_el.children[0];
9718 let inner_el = &grow_el.children[0];
9719
9720 assert!(
9722 (inner_el.y - grow_el.y).abs() < 2.0,
9723 "Inner box ({}) should be at top of grow child ({})",
9724 inner_el.y,
9725 grow_el.y
9726 );
9727 }
9728
9729 #[test]
9730 fn column_flex_grow_child_align_items_center() {
9731 let engine = LayoutEngine::new();
9733 let font_context = FontContext::new();
9734
9735 let text = make_text("Hello", 12.0);
9736
9737 let grow_child = make_styled_view(
9738 Style {
9739 flex_grow: Some(1.0),
9740 flex_direction: Some(FlexDirection::Column),
9741 align_items: Some(AlignItems::Center),
9742 ..Default::default()
9743 },
9744 vec![text],
9745 );
9746
9747 let container = make_styled_view(
9748 Style {
9749 flex_direction: Some(FlexDirection::Column),
9750 height: Some(Dimension::Pt(300.0)),
9751 ..Default::default()
9752 },
9753 vec![grow_child],
9754 );
9755
9756 let doc = Document {
9757 children: vec![Node::page(
9758 PageConfig::default(),
9759 Style::default(),
9760 vec![container],
9761 )],
9762 metadata: Default::default(),
9763 default_page: PageConfig::default(),
9764 first_page: None,
9765 left_page: None,
9766 right_page: None,
9767 named_pages: Default::default(),
9768 attachments: vec![],
9769 zugferd: None,
9770 fonts: vec![],
9771 tagged: false,
9772 pdfa: None,
9773 default_style: None,
9774 embedded_data: None,
9775 flatten_forms: false,
9776 pdf_ua: false,
9777 certification: None,
9778 pdf_version: Default::default(),
9779 pdf_ua2: false,
9780 };
9781
9782 let pages = engine.layout(&doc, &font_context);
9783 let page = &pages[0];
9784
9785 let container_el = page
9786 .elements
9787 .iter()
9788 .find(|e| e.height > 250.0 && e.children.len() == 1)
9789 .expect("Should find outer container");
9790
9791 let grow_el = &container_el.children[0];
9792 assert!(
9793 !grow_el.children.is_empty(),
9794 "Grow child should have text child"
9795 );
9796
9797 let text_el = &grow_el.children[0];
9798 let text_center = text_el.x + text_el.width / 2.0;
9799 let grow_center = grow_el.x + grow_el.width / 2.0;
9800 assert!(
9801 (text_center - grow_center).abs() < 2.0,
9802 "Text center ({}) should be near grow child center ({})",
9803 text_center,
9804 grow_center
9805 );
9806 }
9807
9808 #[test]
9809 fn image_intrinsic_width_respects_height_constraint() {
9810 let engine = LayoutEngine::new();
9814 let font_context = FontContext::new();
9815
9816 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9818
9819 let image_node = Node {
9820 kind: NodeKind::Image {
9821 src: one_px_png.to_string(),
9822 width: None,
9823 height: Some(36.0),
9824 },
9825 style: Style::default(),
9826 children: vec![],
9827 id: None,
9828 source_location: None,
9829 bookmark: None,
9830 href: None,
9831 alt: None,
9832 };
9833
9834 let resolved = image_node.style.resolve(None, 0.0);
9835 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9836
9837 assert!(
9839 (intrinsic - 36.0).abs() < 1.0,
9840 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9841 intrinsic
9842 );
9843 }
9844}