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 {
2619 SizeConstraint::Fixed(fw) => fw,
2620 SizeConstraint::Auto => intrinsic,
2621 };
2622 let lw = if has_explicit_width {
2623 available_width
2624 } else {
2625 w
2626 };
2627 match align {
2628 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2629 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2630 _ => (content_x, available_width),
2631 }
2632 } else {
2633 (content_x, available_width)
2634 };
2635
2636 self.layout_node(
2637 child,
2638 cursor,
2639 pages,
2640 child_x,
2641 layout_w,
2642 parent_style,
2643 font_context,
2644 None,
2645 None,
2646 );
2647
2648 child_ranges.push((child_start, cursor.elements.len()));
2649 }
2650
2651 let container_inner_h: Option<f64> = parent_style
2654 .and_then(|ps| match ps.height {
2655 SizeConstraint::Fixed(h) => {
2656 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2657 }
2658 SizeConstraint::Auto => None,
2659 })
2660 .or_else(|| {
2661 if parent_style.is_none() {
2663 Some(cursor.content_height - start_y)
2664 } else {
2665 None
2666 }
2667 });
2668
2669 if let Some(inner_h) = container_inner_h {
2670 if pages.len() == initial_pages {
2671 let child_styles: Vec<ResolvedStyle> = items
2672 .iter()
2673 .map(|child| child.style.resolve(parent_style, available_width))
2674 .collect();
2675 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2676 if total_grow > 0.0 {
2677 let children_total = cursor.y - start_y;
2678 let slack = (inner_h - children_total).max(0.0);
2679 if slack > 0.0 {
2680 let mut cumulative_shift = 0.0_f64;
2681 for (i, cs) in child_styles.iter().enumerate() {
2682 let (start, end) = child_ranges[i];
2683 if cumulative_shift > 0.001 {
2684 for j in start..end {
2685 offset_element_y(
2686 &mut cursor.elements[j],
2687 cumulative_shift,
2688 );
2689 }
2690 }
2691 if cs.flex_grow > 0.0 {
2692 let extra = slack * (cs.flex_grow / total_grow);
2693 if start < end {
2695 let elem = &mut cursor.elements[end - 1];
2696 elem.height += extra;
2697 reapply_justify_content(elem);
2698 }
2699 cumulative_shift += extra;
2700 }
2701 }
2702 cursor.y += cumulative_shift;
2703 }
2704 }
2705 }
2706 }
2707
2708 if let Some(inner_h) = container_inner_h {
2715 if pages.len() == initial_pages {
2716 let auto_styles: Vec<ResolvedStyle> = items
2717 .iter()
2718 .map(|child| child.style.resolve(parent_style, available_width))
2719 .collect();
2720 let total_autos: usize = auto_styles
2721 .iter()
2722 .map(|s| {
2723 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2724 })
2725 .sum();
2726 if total_autos > 0 {
2727 let children_total = cursor.y - start_y;
2728 let total_slack = (inner_h - children_total).max(0.0);
2729 if total_slack > 0.0 {
2730 let per_auto = total_slack / total_autos as f64;
2731 let mut cumulative_shift = 0.0_f64;
2732 for (i, cs) in auto_styles.iter().enumerate() {
2733 let (start, end) = child_ranges[i];
2734 let mt_auto = cs.margin.top.is_auto();
2735 let mb_auto = cs.margin.bottom.is_auto();
2736 let this_child_shift =
2740 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2741 if this_child_shift > 0.001 {
2742 for j in start..end {
2743 offset_element_y(
2744 &mut cursor.elements[j],
2745 this_child_shift,
2746 );
2747 }
2748 }
2749 cumulative_shift =
2752 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2753 }
2754 cursor.y += cumulative_shift;
2755 }
2756 }
2757 }
2758 }
2759
2760 let needs_justify =
2762 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2763 if needs_justify {
2764 let justify_inner_h = container_inner_h.or_else(|| {
2766 parent_style.and_then(|ps| match ps.height {
2767 SizeConstraint::Fixed(h) => {
2768 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2769 }
2770 SizeConstraint::Auto => None,
2771 })
2772 });
2773 if let Some(inner_h) = justify_inner_h {
2774 let children_total = cursor.y - start_y;
2775 let slack = inner_h - children_total;
2776 if slack > 0.0 {
2777 let n = child_ranges.len();
2778 let offsets: Vec<f64> = match justify {
2779 JustifyContent::FlexEnd => vec![slack; n],
2780 JustifyContent::Center => vec![slack / 2.0; n],
2781 JustifyContent::SpaceBetween => {
2782 if n <= 1 {
2783 vec![0.0; n]
2784 } else {
2785 let per_gap = slack / (n - 1) as f64;
2786 (0..n).map(|i| i as f64 * per_gap).collect()
2787 }
2788 }
2789 JustifyContent::SpaceAround => {
2790 let space = slack / n as f64;
2791 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2792 }
2793 JustifyContent::SpaceEvenly => {
2794 let space = slack / (n + 1) as f64;
2795 (0..n).map(|i| (i + 1) as f64 * space).collect()
2796 }
2797 JustifyContent::FlexStart => vec![0.0; n],
2798 };
2799 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2800 let dy = offsets[i];
2801 if dy.abs() > 0.001 {
2802 for j in start..end {
2803 offset_element_y(&mut cursor.elements[j], dy);
2804 }
2805 }
2806 }
2807 cursor.y += *offsets.last().unwrap_or(&0.0);
2808 }
2809 }
2810 }
2811 }
2812
2813 FlexDirection::Row | FlexDirection::RowReverse => {
2814 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2815 self.layout_flex_row(
2816 &flow_owned,
2817 cursor,
2818 pages,
2819 content_x,
2820 available_width,
2821 parent_style,
2822 column_gap,
2823 row_gap,
2824 font_context,
2825 );
2826 }
2827 }
2828
2829 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
2843 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2844 let ph = parent_style
2845 .and_then(|ps| match ps.height {
2846 SizeConstraint::Fixed(h) => {
2847 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2848 }
2849 SizeConstraint::Auto => None,
2850 })
2851 .unwrap_or_else(|| {
2852 if parent_fragmented {
2853 let first = &pages[entry_page_index];
2854 (first.height - first.config.margin.bottom) - parent_box_y
2855 } else {
2856 cursor.content_y + cursor.y - parent_box_y
2857 }
2858 });
2859 (parent_box_x, parent_box_y, available_width, ph)
2860 } else {
2861 cursor.containing_block
2862 };
2863
2864 for abs_child in &abs_children {
2866 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2867
2868 let child_width = match abs_style.width {
2870 SizeConstraint::Fixed(w) => w,
2871 SizeConstraint::Auto => {
2872 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2874 (cb_w - l - r).max(0.0)
2875 } else {
2876 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2877 }
2878 }
2879 };
2880
2881 let child_height = match abs_style.height {
2882 SizeConstraint::Fixed(h) => h,
2883 SizeConstraint::Auto => {
2884 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2885 }
2886 };
2887
2888 let abs_margin = abs_style.margin.to_edges();
2896 let abs_x = if let Some(l) = abs_style.left {
2897 cb_x + l
2898 } else if let Some(r) = abs_style.right {
2899 cb_x + cb_w - r - child_width - abs_margin.horizontal()
2900 } else {
2901 cb_x
2902 };
2903
2904 let abs_y = if let Some(t) = abs_style.top {
2905 cb_y + t
2906 } else if let Some(b) = abs_style.bottom {
2907 cb_y + cb_h - b - child_height - abs_margin.vertical()
2908 } else {
2909 cb_y
2910 };
2911
2912 let mut abs_cursor = PageCursor::new(&cursor.config);
2914 abs_cursor.y = 0.0;
2915 abs_cursor.content_x = abs_x;
2916 abs_cursor.content_y = abs_y;
2917
2918 self.layout_node(
2919 abs_child,
2920 &mut abs_cursor,
2921 &mut Vec::new(),
2922 abs_x,
2923 child_width,
2924 parent_style,
2925 font_context,
2926 None,
2927 None,
2928 );
2929
2930 if parent_fragmented {
2934 pages[entry_page_index].elements.extend(abs_cursor.elements);
2935 } else {
2936 cursor.elements.extend(abs_cursor.elements);
2937 }
2938 }
2939
2940 cursor.containing_block = saved_cb;
2942 }
2943
2944 #[allow(clippy::too_many_arguments)]
2945 fn layout_flex_row(
2946 &self,
2947 children: &[Node],
2948 cursor: &mut PageCursor,
2949 pages: &mut Vec<LayoutPage>,
2950 content_x: f64,
2951 available_width: f64,
2952 parent_style: Option<&ResolvedStyle>,
2953 column_gap: f64,
2954 row_gap: f64,
2955 font_context: &FontContext,
2956 ) {
2957 if children.is_empty() {
2958 return;
2959 }
2960
2961 let flex_wrap = parent_style
2962 .map(|s| s.flex_wrap)
2963 .unwrap_or(FlexWrap::NoWrap);
2964
2965 let items: Vec<FlexItem> = children
2968 .iter()
2969 .map(|child| {
2970 let style = child.style.resolve(parent_style, available_width);
2971 let base_width = match style.flex_basis {
2972 SizeConstraint::Fixed(w) => w,
2973 SizeConstraint::Auto => match style.width {
2974 SizeConstraint::Fixed(w) => w,
2975 SizeConstraint::Auto => {
2976 self.measure_intrinsic_width(child, &style, font_context)
2977 }
2978 },
2979 };
2980 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2981 FlexItem {
2982 node: child,
2983 style,
2984 base_width,
2985 min_content_width,
2986 }
2987 })
2988 .collect();
2989
2990 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2992 let lines = match flex_wrap {
2993 FlexWrap::NoWrap => {
2994 vec![flex::WrapLine {
2995 start: 0,
2996 end: items.len(),
2997 }]
2998 }
2999 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
3000 FlexWrap::WrapReverse => {
3001 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
3002 l.reverse();
3003 l
3004 }
3005 };
3006
3007 if lines.is_empty() {
3008 return;
3009 }
3010
3011 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3013
3014 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3016
3017 let initial_pages_count = pages.len();
3018 let flex_start_y = cursor.y;
3019 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3020
3021 for (line_idx, line) in lines.iter().enumerate() {
3022 let line_items = &items[line.start..line.end];
3023 let line_count = line.end - line.start;
3024 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3025 let distributable = available_width - line_gap;
3026
3027 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
3029 let remaining = distributable - total_base;
3030
3031 if remaining > 0.0 {
3032 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3033 if total_grow > 0.0 {
3034 for (j, item) in line_items.iter().enumerate() {
3035 final_widths[line.start + j] =
3036 item.base_width + remaining * (item.style.flex_grow / total_grow);
3037 }
3038 }
3039 } else if remaining < 0.0 {
3040 let total_shrink: f64 = line_items
3041 .iter()
3042 .map(|i| i.style.flex_shrink * i.base_width)
3043 .sum();
3044 if total_shrink > 0.0 {
3045 for (j, item) in line_items.iter().enumerate() {
3046 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3047 let w = item.base_width + remaining * factor;
3048 let floor = item.style.min_width.max(item.min_content_width);
3049 final_widths[line.start + j] = w.max(floor);
3050 }
3051 }
3052 }
3053
3054 let mut line_height: f64 = line_items
3056 .iter()
3057 .enumerate()
3058 .map(|(j, item)| {
3059 let fw = final_widths[line.start + j];
3060 self.measure_node_height(item.node, fw, &item.style, font_context)
3061 + item.style.margin.vertical()
3062 })
3063 .fold(0.0f64, f64::max);
3064
3065 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3071 let baseline_ds: Vec<Option<f64>> = line_items
3072 .iter()
3073 .enumerate()
3074 .map(|(j, item)| {
3075 let align = item.style.align_self.unwrap_or(parent_align);
3076 if matches!(align, AlignItems::Baseline) {
3077 let fw = final_widths[line.start + j];
3078 Some(self.flex_item_baseline_distance(
3079 item.node,
3080 &item.style,
3081 fw,
3082 font_context,
3083 ))
3084 } else {
3085 None
3086 }
3087 })
3088 .collect();
3089 let line_baseline = baseline_ds
3090 .iter()
3091 .flatten()
3092 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3093 if let Some(bl) = line_baseline {
3094 for (j, item) in line_items.iter().enumerate() {
3095 if let Some(d) = baseline_ds[j] {
3096 let fw = final_widths[line.start + j];
3097 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3098 + item.style.margin.vertical();
3099 line_height = line_height.max(bl - d + h);
3100 }
3101 }
3102 }
3103
3104 if let Some(ps) = parent_style {
3115 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3116 if let SizeConstraint::Fixed(h) = ps.height {
3117 let inner = h - ps.padding.vertical() - ps.border_width.vertical();
3118 line_height = line_height.max(inner);
3119 }
3120 }
3121 }
3122
3123 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
3130 pages.push(cursor.finalize());
3131 *cursor = cursor.new_page();
3132 }
3133
3134 if line_idx > 0 {
3136 cursor.y += row_gap;
3137 }
3138
3139 let row_start_y = cursor.y;
3140
3141 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
3143 let slack = available_width - actual_total - line_gap;
3144
3145 let (start_offset, between_extra) = match justify {
3146 JustifyContent::FlexStart => (0.0, 0.0),
3147 JustifyContent::FlexEnd => (slack, 0.0),
3148 JustifyContent::Center => (slack / 2.0, 0.0),
3149 JustifyContent::SpaceBetween => {
3150 if line_count > 1 {
3151 (0.0, slack / (line_count as f64 - 1.0))
3152 } else {
3153 (0.0, 0.0)
3154 }
3155 }
3156 JustifyContent::SpaceAround => {
3157 let s = slack / line_count as f64;
3158 (s / 2.0, s)
3159 }
3160 JustifyContent::SpaceEvenly => {
3161 let s = slack / (line_count as f64 + 1.0);
3162 (s, s)
3163 }
3164 };
3165
3166 let line_elem_start = cursor.elements.len();
3167 let mut x = content_x + start_offset;
3168
3169 let line_start_pages = pages.len();
3180
3181 for (j, item) in line_items.iter().enumerate() {
3182 if j > 0 {
3183 x += column_gap + between_extra;
3184 }
3185
3186 let fw = final_widths[line.start + j];
3187
3188 let align = item
3189 .style
3190 .align_self
3191 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3192
3193 let item_height =
3194 self.measure_node_height(item.node, fw, &item.style, font_context);
3195
3196 let has_auto_v = item.style.margin.has_auto_vertical();
3198 let y_offset = if has_auto_v {
3199 let fixed_v = item.style.margin.vertical();
3200 let slack = (line_height - item_height - fixed_v).max(0.0);
3201 let auto_top = item.style.margin.top.is_auto();
3202 let auto_bottom = item.style.margin.bottom.is_auto();
3203 match (auto_top, auto_bottom) {
3204 (true, true) => slack / 2.0,
3205 (true, false) => slack,
3206 (false, true) => 0.0,
3207 (false, false) => 0.0,
3208 }
3209 } else {
3210 match align {
3211 AlignItems::FlexStart => 0.0,
3212 AlignItems::FlexEnd => {
3213 line_height - item_height - item.style.margin.vertical()
3214 }
3215 AlignItems::Center => {
3216 (line_height - item_height - item.style.margin.vertical()) / 2.0
3217 }
3218 AlignItems::Stretch => 0.0,
3219 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3220 (Some(bl), Some(d)) => bl - d,
3221 _ => 0.0,
3222 },
3223 }
3224 };
3225
3226 let cross_h = if matches!(align, AlignItems::Stretch)
3230 && matches!(item.style.height, SizeConstraint::Auto)
3231 && !has_auto_v
3232 {
3233 let stretch_h = line_height - item.style.margin.vertical();
3234 if stretch_h > item_height {
3235 Some(stretch_h)
3236 } else {
3237 None
3238 }
3239 } else {
3240 None
3241 };
3242
3243 let saved_y = cursor.y;
3244 cursor.y = row_start_y + y_offset;
3245
3246 self.layout_node(
3247 item.node,
3248 cursor,
3249 pages,
3250 x,
3251 available_width,
3252 parent_style,
3253 font_context,
3254 cross_h,
3255 Some(fw),
3256 );
3257
3258 cursor.y = saved_y;
3259 x += fw;
3260 }
3261
3262 if line_items.len() > 1 && pages.len() > line_start_pages {
3263 let near = line_items
3264 .iter()
3265 .find_map(|it| first_text_snippet(it.node))
3266 .map(|t| format!(" (row beginning \"{t}\")"))
3267 .unwrap_or_default();
3268 self.defect(format!(
3269 "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}"
3270 ));
3271 }
3272 cursor.y = row_start_y + line_height;
3273 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3274 }
3275
3276 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3278 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3279 if !matches!(align_content, AlignContent::FlexStart)
3280 && !matches!(flex_wrap, FlexWrap::NoWrap)
3281 {
3282 if let Some(parent) = parent_style {
3283 if let SizeConstraint::Fixed(container_h) = parent.height {
3284 let inner_h = container_h
3285 - parent.padding.vertical()
3286 - parent.border_width.vertical();
3287 let total_used = cursor.y - flex_start_y;
3288 let slack = inner_h - total_used;
3289 if slack > 0.0 {
3290 let n = line_infos.len();
3291 let offsets: Vec<f64> = match align_content {
3292 AlignContent::FlexEnd => vec![slack; n],
3293 AlignContent::Center => vec![slack / 2.0; n],
3294 AlignContent::SpaceBetween => {
3295 if n <= 1 {
3296 vec![0.0; n]
3297 } else {
3298 let per_gap = slack / (n - 1) as f64;
3299 (0..n).map(|i| i as f64 * per_gap).collect()
3300 }
3301 }
3302 AlignContent::SpaceAround => {
3303 let space = slack / n as f64;
3304 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3305 }
3306 AlignContent::SpaceEvenly => {
3307 let space = slack / (n + 1) as f64;
3308 (0..n).map(|i| (i + 1) as f64 * space).collect()
3309 }
3310 AlignContent::Stretch => {
3311 let extra = slack / n as f64;
3312 (0..n).map(|i| i as f64 * extra).collect()
3313 }
3314 AlignContent::FlexStart => vec![0.0; n],
3315 };
3316 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3317 let dy = offsets[i];
3318 if dy.abs() > 0.001 {
3319 for j in start..end {
3320 offset_element_y(&mut cursor.elements[j], dy);
3321 }
3322 }
3323 }
3324 cursor.y += *offsets.last().unwrap_or(&0.0);
3325 }
3326 }
3327 }
3328 }
3329 }
3330 }
3331
3332 #[allow(clippy::too_many_arguments)]
3335 fn layout_list(
3336 &self,
3337 node: &Node,
3338 ordered: bool,
3339 marker_type: ListMarkerType,
3340 start: u32,
3341 style: &ResolvedStyle,
3342 cursor: &mut PageCursor,
3343 pages: &mut Vec<LayoutPage>,
3344 x: f64,
3345 available_width: f64,
3346 font_context: &FontContext,
3347 ) {
3348 let margin = &style.margin.to_edges();
3349 let padding = &style.padding;
3350
3351 cursor.y += margin.top;
3352
3353 let list_x = x + margin.left;
3354 let outer_width = available_width - margin.horizontal();
3355 let inner_width = outer_width - padding.horizontal();
3356
3357 let n_items = node
3360 .children
3361 .iter()
3362 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3363 .count() as u32;
3364
3365 let marker_gutter =
3366 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3367
3368 let list_inner_x = list_x + padding.left;
3369 let content_x = list_inner_x + marker_gutter;
3370 let content_width = (inner_width - marker_gutter).max(0.0);
3371
3372 let snapshot = cursor.elements.len();
3375 let list_start_y = cursor.content_y + cursor.y;
3376 cursor.y += padding.top;
3377
3378 let mut item_index: u32 = 0;
3379 for child in &node.children {
3380 if !matches!(child.kind, NodeKind::ListItem) {
3381 continue;
3382 }
3383 let marker_idx = start + item_index;
3384 self.layout_list_item(
3385 child,
3386 marker_idx,
3387 ordered,
3388 marker_type,
3389 marker_gutter,
3390 style,
3391 cursor,
3392 pages,
3393 list_inner_x,
3394 content_x,
3395 content_width,
3396 font_context,
3397 );
3398 item_index += 1;
3399 }
3400
3401 cursor.y += padding.bottom;
3402
3403 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3405 let list_height = cursor.content_y + cursor.y - list_start_y;
3406 cursor.elements.push(LayoutElement {
3407 x: list_x,
3408 y: list_start_y,
3409 width: outer_width,
3410 height: list_height,
3411 draw: DrawCommand::None,
3412 children: item_elements,
3413 node_type: Some("List".to_string()),
3414 resolved_style: Some(style.clone()),
3415 source_location: node.source_location.clone(),
3416 href: None,
3417 bookmark: node.bookmark.clone(),
3418 alt: None,
3419 is_header_row: false,
3420 actual_text: None,
3424 list_numbering: match marker_type {
3425 ListMarkerType::None => None,
3426 ListMarkerType::Disc => Some("Disc"),
3427 ListMarkerType::Circle => Some("Circle"),
3428 ListMarkerType::Square => Some("Square"),
3429 ListMarkerType::Decimal => Some("Decimal"),
3430 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3431 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3432 ListMarkerType::LowerRoman => Some("LowerRoman"),
3433 ListMarkerType::UpperRoman => Some("UpperRoman"),
3434 },
3435 col_span: 1,
3436 overflow: style.overflow,
3437 opacity: style.opacity,
3438 });
3439
3440 cursor.y += margin.bottom;
3441 }
3442
3443 #[allow(clippy::too_many_arguments)]
3444 fn layout_list_item(
3445 &self,
3446 item: &Node,
3447 marker_idx: u32,
3448 ordered: bool,
3449 marker_type: ListMarkerType,
3450 marker_gutter: f64,
3451 parent_style: &ResolvedStyle,
3452 cursor: &mut PageCursor,
3453 pages: &mut Vec<LayoutPage>,
3454 list_inner_x: f64,
3455 content_x: f64,
3456 content_width: f64,
3457 font_context: &FontContext,
3458 ) {
3459 let item_style = item.style.resolve(Some(parent_style), content_width);
3460 let item_margin = item_style.margin.to_edges();
3461
3462 cursor.y += item_margin.top;
3463 let item_start_y = cursor.content_y + cursor.y;
3464 let item_snapshot = cursor.elements.len();
3465
3466 let marker_str = format_marker(marker_idx, ordered, marker_type);
3470 if !marker_str.is_empty() {
3471 let saved_y = cursor.y;
3472 self.layout_text(
3473 &marker_str,
3474 None,
3475 &[],
3476 &item_style,
3477 cursor,
3478 pages,
3479 list_inner_x,
3480 marker_gutter,
3481 font_context,
3482 None,
3483 None,
3484 Some("Lbl"),
3485 );
3486 cursor.y = saved_y;
3487 }
3488
3489 self.layout_children(
3495 &item.children,
3496 &item.style,
3497 cursor,
3498 pages,
3499 content_x,
3500 content_width,
3501 Some(&item_style),
3502 font_context,
3503 );
3504
3505 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3508 let item_height = cursor.content_y + cursor.y - item_start_y;
3509 let item_width = content_x + content_width - list_inner_x;
3510 cursor.elements.push(LayoutElement {
3511 x: list_inner_x,
3512 y: item_start_y,
3513 width: item_width,
3514 height: item_height,
3515 draw: DrawCommand::None,
3516 children: item_children,
3517 node_type: Some("ListItem".to_string()),
3518 resolved_style: Some(item_style.clone()),
3519 source_location: item.source_location.clone(),
3520 href: None,
3521 bookmark: item.bookmark.clone(),
3522 alt: None,
3523 is_header_row: false,
3524 actual_text: None,
3525 list_numbering: None,
3526 col_span: 1,
3527 overflow: item_style.overflow,
3528 opacity: item_style.opacity,
3529 });
3530
3531 cursor.y += item_margin.bottom;
3532 }
3533
3534 #[allow(clippy::too_many_arguments)]
3535 fn layout_table(
3536 &self,
3537 node: &Node,
3538 style: &ResolvedStyle,
3539 column_defs: &[ColumnDef],
3540 cursor: &mut PageCursor,
3541 pages: &mut Vec<LayoutPage>,
3542 x: f64,
3543 available_width: f64,
3544 font_context: &FontContext,
3545 ) {
3546 let padding = &style.padding;
3547 let margin = &style.margin.to_edges();
3548 let border = &style.border_width;
3549
3550 let table_x = x + margin.left;
3551 let table_width = match style.width {
3552 SizeConstraint::Fixed(w) => w,
3553 SizeConstraint::Auto => available_width - margin.horizontal(),
3554 };
3555 let inner_width = table_width - padding.horizontal() - border.horizontal();
3556
3557 let col_widths = self.resolve_column_widths(
3558 column_defs,
3559 inner_width,
3560 &node.children,
3561 style,
3562 font_context,
3563 );
3564
3565 let all_offsets = Self::table_column_offsets(&node.children);
3569 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
3570 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
3571
3572 for (child, offs) in node.children.iter().zip(&all_offsets) {
3573 match &child.kind {
3574 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
3575 _ => body_rows.push((child, offs)),
3576 }
3577 }
3578
3579 if !style.breakable {
3585 let total_height: f64 = node
3586 .children
3587 .iter()
3588 .zip(&all_offsets)
3589 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3590 .sum::<f64>()
3591 + padding.vertical()
3592 + border.vertical();
3593 let fresh_page_available = cursor.content_height
3594 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3595 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3596 if total_height > cursor.remaining_height()
3597 && total_height <= fresh_page_available
3598 && cursor.y > 0.0
3599 {
3600 pages.push(cursor.finalize());
3601 *cursor = cursor.new_page();
3602 }
3603 }
3604
3605 let initial_page_count = pages.len();
3612 let snapshot = cursor.elements.len();
3613 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3614
3615 cursor.y += margin.top + padding.top + border.top;
3616
3617 let cell_x_start = table_x + padding.left + border.left;
3618
3619 if !header_rows.is_empty() {
3644 let total_header_h: f64 = header_rows
3645 .iter()
3646 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3647 .sum();
3648 let first_body_h = body_rows
3649 .first()
3650 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3651 .unwrap_or(0.0);
3652
3653 let needed = total_header_h + first_body_h;
3654 let fresh_page_available = cursor.content_height
3655 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3656 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3657
3658 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3659 pages.push(cursor.finalize());
3660 *cursor = cursor.new_page();
3661 cursor.y += padding.top + border.top;
3662 }
3663 }
3664
3665 for (header_row, offs) in &header_rows {
3666 self.layout_table_row(
3667 header_row,
3668 &col_widths,
3669 offs,
3670 style,
3671 cursor,
3672 cell_x_start,
3673 font_context,
3674 pages,
3675 );
3676 }
3677
3678 for (body_row, offs) in &body_rows {
3679 let row_height =
3680 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
3681
3682 let fresh_page_available = cursor.content_height
3690 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3691 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3692 if row_height > fresh_page_available {
3693 self.defect(format!(
3694 "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",
3695 ));
3696 }
3697 if row_height > cursor.remaining_height()
3698 && cursor.remaining_height() < fresh_page_available - 0.5
3699 {
3700 pages.push(cursor.finalize());
3701 *cursor = cursor.new_page();
3702
3703 cursor.y += padding.top + border.top;
3704 for (header_row, h_offs) in &header_rows {
3705 self.layout_table_row(
3706 header_row,
3707 &col_widths,
3708 h_offs,
3709 style,
3710 cursor,
3711 cell_x_start,
3712 font_context,
3713 pages,
3714 );
3715 }
3716 }
3717
3718 self.layout_table_row(
3719 body_row,
3720 &col_widths,
3721 offs,
3722 style,
3723 cursor,
3724 cell_x_start,
3725 font_context,
3726 pages,
3727 );
3728 }
3729
3730 let has_visual = style.background_color.is_some()
3734 || style.background.is_some()
3735 || style.border_width.top > 0.0
3736 || style.border_width.right > 0.0
3737 || style.border_width.bottom > 0.0
3738 || style.border_width.left > 0.0;
3739 let draw_cmd = if has_visual {
3740 DrawCommand::Rect {
3741 background: style.background_color,
3742 border_width: style.border_width,
3743 border_color: style.border_color,
3744 border_style: style.border_style,
3745 border_radius: style.border_radius,
3746 opacity: 1.0,
3747 box_shadow: style.box_shadow.map(Box::new),
3748 background_gradient: style.background.clone().map(Box::new),
3749 }
3750 } else {
3751 DrawCommand::None
3752 };
3753 let make_wrapper =
3754 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3755 x: table_x,
3756 y,
3757 width: table_width,
3758 height,
3759 draw,
3760 children,
3761 node_type: Some(node_kind_name(&node.kind).to_string()),
3762 resolved_style: Some(style.clone()),
3763 source_location: node.source_location.clone(),
3764 href: node.href.clone(),
3765 bookmark: None,
3766 alt: None,
3767 is_header_row: false,
3768 actual_text: None,
3769 list_numbering: None,
3770 col_span: 1,
3771 overflow: Overflow::default(),
3772 opacity: style.opacity,
3773 };
3774
3775 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3776
3777 if pages.len() == initial_page_count {
3778 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3780 cursor.elements.push(make_wrapper(
3781 rect_start_y,
3782 table_bottom_y - rect_start_y,
3783 child_elements,
3784 draw_cmd,
3785 ));
3786 } else {
3787 let page = &mut pages[initial_page_count];
3806 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3807 let page_content_bottom =
3808 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3809 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3810 if !our_elements.is_empty() {
3811 page.elements.push(make_wrapper(
3812 rect_start_y,
3813 page_content_bottom - rect_start_y,
3814 our_elements,
3815 draw_cmd.clone(),
3816 ));
3817 }
3818
3819 for page in &mut pages[initial_page_count + 1..] {
3821 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3822 let content_top = page.config.margin.top + header_h;
3823 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3824 let content_bottom = page.config.margin.top
3825 + (page.height - page.config.margin.vertical())
3826 - footer_h;
3827 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3828 if !all_elements.is_empty() {
3829 page.elements.push(make_wrapper(
3830 content_top,
3831 content_bottom - content_top,
3832 all_elements,
3833 draw_cmd.clone(),
3834 ));
3835 }
3836 }
3837
3838 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3840 if !all_elements.is_empty() {
3841 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3842 let content_top = cursor.content_y + header_h;
3843 cursor.elements.push(make_wrapper(
3844 content_top,
3845 table_bottom_y - content_top,
3846 all_elements,
3847 draw_cmd,
3848 ));
3849 }
3850 }
3851
3852 cursor.y += padding.bottom + border.bottom + margin.bottom;
3853 }
3854
3855 fn subtree_forces_break(node: &Node) -> bool {
3865 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
3866 return true;
3867 }
3868 if node.style.break_before == Some(true) {
3869 return true;
3870 }
3871 node.children.iter().any(Self::subtree_forces_break)
3872 }
3873
3874 fn occupancy_column_count(children: &[Node]) -> usize {
3880 let offsets = Self::table_column_offsets(children);
3881 children
3882 .iter()
3883 .zip(&offsets)
3884 .map(|(row, offs)| {
3885 row.children
3886 .iter()
3887 .zip(offs)
3888 .map(|(cell, &start)| start + Self::cell_col_span(cell))
3889 .max()
3890 .unwrap_or(0)
3891 })
3892 .max()
3893 .unwrap_or(1)
3894 .max(1)
3895 }
3896
3897 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
3907 fn spans(cell: &Node) -> (usize, u32) {
3908 match &cell.kind {
3909 NodeKind::TableCell { col_span, row_span } => {
3910 ((*col_span).max(1) as usize, (*row_span).max(1))
3911 }
3912 _ => (1, 1),
3913 }
3914 }
3915 let mut pending: Vec<u32> = Vec::new();
3916 let mut out = Vec::with_capacity(rows.len());
3917 for row in rows {
3918 let mut offsets = Vec::with_capacity(row.children.len());
3919 let mut col = 0usize;
3920 for cell in &row.children {
3921 let (span, rspan) = spans(cell);
3922 while pending.get(col).copied().unwrap_or(0) > 0 {
3923 col += 1;
3924 }
3925 offsets.push(col);
3926 if rspan > 1 {
3927 if pending.len() < col + span {
3928 pending.resize(col + span, 0);
3929 }
3930 for slot in pending.iter_mut().take(col + span).skip(col) {
3931 *slot = (*slot).max(rspan);
3932 }
3933 }
3934 col += span;
3935 }
3936 out.push(offsets);
3937 for p in pending.iter_mut() {
3938 *p = p.saturating_sub(1);
3939 }
3940 }
3941 out
3942 }
3943
3944 #[allow(clippy::too_many_arguments)]
3945 fn layout_table_row(
3946 &self,
3947 row: &Node,
3948 col_widths: &[f64],
3949 col_offsets: &[usize],
3950 parent_style: &ResolvedStyle,
3951 cursor: &mut PageCursor,
3952 start_x: f64,
3953 font_context: &FontContext,
3954 pages: &mut Vec<LayoutPage>,
3955 ) {
3956 let row_style = row
3957 .style
3958 .resolve(Some(parent_style), col_widths.iter().sum());
3959
3960 let row_height =
3961 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
3962 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
3963 let row_y = cursor.content_y + cursor.y;
3964 let total_width: f64 = col_widths.iter().sum();
3965
3966 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3967
3968 let row_snapshot = cursor.elements.len();
3970
3971 let row_may_break =
3983 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
3984
3985 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3986 for (cell_i, cell) in row.children.iter().enumerate() {
3990 let span = match &cell.kind {
3991 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
3992 _ => 1,
3993 };
3994 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
3995 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
3996 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
3997
3998 let cell_style = cell.style.resolve(Some(&row_style), col_width);
3999
4000 let cell_snapshot = cursor.elements.len();
4002
4003 let inner_width =
4004 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4005
4006 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4007 let saved_y = cursor.y;
4008 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4009
4010 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4014 let content_h: f64 = cell
4015 .children
4016 .iter()
4017 .map(|ch| {
4018 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4019 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4020 })
4021 .sum();
4022 let inner_row =
4023 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4024 let slack = (inner_row - content_h).max(0.0);
4025 cursor.y += match cell_style.vertical_align {
4026 crate::style::VerticalAlign::Middle => slack / 2.0,
4027 crate::style::VerticalAlign::Bottom => slack,
4028 crate::style::VerticalAlign::Baseline => row_bl
4033 .map(|b| {
4034 let d = self.cell_baseline_distance(
4035 cell,
4036 &cell_style,
4037 inner_width,
4038 font_context,
4039 );
4040 (b - d).max(0.0)
4041 })
4042 .unwrap_or(0.0),
4043 crate::style::VerticalAlign::Top => 0.0,
4044 };
4045 }
4046
4047 let cursor_before_cell = if row_may_break {
4052 Some(cursor.clone())
4053 } else {
4054 None
4055 };
4056 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4057 for child in &cell.children {
4058 self.layout_node(
4059 child,
4060 cursor,
4061 &mut cell_pages,
4062 content_x,
4063 inner_width,
4064 Some(&cell_style),
4065 font_context,
4066 None,
4067 None,
4068 );
4069 }
4070
4071 if !cell_pages.is_empty() {
4073 let post_break_elements = std::mem::take(&mut cursor.elements);
4074 if let Some(last_page) = cell_pages.last_mut() {
4075 last_page.elements.extend(post_break_elements);
4076 }
4077 if !is_header {
4083 all_overflow_pages.extend(cell_pages);
4084 }
4085 *cursor = cursor_before_cell
4091 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4092 }
4093
4094 cursor.y = saved_y;
4095
4096 let cell_children: Vec<LayoutElement> =
4098 cursor.elements.drain(cell_snapshot..).collect();
4099
4100 cursor.elements.push(LayoutElement {
4102 x: cell_x,
4103 y: row_y,
4104 width: col_width,
4105 height: row_height,
4106 draw: if cell_style.background_color.is_some()
4107 || cell_style.border_width.horizontal() > 0.0
4108 || cell_style.border_width.vertical() > 0.0
4109 {
4110 DrawCommand::Rect {
4111 background: cell_style.background_color,
4112 border_width: cell_style.border_width,
4113 border_color: cell_style.border_color,
4114 border_style: cell_style.border_style,
4115 border_radius: cell_style.border_radius,
4116 opacity: 1.0,
4117 box_shadow: cell_style.box_shadow.map(Box::new),
4118 background_gradient: cell_style.background.clone().map(Box::new),
4119 }
4120 } else {
4121 DrawCommand::None
4122 },
4123 children: cell_children,
4124 node_type: Some("TableCell".to_string()),
4125 resolved_style: Some(cell_style.clone()),
4126 source_location: cell.source_location.clone(),
4127 href: None,
4128 bookmark: cell.bookmark.clone(),
4129 alt: None,
4130 is_header_row: is_header,
4131 actual_text: None,
4132 list_numbering: None,
4133 col_span: span as u32,
4134 overflow: Overflow::default(),
4135 opacity: 1.0,
4136 });
4137 }
4138
4139 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
4141 let row_element = LayoutElement {
4142 x: start_x,
4143 y: row_y,
4144 width: total_width,
4145 height: row_height,
4146 draw: if let Some(bg) = row_style.background_color {
4147 DrawCommand::Rect {
4148 background: Some(bg),
4149 border_width: Edges::default(),
4150 border_color: EdgeValues::uniform(Color::BLACK),
4151 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4152 border_radius: CornerValues::uniform(0.0),
4153 opacity: 1.0,
4154 box_shadow: row_style.box_shadow.map(Box::new),
4155 background_gradient: row_style.background.clone().map(Box::new),
4156 }
4157 } else {
4158 DrawCommand::None
4159 },
4160 children: row_children,
4161 node_type: Some("TableRow".to_string()),
4162 resolved_style: Some(row_style.clone()),
4163 source_location: row.source_location.clone(),
4164 href: None,
4165 bookmark: row.bookmark.clone(),
4166 alt: None,
4167 is_header_row: is_header,
4168 actual_text: None,
4169 list_numbering: None,
4170 col_span: 1,
4171 overflow: row_style.overflow,
4172 opacity: row_style.opacity,
4173 };
4174
4175 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4176 first_overflow.elements.push(row_element);
4186 } else {
4187 cursor.elements.push(row_element);
4188 cursor.y += row_height;
4189 }
4190
4191 pages.extend(all_overflow_pages);
4193 }
4194
4195 #[allow(clippy::too_many_arguments)]
4196 #[allow(clippy::too_many_arguments)]
4197 fn layout_text(
4198 &self,
4199 content: &str,
4200 href: Option<&str>,
4201 runs: &[TextRun],
4202 style: &ResolvedStyle,
4203 cursor: &mut PageCursor,
4204 pages: &mut Vec<LayoutPage>,
4205 x: f64,
4206 available_width: f64,
4207 font_context: &FontContext,
4208 source_location: Option<&SourceLocation>,
4209 bookmark: Option<&str>,
4210 node_type_override: Option<&str>,
4214 ) {
4215 if style.border_width.top > 0.0
4222 || style.border_width.right > 0.0
4223 || style.border_width.bottom > 0.0
4224 || style.border_width.left > 0.0
4225 {
4226 self.defect(
4227 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4228 );
4229 }
4230 if style.background_color.is_some() {
4231 self.defect(
4232 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4233 );
4234 }
4235 let margin = &style.margin.to_edges();
4236 let text_x = x + margin.left;
4237 let text_width = match style.width {
4244 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4245 SizeConstraint::Auto => available_width - margin.horizontal(),
4246 };
4247
4248 cursor.y += margin.top;
4249
4250 if !runs.is_empty() {
4252 self.layout_text_runs(
4253 runs,
4254 href,
4255 style,
4256 cursor,
4257 pages,
4258 text_x,
4259 text_width,
4260 font_context,
4261 source_location,
4262 bookmark,
4263 node_type_override,
4264 );
4265 cursor.y += margin.bottom;
4266 return;
4267 }
4268
4269 let content = substitute_page_placeholders(content);
4270 let transformed = apply_text_transform(&content, style.text_transform);
4271 let justify = matches!(style.text_align, TextAlign::Justify);
4272 let lines = match style.line_breaking {
4273 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4274 font_context,
4275 &transformed,
4276 text_width,
4277 style.font_size,
4278 &style.font_family,
4279 style.font_weight,
4280 style.font_style,
4281 style.letter_spacing,
4282 style.hyphens,
4283 style.lang.as_deref(),
4284 justify,
4285 ),
4286 LineBreaking::Greedy => self.text_layout.break_into_lines(
4287 font_context,
4288 &transformed,
4289 text_width,
4290 style.font_size,
4291 &style.font_family,
4292 style.font_weight,
4293 style.font_style,
4294 style.letter_spacing,
4295 style.hyphens,
4296 style.lang.as_deref(),
4297 ),
4298 };
4299
4300 let lines = match style.text_overflow {
4302 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4303 font_context,
4304 lines,
4305 text_width,
4306 style.font_size,
4307 &style.font_family,
4308 style.font_weight,
4309 style.font_style,
4310 style.letter_spacing,
4311 ),
4312 TextOverflow::Clip => self.text_layout.truncate_clip(
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::Wrap => lines,
4323 };
4324
4325 let line_height = style.font_size * style.line_height;
4326
4327 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4329 let decision = page_break::decide_break(
4330 cursor.remaining_height(),
4331 &line_heights,
4332 true,
4333 style.min_orphan_lines as usize,
4334 style.min_widow_lines as usize,
4335 );
4336
4337 let mut snapshot = cursor.elements.len();
4339 let mut container_start_y = cursor.content_y + cursor.y;
4340 let mut is_first_element = true;
4341
4342 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4344 pages.push(cursor.finalize());
4345 *cursor = cursor.new_page();
4346 snapshot = cursor.elements.len();
4347 container_start_y = cursor.content_y + cursor.y;
4348 }
4349
4350 let forced_break_at = match decision {
4352 page_break::BreakDecision::Split {
4353 items_on_current_page,
4354 } => Some(items_on_current_page),
4355 _ => None,
4356 };
4357 let mut first_break_done = false;
4358
4359 for (line_idx, line) in lines.iter().enumerate() {
4360 let needs_break = if let Some(break_at) = forced_break_at {
4362 if !first_break_done && line_idx == break_at {
4363 true
4364 } else {
4365 line_height > cursor.remaining_height()
4366 }
4367 } else {
4368 line_height > cursor.remaining_height()
4369 };
4370
4371 if needs_break {
4372 first_break_done = true;
4373 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4375 if !line_elements.is_empty() {
4376 let container_height = cursor.content_y + cursor.y - container_start_y;
4377 cursor.elements.push(LayoutElement {
4378 x: text_x,
4379 y: container_start_y,
4380 width: text_width,
4381 height: container_height,
4382 draw: DrawCommand::None,
4383 children: line_elements,
4384 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4385 resolved_style: Some(style.clone()),
4386 source_location: source_location.cloned(),
4387 href: href.map(|s| s.to_string()),
4388 bookmark: if is_first_element {
4389 bookmark.map(|s| s.to_string())
4390 } else {
4391 None
4392 },
4393 alt: None,
4394 is_header_row: false,
4395 actual_text: None,
4396 list_numbering: None,
4397 col_span: 1,
4398 overflow: Overflow::default(),
4399 opacity: 1.0,
4400 });
4401 is_first_element = false;
4402 }
4403
4404 pages.push(cursor.finalize());
4405 *cursor = cursor.new_page();
4406
4407 snapshot = cursor.elements.len();
4409 container_start_y = cursor.content_y + cursor.y;
4410 }
4411
4412 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4413
4414 let rendered_width = if glyphs.is_empty() {
4418 line.width
4419 } else {
4420 let last = &glyphs[glyphs.len() - 1];
4421 (last.x_offset + last.x_advance).max(line.width * 0.5)
4422 };
4423
4424 let line_x = match style.text_align {
4425 TextAlign::Left => text_x,
4426 TextAlign::Right => text_x + text_width - rendered_width,
4427 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4428 TextAlign::Justify => text_x,
4429 };
4430
4431 let is_last_line = line_idx == lines.len() - 1;
4439 let user_ws = style.word_spacing;
4440 let (justified_width, word_spacing) =
4441 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4442 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4443 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4444 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4445 let s = glyphs[..=idx]
4446 .iter()
4447 .filter(|g| g.char_value == ' ')
4448 .count();
4449 (w, s)
4450 } else {
4451 (0.0, 0)
4452 };
4453 let slack = text_width - natural_width;
4454 let ws = if space_count > 0 && slack.abs() > 0.01 {
4455 slack / space_count as f64
4456 } else {
4457 0.0
4458 };
4459 (text_width, user_ws + ws)
4460 } else {
4461 (rendered_width, user_ws)
4462 };
4463
4464 let text_line = TextLine {
4465 x: line_x,
4466 y: cursor.content_y
4472 + cursor.y
4473 + baseline_in_line(
4474 line_height,
4475 style.font_size,
4476 font_context.baseline_metrics(
4477 &style.font_family,
4478 style.font_weight,
4479 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4480 ),
4481 ),
4482 glyphs,
4483 width: justified_width,
4484 height: line_height,
4485 word_spacing,
4486 };
4487
4488 cursor.elements.push(LayoutElement {
4489 x: line_x,
4490 y: cursor.content_y + cursor.y,
4491 width: justified_width,
4492 height: line_height,
4493 draw: DrawCommand::Text {
4494 lines: vec![text_line],
4495 color: style.color,
4496 text_decoration: style.text_decoration,
4497 opacity: 1.0,
4498 },
4499 children: vec![],
4500 node_type: Some("TextLine".to_string()),
4501 resolved_style: Some(style.clone()),
4502 source_location: None,
4503 href: href.map(|s| s.to_string()),
4504 bookmark: None,
4505 alt: None,
4506 is_header_row: false,
4507 actual_text: None,
4508 list_numbering: None,
4509 col_span: 1,
4510 overflow: Overflow::default(),
4511 opacity: 1.0,
4512 });
4513
4514 cursor.y += line_height;
4515 }
4516
4517 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4519 if !line_elements.is_empty() {
4520 let container_height = cursor.content_y + cursor.y - container_start_y;
4521 cursor.elements.push(LayoutElement {
4522 x: text_x,
4523 y: container_start_y,
4524 width: text_width,
4525 height: container_height,
4526 draw: DrawCommand::None,
4527 children: line_elements,
4528 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4529 resolved_style: Some(style.clone()),
4530 source_location: source_location.cloned(),
4531 href: href.map(|s| s.to_string()),
4532 bookmark: if is_first_element {
4533 bookmark.map(|s| s.to_string())
4534 } else {
4535 None
4536 },
4537 alt: None,
4538 is_header_row: false,
4539 actual_text: None,
4540 list_numbering: None,
4541 col_span: 1,
4542 overflow: Overflow::default(),
4543 opacity: 1.0,
4544 });
4545 }
4546
4547 cursor.y += margin.bottom;
4548 }
4549
4550 #[allow(clippy::too_many_arguments)]
4552 #[allow(clippy::too_many_arguments)]
4553 fn layout_text_runs(
4554 &self,
4555 runs: &[TextRun],
4556 parent_href: Option<&str>,
4557 style: &ResolvedStyle,
4558 cursor: &mut PageCursor,
4559 pages: &mut Vec<LayoutPage>,
4560 text_x: f64,
4561 text_width: f64,
4562 font_context: &FontContext,
4563 source_location: Option<&SourceLocation>,
4564 bookmark: Option<&str>,
4565 node_type_override: Option<&str>,
4567 ) {
4568 let mut styled_chars: Vec<StyledChar> = Vec::new();
4570 for run in runs {
4571 let run_style = run.style.resolve(Some(style), text_width);
4572 let run_href = run.href.as_deref().or(parent_href);
4573 let transform = run_style.text_transform;
4574 let run_content = substitute_page_placeholders(&run.content);
4575 let mut prev_is_whitespace = true;
4576 for ch in run_content.chars() {
4577 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4578 prev_is_whitespace = ch.is_whitespace();
4579 styled_chars.push(StyledChar {
4580 ch: transformed_ch,
4581 font_family: run_style.font_family.clone(),
4582 font_size: run_style.font_size,
4583 font_weight: run_style.font_weight,
4584 font_style: run_style.font_style,
4585 color: run_style.color,
4586 href: run_href.map(|s| s.to_string()),
4587 text_decoration: run_style.text_decoration,
4588 letter_spacing: run_style.letter_spacing,
4589 });
4590 }
4591 }
4592
4593 let justify = matches!(style.text_align, TextAlign::Justify);
4595 let broken_lines = match style.line_breaking {
4596 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4597 font_context,
4598 &styled_chars,
4599 text_width,
4600 style.hyphens,
4601 style.lang.as_deref(),
4602 justify,
4603 ),
4604 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4605 font_context,
4606 &styled_chars,
4607 text_width,
4608 style.hyphens,
4609 style.lang.as_deref(),
4610 ),
4611 };
4612
4613 let broken_lines = match style.text_overflow {
4615 TextOverflow::Ellipsis => {
4616 self.text_layout
4617 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4618 }
4619 TextOverflow::Clip => {
4620 self.text_layout
4621 .truncate_runs_clip(font_context, broken_lines, text_width)
4622 }
4623 TextOverflow::Wrap => broken_lines,
4624 };
4625
4626 let line_height = style.font_size * style.line_height;
4627
4628 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4630 let decision = page_break::decide_break(
4631 cursor.remaining_height(),
4632 &line_heights,
4633 true,
4634 style.min_orphan_lines as usize,
4635 style.min_widow_lines as usize,
4636 );
4637
4638 let mut snapshot = cursor.elements.len();
4639 let mut container_start_y = cursor.content_y + cursor.y;
4640 let mut is_first_element = true;
4641
4642 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4643 pages.push(cursor.finalize());
4644 *cursor = cursor.new_page();
4645 snapshot = cursor.elements.len();
4646 container_start_y = cursor.content_y + cursor.y;
4647 }
4648
4649 let forced_break_at = match decision {
4650 page_break::BreakDecision::Split {
4651 items_on_current_page,
4652 } => Some(items_on_current_page),
4653 _ => None,
4654 };
4655 let mut first_break_done = false;
4656
4657 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4658 let needs_break = if let Some(break_at) = forced_break_at {
4659 if !first_break_done && line_idx == break_at {
4660 true
4661 } else {
4662 line_height > cursor.remaining_height()
4663 }
4664 } else {
4665 line_height > cursor.remaining_height()
4666 };
4667
4668 if needs_break {
4669 first_break_done = true;
4670 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4671 if !line_elements.is_empty() {
4672 let container_height = cursor.content_y + cursor.y - container_start_y;
4673 cursor.elements.push(LayoutElement {
4674 x: text_x,
4675 y: container_start_y,
4676 width: text_width,
4677 height: container_height,
4678 draw: DrawCommand::None,
4679 children: line_elements,
4680 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4681 resolved_style: Some(style.clone()),
4682 source_location: source_location.cloned(),
4683 href: parent_href.map(|s| s.to_string()),
4684 bookmark: if is_first_element {
4685 bookmark.map(|s| s.to_string())
4686 } else {
4687 None
4688 },
4689 alt: None,
4690 is_header_row: false,
4691 actual_text: None,
4692 list_numbering: None,
4693 col_span: 1,
4694 overflow: Overflow::default(),
4695 opacity: 1.0,
4696 });
4697 is_first_element = false;
4698 }
4699
4700 pages.push(cursor.finalize());
4701 *cursor = cursor.new_page();
4702
4703 snapshot = cursor.elements.len();
4704 container_start_y = cursor.content_y + cursor.y;
4705 }
4706
4707 let line_x = match style.text_align {
4708 TextAlign::Left => text_x,
4709 TextAlign::Right => text_x + text_width - run_line.width,
4710 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
4711 TextAlign::Justify => text_x,
4712 };
4713
4714 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
4715
4716 let is_last_line = line_idx == broken_lines.len() - 1;
4723 let user_ws = style.word_spacing;
4724 let (justified_width, word_spacing) =
4725 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4726 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4727 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4728 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4729 let s = glyphs[..=idx]
4730 .iter()
4731 .filter(|g| g.char_value == ' ')
4732 .count();
4733 (w, s)
4734 } else {
4735 (0.0, 0)
4736 };
4737 let slack = text_width - natural_width;
4738 let ws = if space_count > 0 && slack.abs() > 0.01 {
4739 slack / space_count as f64
4740 } else {
4741 0.0
4742 };
4743 (text_width, user_ws + ws)
4744 } else {
4745 (run_line.width, user_ws)
4746 };
4747
4748 let text_line = TextLine {
4749 x: line_x,
4750 y: cursor.content_y
4756 + cursor.y
4757 + baseline_in_line(
4758 line_height,
4759 style.font_size,
4760 font_context.baseline_metrics(
4761 &style.font_family,
4762 style.font_weight,
4763 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4764 ),
4765 ),
4766 glyphs,
4767 width: justified_width,
4768 height: line_height,
4769 word_spacing,
4770 };
4771
4772 let text_dec = run_line
4774 .chars
4775 .iter()
4776 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4777 .map(|sc| sc.text_decoration)
4778 .unwrap_or(style.text_decoration);
4779
4780 cursor.elements.push(LayoutElement {
4781 x: line_x,
4782 y: cursor.content_y + cursor.y,
4783 width: justified_width,
4784 height: line_height,
4785 draw: DrawCommand::Text {
4786 lines: vec![text_line],
4787 color: style.color,
4788 text_decoration: text_dec,
4789 opacity: 1.0,
4790 },
4791 children: vec![],
4792 node_type: Some("TextLine".to_string()),
4793 resolved_style: Some(style.clone()),
4794 source_location: None,
4795 href: parent_href.map(|s| s.to_string()),
4796 bookmark: None,
4797 alt: None,
4798 is_header_row: false,
4799 actual_text: None,
4800 list_numbering: None,
4801 col_span: 1,
4802 overflow: Overflow::default(),
4803 opacity: 1.0,
4804 });
4805
4806 cursor.y += line_height;
4807 }
4808
4809 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4810 if !line_elements.is_empty() {
4811 let container_height = cursor.content_y + cursor.y - container_start_y;
4812 cursor.elements.push(LayoutElement {
4813 x: text_x,
4814 y: container_start_y,
4815 width: text_width,
4816 height: container_height,
4817 draw: DrawCommand::None,
4818 children: line_elements,
4819 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4820 resolved_style: Some(style.clone()),
4821 source_location: source_location.cloned(),
4822 href: parent_href.map(|s| s.to_string()),
4823 bookmark: if is_first_element {
4824 bookmark.map(|s| s.to_string())
4825 } else {
4826 None
4827 },
4828 alt: None,
4829 is_header_row: false,
4830 actual_text: None,
4831 list_numbering: None,
4832 col_span: 1,
4833 overflow: Overflow::default(),
4834 opacity: 1.0,
4835 });
4836 }
4837 }
4838
4839 fn build_positioned_glyphs_single_style(
4843 &self,
4844 line: &BrokenLine,
4845 style: &ResolvedStyle,
4846 href: Option<&str>,
4847 font_context: &FontContext,
4848 ) -> Vec<PositionedGlyph> {
4849 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4850 let line_text: String = line.chars.iter().collect();
4851 let direction = style.direction;
4852 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4854
4855 let font_runs = crate::font::fallback::segment_by_font(
4858 &line.chars,
4859 &style.font_family,
4860 style.font_weight,
4861 italic,
4862 font_context.registry(),
4863 );
4864 let needs_per_char_fallback = font_runs.len() > 1
4865 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4866
4867 if needs_per_char_fallback {
4869 let bidi_runs = if has_bidi {
4870 bidi::analyze_bidi(&line_text, direction)
4871 } else {
4872 vec![crate::text::bidi::BidiRun {
4873 char_start: 0,
4874 char_end: line.chars.len(),
4875 level: unicode_bidi::Level::ltr(),
4876 is_rtl: false,
4877 }]
4878 };
4879
4880 let mut all_glyphs = Vec::new();
4881 let mut bidi_levels = Vec::new();
4882 let mut x = 0.0_f64;
4883
4884 for bidi_run in &bidi_runs {
4886 for font_run in &font_runs {
4888 let start = font_run.start.max(bidi_run.char_start);
4890 let end = font_run.end.min(bidi_run.char_end);
4891 if start >= end {
4892 continue;
4893 }
4894
4895 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4896 let sub_text: String = sub_chars.iter().collect();
4897 let resolved_family = &font_run.family;
4898
4899 if let Some(font_data) =
4900 font_context.font_data(resolved_family, style.font_weight, italic)
4901 {
4902 if let Some(shaped) = shaping::shape_text_with_direction(
4903 &sub_text,
4904 font_data,
4905 bidi_run.is_rtl,
4906 ) {
4907 let units_per_em = font_context.units_per_em(
4908 resolved_family,
4909 style.font_weight,
4910 italic,
4911 );
4912 let scale = style.font_size / units_per_em as f64;
4913
4914 for sg in &shaped {
4915 let cluster = sg.cluster as usize;
4916 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4917
4918 let cluster_text = if shaped.len() < sub_chars.len() {
4919 let cluster_end =
4920 self.find_cluster_end(&shaped, sg, sub_chars.len());
4921 if cluster_end > cluster + 1 {
4922 Some(
4923 sub_chars[cluster..cluster_end]
4924 .iter()
4925 .collect::<String>(),
4926 )
4927 } else {
4928 None
4929 }
4930 } else {
4931 None
4932 };
4933
4934 let glyph_x = x + sg.x_offset as f64 * scale;
4935 let glyph_y = sg.y_offset as f64 * scale;
4936 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4937
4938 all_glyphs.push(PositionedGlyph {
4939 glyph_id: sg.glyph_id,
4940 x_offset: glyph_x,
4941 y_offset: glyph_y,
4942 x_advance: advance,
4943 font_size: style.font_size,
4944 font_family: Arc::from(resolved_family.as_str()),
4945 font_weight: style.font_weight,
4946 font_style: style.font_style,
4947 char_value,
4948 color: Some(style.color),
4949 href: href.map(|s| s.to_string()),
4950 text_decoration: style.text_decoration,
4951 letter_spacing: style.letter_spacing,
4952 cluster_text,
4953 });
4954 bidi_levels.push(bidi_run.level);
4955 x += advance;
4956 }
4957 continue;
4958 }
4959 }
4960
4961 for i in start..end {
4963 let ch = line.chars[i];
4964 let glyph_x = x;
4965 let char_width = font_context.char_width(
4966 ch,
4967 resolved_family,
4968 style.font_weight,
4969 italic,
4970 style.font_size,
4971 );
4972 let advance = char_width + style.letter_spacing;
4973 all_glyphs.push(PositionedGlyph {
4974 glyph_id: ch as u16,
4975 x_offset: glyph_x,
4976 y_offset: 0.0,
4977 x_advance: advance,
4978 font_size: style.font_size,
4979 font_family: Arc::from(resolved_family.as_str()),
4980 font_weight: style.font_weight,
4981 font_style: style.font_style,
4982 char_value: ch,
4983 color: Some(style.color),
4984 href: href.map(|s| s.to_string()),
4985 text_decoration: style.text_decoration,
4986 letter_spacing: style.letter_spacing,
4987 cluster_text: None,
4988 });
4989 bidi_levels.push(bidi_run.level);
4990 x += advance;
4991 }
4992 }
4993 }
4994
4995 if has_bidi && !all_glyphs.is_empty() {
4997 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4998 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
4999 }
5000 return all_glyphs;
5001 }
5002
5003 if let Some(font_data) =
5006 font_context.font_data(&style.font_family, style.font_weight, italic)
5007 {
5008 if has_bidi {
5009 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5011 let units_per_em =
5012 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5013 let scale = style.font_size / units_per_em as f64;
5014
5015 let mut all_glyphs = Vec::new();
5016 let mut bidi_levels = Vec::new();
5017 let mut x = 0.0_f64;
5018
5019 for run in &bidi_runs {
5020 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5021 let run_text: String = run_chars.iter().collect();
5022
5023 if let Some(shaped) =
5024 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5025 {
5026 for sg in &shaped {
5027 let cluster = sg.cluster as usize;
5028 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5029
5030 let cluster_text = if shaped.len() < run_chars.len() {
5031 let cluster_end =
5032 self.find_cluster_end(&shaped, sg, run_chars.len());
5033 if cluster_end > cluster + 1 {
5034 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
5035 } else {
5036 None
5037 }
5038 } else {
5039 None
5040 };
5041
5042 let glyph_x = x + sg.x_offset as f64 * scale;
5043 let glyph_y = sg.y_offset as f64 * scale;
5044 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5045
5046 all_glyphs.push(PositionedGlyph {
5047 glyph_id: sg.glyph_id,
5048 x_offset: glyph_x,
5049 y_offset: glyph_y,
5050 x_advance: advance,
5051 font_size: style.font_size,
5052 font_family: Arc::from(style.font_family.as_str()),
5053 font_weight: style.font_weight,
5054 font_style: style.font_style,
5055 char_value,
5056 color: Some(style.color),
5057 href: href.map(|s| s.to_string()),
5058 text_decoration: style.text_decoration,
5059 letter_spacing: style.letter_spacing,
5060 cluster_text,
5061 });
5062 bidi_levels.push(run.level);
5063
5064 x += advance;
5065 }
5066 }
5067 }
5068
5069 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5071 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5072 return glyphs;
5073 }
5074
5075 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5077 let units_per_em =
5078 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5079 let scale = style.font_size / units_per_em as f64;
5080
5081 return self.shaped_glyphs_to_positioned(
5082 &shaped,
5083 &line.chars,
5084 &line.char_positions,
5085 scale,
5086 style.font_size,
5087 &style.font_family,
5088 style.font_weight,
5089 style.font_style,
5090 Some(style.color),
5091 href,
5092 style.text_decoration,
5093 style.letter_spacing,
5094 );
5095 }
5096 }
5097
5098 let mut glyphs: Vec<PositionedGlyph> = line
5100 .chars
5101 .iter()
5102 .enumerate()
5103 .map(|(j, ch)| {
5104 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5105 let char_width = font_context.char_width(
5106 *ch,
5107 &style.font_family,
5108 style.font_weight,
5109 italic,
5110 style.font_size,
5111 );
5112 PositionedGlyph {
5113 glyph_id: *ch as u16,
5114 x_offset: glyph_x,
5115 y_offset: 0.0,
5116 x_advance: char_width,
5117 font_size: style.font_size,
5118 font_family: Arc::from(style.font_family.as_str()),
5119 font_weight: style.font_weight,
5120 font_style: style.font_style,
5121 char_value: *ch,
5122 color: Some(style.color),
5123 href: href.map(|s| s.to_string()),
5124 text_decoration: style.text_decoration,
5125 letter_spacing: style.letter_spacing,
5126 cluster_text: None,
5127 }
5128 })
5129 .collect();
5130
5131 if has_bidi && !glyphs.is_empty() {
5133 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5134 let mut levels = Vec::with_capacity(glyphs.len());
5135 let mut char_idx = 0;
5136 for run in &bidi_runs {
5137 for _ in run.char_start..run.char_end {
5138 if char_idx < glyphs.len() {
5139 levels.push(run.level);
5140 char_idx += 1;
5141 }
5142 }
5143 }
5144 while levels.len() < glyphs.len() {
5146 levels.push(unicode_bidi::Level::ltr());
5147 }
5148 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
5149 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5150 }
5151
5152 glyphs
5153 }
5154
5155 fn build_positioned_glyphs_runs(
5160 &self,
5161 run_line: &RunBrokenLine,
5162 font_context: &FontContext,
5163 direction: Direction,
5164 ) -> Vec<PositionedGlyph> {
5165 let chars = &run_line.chars;
5166 if chars.is_empty() {
5167 return vec![];
5168 }
5169
5170 let resolved_families: Vec<String> = chars
5179 .iter()
5180 .map(|sc| {
5181 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5182 if !sc.font_family.contains(',') {
5183 let primary =
5184 font_context
5185 .registry()
5186 .resolve(&sc.font_family, sc.font_weight, italic);
5187 if sc.ch.is_whitespace()
5188 || sc.ch == PAGE_NUMBER_SENTINEL
5189 || sc.ch == TOTAL_PAGES_SENTINEL
5190 || primary.has_char(sc.ch)
5191 {
5192 return sc.font_family.clone();
5193 }
5194 }
5195 let (_, family) = font_context.registry().resolve_for_char(
5196 &sc.font_family,
5197 sc.ch,
5198 sc.font_weight,
5199 italic,
5200 );
5201 family
5202 })
5203 .collect();
5204
5205 let line_text: String = chars.iter().map(|c| c.ch).collect();
5206 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5207 let bidi_runs = if has_bidi {
5208 Some(bidi::analyze_bidi(&line_text, direction))
5209 } else {
5210 None
5211 };
5212
5213 let mut glyphs = Vec::new();
5214 let mut bidi_levels = Vec::new();
5215 let mut i = 0;
5216
5217 while i < chars.len() {
5218 let sc = &chars[i];
5219 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5220 let resolved_family = &resolved_families[i];
5221
5222 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5224 runs.iter()
5225 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5226 });
5227
5228 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5230 {
5231 let run_start = i;
5233 let mut run_end = i + 1;
5234 while run_end < chars.len() {
5235 let next = &chars[run_end];
5236 let next_italic =
5237 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5238 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5239 runs.iter()
5240 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5241 });
5242 if resolved_families[run_end] == *resolved_family
5244 && next.font_weight == sc.font_weight
5245 && next_italic == italic
5246 && (next.font_size - sc.font_size).abs() < 0.001
5247 && next_is_rtl == is_rtl
5248 {
5249 run_end += 1;
5250 } else {
5251 break;
5252 }
5253 }
5254
5255 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5256 if let Some(shaped) =
5257 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5258 {
5259 let units_per_em =
5260 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5261 let scale = sc.font_size / units_per_em as f64;
5262
5263 let run_chars: Vec<char> =
5265 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5266 let run_positions: Vec<f64> = (run_start..run_end)
5267 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5268 .collect();
5269
5270 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5272 &shaped,
5273 &chars[run_start..run_end],
5274 &run_chars,
5275 &run_positions,
5276 scale,
5277 );
5278 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5280 for g in &mut run_glyphs {
5281 g.font_family = resolved_family_arc.clone();
5282 }
5283 let run_level = if is_rtl {
5285 unicode_bidi::Level::rtl()
5286 } else {
5287 unicode_bidi::Level::ltr()
5288 };
5289 for _ in &run_glyphs {
5290 bidi_levels.push(run_level);
5291 }
5292 glyphs.extend(run_glyphs);
5293 i = run_end;
5294 continue;
5295 }
5296 }
5297
5298 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5300 let char_width = font_context.char_width(
5301 sc.ch,
5302 resolved_family,
5303 sc.font_weight,
5304 italic,
5305 sc.font_size,
5306 );
5307 glyphs.push(PositionedGlyph {
5308 glyph_id: sc.ch as u16,
5309 x_offset: glyph_x,
5310 y_offset: 0.0,
5311 x_advance: char_width,
5312 font_size: sc.font_size,
5313 font_family: Arc::from(resolved_family.as_str()),
5314 font_weight: sc.font_weight,
5315 font_style: sc.font_style,
5316 char_value: sc.ch,
5317 color: Some(sc.color),
5318 href: sc.href.clone(),
5319 text_decoration: sc.text_decoration,
5320 letter_spacing: sc.letter_spacing,
5321 cluster_text: None,
5322 });
5323 bidi_levels.push(if is_rtl {
5324 unicode_bidi::Level::rtl()
5325 } else {
5326 unicode_bidi::Level::ltr()
5327 });
5328 i += 1;
5329 }
5330
5331 if has_bidi && !glyphs.is_empty() {
5333 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
5334 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5335 }
5336
5337 glyphs
5338 }
5339
5340 #[allow(clippy::too_many_arguments)]
5342 fn shaped_glyphs_to_positioned(
5343 &self,
5344 shaped: &[shaping::ShapedGlyph],
5345 chars: &[char],
5346 _char_positions: &[f64],
5347 scale: f64,
5348 font_size: f64,
5349 font_family: &str,
5350 font_weight: u32,
5351 font_style: FontStyle,
5352 color: Option<Color>,
5353 href: Option<&str>,
5354 text_decoration: TextDecoration,
5355 letter_spacing: f64,
5356 ) -> Vec<PositionedGlyph> {
5357 let mut result = Vec::with_capacity(shaped.len());
5358 let mut x = 0.0_f64;
5359
5360 for sg in shaped {
5361 let cluster = sg.cluster as usize;
5362 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5363
5364 let cluster_text = if shaped.len() < chars.len() {
5366 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5369 if cluster_end > cluster + 1 {
5370 Some(chars[cluster..cluster_end].iter().collect::<String>())
5371 } else {
5372 None
5373 }
5374 } else {
5375 None
5376 };
5377
5378 let glyph_x = x + sg.x_offset as f64 * scale;
5380 let glyph_y = sg.y_offset as f64 * scale;
5381 let advance = sg.x_advance as f64 * scale + letter_spacing;
5382
5383 result.push(PositionedGlyph {
5384 glyph_id: sg.glyph_id,
5385 x_offset: glyph_x,
5386 y_offset: glyph_y,
5387 x_advance: advance,
5388 font_size,
5389 font_family: Arc::from(font_family),
5390 font_weight,
5391 font_style,
5392 char_value,
5393 color,
5394 href: href.map(|s| s.to_string()),
5395 text_decoration,
5396 letter_spacing,
5397 cluster_text,
5398 });
5399
5400 x += advance;
5401 }
5402
5403 result
5404 }
5405
5406 fn shaped_glyphs_to_positioned_runs(
5408 &self,
5409 shaped: &[shaping::ShapedGlyph],
5410 styled_chars: &[StyledChar],
5411 chars: &[char],
5412 char_positions: &[f64],
5413 scale: f64,
5414 ) -> Vec<PositionedGlyph> {
5415 let mut result = Vec::with_capacity(shaped.len());
5416 let base_x = char_positions.first().copied().unwrap_or(0.0);
5418 let mut x = 0.0_f64;
5419
5420 for sg in shaped {
5421 let cluster = sg.cluster as usize;
5422 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5423 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5424
5425 let cluster_text = if shaped.len() < chars.len() {
5426 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5427 if cluster_end > cluster + 1 {
5428 Some(chars[cluster..cluster_end].iter().collect::<String>())
5429 } else {
5430 None
5431 }
5432 } else {
5433 None
5434 };
5435
5436 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5437 let glyph_y = sg.y_offset as f64 * scale;
5438 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5439
5440 result.push(PositionedGlyph {
5441 glyph_id: sg.glyph_id,
5442 x_offset: glyph_x,
5443 y_offset: glyph_y,
5444 x_advance: advance,
5445 font_size: sc.font_size,
5446 font_family: Arc::from(sc.font_family.as_str()),
5447 font_weight: sc.font_weight,
5448 font_style: sc.font_style,
5449 char_value,
5450 color: Some(sc.color),
5451 href: sc.href.clone(),
5452 text_decoration: sc.text_decoration,
5453 letter_spacing: sc.letter_spacing,
5454 cluster_text,
5455 });
5456
5457 x += advance;
5458 }
5459
5460 result
5461 }
5462
5463 fn find_cluster_end(
5465 &self,
5466 shaped: &[shaping::ShapedGlyph],
5467 current: &shaping::ShapedGlyph,
5468 num_chars: usize,
5469 ) -> usize {
5470 for sg in shaped {
5472 if sg.cluster > current.cluster {
5473 return sg.cluster as usize;
5474 }
5475 }
5476 num_chars
5478 }
5479
5480 fn image_display_size(
5490 &self,
5491 src: &str,
5492 style: &ResolvedStyle,
5493 explicit_width: Option<f64>,
5494 explicit_height: Option<f64>,
5495 available_width: f64,
5496 ) -> (f64, f64) {
5497 let dims = if src.is_empty() {
5498 None
5499 } else {
5500 self.get_image_dimensions(src)
5501 };
5502 let aspect = dims
5503 .map(|(w, h)| {
5504 if w > 0 {
5505 f64::from(h) / f64::from(w)
5506 } else {
5507 0.75
5508 }
5509 })
5510 .unwrap_or(0.75);
5511
5512 let style_w = match style.width {
5513 SizeConstraint::Fixed(w) => Some(w),
5514 SizeConstraint::Auto => None,
5515 };
5516 let style_h = match style.height {
5517 SizeConstraint::Fixed(h) => Some(h),
5518 SizeConstraint::Auto => None,
5519 };
5520 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5521
5522 let width_source = style_w.or(explicit_width);
5523 let height_source = style_h.or(explicit_height);
5524 match (width_source, height_source) {
5525 (Some(w), Some(h)) => (clamp(w), h),
5526 (Some(w), None) => {
5527 let w = clamp(w);
5528 (w, w * aspect)
5529 }
5530 (None, Some(h)) => (clamp(h / aspect), h),
5531 (None, None) => {
5532 let w = clamp(
5535 dims.map(|(w, _)| f64::from(w))
5536 .unwrap_or(available_width)
5537 .min(available_width),
5538 );
5539 (w, w * aspect)
5540 }
5541 }
5542 }
5543
5544 #[allow(clippy::too_many_arguments)]
5545 fn layout_image(
5546 &self,
5547 node: &Node,
5548 style: &ResolvedStyle,
5549 cursor: &mut PageCursor,
5550 pages: &mut Vec<LayoutPage>,
5551 x: f64,
5552 available_width: f64,
5553 explicit_width: Option<f64>,
5554 explicit_height: Option<f64>,
5555 ) {
5556 let margin = &style.margin.to_edges();
5557
5558 let src = match &node.kind {
5560 NodeKind::Image { src, .. } => src.as_str(),
5561 _ => "",
5562 };
5563
5564 let loaded = if !src.is_empty() {
5565 crate::image_loader::load_image(src).ok()
5566 } else {
5567 None
5568 };
5569
5570 let (img_width, img_height) = self.image_display_size(
5571 src,
5572 style,
5573 explicit_width,
5574 explicit_height,
5575 available_width - margin.horizontal(),
5576 );
5577
5578 let total_height = img_height + margin.vertical();
5579
5580 if total_height > cursor.remaining_height() {
5581 pages.push(cursor.finalize());
5582 *cursor = cursor.new_page();
5583 }
5584
5585 cursor.y += margin.top;
5586
5587 let draw = if let Some(image_data) = loaded {
5588 DrawCommand::Image { image_data }
5589 } else {
5590 DrawCommand::ImagePlaceholder
5591 };
5592
5593 cursor.elements.push(LayoutElement {
5594 x: x + margin.left,
5595 y: cursor.content_y + cursor.y,
5596 width: img_width,
5597 height: img_height,
5598 draw,
5599 children: vec![],
5600 node_type: Some(node_kind_name(&node.kind).to_string()),
5601 resolved_style: Some(style.clone()),
5602 source_location: node.source_location.clone(),
5603 href: node.href.clone(),
5604 bookmark: node.bookmark.clone(),
5605 alt: node.alt.clone(),
5606 is_header_row: false,
5607 actual_text: None,
5608 list_numbering: None,
5609 col_span: 1,
5610 overflow: style.overflow,
5611 opacity: style.opacity,
5612 });
5613
5614 cursor.y += img_height + margin.bottom;
5615 }
5616
5617 #[allow(clippy::too_many_arguments)]
5619 fn layout_svg(
5620 &self,
5621 node: &Node,
5622 style: &ResolvedStyle,
5623 cursor: &mut PageCursor,
5624 pages: &mut Vec<LayoutPage>,
5625 x: f64,
5626 _available_width: f64,
5627 svg_width: f64,
5628 svg_height: f64,
5629 view_box: Option<&str>,
5630 content: &str,
5631 ) {
5632 let margin = &style.margin.to_edges();
5633 let total_height = svg_height + margin.vertical();
5634
5635 if total_height > cursor.remaining_height() {
5636 pages.push(cursor.finalize());
5637 *cursor = cursor.new_page();
5638 }
5639
5640 cursor.y += margin.top;
5641
5642 let vb = view_box
5643 .and_then(crate::svg::parse_view_box)
5644 .unwrap_or(crate::svg::ViewBox {
5645 min_x: 0.0,
5646 min_y: 0.0,
5647 width: svg_width,
5648 height: svg_height,
5649 });
5650
5651 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5652
5653 cursor.elements.push(LayoutElement {
5654 x: x + margin.left,
5655 y: cursor.content_y + cursor.y,
5656 width: svg_width,
5657 height: svg_height,
5658 draw: DrawCommand::Svg {
5659 commands,
5660 width: svg_width,
5661 height: svg_height,
5662 viewbox_min_x: vb.min_x,
5663 viewbox_min_y: vb.min_y,
5664 viewbox_width: vb.width,
5665 viewbox_height: vb.height,
5666 clip: false,
5667 },
5668 children: vec![],
5669 node_type: Some("Svg".to_string()),
5670 resolved_style: Some(style.clone()),
5671 source_location: node.source_location.clone(),
5672 href: node.href.clone(),
5673 bookmark: node.bookmark.clone(),
5674 alt: node.alt.clone(),
5675 is_header_row: false,
5676 actual_text: None,
5677 list_numbering: None,
5678 col_span: 1,
5679 overflow: style.overflow,
5680 opacity: style.opacity,
5681 });
5682
5683 cursor.y += svg_height + margin.bottom;
5684 }
5685
5686 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
5688 use crate::svg::SvgCommand;
5689
5690 let mut commands = Vec::new();
5691 let mut cur_x = 0.0_f64;
5692 let mut cur_y = 0.0_f64;
5693
5694 for op in operations {
5695 match op {
5696 CanvasOp::MoveTo { x, y } => {
5697 commands.push(SvgCommand::MoveTo(*x, *y));
5698 cur_x = *x;
5699 cur_y = *y;
5700 }
5701 CanvasOp::LineTo { x, y } => {
5702 commands.push(SvgCommand::LineTo(*x, *y));
5703 cur_x = *x;
5704 cur_y = *y;
5705 }
5706 CanvasOp::BezierCurveTo {
5707 cp1x,
5708 cp1y,
5709 cp2x,
5710 cp2y,
5711 x,
5712 y,
5713 } => {
5714 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
5715 cur_x = *x;
5716 cur_y = *y;
5717 }
5718 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
5719 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
5721 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
5722 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
5723 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
5724 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
5725 cur_x = *x;
5726 cur_y = *y;
5727 }
5728 CanvasOp::ClosePath => {
5729 commands.push(SvgCommand::ClosePath);
5730 }
5731 CanvasOp::Rect {
5732 x,
5733 y,
5734 width,
5735 height,
5736 } => {
5737 commands.push(SvgCommand::MoveTo(*x, *y));
5738 commands.push(SvgCommand::LineTo(*x + *width, *y));
5739 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
5740 commands.push(SvgCommand::LineTo(*x, *y + *height));
5741 commands.push(SvgCommand::ClosePath);
5742 cur_x = *x;
5743 cur_y = *y;
5744 }
5745 CanvasOp::Circle { cx, cy, r } => {
5746 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
5747 }
5748 CanvasOp::Ellipse { cx, cy, rx, ry } => {
5749 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
5750 }
5751 CanvasOp::Arc {
5752 cx,
5753 cy,
5754 r,
5755 start_angle,
5756 end_angle,
5757 counterclockwise,
5758 } => {
5759 let steps = 32;
5763 let mut sweep = end_angle - start_angle;
5764 if !counterclockwise && sweep < 0.0 {
5765 sweep += 2.0 * std::f64::consts::PI;
5766 }
5767 if *counterclockwise && sweep > 0.0 {
5768 sweep -= 2.0 * std::f64::consts::PI;
5769 }
5770 for i in 0..=steps {
5771 let t = *start_angle + sweep * (i as f64 / steps as f64);
5772 let px = cx + r * t.cos();
5773 let py = cy + r * t.sin();
5774 if i == 0 {
5775 commands.push(SvgCommand::MoveTo(px, py));
5776 } else {
5777 commands.push(SvgCommand::LineTo(px, py));
5778 }
5779 }
5780 }
5781 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
5782 CanvasOp::Fill => commands.push(SvgCommand::Fill),
5783 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
5784 CanvasOp::SetFillColor { r, g, b } => {
5785 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
5787 }
5788 CanvasOp::SetStrokeColor { r, g, b } => {
5789 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
5790 }
5791 CanvasOp::SetLineWidth { width } => {
5792 commands.push(SvgCommand::SetStrokeWidth(*width));
5793 }
5794 CanvasOp::SetLineCap { cap } => {
5795 commands.push(SvgCommand::SetLineCap(*cap));
5796 }
5797 CanvasOp::SetLineJoin { join } => {
5798 commands.push(SvgCommand::SetLineJoin(*join));
5799 }
5800 CanvasOp::Save => commands.push(SvgCommand::SaveState),
5801 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
5802 }
5803 }
5804
5805 commands
5806 }
5807
5808 #[allow(clippy::too_many_arguments)]
5810 fn layout_canvas(
5811 &self,
5812 node: &Node,
5813 style: &ResolvedStyle,
5814 cursor: &mut PageCursor,
5815 pages: &mut Vec<LayoutPage>,
5816 x: f64,
5817 _available_width: f64,
5818 canvas_width: f64,
5819 canvas_height: f64,
5820 operations: &[CanvasOp],
5821 ) {
5822 let margin = style.margin.to_edges();
5823 let total_height = canvas_height + margin.top + margin.bottom;
5824
5825 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5827 pages.push(cursor.finalize());
5828 *cursor = cursor.new_page();
5829 }
5830
5831 cursor.y += margin.top;
5832
5833 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5834
5835 cursor.elements.push(LayoutElement {
5836 x: x + margin.left,
5837 y: cursor.content_y + cursor.y,
5838 width: canvas_width,
5839 height: canvas_height,
5840 draw: DrawCommand::Svg {
5841 commands: svg_commands,
5842 width: canvas_width,
5843 height: canvas_height,
5844 viewbox_min_x: 0.0,
5847 viewbox_min_y: 0.0,
5848 viewbox_width: canvas_width,
5849 viewbox_height: canvas_height,
5850 clip: true,
5851 },
5852 children: vec![],
5853 node_type: Some("Canvas".to_string()),
5854 resolved_style: Some(style.clone()),
5855 source_location: node.source_location.clone(),
5856 href: node.href.clone(),
5857 bookmark: node.bookmark.clone(),
5858 alt: node.alt.clone(),
5859 is_header_row: false,
5860 actual_text: None,
5861 list_numbering: None,
5862 col_span: 1,
5863 overflow: style.overflow,
5864 opacity: style.opacity,
5865 });
5866
5867 cursor.y += canvas_height + margin.bottom;
5868 }
5869
5870 #[allow(clippy::too_many_arguments)]
5872 #[allow(clippy::too_many_arguments)]
5874 fn layout_chart(
5875 &self,
5876 node: &Node,
5877 style: &ResolvedStyle,
5878 cursor: &mut PageCursor,
5879 pages: &mut Vec<LayoutPage>,
5880 x: f64,
5881 chart_width: f64,
5882 chart_height: f64,
5883 primitives: Vec<crate::chart::ChartPrimitive>,
5884 node_type_name: &str,
5885 ) {
5886 let margin = &style.margin.to_edges();
5887 let total_height = chart_height + margin.vertical();
5888
5889 if total_height > cursor.remaining_height() {
5890 pages.push(cursor.finalize());
5891 *cursor = cursor.new_page();
5892 }
5893
5894 cursor.y += margin.top;
5895
5896 let draw = DrawCommand::Chart { primitives };
5897
5898 cursor.elements.push(LayoutElement {
5899 x: x + margin.left,
5900 y: cursor.content_y + cursor.y,
5901 width: chart_width,
5902 height: chart_height,
5903 draw,
5904 children: vec![],
5905 node_type: Some(node_type_name.to_string()),
5906 resolved_style: Some(style.clone()),
5907 source_location: node.source_location.clone(),
5908 href: node.href.clone(),
5909 bookmark: node.bookmark.clone(),
5910 alt: node.alt.clone(),
5911 is_header_row: false,
5912 actual_text: None,
5913 list_numbering: None,
5914 col_span: 1,
5915 overflow: style.overflow,
5916 opacity: style.opacity,
5917 });
5918
5919 cursor.y += chart_height + margin.bottom;
5920 }
5921
5922 #[allow(clippy::too_many_arguments)]
5924 fn layout_form_field(
5925 &self,
5926 node: &Node,
5927 style: &ResolvedStyle,
5928 cursor: &mut PageCursor,
5929 pages: &mut Vec<LayoutPage>,
5930 x: f64,
5931 field_width: f64,
5932 field_height: f64,
5933 draw: DrawCommand,
5934 node_type_name: &str,
5935 ) {
5936 let margin = &style.margin.to_edges();
5937 let total_height = field_height + margin.vertical();
5938
5939 if total_height > cursor.remaining_height() {
5940 pages.push(cursor.finalize());
5941 *cursor = cursor.new_page();
5942 }
5943
5944 cursor.y += margin.top;
5945
5946 cursor.elements.push(LayoutElement {
5947 x: x + margin.left,
5948 y: cursor.content_y + cursor.y,
5949 width: field_width,
5950 height: field_height,
5951 draw,
5952 children: vec![],
5953 node_type: Some(node_type_name.to_string()),
5954 resolved_style: Some(style.clone()),
5955 source_location: node.source_location.clone(),
5956 href: node.href.clone(),
5957 bookmark: node.bookmark.clone(),
5958 alt: node.alt.clone(),
5959 is_header_row: false,
5960 actual_text: None,
5961 list_numbering: None,
5962 col_span: 1,
5963 overflow: style.overflow,
5964 opacity: style.opacity,
5965 });
5966
5967 cursor.y += field_height + margin.bottom;
5968 }
5969
5970 #[allow(clippy::too_many_arguments)]
5971 fn layout_barcode(
5972 &self,
5973 node: &Node,
5974 style: &ResolvedStyle,
5975 cursor: &mut PageCursor,
5976 pages: &mut Vec<LayoutPage>,
5977 x: f64,
5978 available_width: f64,
5979 data: &str,
5980 format: crate::barcode::BarcodeFormat,
5981 explicit_width: Option<f64>,
5982 bar_height: f64,
5983 ) {
5984 let margin = &style.margin.to_edges();
5985 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5986 let total_height = bar_height + margin.vertical();
5987
5988 if total_height > cursor.remaining_height() {
5989 pages.push(cursor.finalize());
5990 *cursor = cursor.new_page();
5991 }
5992
5993 cursor.y += margin.top;
5994
5995 let draw = match crate::barcode::generate_barcode(data, format) {
5996 Ok(barcode_data) => {
5997 let bar_width = if barcode_data.bars.is_empty() {
5998 0.0
5999 } else {
6000 display_width / barcode_data.bars.len() as f64
6001 };
6002 DrawCommand::Barcode {
6003 bars: barcode_data.bars,
6004 bar_width,
6005 height: bar_height,
6006 color: style.color,
6007 }
6008 }
6009 Err(_) => DrawCommand::None,
6010 };
6011
6012 cursor.elements.push(LayoutElement {
6013 x: x + margin.left,
6014 y: cursor.content_y + cursor.y,
6015 width: display_width,
6016 height: bar_height,
6017 draw,
6018 children: vec![],
6019 node_type: Some("Barcode".to_string()),
6020 resolved_style: Some(style.clone()),
6021 source_location: node.source_location.clone(),
6022 href: node.href.clone(),
6023 bookmark: node.bookmark.clone(),
6024 alt: node.alt.clone(),
6025 is_header_row: false,
6026 actual_text: Some(data.to_string()),
6027 list_numbering: None,
6028 col_span: 1,
6029 overflow: style.overflow,
6030 opacity: style.opacity,
6031 });
6032
6033 cursor.y += bar_height + margin.bottom;
6034 }
6035
6036 #[allow(clippy::too_many_arguments)]
6038 fn layout_qrcode(
6039 &self,
6040 node: &Node,
6041 style: &ResolvedStyle,
6042 cursor: &mut PageCursor,
6043 pages: &mut Vec<LayoutPage>,
6044 x: f64,
6045 available_width: f64,
6046 data: &str,
6047 explicit_size: Option<f64>,
6048 ) {
6049 let margin = &style.margin.to_edges();
6050 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6051 let total_height = display_size + margin.vertical();
6052
6053 if total_height > cursor.remaining_height() {
6054 pages.push(cursor.finalize());
6055 *cursor = cursor.new_page();
6056 }
6057
6058 cursor.y += margin.top;
6059
6060 let draw = match crate::qrcode::generate_qr(data) {
6061 Ok(matrix) => {
6062 let module_size = display_size / matrix.size as f64;
6063 DrawCommand::QrCode {
6064 modules: matrix.modules,
6065 module_size,
6066 color: style.color,
6067 }
6068 }
6069 Err(_) => DrawCommand::None,
6070 };
6071
6072 cursor.elements.push(LayoutElement {
6073 x: x + margin.left,
6074 y: cursor.content_y + cursor.y,
6075 width: display_size,
6076 height: display_size,
6077 draw,
6078 children: vec![],
6079 node_type: Some("QrCode".to_string()),
6080 resolved_style: Some(style.clone()),
6081 source_location: node.source_location.clone(),
6082 href: node.href.clone(),
6083 bookmark: node.bookmark.clone(),
6084 alt: node.alt.clone(),
6085 is_header_row: false,
6086 actual_text: Some(data.to_string()),
6087 list_numbering: None,
6088 col_span: 1,
6089 overflow: style.overflow,
6090 opacity: style.opacity,
6091 });
6092
6093 cursor.y += display_size + margin.bottom;
6094 }
6095
6096 fn measure_node_height(
6099 &self,
6100 node: &Node,
6101 available_width: f64,
6102 style: &ResolvedStyle,
6103 font_context: &FontContext,
6104 ) -> f64 {
6105 match &node.kind {
6106 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6111 let measure_width = match style.width {
6114 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6115 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6116 };
6117 if !runs.is_empty() {
6124 let mut styled_chars: Vec<StyledChar> = Vec::new();
6125 for run in runs {
6126 let run_style = run.style.resolve(Some(style), measure_width);
6127 let transform = run_style.text_transform;
6128 let run_content = substitute_page_placeholders(&run.content);
6129 let mut prev_is_whitespace = true;
6130 for ch in run_content.chars() {
6131 let transformed_ch =
6132 apply_char_transform(ch, transform, prev_is_whitespace);
6133 prev_is_whitespace = ch.is_whitespace();
6134 styled_chars.push(StyledChar {
6135 ch: transformed_ch,
6136 font_family: run_style.font_family.clone(),
6137 font_size: run_style.font_size,
6138 font_weight: run_style.font_weight,
6139 font_style: run_style.font_style,
6140 color: run_style.color,
6141 href: None,
6142 text_decoration: run_style.text_decoration,
6143 letter_spacing: run_style.letter_spacing,
6144 });
6145 }
6146 }
6147 let justify = matches!(style.text_align, TextAlign::Justify);
6148 let broken_lines = match style.line_breaking {
6149 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6150 font_context,
6151 &styled_chars,
6152 measure_width,
6153 style.hyphens,
6154 style.lang.as_deref(),
6155 justify,
6156 ),
6157 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6158 font_context,
6159 &styled_chars,
6160 measure_width,
6161 style.hyphens,
6162 style.lang.as_deref(),
6163 ),
6164 };
6165 let line_height = style.font_size * style.line_height;
6166 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6167 } else {
6168 let content = substitute_page_placeholders(content);
6169 let transformed = apply_text_transform(&content, style.text_transform);
6170 let justify = matches!(style.text_align, TextAlign::Justify);
6171 let lines = match style.line_breaking {
6172 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6173 font_context,
6174 &transformed,
6175 measure_width,
6176 style.font_size,
6177 &style.font_family,
6178 style.font_weight,
6179 style.font_style,
6180 style.letter_spacing,
6181 style.hyphens,
6182 style.lang.as_deref(),
6183 justify,
6184 ),
6185 LineBreaking::Greedy => self.text_layout.break_into_lines(
6186 font_context,
6187 &transformed,
6188 measure_width,
6189 style.font_size,
6190 &style.font_family,
6191 style.font_weight,
6192 style.font_style,
6193 style.letter_spacing,
6194 style.hyphens,
6195 style.lang.as_deref(),
6196 ),
6197 };
6198 let line_height = style.font_size * style.line_height;
6199 (lines.len() as f64) * line_height + style.padding.vertical()
6200 }
6201 }
6202 NodeKind::Image {
6203 src,
6204 width: explicit_w,
6205 height: explicit_h,
6206 } => {
6207 let (_, h) = self.image_display_size(
6209 src,
6210 style,
6211 *explicit_w,
6212 *explicit_h,
6213 available_width - style.margin.horizontal(),
6214 );
6215 h + style.padding.vertical()
6216 }
6217 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6218 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6219 NodeKind::QrCode { size, .. } => {
6220 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6221 display_size + style.margin.vertical()
6222 }
6223 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6224 NodeKind::BarChart { height, .. }
6225 | NodeKind::LineChart { height, .. }
6226 | NodeKind::PieChart { height, .. }
6227 | NodeKind::AreaChart { height, .. }
6228 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6229 NodeKind::TextField { height, .. }
6230 | NodeKind::Checkbox { height, .. }
6231 | NodeKind::Dropdown { height, .. }
6232 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6233 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6235 if let SizeConstraint::Fixed(h) = style.height {
6242 return h;
6243 }
6244 let outer_width = match style.width {
6245 SizeConstraint::Fixed(w) => w,
6246 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6247 };
6248 let inner_width =
6249 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6250 let col_widths = self.resolve_column_widths(
6251 columns,
6252 inner_width,
6253 &node.children,
6254 style,
6255 font_context,
6256 );
6257 let row_gap = style.row_gap;
6258 let offsets = Self::table_column_offsets(&node.children);
6259 let mut total = 0.0;
6260 for (i, row) in node.children.iter().enumerate() {
6261 if i > 0 {
6262 total += row_gap;
6263 }
6264 total += self.measure_table_row_height(
6265 row,
6266 &col_widths,
6267 &offsets[i],
6268 style,
6269 font_context,
6270 );
6271 }
6272 total + style.padding.vertical() + style.border_width.vertical()
6273 }
6274 NodeKind::TableRow { .. } => {
6275 let n = node.children.len().max(1);
6280 let usable = (available_width - style.margin.horizontal()).max(0.0);
6281 let col_w = usable / n as f64;
6282 let col_widths = vec![col_w; n];
6283 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6284 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6285 }
6286 _ => {
6287 if let SizeConstraint::Fixed(h) = style.height {
6289 return h;
6290 }
6291 let outer_width = match style.width {
6295 SizeConstraint::Fixed(w) => w,
6296 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6297 }
6298 .min(style.max_width)
6299 .max(style.min_width);
6300 let inner_width =
6301 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6302 let children_height =
6303 self.measure_children_height(&node.children, inner_width, style, font_context);
6304 (children_height + style.padding.vertical() + style.border_width.vertical())
6305 .max(style.min_height)
6306 }
6307 }
6308 }
6309
6310 fn measure_children_height(
6311 &self,
6312 children: &[Node],
6313 available_width: f64,
6314 parent_style: &ResolvedStyle,
6315 font_context: &FontContext,
6316 ) -> f64 {
6317 if children
6323 .iter()
6324 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6325 {
6326 let flow: Vec<Node> = children
6327 .iter()
6328 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6329 .cloned()
6330 .collect();
6331 return self.measure_children_height(
6332 &flow,
6333 available_width,
6334 parent_style,
6335 font_context,
6336 );
6337 }
6338 if matches!(parent_style.display, Display::Grid) {
6340 if let Some(template_cols) = &parent_style.grid_template_columns {
6341 let num_columns = template_cols.len();
6342 if num_columns > 0 && !children.is_empty() {
6343 let col_gap = parent_style.column_gap;
6344 let row_gap = parent_style.row_gap;
6345
6346 let content_sizes: Vec<f64> = template_cols
6347 .iter()
6348 .map(|track| {
6349 if matches!(track, GridTrackSize::Auto) {
6350 available_width / num_columns as f64
6351 } else {
6352 0.0
6353 }
6354 })
6355 .collect();
6356
6357 let col_widths = grid::resolve_tracks(
6358 template_cols,
6359 available_width,
6360 col_gap,
6361 &content_sizes,
6362 );
6363
6364 let placements: Vec<Option<&GridPlacement>> = children
6365 .iter()
6366 .map(|child| child.style.grid_placement.as_ref())
6367 .collect();
6368
6369 let item_placements = grid::place_items(&placements, num_columns);
6370 let num_rows = grid::compute_num_rows(&item_placements);
6371
6372 if num_rows == 0 {
6373 return 0.0;
6374 }
6375
6376 let mut row_heights = vec![0.0_f64; num_rows];
6377 for placement in &item_placements {
6378 let cell_width = grid::span_width(
6379 placement.col_start,
6380 placement.col_end,
6381 &col_widths,
6382 col_gap,
6383 );
6384 let child = &children[placement.child_index];
6385 let child_style = child.style.resolve(Some(parent_style), cell_width);
6386 let h =
6387 self.measure_node_height(child, cell_width, &child_style, font_context);
6388 let span = placement.row_end - placement.row_start;
6389 let per_row = h / span as f64;
6390 for rh in row_heights
6391 .iter_mut()
6392 .take(placement.row_end.min(num_rows))
6393 .skip(placement.row_start)
6394 {
6395 if per_row > *rh {
6396 *rh = per_row;
6397 }
6398 }
6399 }
6400
6401 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6402 return row_heights.iter().sum::<f64>() + total_row_gap;
6403 }
6404 }
6405 }
6406
6407 let direction = parent_style.flex_direction;
6408 let row_gap = parent_style.row_gap;
6409 let column_gap = parent_style.column_gap;
6410
6411 match direction {
6412 FlexDirection::Row | FlexDirection::RowReverse => {
6413 let styles: Vec<ResolvedStyle> = children
6416 .iter()
6417 .map(|child| child.style.resolve(Some(parent_style), available_width))
6418 .collect();
6419
6420 let base_widths: Vec<f64> = children
6421 .iter()
6422 .zip(&styles)
6423 .map(|(child, style)| match style.flex_basis {
6424 SizeConstraint::Fixed(w) => w,
6425 SizeConstraint::Auto => match style.width {
6426 SizeConstraint::Fixed(w) => w,
6427 SizeConstraint::Auto => self
6428 .measure_intrinsic_width(child, style, font_context)
6429 .min(available_width),
6430 },
6431 })
6432 .collect();
6433
6434 let lines = match parent_style.flex_wrap {
6435 FlexWrap::NoWrap => {
6436 vec![flex::WrapLine {
6437 start: 0,
6438 end: children.len(),
6439 }]
6440 }
6441 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6442 flex::partition_into_lines(&base_widths, column_gap, available_width)
6443 }
6444 };
6445
6446 let mut final_widths = base_widths.clone();
6448 for line in &lines {
6449 let line_count = line.end - line.start;
6450 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6451 let distributable = available_width - line_gap;
6452 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6453 let remaining = distributable - total_base;
6454
6455 if remaining > 0.0 {
6456 let total_grow: f64 = styles[line.start..line.end]
6457 .iter()
6458 .map(|s| s.flex_grow)
6459 .sum();
6460 if total_grow > 0.0 {
6461 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6462 final_widths[line.start + j] = base_widths[line.start + j]
6463 + remaining * (s.flex_grow / total_grow);
6464 }
6465 }
6466 } else if remaining < 0.0 {
6467 let total_shrink: f64 = styles[line.start..line.end]
6468 .iter()
6469 .enumerate()
6470 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6471 .sum();
6472 if total_shrink > 0.0 {
6473 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6474 let factor =
6475 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6476 let w = base_widths[line.start + j] + remaining * factor;
6477 final_widths[line.start + j] = w.max(s.min_width);
6478 }
6479 }
6480 }
6481 }
6482
6483 let mut total = 0.0;
6484 for (i, line) in lines.iter().enumerate() {
6485 let line_height: f64 = children[line.start..line.end]
6486 .iter()
6487 .enumerate()
6488 .map(|(j, child)| {
6489 let fw = final_widths[line.start + j];
6490 let child_style =
6499 child.style.resolve(Some(parent_style), available_width);
6500 self.measure_node_height(child, fw, &child_style, font_context)
6501 + child_style.margin.vertical()
6502 })
6503 .fold(0.0f64, f64::max);
6504 total += line_height;
6505 if i > 0 {
6506 total += row_gap;
6507 }
6508 }
6509 total
6510 }
6511 FlexDirection::Column | FlexDirection::ColumnReverse => {
6512 let mut total = 0.0;
6513 for (i, child) in children.iter().enumerate() {
6514 let child_style = child.style.resolve(Some(parent_style), available_width);
6515 let child_height = self.measure_node_height(
6516 child,
6517 available_width,
6518 &child_style,
6519 font_context,
6520 );
6521 total += child_height + child_style.margin.vertical();
6522 if i > 0 {
6523 total += row_gap;
6524 }
6525 }
6526 total
6527 }
6528 }
6529 }
6530
6531 fn measure_intrinsic_width(
6533 &self,
6534 node: &Node,
6535 style: &ResolvedStyle,
6536 font_context: &FontContext,
6537 ) -> f64 {
6538 match &node.kind {
6539 NodeKind::Svg { width, .. } => {
6540 *width + style.padding.horizontal() + style.margin.horizontal()
6541 }
6542 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6543 let text_width = if !runs.is_empty() {
6549 runs.iter()
6550 .map(|run| {
6551 let run_style = run.style.resolve(Some(style), 0.0);
6552 let run_content = substitute_page_placeholders(&run.content);
6553 let transformed =
6554 apply_text_transform(&run_content, run_style.text_transform);
6555 let italic = matches!(
6556 run_style.font_style,
6557 FontStyle::Italic | FontStyle::Oblique
6558 );
6559 transformed
6563 .split('\n')
6564 .map(|segment| {
6565 font_context.measure_string(
6566 segment,
6567 &run_style.font_family,
6568 run_style.font_weight,
6569 italic,
6570 run_style.font_size,
6571 run_style.letter_spacing,
6572 )
6573 })
6574 .fold(0.0f64, f64::max)
6575 })
6576 .sum()
6577 } else {
6578 let content = substitute_page_placeholders(content);
6579 let transformed = apply_text_transform(&content, style.text_transform);
6580 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6581 transformed
6582 .split('\n')
6583 .map(|segment| {
6584 font_context.measure_string(
6585 segment,
6586 &style.font_family,
6587 style.font_weight,
6588 italic,
6589 style.font_size,
6590 style.letter_spacing,
6591 )
6592 })
6593 .fold(0.0f64, f64::max)
6594 };
6595 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6598 }
6599 NodeKind::Image {
6600 src, width, height, ..
6601 } => {
6602 let w = if let SizeConstraint::Fixed(w) = style.width {
6603 w
6604 } else if let Some(w) = width {
6605 *w
6606 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6607 let pixel_w = iw as f64;
6608 let pixel_h = ih as f64;
6609 let aspect = if pixel_w > 0.0 {
6610 pixel_h / pixel_w
6611 } else {
6612 0.75
6613 };
6614 let constrained_h = match style.height {
6616 SizeConstraint::Fixed(h) => Some(h),
6617 SizeConstraint::Auto => *height,
6618 };
6619 if let Some(h) = constrained_h {
6620 h / aspect
6621 } else {
6622 pixel_w
6623 }
6624 } else {
6625 100.0
6626 };
6627 w + style.padding.horizontal() + style.margin.horizontal()
6628 }
6629 NodeKind::Barcode { width, .. } => {
6630 let w = width.unwrap_or(0.0);
6631 w + style.padding.horizontal() + style.margin.horizontal()
6632 }
6633 NodeKind::QrCode { size, .. } => {
6634 let display_size = size.unwrap_or(0.0);
6635 display_size + style.padding.horizontal() + style.margin.horizontal()
6636 }
6637 NodeKind::Canvas { width, .. } => {
6638 *width + style.padding.horizontal() + style.margin.horizontal()
6639 }
6640 NodeKind::BarChart { width, .. }
6641 | NodeKind::LineChart { width, .. }
6642 | NodeKind::PieChart { width, .. }
6643 | NodeKind::AreaChart { width, .. }
6644 | NodeKind::DotPlot { width, .. } => {
6645 *width + style.padding.horizontal() + style.margin.horizontal()
6646 }
6647 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6648 *width + style.padding.horizontal() + style.margin.horizontal()
6649 }
6650 NodeKind::Table { columns } => {
6651 let num_cols = node
6656 .children
6657 .iter()
6658 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6659 .max()
6660 .unwrap_or(1)
6661 .max(columns.len().max(1));
6662 let (_, col_max) =
6663 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6664 col_max.iter().sum::<f64>()
6665 + style.padding.horizontal()
6666 + style.margin.horizontal()
6667 + style.border_width.horizontal()
6668 }
6669 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
6670 *width + style.padding.horizontal() + style.margin.horizontal()
6671 }
6672 NodeKind::Watermark { .. } => 0.0, _ => {
6674 if let SizeConstraint::Fixed(w) = style.width {
6682 return w
6683 + style.padding.horizontal()
6684 + style.border_width.horizontal()
6685 + style.margin.horizontal();
6686 }
6687 if node.children.is_empty() {
6689 style.padding.horizontal() + style.margin.horizontal()
6690 } else {
6691 let direction = style.flex_direction;
6692 let gap = style.column_gap;
6704 let mut total = 0.0f64;
6705 for (i, child) in node.children.iter().enumerate() {
6706 let child_style = child.style.resolve(Some(style), 0.0);
6707 let child_width =
6708 self.measure_intrinsic_width(child, &child_style, font_context);
6709 match direction {
6710 FlexDirection::Row | FlexDirection::RowReverse => {
6711 total += child_width;
6712 if i > 0 {
6713 total += gap;
6714 }
6715 }
6716 _ => {
6717 total = total.max(child_width);
6718 }
6719 }
6720 }
6721 total
6722 + style.padding.horizontal()
6723 + style.margin.horizontal()
6724 + style.border_width.horizontal()
6725 }
6726 }
6727 }
6728 }
6729
6730 pub fn measure_min_content_width(
6734 &self,
6735 node: &Node,
6736 style: &ResolvedStyle,
6737 font_context: &FontContext,
6738 ) -> f64 {
6739 match &node.kind {
6740 NodeKind::Table { columns } => {
6741 let num_cols = node
6744 .children
6745 .iter()
6746 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6747 .max()
6748 .unwrap_or(1)
6749 .max(columns.len().max(1));
6750 let (col_min, _) =
6751 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6752 col_min.iter().sum::<f64>()
6753 + style.padding.horizontal()
6754 + style.margin.horizontal()
6755 + style.border_width.horizontal()
6756 }
6757 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6758 let word_width = if !runs.is_empty() {
6759 runs.iter()
6761 .map(|run| {
6762 let run_style = run.style.resolve(Some(style), 0.0);
6763 let run_content = substitute_page_placeholders(&run.content);
6764 let transformed =
6765 apply_text_transform(&run_content, run_style.text_transform);
6766 self.text_layout.measure_widest_word(
6767 font_context,
6768 &transformed,
6769 run_style.font_size,
6770 &run_style.font_family,
6771 run_style.font_weight,
6772 run_style.font_style,
6773 run_style.letter_spacing,
6774 style.hyphens,
6775 style.lang.as_deref(),
6776 )
6777 })
6778 .fold(0.0f64, f64::max)
6779 } else {
6780 let content = substitute_page_placeholders(content);
6781 let transformed = apply_text_transform(&content, style.text_transform);
6782 self.text_layout.measure_widest_word(
6783 font_context,
6784 &transformed,
6785 style.font_size,
6786 &style.font_family,
6787 style.font_weight,
6788 style.font_style,
6789 style.letter_spacing,
6790 style.hyphens,
6791 style.lang.as_deref(),
6792 )
6793 };
6794 word_width + style.padding.horizontal() + style.margin.horizontal()
6795 }
6796 NodeKind::Image { width, .. } => {
6797 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
6798 }
6799 NodeKind::Svg { width, .. } => {
6800 *width + style.padding.horizontal() + style.margin.horizontal()
6801 }
6802 _ => {
6803 if node.children.is_empty() {
6804 style.padding.horizontal()
6805 + style.margin.horizontal()
6806 + style.border_width.horizontal()
6807 } else {
6808 let mut max_child_min = 0.0f64;
6809 for child in &node.children {
6810 let child_style = child.style.resolve(Some(style), 0.0);
6811 let child_min =
6812 self.measure_min_content_width(child, &child_style, font_context);
6813 max_child_min = max_child_min.max(child_min);
6814 }
6815 max_child_min
6816 + style.padding.horizontal()
6817 + style.margin.horizontal()
6818 + style.border_width.horizontal()
6819 }
6820 }
6821 }
6822 }
6823
6824 fn flex_item_baseline_distance(
6831 &self,
6832 item: &Node,
6833 style: &ResolvedStyle,
6834 w: f64,
6835 font_context: &FontContext,
6836 ) -> f64 {
6837 let first_line = match &item.kind {
6838 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
6839 let metrics = font_context.baseline_metrics(
6840 &style.font_family,
6841 style.font_weight,
6842 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
6843 );
6844 baseline_in_line(
6845 style.font_size * style.line_height,
6846 style.font_size,
6847 metrics,
6848 )
6849 }
6850 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
6851 };
6852 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
6853 }
6854
6855 fn cell_first_baseline_in_line(
6862 &self,
6863 cell: &Node,
6864 cell_style: &ResolvedStyle,
6865 w: f64,
6866 font_context: &FontContext,
6867 ) -> f64 {
6868 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
6869 for ch in &node.children {
6870 let s = ch.style.resolve(Some(parent), w);
6871 match &ch.kind {
6872 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
6873 _ => {
6874 if let Some(f) = first(ch, &s, w) {
6875 return Some(f);
6876 }
6877 }
6878 }
6879 }
6880 None
6881 }
6882 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
6883 let metrics = font_context.baseline_metrics(
6884 &s.font_family,
6885 s.font_weight,
6886 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
6887 );
6888 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
6889 }
6890
6891 fn cell_baseline_distance(
6896 &self,
6897 cell: &Node,
6898 cell_style: &ResolvedStyle,
6899 inner_width: f64,
6900 font_context: &FontContext,
6901 ) -> f64 {
6902 cell_style.padding.top
6903 + cell_style.border_width.top
6904 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
6905 }
6906
6907 fn row_baseline(
6910 &self,
6911 row: &Node,
6912 row_style: &ResolvedStyle,
6913 col_widths: &[f64],
6914 col_offsets: &[usize],
6915 font_context: &FontContext,
6916 ) -> Option<f64> {
6917 let mut b: Option<f64> = None;
6918 for (cell_i, cell) in row.children.iter().enumerate() {
6919 let span = match &cell.kind {
6920 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6921 _ => 1,
6922 };
6923 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6924 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6925 let cell_style = cell.style.resolve(Some(row_style), col_width);
6926 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6927 let iw = col_width
6928 - cell_style.padding.horizontal()
6929 - cell_style.border_width.horizontal();
6930 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
6931 b = Some(b.map_or(d, |m: f64| m.max(d)));
6932 }
6933 }
6934 b
6935 }
6936
6937 fn measure_table_row_height(
6938 &self,
6939 row: &Node,
6940 col_widths: &[f64],
6941 col_offsets: &[usize],
6942 parent_style: &ResolvedStyle,
6943 font_context: &FontContext,
6944 ) -> f64 {
6945 let row_style = row
6946 .style
6947 .resolve(Some(parent_style), col_widths.iter().sum());
6948 let mut max_height: f64 = 0.0;
6949 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
6952
6953 for (cell_i, cell) in row.children.iter().enumerate() {
6954 let span = match &cell.kind {
6955 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6956 _ => 1,
6957 };
6958 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6959 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6960 let cell_style = cell.style.resolve(Some(&row_style), col_width);
6961 let inner_width =
6962 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
6963
6964 let mut cell_content_height = 0.0;
6965 for child in &cell.children {
6966 let child_style = child.style.resolve(Some(&cell_style), inner_width);
6967 cell_content_height +=
6968 self.measure_node_height(child, inner_width, &child_style, font_context);
6969 }
6970
6971 let mut total = cell_content_height
6972 + cell_style.padding.vertical()
6973 + cell_style.border_width.vertical();
6974 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6978 if let Some(b) = row_bl {
6979 let d =
6980 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
6981 total += (b - d).max(0.0);
6982 }
6983 }
6984 if let SizeConstraint::Fixed(h) = cell_style.height {
6991 total = total.max(h);
6992 }
6993 max_height = max_height.max(total);
6994 }
6995
6996 max_height.max(row_style.min_height)
6997 }
6998
6999 fn cell_col_span(cell: &Node) -> usize {
7001 match &cell.kind {
7002 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7003 _ => 1,
7004 }
7005 }
7006
7007 fn measure_column_content(
7012 &self,
7013 children: &[Node],
7014 num_cols: usize,
7015 available_width: f64,
7016 table_style: &ResolvedStyle,
7017 font_context: &FontContext,
7018 ) -> (Vec<f64>, Vec<f64>) {
7019 let mut col_min = vec![0.0f64; num_cols];
7020 let mut col_max = vec![0.0f64; num_cols];
7021 let offsets = Self::table_column_offsets(children);
7022 for (row_i, row_node) in children.iter().enumerate() {
7023 for (cell_i, cell) in row_node.children.iter().enumerate() {
7024 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7025 let span = Self::cell_col_span(cell);
7026 let cell_style = cell.style.resolve(Some(table_style), available_width);
7027 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7028 let mut cmin = 0.0f64;
7029 let mut cmax = 0.0f64;
7030 for child in &cell.children {
7031 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7032 cmin =
7033 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7034 cmax =
7035 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7036 }
7037 cmin += chrome;
7038 let mut cmax = cmax.max(cmin) + chrome;
7039 if let SizeConstraint::Fixed(w) = cell_style.width {
7040 cmax = w.max(cmin);
7041 }
7042 let per_min = cmin / span as f64;
7043 let per_max = cmax / span as f64;
7044 for k in col..(col + span).min(num_cols) {
7045 col_min[k] = col_min[k].max(per_min);
7046 col_max[k] = col_max[k].max(per_max);
7047 }
7048 }
7049 }
7050 (col_min, col_max)
7051 }
7052
7053 fn resolve_column_widths(
7063 &self,
7064 defs: &[ColumnDef],
7065 available_width: f64,
7066 children: &[Node],
7067 table_style: &ResolvedStyle,
7068 font_context: &FontContext,
7069 ) -> Vec<f64> {
7070 if defs.is_empty() {
7071 let num_cols = Self::occupancy_column_count(children);
7072
7073 let (col_min, col_max) = self.measure_column_content(
7074 children,
7075 num_cols,
7076 available_width,
7077 table_style,
7078 font_context,
7079 );
7080
7081 let sum_min: f64 = col_min.iter().sum();
7082 let sum_max: f64 = col_max.iter().sum();
7083 let w = available_width;
7084 return if sum_max <= w {
7085 if sum_max <= f64::EPSILON {
7089 vec![w / num_cols as f64; num_cols]
7090 } else {
7091 col_max
7092 .iter()
7093 .map(|m| m + (w - sum_max) * (m / sum_max))
7094 .collect()
7095 }
7096 } else if sum_min <= w {
7097 let denom = (sum_max - sum_min).max(f64::EPSILON);
7100 col_min
7101 .iter()
7102 .zip(&col_max)
7103 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7104 .collect()
7105 } else {
7106 self.defect(format!(
7109 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7110 sum_min, w
7111 ));
7112 let scale = w / sum_min.max(f64::EPSILON);
7113 col_min.iter().map(|m| m * scale).collect()
7114 };
7115 }
7116
7117 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7123 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7124 while defs_vec.len() < num_cols {
7125 defs_vec.push(ColumnDef {
7126 width: ColumnWidth::Auto,
7127 });
7128 }
7129 let defs = &defs_vec[..];
7130
7131 let mut widths = Vec::new();
7132 let mut remaining = available_width;
7133 let mut auto_count = 0;
7134
7135 for def in defs {
7136 match def.width {
7137 ColumnWidth::Fixed(w) => {
7138 widths.push(w);
7139 remaining -= w;
7140 }
7141 ColumnWidth::Fraction(f) => {
7142 let w = available_width * f;
7143 widths.push(w);
7144 remaining -= w;
7145 }
7146 ColumnWidth::Auto => {
7147 widths.push(0.0);
7148 auto_count += 1;
7149 }
7150 }
7151 }
7152
7153 if remaining < 0.0 {
7154 self.defect(format!(
7157 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7158 available_width - remaining,
7159 available_width
7160 ));
7161 remaining = 0.0;
7162 }
7163
7164 if auto_count > 0 {
7165 let auto_width = remaining / auto_count as f64;
7166 for (i, def) in defs.iter().enumerate() {
7167 if matches!(def.width, ColumnWidth::Auto) {
7168 widths[i] = auto_width;
7169 }
7170 }
7171 }
7172
7173 let (col_min, _) = self.measure_column_content(
7180 children,
7181 widths.len(),
7182 available_width,
7183 table_style,
7184 font_context,
7185 );
7186 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7187 if needs_floor {
7188 let sum_min: f64 = col_min.iter().sum();
7189 if sum_min > available_width {
7190 self.defect(format!(
7191 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7192 sum_min, available_width
7193 ));
7194 let scale = available_width / sum_min.max(f64::EPSILON);
7195 return col_min.iter().map(|m| m * scale).collect();
7196 }
7197 let deficit: f64 = widths
7198 .iter()
7199 .zip(&col_min)
7200 .map(|(w, m)| (m - w).max(0.0))
7201 .sum();
7202 let surplus: f64 = widths
7203 .iter()
7204 .zip(&col_min)
7205 .map(|(w, m)| (w - m).max(0.0))
7206 .sum();
7207 let take = if surplus > 0.0 {
7208 deficit / surplus
7209 } else {
7210 0.0
7211 };
7212 widths = widths
7213 .iter()
7214 .zip(&col_min)
7215 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7216 .collect();
7217 }
7218
7219 widths
7220 }
7221
7222 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7223 for (page_index, page) in pages.iter_mut().enumerate() {
7224 if !page.watermarks.is_empty() {
7226 let (page_w, page_h) = page.config.size.dimensions();
7227 let cx = page_w / 2.0;
7228 let cy = page_h / 2.0;
7229
7230 let mut watermark_elements = Vec::new();
7231 for wm_node in &page.watermarks {
7232 if let NodeKind::Watermark {
7233 text,
7234 font_size,
7235 angle,
7236 } = &wm_node.kind
7237 {
7238 let style = wm_node.style.resolve(None, page_w);
7239 let color = style.color;
7240 let opacity = style.opacity;
7241 let angle_rad = angle.to_radians();
7242
7243 let italic =
7245 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7246
7247 let shaped = self.text_layout.shape_text(
7249 font_context,
7250 text,
7251 &style.font_family,
7252 style.font_weight,
7253 style.font_style,
7254 );
7255
7256 let mut glyphs = Vec::new();
7257 let mut x_pos = 0.0;
7258 let text_chars: Vec<char> = text.chars().collect();
7259
7260 if let Some(shaped_glyphs) = shaped {
7261 let units_per_em = font_context.units_per_em(
7263 &style.font_family,
7264 style.font_weight,
7265 italic,
7266 ) as f64;
7267
7268 for sg in &shaped_glyphs {
7269 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7270 let cluster_idx = sg.cluster as usize;
7271 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7272 glyphs.push(PositionedGlyph {
7273 glyph_id: sg.glyph_id,
7274 char_value: ch,
7275 x_offset: x_pos,
7276 y_offset: 0.0,
7277 x_advance: advance,
7278 font_size: *font_size,
7279 font_family: Arc::from(style.font_family.as_str()),
7280 font_weight: style.font_weight,
7281 font_style: style.font_style,
7282 color: Some(color),
7283 href: None,
7284 text_decoration: TextDecoration::None,
7285 letter_spacing: style.letter_spacing,
7286 cluster_text: None,
7287 });
7288 x_pos += advance + style.letter_spacing;
7289 }
7290 } else {
7291 for &ch in &text_chars {
7293 let w = font_context.char_width(
7294 ch,
7295 &style.font_family,
7296 style.font_weight,
7297 italic,
7298 *font_size,
7299 );
7300 glyphs.push(PositionedGlyph {
7301 glyph_id: ch as u16,
7302 char_value: ch,
7303 x_offset: x_pos,
7304 y_offset: 0.0,
7305 x_advance: w,
7306 font_size: *font_size,
7307 font_family: Arc::from(style.font_family.as_str()),
7308 font_weight: style.font_weight,
7309 font_style: style.font_style,
7310 color: Some(color),
7311 href: None,
7312 text_decoration: TextDecoration::None,
7313 letter_spacing: style.letter_spacing,
7314 cluster_text: None,
7315 });
7316 x_pos += w + style.letter_spacing;
7317 }
7318 }
7319
7320 let text_width = x_pos;
7321
7322 let line = TextLine {
7323 x: 0.0,
7324 y: 0.0,
7325 glyphs,
7326 width: text_width,
7327 height: *font_size,
7328 word_spacing: 0.0,
7329 };
7330
7331 watermark_elements.push(LayoutElement {
7332 x: cx,
7333 y: cy,
7334 width: text_width,
7335 height: *font_size,
7336 draw: DrawCommand::Watermark {
7337 lines: vec![line],
7338 color,
7339 opacity,
7340 angle_rad,
7341 font_family: style.font_family.clone(),
7342 },
7343 children: vec![],
7344 node_type: Some("Watermark".to_string()),
7345 resolved_style: None,
7346 source_location: None,
7347 href: None,
7348 bookmark: None,
7349 alt: None,
7350 is_header_row: false,
7351 actual_text: None,
7352 list_numbering: None,
7353 col_span: 1,
7354 overflow: Overflow::default(),
7355 opacity: 1.0,
7356 });
7357 }
7358 }
7359
7360 watermark_elements.append(&mut page.elements);
7362 page.elements = watermark_elements;
7363 page.watermarks.clear();
7364 }
7365
7366 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7367 continue;
7368 }
7369
7370 if !page.fixed_header.is_empty() {
7372 let mut hdr_cursor = PageCursor::new(&page.config);
7373 for (node, _h) in &page.fixed_header {
7374 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7377 continue;
7378 }
7379 let cw = hdr_cursor.content_width;
7380 let cx = hdr_cursor.content_x;
7381 let style = node.style.resolve(None, cw);
7382 self.layout_view(
7383 node,
7384 &style,
7385 &mut hdr_cursor,
7386 &mut Vec::new(),
7387 cx,
7388 cw,
7389 font_context,
7390 );
7391 }
7392 let mut combined = hdr_cursor.elements;
7394 combined.append(&mut page.elements);
7395 page.elements = combined;
7396 }
7397
7398 if !page.fixed_footer.is_empty() {
7403 let mut ftr_cursor = PageCursor::new(&page.config);
7404 let total_ftr: f64 = page
7405 .fixed_footer
7406 .iter()
7407 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7408 .map(|(_, h)| *h)
7409 .sum();
7410 let target_y = ftr_cursor.content_height - total_ftr;
7411 for (node, _h) in &page.fixed_footer {
7413 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7414 continue;
7415 }
7416 let cw = ftr_cursor.content_width;
7417 let cx = ftr_cursor.content_x;
7418 let style = node.style.resolve(None, cw);
7419 self.layout_view(
7420 node,
7421 &style,
7422 &mut ftr_cursor,
7423 &mut Vec::new(),
7424 cx,
7425 cw,
7426 font_context,
7427 );
7428 }
7429 for el in &mut ftr_cursor.elements {
7433 offset_element_y(el, target_y);
7434 }
7435 page.elements.extend(ftr_cursor.elements);
7436 }
7437
7438 page.fixed_header.clear();
7440 page.fixed_footer.clear();
7441 }
7442 }
7443
7444 #[allow(clippy::too_many_arguments)]
7449 fn layout_grid_children(
7450 &self,
7451 children: &[Node],
7452 parent_style: &ResolvedStyle,
7453 cursor: &mut PageCursor,
7454 pages: &mut Vec<LayoutPage>,
7455 x: f64,
7456 available_width: f64,
7457 font_context: &FontContext,
7458 ) {
7459 let template_cols = match &parent_style.grid_template_columns {
7460 Some(cols) => cols,
7461 None => return, };
7463
7464 let num_columns = template_cols.len();
7465 if num_columns == 0 || children.is_empty() {
7466 return;
7467 }
7468
7469 let col_gap = parent_style.column_gap;
7470 let row_gap = parent_style.row_gap;
7471
7472 let content_sizes: Vec<f64> = template_cols
7475 .iter()
7476 .map(|track| {
7477 if matches!(track, GridTrackSize::Auto) {
7478 available_width / num_columns as f64
7481 } else {
7482 0.0
7483 }
7484 })
7485 .collect();
7486
7487 let col_widths =
7488 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7489
7490 let placements: Vec<Option<&GridPlacement>> = children
7492 .iter()
7493 .map(|child| child.style.grid_placement.as_ref())
7494 .collect();
7495
7496 let item_placements = grid::place_items(&placements, num_columns);
7498 let num_rows = grid::compute_num_rows(&item_placements);
7499
7500 if num_rows == 0 {
7501 return;
7502 }
7503
7504 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7506 for placement in &item_placements {
7507 let cell_width =
7508 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7509 let child = &children[placement.child_index];
7510 let child_style = child.style.resolve(Some(parent_style), cell_width);
7511 item_heights[placement.child_index] =
7512 self.measure_node_height(child, cell_width, &child_style, font_context);
7513 }
7514
7515 let template_rows = parent_style.grid_template_rows.as_deref();
7517 let mut row_heights = vec![0.0_f64; num_rows];
7518 for placement in &item_placements {
7519 let h = item_heights[placement.child_index];
7520 let span = placement.row_end - placement.row_start;
7521 let per_row = h / span as f64;
7522 for rh in row_heights
7523 .iter_mut()
7524 .take(placement.row_end.min(num_rows))
7525 .skip(placement.row_start)
7526 {
7527 if per_row > *rh {
7528 *rh = per_row;
7529 }
7530 }
7531 }
7532
7533 if let Some(template) = template_rows {
7535 let auto_row = parent_style.grid_auto_rows.as_ref();
7536 for (r, rh) in row_heights.iter_mut().enumerate() {
7537 let track = template.get(r).or(auto_row);
7538 if let Some(track) = track {
7539 match track {
7540 GridTrackSize::Pt(pts) => *rh = *pts,
7541 GridTrackSize::Auto => {} _ => {} }
7544 }
7545 }
7546 }
7547
7548 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7550 if row_height > cursor.remaining_height() {
7555 pages.push(cursor.finalize());
7556 *cursor = cursor.new_page();
7557 }
7558
7559 let row_start_y = cursor.y;
7560
7561 for placement in &item_placements {
7563 if placement.row_start != row {
7564 continue; }
7566
7567 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7568 let cell_width =
7569 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7570
7571 let child = &children[placement.child_index];
7572
7573 self.layout_node(
7574 child,
7575 cursor,
7576 pages,
7577 cell_x,
7578 cell_width,
7579 Some(parent_style),
7580 font_context,
7581 None,
7582 None,
7583 );
7584 cursor.y = row_start_y;
7586 }
7587
7588 cursor.y = row_start_y + row_height + row_gap;
7589 }
7590
7591 if num_rows > 0 {
7593 cursor.y -= row_gap;
7594 }
7595 }
7596}
7597
7598struct FlexItem<'a> {
7599 node: &'a Node,
7600 style: ResolvedStyle,
7601 base_width: f64,
7602 min_content_width: f64,
7603}
7604
7605fn first_text_snippet(node: &Node) -> Option<String> {
7608 fn walk(n: &Node) -> Option<&str> {
7609 match &n.kind {
7610 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7611 if !content.trim().is_empty() {
7612 return Some(content.trim());
7613 }
7614 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
7615 return Some(r.content.trim());
7616 }
7617 None
7618 }
7619 _ => n.children.iter().find_map(walk),
7620 }
7621 }
7622 walk(node).map(|t| {
7623 let mut s: String = t.chars().take(32).collect();
7624 if t.chars().count() > 32 {
7625 s.push('…');
7626 }
7627 s
7628 })
7629}
7630
7631fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
7641 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
7642}
7643
7644#[cfg(test)]
7645mod tests {
7646 use super::*;
7647 use crate::font::FontContext;
7648
7649 fn make_text(content: &str, font_size: f64) -> Node {
7650 Node {
7651 kind: NodeKind::Text {
7652 content: content.to_string(),
7653 href: None,
7654 runs: vec![],
7655 },
7656 style: Style {
7657 font_size: Some(font_size),
7658 ..Default::default()
7659 },
7660 children: vec![],
7661 id: None,
7662 source_location: None,
7663 bookmark: None,
7664 href: None,
7665 alt: None,
7666 }
7667 }
7668
7669 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
7670 Node {
7671 kind: NodeKind::View,
7672 style,
7673 children,
7674 id: None,
7675 source_location: None,
7676 bookmark: None,
7677 href: None,
7678 alt: None,
7679 }
7680 }
7681
7682 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
7683 Node {
7684 kind: NodeKind::Text {
7685 content: String::new(),
7686 href: None,
7687 runs,
7688 },
7689 style: Style::default(),
7690 children: vec![],
7691 id: None,
7692 source_location: None,
7693 bookmark: None,
7694 href: None,
7695 alt: None,
7696 }
7697 }
7698
7699 #[test]
7700 fn intrinsic_width_measures_runs_not_just_content() {
7701 let engine = LayoutEngine::new();
7705 let font_context = FontContext::new();
7706
7707 let runs_node = make_runs_text(vec![
7708 crate::model::TextRun {
7709 content: "Hello ".to_string(),
7710 style: Style::default(),
7711 href: None,
7712 },
7713 crate::model::TextRun {
7714 content: "World".to_string(),
7715 style: Style {
7716 font_weight: Some(700),
7717 ..Default::default()
7718 },
7719 href: None,
7720 },
7721 ]);
7722 let plain_node = make_text("Hello World", 12.0);
7723
7724 let runs_style = runs_node.style.resolve(None, 0.0);
7725 let plain_style = plain_node.style.resolve(None, 0.0);
7726 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
7727 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
7728
7729 assert!(
7732 runs_w >= plain_w,
7733 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
7734 );
7735 assert!(
7736 runs_w < plain_w * 1.5,
7737 "runs width ({runs_w}) should be close to plain width ({plain_w})"
7738 );
7739 }
7740
7741 #[test]
7742 fn intrinsic_width_of_multiline_text_is_widest_line() {
7743 let engine = LayoutEngine::new();
7744 let font_context = FontContext::new();
7745
7746 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
7747 let widest = make_text("Springfield, IL 62704", 12.0);
7748
7749 let m_style = multiline.style.resolve(None, 0.0);
7750 let w_style = widest.style.resolve(None, 0.0);
7751 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
7752 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
7753
7754 assert!(
7755 (m_w - w_w).abs() < 0.01,
7756 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
7757 );
7758 }
7759
7760 #[test]
7761 fn intrinsic_width_of_heading_measures_its_text() {
7762 let engine = LayoutEngine::new();
7765 let font_context = FontContext::new();
7766
7767 let heading = Node {
7768 kind: NodeKind::Heading {
7769 level: 1,
7770 content: "Invoice #2024-001".to_string(),
7771 href: None,
7772 runs: vec![],
7773 },
7774 style: Style {
7775 font_size: Some(24.0),
7776 ..Default::default()
7777 },
7778 children: vec![],
7779 id: None,
7780 source_location: None,
7781 bookmark: None,
7782 href: None,
7783 alt: None,
7784 };
7785 let style = heading.style.resolve(None, 0.0);
7786 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
7787 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
7788 }
7789
7790 #[test]
7791 fn measure_node_height_of_wrapping_heading_matches_text() {
7792 let engine = LayoutEngine::new();
7799 let font_context = FontContext::new();
7800
7801 let content = "Annual Performance Review";
7802 let heading = Node {
7803 kind: NodeKind::Heading {
7804 level: 1,
7805 content: content.to_string(),
7806 href: None,
7807 runs: vec![],
7808 },
7809 style: Style {
7810 font_size: Some(32.0),
7811 ..Default::default()
7812 },
7813 children: vec![],
7814 id: None,
7815 source_location: None,
7816 bookmark: None,
7817 href: None,
7818 alt: None,
7819 };
7820 let text = make_text(content, 32.0);
7821
7822 let width = 200.0;
7824 let h_style = heading.style.resolve(None, width);
7825 let t_style = text.style.resolve(None, width);
7826 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
7827 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
7828
7829 assert!(
7830 h_height > 32.0,
7831 "a wrapping 32pt heading must measure more than one line, got {h_height}"
7832 );
7833 assert!(
7834 (h_height - t_height).abs() < 0.01,
7835 "heading height ({h_height}) must equal the same text's height ({t_height})"
7836 );
7837 }
7838
7839 #[test]
7840 fn intrinsic_width_flex_row_sums_children() {
7841 let engine = LayoutEngine::new();
7842 let font_context = FontContext::new();
7843
7844 let child1 = make_text("Hello", 14.0);
7845 let child2 = make_text("World", 14.0);
7846
7847 let child1_style = child1.style.resolve(None, 0.0);
7848 let child2_style = child2.style.resolve(None, 0.0);
7849 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
7850 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
7851
7852 let row = make_styled_view(
7853 Style {
7854 flex_direction: Some(FlexDirection::Row),
7855 ..Default::default()
7856 },
7857 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
7858 );
7859 let row_style = row.style.resolve(None, 0.0);
7860 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
7861
7862 assert!(
7863 (row_w - (child1_w + child2_w)).abs() < 0.01,
7864 "Row intrinsic width ({}) should equal sum of children ({} + {})",
7865 row_w,
7866 child1_w,
7867 child2_w
7868 );
7869 }
7870
7871 #[test]
7872 fn intrinsic_width_flex_column_takes_max() {
7873 let engine = LayoutEngine::new();
7874 let font_context = FontContext::new();
7875
7876 let short = make_text("Hi", 14.0);
7877 let long = make_text("Hello World", 14.0);
7878
7879 let short_style = short.style.resolve(None, 0.0);
7880 let long_style = long.style.resolve(None, 0.0);
7881 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
7882 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
7883
7884 let col = make_styled_view(
7885 Style {
7886 flex_direction: Some(FlexDirection::Column),
7887 ..Default::default()
7888 },
7889 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
7890 );
7891 let col_style = col.style.resolve(None, 0.0);
7892 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
7893
7894 assert!(
7895 (col_w - long_w).abs() < 0.01,
7896 "Column intrinsic width ({}) should equal max child ({}, short was {})",
7897 col_w,
7898 long_w,
7899 short_w
7900 );
7901 }
7902
7903 #[test]
7904 fn intrinsic_width_nested_containers() {
7905 let engine = LayoutEngine::new();
7906 let font_context = FontContext::new();
7907
7908 let inner = make_styled_view(
7909 Style {
7910 flex_direction: Some(FlexDirection::Row),
7911 ..Default::default()
7912 },
7913 vec![make_text("A", 12.0), make_text("B", 12.0)],
7914 );
7915 let inner_style = inner.style.resolve(None, 0.0);
7916 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
7917
7918 let outer = make_styled_view(
7919 Style::default(),
7920 vec![make_styled_view(
7921 Style {
7922 flex_direction: Some(FlexDirection::Row),
7923 ..Default::default()
7924 },
7925 vec![make_text("A", 12.0), make_text("B", 12.0)],
7926 )],
7927 );
7928 let outer_style = outer.style.resolve(None, 0.0);
7929 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
7930
7931 assert!(
7932 (outer_w - inner_w).abs() < 0.01,
7933 "Nested container ({}) should match inner container ({})",
7934 outer_w,
7935 inner_w
7936 );
7937 }
7938
7939 #[test]
7940 fn intrinsic_width_row_with_gap() {
7941 let engine = LayoutEngine::new();
7942 let font_context = FontContext::new();
7943
7944 let no_gap = make_styled_view(
7945 Style {
7946 flex_direction: Some(FlexDirection::Row),
7947 ..Default::default()
7948 },
7949 vec![make_text("A", 12.0), make_text("B", 12.0)],
7950 );
7951 let with_gap = make_styled_view(
7952 Style {
7953 flex_direction: Some(FlexDirection::Row),
7954 gap: Some(10.0),
7955 ..Default::default()
7956 },
7957 vec![make_text("A", 12.0), make_text("B", 12.0)],
7958 );
7959
7960 let no_gap_style = no_gap.style.resolve(None, 0.0);
7961 let with_gap_style = with_gap.style.resolve(None, 0.0);
7962 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
7963 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
7964
7965 assert!(
7966 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
7967 "Gap should add 10pt: with_gap={}, no_gap={}",
7968 with_gap_w,
7969 no_gap_w
7970 );
7971 }
7972
7973 #[test]
7974 fn intrinsic_width_empty_container() {
7975 let engine = LayoutEngine::new();
7976 let font_context = FontContext::new();
7977
7978 let padding = 8.0;
7979 let empty = make_styled_view(
7980 Style {
7981 padding: Some(Edges::uniform(padding)),
7982 ..Default::default()
7983 },
7984 vec![],
7985 );
7986 let style = empty.style.resolve(None, 0.0);
7987 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
7988
7989 assert!(
7990 (w - padding * 2.0).abs() < 0.01,
7991 "Empty container width ({}) should equal horizontal padding ({})",
7992 w,
7993 padding * 2.0
7994 );
7995 }
7996
7997 #[test]
8000 fn flex_shrink_respects_min_content_width() {
8001 let engine = LayoutEngine::new();
8005 let font_context = FontContext::new();
8006
8007 let sale_text = make_text("SALE", 12.0);
8008 let sale_style = sale_text.style.resolve(None, 0.0);
8009 let sale_word_width =
8010 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8011 assert!(
8012 sale_word_width > 0.0,
8013 "SALE should have non-zero min-content width"
8014 );
8015
8016 let container = make_styled_view(
8019 Style {
8020 flex_direction: Some(FlexDirection::Row),
8021 width: Some(Dimension::Pt(100.0)),
8022 ..Default::default()
8023 },
8024 vec![
8025 make_styled_view(
8026 Style {
8027 width: Some(Dimension::Pt(80.0)),
8028 flex_shrink: Some(1.0),
8029 ..Default::default()
8030 },
8031 vec![],
8032 ),
8033 make_styled_view(
8034 Style {
8035 width: Some(Dimension::Pt(60.0)),
8036 flex_shrink: Some(1.0),
8037 ..Default::default()
8038 },
8039 vec![make_text("SALE", 12.0)],
8040 ),
8041 ],
8042 );
8043
8044 let doc = Document {
8045 children: vec![Node::page(
8046 PageConfig::default(),
8047 Style::default(),
8048 vec![container],
8049 )],
8050 metadata: Default::default(),
8051 default_page: PageConfig::default(),
8052 first_page: None,
8053 left_page: None,
8054 right_page: None,
8055 named_pages: Default::default(),
8056 attachments: vec![],
8057 zugferd: None,
8058 fonts: vec![],
8059 tagged: false,
8060 pdfa: None,
8061 default_style: None,
8062 embedded_data: None,
8063 flatten_forms: false,
8064 pdf_ua: false,
8065 certification: None,
8066 pdf_version: Default::default(),
8067 pdf_ua2: false,
8068 };
8069
8070 let pages = engine.layout(&doc, &font_context);
8071 assert!(!pages.is_empty());
8072
8073 let page = &pages[0];
8076 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8078 assert!(
8079 container_el.is_some(),
8080 "Should find container with 2 children"
8081 );
8082 let sale_child = &container_el.unwrap().children[1];
8083 assert!(
8084 sale_child.width >= sale_word_width - 0.01,
8085 "SALE child width ({}) should be >= min-content width ({})",
8086 sale_child.width,
8087 sale_word_width
8088 );
8089 }
8090
8091 #[test]
8094 fn column_justify_content_center() {
8095 let engine = LayoutEngine::new();
8098 let font_context = FontContext::new();
8099
8100 let container = make_styled_view(
8101 Style {
8102 flex_direction: Some(FlexDirection::Column),
8103 height: Some(Dimension::Pt(200.0)),
8104 justify_content: Some(JustifyContent::Center),
8105 ..Default::default()
8106 },
8107 vec![make_text("Centered", 12.0)],
8108 );
8109
8110 let doc = Document {
8111 children: vec![Node::page(
8112 PageConfig::default(),
8113 Style::default(),
8114 vec![container],
8115 )],
8116 metadata: Default::default(),
8117 default_page: PageConfig::default(),
8118 first_page: None,
8119 left_page: None,
8120 right_page: None,
8121 named_pages: Default::default(),
8122 attachments: vec![],
8123 zugferd: None,
8124 fonts: vec![],
8125 tagged: false,
8126 pdfa: None,
8127 default_style: None,
8128 embedded_data: None,
8129 flatten_forms: false,
8130 pdf_ua: false,
8131 certification: None,
8132 pdf_version: Default::default(),
8133 pdf_ua2: false,
8134 };
8135
8136 let pages = engine.layout(&doc, &font_context);
8137 let page = &pages[0];
8138
8139 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8142 assert!(
8143 container_el.is_some(),
8144 "Should find container with children"
8145 );
8146 let container_el = container_el.unwrap();
8147 let child = &container_el.children[0];
8148
8149 let child_offset = child.y - container_el.y;
8151 let expected_offset = (200.0 - child.height) / 2.0;
8152 assert!(
8153 (child_offset - expected_offset).abs() < 2.0,
8154 "Child offset ({}) should be near center ({})",
8155 child_offset,
8156 expected_offset
8157 );
8158 }
8159
8160 #[test]
8161 fn column_align_items_center() {
8162 let engine = LayoutEngine::new();
8165 let font_context = FontContext::new();
8166
8167 let container = make_styled_view(
8168 Style {
8169 flex_direction: Some(FlexDirection::Column),
8170 width: Some(Dimension::Pt(300.0)),
8171 align_items: Some(AlignItems::Center),
8172 ..Default::default()
8173 },
8174 vec![make_text("Hi", 12.0)],
8175 );
8176
8177 let doc = Document {
8178 children: vec![Node::page(
8179 PageConfig::default(),
8180 Style::default(),
8181 vec![container],
8182 )],
8183 metadata: Default::default(),
8184 default_page: PageConfig::default(),
8185 first_page: None,
8186 left_page: None,
8187 right_page: None,
8188 named_pages: Default::default(),
8189 attachments: vec![],
8190 zugferd: None,
8191 fonts: vec![],
8192 tagged: false,
8193 pdfa: None,
8194 default_style: None,
8195 embedded_data: None,
8196 flatten_forms: false,
8197 pdf_ua: false,
8198 certification: None,
8199 pdf_version: Default::default(),
8200 pdf_ua2: false,
8201 };
8202
8203 let pages = engine.layout(&doc, &font_context);
8204 let page = &pages[0];
8205
8206 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8207 assert!(container_el.is_some());
8208 let container_el = container_el.unwrap();
8209 let child = &container_el.children[0];
8210
8211 let child_center = child.x + child.width / 2.0;
8213 let container_center = container_el.x + container_el.width / 2.0;
8214 assert!(
8215 (child_center - container_center).abs() < 2.0,
8216 "Child center ({}) should be near container center ({})",
8217 child_center,
8218 container_center
8219 );
8220 }
8221
8222 #[test]
8225 fn absolute_child_positioned_relative_to_parent() {
8226 let engine = LayoutEngine::new();
8230 let font_context = FontContext::new();
8231
8232 let parent = make_styled_view(
8233 Style {
8234 position: Some(crate::model::Position::Relative),
8235 margin: Some(MarginEdges::from_edges(Edges {
8236 top: 50.0,
8237 left: 50.0,
8238 ..Default::default()
8239 })),
8240 width: Some(Dimension::Pt(200.0)),
8241 height: Some(Dimension::Pt(200.0)),
8242 ..Default::default()
8243 },
8244 vec![make_styled_view(
8245 Style {
8246 position: Some(crate::model::Position::Absolute),
8247 top: Some(10.0),
8248 left: Some(10.0),
8249 width: Some(Dimension::Pt(50.0)),
8250 height: Some(Dimension::Pt(50.0)),
8251 ..Default::default()
8252 },
8253 vec![],
8254 )],
8255 );
8256
8257 let doc = Document {
8258 children: vec![Node::page(
8259 PageConfig::default(),
8260 Style::default(),
8261 vec![parent],
8262 )],
8263 metadata: Default::default(),
8264 default_page: PageConfig::default(),
8265 first_page: None,
8266 left_page: None,
8267 right_page: None,
8268 named_pages: Default::default(),
8269 attachments: vec![],
8270 zugferd: None,
8271 fonts: vec![],
8272 tagged: false,
8273 pdfa: None,
8274 default_style: None,
8275 embedded_data: None,
8276 flatten_forms: false,
8277 pdf_ua: false,
8278 certification: None,
8279 pdf_version: Default::default(),
8280 pdf_ua2: false,
8281 };
8282
8283 let pages = engine.layout(&doc, &font_context);
8284 let page = &pages[0];
8285
8286 let parent_el = page
8289 .elements
8290 .iter()
8291 .find(|e| e.width > 190.0 && e.width < 210.0);
8292 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8293 let parent_el = parent_el.unwrap();
8294
8295 let abs_child = parent_el
8297 .children
8298 .iter()
8299 .find(|e| e.width > 45.0 && e.width < 55.0);
8300 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8301 let abs_child = abs_child.unwrap();
8302
8303 let expected_x = parent_el.x + 10.0;
8304 let expected_y = parent_el.y + 10.0;
8305 assert!(
8306 (abs_child.x - expected_x).abs() < 1.0,
8307 "Absolute child x ({}) should be parent.x + 10 ({})",
8308 abs_child.x,
8309 expected_x
8310 );
8311 assert!(
8312 (abs_child.y - expected_y).abs() < 1.0,
8313 "Absolute child y ({}) should be parent.y + 10 ({})",
8314 abs_child.y,
8315 expected_y
8316 );
8317 }
8318
8319 #[test]
8320 fn absolute_escapes_unpositioned_parent_to_page() {
8321 let engine = LayoutEngine::new();
8326 let font_context = FontContext::new();
8327 let parent = make_styled_view(
8328 Style {
8329 margin: Some(MarginEdges::from_edges(Edges {
8330 top: 50.0,
8331 left: 50.0,
8332 ..Default::default()
8333 })),
8334 width: Some(Dimension::Pt(200.0)),
8335 height: Some(Dimension::Pt(200.0)),
8336 ..Default::default()
8337 },
8338 vec![make_styled_view(
8339 Style {
8340 position: Some(crate::model::Position::Absolute),
8341 top: Some(10.0),
8342 left: Some(10.0),
8343 width: Some(Dimension::Pt(50.0)),
8344 height: Some(Dimension::Pt(50.0)),
8345 ..Default::default()
8346 },
8347 vec![],
8348 )],
8349 );
8350 let doc = Document {
8351 children: vec![Node::page(
8352 PageConfig::default(),
8353 Style::default(),
8354 vec![parent],
8355 )],
8356 metadata: Default::default(),
8357 default_page: PageConfig::default(),
8358 first_page: None,
8359 left_page: None,
8360 right_page: None,
8361 named_pages: Default::default(),
8362 attachments: vec![],
8363 zugferd: None,
8364 fonts: vec![],
8365 tagged: false,
8366 pdfa: None,
8367 default_style: None,
8368 embedded_data: None,
8369 flatten_forms: false,
8370 pdf_ua: false,
8371 certification: None,
8372 pdf_version: Default::default(),
8373 pdf_ua2: false,
8374 };
8375 let pages = engine.layout(&doc, &font_context);
8376 let page = &pages[0];
8377 let parent_el = page
8378 .elements
8379 .iter()
8380 .find(|e| e.width > 190.0 && e.width < 210.0)
8381 .expect("parent");
8382 let abs_child = parent_el
8383 .children
8384 .iter()
8385 .find(|e| e.width > 45.0 && e.width < 55.0)
8386 .expect("abs child");
8387 let page_left = PageConfig::default().margin.left;
8388 let page_top = PageConfig::default().margin.top;
8389 assert!(
8390 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
8391 "absolute escapes to the page: x {} should be page_left + 10 ({})",
8392 abs_child.x,
8393 page_left + 10.0
8394 );
8395 assert!(
8396 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
8397 "absolute escapes to the page: y {} should be page_top + 10 ({})",
8398 abs_child.y,
8399 page_top + 10.0
8400 );
8401 assert!(
8402 abs_child.x < parent_el.x,
8403 "child must no longer be parent-relative (parent is 50pt further in)"
8404 );
8405 }
8406
8407 #[test]
8408 fn text_transform_none_passthrough() {
8409 assert_eq!(
8410 apply_text_transform("Hello World", TextTransform::None),
8411 "Hello World"
8412 );
8413 }
8414
8415 #[test]
8416 fn text_transform_uppercase() {
8417 assert_eq!(
8418 apply_text_transform("hello world", TextTransform::Uppercase),
8419 "HELLO WORLD"
8420 );
8421 }
8422
8423 #[test]
8424 fn text_transform_lowercase() {
8425 assert_eq!(
8426 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
8427 "hello world"
8428 );
8429 }
8430
8431 #[test]
8432 fn text_transform_capitalize() {
8433 assert_eq!(
8434 apply_text_transform("hello world", TextTransform::Capitalize),
8435 "Hello World"
8436 );
8437 assert_eq!(
8438 apply_text_transform(" hello world ", TextTransform::Capitalize),
8439 " Hello World "
8440 );
8441 assert_eq!(
8442 apply_text_transform("already Capitalized", TextTransform::Capitalize),
8443 "Already Capitalized"
8444 );
8445 }
8446
8447 #[test]
8448 fn text_transform_capitalize_empty() {
8449 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
8450 }
8451
8452 #[test]
8453 fn apply_char_transform_uppercase() {
8454 assert_eq!(
8455 apply_char_transform('a', TextTransform::Uppercase, false),
8456 'A'
8457 );
8458 assert_eq!(
8459 apply_char_transform('A', TextTransform::Uppercase, false),
8460 'A'
8461 );
8462 }
8463
8464 #[test]
8465 fn apply_char_transform_capitalize_word_start() {
8466 assert_eq!(
8467 apply_char_transform('h', TextTransform::Capitalize, true),
8468 'H'
8469 );
8470 assert_eq!(
8471 apply_char_transform('h', TextTransform::Capitalize, false),
8472 'h'
8473 );
8474 }
8475
8476 #[test]
8479 fn column_flex_grow_single_child_fills_container() {
8480 let engine = LayoutEngine::new();
8483 let font_context = FontContext::new();
8484
8485 let child = make_styled_view(
8486 Style {
8487 flex_grow: Some(1.0),
8488 ..Default::default()
8489 },
8490 vec![make_text("Short", 12.0)],
8491 );
8492
8493 let container = make_styled_view(
8494 Style {
8495 flex_direction: Some(FlexDirection::Column),
8496 height: Some(Dimension::Pt(300.0)),
8497 ..Default::default()
8498 },
8499 vec![child],
8500 );
8501
8502 let doc = Document {
8503 children: vec![Node::page(
8504 PageConfig::default(),
8505 Style::default(),
8506 vec![container],
8507 )],
8508 metadata: Default::default(),
8509 default_page: PageConfig::default(),
8510 first_page: None,
8511 left_page: None,
8512 right_page: None,
8513 named_pages: Default::default(),
8514 attachments: vec![],
8515 zugferd: None,
8516 fonts: vec![],
8517 tagged: false,
8518 pdfa: None,
8519 default_style: None,
8520 embedded_data: None,
8521 flatten_forms: false,
8522 pdf_ua: false,
8523 certification: None,
8524 pdf_version: Default::default(),
8525 pdf_ua2: false,
8526 };
8527
8528 let pages = engine.layout(&doc, &font_context);
8529 let page = &pages[0];
8530
8531 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8532 assert!(container_el.is_some());
8533 let container_el = container_el.unwrap();
8534 assert!(
8535 (container_el.height - 300.0).abs() < 1.0,
8536 "Container should be 300pt, got {}",
8537 container_el.height
8538 );
8539
8540 let child_el = &container_el.children[0];
8541 assert!(
8542 (child_el.height - 300.0).abs() < 1.0,
8543 "flex-grow child should expand to 300pt, got {}",
8544 child_el.height
8545 );
8546 }
8547
8548 #[test]
8549 fn column_flex_grow_two_children_proportional() {
8550 let engine = LayoutEngine::new();
8553 let font_context = FontContext::new();
8554
8555 let child1 = make_styled_view(
8556 Style {
8557 flex_grow: Some(1.0),
8558 ..Default::default()
8559 },
8560 vec![make_text("A", 12.0)],
8561 );
8562 let child2 = make_styled_view(
8563 Style {
8564 flex_grow: Some(2.0),
8565 ..Default::default()
8566 },
8567 vec![make_text("B", 12.0)],
8568 );
8569
8570 let container = make_styled_view(
8571 Style {
8572 flex_direction: Some(FlexDirection::Column),
8573 height: Some(Dimension::Pt(300.0)),
8574 ..Default::default()
8575 },
8576 vec![child1, child2],
8577 );
8578
8579 let doc = Document {
8580 children: vec![Node::page(
8581 PageConfig::default(),
8582 Style::default(),
8583 vec![container],
8584 )],
8585 metadata: Default::default(),
8586 default_page: PageConfig::default(),
8587 first_page: None,
8588 left_page: None,
8589 right_page: None,
8590 named_pages: Default::default(),
8591 attachments: vec![],
8592 zugferd: None,
8593 fonts: vec![],
8594 tagged: false,
8595 pdfa: None,
8596 default_style: None,
8597 embedded_data: None,
8598 flatten_forms: false,
8599 pdf_ua: false,
8600 certification: None,
8601 pdf_version: Default::default(),
8602 pdf_ua2: false,
8603 };
8604
8605 let pages = engine.layout(&doc, &font_context);
8606 let page = &pages[0];
8607
8608 let container_el = page
8609 .elements
8610 .iter()
8611 .find(|e| e.children.len() == 2)
8612 .expect("Should find container with two children");
8613
8614 let c1 = &container_el.children[0];
8615 let c2 = &container_el.children[1];
8616
8617 let total = c1.height + c2.height;
8621 assert!(
8622 (total - 300.0).abs() < 2.0,
8623 "Children should sum to ~300pt, got {}",
8624 total
8625 );
8626
8627 let ratio = c2.height / c1.height;
8630 assert!(
8631 ratio > 1.3 && ratio < 2.5,
8632 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
8633 ratio
8634 );
8635 }
8636
8637 #[test]
8638 fn column_flex_grow_mixed_grow_and_fixed() {
8639 let engine = LayoutEngine::new();
8642 let font_context = FontContext::new();
8643
8644 let fixed_child = make_styled_view(
8645 Style {
8646 height: Some(Dimension::Pt(50.0)),
8647 ..Default::default()
8648 },
8649 vec![make_text("Fixed", 12.0)],
8650 );
8651 let grow_child = make_styled_view(
8652 Style {
8653 flex_grow: Some(1.0),
8654 ..Default::default()
8655 },
8656 vec![make_text("Grow", 12.0)],
8657 );
8658
8659 let container = make_styled_view(
8660 Style {
8661 flex_direction: Some(FlexDirection::Column),
8662 height: Some(Dimension::Pt(300.0)),
8663 ..Default::default()
8664 },
8665 vec![fixed_child, grow_child],
8666 );
8667
8668 let doc = Document {
8669 children: vec![Node::page(
8670 PageConfig::default(),
8671 Style::default(),
8672 vec![container],
8673 )],
8674 metadata: Default::default(),
8675 default_page: PageConfig::default(),
8676 first_page: None,
8677 left_page: None,
8678 right_page: None,
8679 named_pages: Default::default(),
8680 attachments: vec![],
8681 zugferd: None,
8682 fonts: vec![],
8683 tagged: false,
8684 pdfa: None,
8685 default_style: None,
8686 embedded_data: None,
8687 flatten_forms: false,
8688 pdf_ua: false,
8689 certification: None,
8690 pdf_version: Default::default(),
8691 pdf_ua2: false,
8692 };
8693
8694 let pages = engine.layout(&doc, &font_context);
8695 let page = &pages[0];
8696
8697 let container_el = page
8698 .elements
8699 .iter()
8700 .find(|e| e.children.len() == 2)
8701 .expect("Should find container with two children");
8702
8703 let fixed_el = &container_el.children[0];
8704 let grow_el = &container_el.children[1];
8705
8706 assert!(
8708 (fixed_el.height - 50.0).abs() < 1.0,
8709 "Fixed child should stay at 50pt, got {}",
8710 fixed_el.height
8711 );
8712
8713 assert!(
8715 (grow_el.height - 250.0).abs() < 2.0,
8716 "Grow child should expand to ~250pt, got {}",
8717 grow_el.height
8718 );
8719 }
8720
8721 #[test]
8722 fn column_flex_grow_page_level() {
8723 let engine = LayoutEngine::new();
8725 let font_context = FontContext::new();
8726
8727 let grow_child = make_styled_view(
8728 Style {
8729 flex_grow: Some(1.0),
8730 ..Default::default()
8731 },
8732 vec![make_text("Fill page", 12.0)],
8733 );
8734
8735 let doc = Document {
8736 children: vec![Node::page(
8737 PageConfig::default(),
8738 Style::default(),
8739 vec![grow_child],
8740 )],
8741 metadata: Default::default(),
8742 default_page: PageConfig::default(),
8743 first_page: None,
8744 left_page: None,
8745 right_page: None,
8746 named_pages: Default::default(),
8747 attachments: vec![],
8748 zugferd: None,
8749 fonts: vec![],
8750 tagged: false,
8751 pdfa: None,
8752 default_style: None,
8753 embedded_data: None,
8754 flatten_forms: false,
8755 pdf_ua: false,
8756 certification: None,
8757 pdf_version: Default::default(),
8758 pdf_ua2: false,
8759 };
8760
8761 let pages = engine.layout(&doc, &font_context);
8762 let page = &pages[0];
8763
8764 assert!(
8766 !page.elements.is_empty(),
8767 "Page should have at least one element"
8768 );
8769
8770 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
8771 let el = &page.elements[0];
8772 assert!(
8773 (el.height - content_height).abs() < 2.0,
8774 "Page-level flex-grow child should fill content height ({}), got {}",
8775 content_height,
8776 el.height
8777 );
8778 }
8779
8780 #[test]
8781 fn column_flex_grow_with_justify_content() {
8782 let engine = LayoutEngine::new();
8786 let font_context = FontContext::new();
8787
8788 let fixed_child = make_styled_view(
8789 Style {
8790 height: Some(Dimension::Pt(50.0)),
8791 ..Default::default()
8792 },
8793 vec![make_text("Top", 12.0)],
8794 );
8795 let grow_child = make_styled_view(
8796 Style {
8797 flex_grow: Some(1.0),
8798 ..Default::default()
8799 },
8800 vec![make_text("Fill", 12.0)],
8801 );
8802
8803 let container = make_styled_view(
8804 Style {
8805 flex_direction: Some(FlexDirection::Column),
8806 height: Some(Dimension::Pt(300.0)),
8807 justify_content: Some(JustifyContent::Center),
8808 ..Default::default()
8809 },
8810 vec![fixed_child, grow_child],
8811 );
8812
8813 let doc = Document {
8814 children: vec![Node::page(
8815 PageConfig::default(),
8816 Style::default(),
8817 vec![container],
8818 )],
8819 metadata: Default::default(),
8820 default_page: PageConfig::default(),
8821 first_page: None,
8822 left_page: None,
8823 right_page: None,
8824 named_pages: Default::default(),
8825 attachments: vec![],
8826 zugferd: None,
8827 fonts: vec![],
8828 tagged: false,
8829 pdfa: None,
8830 default_style: None,
8831 embedded_data: None,
8832 flatten_forms: false,
8833 pdf_ua: false,
8834 certification: None,
8835 pdf_version: Default::default(),
8836 pdf_ua2: false,
8837 };
8838
8839 let pages = engine.layout(&doc, &font_context);
8840 let page = &pages[0];
8841
8842 let container_el = page
8843 .elements
8844 .iter()
8845 .find(|e| e.children.len() == 2)
8846 .expect("Should find container");
8847
8848 let first_child = &container_el.children[0];
8851 assert!(
8852 (first_child.y - container_el.y).abs() < 1.0,
8853 "First child should be at top of container"
8854 );
8855
8856 let total = container_el.children[0].height + container_el.children[1].height;
8858 assert!(
8859 (total - 300.0).abs() < 2.0,
8860 "Children should fill container, got {}",
8861 total
8862 );
8863 }
8864
8865 #[test]
8866 fn column_flex_grow_child_justify_content_center() {
8867 let engine = LayoutEngine::new();
8870 let font_context = FontContext::new();
8871
8872 let inner_box = make_styled_view(
8874 Style {
8875 height: Some(Dimension::Pt(40.0)),
8876 ..Default::default()
8877 },
8878 vec![make_text("Centered", 12.0)],
8879 );
8880
8881 let grow_child = make_styled_view(
8883 Style {
8884 flex_grow: Some(1.0),
8885 flex_direction: Some(FlexDirection::Column),
8886 justify_content: Some(JustifyContent::Center),
8887 ..Default::default()
8888 },
8889 vec![inner_box],
8890 );
8891
8892 let container = make_styled_view(
8894 Style {
8895 flex_direction: Some(FlexDirection::Column),
8896 height: Some(Dimension::Pt(400.0)),
8897 ..Default::default()
8898 },
8899 vec![grow_child],
8900 );
8901
8902 let doc = Document {
8903 children: vec![Node::page(
8904 PageConfig::default(),
8905 Style::default(),
8906 vec![container],
8907 )],
8908 metadata: Default::default(),
8909 default_page: PageConfig::default(),
8910 first_page: None,
8911 left_page: None,
8912 right_page: None,
8913 named_pages: Default::default(),
8914 attachments: vec![],
8915 zugferd: None,
8916 fonts: vec![],
8917 tagged: false,
8918 pdfa: None,
8919 default_style: None,
8920 embedded_data: None,
8921 flatten_forms: false,
8922 pdf_ua: false,
8923 certification: None,
8924 pdf_version: Default::default(),
8925 pdf_ua2: false,
8926 };
8927
8928 let pages = engine.layout(&doc, &font_context);
8929 let page = &pages[0];
8930
8931 let container_el = page
8933 .elements
8934 .iter()
8935 .find(|e| e.height > 350.0 && e.children.len() == 1)
8936 .expect("Should find outer container");
8937
8938 let grow_el = &container_el.children[0];
8939 assert!(
8940 (grow_el.height - 400.0).abs() < 2.0,
8941 "Grow child should expand to 400, got {}",
8942 grow_el.height
8943 );
8944
8945 let inner_el = &grow_el.children[0];
8947 let expected_center = grow_el.y + grow_el.height / 2.0;
8948 let actual_center = inner_el.y + inner_el.height / 2.0;
8949 assert!(
8950 (actual_center - expected_center).abs() < 2.0,
8951 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
8952 expected_center,
8953 actual_center
8954 );
8955 }
8956
8957 #[test]
8958 fn column_flex_grow_child_justify_content_flex_end() {
8959 let engine = LayoutEngine::new();
8961 let font_context = FontContext::new();
8962
8963 let inner_box = make_styled_view(
8964 Style {
8965 height: Some(Dimension::Pt(30.0)),
8966 ..Default::default()
8967 },
8968 vec![make_text("Bottom", 12.0)],
8969 );
8970
8971 let grow_child = make_styled_view(
8972 Style {
8973 flex_grow: Some(1.0),
8974 flex_direction: Some(FlexDirection::Column),
8975 justify_content: Some(JustifyContent::FlexEnd),
8976 ..Default::default()
8977 },
8978 vec![inner_box],
8979 );
8980
8981 let container = make_styled_view(
8982 Style {
8983 flex_direction: Some(FlexDirection::Column),
8984 height: Some(Dimension::Pt(300.0)),
8985 ..Default::default()
8986 },
8987 vec![grow_child],
8988 );
8989
8990 let doc = Document {
8991 children: vec![Node::page(
8992 PageConfig::default(),
8993 Style::default(),
8994 vec![container],
8995 )],
8996 metadata: Default::default(),
8997 default_page: PageConfig::default(),
8998 first_page: None,
8999 left_page: None,
9000 right_page: None,
9001 named_pages: Default::default(),
9002 attachments: vec![],
9003 zugferd: None,
9004 fonts: vec![],
9005 tagged: false,
9006 pdfa: None,
9007 default_style: None,
9008 embedded_data: None,
9009 flatten_forms: false,
9010 pdf_ua: false,
9011 certification: None,
9012 pdf_version: Default::default(),
9013 pdf_ua2: false,
9014 };
9015
9016 let pages = engine.layout(&doc, &font_context);
9017 let page = &pages[0];
9018
9019 let container_el = page
9020 .elements
9021 .iter()
9022 .find(|e| e.height > 250.0 && e.children.len() == 1)
9023 .expect("Should find outer container");
9024
9025 let grow_el = &container_el.children[0];
9026 let inner_el = &grow_el.children[0];
9027
9028 let inner_bottom = inner_el.y + inner_el.height;
9030 let grow_bottom = grow_el.y + grow_el.height;
9031 assert!(
9032 (inner_bottom - grow_bottom).abs() < 2.0,
9033 "Inner box bottom ({}) should align with grow child bottom ({})",
9034 inner_bottom,
9035 grow_bottom
9036 );
9037 }
9038
9039 #[test]
9040 fn column_flex_grow_child_no_justify_unchanged() {
9041 let engine = LayoutEngine::new();
9043 let font_context = FontContext::new();
9044
9045 let inner_box = make_styled_view(
9046 Style {
9047 height: Some(Dimension::Pt(50.0)),
9048 ..Default::default()
9049 },
9050 vec![make_text("Top", 12.0)],
9051 );
9052
9053 let grow_child = make_styled_view(
9054 Style {
9055 flex_grow: Some(1.0),
9056 flex_direction: Some(FlexDirection::Column),
9057 ..Default::default()
9059 },
9060 vec![inner_box],
9061 );
9062
9063 let container = make_styled_view(
9064 Style {
9065 flex_direction: Some(FlexDirection::Column),
9066 height: Some(Dimension::Pt(300.0)),
9067 ..Default::default()
9068 },
9069 vec![grow_child],
9070 );
9071
9072 let doc = Document {
9073 children: vec![Node::page(
9074 PageConfig::default(),
9075 Style::default(),
9076 vec![container],
9077 )],
9078 metadata: Default::default(),
9079 default_page: PageConfig::default(),
9080 first_page: None,
9081 left_page: None,
9082 right_page: None,
9083 named_pages: Default::default(),
9084 attachments: vec![],
9085 zugferd: None,
9086 fonts: vec![],
9087 tagged: false,
9088 pdfa: None,
9089 default_style: None,
9090 embedded_data: None,
9091 flatten_forms: false,
9092 pdf_ua: false,
9093 certification: None,
9094 pdf_version: Default::default(),
9095 pdf_ua2: false,
9096 };
9097
9098 let pages = engine.layout(&doc, &font_context);
9099 let page = &pages[0];
9100
9101 let container_el = page
9102 .elements
9103 .iter()
9104 .find(|e| e.height > 250.0 && e.children.len() == 1)
9105 .expect("Should find outer container");
9106
9107 let grow_el = &container_el.children[0];
9108 let inner_el = &grow_el.children[0];
9109
9110 assert!(
9112 (inner_el.y - grow_el.y).abs() < 2.0,
9113 "Inner box ({}) should be at top of grow child ({})",
9114 inner_el.y,
9115 grow_el.y
9116 );
9117 }
9118
9119 #[test]
9120 fn column_flex_grow_child_align_items_center() {
9121 let engine = LayoutEngine::new();
9123 let font_context = FontContext::new();
9124
9125 let text = make_text("Hello", 12.0);
9126
9127 let grow_child = make_styled_view(
9128 Style {
9129 flex_grow: Some(1.0),
9130 flex_direction: Some(FlexDirection::Column),
9131 align_items: Some(AlignItems::Center),
9132 ..Default::default()
9133 },
9134 vec![text],
9135 );
9136
9137 let container = make_styled_view(
9138 Style {
9139 flex_direction: Some(FlexDirection::Column),
9140 height: Some(Dimension::Pt(300.0)),
9141 ..Default::default()
9142 },
9143 vec![grow_child],
9144 );
9145
9146 let doc = Document {
9147 children: vec![Node::page(
9148 PageConfig::default(),
9149 Style::default(),
9150 vec![container],
9151 )],
9152 metadata: Default::default(),
9153 default_page: PageConfig::default(),
9154 first_page: None,
9155 left_page: None,
9156 right_page: None,
9157 named_pages: Default::default(),
9158 attachments: vec![],
9159 zugferd: None,
9160 fonts: vec![],
9161 tagged: false,
9162 pdfa: None,
9163 default_style: None,
9164 embedded_data: None,
9165 flatten_forms: false,
9166 pdf_ua: false,
9167 certification: None,
9168 pdf_version: Default::default(),
9169 pdf_ua2: false,
9170 };
9171
9172 let pages = engine.layout(&doc, &font_context);
9173 let page = &pages[0];
9174
9175 let container_el = page
9176 .elements
9177 .iter()
9178 .find(|e| e.height > 250.0 && e.children.len() == 1)
9179 .expect("Should find outer container");
9180
9181 let grow_el = &container_el.children[0];
9182 assert!(
9183 !grow_el.children.is_empty(),
9184 "Grow child should have text child"
9185 );
9186
9187 let text_el = &grow_el.children[0];
9188 let text_center = text_el.x + text_el.width / 2.0;
9189 let grow_center = grow_el.x + grow_el.width / 2.0;
9190 assert!(
9191 (text_center - grow_center).abs() < 2.0,
9192 "Text center ({}) should be near grow child center ({})",
9193 text_center,
9194 grow_center
9195 );
9196 }
9197
9198 #[test]
9199 fn image_intrinsic_width_respects_height_constraint() {
9200 let engine = LayoutEngine::new();
9204 let font_context = FontContext::new();
9205
9206 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9208
9209 let image_node = Node {
9210 kind: NodeKind::Image {
9211 src: one_px_png.to_string(),
9212 width: None,
9213 height: Some(36.0),
9214 },
9215 style: Style::default(),
9216 children: vec![],
9217 id: None,
9218 source_location: None,
9219 bookmark: None,
9220 href: None,
9221 alt: None,
9222 };
9223
9224 let resolved = image_node.style.resolve(None, 0.0);
9225 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9226
9227 assert!(
9229 (intrinsic - 36.0).abs() < 1.0,
9230 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9231 intrinsic
9232 );
9233 }
9234}