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 text_decoration: TextDecoration,
127 pub text_transform: TextTransform,
128 pub color: Color,
130 pub background_color: Option<Color>,
131 pub border_color: EdgeValues<Color>,
132 pub border_style: EdgeValues<crate::style::BorderStyle>,
133 pub border_radius: CornerValues,
134 pub opacity: f64,
135 pub position: Position,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub top: Option<f64>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub right: Option<f64>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub bottom: Option<f64>,
143 #[serde(skip_serializing_if = "Option::is_none")]
144 pub left: Option<f64>,
145 pub overflow: Overflow,
147 pub breakable: bool,
149 pub break_before: bool,
150 pub min_widow_lines: u32,
151 pub min_orphan_lines: u32,
152}
153
154fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
155 match sc {
156 SizeConstraint::Auto => None,
157 SizeConstraint::Fixed(v) => Some(format!("{v}")),
158 }
159}
160
161impl ElementStyleInfo {
162 fn from_resolved(style: &ResolvedStyle) -> Self {
163 ElementStyleInfo {
164 margin: style.margin.to_edges(),
165 padding: style.padding,
166 border_width: style.border_width,
167 width: size_constraint_to_str(&style.width),
168 height: size_constraint_to_str(&style.height),
169 min_width: if style.min_width > 0.0 {
170 Some(style.min_width)
171 } else {
172 None
173 },
174 min_height: if style.min_height > 0.0 {
175 Some(style.min_height)
176 } else {
177 None
178 },
179 max_width: if style.max_width.is_finite() {
180 Some(style.max_width)
181 } else {
182 None
183 },
184 max_height: if style.max_height.is_finite() {
185 Some(style.max_height)
186 } else {
187 None
188 },
189 flex_direction: style.flex_direction,
190 justify_content: style.justify_content,
191 align_items: style.align_items,
192 align_self: style.align_self,
193 flex_wrap: style.flex_wrap,
194 align_content: style.align_content,
195 flex_grow: style.flex_grow,
196 flex_shrink: style.flex_shrink,
197 flex_basis: size_constraint_to_str(&style.flex_basis),
198 gap: style.gap,
199 row_gap: style.row_gap,
200 column_gap: style.column_gap,
201 font_family: style.font_family.clone(),
202 font_size: style.font_size,
203 font_weight: style.font_weight,
204 font_style: style.font_style,
205 line_height: style.line_height,
206 text_align: style.text_align,
207 letter_spacing: style.letter_spacing,
208 text_decoration: style.text_decoration,
209 text_transform: style.text_transform,
210 color: style.color,
211 background_color: style.background_color,
212 border_color: style.border_color,
213 border_style: style.border_style,
214 border_radius: style.border_radius,
215 opacity: style.opacity,
216 position: style.position,
217 top: style.top,
218 right: style.right,
219 bottom: style.bottom,
220 left: style.left,
221 overflow: style.overflow,
222 breakable: style.breakable,
223 break_before: style.break_before,
224 min_widow_lines: style.min_widow_lines,
225 min_orphan_lines: style.min_orphan_lines,
226 }
227 }
228}
229
230impl Default for ElementStyleInfo {
231 fn default() -> Self {
232 ElementStyleInfo {
233 margin: Edges::default(),
234 padding: Edges::default(),
235 border_width: Edges::default(),
236 width: None,
237 height: None,
238 min_width: None,
239 min_height: None,
240 max_width: None,
241 max_height: None,
242 flex_direction: FlexDirection::default(),
243 justify_content: JustifyContent::default(),
244 align_items: AlignItems::default(),
245 align_self: None,
246 flex_wrap: FlexWrap::default(),
247 align_content: AlignContent::default(),
248 flex_grow: 0.0,
249 flex_shrink: 1.0,
250 flex_basis: None,
251 gap: 0.0,
252 row_gap: 0.0,
253 column_gap: 0.0,
254 font_family: "Helvetica".to_string(),
255 font_size: 12.0,
256 font_weight: 400,
257 font_style: FontStyle::default(),
258 line_height: 1.4,
259 text_align: TextAlign::default(),
260 letter_spacing: 0.0,
261 text_decoration: TextDecoration::None,
262 text_transform: TextTransform::None,
263 color: Color::BLACK,
264 background_color: None,
265 border_color: EdgeValues::uniform(Color::BLACK),
266 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
267 border_radius: CornerValues::uniform(0.0),
268 opacity: 1.0,
269 position: Position::default(),
270 top: None,
271 right: None,
272 bottom: None,
273 left: None,
274 overflow: Overflow::default(),
275 breakable: false,
276 break_before: false,
277 min_widow_lines: 2,
278 min_orphan_lines: 2,
279 }
280 }
281}
282
283#[derive(Debug, Clone, Serialize)]
285#[serde(rename_all = "camelCase")]
286pub struct ElementInfo {
287 pub x: f64,
288 pub y: f64,
289 pub width: f64,
290 pub height: f64,
291 pub kind: String,
293 pub node_type: String,
295 pub style: ElementStyleInfo,
297 pub children: Vec<ElementInfo>,
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub source_location: Option<SourceLocation>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub text_content: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub href: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub bookmark: Option<String>,
311}
312
313impl LayoutInfo {
314 pub fn from_pages(pages: &[LayoutPage]) -> Self {
316 LayoutInfo {
317 pages: pages
318 .iter()
319 .map(|page| {
320 let (page_w, page_h) = page.config.size.dimensions();
321 let content_x = page.config.margin.left;
322 let content_y = page.config.margin.top;
323 let content_width = page_w - page.config.margin.horizontal();
324 let content_height = page_h - page.config.margin.vertical();
325
326 let elements = Self::build_element_tree(&page.elements);
327
328 PageInfo {
329 width: page_w,
330 height: page_h,
331 content_x,
332 content_y,
333 content_width,
334 content_height,
335 elements,
336 }
337 })
338 .collect(),
339 }
340 }
341
342 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
343 elems
344 .iter()
345 .map(|elem| {
346 let kind = match &elem.draw {
347 DrawCommand::None => "None",
348 DrawCommand::Rect { .. } => "Rect",
349 DrawCommand::Text { .. } => "Text",
350 DrawCommand::Image { .. } => "Image",
351 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
352 DrawCommand::Svg { .. } => "Svg",
353 DrawCommand::Barcode { .. } => "Barcode",
354 DrawCommand::QrCode { .. } => "QrCode",
355 DrawCommand::Chart { .. } => "Chart",
356 DrawCommand::Watermark { .. } => "Watermark",
357 DrawCommand::FormField { .. } => "FormField",
358 };
359 let text_content = match &elem.draw {
360 DrawCommand::Text { lines, .. } => {
361 let text: String = lines
362 .iter()
363 .flat_map(|line| {
364 line.glyphs.iter().flat_map(|g| {
365 g.cluster_text.as_deref().unwrap_or("").chars().chain(
367 if g.cluster_text.is_none() {
368 Some(g.char_value)
369 } else {
370 None
371 },
372 )
373 })
374 })
375 .collect();
376 if text.is_empty() {
377 None
378 } else {
379 Some(text)
380 }
381 }
382 _ => None,
383 };
384 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
385 let style = elem
386 .resolved_style
387 .as_ref()
388 .map(ElementStyleInfo::from_resolved)
389 .unwrap_or_default();
390 ElementInfo {
391 x: elem.x,
392 y: elem.y,
393 width: elem.width,
394 height: elem.height,
395 kind: kind.to_string(),
396 node_type,
397 style,
398 children: Self::build_element_tree(&elem.children),
399 source_location: elem.source_location.clone(),
400 text_content,
401 href: elem.href.clone(),
402 bookmark: elem.bookmark.clone(),
403 }
404 })
405 .collect()
406 }
407}
408
409#[derive(Debug, Clone)]
411pub struct LayoutPage {
412 pub width: f64,
413 pub height: f64,
414 pub elements: Vec<LayoutElement>,
415 pub(crate) fixed_header: Vec<(Node, f64)>,
417 pub(crate) fixed_footer: Vec<(Node, f64)>,
419 pub(crate) watermarks: Vec<Node>,
421 pub(crate) config: PageConfig,
423 pub(crate) page_name: Option<String>,
425}
426
427#[derive(Debug, Clone)]
429pub struct LayoutElement {
430 pub x: f64,
432 pub y: f64,
433 pub width: f64,
435 pub height: f64,
436 pub draw: DrawCommand,
438 pub children: Vec<LayoutElement>,
440 pub node_type: Option<String>,
442 pub resolved_style: Option<ResolvedStyle>,
444 pub source_location: Option<SourceLocation>,
446 pub href: Option<String>,
448 pub bookmark: Option<String>,
450 pub alt: Option<String>,
452 pub is_header_row: bool,
454 pub list_numbering: Option<&'static str>,
459 pub actual_text: Option<String>,
464 pub col_span: u32,
467 pub overflow: Overflow,
469 pub opacity: f64,
474}
475
476fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
479 match &node.kind {
480 NodeKind::Fixed {
481 pages,
482 page_name: only,
483 exclude_page_names,
484 ..
485 } => {
486 pages.applies(page_index)
487 && match only {
488 Some(n) => page_name == Some(n.as_str()),
489 None => true,
490 }
491 && !exclude_page_names
492 .iter()
493 .any(|n| page_name == Some(n.as_str()))
494 }
495 _ => true,
496 }
497}
498
499fn node_kind_name(kind: &NodeKind) -> &'static str {
501 match kind {
502 NodeKind::View => "View",
503 NodeKind::Text { .. } => "Text",
504 NodeKind::Heading { level: 1, .. } => "H1",
505 NodeKind::Heading { level: 2, .. } => "H2",
506 NodeKind::Heading { level: 3, .. } => "H3",
507 NodeKind::Heading { level: 4, .. } => "H4",
508 NodeKind::Heading { level: 5, .. } => "H5",
509 NodeKind::Heading { .. } => "H6",
513 NodeKind::List { .. } => "List",
514 NodeKind::ListItem => "ListItem",
515 NodeKind::Image { .. } => "Image",
516 NodeKind::Table { .. } => "Table",
517 NodeKind::TableRow { .. } => "TableRow",
518 NodeKind::TableCell { .. } => "TableCell",
519 NodeKind::Fixed {
520 position: FixedPosition::Header,
521 ..
522 } => "FixedHeader",
523 NodeKind::Fixed {
524 position: FixedPosition::Footer,
525 ..
526 } => "FixedFooter",
527 NodeKind::Page { .. } => "Page",
528 NodeKind::PageBreak => "PageBreak",
529 NodeKind::PageName { .. } => "PageName",
530 NodeKind::Svg { .. } => "Svg",
531 NodeKind::Canvas { .. } => "Canvas",
532 NodeKind::Barcode { .. } => "Barcode",
533 NodeKind::QrCode { .. } => "QrCode",
534 NodeKind::BarChart { .. } => "BarChart",
535 NodeKind::LineChart { .. } => "LineChart",
536 NodeKind::PieChart { .. } => "PieChart",
537 NodeKind::AreaChart { .. } => "AreaChart",
538 NodeKind::DotPlot { .. } => "DotPlot",
539 NodeKind::Watermark { .. } => "Watermark",
540 NodeKind::TextField { .. } => "TextField",
541 NodeKind::Checkbox { .. } => "Checkbox",
542 NodeKind::Dropdown { .. } => "Dropdown",
543 NodeKind::RadioButton { .. } => "RadioButton",
544 }
545}
546
547fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
566 node.bookmark.as_ref().map(|title| LayoutElement {
567 x,
568 y,
569 width: 0.0,
570 height: 0.0,
571 draw: DrawCommand::None,
572 children: vec![],
573 node_type: Some("Bookmark".to_string()),
574 resolved_style: None,
575 source_location: None,
576 href: None,
577 bookmark: Some(title.clone()),
578 alt: None,
579 is_header_row: false,
580 actual_text: None,
581 list_numbering: None,
582 col_span: 1,
583 overflow: Overflow::default(),
584 opacity: 1.0,
585 })
586}
587
588fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
595 if !ordered {
596 return match marker_type {
600 ListMarkerType::None => String::new(),
601 _ => "•".to_string(),
602 };
603 }
604 let body = match marker_type {
605 ListMarkerType::LowerAlpha => to_alpha(idx, false),
606 ListMarkerType::UpperAlpha => to_alpha(idx, true),
607 ListMarkerType::LowerRoman => to_roman(idx, false),
608 ListMarkerType::UpperRoman => to_roman(idx, true),
609 ListMarkerType::None => return String::new(),
610 _ => idx.to_string(),
613 };
614 format!("{body}.")
615}
616
617fn to_alpha(mut n: u32, upper: bool) -> String {
620 if n == 0 {
621 return String::new();
622 }
623 let base = if upper { b'A' } else { b'a' };
624 let mut bytes: Vec<u8> = Vec::new();
625 while n > 0 {
626 let rem = ((n - 1) % 26) as u8;
627 bytes.push(base + rem);
628 n = (n - 1) / 26;
629 }
630 bytes.reverse();
631 String::from_utf8(bytes).unwrap_or_default()
632}
633
634fn to_roman(n: u32, upper: bool) -> String {
638 if n == 0 || n > 3999 {
639 return n.to_string();
640 }
641 const UPPER: &[(&str, u32)] = &[
642 ("M", 1000),
643 ("CM", 900),
644 ("D", 500),
645 ("CD", 400),
646 ("C", 100),
647 ("XC", 90),
648 ("L", 50),
649 ("XL", 40),
650 ("X", 10),
651 ("IX", 9),
652 ("V", 5),
653 ("IV", 4),
654 ("I", 1),
655 ];
656 const LOWER: &[(&str, u32)] = &[
657 ("m", 1000),
658 ("cm", 900),
659 ("d", 500),
660 ("cd", 400),
661 ("c", 100),
662 ("xc", 90),
663 ("l", 50),
664 ("xl", 40),
665 ("x", 10),
666 ("ix", 9),
667 ("v", 5),
668 ("iv", 4),
669 ("i", 1),
670 ];
671 let table = if upper { UPPER } else { LOWER };
672 let mut out = String::new();
673 let mut remaining = n;
674 for &(sym, val) in table {
675 while remaining >= val {
676 out.push_str(sym);
677 remaining -= val;
678 }
679 }
680 out
681}
682
683fn compute_marker_gutter_width(
690 ordered: bool,
691 marker_type: ListMarkerType,
692 start: u32,
693 n_items: u32,
694 style: &ResolvedStyle,
695) -> f64 {
696 if matches!(marker_type, ListMarkerType::None) {
697 return 0.0;
698 }
699 let approx_char_w = style.font_size * 0.6;
701 let gap = 6.0_f64;
702 if !ordered {
703 return approx_char_w + gap;
705 }
706 let last_idx = start + n_items.saturating_sub(1);
708 let widest = match marker_type {
709 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
710 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
711 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
712 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
713 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
714 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
715 };
716 widest.chars().count() as f64 * approx_char_w + gap
717}
718
719#[derive(Debug, Clone)]
721pub enum FormFieldType {
722 TextField {
723 value: Option<String>,
724 placeholder: Option<String>,
725 multiline: bool,
726 password: bool,
727 read_only: bool,
728 max_length: Option<u32>,
729 font_size: f64,
730 },
731 Checkbox {
732 checked: bool,
733 read_only: bool,
734 },
735 Dropdown {
736 options: Vec<String>,
737 value: Option<String>,
738 read_only: bool,
739 font_size: f64,
740 },
741 RadioButton {
742 value: String,
743 checked: bool,
744 read_only: bool,
745 },
746}
747
748#[derive(Debug, Clone)]
750pub enum DrawCommand {
751 None,
753 Rect {
755 background: Option<Color>,
756 border_width: Edges,
757 border_color: EdgeValues<Color>,
758 border_style: EdgeValues<crate::style::BorderStyle>,
759 border_radius: CornerValues,
760 opacity: f64,
761 box_shadow: Option<Box<crate::style::BoxShadow>>,
764 background_gradient: Option<Box<crate::style::Background>>,
768 },
769 Text {
771 lines: Vec<TextLine>,
772 color: Color,
773 text_decoration: TextDecoration,
774 opacity: f64,
775 },
776 Image {
778 image_data: crate::image_loader::LoadedImage,
779 },
780 ImagePlaceholder,
782 Svg {
784 commands: Vec<crate::svg::SvgCommand>,
785 width: f64,
787 height: f64,
789 viewbox_min_x: f64,
793 viewbox_min_y: f64,
794 viewbox_width: f64,
795 viewbox_height: f64,
796 clip: bool,
798 },
799 Barcode {
801 bars: Vec<u8>,
802 bar_width: f64,
803 height: f64,
804 color: Color,
805 },
806 QrCode {
808 modules: Vec<Vec<bool>>,
809 module_size: f64,
810 color: Color,
811 },
812 Chart {
814 primitives: Vec<crate::chart::ChartPrimitive>,
815 },
816 Watermark {
818 lines: Vec<TextLine>,
819 color: Color,
820 opacity: f64,
821 angle_rad: f64,
822 font_family: String,
824 },
825 FormField {
827 field_type: FormFieldType,
828 name: String,
829 },
830}
831
832#[derive(Debug, Clone)]
833pub struct TextLine {
834 pub x: f64,
835 pub y: f64,
836 pub glyphs: Vec<PositionedGlyph>,
837 pub width: f64,
838 pub height: f64,
839 pub word_spacing: f64,
841}
842
843#[derive(Debug, Clone)]
844pub struct PositionedGlyph {
845 pub glyph_id: u16,
848 pub x_offset: f64,
850 pub y_offset: f64,
852 pub x_advance: f64,
854 pub font_size: f64,
855 pub font_family: Arc<str>,
861 pub font_weight: u32,
862 pub font_style: FontStyle,
863 pub char_value: char,
865 pub color: Option<Color>,
867 pub href: Option<String>,
869 pub text_decoration: TextDecoration,
871 pub letter_spacing: f64,
873 pub cluster_text: Option<String>,
876}
877
878fn offset_element_y(el: &mut LayoutElement, dy: f64) {
881 el.y += dy;
882 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
883 for line in lines.iter_mut() {
884 line.y += dy;
885 }
886 }
887 for child in &mut el.children {
888 offset_element_y(child, dy);
889 }
890}
891
892fn offset_element_x(el: &mut LayoutElement, dx: f64) {
894 el.x += dx;
895 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
896 for line in lines.iter_mut() {
897 line.x += dx;
898 }
899 }
900 for child in &mut el.children {
901 offset_element_x(child, dx);
902 }
903}
904
905fn reapply_justify_content(elem: &mut LayoutElement) {
909 let style = match elem.resolved_style {
910 Some(ref s) => s,
911 None => return,
912 };
913 if matches!(style.justify_content, JustifyContent::FlexStart) {
914 return;
915 }
916 if elem.children.is_empty() {
917 return;
918 }
919
920 let padding_top = style.padding.top + style.border_width.top;
921 let padding_bottom = style.padding.bottom + style.border_width.bottom;
922 let inner_h = elem.height - padding_top - padding_bottom;
923 let content_top = elem.y + padding_top;
924
925 let last_child = &elem.children[elem.children.len() - 1];
927 let children_bottom = last_child.y + last_child.height;
928 let children_span = children_bottom - content_top;
929 let slack = inner_h - children_span;
930 if slack < 0.001 {
931 return;
932 }
933
934 let n = elem.children.len();
935 let offsets: Vec<f64> = match style.justify_content {
936 JustifyContent::FlexEnd => vec![slack; n],
937 JustifyContent::Center => vec![slack / 2.0; n],
938 JustifyContent::SpaceBetween => {
939 if n <= 1 {
940 vec![0.0; n]
941 } else {
942 let per_gap = slack / (n - 1) as f64;
943 (0..n).map(|i| i as f64 * per_gap).collect()
944 }
945 }
946 JustifyContent::SpaceAround => {
947 let space = slack / n as f64;
948 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
949 }
950 JustifyContent::SpaceEvenly => {
951 let space = slack / (n + 1) as f64;
952 (0..n).map(|i| (i + 1) as f64 * space).collect()
953 }
954 JustifyContent::FlexStart => unreachable!(),
955 };
956
957 for (i, child) in elem.children.iter_mut().enumerate() {
958 let dy = offsets[i];
959 if dy.abs() > 0.001 {
960 offset_element_y(child, dy);
961 }
962 }
963}
964
965fn apply_text_transform(text: &str, transform: TextTransform) -> String {
967 match transform {
968 TextTransform::None => text.to_string(),
969 TextTransform::Uppercase => text.to_uppercase(),
970 TextTransform::Lowercase => text.to_lowercase(),
971 TextTransform::Capitalize => {
972 let mut result = String::with_capacity(text.len());
973 let mut prev_is_whitespace = true;
974 for ch in text.chars() {
975 if prev_is_whitespace && ch.is_alphabetic() {
976 for upper in ch.to_uppercase() {
977 result.push(upper);
978 }
979 } else {
980 result.push(ch);
981 }
982 prev_is_whitespace = ch.is_whitespace();
983 }
984 result
985 }
986 }
987}
988
989pub const PAGE_NUMBER_SENTINEL: char = '\x02';
993
994pub const TOTAL_PAGES_SENTINEL: char = '\x03';
996
997fn substitute_page_placeholders(text: &str) -> String {
1002 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
1003 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
1004 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
1005 } else {
1006 text.to_string()
1007 }
1008}
1009
1010fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1013 match transform {
1014 TextTransform::None => ch,
1015 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1016 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1017 TextTransform::Capitalize => {
1018 if is_word_start && ch.is_alphabetic() {
1019 ch.to_uppercase().next().unwrap_or(ch)
1020 } else {
1021 ch
1022 }
1023 }
1024 }
1025}
1026
1027pub struct LayoutEngine {
1029 text_layout: TextLayout,
1030 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1031 warnings: RefCell<Vec<String>>,
1043 measure_check: bool,
1057}
1058
1059#[derive(Debug, Clone)]
1061struct PageCursor {
1062 config: PageConfig,
1063 base_config: PageConfig,
1066 page_index: usize,
1071 content_width: f64,
1072 content_height: f64,
1073 y: f64,
1074 elements: Vec<LayoutElement>,
1075 fixed_header: Vec<(Node, f64)>,
1076 fixed_footer: Vec<(Node, f64)>,
1077 watermarks: Vec<Node>,
1079 content_x: f64,
1080 content_y: f64,
1081 continuation_top_offset: f64,
1083 containing_block: (f64, f64, f64, f64),
1088 left_config: Option<PageConfig>,
1096 right_config: Option<PageConfig>,
1097 display_config: Option<PageConfig>,
1100 flow_anchor_left: f64,
1105 page_name: Option<String>,
1109 base_display: Option<PageConfig>,
1113 first_display: Option<PageConfig>,
1117 doc_base: PageConfig,
1120 doc_left: Option<PageConfig>,
1121 doc_right: Option<PageConfig>,
1122 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1124}
1125
1126impl PageCursor {
1127 fn new(config: &PageConfig) -> Self {
1128 let (page_w, page_h) = config.size.dimensions();
1129 let content_width = page_w - config.margin.horizontal();
1130 let content_height = page_h - config.margin.vertical();
1131
1132 Self {
1133 config: config.clone(),
1134 base_config: config.clone(),
1135 page_index: 0,
1136 content_width,
1137 content_height,
1138 y: 0.0,
1139 elements: Vec::new(),
1140 fixed_header: Vec::new(),
1141 fixed_footer: Vec::new(),
1142 watermarks: Vec::new(),
1143 content_x: config.margin.left,
1144 content_y: config.margin.top,
1145 continuation_top_offset: 0.0,
1146 containing_block: (
1147 config.margin.left,
1148 config.margin.top,
1149 content_width,
1150 content_height,
1151 ),
1152 left_config: None,
1153 right_config: None,
1154 display_config: None,
1155 flow_anchor_left: config.margin.left,
1156 page_name: None,
1157 base_display: None,
1158 first_display: None,
1159 doc_base: config.clone(),
1160 doc_left: None,
1161 doc_right: None,
1162 named_sets: std::collections::HashMap::new(),
1163 }
1164 }
1165
1166 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1173 if page_number == 1 {
1174 if let Some(first) = &self.first_display {
1175 return Some(first.clone());
1176 }
1177 }
1178 let parity = if page_number % 2 == 1 {
1179 self.right_config.clone()
1180 } else {
1181 self.left_config.clone()
1182 };
1183 parity.or_else(|| self.base_display.clone())
1184 }
1185
1186 fn fixed_applies(&self, node: &Node) -> bool {
1189 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1190 }
1191
1192 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1199 let set = name
1200 .as_deref()
1201 .and_then(|n| self.named_sets.get(n))
1202 .cloned();
1203 let real = set
1204 .as_ref()
1205 .map(|s| s.base.clone())
1206 .unwrap_or_else(|| self.doc_base.clone());
1207 let mut cursor = PageCursor::new(&real);
1208 cursor.page_index = if in_place {
1209 self.page_index
1210 } else {
1211 self.page_index + 1
1212 };
1213 cursor.page_name = name;
1214 cursor.doc_base = self.doc_base.clone();
1215 cursor.doc_left = self.doc_left.clone();
1216 cursor.doc_right = self.doc_right.clone();
1217 cursor.named_sets = self.named_sets.clone();
1218 match &set {
1219 Some(s) => {
1220 cursor.left_config = s.display_left.clone();
1221 cursor.right_config = s.display_right.clone();
1222 cursor.base_display = s.display.clone();
1223 cursor.first_display = s.display_first.clone();
1224 }
1225 None => {
1226 cursor.left_config = self.doc_left.clone();
1230 cursor.right_config = self.doc_right.clone();
1231 }
1232 }
1233 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1234 cursor.flow_anchor_left = self.flow_anchor_left;
1235 cursor.fixed_header = self.fixed_header.clone();
1236 cursor.fixed_footer = self.fixed_footer.clone();
1237 cursor.watermarks = self.watermarks.clone();
1238 cursor.continuation_top_offset = self.continuation_top_offset;
1239
1240 let header_height: f64 = cursor
1241 .fixed_header
1242 .iter()
1243 .filter(|(n, _)| cursor.fixed_applies(n))
1244 .map(|(_, h)| *h)
1245 .sum();
1246 cursor.y = header_height + cursor.continuation_top_offset;
1247 cursor
1248 }
1249
1250 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1253 let mut cursor = PageCursor::new(first);
1254 cursor.base_config = base.clone();
1255 cursor
1256 }
1257
1258 fn remaining_height(&self) -> f64 {
1259 let footer_height: f64 = self
1260 .fixed_footer
1261 .iter()
1262 .filter(|(n, _)| self.fixed_applies(n))
1263 .map(|(_, h)| *h)
1264 .sum();
1265 (self.content_height - self.y - footer_height).max(0.0)
1266 }
1267
1268 fn finalize(&self) -> LayoutPage {
1269 let (page_w, page_h) = self.config.size.dimensions();
1270
1271 let (elements, config) = match &self.display_config {
1277 Some(display) => {
1278 let dx = display.margin.left - self.flow_anchor_left;
1279 let mut elements = self.elements.clone();
1280 if dx != 0.0 {
1281 fn shift(els: &mut [LayoutElement], dx: f64) {
1282 for el in els {
1283 el.x += dx;
1284 shift(&mut el.children, dx);
1285 }
1286 }
1287 shift(&mut elements, dx);
1288 }
1289 (elements, display.clone())
1290 }
1291 None => (self.elements.clone(), self.config.clone()),
1292 };
1293
1294 LayoutPage {
1295 width: page_w,
1296 height: page_h,
1297 elements,
1298 fixed_header: self.fixed_header.clone(),
1299 fixed_footer: self.fixed_footer.clone(),
1300 watermarks: self.watermarks.clone(),
1301 config,
1302 page_name: self.page_name.clone(),
1303 }
1304 }
1305
1306 fn new_page(&self) -> Self {
1307 let mut cursor = PageCursor::new(&self.base_config);
1310 cursor.page_index = self.page_index + 1;
1311 cursor.page_name = self.page_name.clone();
1312 cursor.left_config = self.left_config.clone();
1313 cursor.right_config = self.right_config.clone();
1314 cursor.base_display = self.base_display.clone();
1315 cursor.first_display = self.first_display.clone();
1316 cursor.doc_base = self.doc_base.clone();
1317 cursor.doc_left = self.doc_left.clone();
1318 cursor.doc_right = self.doc_right.clone();
1319 cursor.named_sets = self.named_sets.clone();
1320 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1321 cursor.flow_anchor_left = self.flow_anchor_left;
1322 cursor.fixed_header = self.fixed_header.clone();
1323 cursor.fixed_footer = self.fixed_footer.clone();
1324 cursor.watermarks = self.watermarks.clone();
1325 cursor.continuation_top_offset = self.continuation_top_offset;
1326
1327 let header_height: f64 = cursor
1328 .fixed_header
1329 .iter()
1330 .filter(|(n, _)| cursor.fixed_applies(n))
1331 .map(|(_, h)| *h)
1332 .sum();
1333 cursor.y = header_height + cursor.continuation_top_offset;
1334
1335 cursor
1336 }
1337}
1338
1339impl Default for LayoutEngine {
1340 fn default() -> Self {
1341 Self::new()
1342 }
1343}
1344
1345impl LayoutEngine {
1346 pub fn new() -> Self {
1347 Self {
1348 text_layout: TextLayout::new(),
1349 image_dim_cache: RefCell::new(HashMap::new()),
1350 warnings: RefCell::new(Vec::new()),
1351 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1352 }
1353 }
1354
1355 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1357 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1358 return Some(*dims);
1359 }
1360 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1361 self.image_dim_cache
1362 .borrow_mut()
1363 .insert(src.to_string(), dims);
1364 Some(dims)
1365 } else {
1366 None
1367 }
1368 }
1369
1370 fn defect(&self, msg: String) {
1373 let mut w = self.warnings.borrow_mut();
1374 if !w.contains(&msg) {
1377 w.push(msg);
1378 }
1379 }
1380
1381 pub fn take_warnings(&self) -> Vec<String> {
1385 std::mem::take(&mut *self.warnings.borrow_mut())
1386 }
1387
1388 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1390 self.warnings.borrow_mut().clear();
1391 let mut pages: Vec<LayoutPage> = Vec::new();
1392 let mut cursor = match &document.first_page {
1393 Some(first) => PageCursor::new_first(first, &document.default_page),
1394 None => PageCursor::new(&document.default_page),
1395 };
1396 cursor.left_config = document.left_page.clone();
1402 cursor.right_config = document.right_page.clone();
1403 cursor.doc_base = document.default_page.clone();
1405 cursor.doc_left = document.left_page.clone();
1406 cursor.doc_right = document.right_page.clone();
1407 cursor.named_sets = document.named_pages.clone();
1408 if document.first_page.is_none() {
1409 cursor.display_config = cursor.display_for(1);
1410 }
1411
1412 let base = document.default_style.clone().unwrap_or_default();
1414 let root_style = Style {
1415 lang: base.lang.clone().or(document.metadata.lang.clone()),
1416 ..base
1417 }
1418 .resolve(None, cursor.content_width);
1419
1420 for node in &document.children {
1421 match &node.kind {
1422 NodeKind::Page { config } => {
1423 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1424 pages.push(cursor.finalize());
1425 }
1426 cursor = PageCursor::new(config);
1427 cursor.page_index = pages.len();
1428
1429 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1436 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1437
1438 let cx = cursor.content_x;
1439 let cw = cursor.content_width;
1440 self.layout_children(
1441 &node.children,
1442 &node.style,
1443 &mut cursor,
1444 &mut pages,
1445 cx,
1446 cw,
1447 Some(&page_root),
1448 font_context,
1449 );
1450 }
1451 NodeKind::PageBreak => {
1452 pages.push(cursor.finalize());
1453 cursor = cursor.new_page();
1454 }
1455 _ => {
1456 let cx = cursor.content_x;
1457 let cw = cursor.content_width;
1458 self.layout_node(
1459 node,
1460 &mut cursor,
1461 &mut pages,
1462 cx,
1463 cw,
1464 Some(&root_style),
1465 font_context,
1466 None,
1467 None,
1468 );
1469 }
1470 }
1471 }
1472
1473 if !cursor.elements.is_empty() {
1479 pages.push(cursor.finalize());
1480 }
1481
1482 fn has_visible_ink(els: &[LayoutElement]) -> bool {
1489 els.iter().any(|e| {
1490 let paints = match &e.draw {
1491 DrawCommand::None => false,
1492 DrawCommand::Rect {
1496 background,
1497 border_width,
1498 ..
1499 } => {
1500 background.is_some()
1501 || border_width.top > 0.0
1502 || border_width.right > 0.0
1503 || border_width.bottom > 0.0
1504 || border_width.left > 0.0
1505 }
1506 _ => true,
1507 };
1508 paints || has_visible_ink(&e.children)
1509 })
1510 }
1511 while pages.len() > 1 && !has_visible_ink(&pages.last().unwrap().elements) {
1512 pages.pop();
1513 }
1514
1515 self.inject_fixed_elements(&mut pages, font_context);
1516
1517 pages
1518 }
1519
1520 #[allow(clippy::too_many_arguments)]
1521 fn layout_node(
1522 &self,
1523 node: &Node,
1524 cursor: &mut PageCursor,
1525 pages: &mut Vec<LayoutPage>,
1526 x: f64,
1527 available_width: f64,
1528 parent_style: Option<&ResolvedStyle>,
1529 font_context: &FontContext,
1530 cross_axis_height: Option<f64>,
1531 forced_outer_width: Option<f64>,
1532 ) {
1533 let mut style = node.style.resolve(parent_style, available_width);
1534
1535 if let Some(h) = cross_axis_height {
1538 if matches!(style.height, SizeConstraint::Auto) {
1539 style.height = SizeConstraint::Fixed(h);
1540 }
1541 }
1542
1543 if let Some(w) = forced_outer_width {
1548 style.width = SizeConstraint::Fixed(w);
1549 }
1550
1551 if style.break_before && !cursor.elements.is_empty() {
1562 pages.push(cursor.finalize());
1563 *cursor = cursor.new_page();
1564 }
1565
1566 let elem_start = cursor.elements.len();
1569
1570 match &node.kind {
1571 NodeKind::PageBreak => {
1572 pages.push(cursor.finalize());
1573 *cursor = cursor.new_page();
1574 }
1575
1576 NodeKind::PageName { name } => {
1577 if cursor.page_name.as_deref() != name.as_deref() {
1583 let has_content = !cursor.elements.is_empty();
1590 if has_content {
1591 pages.push(cursor.finalize());
1592 }
1593 *cursor = cursor.renamed_page(name.clone(), !has_content);
1594 }
1595 }
1596
1597 NodeKind::Fixed {
1598 position, pages, ..
1599 } => {
1600 let height = self.measure_node_height(node, available_width, &style, font_context);
1601 match position {
1602 FixedPosition::Header => {
1603 cursor.fixed_header.push((node.clone(), height));
1604 let _ = pages;
1608 if cursor.fixed_applies(node) {
1609 cursor.y += height;
1610 }
1611 }
1612 FixedPosition::Footer => {
1613 cursor.fixed_footer.push((node.clone(), height));
1614 }
1615 }
1616 }
1617
1618 NodeKind::Watermark { .. } => {
1619 cursor.watermarks.push(node.clone());
1621 }
1622
1623 NodeKind::TextField {
1624 name,
1625 value,
1626 placeholder,
1627 width: field_w,
1628 height: field_h,
1629 multiline,
1630 password,
1631 read_only,
1632 max_length,
1633 font_size,
1634 } => {
1635 self.layout_form_field(
1636 node,
1637 &style,
1638 cursor,
1639 pages,
1640 x,
1641 *field_w,
1642 *field_h,
1643 DrawCommand::FormField {
1644 field_type: FormFieldType::TextField {
1645 value: value.clone(),
1646 placeholder: placeholder.clone(),
1647 multiline: *multiline,
1648 password: *password,
1649 read_only: *read_only,
1650 max_length: *max_length,
1651 font_size: *font_size,
1652 },
1653 name: name.clone(),
1654 },
1655 "TextField",
1656 );
1657 }
1658
1659 NodeKind::Checkbox {
1660 name,
1661 checked,
1662 width: field_w,
1663 height: field_h,
1664 read_only,
1665 } => {
1666 self.layout_form_field(
1667 node,
1668 &style,
1669 cursor,
1670 pages,
1671 x,
1672 *field_w,
1673 *field_h,
1674 DrawCommand::FormField {
1675 field_type: FormFieldType::Checkbox {
1676 checked: *checked,
1677 read_only: *read_only,
1678 },
1679 name: name.clone(),
1680 },
1681 "Checkbox",
1682 );
1683 }
1684
1685 NodeKind::Dropdown {
1686 name,
1687 options,
1688 value,
1689 width: field_w,
1690 height: field_h,
1691 read_only,
1692 font_size,
1693 } => {
1694 self.layout_form_field(
1695 node,
1696 &style,
1697 cursor,
1698 pages,
1699 x,
1700 *field_w,
1701 *field_h,
1702 DrawCommand::FormField {
1703 field_type: FormFieldType::Dropdown {
1704 options: options.clone(),
1705 value: value.clone(),
1706 read_only: *read_only,
1707 font_size: *font_size,
1708 },
1709 name: name.clone(),
1710 },
1711 "Dropdown",
1712 );
1713 }
1714
1715 NodeKind::RadioButton {
1716 name,
1717 value,
1718 checked,
1719 width: field_w,
1720 height: field_h,
1721 read_only,
1722 } => {
1723 self.layout_form_field(
1724 node,
1725 &style,
1726 cursor,
1727 pages,
1728 x,
1729 *field_w,
1730 *field_h,
1731 DrawCommand::FormField {
1732 field_type: FormFieldType::RadioButton {
1733 value: value.clone(),
1734 checked: *checked,
1735 read_only: *read_only,
1736 },
1737 name: name.clone(),
1738 },
1739 "RadioButton",
1740 );
1741 }
1742
1743 NodeKind::Text {
1744 content,
1745 href,
1746 runs,
1747 } => {
1748 self.layout_text(
1749 content,
1750 href.as_deref(),
1751 runs,
1752 &style,
1753 cursor,
1754 pages,
1755 x,
1756 available_width,
1757 font_context,
1758 node.source_location.as_ref(),
1759 node.bookmark.as_deref(),
1760 None,
1761 );
1762 }
1763
1764 NodeKind::Heading {
1765 content,
1766 href,
1767 runs,
1768 ..
1769 } => {
1770 let heading_role = node_kind_name(&node.kind); self.layout_text(
1776 content,
1777 href.as_deref(),
1778 runs,
1779 &style,
1780 cursor,
1781 pages,
1782 x,
1783 available_width,
1784 font_context,
1785 node.source_location.as_ref(),
1786 node.bookmark.as_deref(),
1787 Some(heading_role),
1788 );
1789 }
1790
1791 NodeKind::List {
1792 ordered,
1793 marker_type,
1794 start,
1795 } => {
1796 self.layout_list(
1797 node,
1798 *ordered,
1799 *marker_type,
1800 *start,
1801 &style,
1802 cursor,
1803 pages,
1804 x,
1805 available_width,
1806 font_context,
1807 );
1808 }
1809
1810 NodeKind::ListItem => {
1811 self.layout_view(
1816 node,
1817 &style,
1818 cursor,
1819 pages,
1820 x,
1821 available_width,
1822 font_context,
1823 );
1824 }
1825
1826 NodeKind::Image { width, height, .. } => {
1827 self.layout_image(
1828 node,
1829 &style,
1830 cursor,
1831 pages,
1832 x,
1833 available_width,
1834 *width,
1835 *height,
1836 );
1837 }
1838
1839 NodeKind::Table { columns } => {
1840 self.layout_table(
1841 node,
1842 &style,
1843 columns,
1844 cursor,
1845 pages,
1846 x,
1847 available_width,
1848 font_context,
1849 );
1850 }
1851
1852 NodeKind::View | NodeKind::Page { .. } => {
1853 self.layout_view(
1854 node,
1855 &style,
1856 cursor,
1857 pages,
1858 x,
1859 available_width,
1860 font_context,
1861 );
1862 }
1863
1864 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1865 self.layout_view(
1866 node,
1867 &style,
1868 cursor,
1869 pages,
1870 x,
1871 available_width,
1872 font_context,
1873 );
1874 }
1875
1876 NodeKind::Svg {
1877 width: svg_w,
1878 height: svg_h,
1879 view_box,
1880 content,
1881 } => {
1882 self.layout_svg(
1883 node,
1884 &style,
1885 cursor,
1886 pages,
1887 x,
1888 available_width,
1889 *svg_w,
1890 *svg_h,
1891 view_box.as_deref(),
1892 content,
1893 );
1894 }
1895
1896 NodeKind::Barcode {
1897 data,
1898 format,
1899 width: explicit_width,
1900 height: bar_height,
1901 } => {
1902 self.layout_barcode(
1903 node,
1904 &style,
1905 cursor,
1906 pages,
1907 x,
1908 available_width,
1909 data,
1910 *format,
1911 *explicit_width,
1912 *bar_height,
1913 );
1914 }
1915
1916 NodeKind::QrCode {
1917 data,
1918 size: explicit_size,
1919 } => {
1920 self.layout_qrcode(
1921 node,
1922 &style,
1923 cursor,
1924 pages,
1925 x,
1926 available_width,
1927 data,
1928 *explicit_size,
1929 );
1930 }
1931
1932 NodeKind::Canvas {
1933 width: canvas_w,
1934 height: canvas_h,
1935 operations,
1936 } => {
1937 self.layout_canvas(
1938 node,
1939 &style,
1940 cursor,
1941 pages,
1942 x,
1943 available_width,
1944 *canvas_w,
1945 *canvas_h,
1946 operations,
1947 );
1948 }
1949
1950 NodeKind::BarChart {
1951 data,
1952 width: chart_w,
1953 height: chart_h,
1954 color,
1955 show_labels,
1956 show_values,
1957 show_grid,
1958 title,
1959 } => {
1960 let config = crate::chart::bar::BarChartConfig {
1961 color: color.clone(),
1962 show_labels: *show_labels,
1963 show_values: *show_values,
1964 show_grid: *show_grid,
1965 title: title.clone(),
1966 };
1967 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1968 self.layout_chart(
1969 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1970 );
1971 }
1972
1973 NodeKind::LineChart {
1974 series,
1975 labels,
1976 width: chart_w,
1977 height: chart_h,
1978 show_points,
1979 show_grid,
1980 title,
1981 } => {
1982 let config = crate::chart::line::LineChartConfig {
1983 show_points: *show_points,
1984 show_grid: *show_grid,
1985 title: title.clone(),
1986 };
1987 let primitives =
1988 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1989 self.layout_chart(
1990 node,
1991 &style,
1992 cursor,
1993 pages,
1994 x,
1995 *chart_w,
1996 *chart_h,
1997 primitives,
1998 "LineChart",
1999 );
2000 }
2001
2002 NodeKind::PieChart {
2003 data,
2004 width: chart_w,
2005 height: chart_h,
2006 donut,
2007 show_legend,
2008 title,
2009 } => {
2010 let config = crate::chart::pie::PieChartConfig {
2011 donut: *donut,
2012 show_legend: *show_legend,
2013 title: title.clone(),
2014 };
2015 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
2016 self.layout_chart(
2017 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
2018 );
2019 }
2020
2021 NodeKind::AreaChart {
2022 series,
2023 labels,
2024 width: chart_w,
2025 height: chart_h,
2026 show_grid,
2027 title,
2028 } => {
2029 let config = crate::chart::area::AreaChartConfig {
2030 show_grid: *show_grid,
2031 title: title.clone(),
2032 };
2033 let primitives =
2034 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
2035 self.layout_chart(
2036 node,
2037 &style,
2038 cursor,
2039 pages,
2040 x,
2041 *chart_w,
2042 *chart_h,
2043 primitives,
2044 "AreaChart",
2045 );
2046 }
2047
2048 NodeKind::DotPlot {
2049 groups,
2050 width: chart_w,
2051 height: chart_h,
2052 x_min,
2053 x_max,
2054 y_min,
2055 y_max,
2056 x_label,
2057 y_label,
2058 show_legend,
2059 dot_size,
2060 } => {
2061 let config = crate::chart::dot::DotPlotConfig {
2062 x_min: *x_min,
2063 x_max: *x_max,
2064 y_min: *y_min,
2065 y_max: *y_max,
2066 x_label: x_label.clone(),
2067 y_label: y_label.clone(),
2068 show_legend: *show_legend,
2069 dot_size: *dot_size,
2070 };
2071 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2072 self.layout_chart(
2073 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2074 );
2075 }
2076 }
2077
2078 if matches!(style.position, Position::Relative)
2086 && (style.top.is_some()
2087 || style.left.is_some()
2088 || style.right.is_some()
2089 || style.bottom.is_some())
2090 {
2091 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2092 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2093 for el in &mut cursor.elements[elem_start..] {
2094 if dx != 0.0 {
2095 offset_element_x(el, dx);
2096 }
2097 if dy != 0.0 {
2098 offset_element_y(el, dy);
2099 }
2100 }
2101 }
2102 }
2103
2104 #[allow(clippy::too_many_arguments)]
2105 fn layout_view(
2106 &self,
2107 node: &Node,
2108 style: &ResolvedStyle,
2109 cursor: &mut PageCursor,
2110 pages: &mut Vec<LayoutPage>,
2111 x: f64,
2112 available_width: f64,
2113 font_context: &FontContext,
2114 ) {
2115 let padding = &style.padding;
2116 let margin = &style.margin.to_edges();
2117 let border = &style.border_width;
2118
2119 let outer_width = match style.width {
2120 SizeConstraint::Fixed(w) => w,
2121 SizeConstraint::Auto => available_width - margin.horizontal(),
2122 }
2123 .min(style.max_width)
2125 .max(style.min_width);
2126 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2127
2128 let children_height =
2129 self.measure_children_height(&node.children, inner_width, style, font_context);
2130 let total_height = match style.height {
2131 SizeConstraint::Fixed(h) => h,
2132 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2133 }
2134 .max(style.min_height);
2135
2136 let node_x = x + margin.left;
2137
2138 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2139
2140 if fits || !style.breakable {
2141 if !fits && !style.breakable {
2142 pages.push(cursor.finalize());
2143 *cursor = cursor.new_page();
2144 }
2145
2146 let rect_y = cursor.content_y + cursor.y + margin.top;
2148 let snapshot = cursor.elements.len();
2149
2150 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2155 cursor.elements.push(marker);
2156 }
2157
2158 let saved_y = cursor.y;
2159 cursor.y += margin.top + padding.top + border.top;
2160
2161 let pages_before = pages.len();
2162 let children_x = node_x + padding.left + border.left;
2163 let is_grid =
2164 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2165 if is_grid {
2166 self.layout_grid_children(
2167 &node.children,
2168 style,
2169 cursor,
2170 pages,
2171 children_x,
2172 inner_width,
2173 font_context,
2174 );
2175 } else {
2176 self.layout_children(
2177 &node.children,
2178 &node.style,
2179 cursor,
2180 pages,
2181 children_x,
2182 inner_width,
2183 Some(style),
2184 font_context,
2185 );
2186 }
2187
2188 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2190
2191 if self.measure_check
2198 && matches!(style.height, SizeConstraint::Auto)
2199 && pages.len() == pages_before
2200 && !child_elements.is_empty()
2201 {
2202 let content_top = rect_y + padding.top + border.top;
2203 let extent = child_elements
2204 .iter()
2205 .map(|el| el.y + el.height)
2206 .fold(f64::NEG_INFINITY, f64::max)
2207 - content_top;
2208 if extent > 0.0 && children_height - extent > 2.0 {
2209 self.defect(format!(
2210 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2211 node_kind_name(&node.kind),
2212 children_height - extent,
2213 ));
2214 }
2215 }
2216
2217 let rect_element = LayoutElement {
2218 x: node_x,
2219 y: rect_y,
2220 width: outer_width,
2221 height: total_height,
2222 draw: DrawCommand::Rect {
2223 background: style.background_color,
2224 border_width: style.border_width,
2225 border_color: style.border_color,
2226 border_style: style.border_style,
2227 border_radius: style.border_radius,
2228 opacity: 1.0,
2229 box_shadow: style.box_shadow.map(Box::new),
2230 background_gradient: style.background.clone().map(Box::new),
2231 },
2232 children: child_elements,
2233 node_type: Some(node_kind_name(&node.kind).to_string()),
2234 resolved_style: Some(style.clone()),
2235 source_location: node.source_location.clone(),
2236 href: node.href.clone(),
2237 bookmark: None,
2240 alt: None,
2241 is_header_row: false,
2242 actual_text: None,
2243 list_numbering: None,
2244 col_span: 1,
2245 overflow: style.overflow,
2246 opacity: style.opacity,
2247 };
2248 cursor.elements.push(rect_element);
2249
2250 cursor.y = saved_y + total_height + margin.vertical();
2251 } else {
2252 self.layout_breakable_view(
2253 node,
2254 style,
2255 cursor,
2256 pages,
2257 node_x,
2258 outer_width,
2259 inner_width,
2260 font_context,
2261 );
2262 }
2263 }
2264
2265 #[allow(clippy::too_many_arguments)]
2266 fn layout_breakable_view(
2267 &self,
2268 node: &Node,
2269 style: &ResolvedStyle,
2270 cursor: &mut PageCursor,
2271 pages: &mut Vec<LayoutPage>,
2272 node_x: f64,
2273 outer_width: f64,
2274 inner_width: f64,
2275 font_context: &FontContext,
2276 ) {
2277 let padding = &style.padding;
2278 let border = &style.border_width;
2279 let margin = &style.margin.to_edges();
2280
2281 let initial_page_count = pages.len();
2283 let snapshot = cursor.elements.len();
2284 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2285
2286 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2294 cursor.elements.push(marker);
2295 }
2296
2297 cursor.y += margin.top + padding.top + border.top;
2298 let prev_continuation_offset = cursor.continuation_top_offset;
2299 cursor.continuation_top_offset = padding.top + border.top;
2300
2301 let children_x = node_x + padding.left + border.left;
2302 let is_grid =
2303 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2304 if is_grid {
2305 self.layout_grid_children(
2306 &node.children,
2307 style,
2308 cursor,
2309 pages,
2310 children_x,
2311 inner_width,
2312 font_context,
2313 );
2314 } else {
2315 self.layout_children(
2316 &node.children,
2317 &node.style,
2318 cursor,
2319 pages,
2320 children_x,
2321 inner_width,
2322 Some(style),
2323 font_context,
2324 );
2325 }
2326
2327 cursor.continuation_top_offset = prev_continuation_offset;
2328
2329 let has_visual = style.background_color.is_some()
2331 || style.border_width.top > 0.0
2332 || style.border_width.right > 0.0
2333 || style.border_width.bottom > 0.0
2334 || style.border_width.left > 0.0;
2335 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2337
2338 if !needs_wrapper {
2339 cursor.y += padding.bottom + border.bottom + margin.bottom;
2341 return;
2342 }
2343
2344 let draw_cmd = DrawCommand::Rect {
2345 background: style.background_color,
2346 border_width: style.border_width,
2347 border_color: style.border_color,
2348 border_style: style.border_style,
2349 border_radius: style.border_radius,
2350 opacity: 1.0,
2351 box_shadow: style.box_shadow.map(Box::new),
2352 background_gradient: style.background.clone().map(Box::new),
2353 };
2354
2355 if pages.len() == initial_page_count {
2356 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2358 let rect_height =
2359 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2360 cursor.elements.push(LayoutElement {
2361 x: node_x,
2362 y: rect_start_y,
2363 width: outer_width,
2364 height: rect_height,
2365 draw: draw_cmd,
2366 children: child_elements,
2367 node_type: Some(node_kind_name(&node.kind).to_string()),
2368 resolved_style: Some(style.clone()),
2369 source_location: node.source_location.clone(),
2370 href: node.href.clone(),
2371 bookmark: None,
2376 alt: None,
2377 is_header_row: false,
2378 actual_text: None,
2379 list_numbering: None,
2380 col_span: 1,
2381 overflow: style.overflow,
2382 opacity: style.opacity,
2383 });
2384 } else {
2385 let page = &mut pages[initial_page_count];
2389 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2390 let page_content_bottom =
2391 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2392 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
2393 if !our_elements.is_empty() {
2394 let rect_height = page_content_bottom - rect_start_y;
2395 page.elements.push(LayoutElement {
2396 x: node_x,
2397 y: rect_start_y,
2398 width: outer_width,
2399 height: rect_height,
2400 draw: draw_cmd.clone(),
2401 children: our_elements,
2402 node_type: Some(node_kind_name(&node.kind).to_string()),
2403 resolved_style: Some(style.clone()),
2404 source_location: node.source_location.clone(),
2405 href: node.href.clone(),
2406 bookmark: None,
2408 alt: None,
2409 is_header_row: false,
2410 actual_text: None,
2411 list_numbering: None,
2412 col_span: 1,
2413 overflow: Overflow::default(),
2414 opacity: 1.0,
2415 });
2416 }
2417
2418 for page in &mut pages[initial_page_count + 1..] {
2420 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2421 let content_top = page.config.margin.top + header_h;
2422 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2423 let content_bottom = page.config.margin.top
2424 + (page.height - page.config.margin.vertical())
2425 - footer_h;
2426 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2427 if !all_elements.is_empty() {
2428 page.elements.push(LayoutElement {
2429 x: node_x,
2430 y: content_top,
2431 width: outer_width,
2432 height: content_bottom - content_top,
2433 draw: draw_cmd.clone(),
2434 children: all_elements,
2435 node_type: Some(node_kind_name(&node.kind).to_string()),
2436 resolved_style: Some(style.clone()),
2437 source_location: node.source_location.clone(),
2438 href: None,
2439 bookmark: None,
2440 alt: None,
2441 is_header_row: false,
2442 actual_text: None,
2443 list_numbering: None,
2444 col_span: 1,
2445 overflow: Overflow::default(),
2446 opacity: 1.0,
2447 });
2448 }
2449 }
2450
2451 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2453 if !all_elements.is_empty() {
2454 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2455 let content_top = cursor.content_y + header_h;
2456 let rect_height =
2457 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2458 cursor.elements.push(LayoutElement {
2459 x: node_x,
2460 y: content_top,
2461 width: outer_width,
2462 height: rect_height,
2463 draw: draw_cmd,
2464 children: all_elements,
2465 node_type: Some(node_kind_name(&node.kind).to_string()),
2466 resolved_style: Some(style.clone()),
2467 source_location: node.source_location.clone(),
2468 href: None,
2469 bookmark: None,
2470 alt: None,
2471 is_header_row: false,
2472 actual_text: None,
2473 list_numbering: None,
2474 col_span: 1,
2475 overflow: Overflow::default(),
2476 opacity: 1.0,
2477 });
2478 }
2479 }
2480
2481 cursor.y += padding.bottom + border.bottom + margin.bottom;
2482 }
2483
2484 #[allow(clippy::too_many_arguments)]
2485 fn layout_children(
2486 &self,
2487 children: &[Node],
2488 _parent_raw_style: &Style,
2489 cursor: &mut PageCursor,
2490 pages: &mut Vec<LayoutPage>,
2491 content_x: f64,
2492 available_width: f64,
2493 parent_style: Option<&ResolvedStyle>,
2494 font_context: &FontContext,
2495 ) {
2496 let parent_box_y = cursor.content_y + cursor.y;
2498 let parent_box_x = content_x;
2499 let entry_page_index = pages.len();
2503
2504 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2510 let saved_cb = cursor.containing_block;
2511 if parent_positioned {
2512 let cb_height = parent_style
2513 .and_then(|ps| match ps.height {
2514 SizeConstraint::Fixed(h) => {
2515 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2516 }
2517 SizeConstraint::Auto => None,
2518 })
2519 .unwrap_or(saved_cb.3);
2520 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2521 }
2522
2523 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2525 .iter()
2526 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2527
2528 let direction = parent_style
2529 .map(|s| s.flex_direction)
2530 .unwrap_or(FlexDirection::Column);
2531
2532 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2533 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2534
2535 match direction {
2537 FlexDirection::Column | FlexDirection::ColumnReverse => {
2538 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2539 flow_children.into_iter().rev().collect()
2540 } else {
2541 flow_children
2542 };
2543
2544 let justify = parent_style
2545 .map(|s| s.justify_content)
2546 .unwrap_or(JustifyContent::FlexStart);
2547 let align = parent_style
2548 .map(|s| s.align_items)
2549 .unwrap_or(AlignItems::Stretch);
2550
2551 let start_y = cursor.y;
2552 let initial_pages = pages.len();
2553
2554 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2556
2557 for (i, child) in items.iter().enumerate() {
2558 if i > 0 {
2559 cursor.y += row_gap;
2560 }
2561 let child_start = cursor.elements.len();
2562
2563 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2566 let has_auto_h = child_margin.has_auto_horizontal();
2567
2568 let (child_x, layout_w) = if has_auto_h {
2575 let child_style = child.style.resolve(parent_style, available_width);
2576 let has_explicit_width =
2577 matches!(child_style.width, SizeConstraint::Fixed(_));
2578 let intrinsic = self
2579 .measure_intrinsic_width(child, &child_style, font_context)
2580 .min(available_width);
2581 let w = match child_style.width {
2582 SizeConstraint::Fixed(fw) => fw,
2583 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2588 (available_width - child_margin.horizontal())
2589 .min(child_style.max_width)
2590 }
2591 SizeConstraint::Auto => intrinsic,
2592 }
2593 .min(child_style.max_width)
2594 .max(child_style.min_width);
2595 let lw = if has_explicit_width {
2596 available_width
2597 } else {
2598 w
2599 };
2600 let fixed_h = child_margin.horizontal();
2601 let slack = (available_width - w - fixed_h).max(0.0);
2602 let auto_left = child_margin.left.is_auto();
2603 let auto_right = child_margin.right.is_auto();
2604 let ml = match (auto_left, auto_right) {
2605 (true, true) => slack / 2.0,
2606 (true, false) => slack,
2607 (false, true) => 0.0,
2608 (false, false) => 0.0,
2609 };
2610 (content_x + child_margin.left.resolve() + ml, lw)
2611 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2612 let child_style = child.style.resolve(parent_style, available_width);
2613 let has_explicit_width =
2614 matches!(child_style.width, SizeConstraint::Fixed(_));
2615 let intrinsic = self
2616 .measure_intrinsic_width(child, &child_style, font_context)
2617 .min(available_width);
2618 let w = match child_style.width {
2627 SizeConstraint::Fixed(fw) => fw,
2628 SizeConstraint::Auto => intrinsic,
2629 }
2630 .min(child_style.max_width)
2631 .max(child_style.min_width);
2632 let lw = if has_explicit_width {
2633 available_width
2634 } else {
2635 w
2636 };
2637 match align {
2638 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2639 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2640 _ => (content_x, available_width),
2641 }
2642 } else {
2643 (content_x, available_width)
2644 };
2645
2646 self.layout_node(
2647 child,
2648 cursor,
2649 pages,
2650 child_x,
2651 layout_w,
2652 parent_style,
2653 font_context,
2654 None,
2655 None,
2656 );
2657
2658 child_ranges.push((child_start, cursor.elements.len()));
2659 }
2660
2661 let container_inner_h: Option<f64> = parent_style
2664 .and_then(|ps| match ps.height {
2665 SizeConstraint::Fixed(h) => {
2666 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2667 }
2668 SizeConstraint::Auto => None,
2669 })
2670 .or_else(|| {
2671 if parent_style.is_none() {
2673 Some(cursor.content_height - start_y)
2674 } else {
2675 None
2676 }
2677 });
2678
2679 if let Some(inner_h) = container_inner_h {
2680 if pages.len() == initial_pages {
2681 let child_styles: Vec<ResolvedStyle> = items
2682 .iter()
2683 .map(|child| child.style.resolve(parent_style, available_width))
2684 .collect();
2685 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2686 if total_grow > 0.0 {
2687 let children_total = cursor.y - start_y;
2688 let slack = (inner_h - children_total).max(0.0);
2689 if slack > 0.0 {
2690 let mut cumulative_shift = 0.0_f64;
2691 for (i, cs) in child_styles.iter().enumerate() {
2692 let (start, end) = child_ranges[i];
2693 if cumulative_shift > 0.001 {
2694 for j in start..end {
2695 offset_element_y(
2696 &mut cursor.elements[j],
2697 cumulative_shift,
2698 );
2699 }
2700 }
2701 if cs.flex_grow > 0.0 {
2702 let extra = slack * (cs.flex_grow / total_grow);
2703 if start < end {
2705 let elem = &mut cursor.elements[end - 1];
2706 elem.height += extra;
2707 reapply_justify_content(elem);
2708 }
2709 cumulative_shift += extra;
2710 }
2711 }
2712 cursor.y += cumulative_shift;
2713 }
2714 }
2715 }
2716 }
2717
2718 if let Some(inner_h) = container_inner_h {
2725 if pages.len() == initial_pages {
2726 let auto_styles: Vec<ResolvedStyle> = items
2727 .iter()
2728 .map(|child| child.style.resolve(parent_style, available_width))
2729 .collect();
2730 let total_autos: usize = auto_styles
2731 .iter()
2732 .map(|s| {
2733 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2734 })
2735 .sum();
2736 if total_autos > 0 {
2737 let children_total = cursor.y - start_y;
2738 let total_slack = (inner_h - children_total).max(0.0);
2739 if total_slack > 0.0 {
2740 let per_auto = total_slack / total_autos as f64;
2741 let mut cumulative_shift = 0.0_f64;
2742 for (i, cs) in auto_styles.iter().enumerate() {
2743 let (start, end) = child_ranges[i];
2744 let mt_auto = cs.margin.top.is_auto();
2745 let mb_auto = cs.margin.bottom.is_auto();
2746 let this_child_shift =
2750 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2751 if this_child_shift > 0.001 {
2752 for j in start..end {
2753 offset_element_y(
2754 &mut cursor.elements[j],
2755 this_child_shift,
2756 );
2757 }
2758 }
2759 cumulative_shift =
2762 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2763 }
2764 cursor.y += cumulative_shift;
2765 }
2766 }
2767 }
2768 }
2769
2770 let needs_justify =
2772 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2773 if needs_justify {
2774 let justify_inner_h = container_inner_h.or_else(|| {
2776 parent_style.and_then(|ps| match ps.height {
2777 SizeConstraint::Fixed(h) => {
2778 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2779 }
2780 SizeConstraint::Auto => None,
2781 })
2782 });
2783 if let Some(inner_h) = justify_inner_h {
2784 let children_total = cursor.y - start_y;
2785 let slack = inner_h - children_total;
2786 if slack > 0.0 {
2787 let n = child_ranges.len();
2788 let offsets: Vec<f64> = match justify {
2789 JustifyContent::FlexEnd => vec![slack; n],
2790 JustifyContent::Center => vec![slack / 2.0; n],
2791 JustifyContent::SpaceBetween => {
2792 if n <= 1 {
2793 vec![0.0; n]
2794 } else {
2795 let per_gap = slack / (n - 1) as f64;
2796 (0..n).map(|i| i as f64 * per_gap).collect()
2797 }
2798 }
2799 JustifyContent::SpaceAround => {
2800 let space = slack / n as f64;
2801 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2802 }
2803 JustifyContent::SpaceEvenly => {
2804 let space = slack / (n + 1) as f64;
2805 (0..n).map(|i| (i + 1) as f64 * space).collect()
2806 }
2807 JustifyContent::FlexStart => vec![0.0; n],
2808 };
2809 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2810 let dy = offsets[i];
2811 if dy.abs() > 0.001 {
2812 for j in start..end {
2813 offset_element_y(&mut cursor.elements[j], dy);
2814 }
2815 }
2816 }
2817 cursor.y += *offsets.last().unwrap_or(&0.0);
2818 }
2819 }
2820 }
2821 }
2822
2823 FlexDirection::Row | FlexDirection::RowReverse => {
2824 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2825 self.layout_flex_row(
2826 &flow_owned,
2827 cursor,
2828 pages,
2829 content_x,
2830 available_width,
2831 parent_style,
2832 column_gap,
2833 row_gap,
2834 font_context,
2835 );
2836 }
2837 }
2838
2839 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
2853 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2854 let ph = parent_style
2855 .and_then(|ps| match ps.height {
2856 SizeConstraint::Fixed(h) => {
2857 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2858 }
2859 SizeConstraint::Auto => None,
2860 })
2861 .unwrap_or_else(|| {
2862 if parent_fragmented {
2863 let first = &pages[entry_page_index];
2864 (first.height - first.config.margin.bottom) - parent_box_y
2865 } else {
2866 cursor.content_y + cursor.y - parent_box_y
2867 }
2868 });
2869 (parent_box_x, parent_box_y, available_width, ph)
2870 } else {
2871 cursor.containing_block
2872 };
2873
2874 for abs_child in &abs_children {
2876 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2877
2878 let child_width = match abs_style.width {
2880 SizeConstraint::Fixed(w) => w,
2881 SizeConstraint::Auto => {
2882 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2884 (cb_w - l - r).max(0.0)
2885 } else {
2886 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2887 }
2888 }
2889 };
2890
2891 let child_height = match abs_style.height {
2892 SizeConstraint::Fixed(h) => h,
2893 SizeConstraint::Auto => {
2894 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2895 }
2896 };
2897
2898 let abs_margin = abs_style.margin.to_edges();
2906 let abs_x = if let Some(l) = abs_style.left {
2907 cb_x + l
2908 } else if let Some(r) = abs_style.right {
2909 cb_x + cb_w - r - child_width - abs_margin.horizontal()
2910 } else {
2911 cb_x
2912 };
2913
2914 let abs_y = if let Some(t) = abs_style.top {
2915 cb_y + t
2916 } else if let Some(b) = abs_style.bottom {
2917 cb_y + cb_h - b - child_height - abs_margin.vertical()
2918 } else {
2919 cb_y
2920 };
2921
2922 let mut abs_cursor = PageCursor::new(&cursor.config);
2924 abs_cursor.y = 0.0;
2925 abs_cursor.content_x = abs_x;
2926 abs_cursor.content_y = abs_y;
2927
2928 self.layout_node(
2929 abs_child,
2930 &mut abs_cursor,
2931 &mut Vec::new(),
2932 abs_x,
2933 child_width,
2934 parent_style,
2935 font_context,
2936 None,
2937 None,
2938 );
2939
2940 if parent_fragmented {
2944 pages[entry_page_index].elements.extend(abs_cursor.elements);
2945 } else {
2946 cursor.elements.extend(abs_cursor.elements);
2947 }
2948 }
2949
2950 cursor.containing_block = saved_cb;
2952 }
2953
2954 #[allow(clippy::too_many_arguments)]
2955 fn layout_flex_row(
2956 &self,
2957 children: &[Node],
2958 cursor: &mut PageCursor,
2959 pages: &mut Vec<LayoutPage>,
2960 content_x: f64,
2961 available_width: f64,
2962 parent_style: Option<&ResolvedStyle>,
2963 column_gap: f64,
2964 row_gap: f64,
2965 font_context: &FontContext,
2966 ) {
2967 if children.is_empty() {
2968 return;
2969 }
2970
2971 let flex_wrap = parent_style
2972 .map(|s| s.flex_wrap)
2973 .unwrap_or(FlexWrap::NoWrap);
2974
2975 let items: Vec<FlexItem> = children
2978 .iter()
2979 .map(|child| {
2980 let style = child.style.resolve(parent_style, available_width);
2981 let base_width = match style.flex_basis {
2982 SizeConstraint::Fixed(w) => w,
2983 SizeConstraint::Auto => match style.width {
2984 SizeConstraint::Fixed(w) => w,
2985 SizeConstraint::Auto => {
2986 self.measure_intrinsic_width(child, &style, font_context)
2987 }
2988 },
2989 };
2990 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2991 FlexItem {
2992 node: child,
2993 style,
2994 base_width,
2995 min_content_width,
2996 }
2997 })
2998 .collect();
2999
3000 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3002 let lines = match flex_wrap {
3003 FlexWrap::NoWrap => {
3004 vec![flex::WrapLine {
3005 start: 0,
3006 end: items.len(),
3007 }]
3008 }
3009 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
3010 FlexWrap::WrapReverse => {
3011 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
3012 l.reverse();
3013 l
3014 }
3015 };
3016
3017 if lines.is_empty() {
3018 return;
3019 }
3020
3021 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3023
3024 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3026
3027 let initial_pages_count = pages.len();
3028 let flex_start_y = cursor.y;
3029 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3030
3031 for (line_idx, line) in lines.iter().enumerate() {
3032 let line_items = &items[line.start..line.end];
3033 let line_count = line.end - line.start;
3034 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3035 let distributable = available_width - line_gap;
3036
3037 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
3039 let remaining = distributable - total_base;
3040
3041 if remaining > 0.0 {
3042 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3043 if total_grow > 0.0 {
3044 for (j, item) in line_items.iter().enumerate() {
3045 final_widths[line.start + j] =
3046 item.base_width + remaining * (item.style.flex_grow / total_grow);
3047 }
3048 }
3049 } else if remaining < 0.0 {
3050 let total_shrink: f64 = line_items
3051 .iter()
3052 .map(|i| i.style.flex_shrink * i.base_width)
3053 .sum();
3054 if total_shrink > 0.0 {
3055 for (j, item) in line_items.iter().enumerate() {
3056 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3057 let w = item.base_width + remaining * factor;
3058 let floor = item.style.min_width.max(item.min_content_width);
3059 final_widths[line.start + j] = w.max(floor);
3060 }
3061 }
3062 }
3063
3064 let mut line_height: f64 = line_items
3066 .iter()
3067 .enumerate()
3068 .map(|(j, item)| {
3069 let fw = final_widths[line.start + j];
3070 self.measure_node_height(item.node, fw, &item.style, font_context)
3071 + item.style.margin.vertical()
3072 })
3073 .fold(0.0f64, f64::max);
3074
3075 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3081 let baseline_ds: Vec<Option<f64>> = line_items
3082 .iter()
3083 .enumerate()
3084 .map(|(j, item)| {
3085 let align = item.style.align_self.unwrap_or(parent_align);
3086 if matches!(align, AlignItems::Baseline) {
3087 let fw = final_widths[line.start + j];
3088 Some(self.flex_item_baseline_distance(
3089 item.node,
3090 &item.style,
3091 fw,
3092 font_context,
3093 ))
3094 } else {
3095 None
3096 }
3097 })
3098 .collect();
3099 let line_baseline = baseline_ds
3100 .iter()
3101 .flatten()
3102 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3103 if let Some(bl) = line_baseline {
3104 for (j, item) in line_items.iter().enumerate() {
3105 if let Some(d) = baseline_ds[j] {
3106 let fw = final_widths[line.start + j];
3107 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3108 + item.style.margin.vertical();
3109 line_height = line_height.max(bl - d + h);
3110 }
3111 }
3112 }
3113
3114 if let Some(ps) = parent_style {
3125 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3126 if let SizeConstraint::Fixed(h) = ps.height {
3127 let inner = h - ps.padding.vertical() - ps.border_width.vertical();
3128 line_height = line_height.max(inner);
3129 }
3130 }
3131 }
3132
3133 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
3140 pages.push(cursor.finalize());
3141 *cursor = cursor.new_page();
3142 }
3143
3144 if line_idx > 0 {
3146 cursor.y += row_gap;
3147 }
3148
3149 let row_start_y = cursor.y;
3150
3151 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
3153 let slack = available_width - actual_total - line_gap;
3154
3155 let (start_offset, between_extra) = match justify {
3156 JustifyContent::FlexStart => (0.0, 0.0),
3157 JustifyContent::FlexEnd => (slack, 0.0),
3158 JustifyContent::Center => (slack / 2.0, 0.0),
3159 JustifyContent::SpaceBetween => {
3160 if line_count > 1 {
3161 (0.0, slack / (line_count as f64 - 1.0))
3162 } else {
3163 (0.0, 0.0)
3164 }
3165 }
3166 JustifyContent::SpaceAround => {
3167 let s = slack / line_count as f64;
3168 (s / 2.0, s)
3169 }
3170 JustifyContent::SpaceEvenly => {
3171 let s = slack / (line_count as f64 + 1.0);
3172 (s, s)
3173 }
3174 };
3175
3176 let line_elem_start = cursor.elements.len();
3177 let mut x = content_x + start_offset;
3178
3179 let line_start_pages = pages.len();
3190
3191 for (j, item) in line_items.iter().enumerate() {
3192 if j > 0 {
3193 x += column_gap + between_extra;
3194 }
3195
3196 let fw = final_widths[line.start + j];
3197
3198 let align = item
3199 .style
3200 .align_self
3201 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3202
3203 let item_height =
3204 self.measure_node_height(item.node, fw, &item.style, font_context);
3205
3206 let has_auto_v = item.style.margin.has_auto_vertical();
3208 let y_offset = if has_auto_v {
3209 let fixed_v = item.style.margin.vertical();
3210 let slack = (line_height - item_height - fixed_v).max(0.0);
3211 let auto_top = item.style.margin.top.is_auto();
3212 let auto_bottom = item.style.margin.bottom.is_auto();
3213 match (auto_top, auto_bottom) {
3214 (true, true) => slack / 2.0,
3215 (true, false) => slack,
3216 (false, true) => 0.0,
3217 (false, false) => 0.0,
3218 }
3219 } else {
3220 match align {
3221 AlignItems::FlexStart => 0.0,
3222 AlignItems::FlexEnd => {
3223 line_height - item_height - item.style.margin.vertical()
3224 }
3225 AlignItems::Center => {
3226 (line_height - item_height - item.style.margin.vertical()) / 2.0
3227 }
3228 AlignItems::Stretch => 0.0,
3229 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3230 (Some(bl), Some(d)) => bl - d,
3231 _ => 0.0,
3232 },
3233 }
3234 };
3235
3236 let cross_h = if matches!(align, AlignItems::Stretch)
3240 && matches!(item.style.height, SizeConstraint::Auto)
3241 && !has_auto_v
3242 {
3243 let stretch_h = line_height - item.style.margin.vertical();
3244 if stretch_h > item_height {
3245 Some(stretch_h)
3246 } else {
3247 None
3248 }
3249 } else {
3250 None
3251 };
3252
3253 let saved_y = cursor.y;
3254 cursor.y = row_start_y + y_offset;
3255
3256 self.layout_node(
3257 item.node,
3258 cursor,
3259 pages,
3260 x,
3261 available_width,
3262 parent_style,
3263 font_context,
3264 cross_h,
3265 Some(fw),
3266 );
3267
3268 cursor.y = saved_y;
3269 x += fw;
3270 }
3271
3272 if line_items.len() > 1 && pages.len() > line_start_pages {
3273 let near = line_items
3274 .iter()
3275 .find_map(|it| first_text_snippet(it.node))
3276 .map(|t| format!(" (row beginning \"{t}\")"))
3277 .unwrap_or_default();
3278 self.defect(format!(
3279 "render defect: a flex row crossing a page boundary lays its children sequentially — a column taller than the page does not continue side by side{near}"
3280 ));
3281 }
3282 cursor.y = row_start_y + line_height;
3283 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3284 }
3285
3286 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3288 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3289 if !matches!(align_content, AlignContent::FlexStart)
3290 && !matches!(flex_wrap, FlexWrap::NoWrap)
3291 {
3292 if let Some(parent) = parent_style {
3293 if let SizeConstraint::Fixed(container_h) = parent.height {
3294 let inner_h = container_h
3295 - parent.padding.vertical()
3296 - parent.border_width.vertical();
3297 let total_used = cursor.y - flex_start_y;
3298 let slack = inner_h - total_used;
3299 if slack > 0.0 {
3300 let n = line_infos.len();
3301 let offsets: Vec<f64> = match align_content {
3302 AlignContent::FlexEnd => vec![slack; n],
3303 AlignContent::Center => vec![slack / 2.0; n],
3304 AlignContent::SpaceBetween => {
3305 if n <= 1 {
3306 vec![0.0; n]
3307 } else {
3308 let per_gap = slack / (n - 1) as f64;
3309 (0..n).map(|i| i as f64 * per_gap).collect()
3310 }
3311 }
3312 AlignContent::SpaceAround => {
3313 let space = slack / n as f64;
3314 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3315 }
3316 AlignContent::SpaceEvenly => {
3317 let space = slack / (n + 1) as f64;
3318 (0..n).map(|i| (i + 1) as f64 * space).collect()
3319 }
3320 AlignContent::Stretch => {
3321 let extra = slack / n as f64;
3322 (0..n).map(|i| i as f64 * extra).collect()
3323 }
3324 AlignContent::FlexStart => vec![0.0; n],
3325 };
3326 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3327 let dy = offsets[i];
3328 if dy.abs() > 0.001 {
3329 for j in start..end {
3330 offset_element_y(&mut cursor.elements[j], dy);
3331 }
3332 }
3333 }
3334 cursor.y += *offsets.last().unwrap_or(&0.0);
3335 }
3336 }
3337 }
3338 }
3339 }
3340 }
3341
3342 #[allow(clippy::too_many_arguments)]
3345 fn layout_list(
3346 &self,
3347 node: &Node,
3348 ordered: bool,
3349 marker_type: ListMarkerType,
3350 start: u32,
3351 style: &ResolvedStyle,
3352 cursor: &mut PageCursor,
3353 pages: &mut Vec<LayoutPage>,
3354 x: f64,
3355 available_width: f64,
3356 font_context: &FontContext,
3357 ) {
3358 let margin = &style.margin.to_edges();
3359 let padding = &style.padding;
3360
3361 cursor.y += margin.top;
3362
3363 let list_x = x + margin.left;
3364 let outer_width = available_width - margin.horizontal();
3365 let inner_width = outer_width - padding.horizontal();
3366
3367 let n_items = node
3370 .children
3371 .iter()
3372 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3373 .count() as u32;
3374
3375 let marker_gutter =
3376 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3377
3378 let list_inner_x = list_x + padding.left;
3379 let content_x = list_inner_x + marker_gutter;
3380 let content_width = (inner_width - marker_gutter).max(0.0);
3381
3382 let snapshot = cursor.elements.len();
3385 let list_start_y = cursor.content_y + cursor.y;
3386 cursor.y += padding.top;
3387
3388 let mut item_index: u32 = 0;
3389 for child in &node.children {
3390 if !matches!(child.kind, NodeKind::ListItem) {
3391 continue;
3392 }
3393 let marker_idx = start + item_index;
3394 self.layout_list_item(
3395 child,
3396 marker_idx,
3397 ordered,
3398 marker_type,
3399 marker_gutter,
3400 style,
3401 cursor,
3402 pages,
3403 list_inner_x,
3404 content_x,
3405 content_width,
3406 font_context,
3407 );
3408 item_index += 1;
3409 }
3410
3411 cursor.y += padding.bottom;
3412
3413 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3415 let list_height = cursor.content_y + cursor.y - list_start_y;
3416 cursor.elements.push(LayoutElement {
3417 x: list_x,
3418 y: list_start_y,
3419 width: outer_width,
3420 height: list_height,
3421 draw: DrawCommand::None,
3422 children: item_elements,
3423 node_type: Some("List".to_string()),
3424 resolved_style: Some(style.clone()),
3425 source_location: node.source_location.clone(),
3426 href: None,
3427 bookmark: node.bookmark.clone(),
3428 alt: None,
3429 is_header_row: false,
3430 actual_text: None,
3434 list_numbering: match marker_type {
3435 ListMarkerType::None => None,
3436 ListMarkerType::Disc => Some("Disc"),
3437 ListMarkerType::Circle => Some("Circle"),
3438 ListMarkerType::Square => Some("Square"),
3439 ListMarkerType::Decimal => Some("Decimal"),
3440 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3441 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3442 ListMarkerType::LowerRoman => Some("LowerRoman"),
3443 ListMarkerType::UpperRoman => Some("UpperRoman"),
3444 },
3445 col_span: 1,
3446 overflow: style.overflow,
3447 opacity: style.opacity,
3448 });
3449
3450 cursor.y += margin.bottom;
3451 }
3452
3453 #[allow(clippy::too_many_arguments)]
3454 fn layout_list_item(
3455 &self,
3456 item: &Node,
3457 marker_idx: u32,
3458 ordered: bool,
3459 marker_type: ListMarkerType,
3460 marker_gutter: f64,
3461 parent_style: &ResolvedStyle,
3462 cursor: &mut PageCursor,
3463 pages: &mut Vec<LayoutPage>,
3464 list_inner_x: f64,
3465 content_x: f64,
3466 content_width: f64,
3467 font_context: &FontContext,
3468 ) {
3469 let item_style = item.style.resolve(Some(parent_style), content_width);
3470 let item_margin = item_style.margin.to_edges();
3471
3472 cursor.y += item_margin.top;
3473 let item_start_y = cursor.content_y + cursor.y;
3474 let item_snapshot = cursor.elements.len();
3475
3476 let marker_str = format_marker(marker_idx, ordered, marker_type);
3480 if !marker_str.is_empty() {
3481 let saved_y = cursor.y;
3482 self.layout_text(
3483 &marker_str,
3484 None,
3485 &[],
3486 &item_style,
3487 cursor,
3488 pages,
3489 list_inner_x,
3490 marker_gutter,
3491 font_context,
3492 None,
3493 None,
3494 Some("Lbl"),
3495 );
3496 cursor.y = saved_y;
3497 }
3498
3499 self.layout_children(
3505 &item.children,
3506 &item.style,
3507 cursor,
3508 pages,
3509 content_x,
3510 content_width,
3511 Some(&item_style),
3512 font_context,
3513 );
3514
3515 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3518 let item_height = cursor.content_y + cursor.y - item_start_y;
3519 let item_width = content_x + content_width - list_inner_x;
3520 cursor.elements.push(LayoutElement {
3521 x: list_inner_x,
3522 y: item_start_y,
3523 width: item_width,
3524 height: item_height,
3525 draw: DrawCommand::None,
3526 children: item_children,
3527 node_type: Some("ListItem".to_string()),
3528 resolved_style: Some(item_style.clone()),
3529 source_location: item.source_location.clone(),
3530 href: None,
3531 bookmark: item.bookmark.clone(),
3532 alt: None,
3533 is_header_row: false,
3534 actual_text: None,
3535 list_numbering: None,
3536 col_span: 1,
3537 overflow: item_style.overflow,
3538 opacity: item_style.opacity,
3539 });
3540
3541 cursor.y += item_margin.bottom;
3542 }
3543
3544 #[allow(clippy::too_many_arguments)]
3545 fn layout_table(
3546 &self,
3547 node: &Node,
3548 style: &ResolvedStyle,
3549 column_defs: &[ColumnDef],
3550 cursor: &mut PageCursor,
3551 pages: &mut Vec<LayoutPage>,
3552 x: f64,
3553 available_width: f64,
3554 font_context: &FontContext,
3555 ) {
3556 let padding = &style.padding;
3557 let margin = &style.margin.to_edges();
3558 let border = &style.border_width;
3559
3560 let table_x = x + margin.left;
3561 let table_width = match style.width {
3562 SizeConstraint::Fixed(w) => w,
3563 SizeConstraint::Auto => available_width - margin.horizontal(),
3564 };
3565 let inner_width = table_width - padding.horizontal() - border.horizontal();
3566
3567 let col_widths = self.resolve_column_widths(
3568 column_defs,
3569 inner_width,
3570 &node.children,
3571 style,
3572 font_context,
3573 );
3574
3575 let all_offsets = Self::table_column_offsets(&node.children);
3579 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
3580 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
3581
3582 for (child, offs) in node.children.iter().zip(&all_offsets) {
3583 match &child.kind {
3584 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
3585 _ => body_rows.push((child, offs)),
3586 }
3587 }
3588
3589 if !style.breakable {
3595 let total_height: f64 = node
3596 .children
3597 .iter()
3598 .zip(&all_offsets)
3599 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3600 .sum::<f64>()
3601 + padding.vertical()
3602 + border.vertical();
3603 let fresh_page_available = cursor.content_height
3604 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3605 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3606 if total_height > cursor.remaining_height()
3607 && total_height <= fresh_page_available
3608 && cursor.y > 0.0
3609 {
3610 pages.push(cursor.finalize());
3611 *cursor = cursor.new_page();
3612 }
3613 }
3614
3615 let initial_page_count = pages.len();
3622 let snapshot = cursor.elements.len();
3623 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3624
3625 cursor.y += margin.top + padding.top + border.top;
3626
3627 let cell_x_start = table_x + padding.left + border.left;
3628
3629 if !header_rows.is_empty() {
3654 let total_header_h: f64 = header_rows
3655 .iter()
3656 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3657 .sum();
3658 let first_body_h = body_rows
3659 .first()
3660 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3661 .unwrap_or(0.0);
3662
3663 let needed = total_header_h + first_body_h;
3664 let fresh_page_available = cursor.content_height
3665 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3666 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3667
3668 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3669 pages.push(cursor.finalize());
3670 *cursor = cursor.new_page();
3671 cursor.y += padding.top + border.top;
3672 }
3673 }
3674
3675 for (header_row, offs) in &header_rows {
3676 self.layout_table_row(
3677 header_row,
3678 &col_widths,
3679 offs,
3680 style,
3681 cursor,
3682 cell_x_start,
3683 font_context,
3684 pages,
3685 );
3686 }
3687
3688 for (body_row, offs) in &body_rows {
3689 let row_height =
3690 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
3691
3692 let fresh_page_available = cursor.content_height
3700 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3701 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3702 if row_height > fresh_page_available {
3703 self.defect(format!(
3704 "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",
3705 ));
3706 }
3707 if row_height > cursor.remaining_height()
3708 && cursor.remaining_height() < fresh_page_available - 0.5
3709 {
3710 pages.push(cursor.finalize());
3711 *cursor = cursor.new_page();
3712
3713 cursor.y += padding.top + border.top;
3714 for (header_row, h_offs) in &header_rows {
3715 self.layout_table_row(
3716 header_row,
3717 &col_widths,
3718 h_offs,
3719 style,
3720 cursor,
3721 cell_x_start,
3722 font_context,
3723 pages,
3724 );
3725 }
3726 }
3727
3728 self.layout_table_row(
3729 body_row,
3730 &col_widths,
3731 offs,
3732 style,
3733 cursor,
3734 cell_x_start,
3735 font_context,
3736 pages,
3737 );
3738 }
3739
3740 let has_visual = style.background_color.is_some()
3744 || style.background.is_some()
3745 || style.border_width.top > 0.0
3746 || style.border_width.right > 0.0
3747 || style.border_width.bottom > 0.0
3748 || style.border_width.left > 0.0;
3749 let draw_cmd = if has_visual {
3750 DrawCommand::Rect {
3751 background: style.background_color,
3752 border_width: style.border_width,
3753 border_color: style.border_color,
3754 border_style: style.border_style,
3755 border_radius: style.border_radius,
3756 opacity: 1.0,
3757 box_shadow: style.box_shadow.map(Box::new),
3758 background_gradient: style.background.clone().map(Box::new),
3759 }
3760 } else {
3761 DrawCommand::None
3762 };
3763 let make_wrapper =
3764 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3765 x: table_x,
3766 y,
3767 width: table_width,
3768 height,
3769 draw,
3770 children,
3771 node_type: Some(node_kind_name(&node.kind).to_string()),
3772 resolved_style: Some(style.clone()),
3773 source_location: node.source_location.clone(),
3774 href: node.href.clone(),
3775 bookmark: None,
3776 alt: None,
3777 is_header_row: false,
3778 actual_text: None,
3779 list_numbering: None,
3780 col_span: 1,
3781 overflow: Overflow::default(),
3782 opacity: style.opacity,
3783 };
3784
3785 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3786
3787 if pages.len() == initial_page_count {
3788 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3790 cursor.elements.push(make_wrapper(
3791 rect_start_y,
3792 table_bottom_y - rect_start_y,
3793 child_elements,
3794 draw_cmd,
3795 ));
3796 } else {
3797 let page = &mut pages[initial_page_count];
3816 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3817 let page_content_bottom =
3818 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3819 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3820 if !our_elements.is_empty() {
3821 page.elements.push(make_wrapper(
3822 rect_start_y,
3823 page_content_bottom - rect_start_y,
3824 our_elements,
3825 draw_cmd.clone(),
3826 ));
3827 }
3828
3829 for page in &mut pages[initial_page_count + 1..] {
3831 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3832 let content_top = page.config.margin.top + header_h;
3833 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3834 let content_bottom = page.config.margin.top
3835 + (page.height - page.config.margin.vertical())
3836 - footer_h;
3837 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3838 if !all_elements.is_empty() {
3839 page.elements.push(make_wrapper(
3840 content_top,
3841 content_bottom - content_top,
3842 all_elements,
3843 draw_cmd.clone(),
3844 ));
3845 }
3846 }
3847
3848 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3850 if !all_elements.is_empty() {
3851 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3852 let content_top = cursor.content_y + header_h;
3853 cursor.elements.push(make_wrapper(
3854 content_top,
3855 table_bottom_y - content_top,
3856 all_elements,
3857 draw_cmd,
3858 ));
3859 }
3860 }
3861
3862 cursor.y += padding.bottom + border.bottom + margin.bottom;
3863 }
3864
3865 fn subtree_forces_break(node: &Node) -> bool {
3875 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
3876 return true;
3877 }
3878 if node.style.break_before == Some(true) {
3879 return true;
3880 }
3881 node.children.iter().any(Self::subtree_forces_break)
3882 }
3883
3884 fn occupancy_column_count(children: &[Node]) -> usize {
3890 let offsets = Self::table_column_offsets(children);
3891 children
3892 .iter()
3893 .zip(&offsets)
3894 .map(|(row, offs)| {
3895 row.children
3896 .iter()
3897 .zip(offs)
3898 .map(|(cell, &start)| start + Self::cell_col_span(cell))
3899 .max()
3900 .unwrap_or(0)
3901 })
3902 .max()
3903 .unwrap_or(1)
3904 .max(1)
3905 }
3906
3907 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
3917 fn spans(cell: &Node) -> (usize, u32) {
3918 match &cell.kind {
3919 NodeKind::TableCell { col_span, row_span } => {
3920 ((*col_span).max(1) as usize, (*row_span).max(1))
3921 }
3922 _ => (1, 1),
3923 }
3924 }
3925 let mut pending: Vec<u32> = Vec::new();
3926 let mut out = Vec::with_capacity(rows.len());
3927 for row in rows {
3928 let mut offsets = Vec::with_capacity(row.children.len());
3929 let mut col = 0usize;
3930 for cell in &row.children {
3931 let (span, rspan) = spans(cell);
3932 while pending.get(col).copied().unwrap_or(0) > 0 {
3933 col += 1;
3934 }
3935 offsets.push(col);
3936 if rspan > 1 {
3937 if pending.len() < col + span {
3938 pending.resize(col + span, 0);
3939 }
3940 for slot in pending.iter_mut().take(col + span).skip(col) {
3941 *slot = (*slot).max(rspan);
3942 }
3943 }
3944 col += span;
3945 }
3946 out.push(offsets);
3947 for p in pending.iter_mut() {
3948 *p = p.saturating_sub(1);
3949 }
3950 }
3951 out
3952 }
3953
3954 #[allow(clippy::too_many_arguments)]
3955 fn layout_table_row(
3956 &self,
3957 row: &Node,
3958 col_widths: &[f64],
3959 col_offsets: &[usize],
3960 parent_style: &ResolvedStyle,
3961 cursor: &mut PageCursor,
3962 start_x: f64,
3963 font_context: &FontContext,
3964 pages: &mut Vec<LayoutPage>,
3965 ) {
3966 let row_style = row
3967 .style
3968 .resolve(Some(parent_style), col_widths.iter().sum());
3969
3970 let row_height =
3971 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
3972 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
3973 let row_y = cursor.content_y + cursor.y;
3974 let total_width: f64 = col_widths.iter().sum();
3975
3976 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3977
3978 let row_snapshot = cursor.elements.len();
3980
3981 let row_may_break =
3993 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
3994
3995 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3996 for (cell_i, cell) in row.children.iter().enumerate() {
4000 let span = match &cell.kind {
4001 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
4002 _ => 1,
4003 };
4004 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
4005 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
4006 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
4007
4008 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4009
4010 let cell_snapshot = cursor.elements.len();
4012
4013 let inner_width =
4014 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4015
4016 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4017 let saved_y = cursor.y;
4018 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4019
4020 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4024 let content_h: f64 = cell
4025 .children
4026 .iter()
4027 .map(|ch| {
4028 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4029 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4030 })
4031 .sum();
4032 let inner_row =
4033 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4034 let slack = (inner_row - content_h).max(0.0);
4035 cursor.y += match cell_style.vertical_align {
4036 crate::style::VerticalAlign::Middle => slack / 2.0,
4037 crate::style::VerticalAlign::Bottom => slack,
4038 crate::style::VerticalAlign::Baseline => row_bl
4043 .map(|b| {
4044 let d = self.cell_baseline_distance(
4045 cell,
4046 &cell_style,
4047 inner_width,
4048 font_context,
4049 );
4050 (b - d).max(0.0)
4051 })
4052 .unwrap_or(0.0),
4053 crate::style::VerticalAlign::Top => 0.0,
4054 };
4055 }
4056
4057 let cursor_before_cell = if row_may_break {
4062 Some(cursor.clone())
4063 } else {
4064 None
4065 };
4066 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4067 for child in &cell.children {
4068 self.layout_node(
4069 child,
4070 cursor,
4071 &mut cell_pages,
4072 content_x,
4073 inner_width,
4074 Some(&cell_style),
4075 font_context,
4076 None,
4077 None,
4078 );
4079 }
4080
4081 if !cell_pages.is_empty() {
4083 let post_break_elements = std::mem::take(&mut cursor.elements);
4084 if let Some(last_page) = cell_pages.last_mut() {
4085 last_page.elements.extend(post_break_elements);
4086 }
4087 if !is_header {
4093 all_overflow_pages.extend(cell_pages);
4094 }
4095 *cursor = cursor_before_cell
4101 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4102 }
4103
4104 cursor.y = saved_y;
4105
4106 let cell_children: Vec<LayoutElement> =
4108 cursor.elements.drain(cell_snapshot..).collect();
4109
4110 cursor.elements.push(LayoutElement {
4112 x: cell_x,
4113 y: row_y,
4114 width: col_width,
4115 height: row_height,
4116 draw: if cell_style.background_color.is_some()
4117 || cell_style.border_width.horizontal() > 0.0
4118 || cell_style.border_width.vertical() > 0.0
4119 {
4120 DrawCommand::Rect {
4121 background: cell_style.background_color,
4122 border_width: cell_style.border_width,
4123 border_color: cell_style.border_color,
4124 border_style: cell_style.border_style,
4125 border_radius: cell_style.border_radius,
4126 opacity: 1.0,
4127 box_shadow: cell_style.box_shadow.map(Box::new),
4128 background_gradient: cell_style.background.clone().map(Box::new),
4129 }
4130 } else {
4131 DrawCommand::None
4132 },
4133 children: cell_children,
4134 node_type: Some("TableCell".to_string()),
4135 resolved_style: Some(cell_style.clone()),
4136 source_location: cell.source_location.clone(),
4137 href: None,
4138 bookmark: cell.bookmark.clone(),
4139 alt: None,
4140 is_header_row: is_header,
4141 actual_text: None,
4142 list_numbering: None,
4143 col_span: span as u32,
4144 overflow: Overflow::default(),
4145 opacity: 1.0,
4146 });
4147 }
4148
4149 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
4151 let row_element = LayoutElement {
4152 x: start_x,
4153 y: row_y,
4154 width: total_width,
4155 height: row_height,
4156 draw: if let Some(bg) = row_style.background_color {
4157 DrawCommand::Rect {
4158 background: Some(bg),
4159 border_width: Edges::default(),
4160 border_color: EdgeValues::uniform(Color::BLACK),
4161 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4162 border_radius: CornerValues::uniform(0.0),
4163 opacity: 1.0,
4164 box_shadow: row_style.box_shadow.map(Box::new),
4165 background_gradient: row_style.background.clone().map(Box::new),
4166 }
4167 } else {
4168 DrawCommand::None
4169 },
4170 children: row_children,
4171 node_type: Some("TableRow".to_string()),
4172 resolved_style: Some(row_style.clone()),
4173 source_location: row.source_location.clone(),
4174 href: None,
4175 bookmark: row.bookmark.clone(),
4176 alt: None,
4177 is_header_row: is_header,
4178 actual_text: None,
4179 list_numbering: None,
4180 col_span: 1,
4181 overflow: row_style.overflow,
4182 opacity: row_style.opacity,
4183 };
4184
4185 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4186 first_overflow.elements.push(row_element);
4196 } else {
4197 cursor.elements.push(row_element);
4198 cursor.y += row_height;
4199 }
4200
4201 pages.extend(all_overflow_pages);
4203 }
4204
4205 #[allow(clippy::too_many_arguments)]
4206 #[allow(clippy::too_many_arguments)]
4207 fn layout_text(
4208 &self,
4209 content: &str,
4210 href: Option<&str>,
4211 runs: &[TextRun],
4212 style: &ResolvedStyle,
4213 cursor: &mut PageCursor,
4214 pages: &mut Vec<LayoutPage>,
4215 x: f64,
4216 available_width: f64,
4217 font_context: &FontContext,
4218 source_location: Option<&SourceLocation>,
4219 bookmark: Option<&str>,
4220 node_type_override: Option<&str>,
4224 ) {
4225 if style.border_width.top > 0.0
4232 || style.border_width.right > 0.0
4233 || style.border_width.bottom > 0.0
4234 || style.border_width.left > 0.0
4235 {
4236 self.defect(
4237 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4238 );
4239 }
4240 if style.background_color.is_some() {
4241 self.defect(
4242 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4243 );
4244 }
4245 let margin = &style.margin.to_edges();
4246 let text_x = x + margin.left;
4247 let text_width = match style.width {
4254 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4255 SizeConstraint::Auto => available_width - margin.horizontal(),
4256 };
4257
4258 cursor.y += margin.top;
4259
4260 if !runs.is_empty() {
4262 self.layout_text_runs(
4263 runs,
4264 href,
4265 style,
4266 cursor,
4267 pages,
4268 text_x,
4269 text_width,
4270 font_context,
4271 source_location,
4272 bookmark,
4273 node_type_override,
4274 );
4275 cursor.y += margin.bottom;
4276 return;
4277 }
4278
4279 let content = substitute_page_placeholders(content);
4280 let transformed = apply_text_transform(&content, style.text_transform);
4281 let justify = matches!(style.text_align, TextAlign::Justify);
4282 let lines = match style.line_breaking {
4283 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4284 font_context,
4285 &transformed,
4286 text_width,
4287 style.font_size,
4288 &style.font_family,
4289 style.font_weight,
4290 style.font_style,
4291 style.letter_spacing,
4292 style.hyphens,
4293 style.lang.as_deref(),
4294 justify,
4295 ),
4296 LineBreaking::Greedy => self.text_layout.break_into_lines(
4297 font_context,
4298 &transformed,
4299 text_width,
4300 style.font_size,
4301 &style.font_family,
4302 style.font_weight,
4303 style.font_style,
4304 style.letter_spacing,
4305 style.hyphens,
4306 style.lang.as_deref(),
4307 ),
4308 };
4309
4310 let lines = match style.text_overflow {
4312 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4313 font_context,
4314 lines,
4315 text_width,
4316 style.font_size,
4317 &style.font_family,
4318 style.font_weight,
4319 style.font_style,
4320 style.letter_spacing,
4321 ),
4322 TextOverflow::Clip => self.text_layout.truncate_clip(
4323 font_context,
4324 lines,
4325 text_width,
4326 style.font_size,
4327 &style.font_family,
4328 style.font_weight,
4329 style.font_style,
4330 style.letter_spacing,
4331 ),
4332 TextOverflow::Wrap => lines,
4333 };
4334
4335 let line_height = style.font_size * style.line_height;
4336
4337 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4339 let decision = page_break::decide_break(
4340 cursor.remaining_height(),
4341 &line_heights,
4342 true,
4343 style.min_orphan_lines as usize,
4344 style.min_widow_lines as usize,
4345 );
4346
4347 let mut snapshot = cursor.elements.len();
4349 let mut container_start_y = cursor.content_y + cursor.y;
4350 let mut is_first_element = true;
4351
4352 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4354 pages.push(cursor.finalize());
4355 *cursor = cursor.new_page();
4356 snapshot = cursor.elements.len();
4357 container_start_y = cursor.content_y + cursor.y;
4358 }
4359
4360 let forced_break_at = match decision {
4362 page_break::BreakDecision::Split {
4363 items_on_current_page,
4364 } => Some(items_on_current_page),
4365 _ => None,
4366 };
4367 let mut first_break_done = false;
4368
4369 for (line_idx, line) in lines.iter().enumerate() {
4370 let needs_break = if let Some(break_at) = forced_break_at {
4372 if !first_break_done && line_idx == break_at {
4373 true
4374 } else {
4375 line_height > cursor.remaining_height()
4376 }
4377 } else {
4378 line_height > cursor.remaining_height()
4379 };
4380
4381 if needs_break {
4382 first_break_done = true;
4383 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4385 if !line_elements.is_empty() {
4386 let container_height = cursor.content_y + cursor.y - container_start_y;
4387 cursor.elements.push(LayoutElement {
4388 x: text_x,
4389 y: container_start_y,
4390 width: text_width,
4391 height: container_height,
4392 draw: DrawCommand::None,
4393 children: line_elements,
4394 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4395 resolved_style: Some(style.clone()),
4396 source_location: source_location.cloned(),
4397 href: href.map(|s| s.to_string()),
4398 bookmark: if is_first_element {
4399 bookmark.map(|s| s.to_string())
4400 } else {
4401 None
4402 },
4403 alt: None,
4404 is_header_row: false,
4405 actual_text: None,
4406 list_numbering: None,
4407 col_span: 1,
4408 overflow: Overflow::default(),
4409 opacity: 1.0,
4410 });
4411 is_first_element = false;
4412 }
4413
4414 pages.push(cursor.finalize());
4415 *cursor = cursor.new_page();
4416
4417 snapshot = cursor.elements.len();
4419 container_start_y = cursor.content_y + cursor.y;
4420 }
4421
4422 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4423
4424 let rendered_width = if glyphs.is_empty() {
4428 line.width
4429 } else {
4430 let last = &glyphs[glyphs.len() - 1];
4431 (last.x_offset + last.x_advance).max(line.width * 0.5)
4432 };
4433
4434 let line_x = match style.text_align {
4435 TextAlign::Left => text_x,
4436 TextAlign::Right => text_x + text_width - rendered_width,
4437 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4438 TextAlign::Justify => text_x,
4439 };
4440
4441 let is_last_line = line_idx == lines.len() - 1;
4449 let user_ws = style.word_spacing;
4450 let (justified_width, word_spacing) =
4451 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4452 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4453 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4454 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4455 let s = glyphs[..=idx]
4456 .iter()
4457 .filter(|g| g.char_value == ' ')
4458 .count();
4459 (w, s)
4460 } else {
4461 (0.0, 0)
4462 };
4463 let slack = text_width - natural_width;
4464 let ws = if space_count > 0 && slack.abs() > 0.01 {
4465 slack / space_count as f64
4466 } else {
4467 0.0
4468 };
4469 (text_width, user_ws + ws)
4470 } else {
4471 (rendered_width, user_ws)
4472 };
4473
4474 let text_line = TextLine {
4475 x: line_x,
4476 y: cursor.content_y
4482 + cursor.y
4483 + baseline_in_line(
4484 line_height,
4485 style.font_size,
4486 font_context.baseline_metrics(
4487 &style.font_family,
4488 style.font_weight,
4489 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4490 ),
4491 ),
4492 glyphs,
4493 width: justified_width,
4494 height: line_height,
4495 word_spacing,
4496 };
4497
4498 cursor.elements.push(LayoutElement {
4499 x: line_x,
4500 y: cursor.content_y + cursor.y,
4501 width: justified_width,
4502 height: line_height,
4503 draw: DrawCommand::Text {
4504 lines: vec![text_line],
4505 color: style.color,
4506 text_decoration: style.text_decoration,
4507 opacity: 1.0,
4508 },
4509 children: vec![],
4510 node_type: Some("TextLine".to_string()),
4511 resolved_style: Some(style.clone()),
4512 source_location: None,
4513 href: href.map(|s| s.to_string()),
4514 bookmark: None,
4515 alt: None,
4516 is_header_row: false,
4517 actual_text: None,
4518 list_numbering: None,
4519 col_span: 1,
4520 overflow: Overflow::default(),
4521 opacity: 1.0,
4522 });
4523
4524 cursor.y += line_height;
4525 }
4526
4527 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4529 if !line_elements.is_empty() {
4530 let container_height = cursor.content_y + cursor.y - container_start_y;
4531 cursor.elements.push(LayoutElement {
4532 x: text_x,
4533 y: container_start_y,
4534 width: text_width,
4535 height: container_height,
4536 draw: DrawCommand::None,
4537 children: line_elements,
4538 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4539 resolved_style: Some(style.clone()),
4540 source_location: source_location.cloned(),
4541 href: href.map(|s| s.to_string()),
4542 bookmark: if is_first_element {
4543 bookmark.map(|s| s.to_string())
4544 } else {
4545 None
4546 },
4547 alt: None,
4548 is_header_row: false,
4549 actual_text: None,
4550 list_numbering: None,
4551 col_span: 1,
4552 overflow: Overflow::default(),
4553 opacity: 1.0,
4554 });
4555 }
4556
4557 cursor.y += margin.bottom;
4558 }
4559
4560 #[allow(clippy::too_many_arguments)]
4562 #[allow(clippy::too_many_arguments)]
4563 fn layout_text_runs(
4564 &self,
4565 runs: &[TextRun],
4566 parent_href: Option<&str>,
4567 style: &ResolvedStyle,
4568 cursor: &mut PageCursor,
4569 pages: &mut Vec<LayoutPage>,
4570 text_x: f64,
4571 text_width: f64,
4572 font_context: &FontContext,
4573 source_location: Option<&SourceLocation>,
4574 bookmark: Option<&str>,
4575 node_type_override: Option<&str>,
4577 ) {
4578 let mut styled_chars: Vec<StyledChar> = Vec::new();
4580 for run in runs {
4581 let run_style = run.style.resolve(Some(style), text_width);
4582 let run_href = run.href.as_deref().or(parent_href);
4583 let transform = run_style.text_transform;
4584 let run_content = substitute_page_placeholders(&run.content);
4585 let mut prev_is_whitespace = true;
4586 for ch in run_content.chars() {
4587 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4588 prev_is_whitespace = ch.is_whitespace();
4589 styled_chars.push(StyledChar {
4590 ch: transformed_ch,
4591 font_family: run_style.font_family.clone(),
4592 font_size: run_style.font_size,
4593 font_weight: run_style.font_weight,
4594 font_style: run_style.font_style,
4595 color: run_style.color,
4596 href: run_href.map(|s| s.to_string()),
4597 text_decoration: run_style.text_decoration,
4598 letter_spacing: run_style.letter_spacing,
4599 });
4600 }
4601 }
4602
4603 let justify = matches!(style.text_align, TextAlign::Justify);
4605 let broken_lines = match style.line_breaking {
4606 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4607 font_context,
4608 &styled_chars,
4609 text_width,
4610 style.hyphens,
4611 style.lang.as_deref(),
4612 justify,
4613 ),
4614 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4615 font_context,
4616 &styled_chars,
4617 text_width,
4618 style.hyphens,
4619 style.lang.as_deref(),
4620 ),
4621 };
4622
4623 let broken_lines = match style.text_overflow {
4625 TextOverflow::Ellipsis => {
4626 self.text_layout
4627 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4628 }
4629 TextOverflow::Clip => {
4630 self.text_layout
4631 .truncate_runs_clip(font_context, broken_lines, text_width)
4632 }
4633 TextOverflow::Wrap => broken_lines,
4634 };
4635
4636 let line_height = style.font_size * style.line_height;
4637
4638 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4640 let decision = page_break::decide_break(
4641 cursor.remaining_height(),
4642 &line_heights,
4643 true,
4644 style.min_orphan_lines as usize,
4645 style.min_widow_lines as usize,
4646 );
4647
4648 let mut snapshot = cursor.elements.len();
4649 let mut container_start_y = cursor.content_y + cursor.y;
4650 let mut is_first_element = true;
4651
4652 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4653 pages.push(cursor.finalize());
4654 *cursor = cursor.new_page();
4655 snapshot = cursor.elements.len();
4656 container_start_y = cursor.content_y + cursor.y;
4657 }
4658
4659 let forced_break_at = match decision {
4660 page_break::BreakDecision::Split {
4661 items_on_current_page,
4662 } => Some(items_on_current_page),
4663 _ => None,
4664 };
4665 let mut first_break_done = false;
4666
4667 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4668 let needs_break = if let Some(break_at) = forced_break_at {
4669 if !first_break_done && line_idx == break_at {
4670 true
4671 } else {
4672 line_height > cursor.remaining_height()
4673 }
4674 } else {
4675 line_height > cursor.remaining_height()
4676 };
4677
4678 if needs_break {
4679 first_break_done = true;
4680 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4681 if !line_elements.is_empty() {
4682 let container_height = cursor.content_y + cursor.y - container_start_y;
4683 cursor.elements.push(LayoutElement {
4684 x: text_x,
4685 y: container_start_y,
4686 width: text_width,
4687 height: container_height,
4688 draw: DrawCommand::None,
4689 children: line_elements,
4690 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4691 resolved_style: Some(style.clone()),
4692 source_location: source_location.cloned(),
4693 href: parent_href.map(|s| s.to_string()),
4694 bookmark: if is_first_element {
4695 bookmark.map(|s| s.to_string())
4696 } else {
4697 None
4698 },
4699 alt: None,
4700 is_header_row: false,
4701 actual_text: None,
4702 list_numbering: None,
4703 col_span: 1,
4704 overflow: Overflow::default(),
4705 opacity: 1.0,
4706 });
4707 is_first_element = false;
4708 }
4709
4710 pages.push(cursor.finalize());
4711 *cursor = cursor.new_page();
4712
4713 snapshot = cursor.elements.len();
4714 container_start_y = cursor.content_y + cursor.y;
4715 }
4716
4717 let line_x = match style.text_align {
4718 TextAlign::Left => text_x,
4719 TextAlign::Right => text_x + text_width - run_line.width,
4720 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
4721 TextAlign::Justify => text_x,
4722 };
4723
4724 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
4725
4726 let is_last_line = line_idx == broken_lines.len() - 1;
4733 let user_ws = style.word_spacing;
4734 let (justified_width, word_spacing) =
4735 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4736 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4737 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4738 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4739 let s = glyphs[..=idx]
4740 .iter()
4741 .filter(|g| g.char_value == ' ')
4742 .count();
4743 (w, s)
4744 } else {
4745 (0.0, 0)
4746 };
4747 let slack = text_width - natural_width;
4748 let ws = if space_count > 0 && slack.abs() > 0.01 {
4749 slack / space_count as f64
4750 } else {
4751 0.0
4752 };
4753 (text_width, user_ws + ws)
4754 } else {
4755 (run_line.width, user_ws)
4756 };
4757
4758 let text_line = TextLine {
4759 x: line_x,
4760 y: cursor.content_y
4766 + cursor.y
4767 + baseline_in_line(
4768 line_height,
4769 style.font_size,
4770 font_context.baseline_metrics(
4771 &style.font_family,
4772 style.font_weight,
4773 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4774 ),
4775 ),
4776 glyphs,
4777 width: justified_width,
4778 height: line_height,
4779 word_spacing,
4780 };
4781
4782 let text_dec = run_line
4784 .chars
4785 .iter()
4786 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4787 .map(|sc| sc.text_decoration)
4788 .unwrap_or(style.text_decoration);
4789
4790 cursor.elements.push(LayoutElement {
4791 x: line_x,
4792 y: cursor.content_y + cursor.y,
4793 width: justified_width,
4794 height: line_height,
4795 draw: DrawCommand::Text {
4796 lines: vec![text_line],
4797 color: style.color,
4798 text_decoration: text_dec,
4799 opacity: 1.0,
4800 },
4801 children: vec![],
4802 node_type: Some("TextLine".to_string()),
4803 resolved_style: Some(style.clone()),
4804 source_location: None,
4805 href: parent_href.map(|s| s.to_string()),
4806 bookmark: None,
4807 alt: None,
4808 is_header_row: false,
4809 actual_text: None,
4810 list_numbering: None,
4811 col_span: 1,
4812 overflow: Overflow::default(),
4813 opacity: 1.0,
4814 });
4815
4816 cursor.y += line_height;
4817 }
4818
4819 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4820 if !line_elements.is_empty() {
4821 let container_height = cursor.content_y + cursor.y - container_start_y;
4822 cursor.elements.push(LayoutElement {
4823 x: text_x,
4824 y: container_start_y,
4825 width: text_width,
4826 height: container_height,
4827 draw: DrawCommand::None,
4828 children: line_elements,
4829 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4830 resolved_style: Some(style.clone()),
4831 source_location: source_location.cloned(),
4832 href: parent_href.map(|s| s.to_string()),
4833 bookmark: if is_first_element {
4834 bookmark.map(|s| s.to_string())
4835 } else {
4836 None
4837 },
4838 alt: None,
4839 is_header_row: false,
4840 actual_text: None,
4841 list_numbering: None,
4842 col_span: 1,
4843 overflow: Overflow::default(),
4844 opacity: 1.0,
4845 });
4846 }
4847 }
4848
4849 fn build_positioned_glyphs_single_style(
4853 &self,
4854 line: &BrokenLine,
4855 style: &ResolvedStyle,
4856 href: Option<&str>,
4857 font_context: &FontContext,
4858 ) -> Vec<PositionedGlyph> {
4859 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4860 let line_text: String = line.chars.iter().collect();
4861 let direction = style.direction;
4862 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4864
4865 let font_runs = crate::font::fallback::segment_by_font(
4868 &line.chars,
4869 &style.font_family,
4870 style.font_weight,
4871 italic,
4872 font_context.registry(),
4873 );
4874 let needs_per_char_fallback = font_runs.len() > 1
4875 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4876
4877 if needs_per_char_fallback {
4879 let bidi_runs = if has_bidi {
4880 bidi::analyze_bidi(&line_text, direction)
4881 } else {
4882 vec![crate::text::bidi::BidiRun {
4883 char_start: 0,
4884 char_end: line.chars.len(),
4885 level: unicode_bidi::Level::ltr(),
4886 is_rtl: false,
4887 }]
4888 };
4889
4890 let mut all_glyphs = Vec::new();
4891 let mut bidi_levels = Vec::new();
4892 let mut x = 0.0_f64;
4893
4894 for bidi_run in &bidi_runs {
4896 for font_run in &font_runs {
4898 let start = font_run.start.max(bidi_run.char_start);
4900 let end = font_run.end.min(bidi_run.char_end);
4901 if start >= end {
4902 continue;
4903 }
4904
4905 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4906 let sub_text: String = sub_chars.iter().collect();
4907 let resolved_family = &font_run.family;
4908
4909 if let Some(font_data) =
4910 font_context.font_data(resolved_family, style.font_weight, italic)
4911 {
4912 if let Some(shaped) = shaping::shape_text_with_direction(
4913 &sub_text,
4914 font_data,
4915 bidi_run.is_rtl,
4916 ) {
4917 let units_per_em = font_context.units_per_em(
4918 resolved_family,
4919 style.font_weight,
4920 italic,
4921 );
4922 let scale = style.font_size / units_per_em as f64;
4923
4924 for sg in &shaped {
4925 let cluster = sg.cluster as usize;
4926 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4927
4928 let cluster_text = if shaped.len() < sub_chars.len() {
4929 let cluster_end =
4930 self.find_cluster_end(&shaped, sg, sub_chars.len());
4931 if cluster_end > cluster + 1 {
4932 Some(
4933 sub_chars[cluster..cluster_end]
4934 .iter()
4935 .collect::<String>(),
4936 )
4937 } else {
4938 None
4939 }
4940 } else {
4941 None
4942 };
4943
4944 let glyph_x = x + sg.x_offset as f64 * scale;
4945 let glyph_y = sg.y_offset as f64 * scale;
4946 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4947
4948 all_glyphs.push(PositionedGlyph {
4949 glyph_id: sg.glyph_id,
4950 x_offset: glyph_x,
4951 y_offset: glyph_y,
4952 x_advance: advance,
4953 font_size: style.font_size,
4954 font_family: Arc::from(resolved_family.as_str()),
4955 font_weight: style.font_weight,
4956 font_style: style.font_style,
4957 char_value,
4958 color: Some(style.color),
4959 href: href.map(|s| s.to_string()),
4960 text_decoration: style.text_decoration,
4961 letter_spacing: style.letter_spacing,
4962 cluster_text,
4963 });
4964 bidi_levels.push(bidi_run.level);
4965 x += advance;
4966 }
4967 continue;
4968 }
4969 }
4970
4971 for i in start..end {
4973 let ch = line.chars[i];
4974 let glyph_x = x;
4975 let char_width = font_context.char_width(
4976 ch,
4977 resolved_family,
4978 style.font_weight,
4979 italic,
4980 style.font_size,
4981 );
4982 let advance = char_width + style.letter_spacing;
4983 all_glyphs.push(PositionedGlyph {
4984 glyph_id: ch as u16,
4985 x_offset: glyph_x,
4986 y_offset: 0.0,
4987 x_advance: advance,
4988 font_size: style.font_size,
4989 font_family: Arc::from(resolved_family.as_str()),
4990 font_weight: style.font_weight,
4991 font_style: style.font_style,
4992 char_value: ch,
4993 color: Some(style.color),
4994 href: href.map(|s| s.to_string()),
4995 text_decoration: style.text_decoration,
4996 letter_spacing: style.letter_spacing,
4997 cluster_text: None,
4998 });
4999 bidi_levels.push(bidi_run.level);
5000 x += advance;
5001 }
5002 }
5003 }
5004
5005 if has_bidi && !all_glyphs.is_empty() {
5007 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5008 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
5009 }
5010 return all_glyphs;
5011 }
5012
5013 if let Some(font_data) =
5016 font_context.font_data(&style.font_family, style.font_weight, italic)
5017 {
5018 if has_bidi {
5019 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5021 let units_per_em =
5022 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5023 let scale = style.font_size / units_per_em as f64;
5024
5025 let mut all_glyphs = Vec::new();
5026 let mut bidi_levels = Vec::new();
5027 let mut x = 0.0_f64;
5028
5029 for run in &bidi_runs {
5030 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5031 let run_text: String = run_chars.iter().collect();
5032
5033 if let Some(shaped) =
5034 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5035 {
5036 for sg in &shaped {
5037 let cluster = sg.cluster as usize;
5038 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5039
5040 let cluster_text = if shaped.len() < run_chars.len() {
5041 let cluster_end =
5042 self.find_cluster_end(&shaped, sg, run_chars.len());
5043 if cluster_end > cluster + 1 {
5044 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
5045 } else {
5046 None
5047 }
5048 } else {
5049 None
5050 };
5051
5052 let glyph_x = x + sg.x_offset as f64 * scale;
5053 let glyph_y = sg.y_offset as f64 * scale;
5054 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5055
5056 all_glyphs.push(PositionedGlyph {
5057 glyph_id: sg.glyph_id,
5058 x_offset: glyph_x,
5059 y_offset: glyph_y,
5060 x_advance: advance,
5061 font_size: style.font_size,
5062 font_family: Arc::from(style.font_family.as_str()),
5063 font_weight: style.font_weight,
5064 font_style: style.font_style,
5065 char_value,
5066 color: Some(style.color),
5067 href: href.map(|s| s.to_string()),
5068 text_decoration: style.text_decoration,
5069 letter_spacing: style.letter_spacing,
5070 cluster_text,
5071 });
5072 bidi_levels.push(run.level);
5073
5074 x += advance;
5075 }
5076 }
5077 }
5078
5079 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5081 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5082 return glyphs;
5083 }
5084
5085 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5087 let units_per_em =
5088 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5089 let scale = style.font_size / units_per_em as f64;
5090
5091 return self.shaped_glyphs_to_positioned(
5092 &shaped,
5093 &line.chars,
5094 &line.char_positions,
5095 scale,
5096 style.font_size,
5097 &style.font_family,
5098 style.font_weight,
5099 style.font_style,
5100 Some(style.color),
5101 href,
5102 style.text_decoration,
5103 style.letter_spacing,
5104 );
5105 }
5106 }
5107
5108 let mut glyphs: Vec<PositionedGlyph> = line
5110 .chars
5111 .iter()
5112 .enumerate()
5113 .map(|(j, ch)| {
5114 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5115 let char_width = font_context.char_width(
5116 *ch,
5117 &style.font_family,
5118 style.font_weight,
5119 italic,
5120 style.font_size,
5121 );
5122 PositionedGlyph {
5123 glyph_id: *ch as u16,
5124 x_offset: glyph_x,
5125 y_offset: 0.0,
5126 x_advance: char_width,
5127 font_size: style.font_size,
5128 font_family: Arc::from(style.font_family.as_str()),
5129 font_weight: style.font_weight,
5130 font_style: style.font_style,
5131 char_value: *ch,
5132 color: Some(style.color),
5133 href: href.map(|s| s.to_string()),
5134 text_decoration: style.text_decoration,
5135 letter_spacing: style.letter_spacing,
5136 cluster_text: None,
5137 }
5138 })
5139 .collect();
5140
5141 if has_bidi && !glyphs.is_empty() {
5143 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5144 let mut levels = Vec::with_capacity(glyphs.len());
5145 let mut char_idx = 0;
5146 for run in &bidi_runs {
5147 for _ in run.char_start..run.char_end {
5148 if char_idx < glyphs.len() {
5149 levels.push(run.level);
5150 char_idx += 1;
5151 }
5152 }
5153 }
5154 while levels.len() < glyphs.len() {
5156 levels.push(unicode_bidi::Level::ltr());
5157 }
5158 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
5159 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5160 }
5161
5162 glyphs
5163 }
5164
5165 fn build_positioned_glyphs_runs(
5170 &self,
5171 run_line: &RunBrokenLine,
5172 font_context: &FontContext,
5173 direction: Direction,
5174 ) -> Vec<PositionedGlyph> {
5175 let chars = &run_line.chars;
5176 if chars.is_empty() {
5177 return vec![];
5178 }
5179
5180 let resolved_families: Vec<String> = chars
5189 .iter()
5190 .map(|sc| {
5191 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5192 if !sc.font_family.contains(',') {
5193 let primary =
5194 font_context
5195 .registry()
5196 .resolve(&sc.font_family, sc.font_weight, italic);
5197 if sc.ch.is_whitespace()
5198 || sc.ch == PAGE_NUMBER_SENTINEL
5199 || sc.ch == TOTAL_PAGES_SENTINEL
5200 || primary.has_char(sc.ch)
5201 {
5202 return sc.font_family.clone();
5203 }
5204 }
5205 let (_, family) = font_context.registry().resolve_for_char(
5206 &sc.font_family,
5207 sc.ch,
5208 sc.font_weight,
5209 italic,
5210 );
5211 family
5212 })
5213 .collect();
5214
5215 let line_text: String = chars.iter().map(|c| c.ch).collect();
5216 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5217 let bidi_runs = if has_bidi {
5218 Some(bidi::analyze_bidi(&line_text, direction))
5219 } else {
5220 None
5221 };
5222
5223 let mut glyphs = Vec::new();
5224 let mut bidi_levels = Vec::new();
5225 let mut i = 0;
5226
5227 while i < chars.len() {
5228 let sc = &chars[i];
5229 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5230 let resolved_family = &resolved_families[i];
5231
5232 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5234 runs.iter()
5235 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5236 });
5237
5238 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5240 {
5241 let run_start = i;
5243 let mut run_end = i + 1;
5244 while run_end < chars.len() {
5245 let next = &chars[run_end];
5246 let next_italic =
5247 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5248 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5249 runs.iter()
5250 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5251 });
5252 if resolved_families[run_end] == *resolved_family
5254 && next.font_weight == sc.font_weight
5255 && next_italic == italic
5256 && (next.font_size - sc.font_size).abs() < 0.001
5257 && next_is_rtl == is_rtl
5258 {
5259 run_end += 1;
5260 } else {
5261 break;
5262 }
5263 }
5264
5265 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5266 if let Some(shaped) =
5267 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5268 {
5269 let units_per_em =
5270 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5271 let scale = sc.font_size / units_per_em as f64;
5272
5273 let run_chars: Vec<char> =
5275 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5276 let run_positions: Vec<f64> = (run_start..run_end)
5277 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5278 .collect();
5279
5280 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5282 &shaped,
5283 &chars[run_start..run_end],
5284 &run_chars,
5285 &run_positions,
5286 scale,
5287 );
5288 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5290 for g in &mut run_glyphs {
5291 g.font_family = resolved_family_arc.clone();
5292 }
5293 let run_level = if is_rtl {
5295 unicode_bidi::Level::rtl()
5296 } else {
5297 unicode_bidi::Level::ltr()
5298 };
5299 for _ in &run_glyphs {
5300 bidi_levels.push(run_level);
5301 }
5302 glyphs.extend(run_glyphs);
5303 i = run_end;
5304 continue;
5305 }
5306 }
5307
5308 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5310 let char_width = font_context.char_width(
5311 sc.ch,
5312 resolved_family,
5313 sc.font_weight,
5314 italic,
5315 sc.font_size,
5316 );
5317 glyphs.push(PositionedGlyph {
5318 glyph_id: sc.ch as u16,
5319 x_offset: glyph_x,
5320 y_offset: 0.0,
5321 x_advance: char_width,
5322 font_size: sc.font_size,
5323 font_family: Arc::from(resolved_family.as_str()),
5324 font_weight: sc.font_weight,
5325 font_style: sc.font_style,
5326 char_value: sc.ch,
5327 color: Some(sc.color),
5328 href: sc.href.clone(),
5329 text_decoration: sc.text_decoration,
5330 letter_spacing: sc.letter_spacing,
5331 cluster_text: None,
5332 });
5333 bidi_levels.push(if is_rtl {
5334 unicode_bidi::Level::rtl()
5335 } else {
5336 unicode_bidi::Level::ltr()
5337 });
5338 i += 1;
5339 }
5340
5341 if has_bidi && !glyphs.is_empty() {
5343 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
5344 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5345 }
5346
5347 glyphs
5348 }
5349
5350 #[allow(clippy::too_many_arguments)]
5352 fn shaped_glyphs_to_positioned(
5353 &self,
5354 shaped: &[shaping::ShapedGlyph],
5355 chars: &[char],
5356 _char_positions: &[f64],
5357 scale: f64,
5358 font_size: f64,
5359 font_family: &str,
5360 font_weight: u32,
5361 font_style: FontStyle,
5362 color: Option<Color>,
5363 href: Option<&str>,
5364 text_decoration: TextDecoration,
5365 letter_spacing: f64,
5366 ) -> Vec<PositionedGlyph> {
5367 let mut result = Vec::with_capacity(shaped.len());
5368 let mut x = 0.0_f64;
5369
5370 for sg in shaped {
5371 let cluster = sg.cluster as usize;
5372 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5373
5374 let cluster_text = if shaped.len() < chars.len() {
5376 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5379 if cluster_end > cluster + 1 {
5380 Some(chars[cluster..cluster_end].iter().collect::<String>())
5381 } else {
5382 None
5383 }
5384 } else {
5385 None
5386 };
5387
5388 let glyph_x = x + sg.x_offset as f64 * scale;
5390 let glyph_y = sg.y_offset as f64 * scale;
5391 let advance = sg.x_advance as f64 * scale + letter_spacing;
5392
5393 result.push(PositionedGlyph {
5394 glyph_id: sg.glyph_id,
5395 x_offset: glyph_x,
5396 y_offset: glyph_y,
5397 x_advance: advance,
5398 font_size,
5399 font_family: Arc::from(font_family),
5400 font_weight,
5401 font_style,
5402 char_value,
5403 color,
5404 href: href.map(|s| s.to_string()),
5405 text_decoration,
5406 letter_spacing,
5407 cluster_text,
5408 });
5409
5410 x += advance;
5411 }
5412
5413 result
5414 }
5415
5416 fn shaped_glyphs_to_positioned_runs(
5418 &self,
5419 shaped: &[shaping::ShapedGlyph],
5420 styled_chars: &[StyledChar],
5421 chars: &[char],
5422 char_positions: &[f64],
5423 scale: f64,
5424 ) -> Vec<PositionedGlyph> {
5425 let mut result = Vec::with_capacity(shaped.len());
5426 let base_x = char_positions.first().copied().unwrap_or(0.0);
5428 let mut x = 0.0_f64;
5429
5430 for sg in shaped {
5431 let cluster = sg.cluster as usize;
5432 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5433 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5434
5435 let cluster_text = if shaped.len() < chars.len() {
5436 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5437 if cluster_end > cluster + 1 {
5438 Some(chars[cluster..cluster_end].iter().collect::<String>())
5439 } else {
5440 None
5441 }
5442 } else {
5443 None
5444 };
5445
5446 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5447 let glyph_y = sg.y_offset as f64 * scale;
5448 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5449
5450 result.push(PositionedGlyph {
5451 glyph_id: sg.glyph_id,
5452 x_offset: glyph_x,
5453 y_offset: glyph_y,
5454 x_advance: advance,
5455 font_size: sc.font_size,
5456 font_family: Arc::from(sc.font_family.as_str()),
5457 font_weight: sc.font_weight,
5458 font_style: sc.font_style,
5459 char_value,
5460 color: Some(sc.color),
5461 href: sc.href.clone(),
5462 text_decoration: sc.text_decoration,
5463 letter_spacing: sc.letter_spacing,
5464 cluster_text,
5465 });
5466
5467 x += advance;
5468 }
5469
5470 result
5471 }
5472
5473 fn find_cluster_end(
5475 &self,
5476 shaped: &[shaping::ShapedGlyph],
5477 current: &shaping::ShapedGlyph,
5478 num_chars: usize,
5479 ) -> usize {
5480 for sg in shaped {
5482 if sg.cluster > current.cluster {
5483 return sg.cluster as usize;
5484 }
5485 }
5486 num_chars
5488 }
5489
5490 fn image_display_size(
5500 &self,
5501 src: &str,
5502 style: &ResolvedStyle,
5503 explicit_width: Option<f64>,
5504 explicit_height: Option<f64>,
5505 available_width: f64,
5506 ) -> (f64, f64) {
5507 let dims = if src.is_empty() {
5508 None
5509 } else {
5510 self.get_image_dimensions(src)
5511 };
5512 let aspect = dims
5513 .map(|(w, h)| {
5514 if w > 0 {
5515 f64::from(h) / f64::from(w)
5516 } else {
5517 0.75
5518 }
5519 })
5520 .unwrap_or(0.75);
5521
5522 let style_w = match style.width {
5523 SizeConstraint::Fixed(w) => Some(w),
5524 SizeConstraint::Auto => None,
5525 };
5526 let style_h = match style.height {
5527 SizeConstraint::Fixed(h) => Some(h),
5528 SizeConstraint::Auto => None,
5529 };
5530 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5531
5532 let width_source = style_w.or(explicit_width);
5533 let height_source = style_h.or(explicit_height);
5534 match (width_source, height_source) {
5535 (Some(w), Some(h)) => (clamp(w), h),
5536 (Some(w), None) => {
5537 let w = clamp(w);
5538 (w, w * aspect)
5539 }
5540 (None, Some(h)) => (clamp(h / aspect), h),
5541 (None, None) => {
5542 let w = clamp(
5545 dims.map(|(w, _)| f64::from(w))
5546 .unwrap_or(available_width)
5547 .min(available_width),
5548 );
5549 (w, w * aspect)
5550 }
5551 }
5552 }
5553
5554 #[allow(clippy::too_many_arguments)]
5555 fn layout_image(
5556 &self,
5557 node: &Node,
5558 style: &ResolvedStyle,
5559 cursor: &mut PageCursor,
5560 pages: &mut Vec<LayoutPage>,
5561 x: f64,
5562 available_width: f64,
5563 explicit_width: Option<f64>,
5564 explicit_height: Option<f64>,
5565 ) {
5566 let margin = &style.margin.to_edges();
5567
5568 let src = match &node.kind {
5570 NodeKind::Image { src, .. } => src.as_str(),
5571 _ => "",
5572 };
5573
5574 let loaded = if !src.is_empty() {
5575 crate::image_loader::load_image(src).ok()
5576 } else {
5577 None
5578 };
5579
5580 let (img_width, img_height) = self.image_display_size(
5581 src,
5582 style,
5583 explicit_width,
5584 explicit_height,
5585 available_width - margin.horizontal(),
5586 );
5587
5588 let total_height = img_height + margin.vertical();
5589
5590 if total_height > cursor.remaining_height() {
5591 pages.push(cursor.finalize());
5592 *cursor = cursor.new_page();
5593 }
5594
5595 cursor.y += margin.top;
5596
5597 let draw = if let Some(image_data) = loaded {
5598 DrawCommand::Image { image_data }
5599 } else {
5600 DrawCommand::ImagePlaceholder
5601 };
5602
5603 cursor.elements.push(LayoutElement {
5604 x: x + margin.left,
5605 y: cursor.content_y + cursor.y,
5606 width: img_width,
5607 height: img_height,
5608 draw,
5609 children: vec![],
5610 node_type: Some(node_kind_name(&node.kind).to_string()),
5611 resolved_style: Some(style.clone()),
5612 source_location: node.source_location.clone(),
5613 href: node.href.clone(),
5614 bookmark: node.bookmark.clone(),
5615 alt: node.alt.clone(),
5616 is_header_row: false,
5617 actual_text: None,
5618 list_numbering: None,
5619 col_span: 1,
5620 overflow: style.overflow,
5621 opacity: style.opacity,
5622 });
5623
5624 cursor.y += img_height + margin.bottom;
5625 }
5626
5627 #[allow(clippy::too_many_arguments)]
5629 fn layout_svg(
5630 &self,
5631 node: &Node,
5632 style: &ResolvedStyle,
5633 cursor: &mut PageCursor,
5634 pages: &mut Vec<LayoutPage>,
5635 x: f64,
5636 _available_width: f64,
5637 svg_width: f64,
5638 svg_height: f64,
5639 view_box: Option<&str>,
5640 content: &str,
5641 ) {
5642 let margin = &style.margin.to_edges();
5643 let total_height = svg_height + margin.vertical();
5644
5645 if total_height > cursor.remaining_height() {
5646 pages.push(cursor.finalize());
5647 *cursor = cursor.new_page();
5648 }
5649
5650 cursor.y += margin.top;
5651
5652 let vb = view_box
5653 .and_then(crate::svg::parse_view_box)
5654 .unwrap_or(crate::svg::ViewBox {
5655 min_x: 0.0,
5656 min_y: 0.0,
5657 width: svg_width,
5658 height: svg_height,
5659 });
5660
5661 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5662
5663 cursor.elements.push(LayoutElement {
5664 x: x + margin.left,
5665 y: cursor.content_y + cursor.y,
5666 width: svg_width,
5667 height: svg_height,
5668 draw: DrawCommand::Svg {
5669 commands,
5670 width: svg_width,
5671 height: svg_height,
5672 viewbox_min_x: vb.min_x,
5673 viewbox_min_y: vb.min_y,
5674 viewbox_width: vb.width,
5675 viewbox_height: vb.height,
5676 clip: false,
5677 },
5678 children: vec![],
5679 node_type: Some("Svg".to_string()),
5680 resolved_style: Some(style.clone()),
5681 source_location: node.source_location.clone(),
5682 href: node.href.clone(),
5683 bookmark: node.bookmark.clone(),
5684 alt: node.alt.clone(),
5685 is_header_row: false,
5686 actual_text: None,
5687 list_numbering: None,
5688 col_span: 1,
5689 overflow: style.overflow,
5690 opacity: style.opacity,
5691 });
5692
5693 cursor.y += svg_height + margin.bottom;
5694 }
5695
5696 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
5698 use crate::svg::SvgCommand;
5699
5700 let mut commands = Vec::new();
5701 let mut cur_x = 0.0_f64;
5702 let mut cur_y = 0.0_f64;
5703
5704 for op in operations {
5705 match op {
5706 CanvasOp::MoveTo { x, y } => {
5707 commands.push(SvgCommand::MoveTo(*x, *y));
5708 cur_x = *x;
5709 cur_y = *y;
5710 }
5711 CanvasOp::LineTo { x, y } => {
5712 commands.push(SvgCommand::LineTo(*x, *y));
5713 cur_x = *x;
5714 cur_y = *y;
5715 }
5716 CanvasOp::BezierCurveTo {
5717 cp1x,
5718 cp1y,
5719 cp2x,
5720 cp2y,
5721 x,
5722 y,
5723 } => {
5724 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
5725 cur_x = *x;
5726 cur_y = *y;
5727 }
5728 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
5729 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
5731 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
5732 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
5733 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
5734 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
5735 cur_x = *x;
5736 cur_y = *y;
5737 }
5738 CanvasOp::ClosePath => {
5739 commands.push(SvgCommand::ClosePath);
5740 }
5741 CanvasOp::Rect {
5742 x,
5743 y,
5744 width,
5745 height,
5746 } => {
5747 commands.push(SvgCommand::MoveTo(*x, *y));
5748 commands.push(SvgCommand::LineTo(*x + *width, *y));
5749 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
5750 commands.push(SvgCommand::LineTo(*x, *y + *height));
5751 commands.push(SvgCommand::ClosePath);
5752 cur_x = *x;
5753 cur_y = *y;
5754 }
5755 CanvasOp::Circle { cx, cy, r } => {
5756 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
5757 }
5758 CanvasOp::Ellipse { cx, cy, rx, ry } => {
5759 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
5760 }
5761 CanvasOp::Arc {
5762 cx,
5763 cy,
5764 r,
5765 start_angle,
5766 end_angle,
5767 counterclockwise,
5768 } => {
5769 let steps = 32;
5773 let mut sweep = end_angle - start_angle;
5774 if !counterclockwise && sweep < 0.0 {
5775 sweep += 2.0 * std::f64::consts::PI;
5776 }
5777 if *counterclockwise && sweep > 0.0 {
5778 sweep -= 2.0 * std::f64::consts::PI;
5779 }
5780 for i in 0..=steps {
5781 let t = *start_angle + sweep * (i as f64 / steps as f64);
5782 let px = cx + r * t.cos();
5783 let py = cy + r * t.sin();
5784 if i == 0 {
5785 commands.push(SvgCommand::MoveTo(px, py));
5786 } else {
5787 commands.push(SvgCommand::LineTo(px, py));
5788 }
5789 }
5790 }
5791 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
5792 CanvasOp::Fill => commands.push(SvgCommand::Fill),
5793 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
5794 CanvasOp::SetFillColor { r, g, b } => {
5795 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
5797 }
5798 CanvasOp::SetStrokeColor { r, g, b } => {
5799 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
5800 }
5801 CanvasOp::SetLineWidth { width } => {
5802 commands.push(SvgCommand::SetStrokeWidth(*width));
5803 }
5804 CanvasOp::SetLineCap { cap } => {
5805 commands.push(SvgCommand::SetLineCap(*cap));
5806 }
5807 CanvasOp::SetLineJoin { join } => {
5808 commands.push(SvgCommand::SetLineJoin(*join));
5809 }
5810 CanvasOp::Save => commands.push(SvgCommand::SaveState),
5811 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
5812 }
5813 }
5814
5815 commands
5816 }
5817
5818 #[allow(clippy::too_many_arguments)]
5820 fn layout_canvas(
5821 &self,
5822 node: &Node,
5823 style: &ResolvedStyle,
5824 cursor: &mut PageCursor,
5825 pages: &mut Vec<LayoutPage>,
5826 x: f64,
5827 _available_width: f64,
5828 canvas_width: f64,
5829 canvas_height: f64,
5830 operations: &[CanvasOp],
5831 ) {
5832 let margin = style.margin.to_edges();
5833 let total_height = canvas_height + margin.top + margin.bottom;
5834
5835 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5837 pages.push(cursor.finalize());
5838 *cursor = cursor.new_page();
5839 }
5840
5841 cursor.y += margin.top;
5842
5843 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5844
5845 cursor.elements.push(LayoutElement {
5846 x: x + margin.left,
5847 y: cursor.content_y + cursor.y,
5848 width: canvas_width,
5849 height: canvas_height,
5850 draw: DrawCommand::Svg {
5851 commands: svg_commands,
5852 width: canvas_width,
5853 height: canvas_height,
5854 viewbox_min_x: 0.0,
5857 viewbox_min_y: 0.0,
5858 viewbox_width: canvas_width,
5859 viewbox_height: canvas_height,
5860 clip: true,
5861 },
5862 children: vec![],
5863 node_type: Some("Canvas".to_string()),
5864 resolved_style: Some(style.clone()),
5865 source_location: node.source_location.clone(),
5866 href: node.href.clone(),
5867 bookmark: node.bookmark.clone(),
5868 alt: node.alt.clone(),
5869 is_header_row: false,
5870 actual_text: None,
5871 list_numbering: None,
5872 col_span: 1,
5873 overflow: style.overflow,
5874 opacity: style.opacity,
5875 });
5876
5877 cursor.y += canvas_height + margin.bottom;
5878 }
5879
5880 #[allow(clippy::too_many_arguments)]
5882 #[allow(clippy::too_many_arguments)]
5884 fn layout_chart(
5885 &self,
5886 node: &Node,
5887 style: &ResolvedStyle,
5888 cursor: &mut PageCursor,
5889 pages: &mut Vec<LayoutPage>,
5890 x: f64,
5891 chart_width: f64,
5892 chart_height: f64,
5893 primitives: Vec<crate::chart::ChartPrimitive>,
5894 node_type_name: &str,
5895 ) {
5896 let margin = &style.margin.to_edges();
5897 let total_height = chart_height + margin.vertical();
5898
5899 if total_height > cursor.remaining_height() {
5900 pages.push(cursor.finalize());
5901 *cursor = cursor.new_page();
5902 }
5903
5904 cursor.y += margin.top;
5905
5906 let draw = DrawCommand::Chart { primitives };
5907
5908 cursor.elements.push(LayoutElement {
5909 x: x + margin.left,
5910 y: cursor.content_y + cursor.y,
5911 width: chart_width,
5912 height: chart_height,
5913 draw,
5914 children: vec![],
5915 node_type: Some(node_type_name.to_string()),
5916 resolved_style: Some(style.clone()),
5917 source_location: node.source_location.clone(),
5918 href: node.href.clone(),
5919 bookmark: node.bookmark.clone(),
5920 alt: node.alt.clone(),
5921 is_header_row: false,
5922 actual_text: None,
5923 list_numbering: None,
5924 col_span: 1,
5925 overflow: style.overflow,
5926 opacity: style.opacity,
5927 });
5928
5929 cursor.y += chart_height + margin.bottom;
5930 }
5931
5932 #[allow(clippy::too_many_arguments)]
5934 fn layout_form_field(
5935 &self,
5936 node: &Node,
5937 style: &ResolvedStyle,
5938 cursor: &mut PageCursor,
5939 pages: &mut Vec<LayoutPage>,
5940 x: f64,
5941 field_width: f64,
5942 field_height: f64,
5943 draw: DrawCommand,
5944 node_type_name: &str,
5945 ) {
5946 let margin = &style.margin.to_edges();
5947 let total_height = field_height + margin.vertical();
5948
5949 if total_height > cursor.remaining_height() {
5950 pages.push(cursor.finalize());
5951 *cursor = cursor.new_page();
5952 }
5953
5954 cursor.y += margin.top;
5955
5956 cursor.elements.push(LayoutElement {
5957 x: x + margin.left,
5958 y: cursor.content_y + cursor.y,
5959 width: field_width,
5960 height: field_height,
5961 draw,
5962 children: vec![],
5963 node_type: Some(node_type_name.to_string()),
5964 resolved_style: Some(style.clone()),
5965 source_location: node.source_location.clone(),
5966 href: node.href.clone(),
5967 bookmark: node.bookmark.clone(),
5968 alt: node.alt.clone(),
5969 is_header_row: false,
5970 actual_text: None,
5971 list_numbering: None,
5972 col_span: 1,
5973 overflow: style.overflow,
5974 opacity: style.opacity,
5975 });
5976
5977 cursor.y += field_height + margin.bottom;
5978 }
5979
5980 #[allow(clippy::too_many_arguments)]
5981 fn layout_barcode(
5982 &self,
5983 node: &Node,
5984 style: &ResolvedStyle,
5985 cursor: &mut PageCursor,
5986 pages: &mut Vec<LayoutPage>,
5987 x: f64,
5988 available_width: f64,
5989 data: &str,
5990 format: crate::barcode::BarcodeFormat,
5991 explicit_width: Option<f64>,
5992 bar_height: f64,
5993 ) {
5994 let margin = &style.margin.to_edges();
5995 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5996 let total_height = bar_height + margin.vertical();
5997
5998 if total_height > cursor.remaining_height() {
5999 pages.push(cursor.finalize());
6000 *cursor = cursor.new_page();
6001 }
6002
6003 cursor.y += margin.top;
6004
6005 let draw = match crate::barcode::generate_barcode(data, format) {
6006 Ok(barcode_data) => {
6007 let bar_width = if barcode_data.bars.is_empty() {
6008 0.0
6009 } else {
6010 display_width / barcode_data.bars.len() as f64
6011 };
6012 DrawCommand::Barcode {
6013 bars: barcode_data.bars,
6014 bar_width,
6015 height: bar_height,
6016 color: style.color,
6017 }
6018 }
6019 Err(_) => DrawCommand::None,
6020 };
6021
6022 cursor.elements.push(LayoutElement {
6023 x: x + margin.left,
6024 y: cursor.content_y + cursor.y,
6025 width: display_width,
6026 height: bar_height,
6027 draw,
6028 children: vec![],
6029 node_type: Some("Barcode".to_string()),
6030 resolved_style: Some(style.clone()),
6031 source_location: node.source_location.clone(),
6032 href: node.href.clone(),
6033 bookmark: node.bookmark.clone(),
6034 alt: node.alt.clone(),
6035 is_header_row: false,
6036 actual_text: Some(data.to_string()),
6037 list_numbering: None,
6038 col_span: 1,
6039 overflow: style.overflow,
6040 opacity: style.opacity,
6041 });
6042
6043 cursor.y += bar_height + margin.bottom;
6044 }
6045
6046 #[allow(clippy::too_many_arguments)]
6048 fn layout_qrcode(
6049 &self,
6050 node: &Node,
6051 style: &ResolvedStyle,
6052 cursor: &mut PageCursor,
6053 pages: &mut Vec<LayoutPage>,
6054 x: f64,
6055 available_width: f64,
6056 data: &str,
6057 explicit_size: Option<f64>,
6058 ) {
6059 let margin = &style.margin.to_edges();
6060 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6061 let total_height = display_size + margin.vertical();
6062
6063 if total_height > cursor.remaining_height() {
6064 pages.push(cursor.finalize());
6065 *cursor = cursor.new_page();
6066 }
6067
6068 cursor.y += margin.top;
6069
6070 let draw = match crate::qrcode::generate_qr(data) {
6071 Ok(matrix) => {
6072 let module_size = display_size / matrix.size as f64;
6073 DrawCommand::QrCode {
6074 modules: matrix.modules,
6075 module_size,
6076 color: style.color,
6077 }
6078 }
6079 Err(_) => DrawCommand::None,
6080 };
6081
6082 cursor.elements.push(LayoutElement {
6083 x: x + margin.left,
6084 y: cursor.content_y + cursor.y,
6085 width: display_size,
6086 height: display_size,
6087 draw,
6088 children: vec![],
6089 node_type: Some("QrCode".to_string()),
6090 resolved_style: Some(style.clone()),
6091 source_location: node.source_location.clone(),
6092 href: node.href.clone(),
6093 bookmark: node.bookmark.clone(),
6094 alt: node.alt.clone(),
6095 is_header_row: false,
6096 actual_text: Some(data.to_string()),
6097 list_numbering: None,
6098 col_span: 1,
6099 overflow: style.overflow,
6100 opacity: style.opacity,
6101 });
6102
6103 cursor.y += display_size + margin.bottom;
6104 }
6105
6106 fn measure_node_height(
6109 &self,
6110 node: &Node,
6111 available_width: f64,
6112 style: &ResolvedStyle,
6113 font_context: &FontContext,
6114 ) -> f64 {
6115 match &node.kind {
6116 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6121 let measure_width = match style.width {
6124 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6125 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6126 };
6127 if !runs.is_empty() {
6134 let mut styled_chars: Vec<StyledChar> = Vec::new();
6135 for run in runs {
6136 let run_style = run.style.resolve(Some(style), measure_width);
6137 let transform = run_style.text_transform;
6138 let run_content = substitute_page_placeholders(&run.content);
6139 let mut prev_is_whitespace = true;
6140 for ch in run_content.chars() {
6141 let transformed_ch =
6142 apply_char_transform(ch, transform, prev_is_whitespace);
6143 prev_is_whitespace = ch.is_whitespace();
6144 styled_chars.push(StyledChar {
6145 ch: transformed_ch,
6146 font_family: run_style.font_family.clone(),
6147 font_size: run_style.font_size,
6148 font_weight: run_style.font_weight,
6149 font_style: run_style.font_style,
6150 color: run_style.color,
6151 href: None,
6152 text_decoration: run_style.text_decoration,
6153 letter_spacing: run_style.letter_spacing,
6154 });
6155 }
6156 }
6157 let justify = matches!(style.text_align, TextAlign::Justify);
6158 let broken_lines = match style.line_breaking {
6159 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6160 font_context,
6161 &styled_chars,
6162 measure_width,
6163 style.hyphens,
6164 style.lang.as_deref(),
6165 justify,
6166 ),
6167 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6168 font_context,
6169 &styled_chars,
6170 measure_width,
6171 style.hyphens,
6172 style.lang.as_deref(),
6173 ),
6174 };
6175 let line_height = style.font_size * style.line_height;
6176 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6177 } else {
6178 let content = substitute_page_placeholders(content);
6179 let transformed = apply_text_transform(&content, style.text_transform);
6180 let justify = matches!(style.text_align, TextAlign::Justify);
6181 let lines = match style.line_breaking {
6182 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6183 font_context,
6184 &transformed,
6185 measure_width,
6186 style.font_size,
6187 &style.font_family,
6188 style.font_weight,
6189 style.font_style,
6190 style.letter_spacing,
6191 style.hyphens,
6192 style.lang.as_deref(),
6193 justify,
6194 ),
6195 LineBreaking::Greedy => self.text_layout.break_into_lines(
6196 font_context,
6197 &transformed,
6198 measure_width,
6199 style.font_size,
6200 &style.font_family,
6201 style.font_weight,
6202 style.font_style,
6203 style.letter_spacing,
6204 style.hyphens,
6205 style.lang.as_deref(),
6206 ),
6207 };
6208 let line_height = style.font_size * style.line_height;
6209 (lines.len() as f64) * line_height + style.padding.vertical()
6210 }
6211 }
6212 NodeKind::Image {
6213 src,
6214 width: explicit_w,
6215 height: explicit_h,
6216 } => {
6217 let (_, h) = self.image_display_size(
6219 src,
6220 style,
6221 *explicit_w,
6222 *explicit_h,
6223 available_width - style.margin.horizontal(),
6224 );
6225 h + style.padding.vertical()
6226 }
6227 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6228 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6229 NodeKind::QrCode { size, .. } => {
6230 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6231 display_size + style.margin.vertical()
6232 }
6233 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6234 NodeKind::BarChart { height, .. }
6235 | NodeKind::LineChart { height, .. }
6236 | NodeKind::PieChart { height, .. }
6237 | NodeKind::AreaChart { height, .. }
6238 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6239 NodeKind::TextField { height, .. }
6240 | NodeKind::Checkbox { height, .. }
6241 | NodeKind::Dropdown { height, .. }
6242 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6243 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6245 if let SizeConstraint::Fixed(h) = style.height {
6252 return h;
6253 }
6254 let outer_width = match style.width {
6255 SizeConstraint::Fixed(w) => w,
6256 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6257 };
6258 let inner_width =
6259 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6260 let col_widths = self.resolve_column_widths(
6261 columns,
6262 inner_width,
6263 &node.children,
6264 style,
6265 font_context,
6266 );
6267 let row_gap = style.row_gap;
6268 let offsets = Self::table_column_offsets(&node.children);
6269 let mut total = 0.0;
6270 for (i, row) in node.children.iter().enumerate() {
6271 if i > 0 {
6272 total += row_gap;
6273 }
6274 total += self.measure_table_row_height(
6275 row,
6276 &col_widths,
6277 &offsets[i],
6278 style,
6279 font_context,
6280 );
6281 }
6282 total + style.padding.vertical() + style.border_width.vertical()
6283 }
6284 NodeKind::TableRow { .. } => {
6285 let n = node.children.len().max(1);
6290 let usable = (available_width - style.margin.horizontal()).max(0.0);
6291 let col_w = usable / n as f64;
6292 let col_widths = vec![col_w; n];
6293 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6294 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6295 }
6296 _ => {
6297 if let SizeConstraint::Fixed(h) = style.height {
6299 return h;
6300 }
6301 let outer_width = match style.width {
6305 SizeConstraint::Fixed(w) => w,
6306 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6307 }
6308 .min(style.max_width)
6309 .max(style.min_width);
6310 let inner_width =
6311 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6312 let children_height =
6313 self.measure_children_height(&node.children, inner_width, style, font_context);
6314 (children_height + style.padding.vertical() + style.border_width.vertical())
6315 .max(style.min_height)
6316 }
6317 }
6318 }
6319
6320 fn measure_children_height(
6321 &self,
6322 children: &[Node],
6323 available_width: f64,
6324 parent_style: &ResolvedStyle,
6325 font_context: &FontContext,
6326 ) -> f64 {
6327 if children
6333 .iter()
6334 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6335 {
6336 let flow: Vec<Node> = children
6337 .iter()
6338 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6339 .cloned()
6340 .collect();
6341 return self.measure_children_height(
6342 &flow,
6343 available_width,
6344 parent_style,
6345 font_context,
6346 );
6347 }
6348 if matches!(parent_style.display, Display::Grid) {
6350 if let Some(template_cols) = &parent_style.grid_template_columns {
6351 let num_columns = template_cols.len();
6352 if num_columns > 0 && !children.is_empty() {
6353 let col_gap = parent_style.column_gap;
6354 let row_gap = parent_style.row_gap;
6355
6356 let content_sizes: Vec<f64> = template_cols
6357 .iter()
6358 .map(|track| {
6359 if matches!(track, GridTrackSize::Auto) {
6360 available_width / num_columns as f64
6361 } else {
6362 0.0
6363 }
6364 })
6365 .collect();
6366
6367 let col_widths = grid::resolve_tracks(
6368 template_cols,
6369 available_width,
6370 col_gap,
6371 &content_sizes,
6372 );
6373
6374 let placements: Vec<Option<&GridPlacement>> = children
6375 .iter()
6376 .map(|child| child.style.grid_placement.as_ref())
6377 .collect();
6378
6379 let item_placements = grid::place_items(&placements, num_columns);
6380 let num_rows = grid::compute_num_rows(&item_placements);
6381
6382 if num_rows == 0 {
6383 return 0.0;
6384 }
6385
6386 let mut row_heights = vec![0.0_f64; num_rows];
6387 for placement in &item_placements {
6388 let cell_width = grid::span_width(
6389 placement.col_start,
6390 placement.col_end,
6391 &col_widths,
6392 col_gap,
6393 );
6394 let child = &children[placement.child_index];
6395 let child_style = child.style.resolve(Some(parent_style), cell_width);
6396 let h =
6397 self.measure_node_height(child, cell_width, &child_style, font_context);
6398 let span = placement.row_end - placement.row_start;
6399 let per_row = h / span as f64;
6400 for rh in row_heights
6401 .iter_mut()
6402 .take(placement.row_end.min(num_rows))
6403 .skip(placement.row_start)
6404 {
6405 if per_row > *rh {
6406 *rh = per_row;
6407 }
6408 }
6409 }
6410
6411 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6412 return row_heights.iter().sum::<f64>() + total_row_gap;
6413 }
6414 }
6415 }
6416
6417 let direction = parent_style.flex_direction;
6418 let row_gap = parent_style.row_gap;
6419 let column_gap = parent_style.column_gap;
6420
6421 match direction {
6422 FlexDirection::Row | FlexDirection::RowReverse => {
6423 let styles: Vec<ResolvedStyle> = children
6426 .iter()
6427 .map(|child| child.style.resolve(Some(parent_style), available_width))
6428 .collect();
6429
6430 let base_widths: Vec<f64> = children
6431 .iter()
6432 .zip(&styles)
6433 .map(|(child, style)| match style.flex_basis {
6434 SizeConstraint::Fixed(w) => w,
6435 SizeConstraint::Auto => match style.width {
6436 SizeConstraint::Fixed(w) => w,
6437 SizeConstraint::Auto => self
6438 .measure_intrinsic_width(child, style, font_context)
6439 .min(available_width),
6440 },
6441 })
6442 .collect();
6443
6444 let lines = match parent_style.flex_wrap {
6445 FlexWrap::NoWrap => {
6446 vec![flex::WrapLine {
6447 start: 0,
6448 end: children.len(),
6449 }]
6450 }
6451 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6452 flex::partition_into_lines(&base_widths, column_gap, available_width)
6453 }
6454 };
6455
6456 let mut final_widths = base_widths.clone();
6458 for line in &lines {
6459 let line_count = line.end - line.start;
6460 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6461 let distributable = available_width - line_gap;
6462 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6463 let remaining = distributable - total_base;
6464
6465 if remaining > 0.0 {
6466 let total_grow: f64 = styles[line.start..line.end]
6467 .iter()
6468 .map(|s| s.flex_grow)
6469 .sum();
6470 if total_grow > 0.0 {
6471 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6472 final_widths[line.start + j] = base_widths[line.start + j]
6473 + remaining * (s.flex_grow / total_grow);
6474 }
6475 }
6476 } else if remaining < 0.0 {
6477 let total_shrink: f64 = styles[line.start..line.end]
6478 .iter()
6479 .enumerate()
6480 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6481 .sum();
6482 if total_shrink > 0.0 {
6483 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6484 let factor =
6485 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6486 let w = base_widths[line.start + j] + remaining * factor;
6487 final_widths[line.start + j] = w.max(s.min_width);
6488 }
6489 }
6490 }
6491 }
6492
6493 let mut total = 0.0;
6494 for (i, line) in lines.iter().enumerate() {
6495 let line_height: f64 = children[line.start..line.end]
6496 .iter()
6497 .enumerate()
6498 .map(|(j, child)| {
6499 let fw = final_widths[line.start + j];
6500 let child_style =
6509 child.style.resolve(Some(parent_style), available_width);
6510 self.measure_node_height(child, fw, &child_style, font_context)
6511 + child_style.margin.vertical()
6512 })
6513 .fold(0.0f64, f64::max);
6514 total += line_height;
6515 if i > 0 {
6516 total += row_gap;
6517 }
6518 }
6519 total
6520 }
6521 FlexDirection::Column | FlexDirection::ColumnReverse => {
6522 let mut total = 0.0;
6523 for (i, child) in children.iter().enumerate() {
6524 let child_style = child.style.resolve(Some(parent_style), available_width);
6525 let child_height = self.measure_node_height(
6526 child,
6527 available_width,
6528 &child_style,
6529 font_context,
6530 );
6531 total += child_height + child_style.margin.vertical();
6532 if i > 0 {
6533 total += row_gap;
6534 }
6535 }
6536 total
6537 }
6538 }
6539 }
6540
6541 fn measure_intrinsic_width(
6543 &self,
6544 node: &Node,
6545 style: &ResolvedStyle,
6546 font_context: &FontContext,
6547 ) -> f64 {
6548 match &node.kind {
6549 NodeKind::Svg { width, .. } => {
6550 *width + style.padding.horizontal() + style.margin.horizontal()
6551 }
6552 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6553 let text_width = if !runs.is_empty() {
6559 runs.iter()
6560 .map(|run| {
6561 let run_style = run.style.resolve(Some(style), 0.0);
6562 let run_content = substitute_page_placeholders(&run.content);
6563 let transformed =
6564 apply_text_transform(&run_content, run_style.text_transform);
6565 let italic = matches!(
6566 run_style.font_style,
6567 FontStyle::Italic | FontStyle::Oblique
6568 );
6569 transformed
6573 .split('\n')
6574 .map(|segment| {
6575 font_context.measure_string(
6576 segment,
6577 &run_style.font_family,
6578 run_style.font_weight,
6579 italic,
6580 run_style.font_size,
6581 run_style.letter_spacing,
6582 )
6583 })
6584 .fold(0.0f64, f64::max)
6585 })
6586 .sum()
6587 } else {
6588 let content = substitute_page_placeholders(content);
6589 let transformed = apply_text_transform(&content, style.text_transform);
6590 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6591 transformed
6592 .split('\n')
6593 .map(|segment| {
6594 font_context.measure_string(
6595 segment,
6596 &style.font_family,
6597 style.font_weight,
6598 italic,
6599 style.font_size,
6600 style.letter_spacing,
6601 )
6602 })
6603 .fold(0.0f64, f64::max)
6604 };
6605 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6608 }
6609 NodeKind::Image {
6610 src, width, height, ..
6611 } => {
6612 let w = if let SizeConstraint::Fixed(w) = style.width {
6613 w
6614 } else if let Some(w) = width {
6615 *w
6616 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6617 let pixel_w = iw as f64;
6618 let pixel_h = ih as f64;
6619 let aspect = if pixel_w > 0.0 {
6620 pixel_h / pixel_w
6621 } else {
6622 0.75
6623 };
6624 let constrained_h = match style.height {
6626 SizeConstraint::Fixed(h) => Some(h),
6627 SizeConstraint::Auto => *height,
6628 };
6629 if let Some(h) = constrained_h {
6630 h / aspect
6631 } else {
6632 pixel_w
6633 }
6634 } else {
6635 100.0
6636 };
6637 w + style.padding.horizontal() + style.margin.horizontal()
6638 }
6639 NodeKind::Barcode { width, .. } => {
6640 let w = width.unwrap_or(0.0);
6641 w + style.padding.horizontal() + style.margin.horizontal()
6642 }
6643 NodeKind::QrCode { size, .. } => {
6644 let display_size = size.unwrap_or(0.0);
6645 display_size + style.padding.horizontal() + style.margin.horizontal()
6646 }
6647 NodeKind::Canvas { width, .. } => {
6648 *width + style.padding.horizontal() + style.margin.horizontal()
6649 }
6650 NodeKind::BarChart { width, .. }
6651 | NodeKind::LineChart { width, .. }
6652 | NodeKind::PieChart { width, .. }
6653 | NodeKind::AreaChart { width, .. }
6654 | NodeKind::DotPlot { width, .. } => {
6655 *width + style.padding.horizontal() + style.margin.horizontal()
6656 }
6657 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6658 *width + style.padding.horizontal() + style.margin.horizontal()
6659 }
6660 NodeKind::Table { columns } => {
6661 let num_cols = node
6666 .children
6667 .iter()
6668 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6669 .max()
6670 .unwrap_or(1)
6671 .max(columns.len().max(1));
6672 let (_, col_max) =
6673 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6674 col_max.iter().sum::<f64>()
6675 + style.padding.horizontal()
6676 + style.margin.horizontal()
6677 + style.border_width.horizontal()
6678 }
6679 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
6680 *width + style.padding.horizontal() + style.margin.horizontal()
6681 }
6682 NodeKind::Watermark { .. } => 0.0, _ => {
6684 if let SizeConstraint::Fixed(w) = style.width {
6692 return w
6693 + style.padding.horizontal()
6694 + style.border_width.horizontal()
6695 + style.margin.horizontal();
6696 }
6697 if node.children.is_empty() {
6699 style.padding.horizontal() + style.margin.horizontal()
6700 } else {
6701 let direction = style.flex_direction;
6702 let gap = style.column_gap;
6714 let mut total = 0.0f64;
6715 for (i, child) in node.children.iter().enumerate() {
6716 let child_style = child.style.resolve(Some(style), 0.0);
6717 let child_width =
6718 self.measure_intrinsic_width(child, &child_style, font_context);
6719 match direction {
6720 FlexDirection::Row | FlexDirection::RowReverse => {
6721 total += child_width;
6722 if i > 0 {
6723 total += gap;
6724 }
6725 }
6726 _ => {
6727 total = total.max(child_width);
6728 }
6729 }
6730 }
6731 total
6732 + style.padding.horizontal()
6733 + style.margin.horizontal()
6734 + style.border_width.horizontal()
6735 }
6736 }
6737 }
6738 }
6739
6740 pub fn measure_min_content_width(
6744 &self,
6745 node: &Node,
6746 style: &ResolvedStyle,
6747 font_context: &FontContext,
6748 ) -> f64 {
6749 match &node.kind {
6750 NodeKind::Table { columns } => {
6751 let num_cols = node
6754 .children
6755 .iter()
6756 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6757 .max()
6758 .unwrap_or(1)
6759 .max(columns.len().max(1));
6760 let (col_min, _) =
6761 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6762 col_min.iter().sum::<f64>()
6763 + style.padding.horizontal()
6764 + style.margin.horizontal()
6765 + style.border_width.horizontal()
6766 }
6767 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6768 let word_width = if !runs.is_empty() {
6769 runs.iter()
6771 .map(|run| {
6772 let run_style = run.style.resolve(Some(style), 0.0);
6773 let run_content = substitute_page_placeholders(&run.content);
6774 let transformed =
6775 apply_text_transform(&run_content, run_style.text_transform);
6776 self.text_layout.measure_widest_word(
6777 font_context,
6778 &transformed,
6779 run_style.font_size,
6780 &run_style.font_family,
6781 run_style.font_weight,
6782 run_style.font_style,
6783 run_style.letter_spacing,
6784 style.hyphens,
6785 style.lang.as_deref(),
6786 )
6787 })
6788 .fold(0.0f64, f64::max)
6789 } else {
6790 let content = substitute_page_placeholders(content);
6791 let transformed = apply_text_transform(&content, style.text_transform);
6792 self.text_layout.measure_widest_word(
6793 font_context,
6794 &transformed,
6795 style.font_size,
6796 &style.font_family,
6797 style.font_weight,
6798 style.font_style,
6799 style.letter_spacing,
6800 style.hyphens,
6801 style.lang.as_deref(),
6802 )
6803 };
6804 word_width + style.padding.horizontal() + style.margin.horizontal()
6805 }
6806 NodeKind::Image { width, .. } => {
6807 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
6808 }
6809 NodeKind::Svg { width, .. } => {
6810 *width + style.padding.horizontal() + style.margin.horizontal()
6811 }
6812 _ => {
6813 if node.children.is_empty() {
6814 style.padding.horizontal()
6815 + style.margin.horizontal()
6816 + style.border_width.horizontal()
6817 } else {
6818 let mut max_child_min = 0.0f64;
6819 for child in &node.children {
6820 let child_style = child.style.resolve(Some(style), 0.0);
6821 let child_min =
6822 self.measure_min_content_width(child, &child_style, font_context);
6823 max_child_min = max_child_min.max(child_min);
6824 }
6825 max_child_min
6826 + style.padding.horizontal()
6827 + style.margin.horizontal()
6828 + style.border_width.horizontal()
6829 }
6830 }
6831 }
6832 }
6833
6834 fn flex_item_baseline_distance(
6841 &self,
6842 item: &Node,
6843 style: &ResolvedStyle,
6844 w: f64,
6845 font_context: &FontContext,
6846 ) -> f64 {
6847 let first_line = match &item.kind {
6848 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
6849 let metrics = font_context.baseline_metrics(
6850 &style.font_family,
6851 style.font_weight,
6852 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
6853 );
6854 baseline_in_line(
6855 style.font_size * style.line_height,
6856 style.font_size,
6857 metrics,
6858 )
6859 }
6860 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
6861 };
6862 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
6863 }
6864
6865 fn cell_first_baseline_in_line(
6872 &self,
6873 cell: &Node,
6874 cell_style: &ResolvedStyle,
6875 w: f64,
6876 font_context: &FontContext,
6877 ) -> f64 {
6878 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
6879 for ch in &node.children {
6880 let s = ch.style.resolve(Some(parent), w);
6881 match &ch.kind {
6882 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
6883 _ => {
6884 if let Some(f) = first(ch, &s, w) {
6885 return Some(f);
6886 }
6887 }
6888 }
6889 }
6890 None
6891 }
6892 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
6893 let metrics = font_context.baseline_metrics(
6894 &s.font_family,
6895 s.font_weight,
6896 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
6897 );
6898 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
6899 }
6900
6901 fn cell_baseline_distance(
6906 &self,
6907 cell: &Node,
6908 cell_style: &ResolvedStyle,
6909 inner_width: f64,
6910 font_context: &FontContext,
6911 ) -> f64 {
6912 cell_style.padding.top
6913 + cell_style.border_width.top
6914 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
6915 }
6916
6917 fn row_baseline(
6920 &self,
6921 row: &Node,
6922 row_style: &ResolvedStyle,
6923 col_widths: &[f64],
6924 col_offsets: &[usize],
6925 font_context: &FontContext,
6926 ) -> Option<f64> {
6927 let mut b: Option<f64> = None;
6928 for (cell_i, cell) in row.children.iter().enumerate() {
6929 let span = match &cell.kind {
6930 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6931 _ => 1,
6932 };
6933 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6934 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6935 let cell_style = cell.style.resolve(Some(row_style), col_width);
6936 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6937 let iw = col_width
6938 - cell_style.padding.horizontal()
6939 - cell_style.border_width.horizontal();
6940 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
6941 b = Some(b.map_or(d, |m: f64| m.max(d)));
6942 }
6943 }
6944 b
6945 }
6946
6947 fn measure_table_row_height(
6948 &self,
6949 row: &Node,
6950 col_widths: &[f64],
6951 col_offsets: &[usize],
6952 parent_style: &ResolvedStyle,
6953 font_context: &FontContext,
6954 ) -> f64 {
6955 let row_style = row
6956 .style
6957 .resolve(Some(parent_style), col_widths.iter().sum());
6958 let mut max_height: f64 = 0.0;
6959 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
6962
6963 for (cell_i, cell) in row.children.iter().enumerate() {
6964 let span = match &cell.kind {
6965 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6966 _ => 1,
6967 };
6968 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6969 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6970 let cell_style = cell.style.resolve(Some(&row_style), col_width);
6971 let inner_width =
6972 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
6973
6974 let mut cell_content_height = 0.0;
6975 for child in &cell.children {
6976 let child_style = child.style.resolve(Some(&cell_style), inner_width);
6977 cell_content_height +=
6978 self.measure_node_height(child, inner_width, &child_style, font_context);
6979 }
6980
6981 let mut total = cell_content_height
6982 + cell_style.padding.vertical()
6983 + cell_style.border_width.vertical();
6984 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6988 if let Some(b) = row_bl {
6989 let d =
6990 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
6991 total += (b - d).max(0.0);
6992 }
6993 }
6994 if let SizeConstraint::Fixed(h) = cell_style.height {
7001 total = total.max(h);
7002 }
7003 max_height = max_height.max(total);
7004 }
7005
7006 max_height.max(row_style.min_height)
7007 }
7008
7009 fn cell_col_span(cell: &Node) -> usize {
7011 match &cell.kind {
7012 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7013 _ => 1,
7014 }
7015 }
7016
7017 fn measure_column_content(
7022 &self,
7023 children: &[Node],
7024 num_cols: usize,
7025 available_width: f64,
7026 table_style: &ResolvedStyle,
7027 font_context: &FontContext,
7028 ) -> (Vec<f64>, Vec<f64>) {
7029 let mut col_min = vec![0.0f64; num_cols];
7030 let mut col_max = vec![0.0f64; num_cols];
7031 let offsets = Self::table_column_offsets(children);
7032 for (row_i, row_node) in children.iter().enumerate() {
7033 for (cell_i, cell) in row_node.children.iter().enumerate() {
7034 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7035 let span = Self::cell_col_span(cell);
7036 let cell_style = cell.style.resolve(Some(table_style), available_width);
7037 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7038 let mut cmin = 0.0f64;
7039 let mut cmax = 0.0f64;
7040 for child in &cell.children {
7041 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7042 cmin =
7043 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7044 cmax =
7045 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7046 }
7047 cmin += chrome;
7048 let mut cmax = cmax.max(cmin) + chrome;
7049 if let SizeConstraint::Fixed(w) = cell_style.width {
7050 cmax = w.max(cmin);
7051 }
7052 let per_min = cmin / span as f64;
7053 let per_max = cmax / span as f64;
7054 for k in col..(col + span).min(num_cols) {
7055 col_min[k] = col_min[k].max(per_min);
7056 col_max[k] = col_max[k].max(per_max);
7057 }
7058 }
7059 }
7060 (col_min, col_max)
7061 }
7062
7063 fn resolve_column_widths(
7073 &self,
7074 defs: &[ColumnDef],
7075 available_width: f64,
7076 children: &[Node],
7077 table_style: &ResolvedStyle,
7078 font_context: &FontContext,
7079 ) -> Vec<f64> {
7080 if defs.is_empty() {
7081 let num_cols = Self::occupancy_column_count(children);
7082
7083 let (col_min, col_max) = self.measure_column_content(
7084 children,
7085 num_cols,
7086 available_width,
7087 table_style,
7088 font_context,
7089 );
7090
7091 let sum_min: f64 = col_min.iter().sum();
7092 let sum_max: f64 = col_max.iter().sum();
7093 let w = available_width;
7094 return if sum_max <= w {
7095 if sum_max <= f64::EPSILON {
7099 vec![w / num_cols as f64; num_cols]
7100 } else {
7101 col_max
7102 .iter()
7103 .map(|m| m + (w - sum_max) * (m / sum_max))
7104 .collect()
7105 }
7106 } else if sum_min <= w {
7107 let denom = (sum_max - sum_min).max(f64::EPSILON);
7110 col_min
7111 .iter()
7112 .zip(&col_max)
7113 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7114 .collect()
7115 } else {
7116 self.defect(format!(
7119 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7120 sum_min, w
7121 ));
7122 let scale = w / sum_min.max(f64::EPSILON);
7123 col_min.iter().map(|m| m * scale).collect()
7124 };
7125 }
7126
7127 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7133 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7134 while defs_vec.len() < num_cols {
7135 defs_vec.push(ColumnDef {
7136 width: ColumnWidth::Auto,
7137 });
7138 }
7139 let defs = &defs_vec[..];
7140
7141 let mut widths = Vec::new();
7142 let mut remaining = available_width;
7143 let mut auto_count = 0;
7144
7145 for def in defs {
7146 match def.width {
7147 ColumnWidth::Fixed(w) => {
7148 widths.push(w);
7149 remaining -= w;
7150 }
7151 ColumnWidth::Fraction(f) => {
7152 let w = available_width * f;
7153 widths.push(w);
7154 remaining -= w;
7155 }
7156 ColumnWidth::Auto => {
7157 widths.push(0.0);
7158 auto_count += 1;
7159 }
7160 }
7161 }
7162
7163 if remaining < 0.0 {
7164 self.defect(format!(
7167 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7168 available_width - remaining,
7169 available_width
7170 ));
7171 remaining = 0.0;
7172 }
7173
7174 if auto_count > 0 {
7175 let auto_width = remaining / auto_count as f64;
7176 for (i, def) in defs.iter().enumerate() {
7177 if matches!(def.width, ColumnWidth::Auto) {
7178 widths[i] = auto_width;
7179 }
7180 }
7181 }
7182
7183 let (col_min, _) = self.measure_column_content(
7190 children,
7191 widths.len(),
7192 available_width,
7193 table_style,
7194 font_context,
7195 );
7196 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7197 if needs_floor {
7198 let sum_min: f64 = col_min.iter().sum();
7199 if sum_min > available_width {
7200 self.defect(format!(
7201 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7202 sum_min, available_width
7203 ));
7204 let scale = available_width / sum_min.max(f64::EPSILON);
7205 return col_min.iter().map(|m| m * scale).collect();
7206 }
7207 let deficit: f64 = widths
7208 .iter()
7209 .zip(&col_min)
7210 .map(|(w, m)| (m - w).max(0.0))
7211 .sum();
7212 let surplus: f64 = widths
7213 .iter()
7214 .zip(&col_min)
7215 .map(|(w, m)| (w - m).max(0.0))
7216 .sum();
7217 let take = if surplus > 0.0 {
7218 deficit / surplus
7219 } else {
7220 0.0
7221 };
7222 widths = widths
7223 .iter()
7224 .zip(&col_min)
7225 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7226 .collect();
7227 }
7228
7229 widths
7230 }
7231
7232 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7233 for (page_index, page) in pages.iter_mut().enumerate() {
7234 if !page.watermarks.is_empty() {
7236 let (page_w, page_h) = page.config.size.dimensions();
7237 let cx = page_w / 2.0;
7238 let cy = page_h / 2.0;
7239
7240 let mut watermark_elements = Vec::new();
7241 for wm_node in &page.watermarks {
7242 if let NodeKind::Watermark {
7243 text,
7244 font_size,
7245 angle,
7246 } = &wm_node.kind
7247 {
7248 let style = wm_node.style.resolve(None, page_w);
7249 let color = style.color;
7250 let opacity = style.opacity;
7251 let angle_rad = angle.to_radians();
7252
7253 let italic =
7255 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7256
7257 let shaped = self.text_layout.shape_text(
7259 font_context,
7260 text,
7261 &style.font_family,
7262 style.font_weight,
7263 style.font_style,
7264 );
7265
7266 let mut glyphs = Vec::new();
7267 let mut x_pos = 0.0;
7268 let text_chars: Vec<char> = text.chars().collect();
7269
7270 if let Some(shaped_glyphs) = shaped {
7271 let units_per_em = font_context.units_per_em(
7273 &style.font_family,
7274 style.font_weight,
7275 italic,
7276 ) as f64;
7277
7278 for sg in &shaped_glyphs {
7279 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7280 let cluster_idx = sg.cluster as usize;
7281 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7282 glyphs.push(PositionedGlyph {
7283 glyph_id: sg.glyph_id,
7284 char_value: ch,
7285 x_offset: x_pos,
7286 y_offset: 0.0,
7287 x_advance: advance,
7288 font_size: *font_size,
7289 font_family: Arc::from(style.font_family.as_str()),
7290 font_weight: style.font_weight,
7291 font_style: style.font_style,
7292 color: Some(color),
7293 href: None,
7294 text_decoration: TextDecoration::None,
7295 letter_spacing: style.letter_spacing,
7296 cluster_text: None,
7297 });
7298 x_pos += advance + style.letter_spacing;
7299 }
7300 } else {
7301 for &ch in &text_chars {
7303 let w = font_context.char_width(
7304 ch,
7305 &style.font_family,
7306 style.font_weight,
7307 italic,
7308 *font_size,
7309 );
7310 glyphs.push(PositionedGlyph {
7311 glyph_id: ch as u16,
7312 char_value: ch,
7313 x_offset: x_pos,
7314 y_offset: 0.0,
7315 x_advance: w,
7316 font_size: *font_size,
7317 font_family: Arc::from(style.font_family.as_str()),
7318 font_weight: style.font_weight,
7319 font_style: style.font_style,
7320 color: Some(color),
7321 href: None,
7322 text_decoration: TextDecoration::None,
7323 letter_spacing: style.letter_spacing,
7324 cluster_text: None,
7325 });
7326 x_pos += w + style.letter_spacing;
7327 }
7328 }
7329
7330 let text_width = x_pos;
7331
7332 let line = TextLine {
7333 x: 0.0,
7334 y: 0.0,
7335 glyphs,
7336 width: text_width,
7337 height: *font_size,
7338 word_spacing: 0.0,
7339 };
7340
7341 watermark_elements.push(LayoutElement {
7342 x: cx,
7343 y: cy,
7344 width: text_width,
7345 height: *font_size,
7346 draw: DrawCommand::Watermark {
7347 lines: vec![line],
7348 color,
7349 opacity,
7350 angle_rad,
7351 font_family: style.font_family.clone(),
7352 },
7353 children: vec![],
7354 node_type: Some("Watermark".to_string()),
7355 resolved_style: None,
7356 source_location: None,
7357 href: None,
7358 bookmark: None,
7359 alt: None,
7360 is_header_row: false,
7361 actual_text: None,
7362 list_numbering: None,
7363 col_span: 1,
7364 overflow: Overflow::default(),
7365 opacity: 1.0,
7366 });
7367 }
7368 }
7369
7370 watermark_elements.append(&mut page.elements);
7372 page.elements = watermark_elements;
7373 page.watermarks.clear();
7374 }
7375
7376 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7377 continue;
7378 }
7379
7380 if !page.fixed_header.is_empty() {
7382 let mut hdr_cursor = PageCursor::new(&page.config);
7383 for (node, _h) in &page.fixed_header {
7384 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7387 continue;
7388 }
7389 let cw = hdr_cursor.content_width;
7390 let cx = hdr_cursor.content_x;
7391 let style = node.style.resolve(None, cw);
7392 self.layout_view(
7393 node,
7394 &style,
7395 &mut hdr_cursor,
7396 &mut Vec::new(),
7397 cx,
7398 cw,
7399 font_context,
7400 );
7401 }
7402 let mut combined = hdr_cursor.elements;
7404 combined.append(&mut page.elements);
7405 page.elements = combined;
7406 }
7407
7408 if !page.fixed_footer.is_empty() {
7413 let mut ftr_cursor = PageCursor::new(&page.config);
7414 let total_ftr: f64 = page
7415 .fixed_footer
7416 .iter()
7417 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7418 .map(|(_, h)| *h)
7419 .sum();
7420 let target_y = ftr_cursor.content_height - total_ftr;
7421 for (node, _h) in &page.fixed_footer {
7423 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7424 continue;
7425 }
7426 let cw = ftr_cursor.content_width;
7427 let cx = ftr_cursor.content_x;
7428 let style = node.style.resolve(None, cw);
7429 self.layout_view(
7430 node,
7431 &style,
7432 &mut ftr_cursor,
7433 &mut Vec::new(),
7434 cx,
7435 cw,
7436 font_context,
7437 );
7438 }
7439 for el in &mut ftr_cursor.elements {
7443 offset_element_y(el, target_y);
7444 }
7445 page.elements.extend(ftr_cursor.elements);
7446 }
7447
7448 page.fixed_header.clear();
7450 page.fixed_footer.clear();
7451 }
7452 }
7453
7454 #[allow(clippy::too_many_arguments)]
7459 fn layout_grid_children(
7460 &self,
7461 children: &[Node],
7462 parent_style: &ResolvedStyle,
7463 cursor: &mut PageCursor,
7464 pages: &mut Vec<LayoutPage>,
7465 x: f64,
7466 available_width: f64,
7467 font_context: &FontContext,
7468 ) {
7469 let template_cols = match &parent_style.grid_template_columns {
7470 Some(cols) => cols,
7471 None => return, };
7473
7474 let num_columns = template_cols.len();
7475 if num_columns == 0 || children.is_empty() {
7476 return;
7477 }
7478
7479 let col_gap = parent_style.column_gap;
7480 let row_gap = parent_style.row_gap;
7481
7482 let content_sizes: Vec<f64> = template_cols
7485 .iter()
7486 .map(|track| {
7487 if matches!(track, GridTrackSize::Auto) {
7488 available_width / num_columns as f64
7491 } else {
7492 0.0
7493 }
7494 })
7495 .collect();
7496
7497 let col_widths =
7498 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7499
7500 let placements: Vec<Option<&GridPlacement>> = children
7502 .iter()
7503 .map(|child| child.style.grid_placement.as_ref())
7504 .collect();
7505
7506 let item_placements = grid::place_items(&placements, num_columns);
7508 let num_rows = grid::compute_num_rows(&item_placements);
7509
7510 if num_rows == 0 {
7511 return;
7512 }
7513
7514 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7516 for placement in &item_placements {
7517 let cell_width =
7518 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7519 let child = &children[placement.child_index];
7520 let child_style = child.style.resolve(Some(parent_style), cell_width);
7521 item_heights[placement.child_index] =
7522 self.measure_node_height(child, cell_width, &child_style, font_context);
7523 }
7524
7525 let template_rows = parent_style.grid_template_rows.as_deref();
7527 let mut row_heights = vec![0.0_f64; num_rows];
7528 for placement in &item_placements {
7529 let h = item_heights[placement.child_index];
7530 let span = placement.row_end - placement.row_start;
7531 let per_row = h / span as f64;
7532 for rh in row_heights
7533 .iter_mut()
7534 .take(placement.row_end.min(num_rows))
7535 .skip(placement.row_start)
7536 {
7537 if per_row > *rh {
7538 *rh = per_row;
7539 }
7540 }
7541 }
7542
7543 if let Some(template) = template_rows {
7545 let auto_row = parent_style.grid_auto_rows.as_ref();
7546 for (r, rh) in row_heights.iter_mut().enumerate() {
7547 let track = template.get(r).or(auto_row);
7548 if let Some(track) = track {
7549 match track {
7550 GridTrackSize::Pt(pts) => *rh = *pts,
7551 GridTrackSize::Auto => {} _ => {} }
7554 }
7555 }
7556 }
7557
7558 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7560 if row_height > cursor.remaining_height() {
7565 pages.push(cursor.finalize());
7566 *cursor = cursor.new_page();
7567 }
7568
7569 let row_start_y = cursor.y;
7570
7571 for placement in &item_placements {
7573 if placement.row_start != row {
7574 continue; }
7576
7577 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7578 let cell_width =
7579 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7580
7581 let child = &children[placement.child_index];
7582
7583 self.layout_node(
7584 child,
7585 cursor,
7586 pages,
7587 cell_x,
7588 cell_width,
7589 Some(parent_style),
7590 font_context,
7591 None,
7592 None,
7593 );
7594 cursor.y = row_start_y;
7596 }
7597
7598 cursor.y = row_start_y + row_height + row_gap;
7599 }
7600
7601 if num_rows > 0 {
7603 cursor.y -= row_gap;
7604 }
7605 }
7606}
7607
7608struct FlexItem<'a> {
7609 node: &'a Node,
7610 style: ResolvedStyle,
7611 base_width: f64,
7612 min_content_width: f64,
7613}
7614
7615fn first_text_snippet(node: &Node) -> Option<String> {
7618 fn walk(n: &Node) -> Option<&str> {
7619 match &n.kind {
7620 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7621 if !content.trim().is_empty() {
7622 return Some(content.trim());
7623 }
7624 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
7625 return Some(r.content.trim());
7626 }
7627 None
7628 }
7629 _ => n.children.iter().find_map(walk),
7630 }
7631 }
7632 walk(node).map(|t| {
7633 let mut s: String = t.chars().take(32).collect();
7634 if t.chars().count() > 32 {
7635 s.push('…');
7636 }
7637 s
7638 })
7639}
7640
7641fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
7651 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
7652}
7653
7654#[cfg(test)]
7655mod tests {
7656 use super::*;
7657 use crate::font::FontContext;
7658
7659 fn make_text(content: &str, font_size: f64) -> Node {
7660 Node {
7661 kind: NodeKind::Text {
7662 content: content.to_string(),
7663 href: None,
7664 runs: vec![],
7665 },
7666 style: Style {
7667 font_size: Some(font_size),
7668 ..Default::default()
7669 },
7670 children: vec![],
7671 id: None,
7672 source_location: None,
7673 bookmark: None,
7674 href: None,
7675 alt: None,
7676 }
7677 }
7678
7679 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
7680 Node {
7681 kind: NodeKind::View,
7682 style,
7683 children,
7684 id: None,
7685 source_location: None,
7686 bookmark: None,
7687 href: None,
7688 alt: None,
7689 }
7690 }
7691
7692 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
7693 Node {
7694 kind: NodeKind::Text {
7695 content: String::new(),
7696 href: None,
7697 runs,
7698 },
7699 style: Style::default(),
7700 children: vec![],
7701 id: None,
7702 source_location: None,
7703 bookmark: None,
7704 href: None,
7705 alt: None,
7706 }
7707 }
7708
7709 #[test]
7710 fn intrinsic_width_measures_runs_not_just_content() {
7711 let engine = LayoutEngine::new();
7715 let font_context = FontContext::new();
7716
7717 let runs_node = make_runs_text(vec![
7718 crate::model::TextRun {
7719 content: "Hello ".to_string(),
7720 style: Style::default(),
7721 href: None,
7722 },
7723 crate::model::TextRun {
7724 content: "World".to_string(),
7725 style: Style {
7726 font_weight: Some(700),
7727 ..Default::default()
7728 },
7729 href: None,
7730 },
7731 ]);
7732 let plain_node = make_text("Hello World", 12.0);
7733
7734 let runs_style = runs_node.style.resolve(None, 0.0);
7735 let plain_style = plain_node.style.resolve(None, 0.0);
7736 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
7737 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
7738
7739 assert!(
7742 runs_w >= plain_w,
7743 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
7744 );
7745 assert!(
7746 runs_w < plain_w * 1.5,
7747 "runs width ({runs_w}) should be close to plain width ({plain_w})"
7748 );
7749 }
7750
7751 #[test]
7752 fn intrinsic_width_of_multiline_text_is_widest_line() {
7753 let engine = LayoutEngine::new();
7754 let font_context = FontContext::new();
7755
7756 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
7757 let widest = make_text("Springfield, IL 62704", 12.0);
7758
7759 let m_style = multiline.style.resolve(None, 0.0);
7760 let w_style = widest.style.resolve(None, 0.0);
7761 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
7762 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
7763
7764 assert!(
7765 (m_w - w_w).abs() < 0.01,
7766 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
7767 );
7768 }
7769
7770 #[test]
7771 fn intrinsic_width_of_heading_measures_its_text() {
7772 let engine = LayoutEngine::new();
7775 let font_context = FontContext::new();
7776
7777 let heading = Node {
7778 kind: NodeKind::Heading {
7779 level: 1,
7780 content: "Invoice #2024-001".to_string(),
7781 href: None,
7782 runs: vec![],
7783 },
7784 style: Style {
7785 font_size: Some(24.0),
7786 ..Default::default()
7787 },
7788 children: vec![],
7789 id: None,
7790 source_location: None,
7791 bookmark: None,
7792 href: None,
7793 alt: None,
7794 };
7795 let style = heading.style.resolve(None, 0.0);
7796 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
7797 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
7798 }
7799
7800 #[test]
7801 fn measure_node_height_of_wrapping_heading_matches_text() {
7802 let engine = LayoutEngine::new();
7809 let font_context = FontContext::new();
7810
7811 let content = "Annual Performance Review";
7812 let heading = Node {
7813 kind: NodeKind::Heading {
7814 level: 1,
7815 content: content.to_string(),
7816 href: None,
7817 runs: vec![],
7818 },
7819 style: Style {
7820 font_size: Some(32.0),
7821 ..Default::default()
7822 },
7823 children: vec![],
7824 id: None,
7825 source_location: None,
7826 bookmark: None,
7827 href: None,
7828 alt: None,
7829 };
7830 let text = make_text(content, 32.0);
7831
7832 let width = 200.0;
7834 let h_style = heading.style.resolve(None, width);
7835 let t_style = text.style.resolve(None, width);
7836 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
7837 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
7838
7839 assert!(
7840 h_height > 32.0,
7841 "a wrapping 32pt heading must measure more than one line, got {h_height}"
7842 );
7843 assert!(
7844 (h_height - t_height).abs() < 0.01,
7845 "heading height ({h_height}) must equal the same text's height ({t_height})"
7846 );
7847 }
7848
7849 #[test]
7850 fn intrinsic_width_flex_row_sums_children() {
7851 let engine = LayoutEngine::new();
7852 let font_context = FontContext::new();
7853
7854 let child1 = make_text("Hello", 14.0);
7855 let child2 = make_text("World", 14.0);
7856
7857 let child1_style = child1.style.resolve(None, 0.0);
7858 let child2_style = child2.style.resolve(None, 0.0);
7859 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
7860 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
7861
7862 let row = make_styled_view(
7863 Style {
7864 flex_direction: Some(FlexDirection::Row),
7865 ..Default::default()
7866 },
7867 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
7868 );
7869 let row_style = row.style.resolve(None, 0.0);
7870 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
7871
7872 assert!(
7873 (row_w - (child1_w + child2_w)).abs() < 0.01,
7874 "Row intrinsic width ({}) should equal sum of children ({} + {})",
7875 row_w,
7876 child1_w,
7877 child2_w
7878 );
7879 }
7880
7881 #[test]
7882 fn intrinsic_width_flex_column_takes_max() {
7883 let engine = LayoutEngine::new();
7884 let font_context = FontContext::new();
7885
7886 let short = make_text("Hi", 14.0);
7887 let long = make_text("Hello World", 14.0);
7888
7889 let short_style = short.style.resolve(None, 0.0);
7890 let long_style = long.style.resolve(None, 0.0);
7891 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
7892 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
7893
7894 let col = make_styled_view(
7895 Style {
7896 flex_direction: Some(FlexDirection::Column),
7897 ..Default::default()
7898 },
7899 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
7900 );
7901 let col_style = col.style.resolve(None, 0.0);
7902 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
7903
7904 assert!(
7905 (col_w - long_w).abs() < 0.01,
7906 "Column intrinsic width ({}) should equal max child ({}, short was {})",
7907 col_w,
7908 long_w,
7909 short_w
7910 );
7911 }
7912
7913 #[test]
7914 fn intrinsic_width_nested_containers() {
7915 let engine = LayoutEngine::new();
7916 let font_context = FontContext::new();
7917
7918 let inner = make_styled_view(
7919 Style {
7920 flex_direction: Some(FlexDirection::Row),
7921 ..Default::default()
7922 },
7923 vec![make_text("A", 12.0), make_text("B", 12.0)],
7924 );
7925 let inner_style = inner.style.resolve(None, 0.0);
7926 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
7927
7928 let outer = make_styled_view(
7929 Style::default(),
7930 vec![make_styled_view(
7931 Style {
7932 flex_direction: Some(FlexDirection::Row),
7933 ..Default::default()
7934 },
7935 vec![make_text("A", 12.0), make_text("B", 12.0)],
7936 )],
7937 );
7938 let outer_style = outer.style.resolve(None, 0.0);
7939 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
7940
7941 assert!(
7942 (outer_w - inner_w).abs() < 0.01,
7943 "Nested container ({}) should match inner container ({})",
7944 outer_w,
7945 inner_w
7946 );
7947 }
7948
7949 #[test]
7950 fn intrinsic_width_row_with_gap() {
7951 let engine = LayoutEngine::new();
7952 let font_context = FontContext::new();
7953
7954 let no_gap = make_styled_view(
7955 Style {
7956 flex_direction: Some(FlexDirection::Row),
7957 ..Default::default()
7958 },
7959 vec![make_text("A", 12.0), make_text("B", 12.0)],
7960 );
7961 let with_gap = make_styled_view(
7962 Style {
7963 flex_direction: Some(FlexDirection::Row),
7964 gap: Some(10.0),
7965 ..Default::default()
7966 },
7967 vec![make_text("A", 12.0), make_text("B", 12.0)],
7968 );
7969
7970 let no_gap_style = no_gap.style.resolve(None, 0.0);
7971 let with_gap_style = with_gap.style.resolve(None, 0.0);
7972 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
7973 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
7974
7975 assert!(
7976 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
7977 "Gap should add 10pt: with_gap={}, no_gap={}",
7978 with_gap_w,
7979 no_gap_w
7980 );
7981 }
7982
7983 #[test]
7984 fn intrinsic_width_empty_container() {
7985 let engine = LayoutEngine::new();
7986 let font_context = FontContext::new();
7987
7988 let padding = 8.0;
7989 let empty = make_styled_view(
7990 Style {
7991 padding: Some(Edges::uniform(padding)),
7992 ..Default::default()
7993 },
7994 vec![],
7995 );
7996 let style = empty.style.resolve(None, 0.0);
7997 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
7998
7999 assert!(
8000 (w - padding * 2.0).abs() < 0.01,
8001 "Empty container width ({}) should equal horizontal padding ({})",
8002 w,
8003 padding * 2.0
8004 );
8005 }
8006
8007 #[test]
8010 fn flex_shrink_respects_min_content_width() {
8011 let engine = LayoutEngine::new();
8015 let font_context = FontContext::new();
8016
8017 let sale_text = make_text("SALE", 12.0);
8018 let sale_style = sale_text.style.resolve(None, 0.0);
8019 let sale_word_width =
8020 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8021 assert!(
8022 sale_word_width > 0.0,
8023 "SALE should have non-zero min-content width"
8024 );
8025
8026 let container = make_styled_view(
8029 Style {
8030 flex_direction: Some(FlexDirection::Row),
8031 width: Some(Dimension::Pt(100.0)),
8032 ..Default::default()
8033 },
8034 vec![
8035 make_styled_view(
8036 Style {
8037 width: Some(Dimension::Pt(80.0)),
8038 flex_shrink: Some(1.0),
8039 ..Default::default()
8040 },
8041 vec![],
8042 ),
8043 make_styled_view(
8044 Style {
8045 width: Some(Dimension::Pt(60.0)),
8046 flex_shrink: Some(1.0),
8047 ..Default::default()
8048 },
8049 vec![make_text("SALE", 12.0)],
8050 ),
8051 ],
8052 );
8053
8054 let doc = Document {
8055 children: vec![Node::page(
8056 PageConfig::default(),
8057 Style::default(),
8058 vec![container],
8059 )],
8060 metadata: Default::default(),
8061 default_page: PageConfig::default(),
8062 first_page: None,
8063 left_page: None,
8064 right_page: None,
8065 named_pages: Default::default(),
8066 attachments: vec![],
8067 zugferd: None,
8068 fonts: vec![],
8069 tagged: false,
8070 pdfa: None,
8071 default_style: None,
8072 embedded_data: None,
8073 flatten_forms: false,
8074 pdf_ua: false,
8075 certification: None,
8076 pdf_version: Default::default(),
8077 pdf_ua2: false,
8078 };
8079
8080 let pages = engine.layout(&doc, &font_context);
8081 assert!(!pages.is_empty());
8082
8083 let page = &pages[0];
8086 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8088 assert!(
8089 container_el.is_some(),
8090 "Should find container with 2 children"
8091 );
8092 let sale_child = &container_el.unwrap().children[1];
8093 assert!(
8094 sale_child.width >= sale_word_width - 0.01,
8095 "SALE child width ({}) should be >= min-content width ({})",
8096 sale_child.width,
8097 sale_word_width
8098 );
8099 }
8100
8101 #[test]
8104 fn column_justify_content_center() {
8105 let engine = LayoutEngine::new();
8108 let font_context = FontContext::new();
8109
8110 let container = make_styled_view(
8111 Style {
8112 flex_direction: Some(FlexDirection::Column),
8113 height: Some(Dimension::Pt(200.0)),
8114 justify_content: Some(JustifyContent::Center),
8115 ..Default::default()
8116 },
8117 vec![make_text("Centered", 12.0)],
8118 );
8119
8120 let doc = Document {
8121 children: vec![Node::page(
8122 PageConfig::default(),
8123 Style::default(),
8124 vec![container],
8125 )],
8126 metadata: Default::default(),
8127 default_page: PageConfig::default(),
8128 first_page: None,
8129 left_page: None,
8130 right_page: None,
8131 named_pages: Default::default(),
8132 attachments: vec![],
8133 zugferd: None,
8134 fonts: vec![],
8135 tagged: false,
8136 pdfa: None,
8137 default_style: None,
8138 embedded_data: None,
8139 flatten_forms: false,
8140 pdf_ua: false,
8141 certification: None,
8142 pdf_version: Default::default(),
8143 pdf_ua2: false,
8144 };
8145
8146 let pages = engine.layout(&doc, &font_context);
8147 let page = &pages[0];
8148
8149 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8152 assert!(
8153 container_el.is_some(),
8154 "Should find container with children"
8155 );
8156 let container_el = container_el.unwrap();
8157 let child = &container_el.children[0];
8158
8159 let child_offset = child.y - container_el.y;
8161 let expected_offset = (200.0 - child.height) / 2.0;
8162 assert!(
8163 (child_offset - expected_offset).abs() < 2.0,
8164 "Child offset ({}) should be near center ({})",
8165 child_offset,
8166 expected_offset
8167 );
8168 }
8169
8170 #[test]
8171 fn column_align_items_center() {
8172 let engine = LayoutEngine::new();
8175 let font_context = FontContext::new();
8176
8177 let container = make_styled_view(
8178 Style {
8179 flex_direction: Some(FlexDirection::Column),
8180 width: Some(Dimension::Pt(300.0)),
8181 align_items: Some(AlignItems::Center),
8182 ..Default::default()
8183 },
8184 vec![make_text("Hi", 12.0)],
8185 );
8186
8187 let doc = Document {
8188 children: vec![Node::page(
8189 PageConfig::default(),
8190 Style::default(),
8191 vec![container],
8192 )],
8193 metadata: Default::default(),
8194 default_page: PageConfig::default(),
8195 first_page: None,
8196 left_page: None,
8197 right_page: None,
8198 named_pages: Default::default(),
8199 attachments: vec![],
8200 zugferd: None,
8201 fonts: vec![],
8202 tagged: false,
8203 pdfa: None,
8204 default_style: None,
8205 embedded_data: None,
8206 flatten_forms: false,
8207 pdf_ua: false,
8208 certification: None,
8209 pdf_version: Default::default(),
8210 pdf_ua2: false,
8211 };
8212
8213 let pages = engine.layout(&doc, &font_context);
8214 let page = &pages[0];
8215
8216 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8217 assert!(container_el.is_some());
8218 let container_el = container_el.unwrap();
8219 let child = &container_el.children[0];
8220
8221 let child_center = child.x + child.width / 2.0;
8223 let container_center = container_el.x + container_el.width / 2.0;
8224 assert!(
8225 (child_center - container_center).abs() < 2.0,
8226 "Child center ({}) should be near container center ({})",
8227 child_center,
8228 container_center
8229 );
8230 }
8231
8232 #[test]
8235 fn absolute_child_positioned_relative_to_parent() {
8236 let engine = LayoutEngine::new();
8240 let font_context = FontContext::new();
8241
8242 let parent = make_styled_view(
8243 Style {
8244 position: Some(crate::model::Position::Relative),
8245 margin: Some(MarginEdges::from_edges(Edges {
8246 top: 50.0,
8247 left: 50.0,
8248 ..Default::default()
8249 })),
8250 width: Some(Dimension::Pt(200.0)),
8251 height: Some(Dimension::Pt(200.0)),
8252 ..Default::default()
8253 },
8254 vec![make_styled_view(
8255 Style {
8256 position: Some(crate::model::Position::Absolute),
8257 top: Some(10.0),
8258 left: Some(10.0),
8259 width: Some(Dimension::Pt(50.0)),
8260 height: Some(Dimension::Pt(50.0)),
8261 ..Default::default()
8262 },
8263 vec![],
8264 )],
8265 );
8266
8267 let doc = Document {
8268 children: vec![Node::page(
8269 PageConfig::default(),
8270 Style::default(),
8271 vec![parent],
8272 )],
8273 metadata: Default::default(),
8274 default_page: PageConfig::default(),
8275 first_page: None,
8276 left_page: None,
8277 right_page: None,
8278 named_pages: Default::default(),
8279 attachments: vec![],
8280 zugferd: None,
8281 fonts: vec![],
8282 tagged: false,
8283 pdfa: None,
8284 default_style: None,
8285 embedded_data: None,
8286 flatten_forms: false,
8287 pdf_ua: false,
8288 certification: None,
8289 pdf_version: Default::default(),
8290 pdf_ua2: false,
8291 };
8292
8293 let pages = engine.layout(&doc, &font_context);
8294 let page = &pages[0];
8295
8296 let parent_el = page
8299 .elements
8300 .iter()
8301 .find(|e| e.width > 190.0 && e.width < 210.0);
8302 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8303 let parent_el = parent_el.unwrap();
8304
8305 let abs_child = parent_el
8307 .children
8308 .iter()
8309 .find(|e| e.width > 45.0 && e.width < 55.0);
8310 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8311 let abs_child = abs_child.unwrap();
8312
8313 let expected_x = parent_el.x + 10.0;
8314 let expected_y = parent_el.y + 10.0;
8315 assert!(
8316 (abs_child.x - expected_x).abs() < 1.0,
8317 "Absolute child x ({}) should be parent.x + 10 ({})",
8318 abs_child.x,
8319 expected_x
8320 );
8321 assert!(
8322 (abs_child.y - expected_y).abs() < 1.0,
8323 "Absolute child y ({}) should be parent.y + 10 ({})",
8324 abs_child.y,
8325 expected_y
8326 );
8327 }
8328
8329 #[test]
8330 fn absolute_escapes_unpositioned_parent_to_page() {
8331 let engine = LayoutEngine::new();
8336 let font_context = FontContext::new();
8337 let parent = make_styled_view(
8338 Style {
8339 margin: Some(MarginEdges::from_edges(Edges {
8340 top: 50.0,
8341 left: 50.0,
8342 ..Default::default()
8343 })),
8344 width: Some(Dimension::Pt(200.0)),
8345 height: Some(Dimension::Pt(200.0)),
8346 ..Default::default()
8347 },
8348 vec![make_styled_view(
8349 Style {
8350 position: Some(crate::model::Position::Absolute),
8351 top: Some(10.0),
8352 left: Some(10.0),
8353 width: Some(Dimension::Pt(50.0)),
8354 height: Some(Dimension::Pt(50.0)),
8355 ..Default::default()
8356 },
8357 vec![],
8358 )],
8359 );
8360 let doc = Document {
8361 children: vec![Node::page(
8362 PageConfig::default(),
8363 Style::default(),
8364 vec![parent],
8365 )],
8366 metadata: Default::default(),
8367 default_page: PageConfig::default(),
8368 first_page: None,
8369 left_page: None,
8370 right_page: None,
8371 named_pages: Default::default(),
8372 attachments: vec![],
8373 zugferd: None,
8374 fonts: vec![],
8375 tagged: false,
8376 pdfa: None,
8377 default_style: None,
8378 embedded_data: None,
8379 flatten_forms: false,
8380 pdf_ua: false,
8381 certification: None,
8382 pdf_version: Default::default(),
8383 pdf_ua2: false,
8384 };
8385 let pages = engine.layout(&doc, &font_context);
8386 let page = &pages[0];
8387 let parent_el = page
8388 .elements
8389 .iter()
8390 .find(|e| e.width > 190.0 && e.width < 210.0)
8391 .expect("parent");
8392 let abs_child = parent_el
8393 .children
8394 .iter()
8395 .find(|e| e.width > 45.0 && e.width < 55.0)
8396 .expect("abs child");
8397 let page_left = PageConfig::default().margin.left;
8398 let page_top = PageConfig::default().margin.top;
8399 assert!(
8400 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
8401 "absolute escapes to the page: x {} should be page_left + 10 ({})",
8402 abs_child.x,
8403 page_left + 10.0
8404 );
8405 assert!(
8406 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
8407 "absolute escapes to the page: y {} should be page_top + 10 ({})",
8408 abs_child.y,
8409 page_top + 10.0
8410 );
8411 assert!(
8412 abs_child.x < parent_el.x,
8413 "child must no longer be parent-relative (parent is 50pt further in)"
8414 );
8415 }
8416
8417 #[test]
8418 fn text_transform_none_passthrough() {
8419 assert_eq!(
8420 apply_text_transform("Hello World", TextTransform::None),
8421 "Hello World"
8422 );
8423 }
8424
8425 #[test]
8426 fn text_transform_uppercase() {
8427 assert_eq!(
8428 apply_text_transform("hello world", TextTransform::Uppercase),
8429 "HELLO WORLD"
8430 );
8431 }
8432
8433 #[test]
8434 fn text_transform_lowercase() {
8435 assert_eq!(
8436 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
8437 "hello world"
8438 );
8439 }
8440
8441 #[test]
8442 fn text_transform_capitalize() {
8443 assert_eq!(
8444 apply_text_transform("hello world", TextTransform::Capitalize),
8445 "Hello World"
8446 );
8447 assert_eq!(
8448 apply_text_transform(" hello world ", TextTransform::Capitalize),
8449 " Hello World "
8450 );
8451 assert_eq!(
8452 apply_text_transform("already Capitalized", TextTransform::Capitalize),
8453 "Already Capitalized"
8454 );
8455 }
8456
8457 #[test]
8458 fn text_transform_capitalize_empty() {
8459 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
8460 }
8461
8462 #[test]
8463 fn apply_char_transform_uppercase() {
8464 assert_eq!(
8465 apply_char_transform('a', TextTransform::Uppercase, false),
8466 'A'
8467 );
8468 assert_eq!(
8469 apply_char_transform('A', TextTransform::Uppercase, false),
8470 'A'
8471 );
8472 }
8473
8474 #[test]
8475 fn apply_char_transform_capitalize_word_start() {
8476 assert_eq!(
8477 apply_char_transform('h', TextTransform::Capitalize, true),
8478 'H'
8479 );
8480 assert_eq!(
8481 apply_char_transform('h', TextTransform::Capitalize, false),
8482 'h'
8483 );
8484 }
8485
8486 #[test]
8489 fn column_flex_grow_single_child_fills_container() {
8490 let engine = LayoutEngine::new();
8493 let font_context = FontContext::new();
8494
8495 let child = make_styled_view(
8496 Style {
8497 flex_grow: Some(1.0),
8498 ..Default::default()
8499 },
8500 vec![make_text("Short", 12.0)],
8501 );
8502
8503 let container = make_styled_view(
8504 Style {
8505 flex_direction: Some(FlexDirection::Column),
8506 height: Some(Dimension::Pt(300.0)),
8507 ..Default::default()
8508 },
8509 vec![child],
8510 );
8511
8512 let doc = Document {
8513 children: vec![Node::page(
8514 PageConfig::default(),
8515 Style::default(),
8516 vec![container],
8517 )],
8518 metadata: Default::default(),
8519 default_page: PageConfig::default(),
8520 first_page: None,
8521 left_page: None,
8522 right_page: None,
8523 named_pages: Default::default(),
8524 attachments: vec![],
8525 zugferd: None,
8526 fonts: vec![],
8527 tagged: false,
8528 pdfa: None,
8529 default_style: None,
8530 embedded_data: None,
8531 flatten_forms: false,
8532 pdf_ua: false,
8533 certification: None,
8534 pdf_version: Default::default(),
8535 pdf_ua2: false,
8536 };
8537
8538 let pages = engine.layout(&doc, &font_context);
8539 let page = &pages[0];
8540
8541 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8542 assert!(container_el.is_some());
8543 let container_el = container_el.unwrap();
8544 assert!(
8545 (container_el.height - 300.0).abs() < 1.0,
8546 "Container should be 300pt, got {}",
8547 container_el.height
8548 );
8549
8550 let child_el = &container_el.children[0];
8551 assert!(
8552 (child_el.height - 300.0).abs() < 1.0,
8553 "flex-grow child should expand to 300pt, got {}",
8554 child_el.height
8555 );
8556 }
8557
8558 #[test]
8559 fn column_flex_grow_two_children_proportional() {
8560 let engine = LayoutEngine::new();
8563 let font_context = FontContext::new();
8564
8565 let child1 = make_styled_view(
8566 Style {
8567 flex_grow: Some(1.0),
8568 ..Default::default()
8569 },
8570 vec![make_text("A", 12.0)],
8571 );
8572 let child2 = make_styled_view(
8573 Style {
8574 flex_grow: Some(2.0),
8575 ..Default::default()
8576 },
8577 vec![make_text("B", 12.0)],
8578 );
8579
8580 let container = make_styled_view(
8581 Style {
8582 flex_direction: Some(FlexDirection::Column),
8583 height: Some(Dimension::Pt(300.0)),
8584 ..Default::default()
8585 },
8586 vec![child1, child2],
8587 );
8588
8589 let doc = Document {
8590 children: vec![Node::page(
8591 PageConfig::default(),
8592 Style::default(),
8593 vec![container],
8594 )],
8595 metadata: Default::default(),
8596 default_page: PageConfig::default(),
8597 first_page: None,
8598 left_page: None,
8599 right_page: None,
8600 named_pages: Default::default(),
8601 attachments: vec![],
8602 zugferd: None,
8603 fonts: vec![],
8604 tagged: false,
8605 pdfa: None,
8606 default_style: None,
8607 embedded_data: None,
8608 flatten_forms: false,
8609 pdf_ua: false,
8610 certification: None,
8611 pdf_version: Default::default(),
8612 pdf_ua2: false,
8613 };
8614
8615 let pages = engine.layout(&doc, &font_context);
8616 let page = &pages[0];
8617
8618 let container_el = page
8619 .elements
8620 .iter()
8621 .find(|e| e.children.len() == 2)
8622 .expect("Should find container with two children");
8623
8624 let c1 = &container_el.children[0];
8625 let c2 = &container_el.children[1];
8626
8627 let total = c1.height + c2.height;
8631 assert!(
8632 (total - 300.0).abs() < 2.0,
8633 "Children should sum to ~300pt, got {}",
8634 total
8635 );
8636
8637 let ratio = c2.height / c1.height;
8640 assert!(
8641 ratio > 1.3 && ratio < 2.5,
8642 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
8643 ratio
8644 );
8645 }
8646
8647 #[test]
8648 fn column_flex_grow_mixed_grow_and_fixed() {
8649 let engine = LayoutEngine::new();
8652 let font_context = FontContext::new();
8653
8654 let fixed_child = make_styled_view(
8655 Style {
8656 height: Some(Dimension::Pt(50.0)),
8657 ..Default::default()
8658 },
8659 vec![make_text("Fixed", 12.0)],
8660 );
8661 let grow_child = make_styled_view(
8662 Style {
8663 flex_grow: Some(1.0),
8664 ..Default::default()
8665 },
8666 vec![make_text("Grow", 12.0)],
8667 );
8668
8669 let container = make_styled_view(
8670 Style {
8671 flex_direction: Some(FlexDirection::Column),
8672 height: Some(Dimension::Pt(300.0)),
8673 ..Default::default()
8674 },
8675 vec![fixed_child, grow_child],
8676 );
8677
8678 let doc = Document {
8679 children: vec![Node::page(
8680 PageConfig::default(),
8681 Style::default(),
8682 vec![container],
8683 )],
8684 metadata: Default::default(),
8685 default_page: PageConfig::default(),
8686 first_page: None,
8687 left_page: None,
8688 right_page: None,
8689 named_pages: Default::default(),
8690 attachments: vec![],
8691 zugferd: None,
8692 fonts: vec![],
8693 tagged: false,
8694 pdfa: None,
8695 default_style: None,
8696 embedded_data: None,
8697 flatten_forms: false,
8698 pdf_ua: false,
8699 certification: None,
8700 pdf_version: Default::default(),
8701 pdf_ua2: false,
8702 };
8703
8704 let pages = engine.layout(&doc, &font_context);
8705 let page = &pages[0];
8706
8707 let container_el = page
8708 .elements
8709 .iter()
8710 .find(|e| e.children.len() == 2)
8711 .expect("Should find container with two children");
8712
8713 let fixed_el = &container_el.children[0];
8714 let grow_el = &container_el.children[1];
8715
8716 assert!(
8718 (fixed_el.height - 50.0).abs() < 1.0,
8719 "Fixed child should stay at 50pt, got {}",
8720 fixed_el.height
8721 );
8722
8723 assert!(
8725 (grow_el.height - 250.0).abs() < 2.0,
8726 "Grow child should expand to ~250pt, got {}",
8727 grow_el.height
8728 );
8729 }
8730
8731 #[test]
8732 fn column_flex_grow_page_level() {
8733 let engine = LayoutEngine::new();
8735 let font_context = FontContext::new();
8736
8737 let grow_child = make_styled_view(
8738 Style {
8739 flex_grow: Some(1.0),
8740 ..Default::default()
8741 },
8742 vec![make_text("Fill page", 12.0)],
8743 );
8744
8745 let doc = Document {
8746 children: vec![Node::page(
8747 PageConfig::default(),
8748 Style::default(),
8749 vec![grow_child],
8750 )],
8751 metadata: Default::default(),
8752 default_page: PageConfig::default(),
8753 first_page: None,
8754 left_page: None,
8755 right_page: None,
8756 named_pages: Default::default(),
8757 attachments: vec![],
8758 zugferd: None,
8759 fonts: vec![],
8760 tagged: false,
8761 pdfa: None,
8762 default_style: None,
8763 embedded_data: None,
8764 flatten_forms: false,
8765 pdf_ua: false,
8766 certification: None,
8767 pdf_version: Default::default(),
8768 pdf_ua2: false,
8769 };
8770
8771 let pages = engine.layout(&doc, &font_context);
8772 let page = &pages[0];
8773
8774 assert!(
8776 !page.elements.is_empty(),
8777 "Page should have at least one element"
8778 );
8779
8780 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
8781 let el = &page.elements[0];
8782 assert!(
8783 (el.height - content_height).abs() < 2.0,
8784 "Page-level flex-grow child should fill content height ({}), got {}",
8785 content_height,
8786 el.height
8787 );
8788 }
8789
8790 #[test]
8791 fn column_flex_grow_with_justify_content() {
8792 let engine = LayoutEngine::new();
8796 let font_context = FontContext::new();
8797
8798 let fixed_child = make_styled_view(
8799 Style {
8800 height: Some(Dimension::Pt(50.0)),
8801 ..Default::default()
8802 },
8803 vec![make_text("Top", 12.0)],
8804 );
8805 let grow_child = make_styled_view(
8806 Style {
8807 flex_grow: Some(1.0),
8808 ..Default::default()
8809 },
8810 vec![make_text("Fill", 12.0)],
8811 );
8812
8813 let container = make_styled_view(
8814 Style {
8815 flex_direction: Some(FlexDirection::Column),
8816 height: Some(Dimension::Pt(300.0)),
8817 justify_content: Some(JustifyContent::Center),
8818 ..Default::default()
8819 },
8820 vec![fixed_child, grow_child],
8821 );
8822
8823 let doc = Document {
8824 children: vec![Node::page(
8825 PageConfig::default(),
8826 Style::default(),
8827 vec![container],
8828 )],
8829 metadata: Default::default(),
8830 default_page: PageConfig::default(),
8831 first_page: None,
8832 left_page: None,
8833 right_page: None,
8834 named_pages: Default::default(),
8835 attachments: vec![],
8836 zugferd: None,
8837 fonts: vec![],
8838 tagged: false,
8839 pdfa: None,
8840 default_style: None,
8841 embedded_data: None,
8842 flatten_forms: false,
8843 pdf_ua: false,
8844 certification: None,
8845 pdf_version: Default::default(),
8846 pdf_ua2: false,
8847 };
8848
8849 let pages = engine.layout(&doc, &font_context);
8850 let page = &pages[0];
8851
8852 let container_el = page
8853 .elements
8854 .iter()
8855 .find(|e| e.children.len() == 2)
8856 .expect("Should find container");
8857
8858 let first_child = &container_el.children[0];
8861 assert!(
8862 (first_child.y - container_el.y).abs() < 1.0,
8863 "First child should be at top of container"
8864 );
8865
8866 let total = container_el.children[0].height + container_el.children[1].height;
8868 assert!(
8869 (total - 300.0).abs() < 2.0,
8870 "Children should fill container, got {}",
8871 total
8872 );
8873 }
8874
8875 #[test]
8876 fn column_flex_grow_child_justify_content_center() {
8877 let engine = LayoutEngine::new();
8880 let font_context = FontContext::new();
8881
8882 let inner_box = make_styled_view(
8884 Style {
8885 height: Some(Dimension::Pt(40.0)),
8886 ..Default::default()
8887 },
8888 vec![make_text("Centered", 12.0)],
8889 );
8890
8891 let grow_child = make_styled_view(
8893 Style {
8894 flex_grow: Some(1.0),
8895 flex_direction: Some(FlexDirection::Column),
8896 justify_content: Some(JustifyContent::Center),
8897 ..Default::default()
8898 },
8899 vec![inner_box],
8900 );
8901
8902 let container = make_styled_view(
8904 Style {
8905 flex_direction: Some(FlexDirection::Column),
8906 height: Some(Dimension::Pt(400.0)),
8907 ..Default::default()
8908 },
8909 vec![grow_child],
8910 );
8911
8912 let doc = Document {
8913 children: vec![Node::page(
8914 PageConfig::default(),
8915 Style::default(),
8916 vec![container],
8917 )],
8918 metadata: Default::default(),
8919 default_page: PageConfig::default(),
8920 first_page: None,
8921 left_page: None,
8922 right_page: None,
8923 named_pages: Default::default(),
8924 attachments: vec![],
8925 zugferd: None,
8926 fonts: vec![],
8927 tagged: false,
8928 pdfa: None,
8929 default_style: None,
8930 embedded_data: None,
8931 flatten_forms: false,
8932 pdf_ua: false,
8933 certification: None,
8934 pdf_version: Default::default(),
8935 pdf_ua2: false,
8936 };
8937
8938 let pages = engine.layout(&doc, &font_context);
8939 let page = &pages[0];
8940
8941 let container_el = page
8943 .elements
8944 .iter()
8945 .find(|e| e.height > 350.0 && e.children.len() == 1)
8946 .expect("Should find outer container");
8947
8948 let grow_el = &container_el.children[0];
8949 assert!(
8950 (grow_el.height - 400.0).abs() < 2.0,
8951 "Grow child should expand to 400, got {}",
8952 grow_el.height
8953 );
8954
8955 let inner_el = &grow_el.children[0];
8957 let expected_center = grow_el.y + grow_el.height / 2.0;
8958 let actual_center = inner_el.y + inner_el.height / 2.0;
8959 assert!(
8960 (actual_center - expected_center).abs() < 2.0,
8961 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
8962 expected_center,
8963 actual_center
8964 );
8965 }
8966
8967 #[test]
8968 fn column_flex_grow_child_justify_content_flex_end() {
8969 let engine = LayoutEngine::new();
8971 let font_context = FontContext::new();
8972
8973 let inner_box = make_styled_view(
8974 Style {
8975 height: Some(Dimension::Pt(30.0)),
8976 ..Default::default()
8977 },
8978 vec![make_text("Bottom", 12.0)],
8979 );
8980
8981 let grow_child = make_styled_view(
8982 Style {
8983 flex_grow: Some(1.0),
8984 flex_direction: Some(FlexDirection::Column),
8985 justify_content: Some(JustifyContent::FlexEnd),
8986 ..Default::default()
8987 },
8988 vec![inner_box],
8989 );
8990
8991 let container = make_styled_view(
8992 Style {
8993 flex_direction: Some(FlexDirection::Column),
8994 height: Some(Dimension::Pt(300.0)),
8995 ..Default::default()
8996 },
8997 vec![grow_child],
8998 );
8999
9000 let doc = Document {
9001 children: vec![Node::page(
9002 PageConfig::default(),
9003 Style::default(),
9004 vec![container],
9005 )],
9006 metadata: Default::default(),
9007 default_page: PageConfig::default(),
9008 first_page: None,
9009 left_page: None,
9010 right_page: None,
9011 named_pages: Default::default(),
9012 attachments: vec![],
9013 zugferd: None,
9014 fonts: vec![],
9015 tagged: false,
9016 pdfa: None,
9017 default_style: None,
9018 embedded_data: None,
9019 flatten_forms: false,
9020 pdf_ua: false,
9021 certification: None,
9022 pdf_version: Default::default(),
9023 pdf_ua2: false,
9024 };
9025
9026 let pages = engine.layout(&doc, &font_context);
9027 let page = &pages[0];
9028
9029 let container_el = page
9030 .elements
9031 .iter()
9032 .find(|e| e.height > 250.0 && e.children.len() == 1)
9033 .expect("Should find outer container");
9034
9035 let grow_el = &container_el.children[0];
9036 let inner_el = &grow_el.children[0];
9037
9038 let inner_bottom = inner_el.y + inner_el.height;
9040 let grow_bottom = grow_el.y + grow_el.height;
9041 assert!(
9042 (inner_bottom - grow_bottom).abs() < 2.0,
9043 "Inner box bottom ({}) should align with grow child bottom ({})",
9044 inner_bottom,
9045 grow_bottom
9046 );
9047 }
9048
9049 #[test]
9050 fn column_flex_grow_child_no_justify_unchanged() {
9051 let engine = LayoutEngine::new();
9053 let font_context = FontContext::new();
9054
9055 let inner_box = make_styled_view(
9056 Style {
9057 height: Some(Dimension::Pt(50.0)),
9058 ..Default::default()
9059 },
9060 vec![make_text("Top", 12.0)],
9061 );
9062
9063 let grow_child = make_styled_view(
9064 Style {
9065 flex_grow: Some(1.0),
9066 flex_direction: Some(FlexDirection::Column),
9067 ..Default::default()
9069 },
9070 vec![inner_box],
9071 );
9072
9073 let container = make_styled_view(
9074 Style {
9075 flex_direction: Some(FlexDirection::Column),
9076 height: Some(Dimension::Pt(300.0)),
9077 ..Default::default()
9078 },
9079 vec![grow_child],
9080 );
9081
9082 let doc = Document {
9083 children: vec![Node::page(
9084 PageConfig::default(),
9085 Style::default(),
9086 vec![container],
9087 )],
9088 metadata: Default::default(),
9089 default_page: PageConfig::default(),
9090 first_page: None,
9091 left_page: None,
9092 right_page: None,
9093 named_pages: Default::default(),
9094 attachments: vec![],
9095 zugferd: None,
9096 fonts: vec![],
9097 tagged: false,
9098 pdfa: None,
9099 default_style: None,
9100 embedded_data: None,
9101 flatten_forms: false,
9102 pdf_ua: false,
9103 certification: None,
9104 pdf_version: Default::default(),
9105 pdf_ua2: false,
9106 };
9107
9108 let pages = engine.layout(&doc, &font_context);
9109 let page = &pages[0];
9110
9111 let container_el = page
9112 .elements
9113 .iter()
9114 .find(|e| e.height > 250.0 && e.children.len() == 1)
9115 .expect("Should find outer container");
9116
9117 let grow_el = &container_el.children[0];
9118 let inner_el = &grow_el.children[0];
9119
9120 assert!(
9122 (inner_el.y - grow_el.y).abs() < 2.0,
9123 "Inner box ({}) should be at top of grow child ({})",
9124 inner_el.y,
9125 grow_el.y
9126 );
9127 }
9128
9129 #[test]
9130 fn column_flex_grow_child_align_items_center() {
9131 let engine = LayoutEngine::new();
9133 let font_context = FontContext::new();
9134
9135 let text = make_text("Hello", 12.0);
9136
9137 let grow_child = make_styled_view(
9138 Style {
9139 flex_grow: Some(1.0),
9140 flex_direction: Some(FlexDirection::Column),
9141 align_items: Some(AlignItems::Center),
9142 ..Default::default()
9143 },
9144 vec![text],
9145 );
9146
9147 let container = make_styled_view(
9148 Style {
9149 flex_direction: Some(FlexDirection::Column),
9150 height: Some(Dimension::Pt(300.0)),
9151 ..Default::default()
9152 },
9153 vec![grow_child],
9154 );
9155
9156 let doc = Document {
9157 children: vec![Node::page(
9158 PageConfig::default(),
9159 Style::default(),
9160 vec![container],
9161 )],
9162 metadata: Default::default(),
9163 default_page: PageConfig::default(),
9164 first_page: None,
9165 left_page: None,
9166 right_page: None,
9167 named_pages: Default::default(),
9168 attachments: vec![],
9169 zugferd: None,
9170 fonts: vec![],
9171 tagged: false,
9172 pdfa: None,
9173 default_style: None,
9174 embedded_data: None,
9175 flatten_forms: false,
9176 pdf_ua: false,
9177 certification: None,
9178 pdf_version: Default::default(),
9179 pdf_ua2: false,
9180 };
9181
9182 let pages = engine.layout(&doc, &font_context);
9183 let page = &pages[0];
9184
9185 let container_el = page
9186 .elements
9187 .iter()
9188 .find(|e| e.height > 250.0 && e.children.len() == 1)
9189 .expect("Should find outer container");
9190
9191 let grow_el = &container_el.children[0];
9192 assert!(
9193 !grow_el.children.is_empty(),
9194 "Grow child should have text child"
9195 );
9196
9197 let text_el = &grow_el.children[0];
9198 let text_center = text_el.x + text_el.width / 2.0;
9199 let grow_center = grow_el.x + grow_el.width / 2.0;
9200 assert!(
9201 (text_center - grow_center).abs() < 2.0,
9202 "Text center ({}) should be near grow child center ({})",
9203 text_center,
9204 grow_center
9205 );
9206 }
9207
9208 #[test]
9209 fn image_intrinsic_width_respects_height_constraint() {
9210 let engine = LayoutEngine::new();
9214 let font_context = FontContext::new();
9215
9216 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9218
9219 let image_node = Node {
9220 kind: NodeKind::Image {
9221 src: one_px_png.to_string(),
9222 width: None,
9223 height: Some(36.0),
9224 },
9225 style: Style::default(),
9226 children: vec![],
9227 id: None,
9228 source_location: None,
9229 bookmark: None,
9230 href: None,
9231 alt: None,
9232 };
9233
9234 let resolved = image_node.style.resolve(None, 0.0);
9235 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9236
9237 assert!(
9239 (intrinsic - 36.0).abs() < 1.0,
9240 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9241 intrinsic
9242 );
9243 }
9244}