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 col_span: u32,
457 pub overflow: Overflow,
459 pub opacity: f64,
464}
465
466fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
469 match &node.kind {
470 NodeKind::Fixed {
471 pages,
472 page_name: only,
473 exclude_page_names,
474 ..
475 } => {
476 pages.applies(page_index)
477 && match only {
478 Some(n) => page_name == Some(n.as_str()),
479 None => true,
480 }
481 && !exclude_page_names
482 .iter()
483 .any(|n| page_name == Some(n.as_str()))
484 }
485 _ => true,
486 }
487}
488
489fn node_kind_name(kind: &NodeKind) -> &'static str {
491 match kind {
492 NodeKind::View => "View",
493 NodeKind::Text { .. } => "Text",
494 NodeKind::Heading { level: 1, .. } => "H1",
495 NodeKind::Heading { level: 2, .. } => "H2",
496 NodeKind::Heading { level: 3, .. } => "H3",
497 NodeKind::Heading { level: 4, .. } => "H4",
498 NodeKind::Heading { level: 5, .. } => "H5",
499 NodeKind::Heading { .. } => "H6",
503 NodeKind::List { .. } => "List",
504 NodeKind::ListItem => "ListItem",
505 NodeKind::Image { .. } => "Image",
506 NodeKind::Table { .. } => "Table",
507 NodeKind::TableRow { .. } => "TableRow",
508 NodeKind::TableCell { .. } => "TableCell",
509 NodeKind::Fixed {
510 position: FixedPosition::Header,
511 ..
512 } => "FixedHeader",
513 NodeKind::Fixed {
514 position: FixedPosition::Footer,
515 ..
516 } => "FixedFooter",
517 NodeKind::Page { .. } => "Page",
518 NodeKind::PageBreak => "PageBreak",
519 NodeKind::PageName { .. } => "PageName",
520 NodeKind::Svg { .. } => "Svg",
521 NodeKind::Canvas { .. } => "Canvas",
522 NodeKind::Barcode { .. } => "Barcode",
523 NodeKind::QrCode { .. } => "QrCode",
524 NodeKind::BarChart { .. } => "BarChart",
525 NodeKind::LineChart { .. } => "LineChart",
526 NodeKind::PieChart { .. } => "PieChart",
527 NodeKind::AreaChart { .. } => "AreaChart",
528 NodeKind::DotPlot { .. } => "DotPlot",
529 NodeKind::Watermark { .. } => "Watermark",
530 NodeKind::TextField { .. } => "TextField",
531 NodeKind::Checkbox { .. } => "Checkbox",
532 NodeKind::Dropdown { .. } => "Dropdown",
533 NodeKind::RadioButton { .. } => "RadioButton",
534 }
535}
536
537fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
556 node.bookmark.as_ref().map(|title| LayoutElement {
557 x,
558 y,
559 width: 0.0,
560 height: 0.0,
561 draw: DrawCommand::None,
562 children: vec![],
563 node_type: Some("Bookmark".to_string()),
564 resolved_style: None,
565 source_location: None,
566 href: None,
567 bookmark: Some(title.clone()),
568 alt: None,
569 is_header_row: false,
570 col_span: 1,
571 overflow: Overflow::default(),
572 opacity: 1.0,
573 })
574}
575
576fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
583 if !ordered {
584 return match marker_type {
588 ListMarkerType::None => String::new(),
589 _ => "•".to_string(),
590 };
591 }
592 let body = match marker_type {
593 ListMarkerType::LowerAlpha => to_alpha(idx, false),
594 ListMarkerType::UpperAlpha => to_alpha(idx, true),
595 ListMarkerType::LowerRoman => to_roman(idx, false),
596 ListMarkerType::UpperRoman => to_roman(idx, true),
597 ListMarkerType::None => return String::new(),
598 _ => idx.to_string(),
601 };
602 format!("{body}.")
603}
604
605fn to_alpha(mut n: u32, upper: bool) -> String {
608 if n == 0 {
609 return String::new();
610 }
611 let base = if upper { b'A' } else { b'a' };
612 let mut bytes: Vec<u8> = Vec::new();
613 while n > 0 {
614 let rem = ((n - 1) % 26) as u8;
615 bytes.push(base + rem);
616 n = (n - 1) / 26;
617 }
618 bytes.reverse();
619 String::from_utf8(bytes).unwrap_or_default()
620}
621
622fn to_roman(n: u32, upper: bool) -> String {
626 if n == 0 || n > 3999 {
627 return n.to_string();
628 }
629 const UPPER: &[(&str, u32)] = &[
630 ("M", 1000),
631 ("CM", 900),
632 ("D", 500),
633 ("CD", 400),
634 ("C", 100),
635 ("XC", 90),
636 ("L", 50),
637 ("XL", 40),
638 ("X", 10),
639 ("IX", 9),
640 ("V", 5),
641 ("IV", 4),
642 ("I", 1),
643 ];
644 const LOWER: &[(&str, u32)] = &[
645 ("m", 1000),
646 ("cm", 900),
647 ("d", 500),
648 ("cd", 400),
649 ("c", 100),
650 ("xc", 90),
651 ("l", 50),
652 ("xl", 40),
653 ("x", 10),
654 ("ix", 9),
655 ("v", 5),
656 ("iv", 4),
657 ("i", 1),
658 ];
659 let table = if upper { UPPER } else { LOWER };
660 let mut out = String::new();
661 let mut remaining = n;
662 for &(sym, val) in table {
663 while remaining >= val {
664 out.push_str(sym);
665 remaining -= val;
666 }
667 }
668 out
669}
670
671fn compute_marker_gutter_width(
678 ordered: bool,
679 marker_type: ListMarkerType,
680 start: u32,
681 n_items: u32,
682 style: &ResolvedStyle,
683) -> f64 {
684 if matches!(marker_type, ListMarkerType::None) {
685 return 0.0;
686 }
687 let approx_char_w = style.font_size * 0.6;
689 let gap = 6.0_f64;
690 if !ordered {
691 return approx_char_w + gap;
693 }
694 let last_idx = start + n_items.saturating_sub(1);
696 let widest = match marker_type {
697 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
698 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
699 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
700 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
701 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
702 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
703 };
704 widest.chars().count() as f64 * approx_char_w + gap
705}
706
707#[derive(Debug, Clone)]
709pub enum FormFieldType {
710 TextField {
711 value: Option<String>,
712 placeholder: Option<String>,
713 multiline: bool,
714 password: bool,
715 read_only: bool,
716 max_length: Option<u32>,
717 font_size: f64,
718 },
719 Checkbox {
720 checked: bool,
721 read_only: bool,
722 },
723 Dropdown {
724 options: Vec<String>,
725 value: Option<String>,
726 read_only: bool,
727 font_size: f64,
728 },
729 RadioButton {
730 value: String,
731 checked: bool,
732 read_only: bool,
733 },
734}
735
736#[derive(Debug, Clone)]
738pub enum DrawCommand {
739 None,
741 Rect {
743 background: Option<Color>,
744 border_width: Edges,
745 border_color: EdgeValues<Color>,
746 border_style: EdgeValues<crate::style::BorderStyle>,
747 border_radius: CornerValues,
748 opacity: f64,
749 box_shadow: Option<Box<crate::style::BoxShadow>>,
752 background_gradient: Option<Box<crate::style::Background>>,
756 },
757 Text {
759 lines: Vec<TextLine>,
760 color: Color,
761 text_decoration: TextDecoration,
762 opacity: f64,
763 },
764 Image {
766 image_data: crate::image_loader::LoadedImage,
767 },
768 ImagePlaceholder,
770 Svg {
772 commands: Vec<crate::svg::SvgCommand>,
773 width: f64,
775 height: f64,
777 viewbox_min_x: f64,
781 viewbox_min_y: f64,
782 viewbox_width: f64,
783 viewbox_height: f64,
784 clip: bool,
786 },
787 Barcode {
789 bars: Vec<u8>,
790 bar_width: f64,
791 height: f64,
792 color: Color,
793 },
794 QrCode {
796 modules: Vec<Vec<bool>>,
797 module_size: f64,
798 color: Color,
799 },
800 Chart {
802 primitives: Vec<crate::chart::ChartPrimitive>,
803 },
804 Watermark {
806 lines: Vec<TextLine>,
807 color: Color,
808 opacity: f64,
809 angle_rad: f64,
810 font_family: String,
812 },
813 FormField {
815 field_type: FormFieldType,
816 name: String,
817 },
818}
819
820#[derive(Debug, Clone)]
821pub struct TextLine {
822 pub x: f64,
823 pub y: f64,
824 pub glyphs: Vec<PositionedGlyph>,
825 pub width: f64,
826 pub height: f64,
827 pub word_spacing: f64,
829}
830
831#[derive(Debug, Clone)]
832pub struct PositionedGlyph {
833 pub glyph_id: u16,
836 pub x_offset: f64,
838 pub y_offset: f64,
840 pub x_advance: f64,
842 pub font_size: f64,
843 pub font_family: Arc<str>,
849 pub font_weight: u32,
850 pub font_style: FontStyle,
851 pub char_value: char,
853 pub color: Option<Color>,
855 pub href: Option<String>,
857 pub text_decoration: TextDecoration,
859 pub letter_spacing: f64,
861 pub cluster_text: Option<String>,
864}
865
866fn offset_element_y(el: &mut LayoutElement, dy: f64) {
869 el.y += dy;
870 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
871 for line in lines.iter_mut() {
872 line.y += dy;
873 }
874 }
875 for child in &mut el.children {
876 offset_element_y(child, dy);
877 }
878}
879
880fn offset_element_x(el: &mut LayoutElement, dx: f64) {
882 el.x += dx;
883 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
884 for line in lines.iter_mut() {
885 line.x += dx;
886 }
887 }
888 for child in &mut el.children {
889 offset_element_x(child, dx);
890 }
891}
892
893fn reapply_justify_content(elem: &mut LayoutElement) {
897 let style = match elem.resolved_style {
898 Some(ref s) => s,
899 None => return,
900 };
901 if matches!(style.justify_content, JustifyContent::FlexStart) {
902 return;
903 }
904 if elem.children.is_empty() {
905 return;
906 }
907
908 let padding_top = style.padding.top + style.border_width.top;
909 let padding_bottom = style.padding.bottom + style.border_width.bottom;
910 let inner_h = elem.height - padding_top - padding_bottom;
911 let content_top = elem.y + padding_top;
912
913 let last_child = &elem.children[elem.children.len() - 1];
915 let children_bottom = last_child.y + last_child.height;
916 let children_span = children_bottom - content_top;
917 let slack = inner_h - children_span;
918 if slack < 0.001 {
919 return;
920 }
921
922 let n = elem.children.len();
923 let offsets: Vec<f64> = match style.justify_content {
924 JustifyContent::FlexEnd => vec![slack; n],
925 JustifyContent::Center => vec![slack / 2.0; n],
926 JustifyContent::SpaceBetween => {
927 if n <= 1 {
928 vec![0.0; n]
929 } else {
930 let per_gap = slack / (n - 1) as f64;
931 (0..n).map(|i| i as f64 * per_gap).collect()
932 }
933 }
934 JustifyContent::SpaceAround => {
935 let space = slack / n as f64;
936 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
937 }
938 JustifyContent::SpaceEvenly => {
939 let space = slack / (n + 1) as f64;
940 (0..n).map(|i| (i + 1) as f64 * space).collect()
941 }
942 JustifyContent::FlexStart => unreachable!(),
943 };
944
945 for (i, child) in elem.children.iter_mut().enumerate() {
946 let dy = offsets[i];
947 if dy.abs() > 0.001 {
948 offset_element_y(child, dy);
949 }
950 }
951}
952
953fn apply_text_transform(text: &str, transform: TextTransform) -> String {
955 match transform {
956 TextTransform::None => text.to_string(),
957 TextTransform::Uppercase => text.to_uppercase(),
958 TextTransform::Lowercase => text.to_lowercase(),
959 TextTransform::Capitalize => {
960 let mut result = String::with_capacity(text.len());
961 let mut prev_is_whitespace = true;
962 for ch in text.chars() {
963 if prev_is_whitespace && ch.is_alphabetic() {
964 for upper in ch.to_uppercase() {
965 result.push(upper);
966 }
967 } else {
968 result.push(ch);
969 }
970 prev_is_whitespace = ch.is_whitespace();
971 }
972 result
973 }
974 }
975}
976
977pub const PAGE_NUMBER_SENTINEL: char = '\x02';
981
982pub const TOTAL_PAGES_SENTINEL: char = '\x03';
984
985fn substitute_page_placeholders(text: &str) -> String {
990 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
991 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
992 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
993 } else {
994 text.to_string()
995 }
996}
997
998fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1001 match transform {
1002 TextTransform::None => ch,
1003 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1004 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1005 TextTransform::Capitalize => {
1006 if is_word_start && ch.is_alphabetic() {
1007 ch.to_uppercase().next().unwrap_or(ch)
1008 } else {
1009 ch
1010 }
1011 }
1012 }
1013}
1014
1015pub struct LayoutEngine {
1017 text_layout: TextLayout,
1018 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1019 warnings: RefCell<Vec<String>>,
1031 measure_check: bool,
1045}
1046
1047#[derive(Debug, Clone)]
1049struct PageCursor {
1050 config: PageConfig,
1051 base_config: PageConfig,
1054 page_index: usize,
1059 content_width: f64,
1060 content_height: f64,
1061 y: f64,
1062 elements: Vec<LayoutElement>,
1063 fixed_header: Vec<(Node, f64)>,
1064 fixed_footer: Vec<(Node, f64)>,
1065 watermarks: Vec<Node>,
1067 content_x: f64,
1068 content_y: f64,
1069 continuation_top_offset: f64,
1071 containing_block: (f64, f64, f64, f64),
1076 left_config: Option<PageConfig>,
1084 right_config: Option<PageConfig>,
1085 display_config: Option<PageConfig>,
1088 flow_anchor_left: f64,
1093 page_name: Option<String>,
1097 base_display: Option<PageConfig>,
1101 first_display: Option<PageConfig>,
1105 doc_base: PageConfig,
1108 doc_left: Option<PageConfig>,
1109 doc_right: Option<PageConfig>,
1110 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1112}
1113
1114impl PageCursor {
1115 fn new(config: &PageConfig) -> Self {
1116 let (page_w, page_h) = config.size.dimensions();
1117 let content_width = page_w - config.margin.horizontal();
1118 let content_height = page_h - config.margin.vertical();
1119
1120 Self {
1121 config: config.clone(),
1122 base_config: config.clone(),
1123 page_index: 0,
1124 content_width,
1125 content_height,
1126 y: 0.0,
1127 elements: Vec::new(),
1128 fixed_header: Vec::new(),
1129 fixed_footer: Vec::new(),
1130 watermarks: Vec::new(),
1131 content_x: config.margin.left,
1132 content_y: config.margin.top,
1133 continuation_top_offset: 0.0,
1134 containing_block: (
1135 config.margin.left,
1136 config.margin.top,
1137 content_width,
1138 content_height,
1139 ),
1140 left_config: None,
1141 right_config: None,
1142 display_config: None,
1143 flow_anchor_left: config.margin.left,
1144 page_name: None,
1145 base_display: None,
1146 first_display: None,
1147 doc_base: config.clone(),
1148 doc_left: None,
1149 doc_right: None,
1150 named_sets: std::collections::HashMap::new(),
1151 }
1152 }
1153
1154 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1161 if page_number == 1 {
1162 if let Some(first) = &self.first_display {
1163 return Some(first.clone());
1164 }
1165 }
1166 let parity = if page_number % 2 == 1 {
1167 self.right_config.clone()
1168 } else {
1169 self.left_config.clone()
1170 };
1171 parity.or_else(|| self.base_display.clone())
1172 }
1173
1174 fn fixed_applies(&self, node: &Node) -> bool {
1177 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1178 }
1179
1180 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1187 let set = name
1188 .as_deref()
1189 .and_then(|n| self.named_sets.get(n))
1190 .cloned();
1191 let real = set
1192 .as_ref()
1193 .map(|s| s.base.clone())
1194 .unwrap_or_else(|| self.doc_base.clone());
1195 let mut cursor = PageCursor::new(&real);
1196 cursor.page_index = if in_place {
1197 self.page_index
1198 } else {
1199 self.page_index + 1
1200 };
1201 cursor.page_name = name;
1202 cursor.doc_base = self.doc_base.clone();
1203 cursor.doc_left = self.doc_left.clone();
1204 cursor.doc_right = self.doc_right.clone();
1205 cursor.named_sets = self.named_sets.clone();
1206 match &set {
1207 Some(s) => {
1208 cursor.left_config = s.display_left.clone();
1209 cursor.right_config = s.display_right.clone();
1210 cursor.base_display = s.display.clone();
1211 cursor.first_display = s.display_first.clone();
1212 }
1213 None => {
1214 cursor.left_config = self.doc_left.clone();
1218 cursor.right_config = self.doc_right.clone();
1219 }
1220 }
1221 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1222 cursor.flow_anchor_left = self.flow_anchor_left;
1223 cursor.fixed_header = self.fixed_header.clone();
1224 cursor.fixed_footer = self.fixed_footer.clone();
1225 cursor.watermarks = self.watermarks.clone();
1226 cursor.continuation_top_offset = self.continuation_top_offset;
1227
1228 let header_height: f64 = cursor
1229 .fixed_header
1230 .iter()
1231 .filter(|(n, _)| cursor.fixed_applies(n))
1232 .map(|(_, h)| *h)
1233 .sum();
1234 cursor.y = header_height + cursor.continuation_top_offset;
1235 cursor
1236 }
1237
1238 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1241 let mut cursor = PageCursor::new(first);
1242 cursor.base_config = base.clone();
1243 cursor
1244 }
1245
1246 fn remaining_height(&self) -> f64 {
1247 let footer_height: f64 = self
1248 .fixed_footer
1249 .iter()
1250 .filter(|(n, _)| self.fixed_applies(n))
1251 .map(|(_, h)| *h)
1252 .sum();
1253 (self.content_height - self.y - footer_height).max(0.0)
1254 }
1255
1256 fn finalize(&self) -> LayoutPage {
1257 let (page_w, page_h) = self.config.size.dimensions();
1258
1259 let (elements, config) = match &self.display_config {
1265 Some(display) => {
1266 let dx = display.margin.left - self.flow_anchor_left;
1267 let mut elements = self.elements.clone();
1268 if dx != 0.0 {
1269 fn shift(els: &mut [LayoutElement], dx: f64) {
1270 for el in els {
1271 el.x += dx;
1272 shift(&mut el.children, dx);
1273 }
1274 }
1275 shift(&mut elements, dx);
1276 }
1277 (elements, display.clone())
1278 }
1279 None => (self.elements.clone(), self.config.clone()),
1280 };
1281
1282 LayoutPage {
1283 width: page_w,
1284 height: page_h,
1285 elements,
1286 fixed_header: self.fixed_header.clone(),
1287 fixed_footer: self.fixed_footer.clone(),
1288 watermarks: self.watermarks.clone(),
1289 config,
1290 page_name: self.page_name.clone(),
1291 }
1292 }
1293
1294 fn new_page(&self) -> Self {
1295 let mut cursor = PageCursor::new(&self.base_config);
1298 cursor.page_index = self.page_index + 1;
1299 cursor.page_name = self.page_name.clone();
1300 cursor.left_config = self.left_config.clone();
1301 cursor.right_config = self.right_config.clone();
1302 cursor.base_display = self.base_display.clone();
1303 cursor.first_display = self.first_display.clone();
1304 cursor.doc_base = self.doc_base.clone();
1305 cursor.doc_left = self.doc_left.clone();
1306 cursor.doc_right = self.doc_right.clone();
1307 cursor.named_sets = self.named_sets.clone();
1308 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1309 cursor.flow_anchor_left = self.flow_anchor_left;
1310 cursor.fixed_header = self.fixed_header.clone();
1311 cursor.fixed_footer = self.fixed_footer.clone();
1312 cursor.watermarks = self.watermarks.clone();
1313 cursor.continuation_top_offset = self.continuation_top_offset;
1314
1315 let header_height: f64 = cursor
1316 .fixed_header
1317 .iter()
1318 .filter(|(n, _)| cursor.fixed_applies(n))
1319 .map(|(_, h)| *h)
1320 .sum();
1321 cursor.y = header_height + cursor.continuation_top_offset;
1322
1323 cursor
1324 }
1325}
1326
1327impl Default for LayoutEngine {
1328 fn default() -> Self {
1329 Self::new()
1330 }
1331}
1332
1333impl LayoutEngine {
1334 pub fn new() -> Self {
1335 Self {
1336 text_layout: TextLayout::new(),
1337 image_dim_cache: RefCell::new(HashMap::new()),
1338 warnings: RefCell::new(Vec::new()),
1339 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1340 }
1341 }
1342
1343 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1345 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1346 return Some(*dims);
1347 }
1348 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1349 self.image_dim_cache
1350 .borrow_mut()
1351 .insert(src.to_string(), dims);
1352 Some(dims)
1353 } else {
1354 None
1355 }
1356 }
1357
1358 fn defect(&self, msg: String) {
1361 let mut w = self.warnings.borrow_mut();
1362 if !w.contains(&msg) {
1365 w.push(msg);
1366 }
1367 }
1368
1369 pub fn take_warnings(&self) -> Vec<String> {
1373 std::mem::take(&mut *self.warnings.borrow_mut())
1374 }
1375
1376 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1378 self.warnings.borrow_mut().clear();
1379 let mut pages: Vec<LayoutPage> = Vec::new();
1380 let mut cursor = match &document.first_page {
1381 Some(first) => PageCursor::new_first(first, &document.default_page),
1382 None => PageCursor::new(&document.default_page),
1383 };
1384 cursor.left_config = document.left_page.clone();
1390 cursor.right_config = document.right_page.clone();
1391 cursor.doc_base = document.default_page.clone();
1393 cursor.doc_left = document.left_page.clone();
1394 cursor.doc_right = document.right_page.clone();
1395 cursor.named_sets = document.named_pages.clone();
1396 if document.first_page.is_none() {
1397 cursor.display_config = cursor.display_for(1);
1398 }
1399
1400 let base = document.default_style.clone().unwrap_or_default();
1402 let root_style = Style {
1403 lang: base.lang.clone().or(document.metadata.lang.clone()),
1404 ..base
1405 }
1406 .resolve(None, cursor.content_width);
1407
1408 for node in &document.children {
1409 match &node.kind {
1410 NodeKind::Page { config } => {
1411 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1412 pages.push(cursor.finalize());
1413 }
1414 cursor = PageCursor::new(config);
1415 cursor.page_index = pages.len();
1416
1417 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1424 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1425
1426 let cx = cursor.content_x;
1427 let cw = cursor.content_width;
1428 self.layout_children(
1429 &node.children,
1430 &node.style,
1431 &mut cursor,
1432 &mut pages,
1433 cx,
1434 cw,
1435 Some(&page_root),
1436 font_context,
1437 );
1438 }
1439 NodeKind::PageBreak => {
1440 pages.push(cursor.finalize());
1441 cursor = cursor.new_page();
1442 }
1443 _ => {
1444 let cx = cursor.content_x;
1445 let cw = cursor.content_width;
1446 self.layout_node(
1447 node,
1448 &mut cursor,
1449 &mut pages,
1450 cx,
1451 cw,
1452 Some(&root_style),
1453 font_context,
1454 None,
1455 None,
1456 );
1457 }
1458 }
1459 }
1460
1461 if !cursor.elements.is_empty() {
1467 pages.push(cursor.finalize());
1468 }
1469
1470 fn has_visible_ink(els: &[LayoutElement]) -> bool {
1477 els.iter().any(|e| {
1478 let paints = match &e.draw {
1479 DrawCommand::None => false,
1480 DrawCommand::Rect {
1484 background,
1485 border_width,
1486 ..
1487 } => {
1488 background.is_some()
1489 || border_width.top > 0.0
1490 || border_width.right > 0.0
1491 || border_width.bottom > 0.0
1492 || border_width.left > 0.0
1493 }
1494 _ => true,
1495 };
1496 paints || has_visible_ink(&e.children)
1497 })
1498 }
1499 while pages.len() > 1 && !has_visible_ink(&pages.last().unwrap().elements) {
1500 pages.pop();
1501 }
1502
1503 self.inject_fixed_elements(&mut pages, font_context);
1504
1505 pages
1506 }
1507
1508 #[allow(clippy::too_many_arguments)]
1509 fn layout_node(
1510 &self,
1511 node: &Node,
1512 cursor: &mut PageCursor,
1513 pages: &mut Vec<LayoutPage>,
1514 x: f64,
1515 available_width: f64,
1516 parent_style: Option<&ResolvedStyle>,
1517 font_context: &FontContext,
1518 cross_axis_height: Option<f64>,
1519 forced_outer_width: Option<f64>,
1520 ) {
1521 let mut style = node.style.resolve(parent_style, available_width);
1522
1523 if let Some(h) = cross_axis_height {
1526 if matches!(style.height, SizeConstraint::Auto) {
1527 style.height = SizeConstraint::Fixed(h);
1528 }
1529 }
1530
1531 if let Some(w) = forced_outer_width {
1536 style.width = SizeConstraint::Fixed(w);
1537 }
1538
1539 if style.break_before && !cursor.elements.is_empty() {
1550 pages.push(cursor.finalize());
1551 *cursor = cursor.new_page();
1552 }
1553
1554 let elem_start = cursor.elements.len();
1557
1558 match &node.kind {
1559 NodeKind::PageBreak => {
1560 pages.push(cursor.finalize());
1561 *cursor = cursor.new_page();
1562 }
1563
1564 NodeKind::PageName { name } => {
1565 if cursor.page_name.as_deref() != name.as_deref() {
1571 let has_content = !cursor.elements.is_empty();
1578 if has_content {
1579 pages.push(cursor.finalize());
1580 }
1581 *cursor = cursor.renamed_page(name.clone(), !has_content);
1582 }
1583 }
1584
1585 NodeKind::Fixed {
1586 position, pages, ..
1587 } => {
1588 let height = self.measure_node_height(node, available_width, &style, font_context);
1589 match position {
1590 FixedPosition::Header => {
1591 cursor.fixed_header.push((node.clone(), height));
1592 let _ = pages;
1596 if cursor.fixed_applies(node) {
1597 cursor.y += height;
1598 }
1599 }
1600 FixedPosition::Footer => {
1601 cursor.fixed_footer.push((node.clone(), height));
1602 }
1603 }
1604 }
1605
1606 NodeKind::Watermark { .. } => {
1607 cursor.watermarks.push(node.clone());
1609 }
1610
1611 NodeKind::TextField {
1612 name,
1613 value,
1614 placeholder,
1615 width: field_w,
1616 height: field_h,
1617 multiline,
1618 password,
1619 read_only,
1620 max_length,
1621 font_size,
1622 } => {
1623 self.layout_form_field(
1624 node,
1625 &style,
1626 cursor,
1627 pages,
1628 x,
1629 *field_w,
1630 *field_h,
1631 DrawCommand::FormField {
1632 field_type: FormFieldType::TextField {
1633 value: value.clone(),
1634 placeholder: placeholder.clone(),
1635 multiline: *multiline,
1636 password: *password,
1637 read_only: *read_only,
1638 max_length: *max_length,
1639 font_size: *font_size,
1640 },
1641 name: name.clone(),
1642 },
1643 "TextField",
1644 );
1645 }
1646
1647 NodeKind::Checkbox {
1648 name,
1649 checked,
1650 width: field_w,
1651 height: field_h,
1652 read_only,
1653 } => {
1654 self.layout_form_field(
1655 node,
1656 &style,
1657 cursor,
1658 pages,
1659 x,
1660 *field_w,
1661 *field_h,
1662 DrawCommand::FormField {
1663 field_type: FormFieldType::Checkbox {
1664 checked: *checked,
1665 read_only: *read_only,
1666 },
1667 name: name.clone(),
1668 },
1669 "Checkbox",
1670 );
1671 }
1672
1673 NodeKind::Dropdown {
1674 name,
1675 options,
1676 value,
1677 width: field_w,
1678 height: field_h,
1679 read_only,
1680 font_size,
1681 } => {
1682 self.layout_form_field(
1683 node,
1684 &style,
1685 cursor,
1686 pages,
1687 x,
1688 *field_w,
1689 *field_h,
1690 DrawCommand::FormField {
1691 field_type: FormFieldType::Dropdown {
1692 options: options.clone(),
1693 value: value.clone(),
1694 read_only: *read_only,
1695 font_size: *font_size,
1696 },
1697 name: name.clone(),
1698 },
1699 "Dropdown",
1700 );
1701 }
1702
1703 NodeKind::RadioButton {
1704 name,
1705 value,
1706 checked,
1707 width: field_w,
1708 height: field_h,
1709 read_only,
1710 } => {
1711 self.layout_form_field(
1712 node,
1713 &style,
1714 cursor,
1715 pages,
1716 x,
1717 *field_w,
1718 *field_h,
1719 DrawCommand::FormField {
1720 field_type: FormFieldType::RadioButton {
1721 value: value.clone(),
1722 checked: *checked,
1723 read_only: *read_only,
1724 },
1725 name: name.clone(),
1726 },
1727 "RadioButton",
1728 );
1729 }
1730
1731 NodeKind::Text {
1732 content,
1733 href,
1734 runs,
1735 } => {
1736 self.layout_text(
1737 content,
1738 href.as_deref(),
1739 runs,
1740 &style,
1741 cursor,
1742 pages,
1743 x,
1744 available_width,
1745 font_context,
1746 node.source_location.as_ref(),
1747 node.bookmark.as_deref(),
1748 None,
1749 );
1750 }
1751
1752 NodeKind::Heading {
1753 content,
1754 href,
1755 runs,
1756 ..
1757 } => {
1758 let heading_role = node_kind_name(&node.kind); self.layout_text(
1764 content,
1765 href.as_deref(),
1766 runs,
1767 &style,
1768 cursor,
1769 pages,
1770 x,
1771 available_width,
1772 font_context,
1773 node.source_location.as_ref(),
1774 node.bookmark.as_deref(),
1775 Some(heading_role),
1776 );
1777 }
1778
1779 NodeKind::List {
1780 ordered,
1781 marker_type,
1782 start,
1783 } => {
1784 self.layout_list(
1785 node,
1786 *ordered,
1787 *marker_type,
1788 *start,
1789 &style,
1790 cursor,
1791 pages,
1792 x,
1793 available_width,
1794 font_context,
1795 );
1796 }
1797
1798 NodeKind::ListItem => {
1799 self.layout_view(
1804 node,
1805 &style,
1806 cursor,
1807 pages,
1808 x,
1809 available_width,
1810 font_context,
1811 );
1812 }
1813
1814 NodeKind::Image { width, height, .. } => {
1815 self.layout_image(
1816 node,
1817 &style,
1818 cursor,
1819 pages,
1820 x,
1821 available_width,
1822 *width,
1823 *height,
1824 );
1825 }
1826
1827 NodeKind::Table { columns } => {
1828 self.layout_table(
1829 node,
1830 &style,
1831 columns,
1832 cursor,
1833 pages,
1834 x,
1835 available_width,
1836 font_context,
1837 );
1838 }
1839
1840 NodeKind::View | NodeKind::Page { .. } => {
1841 self.layout_view(
1842 node,
1843 &style,
1844 cursor,
1845 pages,
1846 x,
1847 available_width,
1848 font_context,
1849 );
1850 }
1851
1852 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
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::Svg {
1865 width: svg_w,
1866 height: svg_h,
1867 view_box,
1868 content,
1869 } => {
1870 self.layout_svg(
1871 node,
1872 &style,
1873 cursor,
1874 pages,
1875 x,
1876 available_width,
1877 *svg_w,
1878 *svg_h,
1879 view_box.as_deref(),
1880 content,
1881 );
1882 }
1883
1884 NodeKind::Barcode {
1885 data,
1886 format,
1887 width: explicit_width,
1888 height: bar_height,
1889 } => {
1890 self.layout_barcode(
1891 node,
1892 &style,
1893 cursor,
1894 pages,
1895 x,
1896 available_width,
1897 data,
1898 *format,
1899 *explicit_width,
1900 *bar_height,
1901 );
1902 }
1903
1904 NodeKind::QrCode {
1905 data,
1906 size: explicit_size,
1907 } => {
1908 self.layout_qrcode(
1909 node,
1910 &style,
1911 cursor,
1912 pages,
1913 x,
1914 available_width,
1915 data,
1916 *explicit_size,
1917 );
1918 }
1919
1920 NodeKind::Canvas {
1921 width: canvas_w,
1922 height: canvas_h,
1923 operations,
1924 } => {
1925 self.layout_canvas(
1926 node,
1927 &style,
1928 cursor,
1929 pages,
1930 x,
1931 available_width,
1932 *canvas_w,
1933 *canvas_h,
1934 operations,
1935 );
1936 }
1937
1938 NodeKind::BarChart {
1939 data,
1940 width: chart_w,
1941 height: chart_h,
1942 color,
1943 show_labels,
1944 show_values,
1945 show_grid,
1946 title,
1947 } => {
1948 let config = crate::chart::bar::BarChartConfig {
1949 color: color.clone(),
1950 show_labels: *show_labels,
1951 show_values: *show_values,
1952 show_grid: *show_grid,
1953 title: title.clone(),
1954 };
1955 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1956 self.layout_chart(
1957 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1958 );
1959 }
1960
1961 NodeKind::LineChart {
1962 series,
1963 labels,
1964 width: chart_w,
1965 height: chart_h,
1966 show_points,
1967 show_grid,
1968 title,
1969 } => {
1970 let config = crate::chart::line::LineChartConfig {
1971 show_points: *show_points,
1972 show_grid: *show_grid,
1973 title: title.clone(),
1974 };
1975 let primitives =
1976 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1977 self.layout_chart(
1978 node,
1979 &style,
1980 cursor,
1981 pages,
1982 x,
1983 *chart_w,
1984 *chart_h,
1985 primitives,
1986 "LineChart",
1987 );
1988 }
1989
1990 NodeKind::PieChart {
1991 data,
1992 width: chart_w,
1993 height: chart_h,
1994 donut,
1995 show_legend,
1996 title,
1997 } => {
1998 let config = crate::chart::pie::PieChartConfig {
1999 donut: *donut,
2000 show_legend: *show_legend,
2001 title: title.clone(),
2002 };
2003 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
2004 self.layout_chart(
2005 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
2006 );
2007 }
2008
2009 NodeKind::AreaChart {
2010 series,
2011 labels,
2012 width: chart_w,
2013 height: chart_h,
2014 show_grid,
2015 title,
2016 } => {
2017 let config = crate::chart::area::AreaChartConfig {
2018 show_grid: *show_grid,
2019 title: title.clone(),
2020 };
2021 let primitives =
2022 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
2023 self.layout_chart(
2024 node,
2025 &style,
2026 cursor,
2027 pages,
2028 x,
2029 *chart_w,
2030 *chart_h,
2031 primitives,
2032 "AreaChart",
2033 );
2034 }
2035
2036 NodeKind::DotPlot {
2037 groups,
2038 width: chart_w,
2039 height: chart_h,
2040 x_min,
2041 x_max,
2042 y_min,
2043 y_max,
2044 x_label,
2045 y_label,
2046 show_legend,
2047 dot_size,
2048 } => {
2049 let config = crate::chart::dot::DotPlotConfig {
2050 x_min: *x_min,
2051 x_max: *x_max,
2052 y_min: *y_min,
2053 y_max: *y_max,
2054 x_label: x_label.clone(),
2055 y_label: y_label.clone(),
2056 show_legend: *show_legend,
2057 dot_size: *dot_size,
2058 };
2059 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2060 self.layout_chart(
2061 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2062 );
2063 }
2064 }
2065
2066 if matches!(style.position, Position::Relative)
2074 && (style.top.is_some()
2075 || style.left.is_some()
2076 || style.right.is_some()
2077 || style.bottom.is_some())
2078 {
2079 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2080 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2081 for el in &mut cursor.elements[elem_start..] {
2082 if dx != 0.0 {
2083 offset_element_x(el, dx);
2084 }
2085 if dy != 0.0 {
2086 offset_element_y(el, dy);
2087 }
2088 }
2089 }
2090 }
2091
2092 #[allow(clippy::too_many_arguments)]
2093 fn layout_view(
2094 &self,
2095 node: &Node,
2096 style: &ResolvedStyle,
2097 cursor: &mut PageCursor,
2098 pages: &mut Vec<LayoutPage>,
2099 x: f64,
2100 available_width: f64,
2101 font_context: &FontContext,
2102 ) {
2103 let padding = &style.padding;
2104 let margin = &style.margin.to_edges();
2105 let border = &style.border_width;
2106
2107 let outer_width = match style.width {
2108 SizeConstraint::Fixed(w) => w,
2109 SizeConstraint::Auto => available_width - margin.horizontal(),
2110 }
2111 .min(style.max_width)
2113 .max(style.min_width);
2114 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2115
2116 let children_height =
2117 self.measure_children_height(&node.children, inner_width, style, font_context);
2118 let total_height = match style.height {
2119 SizeConstraint::Fixed(h) => h,
2120 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2121 }
2122 .max(style.min_height);
2123
2124 let node_x = x + margin.left;
2125
2126 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2127
2128 if fits || !style.breakable {
2129 if !fits && !style.breakable {
2130 pages.push(cursor.finalize());
2131 *cursor = cursor.new_page();
2132 }
2133
2134 let rect_y = cursor.content_y + cursor.y + margin.top;
2136 let snapshot = cursor.elements.len();
2137
2138 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2143 cursor.elements.push(marker);
2144 }
2145
2146 let saved_y = cursor.y;
2147 cursor.y += margin.top + padding.top + border.top;
2148
2149 let pages_before = pages.len();
2150 let children_x = node_x + padding.left + border.left;
2151 let is_grid =
2152 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2153 if is_grid {
2154 self.layout_grid_children(
2155 &node.children,
2156 style,
2157 cursor,
2158 pages,
2159 children_x,
2160 inner_width,
2161 font_context,
2162 );
2163 } else {
2164 self.layout_children(
2165 &node.children,
2166 &node.style,
2167 cursor,
2168 pages,
2169 children_x,
2170 inner_width,
2171 Some(style),
2172 font_context,
2173 );
2174 }
2175
2176 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2178
2179 if self.measure_check
2186 && matches!(style.height, SizeConstraint::Auto)
2187 && pages.len() == pages_before
2188 && !child_elements.is_empty()
2189 {
2190 let content_top = rect_y + padding.top + border.top;
2191 let extent = child_elements
2192 .iter()
2193 .map(|el| el.y + el.height)
2194 .fold(f64::NEG_INFINITY, f64::max)
2195 - content_top;
2196 if extent > 0.0 && children_height - extent > 2.0 {
2197 self.defect(format!(
2198 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2199 node_kind_name(&node.kind),
2200 children_height - extent,
2201 ));
2202 }
2203 }
2204
2205 let rect_element = LayoutElement {
2206 x: node_x,
2207 y: rect_y,
2208 width: outer_width,
2209 height: total_height,
2210 draw: DrawCommand::Rect {
2211 background: style.background_color,
2212 border_width: style.border_width,
2213 border_color: style.border_color,
2214 border_style: style.border_style,
2215 border_radius: style.border_radius,
2216 opacity: 1.0,
2217 box_shadow: style.box_shadow.map(Box::new),
2218 background_gradient: style.background.clone().map(Box::new),
2219 },
2220 children: child_elements,
2221 node_type: Some(node_kind_name(&node.kind).to_string()),
2222 resolved_style: Some(style.clone()),
2223 source_location: node.source_location.clone(),
2224 href: node.href.clone(),
2225 bookmark: None,
2228 alt: None,
2229 is_header_row: false,
2230 col_span: 1,
2231 overflow: style.overflow,
2232 opacity: style.opacity,
2233 };
2234 cursor.elements.push(rect_element);
2235
2236 cursor.y = saved_y + total_height + margin.vertical();
2237 } else {
2238 self.layout_breakable_view(
2239 node,
2240 style,
2241 cursor,
2242 pages,
2243 node_x,
2244 outer_width,
2245 inner_width,
2246 font_context,
2247 );
2248 }
2249 }
2250
2251 #[allow(clippy::too_many_arguments)]
2252 fn layout_breakable_view(
2253 &self,
2254 node: &Node,
2255 style: &ResolvedStyle,
2256 cursor: &mut PageCursor,
2257 pages: &mut Vec<LayoutPage>,
2258 node_x: f64,
2259 outer_width: f64,
2260 inner_width: f64,
2261 font_context: &FontContext,
2262 ) {
2263 let padding = &style.padding;
2264 let border = &style.border_width;
2265 let margin = &style.margin.to_edges();
2266
2267 let initial_page_count = pages.len();
2269 let snapshot = cursor.elements.len();
2270 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2271
2272 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2280 cursor.elements.push(marker);
2281 }
2282
2283 cursor.y += margin.top + padding.top + border.top;
2284 let prev_continuation_offset = cursor.continuation_top_offset;
2285 cursor.continuation_top_offset = padding.top + border.top;
2286
2287 let children_x = node_x + padding.left + border.left;
2288 let is_grid =
2289 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2290 if is_grid {
2291 self.layout_grid_children(
2292 &node.children,
2293 style,
2294 cursor,
2295 pages,
2296 children_x,
2297 inner_width,
2298 font_context,
2299 );
2300 } else {
2301 self.layout_children(
2302 &node.children,
2303 &node.style,
2304 cursor,
2305 pages,
2306 children_x,
2307 inner_width,
2308 Some(style),
2309 font_context,
2310 );
2311 }
2312
2313 cursor.continuation_top_offset = prev_continuation_offset;
2314
2315 let has_visual = style.background_color.is_some()
2317 || style.border_width.top > 0.0
2318 || style.border_width.right > 0.0
2319 || style.border_width.bottom > 0.0
2320 || style.border_width.left > 0.0;
2321 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2323
2324 if !needs_wrapper {
2325 cursor.y += padding.bottom + border.bottom + margin.bottom;
2327 return;
2328 }
2329
2330 let draw_cmd = DrawCommand::Rect {
2331 background: style.background_color,
2332 border_width: style.border_width,
2333 border_color: style.border_color,
2334 border_style: style.border_style,
2335 border_radius: style.border_radius,
2336 opacity: 1.0,
2337 box_shadow: style.box_shadow.map(Box::new),
2338 background_gradient: style.background.clone().map(Box::new),
2339 };
2340
2341 if pages.len() == initial_page_count {
2342 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2344 let rect_height =
2345 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2346 cursor.elements.push(LayoutElement {
2347 x: node_x,
2348 y: rect_start_y,
2349 width: outer_width,
2350 height: rect_height,
2351 draw: draw_cmd,
2352 children: child_elements,
2353 node_type: Some(node_kind_name(&node.kind).to_string()),
2354 resolved_style: Some(style.clone()),
2355 source_location: node.source_location.clone(),
2356 href: node.href.clone(),
2357 bookmark: None,
2362 alt: None,
2363 is_header_row: false,
2364 col_span: 1,
2365 overflow: style.overflow,
2366 opacity: style.opacity,
2367 });
2368 } else {
2369 let page = &mut pages[initial_page_count];
2373 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2374 let page_content_bottom =
2375 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2376 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
2377 if !our_elements.is_empty() {
2378 let rect_height = page_content_bottom - rect_start_y;
2379 page.elements.push(LayoutElement {
2380 x: node_x,
2381 y: rect_start_y,
2382 width: outer_width,
2383 height: rect_height,
2384 draw: draw_cmd.clone(),
2385 children: our_elements,
2386 node_type: Some(node_kind_name(&node.kind).to_string()),
2387 resolved_style: Some(style.clone()),
2388 source_location: node.source_location.clone(),
2389 href: node.href.clone(),
2390 bookmark: None,
2392 alt: None,
2393 is_header_row: false,
2394 col_span: 1,
2395 overflow: Overflow::default(),
2396 opacity: 1.0,
2397 });
2398 }
2399
2400 for page in &mut pages[initial_page_count + 1..] {
2402 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2403 let content_top = page.config.margin.top + header_h;
2404 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2405 let content_bottom = page.config.margin.top
2406 + (page.height - page.config.margin.vertical())
2407 - footer_h;
2408 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2409 if !all_elements.is_empty() {
2410 page.elements.push(LayoutElement {
2411 x: node_x,
2412 y: content_top,
2413 width: outer_width,
2414 height: content_bottom - content_top,
2415 draw: draw_cmd.clone(),
2416 children: all_elements,
2417 node_type: Some(node_kind_name(&node.kind).to_string()),
2418 resolved_style: Some(style.clone()),
2419 source_location: node.source_location.clone(),
2420 href: None,
2421 bookmark: None,
2422 alt: None,
2423 is_header_row: false,
2424 col_span: 1,
2425 overflow: Overflow::default(),
2426 opacity: 1.0,
2427 });
2428 }
2429 }
2430
2431 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2433 if !all_elements.is_empty() {
2434 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2435 let content_top = cursor.content_y + header_h;
2436 let rect_height =
2437 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2438 cursor.elements.push(LayoutElement {
2439 x: node_x,
2440 y: content_top,
2441 width: outer_width,
2442 height: rect_height,
2443 draw: draw_cmd,
2444 children: all_elements,
2445 node_type: Some(node_kind_name(&node.kind).to_string()),
2446 resolved_style: Some(style.clone()),
2447 source_location: node.source_location.clone(),
2448 href: None,
2449 bookmark: None,
2450 alt: None,
2451 is_header_row: false,
2452 col_span: 1,
2453 overflow: Overflow::default(),
2454 opacity: 1.0,
2455 });
2456 }
2457 }
2458
2459 cursor.y += padding.bottom + border.bottom + margin.bottom;
2460 }
2461
2462 #[allow(clippy::too_many_arguments)]
2463 fn layout_children(
2464 &self,
2465 children: &[Node],
2466 _parent_raw_style: &Style,
2467 cursor: &mut PageCursor,
2468 pages: &mut Vec<LayoutPage>,
2469 content_x: f64,
2470 available_width: f64,
2471 parent_style: Option<&ResolvedStyle>,
2472 font_context: &FontContext,
2473 ) {
2474 let parent_box_y = cursor.content_y + cursor.y;
2476 let parent_box_x = content_x;
2477 let entry_page_index = pages.len();
2481
2482 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2488 let saved_cb = cursor.containing_block;
2489 if parent_positioned {
2490 let cb_height = parent_style
2491 .and_then(|ps| match ps.height {
2492 SizeConstraint::Fixed(h) => {
2493 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2494 }
2495 SizeConstraint::Auto => None,
2496 })
2497 .unwrap_or(saved_cb.3);
2498 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2499 }
2500
2501 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2503 .iter()
2504 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2505
2506 let direction = parent_style
2507 .map(|s| s.flex_direction)
2508 .unwrap_or(FlexDirection::Column);
2509
2510 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2511 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2512
2513 match direction {
2515 FlexDirection::Column | FlexDirection::ColumnReverse => {
2516 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2517 flow_children.into_iter().rev().collect()
2518 } else {
2519 flow_children
2520 };
2521
2522 let justify = parent_style
2523 .map(|s| s.justify_content)
2524 .unwrap_or(JustifyContent::FlexStart);
2525 let align = parent_style
2526 .map(|s| s.align_items)
2527 .unwrap_or(AlignItems::Stretch);
2528
2529 let start_y = cursor.y;
2530 let initial_pages = pages.len();
2531
2532 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2534
2535 for (i, child) in items.iter().enumerate() {
2536 if i > 0 {
2537 cursor.y += row_gap;
2538 }
2539 let child_start = cursor.elements.len();
2540
2541 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2544 let has_auto_h = child_margin.has_auto_horizontal();
2545
2546 let (child_x, layout_w) = if has_auto_h {
2553 let child_style = child.style.resolve(parent_style, available_width);
2554 let has_explicit_width =
2555 matches!(child_style.width, SizeConstraint::Fixed(_));
2556 let intrinsic = self
2557 .measure_intrinsic_width(child, &child_style, font_context)
2558 .min(available_width);
2559 let w = match child_style.width {
2560 SizeConstraint::Fixed(fw) => fw,
2561 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2566 (available_width - child_margin.horizontal())
2567 .min(child_style.max_width)
2568 }
2569 SizeConstraint::Auto => intrinsic,
2570 }
2571 .min(child_style.max_width)
2572 .max(child_style.min_width);
2573 let lw = if has_explicit_width {
2574 available_width
2575 } else {
2576 w
2577 };
2578 let fixed_h = child_margin.horizontal();
2579 let slack = (available_width - w - fixed_h).max(0.0);
2580 let auto_left = child_margin.left.is_auto();
2581 let auto_right = child_margin.right.is_auto();
2582 let ml = match (auto_left, auto_right) {
2583 (true, true) => slack / 2.0,
2584 (true, false) => slack,
2585 (false, true) => 0.0,
2586 (false, false) => 0.0,
2587 };
2588 (content_x + child_margin.left.resolve() + ml, lw)
2589 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2590 let child_style = child.style.resolve(parent_style, available_width);
2591 let has_explicit_width =
2592 matches!(child_style.width, SizeConstraint::Fixed(_));
2593 let intrinsic = self
2594 .measure_intrinsic_width(child, &child_style, font_context)
2595 .min(available_width);
2596 let w = match child_style.width {
2597 SizeConstraint::Fixed(fw) => fw,
2598 SizeConstraint::Auto => intrinsic,
2599 };
2600 let lw = if has_explicit_width {
2601 available_width
2602 } else {
2603 w
2604 };
2605 match align {
2606 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2607 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2608 _ => (content_x, available_width),
2609 }
2610 } else {
2611 (content_x, available_width)
2612 };
2613
2614 self.layout_node(
2615 child,
2616 cursor,
2617 pages,
2618 child_x,
2619 layout_w,
2620 parent_style,
2621 font_context,
2622 None,
2623 None,
2624 );
2625
2626 child_ranges.push((child_start, cursor.elements.len()));
2627 }
2628
2629 let container_inner_h: Option<f64> = parent_style
2632 .and_then(|ps| match ps.height {
2633 SizeConstraint::Fixed(h) => {
2634 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2635 }
2636 SizeConstraint::Auto => None,
2637 })
2638 .or_else(|| {
2639 if parent_style.is_none() {
2641 Some(cursor.content_height - start_y)
2642 } else {
2643 None
2644 }
2645 });
2646
2647 if let Some(inner_h) = container_inner_h {
2648 if pages.len() == initial_pages {
2649 let child_styles: Vec<ResolvedStyle> = items
2650 .iter()
2651 .map(|child| child.style.resolve(parent_style, available_width))
2652 .collect();
2653 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2654 if total_grow > 0.0 {
2655 let children_total = cursor.y - start_y;
2656 let slack = (inner_h - children_total).max(0.0);
2657 if slack > 0.0 {
2658 let mut cumulative_shift = 0.0_f64;
2659 for (i, cs) in child_styles.iter().enumerate() {
2660 let (start, end) = child_ranges[i];
2661 if cumulative_shift > 0.001 {
2662 for j in start..end {
2663 offset_element_y(
2664 &mut cursor.elements[j],
2665 cumulative_shift,
2666 );
2667 }
2668 }
2669 if cs.flex_grow > 0.0 {
2670 let extra = slack * (cs.flex_grow / total_grow);
2671 if start < end {
2673 let elem = &mut cursor.elements[end - 1];
2674 elem.height += extra;
2675 reapply_justify_content(elem);
2676 }
2677 cumulative_shift += extra;
2678 }
2679 }
2680 cursor.y += cumulative_shift;
2681 }
2682 }
2683 }
2684 }
2685
2686 if let Some(inner_h) = container_inner_h {
2693 if pages.len() == initial_pages {
2694 let auto_styles: Vec<ResolvedStyle> = items
2695 .iter()
2696 .map(|child| child.style.resolve(parent_style, available_width))
2697 .collect();
2698 let total_autos: usize = auto_styles
2699 .iter()
2700 .map(|s| {
2701 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2702 })
2703 .sum();
2704 if total_autos > 0 {
2705 let children_total = cursor.y - start_y;
2706 let total_slack = (inner_h - children_total).max(0.0);
2707 if total_slack > 0.0 {
2708 let per_auto = total_slack / total_autos as f64;
2709 let mut cumulative_shift = 0.0_f64;
2710 for (i, cs) in auto_styles.iter().enumerate() {
2711 let (start, end) = child_ranges[i];
2712 let mt_auto = cs.margin.top.is_auto();
2713 let mb_auto = cs.margin.bottom.is_auto();
2714 let this_child_shift =
2718 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2719 if this_child_shift > 0.001 {
2720 for j in start..end {
2721 offset_element_y(
2722 &mut cursor.elements[j],
2723 this_child_shift,
2724 );
2725 }
2726 }
2727 cumulative_shift =
2730 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2731 }
2732 cursor.y += cumulative_shift;
2733 }
2734 }
2735 }
2736 }
2737
2738 let needs_justify =
2740 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2741 if needs_justify {
2742 let justify_inner_h = container_inner_h.or_else(|| {
2744 parent_style.and_then(|ps| match ps.height {
2745 SizeConstraint::Fixed(h) => {
2746 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2747 }
2748 SizeConstraint::Auto => None,
2749 })
2750 });
2751 if let Some(inner_h) = justify_inner_h {
2752 let children_total = cursor.y - start_y;
2753 let slack = inner_h - children_total;
2754 if slack > 0.0 {
2755 let n = child_ranges.len();
2756 let offsets: Vec<f64> = match justify {
2757 JustifyContent::FlexEnd => vec![slack; n],
2758 JustifyContent::Center => vec![slack / 2.0; n],
2759 JustifyContent::SpaceBetween => {
2760 if n <= 1 {
2761 vec![0.0; n]
2762 } else {
2763 let per_gap = slack / (n - 1) as f64;
2764 (0..n).map(|i| i as f64 * per_gap).collect()
2765 }
2766 }
2767 JustifyContent::SpaceAround => {
2768 let space = slack / n as f64;
2769 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2770 }
2771 JustifyContent::SpaceEvenly => {
2772 let space = slack / (n + 1) as f64;
2773 (0..n).map(|i| (i + 1) as f64 * space).collect()
2774 }
2775 JustifyContent::FlexStart => vec![0.0; n],
2776 };
2777 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2778 let dy = offsets[i];
2779 if dy.abs() > 0.001 {
2780 for j in start..end {
2781 offset_element_y(&mut cursor.elements[j], dy);
2782 }
2783 }
2784 }
2785 cursor.y += *offsets.last().unwrap_or(&0.0);
2786 }
2787 }
2788 }
2789 }
2790
2791 FlexDirection::Row | FlexDirection::RowReverse => {
2792 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2793 self.layout_flex_row(
2794 &flow_owned,
2795 cursor,
2796 pages,
2797 content_x,
2798 available_width,
2799 parent_style,
2800 column_gap,
2801 row_gap,
2802 font_context,
2803 );
2804 }
2805 }
2806
2807 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
2821 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2822 let ph = parent_style
2823 .and_then(|ps| match ps.height {
2824 SizeConstraint::Fixed(h) => {
2825 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2826 }
2827 SizeConstraint::Auto => None,
2828 })
2829 .unwrap_or_else(|| {
2830 if parent_fragmented {
2831 let first = &pages[entry_page_index];
2832 (first.height - first.config.margin.bottom) - parent_box_y
2833 } else {
2834 cursor.content_y + cursor.y - parent_box_y
2835 }
2836 });
2837 (parent_box_x, parent_box_y, available_width, ph)
2838 } else {
2839 cursor.containing_block
2840 };
2841
2842 for abs_child in &abs_children {
2844 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2845
2846 let child_width = match abs_style.width {
2848 SizeConstraint::Fixed(w) => w,
2849 SizeConstraint::Auto => {
2850 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2852 (cb_w - l - r).max(0.0)
2853 } else {
2854 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2855 }
2856 }
2857 };
2858
2859 let child_height = match abs_style.height {
2860 SizeConstraint::Fixed(h) => h,
2861 SizeConstraint::Auto => {
2862 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2863 }
2864 };
2865
2866 let abs_margin = abs_style.margin.to_edges();
2874 let abs_x = if let Some(l) = abs_style.left {
2875 cb_x + l
2876 } else if let Some(r) = abs_style.right {
2877 cb_x + cb_w - r - child_width - abs_margin.horizontal()
2878 } else {
2879 cb_x
2880 };
2881
2882 let abs_y = if let Some(t) = abs_style.top {
2883 cb_y + t
2884 } else if let Some(b) = abs_style.bottom {
2885 cb_y + cb_h - b - child_height - abs_margin.vertical()
2886 } else {
2887 cb_y
2888 };
2889
2890 let mut abs_cursor = PageCursor::new(&cursor.config);
2892 abs_cursor.y = 0.0;
2893 abs_cursor.content_x = abs_x;
2894 abs_cursor.content_y = abs_y;
2895
2896 self.layout_node(
2897 abs_child,
2898 &mut abs_cursor,
2899 &mut Vec::new(),
2900 abs_x,
2901 child_width,
2902 parent_style,
2903 font_context,
2904 None,
2905 None,
2906 );
2907
2908 if parent_fragmented {
2912 pages[entry_page_index].elements.extend(abs_cursor.elements);
2913 } else {
2914 cursor.elements.extend(abs_cursor.elements);
2915 }
2916 }
2917
2918 cursor.containing_block = saved_cb;
2920 }
2921
2922 #[allow(clippy::too_many_arguments)]
2923 fn layout_flex_row(
2924 &self,
2925 children: &[Node],
2926 cursor: &mut PageCursor,
2927 pages: &mut Vec<LayoutPage>,
2928 content_x: f64,
2929 available_width: f64,
2930 parent_style: Option<&ResolvedStyle>,
2931 column_gap: f64,
2932 row_gap: f64,
2933 font_context: &FontContext,
2934 ) {
2935 if children.is_empty() {
2936 return;
2937 }
2938
2939 let flex_wrap = parent_style
2940 .map(|s| s.flex_wrap)
2941 .unwrap_or(FlexWrap::NoWrap);
2942
2943 let items: Vec<FlexItem> = children
2946 .iter()
2947 .map(|child| {
2948 let style = child.style.resolve(parent_style, available_width);
2949 let base_width = match style.flex_basis {
2950 SizeConstraint::Fixed(w) => w,
2951 SizeConstraint::Auto => match style.width {
2952 SizeConstraint::Fixed(w) => w,
2953 SizeConstraint::Auto => {
2954 self.measure_intrinsic_width(child, &style, font_context)
2955 }
2956 },
2957 };
2958 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2959 FlexItem {
2960 node: child,
2961 style,
2962 base_width,
2963 min_content_width,
2964 }
2965 })
2966 .collect();
2967
2968 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2970 let lines = match flex_wrap {
2971 FlexWrap::NoWrap => {
2972 vec![flex::WrapLine {
2973 start: 0,
2974 end: items.len(),
2975 }]
2976 }
2977 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2978 FlexWrap::WrapReverse => {
2979 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2980 l.reverse();
2981 l
2982 }
2983 };
2984
2985 if lines.is_empty() {
2986 return;
2987 }
2988
2989 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2991
2992 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2994
2995 let initial_pages_count = pages.len();
2996 let flex_start_y = cursor.y;
2997 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2998
2999 for (line_idx, line) in lines.iter().enumerate() {
3000 let line_items = &items[line.start..line.end];
3001 let line_count = line.end - line.start;
3002 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3003 let distributable = available_width - line_gap;
3004
3005 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
3007 let remaining = distributable - total_base;
3008
3009 if remaining > 0.0 {
3010 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3011 if total_grow > 0.0 {
3012 for (j, item) in line_items.iter().enumerate() {
3013 final_widths[line.start + j] =
3014 item.base_width + remaining * (item.style.flex_grow / total_grow);
3015 }
3016 }
3017 } else if remaining < 0.0 {
3018 let total_shrink: f64 = line_items
3019 .iter()
3020 .map(|i| i.style.flex_shrink * i.base_width)
3021 .sum();
3022 if total_shrink > 0.0 {
3023 for (j, item) in line_items.iter().enumerate() {
3024 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3025 let w = item.base_width + remaining * factor;
3026 let floor = item.style.min_width.max(item.min_content_width);
3027 final_widths[line.start + j] = w.max(floor);
3028 }
3029 }
3030 }
3031
3032 let mut line_height: f64 = line_items
3034 .iter()
3035 .enumerate()
3036 .map(|(j, item)| {
3037 let fw = final_widths[line.start + j];
3038 self.measure_node_height(item.node, fw, &item.style, font_context)
3039 + item.style.margin.vertical()
3040 })
3041 .fold(0.0f64, f64::max);
3042
3043 if let Some(ps) = parent_style {
3054 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3055 if let SizeConstraint::Fixed(h) = ps.height {
3056 let inner = h - ps.padding.vertical() - ps.border_width.vertical();
3057 line_height = line_height.max(inner);
3058 }
3059 }
3060 }
3061
3062 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
3069 pages.push(cursor.finalize());
3070 *cursor = cursor.new_page();
3071 }
3072
3073 if line_idx > 0 {
3075 cursor.y += row_gap;
3076 }
3077
3078 let row_start_y = cursor.y;
3079
3080 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
3082 let slack = available_width - actual_total - line_gap;
3083
3084 let (start_offset, between_extra) = match justify {
3085 JustifyContent::FlexStart => (0.0, 0.0),
3086 JustifyContent::FlexEnd => (slack, 0.0),
3087 JustifyContent::Center => (slack / 2.0, 0.0),
3088 JustifyContent::SpaceBetween => {
3089 if line_count > 1 {
3090 (0.0, slack / (line_count as f64 - 1.0))
3091 } else {
3092 (0.0, 0.0)
3093 }
3094 }
3095 JustifyContent::SpaceAround => {
3096 let s = slack / line_count as f64;
3097 (s / 2.0, s)
3098 }
3099 JustifyContent::SpaceEvenly => {
3100 let s = slack / (line_count as f64 + 1.0);
3101 (s, s)
3102 }
3103 };
3104
3105 let line_elem_start = cursor.elements.len();
3106 let mut x = content_x + start_offset;
3107
3108 let line_start_pages = pages.len();
3119
3120 for (j, item) in line_items.iter().enumerate() {
3121 if j > 0 {
3122 x += column_gap + between_extra;
3123 }
3124
3125 let fw = final_widths[line.start + j];
3126
3127 let align = item
3128 .style
3129 .align_self
3130 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3131
3132 let item_height =
3133 self.measure_node_height(item.node, fw, &item.style, font_context);
3134
3135 let has_auto_v = item.style.margin.has_auto_vertical();
3137 let y_offset = if has_auto_v {
3138 let fixed_v = item.style.margin.vertical();
3139 let slack = (line_height - item_height - fixed_v).max(0.0);
3140 let auto_top = item.style.margin.top.is_auto();
3141 let auto_bottom = item.style.margin.bottom.is_auto();
3142 match (auto_top, auto_bottom) {
3143 (true, true) => slack / 2.0,
3144 (true, false) => slack,
3145 (false, true) => 0.0,
3146 (false, false) => 0.0,
3147 }
3148 } else {
3149 match align {
3150 AlignItems::FlexStart => 0.0,
3151 AlignItems::FlexEnd => {
3152 line_height - item_height - item.style.margin.vertical()
3153 }
3154 AlignItems::Center => {
3155 (line_height - item_height - item.style.margin.vertical()) / 2.0
3156 }
3157 AlignItems::Stretch => 0.0,
3158 AlignItems::Baseline => 0.0,
3159 }
3160 };
3161
3162 let cross_h = if matches!(align, AlignItems::Stretch)
3166 && matches!(item.style.height, SizeConstraint::Auto)
3167 && !has_auto_v
3168 {
3169 let stretch_h = line_height - item.style.margin.vertical();
3170 if stretch_h > item_height {
3171 Some(stretch_h)
3172 } else {
3173 None
3174 }
3175 } else {
3176 None
3177 };
3178
3179 let saved_y = cursor.y;
3180 cursor.y = row_start_y + y_offset;
3181
3182 self.layout_node(
3183 item.node,
3184 cursor,
3185 pages,
3186 x,
3187 available_width,
3188 parent_style,
3189 font_context,
3190 cross_h,
3191 Some(fw),
3192 );
3193
3194 cursor.y = saved_y;
3195 x += fw;
3196 }
3197
3198 if line_items.len() > 1 && pages.len() > line_start_pages {
3199 let near = line_items
3200 .iter()
3201 .find_map(|it| first_text_snippet(it.node))
3202 .map(|t| format!(" (row beginning \"{t}\")"))
3203 .unwrap_or_default();
3204 self.defect(format!(
3205 "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}"
3206 ));
3207 }
3208 cursor.y = row_start_y + line_height;
3209 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3210 }
3211
3212 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3214 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3215 if !matches!(align_content, AlignContent::FlexStart)
3216 && !matches!(flex_wrap, FlexWrap::NoWrap)
3217 {
3218 if let Some(parent) = parent_style {
3219 if let SizeConstraint::Fixed(container_h) = parent.height {
3220 let inner_h = container_h
3221 - parent.padding.vertical()
3222 - parent.border_width.vertical();
3223 let total_used = cursor.y - flex_start_y;
3224 let slack = inner_h - total_used;
3225 if slack > 0.0 {
3226 let n = line_infos.len();
3227 let offsets: Vec<f64> = match align_content {
3228 AlignContent::FlexEnd => vec![slack; n],
3229 AlignContent::Center => vec![slack / 2.0; n],
3230 AlignContent::SpaceBetween => {
3231 if n <= 1 {
3232 vec![0.0; n]
3233 } else {
3234 let per_gap = slack / (n - 1) as f64;
3235 (0..n).map(|i| i as f64 * per_gap).collect()
3236 }
3237 }
3238 AlignContent::SpaceAround => {
3239 let space = slack / n as f64;
3240 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3241 }
3242 AlignContent::SpaceEvenly => {
3243 let space = slack / (n + 1) as f64;
3244 (0..n).map(|i| (i + 1) as f64 * space).collect()
3245 }
3246 AlignContent::Stretch => {
3247 let extra = slack / n as f64;
3248 (0..n).map(|i| i as f64 * extra).collect()
3249 }
3250 AlignContent::FlexStart => vec![0.0; n],
3251 };
3252 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3253 let dy = offsets[i];
3254 if dy.abs() > 0.001 {
3255 for j in start..end {
3256 offset_element_y(&mut cursor.elements[j], dy);
3257 }
3258 }
3259 }
3260 cursor.y += *offsets.last().unwrap_or(&0.0);
3261 }
3262 }
3263 }
3264 }
3265 }
3266 }
3267
3268 #[allow(clippy::too_many_arguments)]
3271 fn layout_list(
3272 &self,
3273 node: &Node,
3274 ordered: bool,
3275 marker_type: ListMarkerType,
3276 start: u32,
3277 style: &ResolvedStyle,
3278 cursor: &mut PageCursor,
3279 pages: &mut Vec<LayoutPage>,
3280 x: f64,
3281 available_width: f64,
3282 font_context: &FontContext,
3283 ) {
3284 let margin = &style.margin.to_edges();
3285 let padding = &style.padding;
3286
3287 cursor.y += margin.top;
3288
3289 let list_x = x + margin.left;
3290 let outer_width = available_width - margin.horizontal();
3291 let inner_width = outer_width - padding.horizontal();
3292
3293 let n_items = node
3296 .children
3297 .iter()
3298 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3299 .count() as u32;
3300
3301 let marker_gutter =
3302 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3303
3304 let list_inner_x = list_x + padding.left;
3305 let content_x = list_inner_x + marker_gutter;
3306 let content_width = (inner_width - marker_gutter).max(0.0);
3307
3308 let snapshot = cursor.elements.len();
3311 let list_start_y = cursor.content_y + cursor.y;
3312 cursor.y += padding.top;
3313
3314 let mut item_index: u32 = 0;
3315 for child in &node.children {
3316 if !matches!(child.kind, NodeKind::ListItem) {
3317 continue;
3318 }
3319 let marker_idx = start + item_index;
3320 self.layout_list_item(
3321 child,
3322 marker_idx,
3323 ordered,
3324 marker_type,
3325 marker_gutter,
3326 style,
3327 cursor,
3328 pages,
3329 list_inner_x,
3330 content_x,
3331 content_width,
3332 font_context,
3333 );
3334 item_index += 1;
3335 }
3336
3337 cursor.y += padding.bottom;
3338
3339 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3341 let list_height = cursor.content_y + cursor.y - list_start_y;
3342 cursor.elements.push(LayoutElement {
3343 x: list_x,
3344 y: list_start_y,
3345 width: outer_width,
3346 height: list_height,
3347 draw: DrawCommand::None,
3348 children: item_elements,
3349 node_type: Some("List".to_string()),
3350 resolved_style: Some(style.clone()),
3351 source_location: node.source_location.clone(),
3352 href: None,
3353 bookmark: node.bookmark.clone(),
3354 alt: None,
3355 is_header_row: false,
3356 col_span: 1,
3357 overflow: style.overflow,
3358 opacity: style.opacity,
3359 });
3360
3361 cursor.y += margin.bottom;
3362 }
3363
3364 #[allow(clippy::too_many_arguments)]
3365 fn layout_list_item(
3366 &self,
3367 item: &Node,
3368 marker_idx: u32,
3369 ordered: bool,
3370 marker_type: ListMarkerType,
3371 marker_gutter: f64,
3372 parent_style: &ResolvedStyle,
3373 cursor: &mut PageCursor,
3374 pages: &mut Vec<LayoutPage>,
3375 list_inner_x: f64,
3376 content_x: f64,
3377 content_width: f64,
3378 font_context: &FontContext,
3379 ) {
3380 let item_style = item.style.resolve(Some(parent_style), content_width);
3381 let item_margin = item_style.margin.to_edges();
3382
3383 cursor.y += item_margin.top;
3384 let item_start_y = cursor.content_y + cursor.y;
3385 let item_snapshot = cursor.elements.len();
3386
3387 let marker_str = format_marker(marker_idx, ordered, marker_type);
3391 if !marker_str.is_empty() {
3392 let saved_y = cursor.y;
3393 self.layout_text(
3394 &marker_str,
3395 None,
3396 &[],
3397 &item_style,
3398 cursor,
3399 pages,
3400 list_inner_x,
3401 marker_gutter,
3402 font_context,
3403 None,
3404 None,
3405 Some("Lbl"),
3406 );
3407 cursor.y = saved_y;
3408 }
3409
3410 self.layout_children(
3416 &item.children,
3417 &item.style,
3418 cursor,
3419 pages,
3420 content_x,
3421 content_width,
3422 Some(&item_style),
3423 font_context,
3424 );
3425
3426 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3429 let item_height = cursor.content_y + cursor.y - item_start_y;
3430 let item_width = content_x + content_width - list_inner_x;
3431 cursor.elements.push(LayoutElement {
3432 x: list_inner_x,
3433 y: item_start_y,
3434 width: item_width,
3435 height: item_height,
3436 draw: DrawCommand::None,
3437 children: item_children,
3438 node_type: Some("ListItem".to_string()),
3439 resolved_style: Some(item_style.clone()),
3440 source_location: item.source_location.clone(),
3441 href: None,
3442 bookmark: item.bookmark.clone(),
3443 alt: None,
3444 is_header_row: false,
3445 col_span: 1,
3446 overflow: item_style.overflow,
3447 opacity: item_style.opacity,
3448 });
3449
3450 cursor.y += item_margin.bottom;
3451 }
3452
3453 #[allow(clippy::too_many_arguments)]
3454 fn layout_table(
3455 &self,
3456 node: &Node,
3457 style: &ResolvedStyle,
3458 column_defs: &[ColumnDef],
3459 cursor: &mut PageCursor,
3460 pages: &mut Vec<LayoutPage>,
3461 x: f64,
3462 available_width: f64,
3463 font_context: &FontContext,
3464 ) {
3465 let padding = &style.padding;
3466 let margin = &style.margin.to_edges();
3467 let border = &style.border_width;
3468
3469 let table_x = x + margin.left;
3470 let table_width = match style.width {
3471 SizeConstraint::Fixed(w) => w,
3472 SizeConstraint::Auto => available_width - margin.horizontal(),
3473 };
3474 let inner_width = table_width - padding.horizontal() - border.horizontal();
3475
3476 let col_widths = self.resolve_column_widths(
3477 column_defs,
3478 inner_width,
3479 &node.children,
3480 style,
3481 font_context,
3482 );
3483
3484 let all_offsets = Self::table_column_offsets(&node.children);
3488 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
3489 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
3490
3491 for (child, offs) in node.children.iter().zip(&all_offsets) {
3492 match &child.kind {
3493 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
3494 _ => body_rows.push((child, offs)),
3495 }
3496 }
3497
3498 if !style.breakable {
3504 let total_height: f64 = node
3505 .children
3506 .iter()
3507 .zip(&all_offsets)
3508 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3509 .sum::<f64>()
3510 + padding.vertical()
3511 + border.vertical();
3512 let fresh_page_available = cursor.content_height
3513 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3514 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3515 if total_height > cursor.remaining_height()
3516 && total_height <= fresh_page_available
3517 && cursor.y > 0.0
3518 {
3519 pages.push(cursor.finalize());
3520 *cursor = cursor.new_page();
3521 }
3522 }
3523
3524 let initial_page_count = pages.len();
3531 let snapshot = cursor.elements.len();
3532 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3533
3534 cursor.y += margin.top + padding.top + border.top;
3535
3536 let cell_x_start = table_x + padding.left + border.left;
3537
3538 if !header_rows.is_empty() {
3563 let total_header_h: f64 = header_rows
3564 .iter()
3565 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3566 .sum();
3567 let first_body_h = body_rows
3568 .first()
3569 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3570 .unwrap_or(0.0);
3571
3572 let needed = total_header_h + first_body_h;
3573 let fresh_page_available = cursor.content_height
3574 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3575 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3576
3577 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3578 pages.push(cursor.finalize());
3579 *cursor = cursor.new_page();
3580 cursor.y += padding.top + border.top;
3581 }
3582 }
3583
3584 for (header_row, offs) in &header_rows {
3585 self.layout_table_row(
3586 header_row,
3587 &col_widths,
3588 offs,
3589 style,
3590 cursor,
3591 cell_x_start,
3592 font_context,
3593 pages,
3594 );
3595 }
3596
3597 for (body_row, offs) in &body_rows {
3598 let row_height =
3599 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
3600
3601 let fresh_page_available = cursor.content_height
3609 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3610 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3611 if row_height > fresh_page_available {
3612 self.defect(format!(
3613 "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",
3614 ));
3615 }
3616 if row_height > cursor.remaining_height()
3617 && cursor.remaining_height() < fresh_page_available - 0.5
3618 {
3619 pages.push(cursor.finalize());
3620 *cursor = cursor.new_page();
3621
3622 cursor.y += padding.top + border.top;
3623 for (header_row, h_offs) in &header_rows {
3624 self.layout_table_row(
3625 header_row,
3626 &col_widths,
3627 h_offs,
3628 style,
3629 cursor,
3630 cell_x_start,
3631 font_context,
3632 pages,
3633 );
3634 }
3635 }
3636
3637 self.layout_table_row(
3638 body_row,
3639 &col_widths,
3640 offs,
3641 style,
3642 cursor,
3643 cell_x_start,
3644 font_context,
3645 pages,
3646 );
3647 }
3648
3649 let has_visual = style.background_color.is_some()
3653 || style.background.is_some()
3654 || style.border_width.top > 0.0
3655 || style.border_width.right > 0.0
3656 || style.border_width.bottom > 0.0
3657 || style.border_width.left > 0.0;
3658 let draw_cmd = if has_visual {
3659 DrawCommand::Rect {
3660 background: style.background_color,
3661 border_width: style.border_width,
3662 border_color: style.border_color,
3663 border_style: style.border_style,
3664 border_radius: style.border_radius,
3665 opacity: 1.0,
3666 box_shadow: style.box_shadow.map(Box::new),
3667 background_gradient: style.background.clone().map(Box::new),
3668 }
3669 } else {
3670 DrawCommand::None
3671 };
3672 let make_wrapper =
3673 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3674 x: table_x,
3675 y,
3676 width: table_width,
3677 height,
3678 draw,
3679 children,
3680 node_type: Some(node_kind_name(&node.kind).to_string()),
3681 resolved_style: Some(style.clone()),
3682 source_location: node.source_location.clone(),
3683 href: node.href.clone(),
3684 bookmark: None,
3685 alt: None,
3686 is_header_row: false,
3687 col_span: 1,
3688 overflow: Overflow::default(),
3689 opacity: style.opacity,
3690 };
3691
3692 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3693
3694 if pages.len() == initial_page_count {
3695 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3697 cursor.elements.push(make_wrapper(
3698 rect_start_y,
3699 table_bottom_y - rect_start_y,
3700 child_elements,
3701 draw_cmd,
3702 ));
3703 } else {
3704 let page = &mut pages[initial_page_count];
3723 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3724 let page_content_bottom =
3725 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3726 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3727 if !our_elements.is_empty() {
3728 page.elements.push(make_wrapper(
3729 rect_start_y,
3730 page_content_bottom - rect_start_y,
3731 our_elements,
3732 draw_cmd.clone(),
3733 ));
3734 }
3735
3736 for page in &mut pages[initial_page_count + 1..] {
3738 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3739 let content_top = page.config.margin.top + header_h;
3740 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3741 let content_bottom = page.config.margin.top
3742 + (page.height - page.config.margin.vertical())
3743 - footer_h;
3744 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3745 if !all_elements.is_empty() {
3746 page.elements.push(make_wrapper(
3747 content_top,
3748 content_bottom - content_top,
3749 all_elements,
3750 draw_cmd.clone(),
3751 ));
3752 }
3753 }
3754
3755 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3757 if !all_elements.is_empty() {
3758 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3759 let content_top = cursor.content_y + header_h;
3760 cursor.elements.push(make_wrapper(
3761 content_top,
3762 table_bottom_y - content_top,
3763 all_elements,
3764 draw_cmd,
3765 ));
3766 }
3767 }
3768
3769 cursor.y += padding.bottom + border.bottom + margin.bottom;
3770 }
3771
3772 fn subtree_forces_break(node: &Node) -> bool {
3782 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
3783 return true;
3784 }
3785 if node.style.break_before == Some(true) {
3786 return true;
3787 }
3788 node.children.iter().any(Self::subtree_forces_break)
3789 }
3790
3791 fn occupancy_column_count(children: &[Node]) -> usize {
3797 let offsets = Self::table_column_offsets(children);
3798 children
3799 .iter()
3800 .zip(&offsets)
3801 .map(|(row, offs)| {
3802 row.children
3803 .iter()
3804 .zip(offs)
3805 .map(|(cell, &start)| start + Self::cell_col_span(cell))
3806 .max()
3807 .unwrap_or(0)
3808 })
3809 .max()
3810 .unwrap_or(1)
3811 .max(1)
3812 }
3813
3814 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
3824 fn spans(cell: &Node) -> (usize, u32) {
3825 match &cell.kind {
3826 NodeKind::TableCell { col_span, row_span } => {
3827 ((*col_span).max(1) as usize, (*row_span).max(1))
3828 }
3829 _ => (1, 1),
3830 }
3831 }
3832 let mut pending: Vec<u32> = Vec::new();
3833 let mut out = Vec::with_capacity(rows.len());
3834 for row in rows {
3835 let mut offsets = Vec::with_capacity(row.children.len());
3836 let mut col = 0usize;
3837 for cell in &row.children {
3838 let (span, rspan) = spans(cell);
3839 while pending.get(col).copied().unwrap_or(0) > 0 {
3840 col += 1;
3841 }
3842 offsets.push(col);
3843 if rspan > 1 {
3844 if pending.len() < col + span {
3845 pending.resize(col + span, 0);
3846 }
3847 for slot in pending.iter_mut().take(col + span).skip(col) {
3848 *slot = (*slot).max(rspan);
3849 }
3850 }
3851 col += span;
3852 }
3853 out.push(offsets);
3854 for p in pending.iter_mut() {
3855 *p = p.saturating_sub(1);
3856 }
3857 }
3858 out
3859 }
3860
3861 #[allow(clippy::too_many_arguments)]
3862 fn layout_table_row(
3863 &self,
3864 row: &Node,
3865 col_widths: &[f64],
3866 col_offsets: &[usize],
3867 parent_style: &ResolvedStyle,
3868 cursor: &mut PageCursor,
3869 start_x: f64,
3870 font_context: &FontContext,
3871 pages: &mut Vec<LayoutPage>,
3872 ) {
3873 let row_style = row
3874 .style
3875 .resolve(Some(parent_style), col_widths.iter().sum());
3876
3877 let row_height =
3878 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
3879 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets);
3880 let row_y = cursor.content_y + cursor.y;
3881 let total_width: f64 = col_widths.iter().sum();
3882
3883 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3884
3885 let row_snapshot = cursor.elements.len();
3887
3888 let row_may_break =
3900 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
3901
3902 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3903 for (cell_i, cell) in row.children.iter().enumerate() {
3907 let span = match &cell.kind {
3908 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
3909 _ => 1,
3910 };
3911 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
3912 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
3913 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
3914
3915 let cell_style = cell.style.resolve(Some(&row_style), col_width);
3916
3917 let cell_snapshot = cursor.elements.len();
3919
3920 let inner_width =
3921 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
3922
3923 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
3924 let saved_y = cursor.y;
3925 cursor.y += cell_style.padding.top + cell_style.border_width.top;
3926
3927 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
3931 let content_h: f64 = cell
3932 .children
3933 .iter()
3934 .map(|ch| {
3935 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
3936 self.measure_node_height(ch, inner_width, &ch_style, font_context)
3937 })
3938 .sum();
3939 let inner_row =
3940 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
3941 let slack = (inner_row - content_h).max(0.0);
3942 cursor.y += match cell_style.vertical_align {
3943 crate::style::VerticalAlign::Middle => slack / 2.0,
3944 crate::style::VerticalAlign::Bottom => slack,
3945 crate::style::VerticalAlign::Baseline => row_bl
3950 .map(|b| {
3951 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
3952 (b - d).max(0.0)
3953 })
3954 .unwrap_or(0.0),
3955 crate::style::VerticalAlign::Top => 0.0,
3956 };
3957 }
3958
3959 let cursor_before_cell = if row_may_break {
3964 Some(cursor.clone())
3965 } else {
3966 None
3967 };
3968 let mut cell_pages: Vec<LayoutPage> = Vec::new();
3969 for child in &cell.children {
3970 self.layout_node(
3971 child,
3972 cursor,
3973 &mut cell_pages,
3974 content_x,
3975 inner_width,
3976 Some(&cell_style),
3977 font_context,
3978 None,
3979 None,
3980 );
3981 }
3982
3983 if !cell_pages.is_empty() {
3985 let post_break_elements = std::mem::take(&mut cursor.elements);
3986 if let Some(last_page) = cell_pages.last_mut() {
3987 last_page.elements.extend(post_break_elements);
3988 }
3989 if !is_header {
3995 all_overflow_pages.extend(cell_pages);
3996 }
3997 *cursor = cursor_before_cell
4003 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4004 }
4005
4006 cursor.y = saved_y;
4007
4008 let cell_children: Vec<LayoutElement> =
4010 cursor.elements.drain(cell_snapshot..).collect();
4011
4012 cursor.elements.push(LayoutElement {
4014 x: cell_x,
4015 y: row_y,
4016 width: col_width,
4017 height: row_height,
4018 draw: if cell_style.background_color.is_some()
4019 || cell_style.border_width.horizontal() > 0.0
4020 || cell_style.border_width.vertical() > 0.0
4021 {
4022 DrawCommand::Rect {
4023 background: cell_style.background_color,
4024 border_width: cell_style.border_width,
4025 border_color: cell_style.border_color,
4026 border_style: cell_style.border_style,
4027 border_radius: cell_style.border_radius,
4028 opacity: 1.0,
4029 box_shadow: cell_style.box_shadow.map(Box::new),
4030 background_gradient: cell_style.background.clone().map(Box::new),
4031 }
4032 } else {
4033 DrawCommand::None
4034 },
4035 children: cell_children,
4036 node_type: Some("TableCell".to_string()),
4037 resolved_style: Some(cell_style.clone()),
4038 source_location: cell.source_location.clone(),
4039 href: None,
4040 bookmark: cell.bookmark.clone(),
4041 alt: None,
4042 is_header_row: is_header,
4043 col_span: span as u32,
4044 overflow: Overflow::default(),
4045 opacity: 1.0,
4046 });
4047 }
4048
4049 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
4051 let row_element = LayoutElement {
4052 x: start_x,
4053 y: row_y,
4054 width: total_width,
4055 height: row_height,
4056 draw: if let Some(bg) = row_style.background_color {
4057 DrawCommand::Rect {
4058 background: Some(bg),
4059 border_width: Edges::default(),
4060 border_color: EdgeValues::uniform(Color::BLACK),
4061 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4062 border_radius: CornerValues::uniform(0.0),
4063 opacity: 1.0,
4064 box_shadow: row_style.box_shadow.map(Box::new),
4065 background_gradient: row_style.background.clone().map(Box::new),
4066 }
4067 } else {
4068 DrawCommand::None
4069 },
4070 children: row_children,
4071 node_type: Some("TableRow".to_string()),
4072 resolved_style: Some(row_style.clone()),
4073 source_location: row.source_location.clone(),
4074 href: None,
4075 bookmark: row.bookmark.clone(),
4076 alt: None,
4077 is_header_row: is_header,
4078 col_span: 1,
4079 overflow: row_style.overflow,
4080 opacity: row_style.opacity,
4081 };
4082
4083 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4084 first_overflow.elements.push(row_element);
4094 } else {
4095 cursor.elements.push(row_element);
4096 cursor.y += row_height;
4097 }
4098
4099 pages.extend(all_overflow_pages);
4101 }
4102
4103 #[allow(clippy::too_many_arguments)]
4104 #[allow(clippy::too_many_arguments)]
4105 fn layout_text(
4106 &self,
4107 content: &str,
4108 href: Option<&str>,
4109 runs: &[TextRun],
4110 style: &ResolvedStyle,
4111 cursor: &mut PageCursor,
4112 pages: &mut Vec<LayoutPage>,
4113 x: f64,
4114 available_width: f64,
4115 font_context: &FontContext,
4116 source_location: Option<&SourceLocation>,
4117 bookmark: Option<&str>,
4118 node_type_override: Option<&str>,
4122 ) {
4123 if style.border_width.top > 0.0
4130 || style.border_width.right > 0.0
4131 || style.border_width.bottom > 0.0
4132 || style.border_width.left > 0.0
4133 {
4134 self.defect(
4135 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4136 );
4137 }
4138 if style.background_color.is_some() {
4139 self.defect(
4140 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4141 );
4142 }
4143 let margin = &style.margin.to_edges();
4144 let text_x = x + margin.left;
4145 let text_width = match style.width {
4152 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4153 SizeConstraint::Auto => available_width - margin.horizontal(),
4154 };
4155
4156 cursor.y += margin.top;
4157
4158 if !runs.is_empty() {
4160 self.layout_text_runs(
4161 runs,
4162 href,
4163 style,
4164 cursor,
4165 pages,
4166 text_x,
4167 text_width,
4168 font_context,
4169 source_location,
4170 bookmark,
4171 node_type_override,
4172 );
4173 cursor.y += margin.bottom;
4174 return;
4175 }
4176
4177 let content = substitute_page_placeholders(content);
4178 let transformed = apply_text_transform(&content, style.text_transform);
4179 let justify = matches!(style.text_align, TextAlign::Justify);
4180 let lines = match style.line_breaking {
4181 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4182 font_context,
4183 &transformed,
4184 text_width,
4185 style.font_size,
4186 &style.font_family,
4187 style.font_weight,
4188 style.font_style,
4189 style.letter_spacing,
4190 style.hyphens,
4191 style.lang.as_deref(),
4192 justify,
4193 ),
4194 LineBreaking::Greedy => self.text_layout.break_into_lines(
4195 font_context,
4196 &transformed,
4197 text_width,
4198 style.font_size,
4199 &style.font_family,
4200 style.font_weight,
4201 style.font_style,
4202 style.letter_spacing,
4203 style.hyphens,
4204 style.lang.as_deref(),
4205 ),
4206 };
4207
4208 let lines = match style.text_overflow {
4210 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4211 font_context,
4212 lines,
4213 text_width,
4214 style.font_size,
4215 &style.font_family,
4216 style.font_weight,
4217 style.font_style,
4218 style.letter_spacing,
4219 ),
4220 TextOverflow::Clip => self.text_layout.truncate_clip(
4221 font_context,
4222 lines,
4223 text_width,
4224 style.font_size,
4225 &style.font_family,
4226 style.font_weight,
4227 style.font_style,
4228 style.letter_spacing,
4229 ),
4230 TextOverflow::Wrap => lines,
4231 };
4232
4233 let line_height = style.font_size * style.line_height;
4234
4235 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4237 let decision = page_break::decide_break(
4238 cursor.remaining_height(),
4239 &line_heights,
4240 true,
4241 style.min_orphan_lines as usize,
4242 style.min_widow_lines as usize,
4243 );
4244
4245 let mut snapshot = cursor.elements.len();
4247 let mut container_start_y = cursor.content_y + cursor.y;
4248 let mut is_first_element = true;
4249
4250 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4252 pages.push(cursor.finalize());
4253 *cursor = cursor.new_page();
4254 snapshot = cursor.elements.len();
4255 container_start_y = cursor.content_y + cursor.y;
4256 }
4257
4258 let forced_break_at = match decision {
4260 page_break::BreakDecision::Split {
4261 items_on_current_page,
4262 } => Some(items_on_current_page),
4263 _ => None,
4264 };
4265 let mut first_break_done = false;
4266
4267 for (line_idx, line) in lines.iter().enumerate() {
4268 let needs_break = if let Some(break_at) = forced_break_at {
4270 if !first_break_done && line_idx == break_at {
4271 true
4272 } else {
4273 line_height > cursor.remaining_height()
4274 }
4275 } else {
4276 line_height > cursor.remaining_height()
4277 };
4278
4279 if needs_break {
4280 first_break_done = true;
4281 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4283 if !line_elements.is_empty() {
4284 let container_height = cursor.content_y + cursor.y - container_start_y;
4285 cursor.elements.push(LayoutElement {
4286 x: text_x,
4287 y: container_start_y,
4288 width: text_width,
4289 height: container_height,
4290 draw: DrawCommand::None,
4291 children: line_elements,
4292 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4293 resolved_style: Some(style.clone()),
4294 source_location: source_location.cloned(),
4295 href: href.map(|s| s.to_string()),
4296 bookmark: if is_first_element {
4297 bookmark.map(|s| s.to_string())
4298 } else {
4299 None
4300 },
4301 alt: None,
4302 is_header_row: false,
4303 col_span: 1,
4304 overflow: Overflow::default(),
4305 opacity: 1.0,
4306 });
4307 is_first_element = false;
4308 }
4309
4310 pages.push(cursor.finalize());
4311 *cursor = cursor.new_page();
4312
4313 snapshot = cursor.elements.len();
4315 container_start_y = cursor.content_y + cursor.y;
4316 }
4317
4318 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4319
4320 let rendered_width = if glyphs.is_empty() {
4324 line.width
4325 } else {
4326 let last = &glyphs[glyphs.len() - 1];
4327 (last.x_offset + last.x_advance).max(line.width * 0.5)
4328 };
4329
4330 let line_x = match style.text_align {
4331 TextAlign::Left => text_x,
4332 TextAlign::Right => text_x + text_width - rendered_width,
4333 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4334 TextAlign::Justify => text_x,
4335 };
4336
4337 let is_last_line = line_idx == lines.len() - 1;
4345 let user_ws = style.word_spacing;
4346 let (justified_width, word_spacing) =
4347 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4348 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4349 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4350 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4351 let s = glyphs[..=idx]
4352 .iter()
4353 .filter(|g| g.char_value == ' ')
4354 .count();
4355 (w, s)
4356 } else {
4357 (0.0, 0)
4358 };
4359 let slack = text_width - natural_width;
4360 let ws = if space_count > 0 && slack.abs() > 0.01 {
4361 slack / space_count as f64
4362 } else {
4363 0.0
4364 };
4365 (text_width, user_ws + ws)
4366 } else {
4367 (rendered_width, user_ws)
4368 };
4369
4370 let text_line = TextLine {
4371 x: line_x,
4372 y: cursor.content_y
4378 + cursor.y
4379 + (line_height - style.font_size) / 2.0
4380 + style.font_size,
4381 glyphs,
4382 width: justified_width,
4383 height: line_height,
4384 word_spacing,
4385 };
4386
4387 cursor.elements.push(LayoutElement {
4388 x: line_x,
4389 y: cursor.content_y + cursor.y,
4390 width: justified_width,
4391 height: line_height,
4392 draw: DrawCommand::Text {
4393 lines: vec![text_line],
4394 color: style.color,
4395 text_decoration: style.text_decoration,
4396 opacity: 1.0,
4397 },
4398 children: vec![],
4399 node_type: Some("TextLine".to_string()),
4400 resolved_style: Some(style.clone()),
4401 source_location: None,
4402 href: href.map(|s| s.to_string()),
4403 bookmark: None,
4404 alt: None,
4405 is_header_row: false,
4406 col_span: 1,
4407 overflow: Overflow::default(),
4408 opacity: 1.0,
4409 });
4410
4411 cursor.y += line_height;
4412 }
4413
4414 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4416 if !line_elements.is_empty() {
4417 let container_height = cursor.content_y + cursor.y - container_start_y;
4418 cursor.elements.push(LayoutElement {
4419 x: text_x,
4420 y: container_start_y,
4421 width: text_width,
4422 height: container_height,
4423 draw: DrawCommand::None,
4424 children: line_elements,
4425 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4426 resolved_style: Some(style.clone()),
4427 source_location: source_location.cloned(),
4428 href: href.map(|s| s.to_string()),
4429 bookmark: if is_first_element {
4430 bookmark.map(|s| s.to_string())
4431 } else {
4432 None
4433 },
4434 alt: None,
4435 is_header_row: false,
4436 col_span: 1,
4437 overflow: Overflow::default(),
4438 opacity: 1.0,
4439 });
4440 }
4441
4442 cursor.y += margin.bottom;
4443 }
4444
4445 #[allow(clippy::too_many_arguments)]
4447 #[allow(clippy::too_many_arguments)]
4448 fn layout_text_runs(
4449 &self,
4450 runs: &[TextRun],
4451 parent_href: Option<&str>,
4452 style: &ResolvedStyle,
4453 cursor: &mut PageCursor,
4454 pages: &mut Vec<LayoutPage>,
4455 text_x: f64,
4456 text_width: f64,
4457 font_context: &FontContext,
4458 source_location: Option<&SourceLocation>,
4459 bookmark: Option<&str>,
4460 node_type_override: Option<&str>,
4462 ) {
4463 let mut styled_chars: Vec<StyledChar> = Vec::new();
4465 for run in runs {
4466 let run_style = run.style.resolve(Some(style), text_width);
4467 let run_href = run.href.as_deref().or(parent_href);
4468 let transform = run_style.text_transform;
4469 let run_content = substitute_page_placeholders(&run.content);
4470 let mut prev_is_whitespace = true;
4471 for ch in run_content.chars() {
4472 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4473 prev_is_whitespace = ch.is_whitespace();
4474 styled_chars.push(StyledChar {
4475 ch: transformed_ch,
4476 font_family: run_style.font_family.clone(),
4477 font_size: run_style.font_size,
4478 font_weight: run_style.font_weight,
4479 font_style: run_style.font_style,
4480 color: run_style.color,
4481 href: run_href.map(|s| s.to_string()),
4482 text_decoration: run_style.text_decoration,
4483 letter_spacing: run_style.letter_spacing,
4484 });
4485 }
4486 }
4487
4488 let justify = matches!(style.text_align, TextAlign::Justify);
4490 let broken_lines = match style.line_breaking {
4491 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4492 font_context,
4493 &styled_chars,
4494 text_width,
4495 style.hyphens,
4496 style.lang.as_deref(),
4497 justify,
4498 ),
4499 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4500 font_context,
4501 &styled_chars,
4502 text_width,
4503 style.hyphens,
4504 style.lang.as_deref(),
4505 ),
4506 };
4507
4508 let broken_lines = match style.text_overflow {
4510 TextOverflow::Ellipsis => {
4511 self.text_layout
4512 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4513 }
4514 TextOverflow::Clip => {
4515 self.text_layout
4516 .truncate_runs_clip(font_context, broken_lines, text_width)
4517 }
4518 TextOverflow::Wrap => broken_lines,
4519 };
4520
4521 let line_height = style.font_size * style.line_height;
4522
4523 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4525 let decision = page_break::decide_break(
4526 cursor.remaining_height(),
4527 &line_heights,
4528 true,
4529 style.min_orphan_lines as usize,
4530 style.min_widow_lines as usize,
4531 );
4532
4533 let mut snapshot = cursor.elements.len();
4534 let mut container_start_y = cursor.content_y + cursor.y;
4535 let mut is_first_element = true;
4536
4537 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4538 pages.push(cursor.finalize());
4539 *cursor = cursor.new_page();
4540 snapshot = cursor.elements.len();
4541 container_start_y = cursor.content_y + cursor.y;
4542 }
4543
4544 let forced_break_at = match decision {
4545 page_break::BreakDecision::Split {
4546 items_on_current_page,
4547 } => Some(items_on_current_page),
4548 _ => None,
4549 };
4550 let mut first_break_done = false;
4551
4552 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4553 let needs_break = if let Some(break_at) = forced_break_at {
4554 if !first_break_done && line_idx == break_at {
4555 true
4556 } else {
4557 line_height > cursor.remaining_height()
4558 }
4559 } else {
4560 line_height > cursor.remaining_height()
4561 };
4562
4563 if needs_break {
4564 first_break_done = true;
4565 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4566 if !line_elements.is_empty() {
4567 let container_height = cursor.content_y + cursor.y - container_start_y;
4568 cursor.elements.push(LayoutElement {
4569 x: text_x,
4570 y: container_start_y,
4571 width: text_width,
4572 height: container_height,
4573 draw: DrawCommand::None,
4574 children: line_elements,
4575 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4576 resolved_style: Some(style.clone()),
4577 source_location: source_location.cloned(),
4578 href: parent_href.map(|s| s.to_string()),
4579 bookmark: if is_first_element {
4580 bookmark.map(|s| s.to_string())
4581 } else {
4582 None
4583 },
4584 alt: None,
4585 is_header_row: false,
4586 col_span: 1,
4587 overflow: Overflow::default(),
4588 opacity: 1.0,
4589 });
4590 is_first_element = false;
4591 }
4592
4593 pages.push(cursor.finalize());
4594 *cursor = cursor.new_page();
4595
4596 snapshot = cursor.elements.len();
4597 container_start_y = cursor.content_y + cursor.y;
4598 }
4599
4600 let line_x = match style.text_align {
4601 TextAlign::Left => text_x,
4602 TextAlign::Right => text_x + text_width - run_line.width,
4603 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
4604 TextAlign::Justify => text_x,
4605 };
4606
4607 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
4608
4609 let is_last_line = line_idx == broken_lines.len() - 1;
4616 let user_ws = style.word_spacing;
4617 let (justified_width, word_spacing) =
4618 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4619 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4620 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4621 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4622 let s = glyphs[..=idx]
4623 .iter()
4624 .filter(|g| g.char_value == ' ')
4625 .count();
4626 (w, s)
4627 } else {
4628 (0.0, 0)
4629 };
4630 let slack = text_width - natural_width;
4631 let ws = if space_count > 0 && slack.abs() > 0.01 {
4632 slack / space_count as f64
4633 } else {
4634 0.0
4635 };
4636 (text_width, user_ws + ws)
4637 } else {
4638 (run_line.width, user_ws)
4639 };
4640
4641 let text_line = TextLine {
4642 x: line_x,
4643 y: cursor.content_y
4649 + cursor.y
4650 + (line_height - style.font_size) / 2.0
4651 + style.font_size,
4652 glyphs,
4653 width: justified_width,
4654 height: line_height,
4655 word_spacing,
4656 };
4657
4658 let text_dec = run_line
4660 .chars
4661 .iter()
4662 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4663 .map(|sc| sc.text_decoration)
4664 .unwrap_or(style.text_decoration);
4665
4666 cursor.elements.push(LayoutElement {
4667 x: line_x,
4668 y: cursor.content_y + cursor.y,
4669 width: justified_width,
4670 height: line_height,
4671 draw: DrawCommand::Text {
4672 lines: vec![text_line],
4673 color: style.color,
4674 text_decoration: text_dec,
4675 opacity: 1.0,
4676 },
4677 children: vec![],
4678 node_type: Some("TextLine".to_string()),
4679 resolved_style: Some(style.clone()),
4680 source_location: None,
4681 href: parent_href.map(|s| s.to_string()),
4682 bookmark: None,
4683 alt: None,
4684 is_header_row: false,
4685 col_span: 1,
4686 overflow: Overflow::default(),
4687 opacity: 1.0,
4688 });
4689
4690 cursor.y += line_height;
4691 }
4692
4693 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4694 if !line_elements.is_empty() {
4695 let container_height = cursor.content_y + cursor.y - container_start_y;
4696 cursor.elements.push(LayoutElement {
4697 x: text_x,
4698 y: container_start_y,
4699 width: text_width,
4700 height: container_height,
4701 draw: DrawCommand::None,
4702 children: line_elements,
4703 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4704 resolved_style: Some(style.clone()),
4705 source_location: source_location.cloned(),
4706 href: parent_href.map(|s| s.to_string()),
4707 bookmark: if is_first_element {
4708 bookmark.map(|s| s.to_string())
4709 } else {
4710 None
4711 },
4712 alt: None,
4713 is_header_row: false,
4714 col_span: 1,
4715 overflow: Overflow::default(),
4716 opacity: 1.0,
4717 });
4718 }
4719 }
4720
4721 fn build_positioned_glyphs_single_style(
4725 &self,
4726 line: &BrokenLine,
4727 style: &ResolvedStyle,
4728 href: Option<&str>,
4729 font_context: &FontContext,
4730 ) -> Vec<PositionedGlyph> {
4731 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4732 let line_text: String = line.chars.iter().collect();
4733 let direction = style.direction;
4734 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4736
4737 let font_runs = crate::font::fallback::segment_by_font(
4740 &line.chars,
4741 &style.font_family,
4742 style.font_weight,
4743 italic,
4744 font_context.registry(),
4745 );
4746 let needs_per_char_fallback = font_runs.len() > 1
4747 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4748
4749 if needs_per_char_fallback {
4751 let bidi_runs = if has_bidi {
4752 bidi::analyze_bidi(&line_text, direction)
4753 } else {
4754 vec![crate::text::bidi::BidiRun {
4755 char_start: 0,
4756 char_end: line.chars.len(),
4757 level: unicode_bidi::Level::ltr(),
4758 is_rtl: false,
4759 }]
4760 };
4761
4762 let mut all_glyphs = Vec::new();
4763 let mut bidi_levels = Vec::new();
4764 let mut x = 0.0_f64;
4765
4766 for bidi_run in &bidi_runs {
4768 for font_run in &font_runs {
4770 let start = font_run.start.max(bidi_run.char_start);
4772 let end = font_run.end.min(bidi_run.char_end);
4773 if start >= end {
4774 continue;
4775 }
4776
4777 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4778 let sub_text: String = sub_chars.iter().collect();
4779 let resolved_family = &font_run.family;
4780
4781 if let Some(font_data) =
4782 font_context.font_data(resolved_family, style.font_weight, italic)
4783 {
4784 if let Some(shaped) = shaping::shape_text_with_direction(
4785 &sub_text,
4786 font_data,
4787 bidi_run.is_rtl,
4788 ) {
4789 let units_per_em = font_context.units_per_em(
4790 resolved_family,
4791 style.font_weight,
4792 italic,
4793 );
4794 let scale = style.font_size / units_per_em as f64;
4795
4796 for sg in &shaped {
4797 let cluster = sg.cluster as usize;
4798 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4799
4800 let cluster_text = if shaped.len() < sub_chars.len() {
4801 let cluster_end =
4802 self.find_cluster_end(&shaped, sg, sub_chars.len());
4803 if cluster_end > cluster + 1 {
4804 Some(
4805 sub_chars[cluster..cluster_end]
4806 .iter()
4807 .collect::<String>(),
4808 )
4809 } else {
4810 None
4811 }
4812 } else {
4813 None
4814 };
4815
4816 let glyph_x = x + sg.x_offset as f64 * scale;
4817 let glyph_y = sg.y_offset as f64 * scale;
4818 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4819
4820 all_glyphs.push(PositionedGlyph {
4821 glyph_id: sg.glyph_id,
4822 x_offset: glyph_x,
4823 y_offset: glyph_y,
4824 x_advance: advance,
4825 font_size: style.font_size,
4826 font_family: Arc::from(resolved_family.as_str()),
4827 font_weight: style.font_weight,
4828 font_style: style.font_style,
4829 char_value,
4830 color: Some(style.color),
4831 href: href.map(|s| s.to_string()),
4832 text_decoration: style.text_decoration,
4833 letter_spacing: style.letter_spacing,
4834 cluster_text,
4835 });
4836 bidi_levels.push(bidi_run.level);
4837 x += advance;
4838 }
4839 continue;
4840 }
4841 }
4842
4843 for i in start..end {
4845 let ch = line.chars[i];
4846 let glyph_x = x;
4847 let char_width = font_context.char_width(
4848 ch,
4849 resolved_family,
4850 style.font_weight,
4851 italic,
4852 style.font_size,
4853 );
4854 let advance = char_width + style.letter_spacing;
4855 all_glyphs.push(PositionedGlyph {
4856 glyph_id: ch as u16,
4857 x_offset: glyph_x,
4858 y_offset: 0.0,
4859 x_advance: advance,
4860 font_size: style.font_size,
4861 font_family: Arc::from(resolved_family.as_str()),
4862 font_weight: style.font_weight,
4863 font_style: style.font_style,
4864 char_value: ch,
4865 color: Some(style.color),
4866 href: href.map(|s| s.to_string()),
4867 text_decoration: style.text_decoration,
4868 letter_spacing: style.letter_spacing,
4869 cluster_text: None,
4870 });
4871 bidi_levels.push(bidi_run.level);
4872 x += advance;
4873 }
4874 }
4875 }
4876
4877 if has_bidi && !all_glyphs.is_empty() {
4879 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4880 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
4881 }
4882 return all_glyphs;
4883 }
4884
4885 if let Some(font_data) =
4888 font_context.font_data(&style.font_family, style.font_weight, italic)
4889 {
4890 if has_bidi {
4891 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4893 let units_per_em =
4894 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4895 let scale = style.font_size / units_per_em as f64;
4896
4897 let mut all_glyphs = Vec::new();
4898 let mut bidi_levels = Vec::new();
4899 let mut x = 0.0_f64;
4900
4901 for run in &bidi_runs {
4902 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
4903 let run_text: String = run_chars.iter().collect();
4904
4905 if let Some(shaped) =
4906 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
4907 {
4908 for sg in &shaped {
4909 let cluster = sg.cluster as usize;
4910 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
4911
4912 let cluster_text = if shaped.len() < run_chars.len() {
4913 let cluster_end =
4914 self.find_cluster_end(&shaped, sg, run_chars.len());
4915 if cluster_end > cluster + 1 {
4916 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
4917 } else {
4918 None
4919 }
4920 } else {
4921 None
4922 };
4923
4924 let glyph_x = x + sg.x_offset as f64 * scale;
4925 let glyph_y = sg.y_offset as f64 * scale;
4926 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4927
4928 all_glyphs.push(PositionedGlyph {
4929 glyph_id: sg.glyph_id,
4930 x_offset: glyph_x,
4931 y_offset: glyph_y,
4932 x_advance: advance,
4933 font_size: style.font_size,
4934 font_family: Arc::from(style.font_family.as_str()),
4935 font_weight: style.font_weight,
4936 font_style: style.font_style,
4937 char_value,
4938 color: Some(style.color),
4939 href: href.map(|s| s.to_string()),
4940 text_decoration: style.text_decoration,
4941 letter_spacing: style.letter_spacing,
4942 cluster_text,
4943 });
4944 bidi_levels.push(run.level);
4945
4946 x += advance;
4947 }
4948 }
4949 }
4950
4951 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4953 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4954 return glyphs;
4955 }
4956
4957 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
4959 let units_per_em =
4960 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4961 let scale = style.font_size / units_per_em as f64;
4962
4963 return self.shaped_glyphs_to_positioned(
4964 &shaped,
4965 &line.chars,
4966 &line.char_positions,
4967 scale,
4968 style.font_size,
4969 &style.font_family,
4970 style.font_weight,
4971 style.font_style,
4972 Some(style.color),
4973 href,
4974 style.text_decoration,
4975 style.letter_spacing,
4976 );
4977 }
4978 }
4979
4980 let mut glyphs: Vec<PositionedGlyph> = line
4982 .chars
4983 .iter()
4984 .enumerate()
4985 .map(|(j, ch)| {
4986 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
4987 let char_width = font_context.char_width(
4988 *ch,
4989 &style.font_family,
4990 style.font_weight,
4991 italic,
4992 style.font_size,
4993 );
4994 PositionedGlyph {
4995 glyph_id: *ch as u16,
4996 x_offset: glyph_x,
4997 y_offset: 0.0,
4998 x_advance: char_width,
4999 font_size: style.font_size,
5000 font_family: Arc::from(style.font_family.as_str()),
5001 font_weight: style.font_weight,
5002 font_style: style.font_style,
5003 char_value: *ch,
5004 color: Some(style.color),
5005 href: href.map(|s| s.to_string()),
5006 text_decoration: style.text_decoration,
5007 letter_spacing: style.letter_spacing,
5008 cluster_text: None,
5009 }
5010 })
5011 .collect();
5012
5013 if has_bidi && !glyphs.is_empty() {
5015 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5016 let mut levels = Vec::with_capacity(glyphs.len());
5017 let mut char_idx = 0;
5018 for run in &bidi_runs {
5019 for _ in run.char_start..run.char_end {
5020 if char_idx < glyphs.len() {
5021 levels.push(run.level);
5022 char_idx += 1;
5023 }
5024 }
5025 }
5026 while levels.len() < glyphs.len() {
5028 levels.push(unicode_bidi::Level::ltr());
5029 }
5030 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
5031 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5032 }
5033
5034 glyphs
5035 }
5036
5037 fn build_positioned_glyphs_runs(
5042 &self,
5043 run_line: &RunBrokenLine,
5044 font_context: &FontContext,
5045 direction: Direction,
5046 ) -> Vec<PositionedGlyph> {
5047 let chars = &run_line.chars;
5048 if chars.is_empty() {
5049 return vec![];
5050 }
5051
5052 let resolved_families: Vec<String> = chars
5061 .iter()
5062 .map(|sc| {
5063 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5064 if !sc.font_family.contains(',') {
5065 let primary =
5066 font_context
5067 .registry()
5068 .resolve(&sc.font_family, sc.font_weight, italic);
5069 if sc.ch.is_whitespace()
5070 || sc.ch == PAGE_NUMBER_SENTINEL
5071 || sc.ch == TOTAL_PAGES_SENTINEL
5072 || primary.has_char(sc.ch)
5073 {
5074 return sc.font_family.clone();
5075 }
5076 }
5077 let (_, family) = font_context.registry().resolve_for_char(
5078 &sc.font_family,
5079 sc.ch,
5080 sc.font_weight,
5081 italic,
5082 );
5083 family
5084 })
5085 .collect();
5086
5087 let line_text: String = chars.iter().map(|c| c.ch).collect();
5088 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5089 let bidi_runs = if has_bidi {
5090 Some(bidi::analyze_bidi(&line_text, direction))
5091 } else {
5092 None
5093 };
5094
5095 let mut glyphs = Vec::new();
5096 let mut bidi_levels = Vec::new();
5097 let mut i = 0;
5098
5099 while i < chars.len() {
5100 let sc = &chars[i];
5101 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5102 let resolved_family = &resolved_families[i];
5103
5104 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5106 runs.iter()
5107 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5108 });
5109
5110 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5112 {
5113 let run_start = i;
5115 let mut run_end = i + 1;
5116 while run_end < chars.len() {
5117 let next = &chars[run_end];
5118 let next_italic =
5119 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5120 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5121 runs.iter()
5122 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5123 });
5124 if resolved_families[run_end] == *resolved_family
5126 && next.font_weight == sc.font_weight
5127 && next_italic == italic
5128 && (next.font_size - sc.font_size).abs() < 0.001
5129 && next_is_rtl == is_rtl
5130 {
5131 run_end += 1;
5132 } else {
5133 break;
5134 }
5135 }
5136
5137 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5138 if let Some(shaped) =
5139 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5140 {
5141 let units_per_em =
5142 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5143 let scale = sc.font_size / units_per_em as f64;
5144
5145 let run_chars: Vec<char> =
5147 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5148 let run_positions: Vec<f64> = (run_start..run_end)
5149 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5150 .collect();
5151
5152 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5154 &shaped,
5155 &chars[run_start..run_end],
5156 &run_chars,
5157 &run_positions,
5158 scale,
5159 );
5160 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5162 for g in &mut run_glyphs {
5163 g.font_family = resolved_family_arc.clone();
5164 }
5165 let run_level = if is_rtl {
5167 unicode_bidi::Level::rtl()
5168 } else {
5169 unicode_bidi::Level::ltr()
5170 };
5171 for _ in &run_glyphs {
5172 bidi_levels.push(run_level);
5173 }
5174 glyphs.extend(run_glyphs);
5175 i = run_end;
5176 continue;
5177 }
5178 }
5179
5180 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5182 let char_width = font_context.char_width(
5183 sc.ch,
5184 resolved_family,
5185 sc.font_weight,
5186 italic,
5187 sc.font_size,
5188 );
5189 glyphs.push(PositionedGlyph {
5190 glyph_id: sc.ch as u16,
5191 x_offset: glyph_x,
5192 y_offset: 0.0,
5193 x_advance: char_width,
5194 font_size: sc.font_size,
5195 font_family: Arc::from(resolved_family.as_str()),
5196 font_weight: sc.font_weight,
5197 font_style: sc.font_style,
5198 char_value: sc.ch,
5199 color: Some(sc.color),
5200 href: sc.href.clone(),
5201 text_decoration: sc.text_decoration,
5202 letter_spacing: sc.letter_spacing,
5203 cluster_text: None,
5204 });
5205 bidi_levels.push(if is_rtl {
5206 unicode_bidi::Level::rtl()
5207 } else {
5208 unicode_bidi::Level::ltr()
5209 });
5210 i += 1;
5211 }
5212
5213 if has_bidi && !glyphs.is_empty() {
5215 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
5216 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5217 }
5218
5219 glyphs
5220 }
5221
5222 #[allow(clippy::too_many_arguments)]
5224 fn shaped_glyphs_to_positioned(
5225 &self,
5226 shaped: &[shaping::ShapedGlyph],
5227 chars: &[char],
5228 _char_positions: &[f64],
5229 scale: f64,
5230 font_size: f64,
5231 font_family: &str,
5232 font_weight: u32,
5233 font_style: FontStyle,
5234 color: Option<Color>,
5235 href: Option<&str>,
5236 text_decoration: TextDecoration,
5237 letter_spacing: f64,
5238 ) -> Vec<PositionedGlyph> {
5239 let mut result = Vec::with_capacity(shaped.len());
5240 let mut x = 0.0_f64;
5241
5242 for sg in shaped {
5243 let cluster = sg.cluster as usize;
5244 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5245
5246 let cluster_text = if shaped.len() < chars.len() {
5248 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5251 if cluster_end > cluster + 1 {
5252 Some(chars[cluster..cluster_end].iter().collect::<String>())
5253 } else {
5254 None
5255 }
5256 } else {
5257 None
5258 };
5259
5260 let glyph_x = x + sg.x_offset as f64 * scale;
5262 let glyph_y = sg.y_offset as f64 * scale;
5263 let advance = sg.x_advance as f64 * scale + letter_spacing;
5264
5265 result.push(PositionedGlyph {
5266 glyph_id: sg.glyph_id,
5267 x_offset: glyph_x,
5268 y_offset: glyph_y,
5269 x_advance: advance,
5270 font_size,
5271 font_family: Arc::from(font_family),
5272 font_weight,
5273 font_style,
5274 char_value,
5275 color,
5276 href: href.map(|s| s.to_string()),
5277 text_decoration,
5278 letter_spacing,
5279 cluster_text,
5280 });
5281
5282 x += advance;
5283 }
5284
5285 result
5286 }
5287
5288 fn shaped_glyphs_to_positioned_runs(
5290 &self,
5291 shaped: &[shaping::ShapedGlyph],
5292 styled_chars: &[StyledChar],
5293 chars: &[char],
5294 char_positions: &[f64],
5295 scale: f64,
5296 ) -> Vec<PositionedGlyph> {
5297 let mut result = Vec::with_capacity(shaped.len());
5298 let base_x = char_positions.first().copied().unwrap_or(0.0);
5300 let mut x = 0.0_f64;
5301
5302 for sg in shaped {
5303 let cluster = sg.cluster as usize;
5304 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5305 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5306
5307 let cluster_text = if shaped.len() < chars.len() {
5308 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5309 if cluster_end > cluster + 1 {
5310 Some(chars[cluster..cluster_end].iter().collect::<String>())
5311 } else {
5312 None
5313 }
5314 } else {
5315 None
5316 };
5317
5318 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5319 let glyph_y = sg.y_offset as f64 * scale;
5320 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5321
5322 result.push(PositionedGlyph {
5323 glyph_id: sg.glyph_id,
5324 x_offset: glyph_x,
5325 y_offset: glyph_y,
5326 x_advance: advance,
5327 font_size: sc.font_size,
5328 font_family: Arc::from(sc.font_family.as_str()),
5329 font_weight: sc.font_weight,
5330 font_style: sc.font_style,
5331 char_value,
5332 color: Some(sc.color),
5333 href: sc.href.clone(),
5334 text_decoration: sc.text_decoration,
5335 letter_spacing: sc.letter_spacing,
5336 cluster_text,
5337 });
5338
5339 x += advance;
5340 }
5341
5342 result
5343 }
5344
5345 fn find_cluster_end(
5347 &self,
5348 shaped: &[shaping::ShapedGlyph],
5349 current: &shaping::ShapedGlyph,
5350 num_chars: usize,
5351 ) -> usize {
5352 for sg in shaped {
5354 if sg.cluster > current.cluster {
5355 return sg.cluster as usize;
5356 }
5357 }
5358 num_chars
5360 }
5361
5362 fn image_display_size(
5372 &self,
5373 src: &str,
5374 style: &ResolvedStyle,
5375 explicit_width: Option<f64>,
5376 explicit_height: Option<f64>,
5377 available_width: f64,
5378 ) -> (f64, f64) {
5379 let dims = if src.is_empty() {
5380 None
5381 } else {
5382 self.get_image_dimensions(src)
5383 };
5384 let aspect = dims
5385 .map(|(w, h)| {
5386 if w > 0 {
5387 f64::from(h) / f64::from(w)
5388 } else {
5389 0.75
5390 }
5391 })
5392 .unwrap_or(0.75);
5393
5394 let style_w = match style.width {
5395 SizeConstraint::Fixed(w) => Some(w),
5396 SizeConstraint::Auto => None,
5397 };
5398 let style_h = match style.height {
5399 SizeConstraint::Fixed(h) => Some(h),
5400 SizeConstraint::Auto => None,
5401 };
5402 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5403
5404 let width_source = style_w.or(explicit_width);
5405 let height_source = style_h.or(explicit_height);
5406 match (width_source, height_source) {
5407 (Some(w), Some(h)) => (clamp(w), h),
5408 (Some(w), None) => {
5409 let w = clamp(w);
5410 (w, w * aspect)
5411 }
5412 (None, Some(h)) => (clamp(h / aspect), h),
5413 (None, None) => {
5414 let w = clamp(
5417 dims.map(|(w, _)| f64::from(w))
5418 .unwrap_or(available_width)
5419 .min(available_width),
5420 );
5421 (w, w * aspect)
5422 }
5423 }
5424 }
5425
5426 #[allow(clippy::too_many_arguments)]
5427 fn layout_image(
5428 &self,
5429 node: &Node,
5430 style: &ResolvedStyle,
5431 cursor: &mut PageCursor,
5432 pages: &mut Vec<LayoutPage>,
5433 x: f64,
5434 available_width: f64,
5435 explicit_width: Option<f64>,
5436 explicit_height: Option<f64>,
5437 ) {
5438 let margin = &style.margin.to_edges();
5439
5440 let src = match &node.kind {
5442 NodeKind::Image { src, .. } => src.as_str(),
5443 _ => "",
5444 };
5445
5446 let loaded = if !src.is_empty() {
5447 crate::image_loader::load_image(src).ok()
5448 } else {
5449 None
5450 };
5451
5452 let (img_width, img_height) = self.image_display_size(
5453 src,
5454 style,
5455 explicit_width,
5456 explicit_height,
5457 available_width - margin.horizontal(),
5458 );
5459
5460 let total_height = img_height + margin.vertical();
5461
5462 if total_height > cursor.remaining_height() {
5463 pages.push(cursor.finalize());
5464 *cursor = cursor.new_page();
5465 }
5466
5467 cursor.y += margin.top;
5468
5469 let draw = if let Some(image_data) = loaded {
5470 DrawCommand::Image { image_data }
5471 } else {
5472 DrawCommand::ImagePlaceholder
5473 };
5474
5475 cursor.elements.push(LayoutElement {
5476 x: x + margin.left,
5477 y: cursor.content_y + cursor.y,
5478 width: img_width,
5479 height: img_height,
5480 draw,
5481 children: vec![],
5482 node_type: Some(node_kind_name(&node.kind).to_string()),
5483 resolved_style: Some(style.clone()),
5484 source_location: node.source_location.clone(),
5485 href: node.href.clone(),
5486 bookmark: node.bookmark.clone(),
5487 alt: node.alt.clone(),
5488 is_header_row: false,
5489 col_span: 1,
5490 overflow: style.overflow,
5491 opacity: style.opacity,
5492 });
5493
5494 cursor.y += img_height + margin.bottom;
5495 }
5496
5497 #[allow(clippy::too_many_arguments)]
5499 fn layout_svg(
5500 &self,
5501 node: &Node,
5502 style: &ResolvedStyle,
5503 cursor: &mut PageCursor,
5504 pages: &mut Vec<LayoutPage>,
5505 x: f64,
5506 _available_width: f64,
5507 svg_width: f64,
5508 svg_height: f64,
5509 view_box: Option<&str>,
5510 content: &str,
5511 ) {
5512 let margin = &style.margin.to_edges();
5513 let total_height = svg_height + margin.vertical();
5514
5515 if total_height > cursor.remaining_height() {
5516 pages.push(cursor.finalize());
5517 *cursor = cursor.new_page();
5518 }
5519
5520 cursor.y += margin.top;
5521
5522 let vb = view_box
5523 .and_then(crate::svg::parse_view_box)
5524 .unwrap_or(crate::svg::ViewBox {
5525 min_x: 0.0,
5526 min_y: 0.0,
5527 width: svg_width,
5528 height: svg_height,
5529 });
5530
5531 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5532
5533 cursor.elements.push(LayoutElement {
5534 x: x + margin.left,
5535 y: cursor.content_y + cursor.y,
5536 width: svg_width,
5537 height: svg_height,
5538 draw: DrawCommand::Svg {
5539 commands,
5540 width: svg_width,
5541 height: svg_height,
5542 viewbox_min_x: vb.min_x,
5543 viewbox_min_y: vb.min_y,
5544 viewbox_width: vb.width,
5545 viewbox_height: vb.height,
5546 clip: false,
5547 },
5548 children: vec![],
5549 node_type: Some("Svg".to_string()),
5550 resolved_style: Some(style.clone()),
5551 source_location: node.source_location.clone(),
5552 href: node.href.clone(),
5553 bookmark: node.bookmark.clone(),
5554 alt: node.alt.clone(),
5555 is_header_row: false,
5556 col_span: 1,
5557 overflow: style.overflow,
5558 opacity: style.opacity,
5559 });
5560
5561 cursor.y += svg_height + margin.bottom;
5562 }
5563
5564 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
5566 use crate::svg::SvgCommand;
5567
5568 let mut commands = Vec::new();
5569 let mut cur_x = 0.0_f64;
5570 let mut cur_y = 0.0_f64;
5571
5572 for op in operations {
5573 match op {
5574 CanvasOp::MoveTo { x, y } => {
5575 commands.push(SvgCommand::MoveTo(*x, *y));
5576 cur_x = *x;
5577 cur_y = *y;
5578 }
5579 CanvasOp::LineTo { x, y } => {
5580 commands.push(SvgCommand::LineTo(*x, *y));
5581 cur_x = *x;
5582 cur_y = *y;
5583 }
5584 CanvasOp::BezierCurveTo {
5585 cp1x,
5586 cp1y,
5587 cp2x,
5588 cp2y,
5589 x,
5590 y,
5591 } => {
5592 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
5593 cur_x = *x;
5594 cur_y = *y;
5595 }
5596 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
5597 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
5599 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
5600 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
5601 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
5602 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
5603 cur_x = *x;
5604 cur_y = *y;
5605 }
5606 CanvasOp::ClosePath => {
5607 commands.push(SvgCommand::ClosePath);
5608 }
5609 CanvasOp::Rect {
5610 x,
5611 y,
5612 width,
5613 height,
5614 } => {
5615 commands.push(SvgCommand::MoveTo(*x, *y));
5616 commands.push(SvgCommand::LineTo(*x + *width, *y));
5617 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
5618 commands.push(SvgCommand::LineTo(*x, *y + *height));
5619 commands.push(SvgCommand::ClosePath);
5620 cur_x = *x;
5621 cur_y = *y;
5622 }
5623 CanvasOp::Circle { cx, cy, r } => {
5624 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
5625 }
5626 CanvasOp::Ellipse { cx, cy, rx, ry } => {
5627 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
5628 }
5629 CanvasOp::Arc {
5630 cx,
5631 cy,
5632 r,
5633 start_angle,
5634 end_angle,
5635 counterclockwise,
5636 } => {
5637 let steps = 32;
5641 let mut sweep = end_angle - start_angle;
5642 if !counterclockwise && sweep < 0.0 {
5643 sweep += 2.0 * std::f64::consts::PI;
5644 }
5645 if *counterclockwise && sweep > 0.0 {
5646 sweep -= 2.0 * std::f64::consts::PI;
5647 }
5648 for i in 0..=steps {
5649 let t = *start_angle + sweep * (i as f64 / steps as f64);
5650 let px = cx + r * t.cos();
5651 let py = cy + r * t.sin();
5652 if i == 0 {
5653 commands.push(SvgCommand::MoveTo(px, py));
5654 } else {
5655 commands.push(SvgCommand::LineTo(px, py));
5656 }
5657 }
5658 }
5659 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
5660 CanvasOp::Fill => commands.push(SvgCommand::Fill),
5661 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
5662 CanvasOp::SetFillColor { r, g, b } => {
5663 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
5665 }
5666 CanvasOp::SetStrokeColor { r, g, b } => {
5667 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
5668 }
5669 CanvasOp::SetLineWidth { width } => {
5670 commands.push(SvgCommand::SetStrokeWidth(*width));
5671 }
5672 CanvasOp::SetLineCap { cap } => {
5673 commands.push(SvgCommand::SetLineCap(*cap));
5674 }
5675 CanvasOp::SetLineJoin { join } => {
5676 commands.push(SvgCommand::SetLineJoin(*join));
5677 }
5678 CanvasOp::Save => commands.push(SvgCommand::SaveState),
5679 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
5680 }
5681 }
5682
5683 commands
5684 }
5685
5686 #[allow(clippy::too_many_arguments)]
5688 fn layout_canvas(
5689 &self,
5690 node: &Node,
5691 style: &ResolvedStyle,
5692 cursor: &mut PageCursor,
5693 pages: &mut Vec<LayoutPage>,
5694 x: f64,
5695 _available_width: f64,
5696 canvas_width: f64,
5697 canvas_height: f64,
5698 operations: &[CanvasOp],
5699 ) {
5700 let margin = style.margin.to_edges();
5701 let total_height = canvas_height + margin.top + margin.bottom;
5702
5703 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5705 pages.push(cursor.finalize());
5706 *cursor = cursor.new_page();
5707 }
5708
5709 cursor.y += margin.top;
5710
5711 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5712
5713 cursor.elements.push(LayoutElement {
5714 x: x + margin.left,
5715 y: cursor.content_y + cursor.y,
5716 width: canvas_width,
5717 height: canvas_height,
5718 draw: DrawCommand::Svg {
5719 commands: svg_commands,
5720 width: canvas_width,
5721 height: canvas_height,
5722 viewbox_min_x: 0.0,
5725 viewbox_min_y: 0.0,
5726 viewbox_width: canvas_width,
5727 viewbox_height: canvas_height,
5728 clip: true,
5729 },
5730 children: vec![],
5731 node_type: Some("Canvas".to_string()),
5732 resolved_style: Some(style.clone()),
5733 source_location: node.source_location.clone(),
5734 href: node.href.clone(),
5735 bookmark: node.bookmark.clone(),
5736 alt: node.alt.clone(),
5737 is_header_row: false,
5738 col_span: 1,
5739 overflow: style.overflow,
5740 opacity: style.opacity,
5741 });
5742
5743 cursor.y += canvas_height + margin.bottom;
5744 }
5745
5746 #[allow(clippy::too_many_arguments)]
5748 #[allow(clippy::too_many_arguments)]
5750 fn layout_chart(
5751 &self,
5752 node: &Node,
5753 style: &ResolvedStyle,
5754 cursor: &mut PageCursor,
5755 pages: &mut Vec<LayoutPage>,
5756 x: f64,
5757 chart_width: f64,
5758 chart_height: f64,
5759 primitives: Vec<crate::chart::ChartPrimitive>,
5760 node_type_name: &str,
5761 ) {
5762 let margin = &style.margin.to_edges();
5763 let total_height = chart_height + margin.vertical();
5764
5765 if total_height > cursor.remaining_height() {
5766 pages.push(cursor.finalize());
5767 *cursor = cursor.new_page();
5768 }
5769
5770 cursor.y += margin.top;
5771
5772 let draw = DrawCommand::Chart { primitives };
5773
5774 cursor.elements.push(LayoutElement {
5775 x: x + margin.left,
5776 y: cursor.content_y + cursor.y,
5777 width: chart_width,
5778 height: chart_height,
5779 draw,
5780 children: vec![],
5781 node_type: Some(node_type_name.to_string()),
5782 resolved_style: Some(style.clone()),
5783 source_location: node.source_location.clone(),
5784 href: node.href.clone(),
5785 bookmark: node.bookmark.clone(),
5786 alt: node.alt.clone(),
5787 is_header_row: false,
5788 col_span: 1,
5789 overflow: style.overflow,
5790 opacity: style.opacity,
5791 });
5792
5793 cursor.y += chart_height + margin.bottom;
5794 }
5795
5796 #[allow(clippy::too_many_arguments)]
5798 fn layout_form_field(
5799 &self,
5800 node: &Node,
5801 style: &ResolvedStyle,
5802 cursor: &mut PageCursor,
5803 pages: &mut Vec<LayoutPage>,
5804 x: f64,
5805 field_width: f64,
5806 field_height: f64,
5807 draw: DrawCommand,
5808 node_type_name: &str,
5809 ) {
5810 let margin = &style.margin.to_edges();
5811 let total_height = field_height + margin.vertical();
5812
5813 if total_height > cursor.remaining_height() {
5814 pages.push(cursor.finalize());
5815 *cursor = cursor.new_page();
5816 }
5817
5818 cursor.y += margin.top;
5819
5820 cursor.elements.push(LayoutElement {
5821 x: x + margin.left,
5822 y: cursor.content_y + cursor.y,
5823 width: field_width,
5824 height: field_height,
5825 draw,
5826 children: vec![],
5827 node_type: Some(node_type_name.to_string()),
5828 resolved_style: Some(style.clone()),
5829 source_location: node.source_location.clone(),
5830 href: node.href.clone(),
5831 bookmark: node.bookmark.clone(),
5832 alt: node.alt.clone(),
5833 is_header_row: false,
5834 col_span: 1,
5835 overflow: style.overflow,
5836 opacity: style.opacity,
5837 });
5838
5839 cursor.y += field_height + margin.bottom;
5840 }
5841
5842 #[allow(clippy::too_many_arguments)]
5843 fn layout_barcode(
5844 &self,
5845 node: &Node,
5846 style: &ResolvedStyle,
5847 cursor: &mut PageCursor,
5848 pages: &mut Vec<LayoutPage>,
5849 x: f64,
5850 available_width: f64,
5851 data: &str,
5852 format: crate::barcode::BarcodeFormat,
5853 explicit_width: Option<f64>,
5854 bar_height: f64,
5855 ) {
5856 let margin = &style.margin.to_edges();
5857 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5858 let total_height = bar_height + margin.vertical();
5859
5860 if total_height > cursor.remaining_height() {
5861 pages.push(cursor.finalize());
5862 *cursor = cursor.new_page();
5863 }
5864
5865 cursor.y += margin.top;
5866
5867 let draw = match crate::barcode::generate_barcode(data, format) {
5868 Ok(barcode_data) => {
5869 let bar_width = if barcode_data.bars.is_empty() {
5870 0.0
5871 } else {
5872 display_width / barcode_data.bars.len() as f64
5873 };
5874 DrawCommand::Barcode {
5875 bars: barcode_data.bars,
5876 bar_width,
5877 height: bar_height,
5878 color: style.color,
5879 }
5880 }
5881 Err(_) => DrawCommand::None,
5882 };
5883
5884 cursor.elements.push(LayoutElement {
5885 x: x + margin.left,
5886 y: cursor.content_y + cursor.y,
5887 width: display_width,
5888 height: bar_height,
5889 draw,
5890 children: vec![],
5891 node_type: Some("Barcode".to_string()),
5892 resolved_style: Some(style.clone()),
5893 source_location: node.source_location.clone(),
5894 href: node.href.clone(),
5895 bookmark: node.bookmark.clone(),
5896 alt: node.alt.clone(),
5897 is_header_row: false,
5898 col_span: 1,
5899 overflow: style.overflow,
5900 opacity: style.opacity,
5901 });
5902
5903 cursor.y += bar_height + margin.bottom;
5904 }
5905
5906 #[allow(clippy::too_many_arguments)]
5908 fn layout_qrcode(
5909 &self,
5910 node: &Node,
5911 style: &ResolvedStyle,
5912 cursor: &mut PageCursor,
5913 pages: &mut Vec<LayoutPage>,
5914 x: f64,
5915 available_width: f64,
5916 data: &str,
5917 explicit_size: Option<f64>,
5918 ) {
5919 let margin = &style.margin.to_edges();
5920 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
5921 let total_height = display_size + margin.vertical();
5922
5923 if total_height > cursor.remaining_height() {
5924 pages.push(cursor.finalize());
5925 *cursor = cursor.new_page();
5926 }
5927
5928 cursor.y += margin.top;
5929
5930 let draw = match crate::qrcode::generate_qr(data) {
5931 Ok(matrix) => {
5932 let module_size = display_size / matrix.size as f64;
5933 DrawCommand::QrCode {
5934 modules: matrix.modules,
5935 module_size,
5936 color: style.color,
5937 }
5938 }
5939 Err(_) => DrawCommand::None,
5940 };
5941
5942 cursor.elements.push(LayoutElement {
5943 x: x + margin.left,
5944 y: cursor.content_y + cursor.y,
5945 width: display_size,
5946 height: display_size,
5947 draw,
5948 children: vec![],
5949 node_type: Some("QrCode".to_string()),
5950 resolved_style: Some(style.clone()),
5951 source_location: node.source_location.clone(),
5952 href: node.href.clone(),
5953 bookmark: node.bookmark.clone(),
5954 alt: node.alt.clone(),
5955 is_header_row: false,
5956 col_span: 1,
5957 overflow: style.overflow,
5958 opacity: style.opacity,
5959 });
5960
5961 cursor.y += display_size + margin.bottom;
5962 }
5963
5964 fn measure_node_height(
5967 &self,
5968 node: &Node,
5969 available_width: f64,
5970 style: &ResolvedStyle,
5971 font_context: &FontContext,
5972 ) -> f64 {
5973 match &node.kind {
5974 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5979 let measure_width = match style.width {
5982 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
5983 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5984 };
5985 if !runs.is_empty() {
5992 let mut styled_chars: Vec<StyledChar> = Vec::new();
5993 for run in runs {
5994 let run_style = run.style.resolve(Some(style), measure_width);
5995 let transform = run_style.text_transform;
5996 let run_content = substitute_page_placeholders(&run.content);
5997 let mut prev_is_whitespace = true;
5998 for ch in run_content.chars() {
5999 let transformed_ch =
6000 apply_char_transform(ch, transform, prev_is_whitespace);
6001 prev_is_whitespace = ch.is_whitespace();
6002 styled_chars.push(StyledChar {
6003 ch: transformed_ch,
6004 font_family: run_style.font_family.clone(),
6005 font_size: run_style.font_size,
6006 font_weight: run_style.font_weight,
6007 font_style: run_style.font_style,
6008 color: run_style.color,
6009 href: None,
6010 text_decoration: run_style.text_decoration,
6011 letter_spacing: run_style.letter_spacing,
6012 });
6013 }
6014 }
6015 let justify = matches!(style.text_align, TextAlign::Justify);
6016 let broken_lines = match style.line_breaking {
6017 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6018 font_context,
6019 &styled_chars,
6020 measure_width,
6021 style.hyphens,
6022 style.lang.as_deref(),
6023 justify,
6024 ),
6025 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6026 font_context,
6027 &styled_chars,
6028 measure_width,
6029 style.hyphens,
6030 style.lang.as_deref(),
6031 ),
6032 };
6033 let line_height = style.font_size * style.line_height;
6034 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6035 } else {
6036 let content = substitute_page_placeholders(content);
6037 let transformed = apply_text_transform(&content, style.text_transform);
6038 let justify = matches!(style.text_align, TextAlign::Justify);
6039 let lines = match style.line_breaking {
6040 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6041 font_context,
6042 &transformed,
6043 measure_width,
6044 style.font_size,
6045 &style.font_family,
6046 style.font_weight,
6047 style.font_style,
6048 style.letter_spacing,
6049 style.hyphens,
6050 style.lang.as_deref(),
6051 justify,
6052 ),
6053 LineBreaking::Greedy => self.text_layout.break_into_lines(
6054 font_context,
6055 &transformed,
6056 measure_width,
6057 style.font_size,
6058 &style.font_family,
6059 style.font_weight,
6060 style.font_style,
6061 style.letter_spacing,
6062 style.hyphens,
6063 style.lang.as_deref(),
6064 ),
6065 };
6066 let line_height = style.font_size * style.line_height;
6067 (lines.len() as f64) * line_height + style.padding.vertical()
6068 }
6069 }
6070 NodeKind::Image {
6071 src,
6072 width: explicit_w,
6073 height: explicit_h,
6074 } => {
6075 let (_, h) = self.image_display_size(
6077 src,
6078 style,
6079 *explicit_w,
6080 *explicit_h,
6081 available_width - style.margin.horizontal(),
6082 );
6083 h + style.padding.vertical()
6084 }
6085 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6086 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6087 NodeKind::QrCode { size, .. } => {
6088 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6089 display_size + style.margin.vertical()
6090 }
6091 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6092 NodeKind::BarChart { height, .. }
6093 | NodeKind::LineChart { height, .. }
6094 | NodeKind::PieChart { height, .. }
6095 | NodeKind::AreaChart { height, .. }
6096 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6097 NodeKind::TextField { height, .. }
6098 | NodeKind::Checkbox { height, .. }
6099 | NodeKind::Dropdown { height, .. }
6100 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6101 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6103 if let SizeConstraint::Fixed(h) = style.height {
6110 return h;
6111 }
6112 let outer_width = match style.width {
6113 SizeConstraint::Fixed(w) => w,
6114 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6115 };
6116 let inner_width =
6117 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6118 let col_widths = self.resolve_column_widths(
6119 columns,
6120 inner_width,
6121 &node.children,
6122 style,
6123 font_context,
6124 );
6125 let row_gap = style.row_gap;
6126 let offsets = Self::table_column_offsets(&node.children);
6127 let mut total = 0.0;
6128 for (i, row) in node.children.iter().enumerate() {
6129 if i > 0 {
6130 total += row_gap;
6131 }
6132 total += self.measure_table_row_height(
6133 row,
6134 &col_widths,
6135 &offsets[i],
6136 style,
6137 font_context,
6138 );
6139 }
6140 total + style.padding.vertical() + style.border_width.vertical()
6141 }
6142 NodeKind::TableRow { .. } => {
6143 let n = node.children.len().max(1);
6148 let usable = (available_width - style.margin.horizontal()).max(0.0);
6149 let col_w = usable / n as f64;
6150 let col_widths = vec![col_w; n];
6151 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6152 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6153 }
6154 _ => {
6155 if let SizeConstraint::Fixed(h) = style.height {
6157 return h;
6158 }
6159 let outer_width = match style.width {
6163 SizeConstraint::Fixed(w) => w,
6164 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6165 }
6166 .min(style.max_width)
6167 .max(style.min_width);
6168 let inner_width =
6169 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6170 let children_height =
6171 self.measure_children_height(&node.children, inner_width, style, font_context);
6172 (children_height + style.padding.vertical() + style.border_width.vertical())
6173 .max(style.min_height)
6174 }
6175 }
6176 }
6177
6178 fn measure_children_height(
6179 &self,
6180 children: &[Node],
6181 available_width: f64,
6182 parent_style: &ResolvedStyle,
6183 font_context: &FontContext,
6184 ) -> f64 {
6185 if children
6191 .iter()
6192 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6193 {
6194 let flow: Vec<Node> = children
6195 .iter()
6196 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6197 .cloned()
6198 .collect();
6199 return self.measure_children_height(
6200 &flow,
6201 available_width,
6202 parent_style,
6203 font_context,
6204 );
6205 }
6206 if matches!(parent_style.display, Display::Grid) {
6208 if let Some(template_cols) = &parent_style.grid_template_columns {
6209 let num_columns = template_cols.len();
6210 if num_columns > 0 && !children.is_empty() {
6211 let col_gap = parent_style.column_gap;
6212 let row_gap = parent_style.row_gap;
6213
6214 let content_sizes: Vec<f64> = template_cols
6215 .iter()
6216 .map(|track| {
6217 if matches!(track, GridTrackSize::Auto) {
6218 available_width / num_columns as f64
6219 } else {
6220 0.0
6221 }
6222 })
6223 .collect();
6224
6225 let col_widths = grid::resolve_tracks(
6226 template_cols,
6227 available_width,
6228 col_gap,
6229 &content_sizes,
6230 );
6231
6232 let placements: Vec<Option<&GridPlacement>> = children
6233 .iter()
6234 .map(|child| child.style.grid_placement.as_ref())
6235 .collect();
6236
6237 let item_placements = grid::place_items(&placements, num_columns);
6238 let num_rows = grid::compute_num_rows(&item_placements);
6239
6240 if num_rows == 0 {
6241 return 0.0;
6242 }
6243
6244 let mut row_heights = vec![0.0_f64; num_rows];
6245 for placement in &item_placements {
6246 let cell_width = grid::span_width(
6247 placement.col_start,
6248 placement.col_end,
6249 &col_widths,
6250 col_gap,
6251 );
6252 let child = &children[placement.child_index];
6253 let child_style = child.style.resolve(Some(parent_style), cell_width);
6254 let h =
6255 self.measure_node_height(child, cell_width, &child_style, font_context);
6256 let span = placement.row_end - placement.row_start;
6257 let per_row = h / span as f64;
6258 for rh in row_heights
6259 .iter_mut()
6260 .take(placement.row_end.min(num_rows))
6261 .skip(placement.row_start)
6262 {
6263 if per_row > *rh {
6264 *rh = per_row;
6265 }
6266 }
6267 }
6268
6269 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6270 return row_heights.iter().sum::<f64>() + total_row_gap;
6271 }
6272 }
6273 }
6274
6275 let direction = parent_style.flex_direction;
6276 let row_gap = parent_style.row_gap;
6277 let column_gap = parent_style.column_gap;
6278
6279 match direction {
6280 FlexDirection::Row | FlexDirection::RowReverse => {
6281 let styles: Vec<ResolvedStyle> = children
6284 .iter()
6285 .map(|child| child.style.resolve(Some(parent_style), available_width))
6286 .collect();
6287
6288 let base_widths: Vec<f64> = children
6289 .iter()
6290 .zip(&styles)
6291 .map(|(child, style)| match style.flex_basis {
6292 SizeConstraint::Fixed(w) => w,
6293 SizeConstraint::Auto => match style.width {
6294 SizeConstraint::Fixed(w) => w,
6295 SizeConstraint::Auto => self
6296 .measure_intrinsic_width(child, style, font_context)
6297 .min(available_width),
6298 },
6299 })
6300 .collect();
6301
6302 let lines = match parent_style.flex_wrap {
6303 FlexWrap::NoWrap => {
6304 vec![flex::WrapLine {
6305 start: 0,
6306 end: children.len(),
6307 }]
6308 }
6309 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6310 flex::partition_into_lines(&base_widths, column_gap, available_width)
6311 }
6312 };
6313
6314 let mut final_widths = base_widths.clone();
6316 for line in &lines {
6317 let line_count = line.end - line.start;
6318 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6319 let distributable = available_width - line_gap;
6320 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6321 let remaining = distributable - total_base;
6322
6323 if remaining > 0.0 {
6324 let total_grow: f64 = styles[line.start..line.end]
6325 .iter()
6326 .map(|s| s.flex_grow)
6327 .sum();
6328 if total_grow > 0.0 {
6329 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6330 final_widths[line.start + j] = base_widths[line.start + j]
6331 + remaining * (s.flex_grow / total_grow);
6332 }
6333 }
6334 } else if remaining < 0.0 {
6335 let total_shrink: f64 = styles[line.start..line.end]
6336 .iter()
6337 .enumerate()
6338 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6339 .sum();
6340 if total_shrink > 0.0 {
6341 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6342 let factor =
6343 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6344 let w = base_widths[line.start + j] + remaining * factor;
6345 final_widths[line.start + j] = w.max(s.min_width);
6346 }
6347 }
6348 }
6349 }
6350
6351 let mut total = 0.0;
6352 for (i, line) in lines.iter().enumerate() {
6353 let line_height: f64 = children[line.start..line.end]
6354 .iter()
6355 .enumerate()
6356 .map(|(j, child)| {
6357 let fw = final_widths[line.start + j];
6358 let child_style =
6367 child.style.resolve(Some(parent_style), available_width);
6368 self.measure_node_height(child, fw, &child_style, font_context)
6369 + child_style.margin.vertical()
6370 })
6371 .fold(0.0f64, f64::max);
6372 total += line_height;
6373 if i > 0 {
6374 total += row_gap;
6375 }
6376 }
6377 total
6378 }
6379 FlexDirection::Column | FlexDirection::ColumnReverse => {
6380 let mut total = 0.0;
6381 for (i, child) in children.iter().enumerate() {
6382 let child_style = child.style.resolve(Some(parent_style), available_width);
6383 let child_height = self.measure_node_height(
6384 child,
6385 available_width,
6386 &child_style,
6387 font_context,
6388 );
6389 total += child_height + child_style.margin.vertical();
6390 if i > 0 {
6391 total += row_gap;
6392 }
6393 }
6394 total
6395 }
6396 }
6397 }
6398
6399 fn measure_intrinsic_width(
6401 &self,
6402 node: &Node,
6403 style: &ResolvedStyle,
6404 font_context: &FontContext,
6405 ) -> f64 {
6406 match &node.kind {
6407 NodeKind::Svg { width, .. } => {
6408 *width + style.padding.horizontal() + style.margin.horizontal()
6409 }
6410 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6411 let text_width = if !runs.is_empty() {
6417 runs.iter()
6418 .map(|run| {
6419 let run_style = run.style.resolve(Some(style), 0.0);
6420 let run_content = substitute_page_placeholders(&run.content);
6421 let transformed =
6422 apply_text_transform(&run_content, run_style.text_transform);
6423 let italic = matches!(
6424 run_style.font_style,
6425 FontStyle::Italic | FontStyle::Oblique
6426 );
6427 transformed
6431 .split('\n')
6432 .map(|segment| {
6433 font_context.measure_string(
6434 segment,
6435 &run_style.font_family,
6436 run_style.font_weight,
6437 italic,
6438 run_style.font_size,
6439 run_style.letter_spacing,
6440 )
6441 })
6442 .fold(0.0f64, f64::max)
6443 })
6444 .sum()
6445 } else {
6446 let content = substitute_page_placeholders(content);
6447 let transformed = apply_text_transform(&content, style.text_transform);
6448 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6449 transformed
6450 .split('\n')
6451 .map(|segment| {
6452 font_context.measure_string(
6453 segment,
6454 &style.font_family,
6455 style.font_weight,
6456 italic,
6457 style.font_size,
6458 style.letter_spacing,
6459 )
6460 })
6461 .fold(0.0f64, f64::max)
6462 };
6463 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6466 }
6467 NodeKind::Image {
6468 src, width, height, ..
6469 } => {
6470 let w = if let SizeConstraint::Fixed(w) = style.width {
6471 w
6472 } else if let Some(w) = width {
6473 *w
6474 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6475 let pixel_w = iw as f64;
6476 let pixel_h = ih as f64;
6477 let aspect = if pixel_w > 0.0 {
6478 pixel_h / pixel_w
6479 } else {
6480 0.75
6481 };
6482 let constrained_h = match style.height {
6484 SizeConstraint::Fixed(h) => Some(h),
6485 SizeConstraint::Auto => *height,
6486 };
6487 if let Some(h) = constrained_h {
6488 h / aspect
6489 } else {
6490 pixel_w
6491 }
6492 } else {
6493 100.0
6494 };
6495 w + style.padding.horizontal() + style.margin.horizontal()
6496 }
6497 NodeKind::Barcode { width, .. } => {
6498 let w = width.unwrap_or(0.0);
6499 w + style.padding.horizontal() + style.margin.horizontal()
6500 }
6501 NodeKind::QrCode { size, .. } => {
6502 let display_size = size.unwrap_or(0.0);
6503 display_size + style.padding.horizontal() + style.margin.horizontal()
6504 }
6505 NodeKind::Canvas { width, .. } => {
6506 *width + style.padding.horizontal() + style.margin.horizontal()
6507 }
6508 NodeKind::BarChart { width, .. }
6509 | NodeKind::LineChart { width, .. }
6510 | NodeKind::PieChart { width, .. }
6511 | NodeKind::AreaChart { width, .. }
6512 | NodeKind::DotPlot { width, .. } => {
6513 *width + style.padding.horizontal() + style.margin.horizontal()
6514 }
6515 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6516 *width + style.padding.horizontal() + style.margin.horizontal()
6517 }
6518 NodeKind::Table { columns } => {
6519 let num_cols = node
6524 .children
6525 .iter()
6526 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6527 .max()
6528 .unwrap_or(1)
6529 .max(columns.len().max(1));
6530 let (_, col_max) =
6531 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6532 col_max.iter().sum::<f64>()
6533 + style.padding.horizontal()
6534 + style.margin.horizontal()
6535 + style.border_width.horizontal()
6536 }
6537 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
6538 *width + style.padding.horizontal() + style.margin.horizontal()
6539 }
6540 NodeKind::Watermark { .. } => 0.0, _ => {
6542 if node.children.is_empty() {
6544 style.padding.horizontal() + style.margin.horizontal()
6545 } else {
6546 let direction = style.flex_direction;
6547 let gap = style.gap;
6548 let mut total = 0.0f64;
6549 for (i, child) in node.children.iter().enumerate() {
6550 let child_style = child.style.resolve(Some(style), 0.0);
6551 let child_width =
6552 self.measure_intrinsic_width(child, &child_style, font_context);
6553 match direction {
6554 FlexDirection::Row | FlexDirection::RowReverse => {
6555 total += child_width;
6556 if i > 0 {
6557 total += gap;
6558 }
6559 }
6560 _ => {
6561 total = total.max(child_width);
6562 }
6563 }
6564 }
6565 total
6566 + style.padding.horizontal()
6567 + style.margin.horizontal()
6568 + style.border_width.horizontal()
6569 }
6570 }
6571 }
6572 }
6573
6574 pub fn measure_min_content_width(
6578 &self,
6579 node: &Node,
6580 style: &ResolvedStyle,
6581 font_context: &FontContext,
6582 ) -> f64 {
6583 match &node.kind {
6584 NodeKind::Table { columns } => {
6585 let num_cols = node
6588 .children
6589 .iter()
6590 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6591 .max()
6592 .unwrap_or(1)
6593 .max(columns.len().max(1));
6594 let (col_min, _) =
6595 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6596 col_min.iter().sum::<f64>()
6597 + style.padding.horizontal()
6598 + style.margin.horizontal()
6599 + style.border_width.horizontal()
6600 }
6601 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6602 let word_width = if !runs.is_empty() {
6603 runs.iter()
6605 .map(|run| {
6606 let run_style = run.style.resolve(Some(style), 0.0);
6607 let run_content = substitute_page_placeholders(&run.content);
6608 let transformed =
6609 apply_text_transform(&run_content, run_style.text_transform);
6610 self.text_layout.measure_widest_word(
6611 font_context,
6612 &transformed,
6613 run_style.font_size,
6614 &run_style.font_family,
6615 run_style.font_weight,
6616 run_style.font_style,
6617 run_style.letter_spacing,
6618 style.hyphens,
6619 style.lang.as_deref(),
6620 )
6621 })
6622 .fold(0.0f64, f64::max)
6623 } else {
6624 let content = substitute_page_placeholders(content);
6625 let transformed = apply_text_transform(&content, style.text_transform);
6626 self.text_layout.measure_widest_word(
6627 font_context,
6628 &transformed,
6629 style.font_size,
6630 &style.font_family,
6631 style.font_weight,
6632 style.font_style,
6633 style.letter_spacing,
6634 style.hyphens,
6635 style.lang.as_deref(),
6636 )
6637 };
6638 word_width + style.padding.horizontal() + style.margin.horizontal()
6639 }
6640 NodeKind::Image { width, .. } => {
6641 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
6642 }
6643 NodeKind::Svg { width, .. } => {
6644 *width + style.padding.horizontal() + style.margin.horizontal()
6645 }
6646 _ => {
6647 if node.children.is_empty() {
6648 style.padding.horizontal()
6649 + style.margin.horizontal()
6650 + style.border_width.horizontal()
6651 } else {
6652 let mut max_child_min = 0.0f64;
6653 for child in &node.children {
6654 let child_style = child.style.resolve(Some(style), 0.0);
6655 let child_min =
6656 self.measure_min_content_width(child, &child_style, font_context);
6657 max_child_min = max_child_min.max(child_min);
6658 }
6659 max_child_min
6660 + style.padding.horizontal()
6661 + style.margin.horizontal()
6662 + style.border_width.horizontal()
6663 }
6664 }
6665 }
6666 }
6667
6668 fn cell_first_baseline_in_line(&self, cell: &Node, cell_style: &ResolvedStyle, w: f64) -> f64 {
6674 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<(f64, f64)> {
6675 for ch in &node.children {
6676 let s = ch.style.resolve(Some(parent), w);
6677 match &ch.kind {
6678 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
6679 return Some((s.font_size, s.line_height))
6680 }
6681 _ => {
6682 if let Some(f) = first(ch, &s, w) {
6683 return Some(f);
6684 }
6685 }
6686 }
6687 }
6688 None
6689 }
6690 let (fs, lh) =
6691 first(cell, cell_style, w).unwrap_or((cell_style.font_size, cell_style.line_height));
6692 (fs * lh - fs) / 2.0 + fs
6693 }
6694
6695 fn cell_baseline_distance(
6700 &self,
6701 cell: &Node,
6702 cell_style: &ResolvedStyle,
6703 inner_width: f64,
6704 ) -> f64 {
6705 cell_style.padding.top
6706 + cell_style.border_width.top
6707 + self.cell_first_baseline_in_line(cell, cell_style, inner_width)
6708 }
6709
6710 fn row_baseline(
6713 &self,
6714 row: &Node,
6715 row_style: &ResolvedStyle,
6716 col_widths: &[f64],
6717 col_offsets: &[usize],
6718 ) -> Option<f64> {
6719 let mut b: Option<f64> = None;
6720 for (cell_i, cell) in row.children.iter().enumerate() {
6721 let span = match &cell.kind {
6722 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6723 _ => 1,
6724 };
6725 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6726 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6727 let cell_style = cell.style.resolve(Some(row_style), col_width);
6728 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6729 let iw = col_width
6730 - cell_style.padding.horizontal()
6731 - cell_style.border_width.horizontal();
6732 let d = self.cell_baseline_distance(cell, &cell_style, iw);
6733 b = Some(b.map_or(d, |m: f64| m.max(d)));
6734 }
6735 }
6736 b
6737 }
6738
6739 fn measure_table_row_height(
6740 &self,
6741 row: &Node,
6742 col_widths: &[f64],
6743 col_offsets: &[usize],
6744 parent_style: &ResolvedStyle,
6745 font_context: &FontContext,
6746 ) -> f64 {
6747 let row_style = row
6748 .style
6749 .resolve(Some(parent_style), col_widths.iter().sum());
6750 let mut max_height: f64 = 0.0;
6751 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets);
6754
6755 for (cell_i, cell) in row.children.iter().enumerate() {
6756 let span = match &cell.kind {
6757 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6758 _ => 1,
6759 };
6760 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
6761 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
6762 let cell_style = cell.style.resolve(Some(&row_style), col_width);
6763 let inner_width =
6764 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
6765
6766 let mut cell_content_height = 0.0;
6767 for child in &cell.children {
6768 let child_style = child.style.resolve(Some(&cell_style), inner_width);
6769 cell_content_height +=
6770 self.measure_node_height(child, inner_width, &child_style, font_context);
6771 }
6772
6773 let mut total = cell_content_height
6774 + cell_style.padding.vertical()
6775 + cell_style.border_width.vertical();
6776 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6780 if let Some(b) = row_bl {
6781 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
6782 total += (b - d).max(0.0);
6783 }
6784 }
6785 if let SizeConstraint::Fixed(h) = cell_style.height {
6792 total = total.max(h);
6793 }
6794 max_height = max_height.max(total);
6795 }
6796
6797 max_height.max(row_style.min_height)
6798 }
6799
6800 fn cell_col_span(cell: &Node) -> usize {
6802 match &cell.kind {
6803 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6804 _ => 1,
6805 }
6806 }
6807
6808 fn measure_column_content(
6813 &self,
6814 children: &[Node],
6815 num_cols: usize,
6816 available_width: f64,
6817 table_style: &ResolvedStyle,
6818 font_context: &FontContext,
6819 ) -> (Vec<f64>, Vec<f64>) {
6820 let mut col_min = vec![0.0f64; num_cols];
6821 let mut col_max = vec![0.0f64; num_cols];
6822 let offsets = Self::table_column_offsets(children);
6823 for (row_i, row_node) in children.iter().enumerate() {
6824 for (cell_i, cell) in row_node.children.iter().enumerate() {
6825 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
6826 let span = Self::cell_col_span(cell);
6827 let cell_style = cell.style.resolve(Some(table_style), available_width);
6828 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
6829 let mut cmin = 0.0f64;
6830 let mut cmax = 0.0f64;
6831 for child in &cell.children {
6832 let child_style = child.style.resolve(Some(&cell_style), 0.0);
6833 cmin =
6834 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
6835 cmax =
6836 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
6837 }
6838 cmin += chrome;
6839 let mut cmax = cmax.max(cmin) + chrome;
6840 if let SizeConstraint::Fixed(w) = cell_style.width {
6841 cmax = w.max(cmin);
6842 }
6843 let per_min = cmin / span as f64;
6844 let per_max = cmax / span as f64;
6845 for k in col..(col + span).min(num_cols) {
6846 col_min[k] = col_min[k].max(per_min);
6847 col_max[k] = col_max[k].max(per_max);
6848 }
6849 }
6850 }
6851 (col_min, col_max)
6852 }
6853
6854 fn resolve_column_widths(
6864 &self,
6865 defs: &[ColumnDef],
6866 available_width: f64,
6867 children: &[Node],
6868 table_style: &ResolvedStyle,
6869 font_context: &FontContext,
6870 ) -> Vec<f64> {
6871 if defs.is_empty() {
6872 let num_cols = Self::occupancy_column_count(children);
6873
6874 let (col_min, col_max) = self.measure_column_content(
6875 children,
6876 num_cols,
6877 available_width,
6878 table_style,
6879 font_context,
6880 );
6881
6882 let sum_min: f64 = col_min.iter().sum();
6883 let sum_max: f64 = col_max.iter().sum();
6884 let w = available_width;
6885 return if sum_max <= w {
6886 if sum_max <= f64::EPSILON {
6890 vec![w / num_cols as f64; num_cols]
6891 } else {
6892 col_max
6893 .iter()
6894 .map(|m| m + (w - sum_max) * (m / sum_max))
6895 .collect()
6896 }
6897 } else if sum_min <= w {
6898 let denom = (sum_max - sum_min).max(f64::EPSILON);
6901 col_min
6902 .iter()
6903 .zip(&col_max)
6904 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
6905 .collect()
6906 } else {
6907 self.defect(format!(
6910 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
6911 sum_min, w
6912 ));
6913 let scale = w / sum_min.max(f64::EPSILON);
6914 col_min.iter().map(|m| m * scale).collect()
6915 };
6916 }
6917
6918 let num_cols = Self::occupancy_column_count(children).max(defs.len());
6924 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
6925 while defs_vec.len() < num_cols {
6926 defs_vec.push(ColumnDef {
6927 width: ColumnWidth::Auto,
6928 });
6929 }
6930 let defs = &defs_vec[..];
6931
6932 let mut widths = Vec::new();
6933 let mut remaining = available_width;
6934 let mut auto_count = 0;
6935
6936 for def in defs {
6937 match def.width {
6938 ColumnWidth::Fixed(w) => {
6939 widths.push(w);
6940 remaining -= w;
6941 }
6942 ColumnWidth::Fraction(f) => {
6943 let w = available_width * f;
6944 widths.push(w);
6945 remaining -= w;
6946 }
6947 ColumnWidth::Auto => {
6948 widths.push(0.0);
6949 auto_count += 1;
6950 }
6951 }
6952 }
6953
6954 if remaining < 0.0 {
6955 self.defect(format!(
6958 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
6959 available_width - remaining,
6960 available_width
6961 ));
6962 remaining = 0.0;
6963 }
6964
6965 if auto_count > 0 {
6966 let auto_width = remaining / auto_count as f64;
6967 for (i, def) in defs.iter().enumerate() {
6968 if matches!(def.width, ColumnWidth::Auto) {
6969 widths[i] = auto_width;
6970 }
6971 }
6972 }
6973
6974 let (col_min, _) = self.measure_column_content(
6981 children,
6982 widths.len(),
6983 available_width,
6984 table_style,
6985 font_context,
6986 );
6987 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
6988 if needs_floor {
6989 let sum_min: f64 = col_min.iter().sum();
6990 if sum_min > available_width {
6991 self.defect(format!(
6992 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
6993 sum_min, available_width
6994 ));
6995 let scale = available_width / sum_min.max(f64::EPSILON);
6996 return col_min.iter().map(|m| m * scale).collect();
6997 }
6998 let deficit: f64 = widths
6999 .iter()
7000 .zip(&col_min)
7001 .map(|(w, m)| (m - w).max(0.0))
7002 .sum();
7003 let surplus: f64 = widths
7004 .iter()
7005 .zip(&col_min)
7006 .map(|(w, m)| (w - m).max(0.0))
7007 .sum();
7008 let take = if surplus > 0.0 {
7009 deficit / surplus
7010 } else {
7011 0.0
7012 };
7013 widths = widths
7014 .iter()
7015 .zip(&col_min)
7016 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7017 .collect();
7018 }
7019
7020 widths
7021 }
7022
7023 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7024 for (page_index, page) in pages.iter_mut().enumerate() {
7025 if !page.watermarks.is_empty() {
7027 let (page_w, page_h) = page.config.size.dimensions();
7028 let cx = page_w / 2.0;
7029 let cy = page_h / 2.0;
7030
7031 let mut watermark_elements = Vec::new();
7032 for wm_node in &page.watermarks {
7033 if let NodeKind::Watermark {
7034 text,
7035 font_size,
7036 angle,
7037 } = &wm_node.kind
7038 {
7039 let style = wm_node.style.resolve(None, page_w);
7040 let color = style.color;
7041 let opacity = style.opacity;
7042 let angle_rad = angle.to_radians();
7043
7044 let italic =
7046 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7047
7048 let shaped = self.text_layout.shape_text(
7050 font_context,
7051 text,
7052 &style.font_family,
7053 style.font_weight,
7054 style.font_style,
7055 );
7056
7057 let mut glyphs = Vec::new();
7058 let mut x_pos = 0.0;
7059 let text_chars: Vec<char> = text.chars().collect();
7060
7061 if let Some(shaped_glyphs) = shaped {
7062 let units_per_em = font_context.units_per_em(
7064 &style.font_family,
7065 style.font_weight,
7066 italic,
7067 ) as f64;
7068
7069 for sg in &shaped_glyphs {
7070 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7071 let cluster_idx = sg.cluster as usize;
7072 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7073 glyphs.push(PositionedGlyph {
7074 glyph_id: sg.glyph_id,
7075 char_value: ch,
7076 x_offset: x_pos,
7077 y_offset: 0.0,
7078 x_advance: advance,
7079 font_size: *font_size,
7080 font_family: Arc::from(style.font_family.as_str()),
7081 font_weight: style.font_weight,
7082 font_style: style.font_style,
7083 color: Some(color),
7084 href: None,
7085 text_decoration: TextDecoration::None,
7086 letter_spacing: style.letter_spacing,
7087 cluster_text: None,
7088 });
7089 x_pos += advance + style.letter_spacing;
7090 }
7091 } else {
7092 for &ch in &text_chars {
7094 let w = font_context.char_width(
7095 ch,
7096 &style.font_family,
7097 style.font_weight,
7098 italic,
7099 *font_size,
7100 );
7101 glyphs.push(PositionedGlyph {
7102 glyph_id: ch as u16,
7103 char_value: ch,
7104 x_offset: x_pos,
7105 y_offset: 0.0,
7106 x_advance: w,
7107 font_size: *font_size,
7108 font_family: Arc::from(style.font_family.as_str()),
7109 font_weight: style.font_weight,
7110 font_style: style.font_style,
7111 color: Some(color),
7112 href: None,
7113 text_decoration: TextDecoration::None,
7114 letter_spacing: style.letter_spacing,
7115 cluster_text: None,
7116 });
7117 x_pos += w + style.letter_spacing;
7118 }
7119 }
7120
7121 let text_width = x_pos;
7122
7123 let line = TextLine {
7124 x: 0.0,
7125 y: 0.0,
7126 glyphs,
7127 width: text_width,
7128 height: *font_size,
7129 word_spacing: 0.0,
7130 };
7131
7132 watermark_elements.push(LayoutElement {
7133 x: cx,
7134 y: cy,
7135 width: text_width,
7136 height: *font_size,
7137 draw: DrawCommand::Watermark {
7138 lines: vec![line],
7139 color,
7140 opacity,
7141 angle_rad,
7142 font_family: style.font_family.clone(),
7143 },
7144 children: vec![],
7145 node_type: Some("Watermark".to_string()),
7146 resolved_style: None,
7147 source_location: None,
7148 href: None,
7149 bookmark: None,
7150 alt: None,
7151 is_header_row: false,
7152 col_span: 1,
7153 overflow: Overflow::default(),
7154 opacity: 1.0,
7155 });
7156 }
7157 }
7158
7159 watermark_elements.append(&mut page.elements);
7161 page.elements = watermark_elements;
7162 page.watermarks.clear();
7163 }
7164
7165 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7166 continue;
7167 }
7168
7169 if !page.fixed_header.is_empty() {
7171 let mut hdr_cursor = PageCursor::new(&page.config);
7172 for (node, _h) in &page.fixed_header {
7173 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7176 continue;
7177 }
7178 let cw = hdr_cursor.content_width;
7179 let cx = hdr_cursor.content_x;
7180 let style = node.style.resolve(None, cw);
7181 self.layout_view(
7182 node,
7183 &style,
7184 &mut hdr_cursor,
7185 &mut Vec::new(),
7186 cx,
7187 cw,
7188 font_context,
7189 );
7190 }
7191 let mut combined = hdr_cursor.elements;
7193 combined.append(&mut page.elements);
7194 page.elements = combined;
7195 }
7196
7197 if !page.fixed_footer.is_empty() {
7202 let mut ftr_cursor = PageCursor::new(&page.config);
7203 let total_ftr: f64 = page
7204 .fixed_footer
7205 .iter()
7206 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7207 .map(|(_, h)| *h)
7208 .sum();
7209 let target_y = ftr_cursor.content_height - total_ftr;
7210 for (node, _h) in &page.fixed_footer {
7212 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7213 continue;
7214 }
7215 let cw = ftr_cursor.content_width;
7216 let cx = ftr_cursor.content_x;
7217 let style = node.style.resolve(None, cw);
7218 self.layout_view(
7219 node,
7220 &style,
7221 &mut ftr_cursor,
7222 &mut Vec::new(),
7223 cx,
7224 cw,
7225 font_context,
7226 );
7227 }
7228 for el in &mut ftr_cursor.elements {
7232 offset_element_y(el, target_y);
7233 }
7234 page.elements.extend(ftr_cursor.elements);
7235 }
7236
7237 page.fixed_header.clear();
7239 page.fixed_footer.clear();
7240 }
7241 }
7242
7243 #[allow(clippy::too_many_arguments)]
7248 fn layout_grid_children(
7249 &self,
7250 children: &[Node],
7251 parent_style: &ResolvedStyle,
7252 cursor: &mut PageCursor,
7253 pages: &mut Vec<LayoutPage>,
7254 x: f64,
7255 available_width: f64,
7256 font_context: &FontContext,
7257 ) {
7258 let template_cols = match &parent_style.grid_template_columns {
7259 Some(cols) => cols,
7260 None => return, };
7262
7263 let num_columns = template_cols.len();
7264 if num_columns == 0 || children.is_empty() {
7265 return;
7266 }
7267
7268 let col_gap = parent_style.column_gap;
7269 let row_gap = parent_style.row_gap;
7270
7271 let content_sizes: Vec<f64> = template_cols
7274 .iter()
7275 .map(|track| {
7276 if matches!(track, GridTrackSize::Auto) {
7277 available_width / num_columns as f64
7280 } else {
7281 0.0
7282 }
7283 })
7284 .collect();
7285
7286 let col_widths =
7287 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7288
7289 let placements: Vec<Option<&GridPlacement>> = children
7291 .iter()
7292 .map(|child| child.style.grid_placement.as_ref())
7293 .collect();
7294
7295 let item_placements = grid::place_items(&placements, num_columns);
7297 let num_rows = grid::compute_num_rows(&item_placements);
7298
7299 if num_rows == 0 {
7300 return;
7301 }
7302
7303 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7305 for placement in &item_placements {
7306 let cell_width =
7307 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7308 let child = &children[placement.child_index];
7309 let child_style = child.style.resolve(Some(parent_style), cell_width);
7310 item_heights[placement.child_index] =
7311 self.measure_node_height(child, cell_width, &child_style, font_context);
7312 }
7313
7314 let template_rows = parent_style.grid_template_rows.as_deref();
7316 let mut row_heights = vec![0.0_f64; num_rows];
7317 for placement in &item_placements {
7318 let h = item_heights[placement.child_index];
7319 let span = placement.row_end - placement.row_start;
7320 let per_row = h / span as f64;
7321 for rh in row_heights
7322 .iter_mut()
7323 .take(placement.row_end.min(num_rows))
7324 .skip(placement.row_start)
7325 {
7326 if per_row > *rh {
7327 *rh = per_row;
7328 }
7329 }
7330 }
7331
7332 if let Some(template) = template_rows {
7334 let auto_row = parent_style.grid_auto_rows.as_ref();
7335 for (r, rh) in row_heights.iter_mut().enumerate() {
7336 let track = template.get(r).or(auto_row);
7337 if let Some(track) = track {
7338 match track {
7339 GridTrackSize::Pt(pts) => *rh = *pts,
7340 GridTrackSize::Auto => {} _ => {} }
7343 }
7344 }
7345 }
7346
7347 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7349 if row_height > cursor.remaining_height() {
7354 pages.push(cursor.finalize());
7355 *cursor = cursor.new_page();
7356 }
7357
7358 let row_start_y = cursor.y;
7359
7360 for placement in &item_placements {
7362 if placement.row_start != row {
7363 continue; }
7365
7366 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7367 let cell_width =
7368 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7369
7370 let child = &children[placement.child_index];
7371
7372 self.layout_node(
7373 child,
7374 cursor,
7375 pages,
7376 cell_x,
7377 cell_width,
7378 Some(parent_style),
7379 font_context,
7380 None,
7381 None,
7382 );
7383 cursor.y = row_start_y;
7385 }
7386
7387 cursor.y = row_start_y + row_height + row_gap;
7388 }
7389
7390 if num_rows > 0 {
7392 cursor.y -= row_gap;
7393 }
7394 }
7395}
7396
7397struct FlexItem<'a> {
7398 node: &'a Node,
7399 style: ResolvedStyle,
7400 base_width: f64,
7401 min_content_width: f64,
7402}
7403
7404fn first_text_snippet(node: &Node) -> Option<String> {
7407 fn walk(n: &Node) -> Option<&str> {
7408 match &n.kind {
7409 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7410 if !content.trim().is_empty() {
7411 return Some(content.trim());
7412 }
7413 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
7414 return Some(r.content.trim());
7415 }
7416 None
7417 }
7418 _ => n.children.iter().find_map(walk),
7419 }
7420 }
7421 walk(node).map(|t| {
7422 let mut s: String = t.chars().take(32).collect();
7423 if t.chars().count() > 32 {
7424 s.push('…');
7425 }
7426 s
7427 })
7428}
7429
7430#[cfg(test)]
7431mod tests {
7432 use super::*;
7433 use crate::font::FontContext;
7434
7435 fn make_text(content: &str, font_size: f64) -> Node {
7436 Node {
7437 kind: NodeKind::Text {
7438 content: content.to_string(),
7439 href: None,
7440 runs: vec![],
7441 },
7442 style: Style {
7443 font_size: Some(font_size),
7444 ..Default::default()
7445 },
7446 children: vec![],
7447 id: None,
7448 source_location: None,
7449 bookmark: None,
7450 href: None,
7451 alt: None,
7452 }
7453 }
7454
7455 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
7456 Node {
7457 kind: NodeKind::View,
7458 style,
7459 children,
7460 id: None,
7461 source_location: None,
7462 bookmark: None,
7463 href: None,
7464 alt: None,
7465 }
7466 }
7467
7468 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
7469 Node {
7470 kind: NodeKind::Text {
7471 content: String::new(),
7472 href: None,
7473 runs,
7474 },
7475 style: Style::default(),
7476 children: vec![],
7477 id: None,
7478 source_location: None,
7479 bookmark: None,
7480 href: None,
7481 alt: None,
7482 }
7483 }
7484
7485 #[test]
7486 fn intrinsic_width_measures_runs_not_just_content() {
7487 let engine = LayoutEngine::new();
7491 let font_context = FontContext::new();
7492
7493 let runs_node = make_runs_text(vec![
7494 crate::model::TextRun {
7495 content: "Hello ".to_string(),
7496 style: Style::default(),
7497 href: None,
7498 },
7499 crate::model::TextRun {
7500 content: "World".to_string(),
7501 style: Style {
7502 font_weight: Some(700),
7503 ..Default::default()
7504 },
7505 href: None,
7506 },
7507 ]);
7508 let plain_node = make_text("Hello World", 12.0);
7509
7510 let runs_style = runs_node.style.resolve(None, 0.0);
7511 let plain_style = plain_node.style.resolve(None, 0.0);
7512 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
7513 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
7514
7515 assert!(
7518 runs_w >= plain_w,
7519 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
7520 );
7521 assert!(
7522 runs_w < plain_w * 1.5,
7523 "runs width ({runs_w}) should be close to plain width ({plain_w})"
7524 );
7525 }
7526
7527 #[test]
7528 fn intrinsic_width_of_multiline_text_is_widest_line() {
7529 let engine = LayoutEngine::new();
7530 let font_context = FontContext::new();
7531
7532 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
7533 let widest = make_text("Springfield, IL 62704", 12.0);
7534
7535 let m_style = multiline.style.resolve(None, 0.0);
7536 let w_style = widest.style.resolve(None, 0.0);
7537 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
7538 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
7539
7540 assert!(
7541 (m_w - w_w).abs() < 0.01,
7542 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
7543 );
7544 }
7545
7546 #[test]
7547 fn intrinsic_width_of_heading_measures_its_text() {
7548 let engine = LayoutEngine::new();
7551 let font_context = FontContext::new();
7552
7553 let heading = Node {
7554 kind: NodeKind::Heading {
7555 level: 1,
7556 content: "Invoice #2024-001".to_string(),
7557 href: None,
7558 runs: vec![],
7559 },
7560 style: Style {
7561 font_size: Some(24.0),
7562 ..Default::default()
7563 },
7564 children: vec![],
7565 id: None,
7566 source_location: None,
7567 bookmark: None,
7568 href: None,
7569 alt: None,
7570 };
7571 let style = heading.style.resolve(None, 0.0);
7572 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
7573 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
7574 }
7575
7576 #[test]
7577 fn measure_node_height_of_wrapping_heading_matches_text() {
7578 let engine = LayoutEngine::new();
7585 let font_context = FontContext::new();
7586
7587 let content = "Annual Performance Review";
7588 let heading = Node {
7589 kind: NodeKind::Heading {
7590 level: 1,
7591 content: content.to_string(),
7592 href: None,
7593 runs: vec![],
7594 },
7595 style: Style {
7596 font_size: Some(32.0),
7597 ..Default::default()
7598 },
7599 children: vec![],
7600 id: None,
7601 source_location: None,
7602 bookmark: None,
7603 href: None,
7604 alt: None,
7605 };
7606 let text = make_text(content, 32.0);
7607
7608 let width = 200.0;
7610 let h_style = heading.style.resolve(None, width);
7611 let t_style = text.style.resolve(None, width);
7612 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
7613 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
7614
7615 assert!(
7616 h_height > 32.0,
7617 "a wrapping 32pt heading must measure more than one line, got {h_height}"
7618 );
7619 assert!(
7620 (h_height - t_height).abs() < 0.01,
7621 "heading height ({h_height}) must equal the same text's height ({t_height})"
7622 );
7623 }
7624
7625 #[test]
7626 fn intrinsic_width_flex_row_sums_children() {
7627 let engine = LayoutEngine::new();
7628 let font_context = FontContext::new();
7629
7630 let child1 = make_text("Hello", 14.0);
7631 let child2 = make_text("World", 14.0);
7632
7633 let child1_style = child1.style.resolve(None, 0.0);
7634 let child2_style = child2.style.resolve(None, 0.0);
7635 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
7636 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
7637
7638 let row = make_styled_view(
7639 Style {
7640 flex_direction: Some(FlexDirection::Row),
7641 ..Default::default()
7642 },
7643 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
7644 );
7645 let row_style = row.style.resolve(None, 0.0);
7646 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
7647
7648 assert!(
7649 (row_w - (child1_w + child2_w)).abs() < 0.01,
7650 "Row intrinsic width ({}) should equal sum of children ({} + {})",
7651 row_w,
7652 child1_w,
7653 child2_w
7654 );
7655 }
7656
7657 #[test]
7658 fn intrinsic_width_flex_column_takes_max() {
7659 let engine = LayoutEngine::new();
7660 let font_context = FontContext::new();
7661
7662 let short = make_text("Hi", 14.0);
7663 let long = make_text("Hello World", 14.0);
7664
7665 let short_style = short.style.resolve(None, 0.0);
7666 let long_style = long.style.resolve(None, 0.0);
7667 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
7668 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
7669
7670 let col = make_styled_view(
7671 Style {
7672 flex_direction: Some(FlexDirection::Column),
7673 ..Default::default()
7674 },
7675 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
7676 );
7677 let col_style = col.style.resolve(None, 0.0);
7678 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
7679
7680 assert!(
7681 (col_w - long_w).abs() < 0.01,
7682 "Column intrinsic width ({}) should equal max child ({}, short was {})",
7683 col_w,
7684 long_w,
7685 short_w
7686 );
7687 }
7688
7689 #[test]
7690 fn intrinsic_width_nested_containers() {
7691 let engine = LayoutEngine::new();
7692 let font_context = FontContext::new();
7693
7694 let inner = make_styled_view(
7695 Style {
7696 flex_direction: Some(FlexDirection::Row),
7697 ..Default::default()
7698 },
7699 vec![make_text("A", 12.0), make_text("B", 12.0)],
7700 );
7701 let inner_style = inner.style.resolve(None, 0.0);
7702 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
7703
7704 let outer = make_styled_view(
7705 Style::default(),
7706 vec![make_styled_view(
7707 Style {
7708 flex_direction: Some(FlexDirection::Row),
7709 ..Default::default()
7710 },
7711 vec![make_text("A", 12.0), make_text("B", 12.0)],
7712 )],
7713 );
7714 let outer_style = outer.style.resolve(None, 0.0);
7715 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
7716
7717 assert!(
7718 (outer_w - inner_w).abs() < 0.01,
7719 "Nested container ({}) should match inner container ({})",
7720 outer_w,
7721 inner_w
7722 );
7723 }
7724
7725 #[test]
7726 fn intrinsic_width_row_with_gap() {
7727 let engine = LayoutEngine::new();
7728 let font_context = FontContext::new();
7729
7730 let no_gap = make_styled_view(
7731 Style {
7732 flex_direction: Some(FlexDirection::Row),
7733 ..Default::default()
7734 },
7735 vec![make_text("A", 12.0), make_text("B", 12.0)],
7736 );
7737 let with_gap = make_styled_view(
7738 Style {
7739 flex_direction: Some(FlexDirection::Row),
7740 gap: Some(10.0),
7741 ..Default::default()
7742 },
7743 vec![make_text("A", 12.0), make_text("B", 12.0)],
7744 );
7745
7746 let no_gap_style = no_gap.style.resolve(None, 0.0);
7747 let with_gap_style = with_gap.style.resolve(None, 0.0);
7748 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
7749 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
7750
7751 assert!(
7752 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
7753 "Gap should add 10pt: with_gap={}, no_gap={}",
7754 with_gap_w,
7755 no_gap_w
7756 );
7757 }
7758
7759 #[test]
7760 fn intrinsic_width_empty_container() {
7761 let engine = LayoutEngine::new();
7762 let font_context = FontContext::new();
7763
7764 let padding = 8.0;
7765 let empty = make_styled_view(
7766 Style {
7767 padding: Some(Edges::uniform(padding)),
7768 ..Default::default()
7769 },
7770 vec![],
7771 );
7772 let style = empty.style.resolve(None, 0.0);
7773 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
7774
7775 assert!(
7776 (w - padding * 2.0).abs() < 0.01,
7777 "Empty container width ({}) should equal horizontal padding ({})",
7778 w,
7779 padding * 2.0
7780 );
7781 }
7782
7783 #[test]
7786 fn flex_shrink_respects_min_content_width() {
7787 let engine = LayoutEngine::new();
7791 let font_context = FontContext::new();
7792
7793 let sale_text = make_text("SALE", 12.0);
7794 let sale_style = sale_text.style.resolve(None, 0.0);
7795 let sale_word_width =
7796 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
7797 assert!(
7798 sale_word_width > 0.0,
7799 "SALE should have non-zero min-content width"
7800 );
7801
7802 let container = make_styled_view(
7805 Style {
7806 flex_direction: Some(FlexDirection::Row),
7807 width: Some(Dimension::Pt(100.0)),
7808 ..Default::default()
7809 },
7810 vec![
7811 make_styled_view(
7812 Style {
7813 width: Some(Dimension::Pt(80.0)),
7814 flex_shrink: Some(1.0),
7815 ..Default::default()
7816 },
7817 vec![],
7818 ),
7819 make_styled_view(
7820 Style {
7821 width: Some(Dimension::Pt(60.0)),
7822 flex_shrink: Some(1.0),
7823 ..Default::default()
7824 },
7825 vec![make_text("SALE", 12.0)],
7826 ),
7827 ],
7828 );
7829
7830 let doc = Document {
7831 children: vec![Node::page(
7832 PageConfig::default(),
7833 Style::default(),
7834 vec![container],
7835 )],
7836 metadata: Default::default(),
7837 default_page: PageConfig::default(),
7838 first_page: None,
7839 left_page: None,
7840 right_page: None,
7841 named_pages: Default::default(),
7842 attachments: vec![],
7843 zugferd: None,
7844 fonts: vec![],
7845 tagged: false,
7846 pdfa: None,
7847 default_style: None,
7848 embedded_data: None,
7849 flatten_forms: false,
7850 pdf_ua: false,
7851 certification: None,
7852 };
7853
7854 let pages = engine.layout(&doc, &font_context);
7855 assert!(!pages.is_empty());
7856
7857 let page = &pages[0];
7860 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
7862 assert!(
7863 container_el.is_some(),
7864 "Should find container with 2 children"
7865 );
7866 let sale_child = &container_el.unwrap().children[1];
7867 assert!(
7868 sale_child.width >= sale_word_width - 0.01,
7869 "SALE child width ({}) should be >= min-content width ({})",
7870 sale_child.width,
7871 sale_word_width
7872 );
7873 }
7874
7875 #[test]
7878 fn column_justify_content_center() {
7879 let engine = LayoutEngine::new();
7882 let font_context = FontContext::new();
7883
7884 let container = make_styled_view(
7885 Style {
7886 flex_direction: Some(FlexDirection::Column),
7887 height: Some(Dimension::Pt(200.0)),
7888 justify_content: Some(JustifyContent::Center),
7889 ..Default::default()
7890 },
7891 vec![make_text("Centered", 12.0)],
7892 );
7893
7894 let doc = Document {
7895 children: vec![Node::page(
7896 PageConfig::default(),
7897 Style::default(),
7898 vec![container],
7899 )],
7900 metadata: Default::default(),
7901 default_page: PageConfig::default(),
7902 first_page: None,
7903 left_page: None,
7904 right_page: None,
7905 named_pages: Default::default(),
7906 attachments: vec![],
7907 zugferd: None,
7908 fonts: vec![],
7909 tagged: false,
7910 pdfa: None,
7911 default_style: None,
7912 embedded_data: None,
7913 flatten_forms: false,
7914 pdf_ua: false,
7915 certification: None,
7916 };
7917
7918 let pages = engine.layout(&doc, &font_context);
7919 let page = &pages[0];
7920
7921 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7924 assert!(
7925 container_el.is_some(),
7926 "Should find container with children"
7927 );
7928 let container_el = container_el.unwrap();
7929 let child = &container_el.children[0];
7930
7931 let child_offset = child.y - container_el.y;
7933 let expected_offset = (200.0 - child.height) / 2.0;
7934 assert!(
7935 (child_offset - expected_offset).abs() < 2.0,
7936 "Child offset ({}) should be near center ({})",
7937 child_offset,
7938 expected_offset
7939 );
7940 }
7941
7942 #[test]
7943 fn column_align_items_center() {
7944 let engine = LayoutEngine::new();
7947 let font_context = FontContext::new();
7948
7949 let container = make_styled_view(
7950 Style {
7951 flex_direction: Some(FlexDirection::Column),
7952 width: Some(Dimension::Pt(300.0)),
7953 align_items: Some(AlignItems::Center),
7954 ..Default::default()
7955 },
7956 vec![make_text("Hi", 12.0)],
7957 );
7958
7959 let doc = Document {
7960 children: vec![Node::page(
7961 PageConfig::default(),
7962 Style::default(),
7963 vec![container],
7964 )],
7965 metadata: Default::default(),
7966 default_page: PageConfig::default(),
7967 first_page: None,
7968 left_page: None,
7969 right_page: None,
7970 named_pages: Default::default(),
7971 attachments: vec![],
7972 zugferd: None,
7973 fonts: vec![],
7974 tagged: false,
7975 pdfa: None,
7976 default_style: None,
7977 embedded_data: None,
7978 flatten_forms: false,
7979 pdf_ua: false,
7980 certification: None,
7981 };
7982
7983 let pages = engine.layout(&doc, &font_context);
7984 let page = &pages[0];
7985
7986 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7987 assert!(container_el.is_some());
7988 let container_el = container_el.unwrap();
7989 let child = &container_el.children[0];
7990
7991 let child_center = child.x + child.width / 2.0;
7993 let container_center = container_el.x + container_el.width / 2.0;
7994 assert!(
7995 (child_center - container_center).abs() < 2.0,
7996 "Child center ({}) should be near container center ({})",
7997 child_center,
7998 container_center
7999 );
8000 }
8001
8002 #[test]
8005 fn absolute_child_positioned_relative_to_parent() {
8006 let engine = LayoutEngine::new();
8010 let font_context = FontContext::new();
8011
8012 let parent = make_styled_view(
8013 Style {
8014 position: Some(crate::model::Position::Relative),
8015 margin: Some(MarginEdges::from_edges(Edges {
8016 top: 50.0,
8017 left: 50.0,
8018 ..Default::default()
8019 })),
8020 width: Some(Dimension::Pt(200.0)),
8021 height: Some(Dimension::Pt(200.0)),
8022 ..Default::default()
8023 },
8024 vec![make_styled_view(
8025 Style {
8026 position: Some(crate::model::Position::Absolute),
8027 top: Some(10.0),
8028 left: Some(10.0),
8029 width: Some(Dimension::Pt(50.0)),
8030 height: Some(Dimension::Pt(50.0)),
8031 ..Default::default()
8032 },
8033 vec![],
8034 )],
8035 );
8036
8037 let doc = Document {
8038 children: vec![Node::page(
8039 PageConfig::default(),
8040 Style::default(),
8041 vec![parent],
8042 )],
8043 metadata: Default::default(),
8044 default_page: PageConfig::default(),
8045 first_page: None,
8046 left_page: None,
8047 right_page: None,
8048 named_pages: Default::default(),
8049 attachments: vec![],
8050 zugferd: None,
8051 fonts: vec![],
8052 tagged: false,
8053 pdfa: None,
8054 default_style: None,
8055 embedded_data: None,
8056 flatten_forms: false,
8057 pdf_ua: false,
8058 certification: None,
8059 };
8060
8061 let pages = engine.layout(&doc, &font_context);
8062 let page = &pages[0];
8063
8064 let parent_el = page
8067 .elements
8068 .iter()
8069 .find(|e| e.width > 190.0 && e.width < 210.0);
8070 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8071 let parent_el = parent_el.unwrap();
8072
8073 let abs_child = parent_el
8075 .children
8076 .iter()
8077 .find(|e| e.width > 45.0 && e.width < 55.0);
8078 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8079 let abs_child = abs_child.unwrap();
8080
8081 let expected_x = parent_el.x + 10.0;
8082 let expected_y = parent_el.y + 10.0;
8083 assert!(
8084 (abs_child.x - expected_x).abs() < 1.0,
8085 "Absolute child x ({}) should be parent.x + 10 ({})",
8086 abs_child.x,
8087 expected_x
8088 );
8089 assert!(
8090 (abs_child.y - expected_y).abs() < 1.0,
8091 "Absolute child y ({}) should be parent.y + 10 ({})",
8092 abs_child.y,
8093 expected_y
8094 );
8095 }
8096
8097 #[test]
8098 fn absolute_escapes_unpositioned_parent_to_page() {
8099 let engine = LayoutEngine::new();
8104 let font_context = FontContext::new();
8105 let parent = make_styled_view(
8106 Style {
8107 margin: Some(MarginEdges::from_edges(Edges {
8108 top: 50.0,
8109 left: 50.0,
8110 ..Default::default()
8111 })),
8112 width: Some(Dimension::Pt(200.0)),
8113 height: Some(Dimension::Pt(200.0)),
8114 ..Default::default()
8115 },
8116 vec![make_styled_view(
8117 Style {
8118 position: Some(crate::model::Position::Absolute),
8119 top: Some(10.0),
8120 left: Some(10.0),
8121 width: Some(Dimension::Pt(50.0)),
8122 height: Some(Dimension::Pt(50.0)),
8123 ..Default::default()
8124 },
8125 vec![],
8126 )],
8127 );
8128 let doc = Document {
8129 children: vec![Node::page(
8130 PageConfig::default(),
8131 Style::default(),
8132 vec![parent],
8133 )],
8134 metadata: Default::default(),
8135 default_page: PageConfig::default(),
8136 first_page: None,
8137 left_page: None,
8138 right_page: None,
8139 named_pages: Default::default(),
8140 attachments: vec![],
8141 zugferd: None,
8142 fonts: vec![],
8143 tagged: false,
8144 pdfa: None,
8145 default_style: None,
8146 embedded_data: None,
8147 flatten_forms: false,
8148 pdf_ua: false,
8149 certification: None,
8150 };
8151 let pages = engine.layout(&doc, &font_context);
8152 let page = &pages[0];
8153 let parent_el = page
8154 .elements
8155 .iter()
8156 .find(|e| e.width > 190.0 && e.width < 210.0)
8157 .expect("parent");
8158 let abs_child = parent_el
8159 .children
8160 .iter()
8161 .find(|e| e.width > 45.0 && e.width < 55.0)
8162 .expect("abs child");
8163 let page_left = PageConfig::default().margin.left;
8164 let page_top = PageConfig::default().margin.top;
8165 assert!(
8166 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
8167 "absolute escapes to the page: x {} should be page_left + 10 ({})",
8168 abs_child.x,
8169 page_left + 10.0
8170 );
8171 assert!(
8172 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
8173 "absolute escapes to the page: y {} should be page_top + 10 ({})",
8174 abs_child.y,
8175 page_top + 10.0
8176 );
8177 assert!(
8178 abs_child.x < parent_el.x,
8179 "child must no longer be parent-relative (parent is 50pt further in)"
8180 );
8181 }
8182
8183 #[test]
8184 fn text_transform_none_passthrough() {
8185 assert_eq!(
8186 apply_text_transform("Hello World", TextTransform::None),
8187 "Hello World"
8188 );
8189 }
8190
8191 #[test]
8192 fn text_transform_uppercase() {
8193 assert_eq!(
8194 apply_text_transform("hello world", TextTransform::Uppercase),
8195 "HELLO WORLD"
8196 );
8197 }
8198
8199 #[test]
8200 fn text_transform_lowercase() {
8201 assert_eq!(
8202 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
8203 "hello world"
8204 );
8205 }
8206
8207 #[test]
8208 fn text_transform_capitalize() {
8209 assert_eq!(
8210 apply_text_transform("hello world", TextTransform::Capitalize),
8211 "Hello World"
8212 );
8213 assert_eq!(
8214 apply_text_transform(" hello world ", TextTransform::Capitalize),
8215 " Hello World "
8216 );
8217 assert_eq!(
8218 apply_text_transform("already Capitalized", TextTransform::Capitalize),
8219 "Already Capitalized"
8220 );
8221 }
8222
8223 #[test]
8224 fn text_transform_capitalize_empty() {
8225 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
8226 }
8227
8228 #[test]
8229 fn apply_char_transform_uppercase() {
8230 assert_eq!(
8231 apply_char_transform('a', TextTransform::Uppercase, false),
8232 'A'
8233 );
8234 assert_eq!(
8235 apply_char_transform('A', TextTransform::Uppercase, false),
8236 'A'
8237 );
8238 }
8239
8240 #[test]
8241 fn apply_char_transform_capitalize_word_start() {
8242 assert_eq!(
8243 apply_char_transform('h', TextTransform::Capitalize, true),
8244 'H'
8245 );
8246 assert_eq!(
8247 apply_char_transform('h', TextTransform::Capitalize, false),
8248 'h'
8249 );
8250 }
8251
8252 #[test]
8255 fn column_flex_grow_single_child_fills_container() {
8256 let engine = LayoutEngine::new();
8259 let font_context = FontContext::new();
8260
8261 let child = make_styled_view(
8262 Style {
8263 flex_grow: Some(1.0),
8264 ..Default::default()
8265 },
8266 vec![make_text("Short", 12.0)],
8267 );
8268
8269 let container = make_styled_view(
8270 Style {
8271 flex_direction: Some(FlexDirection::Column),
8272 height: Some(Dimension::Pt(300.0)),
8273 ..Default::default()
8274 },
8275 vec![child],
8276 );
8277
8278 let doc = Document {
8279 children: vec![Node::page(
8280 PageConfig::default(),
8281 Style::default(),
8282 vec![container],
8283 )],
8284 metadata: Default::default(),
8285 default_page: PageConfig::default(),
8286 first_page: None,
8287 left_page: None,
8288 right_page: None,
8289 named_pages: Default::default(),
8290 attachments: vec![],
8291 zugferd: None,
8292 fonts: vec![],
8293 tagged: false,
8294 pdfa: None,
8295 default_style: None,
8296 embedded_data: None,
8297 flatten_forms: false,
8298 pdf_ua: false,
8299 certification: None,
8300 };
8301
8302 let pages = engine.layout(&doc, &font_context);
8303 let page = &pages[0];
8304
8305 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8306 assert!(container_el.is_some());
8307 let container_el = container_el.unwrap();
8308 assert!(
8309 (container_el.height - 300.0).abs() < 1.0,
8310 "Container should be 300pt, got {}",
8311 container_el.height
8312 );
8313
8314 let child_el = &container_el.children[0];
8315 assert!(
8316 (child_el.height - 300.0).abs() < 1.0,
8317 "flex-grow child should expand to 300pt, got {}",
8318 child_el.height
8319 );
8320 }
8321
8322 #[test]
8323 fn column_flex_grow_two_children_proportional() {
8324 let engine = LayoutEngine::new();
8327 let font_context = FontContext::new();
8328
8329 let child1 = make_styled_view(
8330 Style {
8331 flex_grow: Some(1.0),
8332 ..Default::default()
8333 },
8334 vec![make_text("A", 12.0)],
8335 );
8336 let child2 = make_styled_view(
8337 Style {
8338 flex_grow: Some(2.0),
8339 ..Default::default()
8340 },
8341 vec![make_text("B", 12.0)],
8342 );
8343
8344 let container = make_styled_view(
8345 Style {
8346 flex_direction: Some(FlexDirection::Column),
8347 height: Some(Dimension::Pt(300.0)),
8348 ..Default::default()
8349 },
8350 vec![child1, child2],
8351 );
8352
8353 let doc = Document {
8354 children: vec![Node::page(
8355 PageConfig::default(),
8356 Style::default(),
8357 vec![container],
8358 )],
8359 metadata: Default::default(),
8360 default_page: PageConfig::default(),
8361 first_page: None,
8362 left_page: None,
8363 right_page: None,
8364 named_pages: Default::default(),
8365 attachments: vec![],
8366 zugferd: None,
8367 fonts: vec![],
8368 tagged: false,
8369 pdfa: None,
8370 default_style: None,
8371 embedded_data: None,
8372 flatten_forms: false,
8373 pdf_ua: false,
8374 certification: None,
8375 };
8376
8377 let pages = engine.layout(&doc, &font_context);
8378 let page = &pages[0];
8379
8380 let container_el = page
8381 .elements
8382 .iter()
8383 .find(|e| e.children.len() == 2)
8384 .expect("Should find container with two children");
8385
8386 let c1 = &container_el.children[0];
8387 let c2 = &container_el.children[1];
8388
8389 let total = c1.height + c2.height;
8393 assert!(
8394 (total - 300.0).abs() < 2.0,
8395 "Children should sum to ~300pt, got {}",
8396 total
8397 );
8398
8399 let ratio = c2.height / c1.height;
8402 assert!(
8403 ratio > 1.3 && ratio < 2.5,
8404 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
8405 ratio
8406 );
8407 }
8408
8409 #[test]
8410 fn column_flex_grow_mixed_grow_and_fixed() {
8411 let engine = LayoutEngine::new();
8414 let font_context = FontContext::new();
8415
8416 let fixed_child = make_styled_view(
8417 Style {
8418 height: Some(Dimension::Pt(50.0)),
8419 ..Default::default()
8420 },
8421 vec![make_text("Fixed", 12.0)],
8422 );
8423 let grow_child = make_styled_view(
8424 Style {
8425 flex_grow: Some(1.0),
8426 ..Default::default()
8427 },
8428 vec![make_text("Grow", 12.0)],
8429 );
8430
8431 let container = make_styled_view(
8432 Style {
8433 flex_direction: Some(FlexDirection::Column),
8434 height: Some(Dimension::Pt(300.0)),
8435 ..Default::default()
8436 },
8437 vec![fixed_child, grow_child],
8438 );
8439
8440 let doc = Document {
8441 children: vec![Node::page(
8442 PageConfig::default(),
8443 Style::default(),
8444 vec![container],
8445 )],
8446 metadata: Default::default(),
8447 default_page: PageConfig::default(),
8448 first_page: None,
8449 left_page: None,
8450 right_page: None,
8451 named_pages: Default::default(),
8452 attachments: vec![],
8453 zugferd: None,
8454 fonts: vec![],
8455 tagged: false,
8456 pdfa: None,
8457 default_style: None,
8458 embedded_data: None,
8459 flatten_forms: false,
8460 pdf_ua: false,
8461 certification: None,
8462 };
8463
8464 let pages = engine.layout(&doc, &font_context);
8465 let page = &pages[0];
8466
8467 let container_el = page
8468 .elements
8469 .iter()
8470 .find(|e| e.children.len() == 2)
8471 .expect("Should find container with two children");
8472
8473 let fixed_el = &container_el.children[0];
8474 let grow_el = &container_el.children[1];
8475
8476 assert!(
8478 (fixed_el.height - 50.0).abs() < 1.0,
8479 "Fixed child should stay at 50pt, got {}",
8480 fixed_el.height
8481 );
8482
8483 assert!(
8485 (grow_el.height - 250.0).abs() < 2.0,
8486 "Grow child should expand to ~250pt, got {}",
8487 grow_el.height
8488 );
8489 }
8490
8491 #[test]
8492 fn column_flex_grow_page_level() {
8493 let engine = LayoutEngine::new();
8495 let font_context = FontContext::new();
8496
8497 let grow_child = make_styled_view(
8498 Style {
8499 flex_grow: Some(1.0),
8500 ..Default::default()
8501 },
8502 vec![make_text("Fill page", 12.0)],
8503 );
8504
8505 let doc = Document {
8506 children: vec![Node::page(
8507 PageConfig::default(),
8508 Style::default(),
8509 vec![grow_child],
8510 )],
8511 metadata: Default::default(),
8512 default_page: PageConfig::default(),
8513 first_page: None,
8514 left_page: None,
8515 right_page: None,
8516 named_pages: Default::default(),
8517 attachments: vec![],
8518 zugferd: None,
8519 fonts: vec![],
8520 tagged: false,
8521 pdfa: None,
8522 default_style: None,
8523 embedded_data: None,
8524 flatten_forms: false,
8525 pdf_ua: false,
8526 certification: None,
8527 };
8528
8529 let pages = engine.layout(&doc, &font_context);
8530 let page = &pages[0];
8531
8532 assert!(
8534 !page.elements.is_empty(),
8535 "Page should have at least one element"
8536 );
8537
8538 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
8539 let el = &page.elements[0];
8540 assert!(
8541 (el.height - content_height).abs() < 2.0,
8542 "Page-level flex-grow child should fill content height ({}), got {}",
8543 content_height,
8544 el.height
8545 );
8546 }
8547
8548 #[test]
8549 fn column_flex_grow_with_justify_content() {
8550 let engine = LayoutEngine::new();
8554 let font_context = FontContext::new();
8555
8556 let fixed_child = make_styled_view(
8557 Style {
8558 height: Some(Dimension::Pt(50.0)),
8559 ..Default::default()
8560 },
8561 vec![make_text("Top", 12.0)],
8562 );
8563 let grow_child = make_styled_view(
8564 Style {
8565 flex_grow: Some(1.0),
8566 ..Default::default()
8567 },
8568 vec![make_text("Fill", 12.0)],
8569 );
8570
8571 let container = make_styled_view(
8572 Style {
8573 flex_direction: Some(FlexDirection::Column),
8574 height: Some(Dimension::Pt(300.0)),
8575 justify_content: Some(JustifyContent::Center),
8576 ..Default::default()
8577 },
8578 vec![fixed_child, grow_child],
8579 );
8580
8581 let doc = Document {
8582 children: vec![Node::page(
8583 PageConfig::default(),
8584 Style::default(),
8585 vec![container],
8586 )],
8587 metadata: Default::default(),
8588 default_page: PageConfig::default(),
8589 first_page: None,
8590 left_page: None,
8591 right_page: None,
8592 named_pages: Default::default(),
8593 attachments: vec![],
8594 zugferd: None,
8595 fonts: vec![],
8596 tagged: false,
8597 pdfa: None,
8598 default_style: None,
8599 embedded_data: None,
8600 flatten_forms: false,
8601 pdf_ua: false,
8602 certification: None,
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");
8613
8614 let first_child = &container_el.children[0];
8617 assert!(
8618 (first_child.y - container_el.y).abs() < 1.0,
8619 "First child should be at top of container"
8620 );
8621
8622 let total = container_el.children[0].height + container_el.children[1].height;
8624 assert!(
8625 (total - 300.0).abs() < 2.0,
8626 "Children should fill container, got {}",
8627 total
8628 );
8629 }
8630
8631 #[test]
8632 fn column_flex_grow_child_justify_content_center() {
8633 let engine = LayoutEngine::new();
8636 let font_context = FontContext::new();
8637
8638 let inner_box = make_styled_view(
8640 Style {
8641 height: Some(Dimension::Pt(40.0)),
8642 ..Default::default()
8643 },
8644 vec![make_text("Centered", 12.0)],
8645 );
8646
8647 let grow_child = make_styled_view(
8649 Style {
8650 flex_grow: Some(1.0),
8651 flex_direction: Some(FlexDirection::Column),
8652 justify_content: Some(JustifyContent::Center),
8653 ..Default::default()
8654 },
8655 vec![inner_box],
8656 );
8657
8658 let container = make_styled_view(
8660 Style {
8661 flex_direction: Some(FlexDirection::Column),
8662 height: Some(Dimension::Pt(400.0)),
8663 ..Default::default()
8664 },
8665 vec![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 };
8691
8692 let pages = engine.layout(&doc, &font_context);
8693 let page = &pages[0];
8694
8695 let container_el = page
8697 .elements
8698 .iter()
8699 .find(|e| e.height > 350.0 && e.children.len() == 1)
8700 .expect("Should find outer container");
8701
8702 let grow_el = &container_el.children[0];
8703 assert!(
8704 (grow_el.height - 400.0).abs() < 2.0,
8705 "Grow child should expand to 400, got {}",
8706 grow_el.height
8707 );
8708
8709 let inner_el = &grow_el.children[0];
8711 let expected_center = grow_el.y + grow_el.height / 2.0;
8712 let actual_center = inner_el.y + inner_el.height / 2.0;
8713 assert!(
8714 (actual_center - expected_center).abs() < 2.0,
8715 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
8716 expected_center,
8717 actual_center
8718 );
8719 }
8720
8721 #[test]
8722 fn column_flex_grow_child_justify_content_flex_end() {
8723 let engine = LayoutEngine::new();
8725 let font_context = FontContext::new();
8726
8727 let inner_box = make_styled_view(
8728 Style {
8729 height: Some(Dimension::Pt(30.0)),
8730 ..Default::default()
8731 },
8732 vec![make_text("Bottom", 12.0)],
8733 );
8734
8735 let grow_child = make_styled_view(
8736 Style {
8737 flex_grow: Some(1.0),
8738 flex_direction: Some(FlexDirection::Column),
8739 justify_content: Some(JustifyContent::FlexEnd),
8740 ..Default::default()
8741 },
8742 vec![inner_box],
8743 );
8744
8745 let container = make_styled_view(
8746 Style {
8747 flex_direction: Some(FlexDirection::Column),
8748 height: Some(Dimension::Pt(300.0)),
8749 ..Default::default()
8750 },
8751 vec![grow_child],
8752 );
8753
8754 let doc = Document {
8755 children: vec![Node::page(
8756 PageConfig::default(),
8757 Style::default(),
8758 vec![container],
8759 )],
8760 metadata: Default::default(),
8761 default_page: PageConfig::default(),
8762 first_page: None,
8763 left_page: None,
8764 right_page: None,
8765 named_pages: Default::default(),
8766 attachments: vec![],
8767 zugferd: None,
8768 fonts: vec![],
8769 tagged: false,
8770 pdfa: None,
8771 default_style: None,
8772 embedded_data: None,
8773 flatten_forms: false,
8774 pdf_ua: false,
8775 certification: None,
8776 };
8777
8778 let pages = engine.layout(&doc, &font_context);
8779 let page = &pages[0];
8780
8781 let container_el = page
8782 .elements
8783 .iter()
8784 .find(|e| e.height > 250.0 && e.children.len() == 1)
8785 .expect("Should find outer container");
8786
8787 let grow_el = &container_el.children[0];
8788 let inner_el = &grow_el.children[0];
8789
8790 let inner_bottom = inner_el.y + inner_el.height;
8792 let grow_bottom = grow_el.y + grow_el.height;
8793 assert!(
8794 (inner_bottom - grow_bottom).abs() < 2.0,
8795 "Inner box bottom ({}) should align with grow child bottom ({})",
8796 inner_bottom,
8797 grow_bottom
8798 );
8799 }
8800
8801 #[test]
8802 fn column_flex_grow_child_no_justify_unchanged() {
8803 let engine = LayoutEngine::new();
8805 let font_context = FontContext::new();
8806
8807 let inner_box = make_styled_view(
8808 Style {
8809 height: Some(Dimension::Pt(50.0)),
8810 ..Default::default()
8811 },
8812 vec![make_text("Top", 12.0)],
8813 );
8814
8815 let grow_child = make_styled_view(
8816 Style {
8817 flex_grow: Some(1.0),
8818 flex_direction: Some(FlexDirection::Column),
8819 ..Default::default()
8821 },
8822 vec![inner_box],
8823 );
8824
8825 let container = make_styled_view(
8826 Style {
8827 flex_direction: Some(FlexDirection::Column),
8828 height: Some(Dimension::Pt(300.0)),
8829 ..Default::default()
8830 },
8831 vec![grow_child],
8832 );
8833
8834 let doc = Document {
8835 children: vec![Node::page(
8836 PageConfig::default(),
8837 Style::default(),
8838 vec![container],
8839 )],
8840 metadata: Default::default(),
8841 default_page: PageConfig::default(),
8842 first_page: None,
8843 left_page: None,
8844 right_page: None,
8845 named_pages: Default::default(),
8846 attachments: vec![],
8847 zugferd: None,
8848 fonts: vec![],
8849 tagged: false,
8850 pdfa: None,
8851 default_style: None,
8852 embedded_data: None,
8853 flatten_forms: false,
8854 pdf_ua: false,
8855 certification: None,
8856 };
8857
8858 let pages = engine.layout(&doc, &font_context);
8859 let page = &pages[0];
8860
8861 let container_el = page
8862 .elements
8863 .iter()
8864 .find(|e| e.height > 250.0 && e.children.len() == 1)
8865 .expect("Should find outer container");
8866
8867 let grow_el = &container_el.children[0];
8868 let inner_el = &grow_el.children[0];
8869
8870 assert!(
8872 (inner_el.y - grow_el.y).abs() < 2.0,
8873 "Inner box ({}) should be at top of grow child ({})",
8874 inner_el.y,
8875 grow_el.y
8876 );
8877 }
8878
8879 #[test]
8880 fn column_flex_grow_child_align_items_center() {
8881 let engine = LayoutEngine::new();
8883 let font_context = FontContext::new();
8884
8885 let text = make_text("Hello", 12.0);
8886
8887 let grow_child = make_styled_view(
8888 Style {
8889 flex_grow: Some(1.0),
8890 flex_direction: Some(FlexDirection::Column),
8891 align_items: Some(AlignItems::Center),
8892 ..Default::default()
8893 },
8894 vec![text],
8895 );
8896
8897 let container = make_styled_view(
8898 Style {
8899 flex_direction: Some(FlexDirection::Column),
8900 height: Some(Dimension::Pt(300.0)),
8901 ..Default::default()
8902 },
8903 vec![grow_child],
8904 );
8905
8906 let doc = Document {
8907 children: vec![Node::page(
8908 PageConfig::default(),
8909 Style::default(),
8910 vec![container],
8911 )],
8912 metadata: Default::default(),
8913 default_page: PageConfig::default(),
8914 first_page: None,
8915 left_page: None,
8916 right_page: None,
8917 named_pages: Default::default(),
8918 attachments: vec![],
8919 zugferd: None,
8920 fonts: vec![],
8921 tagged: false,
8922 pdfa: None,
8923 default_style: None,
8924 embedded_data: None,
8925 flatten_forms: false,
8926 pdf_ua: false,
8927 certification: None,
8928 };
8929
8930 let pages = engine.layout(&doc, &font_context);
8931 let page = &pages[0];
8932
8933 let container_el = page
8934 .elements
8935 .iter()
8936 .find(|e| e.height > 250.0 && e.children.len() == 1)
8937 .expect("Should find outer container");
8938
8939 let grow_el = &container_el.children[0];
8940 assert!(
8941 !grow_el.children.is_empty(),
8942 "Grow child should have text child"
8943 );
8944
8945 let text_el = &grow_el.children[0];
8946 let text_center = text_el.x + text_el.width / 2.0;
8947 let grow_center = grow_el.x + grow_el.width / 2.0;
8948 assert!(
8949 (text_center - grow_center).abs() < 2.0,
8950 "Text center ({}) should be near grow child center ({})",
8951 text_center,
8952 grow_center
8953 );
8954 }
8955
8956 #[test]
8957 fn image_intrinsic_width_respects_height_constraint() {
8958 let engine = LayoutEngine::new();
8962 let font_context = FontContext::new();
8963
8964 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
8966
8967 let image_node = Node {
8968 kind: NodeKind::Image {
8969 src: one_px_png.to_string(),
8970 width: None,
8971 height: Some(36.0),
8972 },
8973 style: Style::default(),
8974 children: vec![],
8975 id: None,
8976 source_location: None,
8977 bookmark: None,
8978 href: None,
8979 alt: None,
8980 };
8981
8982 let resolved = image_node.style.resolve(None, 0.0);
8983 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
8984
8985 assert!(
8987 (intrinsic - 36.0).abs() < 1.0,
8988 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
8989 intrinsic
8990 );
8991 }
8992}