1pub mod flex;
35pub mod grid;
36pub mod page_break;
37
38use std::cell::RefCell;
39use std::collections::HashMap;
40use std::sync::Arc;
41
42use serde::Serialize;
43
44use crate::font::FontContext;
45use crate::model::*;
46use crate::style::*;
47use crate::text::bidi;
48use crate::text::shaping;
49use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
50
51#[derive(Debug, Clone, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct BookmarkEntry {
55 pub title: String,
56 pub page_index: usize,
57 pub y: f64,
58}
59
60#[derive(Debug, Clone, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct LayoutInfo {
66 pub pages: Vec<PageInfo>,
67}
68
69#[derive(Debug, Clone, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct PageInfo {
73 pub width: f64,
74 pub height: f64,
75 pub content_x: f64,
76 pub content_y: f64,
77 pub content_width: f64,
78 pub content_height: f64,
79 pub elements: Vec<ElementInfo>,
80}
81
82#[derive(Debug, Clone, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ElementStyleInfo {
86 pub margin: Edges,
88 pub padding: Edges,
89 pub border_width: Edges,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub width: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub height: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub min_width: Option<f64>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub min_height: Option<f64>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub max_width: Option<f64>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub max_height: Option<f64>,
102 pub flex_direction: FlexDirection,
104 pub justify_content: JustifyContent,
105 pub align_items: AlignItems,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub align_self: Option<AlignItems>,
108 pub flex_wrap: FlexWrap,
109 pub align_content: AlignContent,
110 pub flex_grow: f64,
111 pub flex_shrink: f64,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub flex_basis: Option<String>,
114 pub gap: f64,
115 pub row_gap: f64,
116 pub column_gap: f64,
117 pub font_family: String,
119 pub font_size: f64,
120 pub font_weight: u32,
121 pub font_style: FontStyle,
122 pub line_height: f64,
123 pub text_align: TextAlign,
124 pub letter_spacing: f64,
125 pub text_decoration: TextDecoration,
126 pub text_transform: TextTransform,
127 pub color: Color,
129 pub background_color: Option<Color>,
130 pub border_color: EdgeValues<Color>,
131 pub border_style: EdgeValues<crate::style::BorderStyle>,
132 pub border_radius: CornerValues,
133 pub opacity: f64,
134 pub position: Position,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub top: Option<f64>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub right: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub bottom: Option<f64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub left: Option<f64>,
144 pub overflow: Overflow,
146 pub breakable: bool,
148 pub break_before: bool,
149 pub min_widow_lines: u32,
150 pub min_orphan_lines: u32,
151}
152
153fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
154 match sc {
155 SizeConstraint::Auto => None,
156 SizeConstraint::Fixed(v) => Some(format!("{v}")),
157 }
158}
159
160impl ElementStyleInfo {
161 fn from_resolved(style: &ResolvedStyle) -> Self {
162 ElementStyleInfo {
163 margin: style.margin.to_edges(),
164 padding: style.padding,
165 border_width: style.border_width,
166 width: size_constraint_to_str(&style.width),
167 height: size_constraint_to_str(&style.height),
168 min_width: if style.min_width > 0.0 {
169 Some(style.min_width)
170 } else {
171 None
172 },
173 min_height: if style.min_height > 0.0 {
174 Some(style.min_height)
175 } else {
176 None
177 },
178 max_width: if style.max_width.is_finite() {
179 Some(style.max_width)
180 } else {
181 None
182 },
183 max_height: if style.max_height.is_finite() {
184 Some(style.max_height)
185 } else {
186 None
187 },
188 flex_direction: style.flex_direction,
189 justify_content: style.justify_content,
190 align_items: style.align_items,
191 align_self: style.align_self,
192 flex_wrap: style.flex_wrap,
193 align_content: style.align_content,
194 flex_grow: style.flex_grow,
195 flex_shrink: style.flex_shrink,
196 flex_basis: size_constraint_to_str(&style.flex_basis),
197 gap: style.gap,
198 row_gap: style.row_gap,
199 column_gap: style.column_gap,
200 font_family: style.font_family.clone(),
201 font_size: style.font_size,
202 font_weight: style.font_weight,
203 font_style: style.font_style,
204 line_height: style.line_height,
205 text_align: style.text_align,
206 letter_spacing: style.letter_spacing,
207 text_decoration: style.text_decoration,
208 text_transform: style.text_transform,
209 color: style.color,
210 background_color: style.background_color,
211 border_color: style.border_color,
212 border_style: style.border_style,
213 border_radius: style.border_radius,
214 opacity: style.opacity,
215 position: style.position,
216 top: style.top,
217 right: style.right,
218 bottom: style.bottom,
219 left: style.left,
220 overflow: style.overflow,
221 breakable: style.breakable,
222 break_before: style.break_before,
223 min_widow_lines: style.min_widow_lines,
224 min_orphan_lines: style.min_orphan_lines,
225 }
226 }
227}
228
229impl Default for ElementStyleInfo {
230 fn default() -> Self {
231 ElementStyleInfo {
232 margin: Edges::default(),
233 padding: Edges::default(),
234 border_width: Edges::default(),
235 width: None,
236 height: None,
237 min_width: None,
238 min_height: None,
239 max_width: None,
240 max_height: None,
241 flex_direction: FlexDirection::default(),
242 justify_content: JustifyContent::default(),
243 align_items: AlignItems::default(),
244 align_self: None,
245 flex_wrap: FlexWrap::default(),
246 align_content: AlignContent::default(),
247 flex_grow: 0.0,
248 flex_shrink: 1.0,
249 flex_basis: None,
250 gap: 0.0,
251 row_gap: 0.0,
252 column_gap: 0.0,
253 font_family: "Helvetica".to_string(),
254 font_size: 12.0,
255 font_weight: 400,
256 font_style: FontStyle::default(),
257 line_height: 1.4,
258 text_align: TextAlign::default(),
259 letter_spacing: 0.0,
260 text_decoration: TextDecoration::None,
261 text_transform: TextTransform::None,
262 color: Color::BLACK,
263 background_color: None,
264 border_color: EdgeValues::uniform(Color::BLACK),
265 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
266 border_radius: CornerValues::uniform(0.0),
267 opacity: 1.0,
268 position: Position::default(),
269 top: None,
270 right: None,
271 bottom: None,
272 left: None,
273 overflow: Overflow::default(),
274 breakable: false,
275 break_before: false,
276 min_widow_lines: 2,
277 min_orphan_lines: 2,
278 }
279 }
280}
281
282#[derive(Debug, Clone, Serialize)]
284#[serde(rename_all = "camelCase")]
285pub struct ElementInfo {
286 pub x: f64,
287 pub y: f64,
288 pub width: f64,
289 pub height: f64,
290 pub kind: String,
292 pub node_type: String,
294 pub style: ElementStyleInfo,
296 pub children: Vec<ElementInfo>,
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub source_location: Option<SourceLocation>,
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub text_content: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub href: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub bookmark: Option<String>,
310}
311
312impl LayoutInfo {
313 pub fn from_pages(pages: &[LayoutPage]) -> Self {
315 LayoutInfo {
316 pages: pages
317 .iter()
318 .map(|page| {
319 let (page_w, page_h) = page.config.size.dimensions();
320 let content_x = page.config.margin.left;
321 let content_y = page.config.margin.top;
322 let content_width = page_w - page.config.margin.horizontal();
323 let content_height = page_h - page.config.margin.vertical();
324
325 let elements = Self::build_element_tree(&page.elements);
326
327 PageInfo {
328 width: page_w,
329 height: page_h,
330 content_x,
331 content_y,
332 content_width,
333 content_height,
334 elements,
335 }
336 })
337 .collect(),
338 }
339 }
340
341 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
342 elems
343 .iter()
344 .map(|elem| {
345 let kind = match &elem.draw {
346 DrawCommand::None => "None",
347 DrawCommand::Rect { .. } => "Rect",
348 DrawCommand::Text { .. } => "Text",
349 DrawCommand::Image { .. } => "Image",
350 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
351 DrawCommand::Svg { .. } => "Svg",
352 DrawCommand::Barcode { .. } => "Barcode",
353 DrawCommand::QrCode { .. } => "QrCode",
354 DrawCommand::Chart { .. } => "Chart",
355 DrawCommand::Watermark { .. } => "Watermark",
356 DrawCommand::FormField { .. } => "FormField",
357 };
358 let text_content = match &elem.draw {
359 DrawCommand::Text { lines, .. } => {
360 let text: String = lines
361 .iter()
362 .flat_map(|line| {
363 line.glyphs.iter().flat_map(|g| {
364 g.cluster_text.as_deref().unwrap_or("").chars().chain(
366 if g.cluster_text.is_none() {
367 Some(g.char_value)
368 } else {
369 None
370 },
371 )
372 })
373 })
374 .collect();
375 if text.is_empty() {
376 None
377 } else {
378 Some(text)
379 }
380 }
381 _ => None,
382 };
383 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
384 let style = elem
385 .resolved_style
386 .as_ref()
387 .map(ElementStyleInfo::from_resolved)
388 .unwrap_or_default();
389 ElementInfo {
390 x: elem.x,
391 y: elem.y,
392 width: elem.width,
393 height: elem.height,
394 kind: kind.to_string(),
395 node_type,
396 style,
397 children: Self::build_element_tree(&elem.children),
398 source_location: elem.source_location.clone(),
399 text_content,
400 href: elem.href.clone(),
401 bookmark: elem.bookmark.clone(),
402 }
403 })
404 .collect()
405 }
406}
407
408#[derive(Debug, Clone)]
410pub struct LayoutPage {
411 pub width: f64,
412 pub height: f64,
413 pub elements: Vec<LayoutElement>,
414 pub(crate) fixed_header: Vec<(Node, f64)>,
416 pub(crate) fixed_footer: Vec<(Node, f64)>,
418 pub(crate) watermarks: Vec<Node>,
420 pub(crate) config: PageConfig,
422 pub(crate) page_name: Option<String>,
424}
425
426#[derive(Debug, Clone)]
428pub struct LayoutElement {
429 pub x: f64,
431 pub y: f64,
432 pub width: f64,
434 pub height: f64,
435 pub draw: DrawCommand,
437 pub children: Vec<LayoutElement>,
439 pub node_type: Option<String>,
441 pub resolved_style: Option<ResolvedStyle>,
443 pub source_location: Option<SourceLocation>,
445 pub href: Option<String>,
447 pub bookmark: Option<String>,
449 pub alt: Option<String>,
451 pub is_header_row: bool,
453 pub col_span: u32,
456 pub overflow: Overflow,
458 pub opacity: f64,
463}
464
465fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
468 match &node.kind {
469 NodeKind::Fixed {
470 pages,
471 page_name: only,
472 exclude_page_names,
473 ..
474 } => {
475 pages.applies(page_index)
476 && match only {
477 Some(n) => page_name == Some(n.as_str()),
478 None => true,
479 }
480 && !exclude_page_names
481 .iter()
482 .any(|n| page_name == Some(n.as_str()))
483 }
484 _ => true,
485 }
486}
487
488fn node_kind_name(kind: &NodeKind) -> &'static str {
490 match kind {
491 NodeKind::View => "View",
492 NodeKind::Text { .. } => "Text",
493 NodeKind::Heading { level: 1, .. } => "H1",
494 NodeKind::Heading { level: 2, .. } => "H2",
495 NodeKind::Heading { level: 3, .. } => "H3",
496 NodeKind::Heading { level: 4, .. } => "H4",
497 NodeKind::Heading { level: 5, .. } => "H5",
498 NodeKind::Heading { .. } => "H6",
502 NodeKind::List { .. } => "List",
503 NodeKind::ListItem => "ListItem",
504 NodeKind::Image { .. } => "Image",
505 NodeKind::Table { .. } => "Table",
506 NodeKind::TableRow { .. } => "TableRow",
507 NodeKind::TableCell { .. } => "TableCell",
508 NodeKind::Fixed {
509 position: FixedPosition::Header,
510 ..
511 } => "FixedHeader",
512 NodeKind::Fixed {
513 position: FixedPosition::Footer,
514 ..
515 } => "FixedFooter",
516 NodeKind::Page { .. } => "Page",
517 NodeKind::PageBreak => "PageBreak",
518 NodeKind::PageName { .. } => "PageName",
519 NodeKind::Svg { .. } => "Svg",
520 NodeKind::Canvas { .. } => "Canvas",
521 NodeKind::Barcode { .. } => "Barcode",
522 NodeKind::QrCode { .. } => "QrCode",
523 NodeKind::BarChart { .. } => "BarChart",
524 NodeKind::LineChart { .. } => "LineChart",
525 NodeKind::PieChart { .. } => "PieChart",
526 NodeKind::AreaChart { .. } => "AreaChart",
527 NodeKind::DotPlot { .. } => "DotPlot",
528 NodeKind::Watermark { .. } => "Watermark",
529 NodeKind::TextField { .. } => "TextField",
530 NodeKind::Checkbox { .. } => "Checkbox",
531 NodeKind::Dropdown { .. } => "Dropdown",
532 NodeKind::RadioButton { .. } => "RadioButton",
533 }
534}
535
536fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
555 node.bookmark.as_ref().map(|title| LayoutElement {
556 x,
557 y,
558 width: 0.0,
559 height: 0.0,
560 draw: DrawCommand::None,
561 children: vec![],
562 node_type: Some("Bookmark".to_string()),
563 resolved_style: None,
564 source_location: None,
565 href: None,
566 bookmark: Some(title.clone()),
567 alt: None,
568 is_header_row: false,
569 col_span: 1,
570 overflow: Overflow::default(),
571 opacity: 1.0,
572 })
573}
574
575fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
582 if !ordered {
583 return match marker_type {
587 ListMarkerType::None => String::new(),
588 _ => "•".to_string(),
589 };
590 }
591 let body = match marker_type {
592 ListMarkerType::LowerAlpha => to_alpha(idx, false),
593 ListMarkerType::UpperAlpha => to_alpha(idx, true),
594 ListMarkerType::LowerRoman => to_roman(idx, false),
595 ListMarkerType::UpperRoman => to_roman(idx, true),
596 ListMarkerType::None => return String::new(),
597 _ => idx.to_string(),
600 };
601 format!("{body}.")
602}
603
604fn to_alpha(mut n: u32, upper: bool) -> String {
607 if n == 0 {
608 return String::new();
609 }
610 let base = if upper { b'A' } else { b'a' };
611 let mut bytes: Vec<u8> = Vec::new();
612 while n > 0 {
613 let rem = ((n - 1) % 26) as u8;
614 bytes.push(base + rem);
615 n = (n - 1) / 26;
616 }
617 bytes.reverse();
618 String::from_utf8(bytes).unwrap_or_default()
619}
620
621fn to_roman(n: u32, upper: bool) -> String {
625 if n == 0 || n > 3999 {
626 return n.to_string();
627 }
628 const UPPER: &[(&str, u32)] = &[
629 ("M", 1000),
630 ("CM", 900),
631 ("D", 500),
632 ("CD", 400),
633 ("C", 100),
634 ("XC", 90),
635 ("L", 50),
636 ("XL", 40),
637 ("X", 10),
638 ("IX", 9),
639 ("V", 5),
640 ("IV", 4),
641 ("I", 1),
642 ];
643 const LOWER: &[(&str, u32)] = &[
644 ("m", 1000),
645 ("cm", 900),
646 ("d", 500),
647 ("cd", 400),
648 ("c", 100),
649 ("xc", 90),
650 ("l", 50),
651 ("xl", 40),
652 ("x", 10),
653 ("ix", 9),
654 ("v", 5),
655 ("iv", 4),
656 ("i", 1),
657 ];
658 let table = if upper { UPPER } else { LOWER };
659 let mut out = String::new();
660 let mut remaining = n;
661 for &(sym, val) in table {
662 while remaining >= val {
663 out.push_str(sym);
664 remaining -= val;
665 }
666 }
667 out
668}
669
670fn compute_marker_gutter_width(
677 ordered: bool,
678 marker_type: ListMarkerType,
679 start: u32,
680 n_items: u32,
681 style: &ResolvedStyle,
682) -> f64 {
683 if matches!(marker_type, ListMarkerType::None) {
684 return 0.0;
685 }
686 let approx_char_w = style.font_size * 0.6;
688 let gap = 6.0_f64;
689 if !ordered {
690 return approx_char_w + gap;
692 }
693 let last_idx = start + n_items.saturating_sub(1);
695 let widest = match marker_type {
696 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
697 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
698 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
699 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
700 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
701 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
702 };
703 widest.chars().count() as f64 * approx_char_w + gap
704}
705
706#[derive(Debug, Clone)]
708pub enum FormFieldType {
709 TextField {
710 value: Option<String>,
711 placeholder: Option<String>,
712 multiline: bool,
713 password: bool,
714 read_only: bool,
715 max_length: Option<u32>,
716 font_size: f64,
717 },
718 Checkbox {
719 checked: bool,
720 read_only: bool,
721 },
722 Dropdown {
723 options: Vec<String>,
724 value: Option<String>,
725 read_only: bool,
726 font_size: f64,
727 },
728 RadioButton {
729 value: String,
730 checked: bool,
731 read_only: bool,
732 },
733}
734
735#[derive(Debug, Clone)]
737pub enum DrawCommand {
738 None,
740 Rect {
742 background: Option<Color>,
743 border_width: Edges,
744 border_color: EdgeValues<Color>,
745 border_style: EdgeValues<crate::style::BorderStyle>,
746 border_radius: CornerValues,
747 opacity: f64,
748 box_shadow: Option<Box<crate::style::BoxShadow>>,
751 background_gradient: Option<Box<crate::style::Background>>,
755 },
756 Text {
758 lines: Vec<TextLine>,
759 color: Color,
760 text_decoration: TextDecoration,
761 opacity: f64,
762 },
763 Image {
765 image_data: crate::image_loader::LoadedImage,
766 },
767 ImagePlaceholder,
769 Svg {
771 commands: Vec<crate::svg::SvgCommand>,
772 width: f64,
774 height: f64,
776 viewbox_min_x: f64,
780 viewbox_min_y: f64,
781 viewbox_width: f64,
782 viewbox_height: f64,
783 clip: bool,
785 },
786 Barcode {
788 bars: Vec<u8>,
789 bar_width: f64,
790 height: f64,
791 color: Color,
792 },
793 QrCode {
795 modules: Vec<Vec<bool>>,
796 module_size: f64,
797 color: Color,
798 },
799 Chart {
801 primitives: Vec<crate::chart::ChartPrimitive>,
802 },
803 Watermark {
805 lines: Vec<TextLine>,
806 color: Color,
807 opacity: f64,
808 angle_rad: f64,
809 font_family: String,
811 },
812 FormField {
814 field_type: FormFieldType,
815 name: String,
816 },
817}
818
819#[derive(Debug, Clone)]
820pub struct TextLine {
821 pub x: f64,
822 pub y: f64,
823 pub glyphs: Vec<PositionedGlyph>,
824 pub width: f64,
825 pub height: f64,
826 pub word_spacing: f64,
828}
829
830#[derive(Debug, Clone)]
831pub struct PositionedGlyph {
832 pub glyph_id: u16,
835 pub x_offset: f64,
837 pub y_offset: f64,
839 pub x_advance: f64,
841 pub font_size: f64,
842 pub font_family: Arc<str>,
848 pub font_weight: u32,
849 pub font_style: FontStyle,
850 pub char_value: char,
852 pub color: Option<Color>,
854 pub href: Option<String>,
856 pub text_decoration: TextDecoration,
858 pub letter_spacing: f64,
860 pub cluster_text: Option<String>,
863}
864
865fn offset_element_y(el: &mut LayoutElement, dy: f64) {
868 el.y += dy;
869 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
870 for line in lines.iter_mut() {
871 line.y += dy;
872 }
873 }
874 for child in &mut el.children {
875 offset_element_y(child, dy);
876 }
877}
878
879fn offset_element_x(el: &mut LayoutElement, dx: f64) {
881 el.x += dx;
882 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
883 for line in lines.iter_mut() {
884 line.x += dx;
885 }
886 }
887 for child in &mut el.children {
888 offset_element_x(child, dx);
889 }
890}
891
892fn reapply_justify_content(elem: &mut LayoutElement) {
896 let style = match elem.resolved_style {
897 Some(ref s) => s,
898 None => return,
899 };
900 if matches!(style.justify_content, JustifyContent::FlexStart) {
901 return;
902 }
903 if elem.children.is_empty() {
904 return;
905 }
906
907 let padding_top = style.padding.top + style.border_width.top;
908 let padding_bottom = style.padding.bottom + style.border_width.bottom;
909 let inner_h = elem.height - padding_top - padding_bottom;
910 let content_top = elem.y + padding_top;
911
912 let last_child = &elem.children[elem.children.len() - 1];
914 let children_bottom = last_child.y + last_child.height;
915 let children_span = children_bottom - content_top;
916 let slack = inner_h - children_span;
917 if slack < 0.001 {
918 return;
919 }
920
921 let n = elem.children.len();
922 let offsets: Vec<f64> = match style.justify_content {
923 JustifyContent::FlexEnd => vec![slack; n],
924 JustifyContent::Center => vec![slack / 2.0; n],
925 JustifyContent::SpaceBetween => {
926 if n <= 1 {
927 vec![0.0; n]
928 } else {
929 let per_gap = slack / (n - 1) as f64;
930 (0..n).map(|i| i as f64 * per_gap).collect()
931 }
932 }
933 JustifyContent::SpaceAround => {
934 let space = slack / n as f64;
935 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
936 }
937 JustifyContent::SpaceEvenly => {
938 let space = slack / (n + 1) as f64;
939 (0..n).map(|i| (i + 1) as f64 * space).collect()
940 }
941 JustifyContent::FlexStart => unreachable!(),
942 };
943
944 for (i, child) in elem.children.iter_mut().enumerate() {
945 let dy = offsets[i];
946 if dy.abs() > 0.001 {
947 offset_element_y(child, dy);
948 }
949 }
950}
951
952fn apply_text_transform(text: &str, transform: TextTransform) -> String {
954 match transform {
955 TextTransform::None => text.to_string(),
956 TextTransform::Uppercase => text.to_uppercase(),
957 TextTransform::Lowercase => text.to_lowercase(),
958 TextTransform::Capitalize => {
959 let mut result = String::with_capacity(text.len());
960 let mut prev_is_whitespace = true;
961 for ch in text.chars() {
962 if prev_is_whitespace && ch.is_alphabetic() {
963 for upper in ch.to_uppercase() {
964 result.push(upper);
965 }
966 } else {
967 result.push(ch);
968 }
969 prev_is_whitespace = ch.is_whitespace();
970 }
971 result
972 }
973 }
974}
975
976pub const PAGE_NUMBER_SENTINEL: char = '\x02';
980
981pub const TOTAL_PAGES_SENTINEL: char = '\x03';
983
984fn substitute_page_placeholders(text: &str) -> String {
989 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
990 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
991 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
992 } else {
993 text.to_string()
994 }
995}
996
997fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1000 match transform {
1001 TextTransform::None => ch,
1002 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1003 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1004 TextTransform::Capitalize => {
1005 if is_word_start && ch.is_alphabetic() {
1006 ch.to_uppercase().next().unwrap_or(ch)
1007 } else {
1008 ch
1009 }
1010 }
1011 }
1012}
1013
1014pub struct LayoutEngine {
1016 text_layout: TextLayout,
1017 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1018 warnings: RefCell<Vec<String>>,
1030 measure_check: bool,
1044}
1045
1046#[derive(Debug, Clone)]
1048struct PageCursor {
1049 config: PageConfig,
1050 base_config: PageConfig,
1053 page_index: usize,
1058 content_width: f64,
1059 content_height: f64,
1060 y: f64,
1061 elements: Vec<LayoutElement>,
1062 fixed_header: Vec<(Node, f64)>,
1063 fixed_footer: Vec<(Node, f64)>,
1064 watermarks: Vec<Node>,
1066 content_x: f64,
1067 content_y: f64,
1068 continuation_top_offset: f64,
1070 containing_block: (f64, f64, f64, f64),
1075 left_config: Option<PageConfig>,
1083 right_config: Option<PageConfig>,
1084 display_config: Option<PageConfig>,
1087 flow_anchor_left: f64,
1092 page_name: Option<String>,
1096 base_display: Option<PageConfig>,
1100 first_display: Option<PageConfig>,
1104 doc_base: PageConfig,
1107 doc_left: Option<PageConfig>,
1108 doc_right: Option<PageConfig>,
1109 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1111}
1112
1113impl PageCursor {
1114 fn new(config: &PageConfig) -> Self {
1115 let (page_w, page_h) = config.size.dimensions();
1116 let content_width = page_w - config.margin.horizontal();
1117 let content_height = page_h - config.margin.vertical();
1118
1119 Self {
1120 config: config.clone(),
1121 base_config: config.clone(),
1122 page_index: 0,
1123 content_width,
1124 content_height,
1125 y: 0.0,
1126 elements: Vec::new(),
1127 fixed_header: Vec::new(),
1128 fixed_footer: Vec::new(),
1129 watermarks: Vec::new(),
1130 content_x: config.margin.left,
1131 content_y: config.margin.top,
1132 continuation_top_offset: 0.0,
1133 containing_block: (
1134 config.margin.left,
1135 config.margin.top,
1136 content_width,
1137 content_height,
1138 ),
1139 left_config: None,
1140 right_config: None,
1141 display_config: None,
1142 flow_anchor_left: config.margin.left,
1143 page_name: None,
1144 base_display: None,
1145 first_display: None,
1146 doc_base: config.clone(),
1147 doc_left: None,
1148 doc_right: None,
1149 named_sets: std::collections::HashMap::new(),
1150 }
1151 }
1152
1153 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1160 if page_number == 1 {
1161 if let Some(first) = &self.first_display {
1162 return Some(first.clone());
1163 }
1164 }
1165 let parity = if page_number % 2 == 1 {
1166 self.right_config.clone()
1167 } else {
1168 self.left_config.clone()
1169 };
1170 parity.or_else(|| self.base_display.clone())
1171 }
1172
1173 fn fixed_applies(&self, node: &Node) -> bool {
1176 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1177 }
1178
1179 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1186 let set = name
1187 .as_deref()
1188 .and_then(|n| self.named_sets.get(n))
1189 .cloned();
1190 let real = set
1191 .as_ref()
1192 .map(|s| s.base.clone())
1193 .unwrap_or_else(|| self.doc_base.clone());
1194 let mut cursor = PageCursor::new(&real);
1195 cursor.page_index = if in_place {
1196 self.page_index
1197 } else {
1198 self.page_index + 1
1199 };
1200 cursor.page_name = name;
1201 cursor.doc_base = self.doc_base.clone();
1202 cursor.doc_left = self.doc_left.clone();
1203 cursor.doc_right = self.doc_right.clone();
1204 cursor.named_sets = self.named_sets.clone();
1205 match &set {
1206 Some(s) => {
1207 cursor.left_config = s.display_left.clone();
1208 cursor.right_config = s.display_right.clone();
1209 cursor.base_display = s.display.clone();
1210 cursor.first_display = s.display_first.clone();
1211 }
1212 None => {
1213 cursor.left_config = self.doc_left.clone();
1217 cursor.right_config = self.doc_right.clone();
1218 }
1219 }
1220 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1221 cursor.flow_anchor_left = self.flow_anchor_left;
1222 cursor.fixed_header = self.fixed_header.clone();
1223 cursor.fixed_footer = self.fixed_footer.clone();
1224 cursor.watermarks = self.watermarks.clone();
1225 cursor.continuation_top_offset = self.continuation_top_offset;
1226
1227 let header_height: f64 = cursor
1228 .fixed_header
1229 .iter()
1230 .filter(|(n, _)| cursor.fixed_applies(n))
1231 .map(|(_, h)| *h)
1232 .sum();
1233 cursor.y = header_height + cursor.continuation_top_offset;
1234 cursor
1235 }
1236
1237 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1240 let mut cursor = PageCursor::new(first);
1241 cursor.base_config = base.clone();
1242 cursor
1243 }
1244
1245 fn remaining_height(&self) -> f64 {
1246 let footer_height: f64 = self
1247 .fixed_footer
1248 .iter()
1249 .filter(|(n, _)| self.fixed_applies(n))
1250 .map(|(_, h)| *h)
1251 .sum();
1252 (self.content_height - self.y - footer_height).max(0.0)
1253 }
1254
1255 fn finalize(&self) -> LayoutPage {
1256 let (page_w, page_h) = self.config.size.dimensions();
1257
1258 let (elements, config) = match &self.display_config {
1264 Some(display) => {
1265 let dx = display.margin.left - self.flow_anchor_left;
1266 let mut elements = self.elements.clone();
1267 if dx != 0.0 {
1268 fn shift(els: &mut [LayoutElement], dx: f64) {
1269 for el in els {
1270 el.x += dx;
1271 shift(&mut el.children, dx);
1272 }
1273 }
1274 shift(&mut elements, dx);
1275 }
1276 (elements, display.clone())
1277 }
1278 None => (self.elements.clone(), self.config.clone()),
1279 };
1280
1281 LayoutPage {
1282 width: page_w,
1283 height: page_h,
1284 elements,
1285 fixed_header: self.fixed_header.clone(),
1286 fixed_footer: self.fixed_footer.clone(),
1287 watermarks: self.watermarks.clone(),
1288 config,
1289 page_name: self.page_name.clone(),
1290 }
1291 }
1292
1293 fn new_page(&self) -> Self {
1294 let mut cursor = PageCursor::new(&self.base_config);
1297 cursor.page_index = self.page_index + 1;
1298 cursor.page_name = self.page_name.clone();
1299 cursor.left_config = self.left_config.clone();
1300 cursor.right_config = self.right_config.clone();
1301 cursor.base_display = self.base_display.clone();
1302 cursor.first_display = self.first_display.clone();
1303 cursor.doc_base = self.doc_base.clone();
1304 cursor.doc_left = self.doc_left.clone();
1305 cursor.doc_right = self.doc_right.clone();
1306 cursor.named_sets = self.named_sets.clone();
1307 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1308 cursor.flow_anchor_left = self.flow_anchor_left;
1309 cursor.fixed_header = self.fixed_header.clone();
1310 cursor.fixed_footer = self.fixed_footer.clone();
1311 cursor.watermarks = self.watermarks.clone();
1312 cursor.continuation_top_offset = self.continuation_top_offset;
1313
1314 let header_height: f64 = cursor
1315 .fixed_header
1316 .iter()
1317 .filter(|(n, _)| cursor.fixed_applies(n))
1318 .map(|(_, h)| *h)
1319 .sum();
1320 cursor.y = header_height + cursor.continuation_top_offset;
1321
1322 cursor
1323 }
1324}
1325
1326impl Default for LayoutEngine {
1327 fn default() -> Self {
1328 Self::new()
1329 }
1330}
1331
1332impl LayoutEngine {
1333 pub fn new() -> Self {
1334 Self {
1335 text_layout: TextLayout::new(),
1336 image_dim_cache: RefCell::new(HashMap::new()),
1337 warnings: RefCell::new(Vec::new()),
1338 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1339 }
1340 }
1341
1342 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1344 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1345 return Some(*dims);
1346 }
1347 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1348 self.image_dim_cache
1349 .borrow_mut()
1350 .insert(src.to_string(), dims);
1351 Some(dims)
1352 } else {
1353 None
1354 }
1355 }
1356
1357 fn defect(&self, msg: String) {
1360 let mut w = self.warnings.borrow_mut();
1361 if !w.contains(&msg) {
1364 w.push(msg);
1365 }
1366 }
1367
1368 pub fn take_warnings(&self) -> Vec<String> {
1372 std::mem::take(&mut *self.warnings.borrow_mut())
1373 }
1374
1375 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1377 self.warnings.borrow_mut().clear();
1378 let mut pages: Vec<LayoutPage> = Vec::new();
1379 let mut cursor = match &document.first_page {
1380 Some(first) => PageCursor::new_first(first, &document.default_page),
1381 None => PageCursor::new(&document.default_page),
1382 };
1383 cursor.left_config = document.left_page.clone();
1389 cursor.right_config = document.right_page.clone();
1390 cursor.doc_base = document.default_page.clone();
1392 cursor.doc_left = document.left_page.clone();
1393 cursor.doc_right = document.right_page.clone();
1394 cursor.named_sets = document.named_pages.clone();
1395 if document.first_page.is_none() {
1396 cursor.display_config = cursor.display_for(1);
1397 }
1398
1399 let base = document.default_style.clone().unwrap_or_default();
1401 let root_style = Style {
1402 lang: base.lang.clone().or(document.metadata.lang.clone()),
1403 ..base
1404 }
1405 .resolve(None, cursor.content_width);
1406
1407 for node in &document.children {
1408 match &node.kind {
1409 NodeKind::Page { config } => {
1410 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1411 pages.push(cursor.finalize());
1412 }
1413 cursor = PageCursor::new(config);
1414 cursor.page_index = pages.len();
1415
1416 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1423 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1424
1425 let cx = cursor.content_x;
1426 let cw = cursor.content_width;
1427 self.layout_children(
1428 &node.children,
1429 &node.style,
1430 &mut cursor,
1431 &mut pages,
1432 cx,
1433 cw,
1434 Some(&page_root),
1435 font_context,
1436 );
1437 }
1438 NodeKind::PageBreak => {
1439 pages.push(cursor.finalize());
1440 cursor = cursor.new_page();
1441 }
1442 _ => {
1443 let cx = cursor.content_x;
1444 let cw = cursor.content_width;
1445 self.layout_node(
1446 node,
1447 &mut cursor,
1448 &mut pages,
1449 cx,
1450 cw,
1451 Some(&root_style),
1452 font_context,
1453 None,
1454 None,
1455 );
1456 }
1457 }
1458 }
1459
1460 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1461 pages.push(cursor.finalize());
1462 }
1463
1464 self.inject_fixed_elements(&mut pages, font_context);
1465
1466 pages
1467 }
1468
1469 #[allow(clippy::too_many_arguments)]
1470 fn layout_node(
1471 &self,
1472 node: &Node,
1473 cursor: &mut PageCursor,
1474 pages: &mut Vec<LayoutPage>,
1475 x: f64,
1476 available_width: f64,
1477 parent_style: Option<&ResolvedStyle>,
1478 font_context: &FontContext,
1479 cross_axis_height: Option<f64>,
1480 forced_outer_width: Option<f64>,
1481 ) {
1482 let mut style = node.style.resolve(parent_style, available_width);
1483
1484 if let Some(h) = cross_axis_height {
1487 if matches!(style.height, SizeConstraint::Auto) {
1488 style.height = SizeConstraint::Fixed(h);
1489 }
1490 }
1491
1492 if let Some(w) = forced_outer_width {
1497 style.width = SizeConstraint::Fixed(w);
1498 }
1499
1500 if style.break_before && !cursor.elements.is_empty() {
1511 pages.push(cursor.finalize());
1512 *cursor = cursor.new_page();
1513 }
1514
1515 let elem_start = cursor.elements.len();
1518
1519 match &node.kind {
1520 NodeKind::PageBreak => {
1521 pages.push(cursor.finalize());
1522 *cursor = cursor.new_page();
1523 }
1524
1525 NodeKind::PageName { name } => {
1526 if cursor.page_name.as_deref() != name.as_deref() {
1532 let fresh_y: f64 = cursor
1535 .fixed_header
1536 .iter()
1537 .filter(|(n, _)| cursor.fixed_applies(n))
1538 .map(|(_, h)| *h)
1539 .sum::<f64>()
1540 + cursor.continuation_top_offset;
1541 let has_content = !cursor.elements.is_empty() || cursor.y > fresh_y + 0.01;
1542 if has_content {
1543 pages.push(cursor.finalize());
1544 }
1545 *cursor = cursor.renamed_page(name.clone(), !has_content);
1546 }
1547 }
1548
1549 NodeKind::Fixed {
1550 position, pages, ..
1551 } => {
1552 let height = self.measure_node_height(node, available_width, &style, font_context);
1553 match position {
1554 FixedPosition::Header => {
1555 cursor.fixed_header.push((node.clone(), height));
1556 let _ = pages;
1560 if cursor.fixed_applies(node) {
1561 cursor.y += height;
1562 }
1563 }
1564 FixedPosition::Footer => {
1565 cursor.fixed_footer.push((node.clone(), height));
1566 }
1567 }
1568 }
1569
1570 NodeKind::Watermark { .. } => {
1571 cursor.watermarks.push(node.clone());
1573 }
1574
1575 NodeKind::TextField {
1576 name,
1577 value,
1578 placeholder,
1579 width: field_w,
1580 height: field_h,
1581 multiline,
1582 password,
1583 read_only,
1584 max_length,
1585 font_size,
1586 } => {
1587 self.layout_form_field(
1588 node,
1589 &style,
1590 cursor,
1591 pages,
1592 x,
1593 *field_w,
1594 *field_h,
1595 DrawCommand::FormField {
1596 field_type: FormFieldType::TextField {
1597 value: value.clone(),
1598 placeholder: placeholder.clone(),
1599 multiline: *multiline,
1600 password: *password,
1601 read_only: *read_only,
1602 max_length: *max_length,
1603 font_size: *font_size,
1604 },
1605 name: name.clone(),
1606 },
1607 "TextField",
1608 );
1609 }
1610
1611 NodeKind::Checkbox {
1612 name,
1613 checked,
1614 width: field_w,
1615 height: field_h,
1616 read_only,
1617 } => {
1618 self.layout_form_field(
1619 node,
1620 &style,
1621 cursor,
1622 pages,
1623 x,
1624 *field_w,
1625 *field_h,
1626 DrawCommand::FormField {
1627 field_type: FormFieldType::Checkbox {
1628 checked: *checked,
1629 read_only: *read_only,
1630 },
1631 name: name.clone(),
1632 },
1633 "Checkbox",
1634 );
1635 }
1636
1637 NodeKind::Dropdown {
1638 name,
1639 options,
1640 value,
1641 width: field_w,
1642 height: field_h,
1643 read_only,
1644 font_size,
1645 } => {
1646 self.layout_form_field(
1647 node,
1648 &style,
1649 cursor,
1650 pages,
1651 x,
1652 *field_w,
1653 *field_h,
1654 DrawCommand::FormField {
1655 field_type: FormFieldType::Dropdown {
1656 options: options.clone(),
1657 value: value.clone(),
1658 read_only: *read_only,
1659 font_size: *font_size,
1660 },
1661 name: name.clone(),
1662 },
1663 "Dropdown",
1664 );
1665 }
1666
1667 NodeKind::RadioButton {
1668 name,
1669 value,
1670 checked,
1671 width: field_w,
1672 height: field_h,
1673 read_only,
1674 } => {
1675 self.layout_form_field(
1676 node,
1677 &style,
1678 cursor,
1679 pages,
1680 x,
1681 *field_w,
1682 *field_h,
1683 DrawCommand::FormField {
1684 field_type: FormFieldType::RadioButton {
1685 value: value.clone(),
1686 checked: *checked,
1687 read_only: *read_only,
1688 },
1689 name: name.clone(),
1690 },
1691 "RadioButton",
1692 );
1693 }
1694
1695 NodeKind::Text {
1696 content,
1697 href,
1698 runs,
1699 } => {
1700 self.layout_text(
1701 content,
1702 href.as_deref(),
1703 runs,
1704 &style,
1705 cursor,
1706 pages,
1707 x,
1708 available_width,
1709 font_context,
1710 node.source_location.as_ref(),
1711 node.bookmark.as_deref(),
1712 None,
1713 );
1714 }
1715
1716 NodeKind::Heading {
1717 content,
1718 href,
1719 runs,
1720 ..
1721 } => {
1722 let heading_role = node_kind_name(&node.kind); self.layout_text(
1728 content,
1729 href.as_deref(),
1730 runs,
1731 &style,
1732 cursor,
1733 pages,
1734 x,
1735 available_width,
1736 font_context,
1737 node.source_location.as_ref(),
1738 node.bookmark.as_deref(),
1739 Some(heading_role),
1740 );
1741 }
1742
1743 NodeKind::List {
1744 ordered,
1745 marker_type,
1746 start,
1747 } => {
1748 self.layout_list(
1749 node,
1750 *ordered,
1751 *marker_type,
1752 *start,
1753 &style,
1754 cursor,
1755 pages,
1756 x,
1757 available_width,
1758 font_context,
1759 );
1760 }
1761
1762 NodeKind::ListItem => {
1763 self.layout_view(
1768 node,
1769 &style,
1770 cursor,
1771 pages,
1772 x,
1773 available_width,
1774 font_context,
1775 );
1776 }
1777
1778 NodeKind::Image { width, height, .. } => {
1779 self.layout_image(
1780 node,
1781 &style,
1782 cursor,
1783 pages,
1784 x,
1785 available_width,
1786 *width,
1787 *height,
1788 );
1789 }
1790
1791 NodeKind::Table { columns } => {
1792 self.layout_table(
1793 node,
1794 &style,
1795 columns,
1796 cursor,
1797 pages,
1798 x,
1799 available_width,
1800 font_context,
1801 );
1802 }
1803
1804 NodeKind::View | NodeKind::Page { .. } => {
1805 self.layout_view(
1806 node,
1807 &style,
1808 cursor,
1809 pages,
1810 x,
1811 available_width,
1812 font_context,
1813 );
1814 }
1815
1816 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1817 self.layout_view(
1818 node,
1819 &style,
1820 cursor,
1821 pages,
1822 x,
1823 available_width,
1824 font_context,
1825 );
1826 }
1827
1828 NodeKind::Svg {
1829 width: svg_w,
1830 height: svg_h,
1831 view_box,
1832 content,
1833 } => {
1834 self.layout_svg(
1835 node,
1836 &style,
1837 cursor,
1838 pages,
1839 x,
1840 available_width,
1841 *svg_w,
1842 *svg_h,
1843 view_box.as_deref(),
1844 content,
1845 );
1846 }
1847
1848 NodeKind::Barcode {
1849 data,
1850 format,
1851 width: explicit_width,
1852 height: bar_height,
1853 } => {
1854 self.layout_barcode(
1855 node,
1856 &style,
1857 cursor,
1858 pages,
1859 x,
1860 available_width,
1861 data,
1862 *format,
1863 *explicit_width,
1864 *bar_height,
1865 );
1866 }
1867
1868 NodeKind::QrCode {
1869 data,
1870 size: explicit_size,
1871 } => {
1872 self.layout_qrcode(
1873 node,
1874 &style,
1875 cursor,
1876 pages,
1877 x,
1878 available_width,
1879 data,
1880 *explicit_size,
1881 );
1882 }
1883
1884 NodeKind::Canvas {
1885 width: canvas_w,
1886 height: canvas_h,
1887 operations,
1888 } => {
1889 self.layout_canvas(
1890 node,
1891 &style,
1892 cursor,
1893 pages,
1894 x,
1895 available_width,
1896 *canvas_w,
1897 *canvas_h,
1898 operations,
1899 );
1900 }
1901
1902 NodeKind::BarChart {
1903 data,
1904 width: chart_w,
1905 height: chart_h,
1906 color,
1907 show_labels,
1908 show_values,
1909 show_grid,
1910 title,
1911 } => {
1912 let config = crate::chart::bar::BarChartConfig {
1913 color: color.clone(),
1914 show_labels: *show_labels,
1915 show_values: *show_values,
1916 show_grid: *show_grid,
1917 title: title.clone(),
1918 };
1919 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1920 self.layout_chart(
1921 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1922 );
1923 }
1924
1925 NodeKind::LineChart {
1926 series,
1927 labels,
1928 width: chart_w,
1929 height: chart_h,
1930 show_points,
1931 show_grid,
1932 title,
1933 } => {
1934 let config = crate::chart::line::LineChartConfig {
1935 show_points: *show_points,
1936 show_grid: *show_grid,
1937 title: title.clone(),
1938 };
1939 let primitives =
1940 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1941 self.layout_chart(
1942 node,
1943 &style,
1944 cursor,
1945 pages,
1946 x,
1947 *chart_w,
1948 *chart_h,
1949 primitives,
1950 "LineChart",
1951 );
1952 }
1953
1954 NodeKind::PieChart {
1955 data,
1956 width: chart_w,
1957 height: chart_h,
1958 donut,
1959 show_legend,
1960 title,
1961 } => {
1962 let config = crate::chart::pie::PieChartConfig {
1963 donut: *donut,
1964 show_legend: *show_legend,
1965 title: title.clone(),
1966 };
1967 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1968 self.layout_chart(
1969 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1970 );
1971 }
1972
1973 NodeKind::AreaChart {
1974 series,
1975 labels,
1976 width: chart_w,
1977 height: chart_h,
1978 show_grid,
1979 title,
1980 } => {
1981 let config = crate::chart::area::AreaChartConfig {
1982 show_grid: *show_grid,
1983 title: title.clone(),
1984 };
1985 let primitives =
1986 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1987 self.layout_chart(
1988 node,
1989 &style,
1990 cursor,
1991 pages,
1992 x,
1993 *chart_w,
1994 *chart_h,
1995 primitives,
1996 "AreaChart",
1997 );
1998 }
1999
2000 NodeKind::DotPlot {
2001 groups,
2002 width: chart_w,
2003 height: chart_h,
2004 x_min,
2005 x_max,
2006 y_min,
2007 y_max,
2008 x_label,
2009 y_label,
2010 show_legend,
2011 dot_size,
2012 } => {
2013 let config = crate::chart::dot::DotPlotConfig {
2014 x_min: *x_min,
2015 x_max: *x_max,
2016 y_min: *y_min,
2017 y_max: *y_max,
2018 x_label: x_label.clone(),
2019 y_label: y_label.clone(),
2020 show_legend: *show_legend,
2021 dot_size: *dot_size,
2022 };
2023 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2024 self.layout_chart(
2025 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2026 );
2027 }
2028 }
2029
2030 if matches!(style.position, Position::Relative)
2038 && (style.top.is_some()
2039 || style.left.is_some()
2040 || style.right.is_some()
2041 || style.bottom.is_some())
2042 {
2043 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2044 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2045 for el in &mut cursor.elements[elem_start..] {
2046 if dx != 0.0 {
2047 offset_element_x(el, dx);
2048 }
2049 if dy != 0.0 {
2050 offset_element_y(el, dy);
2051 }
2052 }
2053 }
2054 }
2055
2056 #[allow(clippy::too_many_arguments)]
2057 fn layout_view(
2058 &self,
2059 node: &Node,
2060 style: &ResolvedStyle,
2061 cursor: &mut PageCursor,
2062 pages: &mut Vec<LayoutPage>,
2063 x: f64,
2064 available_width: f64,
2065 font_context: &FontContext,
2066 ) {
2067 let padding = &style.padding;
2068 let margin = &style.margin.to_edges();
2069 let border = &style.border_width;
2070
2071 let outer_width = match style.width {
2072 SizeConstraint::Fixed(w) => w,
2073 SizeConstraint::Auto => available_width - margin.horizontal(),
2074 }
2075 .min(style.max_width)
2077 .max(style.min_width);
2078 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2079
2080 let children_height =
2081 self.measure_children_height(&node.children, inner_width, style, font_context);
2082 let total_height = match style.height {
2083 SizeConstraint::Fixed(h) => h,
2084 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2085 }
2086 .max(style.min_height);
2087
2088 let node_x = x + margin.left;
2089
2090 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2091
2092 if fits || !style.breakable {
2093 if !fits && !style.breakable {
2094 pages.push(cursor.finalize());
2095 *cursor = cursor.new_page();
2096 }
2097
2098 let rect_y = cursor.content_y + cursor.y + margin.top;
2100 let snapshot = cursor.elements.len();
2101
2102 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2107 cursor.elements.push(marker);
2108 }
2109
2110 let saved_y = cursor.y;
2111 cursor.y += margin.top + padding.top + border.top;
2112
2113 let pages_before = pages.len();
2114 let children_x = node_x + padding.left + border.left;
2115 let is_grid =
2116 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2117 if is_grid {
2118 self.layout_grid_children(
2119 &node.children,
2120 style,
2121 cursor,
2122 pages,
2123 children_x,
2124 inner_width,
2125 font_context,
2126 );
2127 } else {
2128 self.layout_children(
2129 &node.children,
2130 &node.style,
2131 cursor,
2132 pages,
2133 children_x,
2134 inner_width,
2135 Some(style),
2136 font_context,
2137 );
2138 }
2139
2140 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2142
2143 if self.measure_check
2150 && matches!(style.height, SizeConstraint::Auto)
2151 && pages.len() == pages_before
2152 && !child_elements.is_empty()
2153 {
2154 let content_top = rect_y + padding.top + border.top;
2155 let extent = child_elements
2156 .iter()
2157 .map(|el| el.y + el.height)
2158 .fold(f64::NEG_INFINITY, f64::max)
2159 - content_top;
2160 if extent > 0.0 && children_height - extent > 2.0 {
2161 self.defect(format!(
2162 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2163 node_kind_name(&node.kind),
2164 children_height - extent,
2165 ));
2166 }
2167 }
2168
2169 let rect_element = LayoutElement {
2170 x: node_x,
2171 y: rect_y,
2172 width: outer_width,
2173 height: total_height,
2174 draw: DrawCommand::Rect {
2175 background: style.background_color,
2176 border_width: style.border_width,
2177 border_color: style.border_color,
2178 border_style: style.border_style,
2179 border_radius: style.border_radius,
2180 opacity: 1.0,
2181 box_shadow: style.box_shadow.map(Box::new),
2182 background_gradient: style.background.clone().map(Box::new),
2183 },
2184 children: child_elements,
2185 node_type: Some(node_kind_name(&node.kind).to_string()),
2186 resolved_style: Some(style.clone()),
2187 source_location: node.source_location.clone(),
2188 href: node.href.clone(),
2189 bookmark: None,
2192 alt: None,
2193 is_header_row: false,
2194 col_span: 1,
2195 overflow: style.overflow,
2196 opacity: style.opacity,
2197 };
2198 cursor.elements.push(rect_element);
2199
2200 cursor.y = saved_y + total_height + margin.vertical();
2201 } else {
2202 self.layout_breakable_view(
2203 node,
2204 style,
2205 cursor,
2206 pages,
2207 node_x,
2208 outer_width,
2209 inner_width,
2210 font_context,
2211 );
2212 }
2213 }
2214
2215 #[allow(clippy::too_many_arguments)]
2216 fn layout_breakable_view(
2217 &self,
2218 node: &Node,
2219 style: &ResolvedStyle,
2220 cursor: &mut PageCursor,
2221 pages: &mut Vec<LayoutPage>,
2222 node_x: f64,
2223 outer_width: f64,
2224 inner_width: f64,
2225 font_context: &FontContext,
2226 ) {
2227 let padding = &style.padding;
2228 let border = &style.border_width;
2229 let margin = &style.margin.to_edges();
2230
2231 let initial_page_count = pages.len();
2233 let snapshot = cursor.elements.len();
2234 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2235
2236 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2244 cursor.elements.push(marker);
2245 }
2246
2247 cursor.y += margin.top + padding.top + border.top;
2248 let prev_continuation_offset = cursor.continuation_top_offset;
2249 cursor.continuation_top_offset = padding.top + border.top;
2250
2251 let children_x = node_x + padding.left + border.left;
2252 let is_grid =
2253 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2254 if is_grid {
2255 self.layout_grid_children(
2256 &node.children,
2257 style,
2258 cursor,
2259 pages,
2260 children_x,
2261 inner_width,
2262 font_context,
2263 );
2264 } else {
2265 self.layout_children(
2266 &node.children,
2267 &node.style,
2268 cursor,
2269 pages,
2270 children_x,
2271 inner_width,
2272 Some(style),
2273 font_context,
2274 );
2275 }
2276
2277 cursor.continuation_top_offset = prev_continuation_offset;
2278
2279 let has_visual = style.background_color.is_some()
2281 || style.border_width.top > 0.0
2282 || style.border_width.right > 0.0
2283 || style.border_width.bottom > 0.0
2284 || style.border_width.left > 0.0;
2285 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2287
2288 if !needs_wrapper {
2289 cursor.y += padding.bottom + border.bottom + margin.bottom;
2291 return;
2292 }
2293
2294 let draw_cmd = DrawCommand::Rect {
2295 background: style.background_color,
2296 border_width: style.border_width,
2297 border_color: style.border_color,
2298 border_style: style.border_style,
2299 border_radius: style.border_radius,
2300 opacity: 1.0,
2301 box_shadow: style.box_shadow.map(Box::new),
2302 background_gradient: style.background.clone().map(Box::new),
2303 };
2304
2305 if pages.len() == initial_page_count {
2306 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2308 let rect_height =
2309 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2310 cursor.elements.push(LayoutElement {
2311 x: node_x,
2312 y: rect_start_y,
2313 width: outer_width,
2314 height: rect_height,
2315 draw: draw_cmd,
2316 children: child_elements,
2317 node_type: Some(node_kind_name(&node.kind).to_string()),
2318 resolved_style: Some(style.clone()),
2319 source_location: node.source_location.clone(),
2320 href: node.href.clone(),
2321 bookmark: None,
2326 alt: None,
2327 is_header_row: false,
2328 col_span: 1,
2329 overflow: style.overflow,
2330 opacity: style.opacity,
2331 });
2332 } else {
2333 let page = &mut pages[initial_page_count];
2337 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2338 let page_content_bottom =
2339 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2340 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
2341 if !our_elements.is_empty() {
2342 let rect_height = page_content_bottom - rect_start_y;
2343 page.elements.push(LayoutElement {
2344 x: node_x,
2345 y: rect_start_y,
2346 width: outer_width,
2347 height: rect_height,
2348 draw: draw_cmd.clone(),
2349 children: our_elements,
2350 node_type: Some(node_kind_name(&node.kind).to_string()),
2351 resolved_style: Some(style.clone()),
2352 source_location: node.source_location.clone(),
2353 href: node.href.clone(),
2354 bookmark: None,
2356 alt: None,
2357 is_header_row: false,
2358 col_span: 1,
2359 overflow: Overflow::default(),
2360 opacity: 1.0,
2361 });
2362 }
2363
2364 for page in &mut pages[initial_page_count + 1..] {
2366 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2367 let content_top = page.config.margin.top + header_h;
2368 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2369 let content_bottom = page.config.margin.top
2370 + (page.height - page.config.margin.vertical())
2371 - footer_h;
2372 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2373 if !all_elements.is_empty() {
2374 page.elements.push(LayoutElement {
2375 x: node_x,
2376 y: content_top,
2377 width: outer_width,
2378 height: content_bottom - content_top,
2379 draw: draw_cmd.clone(),
2380 children: all_elements,
2381 node_type: Some(node_kind_name(&node.kind).to_string()),
2382 resolved_style: Some(style.clone()),
2383 source_location: node.source_location.clone(),
2384 href: None,
2385 bookmark: None,
2386 alt: None,
2387 is_header_row: false,
2388 col_span: 1,
2389 overflow: Overflow::default(),
2390 opacity: 1.0,
2391 });
2392 }
2393 }
2394
2395 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2397 if !all_elements.is_empty() {
2398 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2399 let content_top = cursor.content_y + header_h;
2400 let rect_height =
2401 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2402 cursor.elements.push(LayoutElement {
2403 x: node_x,
2404 y: content_top,
2405 width: outer_width,
2406 height: rect_height,
2407 draw: draw_cmd,
2408 children: all_elements,
2409 node_type: Some(node_kind_name(&node.kind).to_string()),
2410 resolved_style: Some(style.clone()),
2411 source_location: node.source_location.clone(),
2412 href: None,
2413 bookmark: None,
2414 alt: None,
2415 is_header_row: false,
2416 col_span: 1,
2417 overflow: Overflow::default(),
2418 opacity: 1.0,
2419 });
2420 }
2421 }
2422
2423 cursor.y += padding.bottom + border.bottom + margin.bottom;
2424 }
2425
2426 #[allow(clippy::too_many_arguments)]
2427 fn layout_children(
2428 &self,
2429 children: &[Node],
2430 _parent_raw_style: &Style,
2431 cursor: &mut PageCursor,
2432 pages: &mut Vec<LayoutPage>,
2433 content_x: f64,
2434 available_width: f64,
2435 parent_style: Option<&ResolvedStyle>,
2436 font_context: &FontContext,
2437 ) {
2438 let parent_box_y = cursor.content_y + cursor.y;
2440 let parent_box_x = content_x;
2441
2442 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2448 let saved_cb = cursor.containing_block;
2449 if parent_positioned {
2450 let cb_height = parent_style
2451 .and_then(|ps| match ps.height {
2452 SizeConstraint::Fixed(h) => {
2453 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2454 }
2455 SizeConstraint::Auto => None,
2456 })
2457 .unwrap_or(saved_cb.3);
2458 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2459 }
2460
2461 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2463 .iter()
2464 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2465
2466 let direction = parent_style
2467 .map(|s| s.flex_direction)
2468 .unwrap_or(FlexDirection::Column);
2469
2470 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2471 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2472
2473 match direction {
2475 FlexDirection::Column | FlexDirection::ColumnReverse => {
2476 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2477 flow_children.into_iter().rev().collect()
2478 } else {
2479 flow_children
2480 };
2481
2482 let justify = parent_style
2483 .map(|s| s.justify_content)
2484 .unwrap_or(JustifyContent::FlexStart);
2485 let align = parent_style
2486 .map(|s| s.align_items)
2487 .unwrap_or(AlignItems::Stretch);
2488
2489 let start_y = cursor.y;
2490 let initial_pages = pages.len();
2491
2492 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2494
2495 for (i, child) in items.iter().enumerate() {
2496 if i > 0 {
2497 cursor.y += row_gap;
2498 }
2499 let child_start = cursor.elements.len();
2500
2501 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2504 let has_auto_h = child_margin.has_auto_horizontal();
2505
2506 let (child_x, layout_w) = if has_auto_h {
2513 let child_style = child.style.resolve(parent_style, available_width);
2514 let has_explicit_width =
2515 matches!(child_style.width, SizeConstraint::Fixed(_));
2516 let intrinsic = self
2517 .measure_intrinsic_width(child, &child_style, font_context)
2518 .min(available_width);
2519 let w = match child_style.width {
2520 SizeConstraint::Fixed(fw) => fw,
2521 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2526 (available_width - child_margin.horizontal())
2527 .min(child_style.max_width)
2528 }
2529 SizeConstraint::Auto => intrinsic,
2530 }
2531 .min(child_style.max_width)
2532 .max(child_style.min_width);
2533 let lw = if has_explicit_width {
2534 available_width
2535 } else {
2536 w
2537 };
2538 let fixed_h = child_margin.horizontal();
2539 let slack = (available_width - w - fixed_h).max(0.0);
2540 let auto_left = child_margin.left.is_auto();
2541 let auto_right = child_margin.right.is_auto();
2542 let ml = match (auto_left, auto_right) {
2543 (true, true) => slack / 2.0,
2544 (true, false) => slack,
2545 (false, true) => 0.0,
2546 (false, false) => 0.0,
2547 };
2548 (content_x + child_margin.left.resolve() + ml, lw)
2549 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2550 let child_style = child.style.resolve(parent_style, available_width);
2551 let has_explicit_width =
2552 matches!(child_style.width, SizeConstraint::Fixed(_));
2553 let intrinsic = self
2554 .measure_intrinsic_width(child, &child_style, font_context)
2555 .min(available_width);
2556 let w = match child_style.width {
2557 SizeConstraint::Fixed(fw) => fw,
2558 SizeConstraint::Auto => intrinsic,
2559 };
2560 let lw = if has_explicit_width {
2561 available_width
2562 } else {
2563 w
2564 };
2565 match align {
2566 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2567 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2568 _ => (content_x, available_width),
2569 }
2570 } else {
2571 (content_x, available_width)
2572 };
2573
2574 self.layout_node(
2575 child,
2576 cursor,
2577 pages,
2578 child_x,
2579 layout_w,
2580 parent_style,
2581 font_context,
2582 None,
2583 None,
2584 );
2585
2586 child_ranges.push((child_start, cursor.elements.len()));
2587 }
2588
2589 let container_inner_h: Option<f64> = parent_style
2592 .and_then(|ps| match ps.height {
2593 SizeConstraint::Fixed(h) => {
2594 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2595 }
2596 SizeConstraint::Auto => None,
2597 })
2598 .or_else(|| {
2599 if parent_style.is_none() {
2601 Some(cursor.content_height - start_y)
2602 } else {
2603 None
2604 }
2605 });
2606
2607 if let Some(inner_h) = container_inner_h {
2608 if pages.len() == initial_pages {
2609 let child_styles: Vec<ResolvedStyle> = items
2610 .iter()
2611 .map(|child| child.style.resolve(parent_style, available_width))
2612 .collect();
2613 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2614 if total_grow > 0.0 {
2615 let children_total = cursor.y - start_y;
2616 let slack = (inner_h - children_total).max(0.0);
2617 if slack > 0.0 {
2618 let mut cumulative_shift = 0.0_f64;
2619 for (i, cs) in child_styles.iter().enumerate() {
2620 let (start, end) = child_ranges[i];
2621 if cumulative_shift > 0.001 {
2622 for j in start..end {
2623 offset_element_y(
2624 &mut cursor.elements[j],
2625 cumulative_shift,
2626 );
2627 }
2628 }
2629 if cs.flex_grow > 0.0 {
2630 let extra = slack * (cs.flex_grow / total_grow);
2631 if start < end {
2633 let elem = &mut cursor.elements[end - 1];
2634 elem.height += extra;
2635 reapply_justify_content(elem);
2636 }
2637 cumulative_shift += extra;
2638 }
2639 }
2640 cursor.y += cumulative_shift;
2641 }
2642 }
2643 }
2644 }
2645
2646 if let Some(inner_h) = container_inner_h {
2653 if pages.len() == initial_pages {
2654 let auto_styles: Vec<ResolvedStyle> = items
2655 .iter()
2656 .map(|child| child.style.resolve(parent_style, available_width))
2657 .collect();
2658 let total_autos: usize = auto_styles
2659 .iter()
2660 .map(|s| {
2661 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2662 })
2663 .sum();
2664 if total_autos > 0 {
2665 let children_total = cursor.y - start_y;
2666 let total_slack = (inner_h - children_total).max(0.0);
2667 if total_slack > 0.0 {
2668 let per_auto = total_slack / total_autos as f64;
2669 let mut cumulative_shift = 0.0_f64;
2670 for (i, cs) in auto_styles.iter().enumerate() {
2671 let (start, end) = child_ranges[i];
2672 let mt_auto = cs.margin.top.is_auto();
2673 let mb_auto = cs.margin.bottom.is_auto();
2674 let this_child_shift =
2678 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2679 if this_child_shift > 0.001 {
2680 for j in start..end {
2681 offset_element_y(
2682 &mut cursor.elements[j],
2683 this_child_shift,
2684 );
2685 }
2686 }
2687 cumulative_shift =
2690 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2691 }
2692 cursor.y += cumulative_shift;
2693 }
2694 }
2695 }
2696 }
2697
2698 let needs_justify =
2700 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2701 if needs_justify {
2702 let justify_inner_h = container_inner_h.or_else(|| {
2704 parent_style.and_then(|ps| match ps.height {
2705 SizeConstraint::Fixed(h) => {
2706 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2707 }
2708 SizeConstraint::Auto => None,
2709 })
2710 });
2711 if let Some(inner_h) = justify_inner_h {
2712 let children_total = cursor.y - start_y;
2713 let slack = inner_h - children_total;
2714 if slack > 0.0 {
2715 let n = child_ranges.len();
2716 let offsets: Vec<f64> = match justify {
2717 JustifyContent::FlexEnd => vec![slack; n],
2718 JustifyContent::Center => vec![slack / 2.0; n],
2719 JustifyContent::SpaceBetween => {
2720 if n <= 1 {
2721 vec![0.0; n]
2722 } else {
2723 let per_gap = slack / (n - 1) as f64;
2724 (0..n).map(|i| i as f64 * per_gap).collect()
2725 }
2726 }
2727 JustifyContent::SpaceAround => {
2728 let space = slack / n as f64;
2729 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2730 }
2731 JustifyContent::SpaceEvenly => {
2732 let space = slack / (n + 1) as f64;
2733 (0..n).map(|i| (i + 1) as f64 * space).collect()
2734 }
2735 JustifyContent::FlexStart => vec![0.0; n],
2736 };
2737 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2738 let dy = offsets[i];
2739 if dy.abs() > 0.001 {
2740 for j in start..end {
2741 offset_element_y(&mut cursor.elements[j], dy);
2742 }
2743 }
2744 }
2745 cursor.y += *offsets.last().unwrap_or(&0.0);
2746 }
2747 }
2748 }
2749 }
2750
2751 FlexDirection::Row | FlexDirection::RowReverse => {
2752 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2753 self.layout_flex_row(
2754 &flow_owned,
2755 cursor,
2756 pages,
2757 content_x,
2758 available_width,
2759 parent_style,
2760 column_gap,
2761 row_gap,
2762 font_context,
2763 );
2764 }
2765 }
2766
2767 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2773 let ph = parent_style
2774 .and_then(|ps| match ps.height {
2775 SizeConstraint::Fixed(h) => {
2776 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2777 }
2778 SizeConstraint::Auto => None,
2779 })
2780 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
2781 (parent_box_x, parent_box_y, available_width, ph)
2782 } else {
2783 cursor.containing_block
2784 };
2785
2786 for abs_child in &abs_children {
2788 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2789
2790 let child_width = match abs_style.width {
2792 SizeConstraint::Fixed(w) => w,
2793 SizeConstraint::Auto => {
2794 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2796 (cb_w - l - r).max(0.0)
2797 } else {
2798 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2799 }
2800 }
2801 };
2802
2803 let child_height = match abs_style.height {
2804 SizeConstraint::Fixed(h) => h,
2805 SizeConstraint::Auto => {
2806 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2807 }
2808 };
2809
2810 let abs_margin = abs_style.margin.to_edges();
2818 let abs_x = if let Some(l) = abs_style.left {
2819 cb_x + l
2820 } else if let Some(r) = abs_style.right {
2821 cb_x + cb_w - r - child_width - abs_margin.horizontal()
2822 } else {
2823 cb_x
2824 };
2825
2826 let abs_y = if let Some(t) = abs_style.top {
2827 cb_y + t
2828 } else if let Some(b) = abs_style.bottom {
2829 cb_y + cb_h - b - child_height - abs_margin.vertical()
2830 } else {
2831 cb_y
2832 };
2833
2834 let mut abs_cursor = PageCursor::new(&cursor.config);
2836 abs_cursor.y = 0.0;
2837 abs_cursor.content_x = abs_x;
2838 abs_cursor.content_y = abs_y;
2839
2840 self.layout_node(
2841 abs_child,
2842 &mut abs_cursor,
2843 &mut Vec::new(),
2844 abs_x,
2845 child_width,
2846 parent_style,
2847 font_context,
2848 None,
2849 None,
2850 );
2851
2852 cursor.elements.extend(abs_cursor.elements);
2854 }
2855
2856 cursor.containing_block = saved_cb;
2858 }
2859
2860 #[allow(clippy::too_many_arguments)]
2861 fn layout_flex_row(
2862 &self,
2863 children: &[Node],
2864 cursor: &mut PageCursor,
2865 pages: &mut Vec<LayoutPage>,
2866 content_x: f64,
2867 available_width: f64,
2868 parent_style: Option<&ResolvedStyle>,
2869 column_gap: f64,
2870 row_gap: f64,
2871 font_context: &FontContext,
2872 ) {
2873 if children.is_empty() {
2874 return;
2875 }
2876
2877 let flex_wrap = parent_style
2878 .map(|s| s.flex_wrap)
2879 .unwrap_or(FlexWrap::NoWrap);
2880
2881 let items: Vec<FlexItem> = children
2884 .iter()
2885 .map(|child| {
2886 let style = child.style.resolve(parent_style, available_width);
2887 let base_width = match style.flex_basis {
2888 SizeConstraint::Fixed(w) => w,
2889 SizeConstraint::Auto => match style.width {
2890 SizeConstraint::Fixed(w) => w,
2891 SizeConstraint::Auto => {
2892 self.measure_intrinsic_width(child, &style, font_context)
2893 }
2894 },
2895 };
2896 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2897 FlexItem {
2898 node: child,
2899 style,
2900 base_width,
2901 min_content_width,
2902 }
2903 })
2904 .collect();
2905
2906 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2908 let lines = match flex_wrap {
2909 FlexWrap::NoWrap => {
2910 vec![flex::WrapLine {
2911 start: 0,
2912 end: items.len(),
2913 }]
2914 }
2915 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2916 FlexWrap::WrapReverse => {
2917 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2918 l.reverse();
2919 l
2920 }
2921 };
2922
2923 if lines.is_empty() {
2924 return;
2925 }
2926
2927 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2929
2930 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2932
2933 let initial_pages_count = pages.len();
2934 let flex_start_y = cursor.y;
2935 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2936
2937 for (line_idx, line) in lines.iter().enumerate() {
2938 let line_items = &items[line.start..line.end];
2939 let line_count = line.end - line.start;
2940 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
2941 let distributable = available_width - line_gap;
2942
2943 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
2945 let remaining = distributable - total_base;
2946
2947 if remaining > 0.0 {
2948 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
2949 if total_grow > 0.0 {
2950 for (j, item) in line_items.iter().enumerate() {
2951 final_widths[line.start + j] =
2952 item.base_width + remaining * (item.style.flex_grow / total_grow);
2953 }
2954 }
2955 } else if remaining < 0.0 {
2956 let total_shrink: f64 = line_items
2957 .iter()
2958 .map(|i| i.style.flex_shrink * i.base_width)
2959 .sum();
2960 if total_shrink > 0.0 {
2961 for (j, item) in line_items.iter().enumerate() {
2962 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
2963 let w = item.base_width + remaining * factor;
2964 let floor = item.style.min_width.max(item.min_content_width);
2965 final_widths[line.start + j] = w.max(floor);
2966 }
2967 }
2968 }
2969
2970 let line_height: f64 = line_items
2972 .iter()
2973 .enumerate()
2974 .map(|(j, item)| {
2975 let fw = final_widths[line.start + j];
2976 self.measure_node_height(item.node, fw, &item.style, font_context)
2977 + item.style.margin.vertical()
2978 })
2979 .fold(0.0f64, f64::max);
2980
2981 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
2988 pages.push(cursor.finalize());
2989 *cursor = cursor.new_page();
2990 }
2991
2992 if line_idx > 0 {
2994 cursor.y += row_gap;
2995 }
2996
2997 let row_start_y = cursor.y;
2998
2999 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
3001 let slack = available_width - actual_total - line_gap;
3002
3003 let (start_offset, between_extra) = match justify {
3004 JustifyContent::FlexStart => (0.0, 0.0),
3005 JustifyContent::FlexEnd => (slack, 0.0),
3006 JustifyContent::Center => (slack / 2.0, 0.0),
3007 JustifyContent::SpaceBetween => {
3008 if line_count > 1 {
3009 (0.0, slack / (line_count as f64 - 1.0))
3010 } else {
3011 (0.0, 0.0)
3012 }
3013 }
3014 JustifyContent::SpaceAround => {
3015 let s = slack / line_count as f64;
3016 (s / 2.0, s)
3017 }
3018 JustifyContent::SpaceEvenly => {
3019 let s = slack / (line_count as f64 + 1.0);
3020 (s, s)
3021 }
3022 };
3023
3024 let line_elem_start = cursor.elements.len();
3025 let mut x = content_x + start_offset;
3026
3027 for (j, item) in line_items.iter().enumerate() {
3028 if j > 0 {
3029 x += column_gap + between_extra;
3030 }
3031
3032 let fw = final_widths[line.start + j];
3033
3034 let align = item
3035 .style
3036 .align_self
3037 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3038
3039 let item_height =
3040 self.measure_node_height(item.node, fw, &item.style, font_context);
3041
3042 let has_auto_v = item.style.margin.has_auto_vertical();
3044 let y_offset = if has_auto_v {
3045 let fixed_v = item.style.margin.vertical();
3046 let slack = (line_height - item_height - fixed_v).max(0.0);
3047 let auto_top = item.style.margin.top.is_auto();
3048 let auto_bottom = item.style.margin.bottom.is_auto();
3049 match (auto_top, auto_bottom) {
3050 (true, true) => slack / 2.0,
3051 (true, false) => slack,
3052 (false, true) => 0.0,
3053 (false, false) => 0.0,
3054 }
3055 } else {
3056 match align {
3057 AlignItems::FlexStart => 0.0,
3058 AlignItems::FlexEnd => {
3059 line_height - item_height - item.style.margin.vertical()
3060 }
3061 AlignItems::Center => {
3062 (line_height - item_height - item.style.margin.vertical()) / 2.0
3063 }
3064 AlignItems::Stretch => 0.0,
3065 AlignItems::Baseline => 0.0,
3066 }
3067 };
3068
3069 let cross_h = if matches!(align, AlignItems::Stretch)
3073 && matches!(item.style.height, SizeConstraint::Auto)
3074 && !has_auto_v
3075 {
3076 let stretch_h = line_height - item.style.margin.vertical();
3077 if stretch_h > item_height {
3078 Some(stretch_h)
3079 } else {
3080 None
3081 }
3082 } else {
3083 None
3084 };
3085
3086 let saved_y = cursor.y;
3087 cursor.y = row_start_y + y_offset;
3088
3089 self.layout_node(
3090 item.node,
3091 cursor,
3092 pages,
3093 x,
3094 available_width,
3095 parent_style,
3096 font_context,
3097 cross_h,
3098 Some(fw),
3099 );
3100
3101 cursor.y = saved_y;
3102 x += fw;
3103 }
3104
3105 cursor.y = row_start_y + line_height;
3106 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3107 }
3108
3109 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3111 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3112 if !matches!(align_content, AlignContent::FlexStart)
3113 && !matches!(flex_wrap, FlexWrap::NoWrap)
3114 {
3115 if let Some(parent) = parent_style {
3116 if let SizeConstraint::Fixed(container_h) = parent.height {
3117 let inner_h = container_h
3118 - parent.padding.vertical()
3119 - parent.border_width.vertical();
3120 let total_used = cursor.y - flex_start_y;
3121 let slack = inner_h - total_used;
3122 if slack > 0.0 {
3123 let n = line_infos.len();
3124 let offsets: Vec<f64> = match align_content {
3125 AlignContent::FlexEnd => vec![slack; n],
3126 AlignContent::Center => vec![slack / 2.0; n],
3127 AlignContent::SpaceBetween => {
3128 if n <= 1 {
3129 vec![0.0; n]
3130 } else {
3131 let per_gap = slack / (n - 1) as f64;
3132 (0..n).map(|i| i as f64 * per_gap).collect()
3133 }
3134 }
3135 AlignContent::SpaceAround => {
3136 let space = slack / n as f64;
3137 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3138 }
3139 AlignContent::SpaceEvenly => {
3140 let space = slack / (n + 1) as f64;
3141 (0..n).map(|i| (i + 1) as f64 * space).collect()
3142 }
3143 AlignContent::Stretch => {
3144 let extra = slack / n as f64;
3145 (0..n).map(|i| i as f64 * extra).collect()
3146 }
3147 AlignContent::FlexStart => vec![0.0; n],
3148 };
3149 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3150 let dy = offsets[i];
3151 if dy.abs() > 0.001 {
3152 for j in start..end {
3153 offset_element_y(&mut cursor.elements[j], dy);
3154 }
3155 }
3156 }
3157 cursor.y += *offsets.last().unwrap_or(&0.0);
3158 }
3159 }
3160 }
3161 }
3162 }
3163 }
3164
3165 #[allow(clippy::too_many_arguments)]
3168 fn layout_list(
3169 &self,
3170 node: &Node,
3171 ordered: bool,
3172 marker_type: ListMarkerType,
3173 start: u32,
3174 style: &ResolvedStyle,
3175 cursor: &mut PageCursor,
3176 pages: &mut Vec<LayoutPage>,
3177 x: f64,
3178 available_width: f64,
3179 font_context: &FontContext,
3180 ) {
3181 let margin = &style.margin.to_edges();
3182 let padding = &style.padding;
3183
3184 cursor.y += margin.top;
3185
3186 let list_x = x + margin.left;
3187 let outer_width = available_width - margin.horizontal();
3188 let inner_width = outer_width - padding.horizontal();
3189
3190 let n_items = node
3193 .children
3194 .iter()
3195 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3196 .count() as u32;
3197
3198 let marker_gutter =
3199 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3200
3201 let list_inner_x = list_x + padding.left;
3202 let content_x = list_inner_x + marker_gutter;
3203 let content_width = (inner_width - marker_gutter).max(0.0);
3204
3205 let snapshot = cursor.elements.len();
3208 let list_start_y = cursor.content_y + cursor.y;
3209 cursor.y += padding.top;
3210
3211 let mut item_index: u32 = 0;
3212 for child in &node.children {
3213 if !matches!(child.kind, NodeKind::ListItem) {
3214 continue;
3215 }
3216 let marker_idx = start + item_index;
3217 self.layout_list_item(
3218 child,
3219 marker_idx,
3220 ordered,
3221 marker_type,
3222 marker_gutter,
3223 style,
3224 cursor,
3225 pages,
3226 list_inner_x,
3227 content_x,
3228 content_width,
3229 font_context,
3230 );
3231 item_index += 1;
3232 }
3233
3234 cursor.y += padding.bottom;
3235
3236 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3238 let list_height = cursor.content_y + cursor.y - list_start_y;
3239 cursor.elements.push(LayoutElement {
3240 x: list_x,
3241 y: list_start_y,
3242 width: outer_width,
3243 height: list_height,
3244 draw: DrawCommand::None,
3245 children: item_elements,
3246 node_type: Some("List".to_string()),
3247 resolved_style: Some(style.clone()),
3248 source_location: node.source_location.clone(),
3249 href: None,
3250 bookmark: node.bookmark.clone(),
3251 alt: None,
3252 is_header_row: false,
3253 col_span: 1,
3254 overflow: style.overflow,
3255 opacity: style.opacity,
3256 });
3257
3258 cursor.y += margin.bottom;
3259 }
3260
3261 #[allow(clippy::too_many_arguments)]
3262 fn layout_list_item(
3263 &self,
3264 item: &Node,
3265 marker_idx: u32,
3266 ordered: bool,
3267 marker_type: ListMarkerType,
3268 marker_gutter: f64,
3269 parent_style: &ResolvedStyle,
3270 cursor: &mut PageCursor,
3271 pages: &mut Vec<LayoutPage>,
3272 list_inner_x: f64,
3273 content_x: f64,
3274 content_width: f64,
3275 font_context: &FontContext,
3276 ) {
3277 let item_style = item.style.resolve(Some(parent_style), content_width);
3278 let item_margin = item_style.margin.to_edges();
3279
3280 cursor.y += item_margin.top;
3281 let item_start_y = cursor.content_y + cursor.y;
3282 let item_snapshot = cursor.elements.len();
3283
3284 let marker_str = format_marker(marker_idx, ordered, marker_type);
3288 if !marker_str.is_empty() {
3289 let saved_y = cursor.y;
3290 self.layout_text(
3291 &marker_str,
3292 None,
3293 &[],
3294 &item_style,
3295 cursor,
3296 pages,
3297 list_inner_x,
3298 marker_gutter,
3299 font_context,
3300 None,
3301 None,
3302 Some("Lbl"),
3303 );
3304 cursor.y = saved_y;
3305 }
3306
3307 self.layout_children(
3313 &item.children,
3314 &item.style,
3315 cursor,
3316 pages,
3317 content_x,
3318 content_width,
3319 Some(&item_style),
3320 font_context,
3321 );
3322
3323 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3326 let item_height = cursor.content_y + cursor.y - item_start_y;
3327 let item_width = content_x + content_width - list_inner_x;
3328 cursor.elements.push(LayoutElement {
3329 x: list_inner_x,
3330 y: item_start_y,
3331 width: item_width,
3332 height: item_height,
3333 draw: DrawCommand::None,
3334 children: item_children,
3335 node_type: Some("ListItem".to_string()),
3336 resolved_style: Some(item_style.clone()),
3337 source_location: item.source_location.clone(),
3338 href: None,
3339 bookmark: item.bookmark.clone(),
3340 alt: None,
3341 is_header_row: false,
3342 col_span: 1,
3343 overflow: item_style.overflow,
3344 opacity: item_style.opacity,
3345 });
3346
3347 cursor.y += item_margin.bottom;
3348 }
3349
3350 #[allow(clippy::too_many_arguments)]
3351 fn layout_table(
3352 &self,
3353 node: &Node,
3354 style: &ResolvedStyle,
3355 column_defs: &[ColumnDef],
3356 cursor: &mut PageCursor,
3357 pages: &mut Vec<LayoutPage>,
3358 x: f64,
3359 available_width: f64,
3360 font_context: &FontContext,
3361 ) {
3362 let padding = &style.padding;
3363 let margin = &style.margin.to_edges();
3364 let border = &style.border_width;
3365
3366 let table_x = x + margin.left;
3367 let table_width = match style.width {
3368 SizeConstraint::Fixed(w) => w,
3369 SizeConstraint::Auto => available_width - margin.horizontal(),
3370 };
3371 let inner_width = table_width - padding.horizontal() - border.horizontal();
3372
3373 let col_widths = self.resolve_column_widths(
3374 column_defs,
3375 inner_width,
3376 &node.children,
3377 style,
3378 font_context,
3379 );
3380
3381 let mut header_rows: Vec<&Node> = Vec::new();
3382 let mut body_rows: Vec<&Node> = Vec::new();
3383
3384 for child in &node.children {
3385 match &child.kind {
3386 NodeKind::TableRow { is_header: true } => header_rows.push(child),
3387 _ => body_rows.push(child),
3388 }
3389 }
3390
3391 if !style.breakable {
3397 let total_height: f64 = node
3398 .children
3399 .iter()
3400 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3401 .sum::<f64>()
3402 + padding.vertical()
3403 + border.vertical();
3404 let fresh_page_available = cursor.content_height
3405 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3406 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3407 if total_height > cursor.remaining_height()
3408 && total_height <= fresh_page_available
3409 && cursor.y > 0.0
3410 {
3411 pages.push(cursor.finalize());
3412 *cursor = cursor.new_page();
3413 }
3414 }
3415
3416 let initial_page_count = pages.len();
3423 let snapshot = cursor.elements.len();
3424 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3425
3426 cursor.y += margin.top + padding.top + border.top;
3427
3428 let cell_x_start = table_x + padding.left + border.left;
3429
3430 if !header_rows.is_empty() {
3455 let total_header_h: f64 = header_rows
3456 .iter()
3457 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3458 .sum();
3459 let first_body_h = body_rows
3460 .first()
3461 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3462 .unwrap_or(0.0);
3463
3464 let needed = total_header_h + first_body_h;
3465 let fresh_page_available = cursor.content_height
3466 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3467 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3468
3469 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3470 pages.push(cursor.finalize());
3471 *cursor = cursor.new_page();
3472 cursor.y += padding.top + border.top;
3473 }
3474 }
3475
3476 for header_row in &header_rows {
3477 self.layout_table_row(
3478 header_row,
3479 &col_widths,
3480 style,
3481 cursor,
3482 cell_x_start,
3483 font_context,
3484 pages,
3485 );
3486 }
3487
3488 for body_row in &body_rows {
3489 let row_height =
3490 self.measure_table_row_height(body_row, &col_widths, style, font_context);
3491
3492 let fresh_page_available = cursor.content_height
3500 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3501 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3502 if row_height > fresh_page_available {
3503 self.defect(format!(
3504 "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",
3505 ));
3506 }
3507 if row_height > cursor.remaining_height()
3508 && cursor.remaining_height() < fresh_page_available - 0.5
3509 {
3510 pages.push(cursor.finalize());
3511 *cursor = cursor.new_page();
3512
3513 cursor.y += padding.top + border.top;
3514 for header_row in &header_rows {
3515 self.layout_table_row(
3516 header_row,
3517 &col_widths,
3518 style,
3519 cursor,
3520 cell_x_start,
3521 font_context,
3522 pages,
3523 );
3524 }
3525 }
3526
3527 self.layout_table_row(
3528 body_row,
3529 &col_widths,
3530 style,
3531 cursor,
3532 cell_x_start,
3533 font_context,
3534 pages,
3535 );
3536 }
3537
3538 let has_visual = style.background_color.is_some()
3542 || style.background.is_some()
3543 || style.border_width.top > 0.0
3544 || style.border_width.right > 0.0
3545 || style.border_width.bottom > 0.0
3546 || style.border_width.left > 0.0;
3547 let draw_cmd = if has_visual {
3548 DrawCommand::Rect {
3549 background: style.background_color,
3550 border_width: style.border_width,
3551 border_color: style.border_color,
3552 border_style: style.border_style,
3553 border_radius: style.border_radius,
3554 opacity: 1.0,
3555 box_shadow: style.box_shadow.map(Box::new),
3556 background_gradient: style.background.clone().map(Box::new),
3557 }
3558 } else {
3559 DrawCommand::None
3560 };
3561 let make_wrapper =
3562 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3563 x: table_x,
3564 y,
3565 width: table_width,
3566 height,
3567 draw,
3568 children,
3569 node_type: Some(node_kind_name(&node.kind).to_string()),
3570 resolved_style: Some(style.clone()),
3571 source_location: node.source_location.clone(),
3572 href: node.href.clone(),
3573 bookmark: None,
3574 alt: None,
3575 is_header_row: false,
3576 col_span: 1,
3577 overflow: Overflow::default(),
3578 opacity: style.opacity,
3579 };
3580
3581 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3582
3583 if pages.len() == initial_page_count {
3584 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3586 cursor.elements.push(make_wrapper(
3587 rect_start_y,
3588 table_bottom_y - rect_start_y,
3589 child_elements,
3590 draw_cmd,
3591 ));
3592 } else {
3593 let page = &mut pages[initial_page_count];
3612 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3613 let page_content_bottom =
3614 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3615 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3616 if !our_elements.is_empty() {
3617 page.elements.push(make_wrapper(
3618 rect_start_y,
3619 page_content_bottom - rect_start_y,
3620 our_elements,
3621 draw_cmd.clone(),
3622 ));
3623 }
3624
3625 for page in &mut pages[initial_page_count + 1..] {
3627 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3628 let content_top = page.config.margin.top + header_h;
3629 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3630 let content_bottom = page.config.margin.top
3631 + (page.height - page.config.margin.vertical())
3632 - footer_h;
3633 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3634 if !all_elements.is_empty() {
3635 page.elements.push(make_wrapper(
3636 content_top,
3637 content_bottom - content_top,
3638 all_elements,
3639 draw_cmd.clone(),
3640 ));
3641 }
3642 }
3643
3644 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3646 if !all_elements.is_empty() {
3647 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3648 let content_top = cursor.content_y + header_h;
3649 cursor.elements.push(make_wrapper(
3650 content_top,
3651 table_bottom_y - content_top,
3652 all_elements,
3653 draw_cmd,
3654 ));
3655 }
3656 }
3657
3658 cursor.y += padding.bottom + border.bottom + margin.bottom;
3659 }
3660
3661 fn subtree_forces_break(node: &Node) -> bool {
3671 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
3672 return true;
3673 }
3674 if node.style.break_before == Some(true) {
3675 return true;
3676 }
3677 node.children.iter().any(Self::subtree_forces_break)
3678 }
3679
3680 #[allow(clippy::too_many_arguments)]
3681 fn layout_table_row(
3682 &self,
3683 row: &Node,
3684 col_widths: &[f64],
3685 parent_style: &ResolvedStyle,
3686 cursor: &mut PageCursor,
3687 start_x: f64,
3688 font_context: &FontContext,
3689 pages: &mut Vec<LayoutPage>,
3690 ) {
3691 let row_style = row
3692 .style
3693 .resolve(Some(parent_style), col_widths.iter().sum());
3694
3695 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
3696 let row_bl = self.row_baseline(row, &row_style, col_widths);
3697 let row_y = cursor.content_y + cursor.y;
3698 let total_width: f64 = col_widths.iter().sum();
3699
3700 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3701
3702 let row_snapshot = cursor.elements.len();
3704
3705 let row_may_break =
3717 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
3718
3719 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3720 let mut cell_x = start_x;
3721 let mut col_idx = 0usize;
3727 for cell in row.children.iter() {
3728 let span = match &cell.kind {
3729 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
3730 _ => 1,
3731 };
3732 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
3733 col_idx += span;
3734
3735 let cell_style = cell.style.resolve(Some(&row_style), col_width);
3736
3737 let cell_snapshot = cursor.elements.len();
3739
3740 let inner_width =
3741 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
3742
3743 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
3744 let saved_y = cursor.y;
3745 cursor.y += cell_style.padding.top + cell_style.border_width.top;
3746
3747 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
3751 let content_h: f64 = cell
3752 .children
3753 .iter()
3754 .map(|ch| {
3755 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
3756 self.measure_node_height(ch, inner_width, &ch_style, font_context)
3757 })
3758 .sum();
3759 let inner_row =
3760 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
3761 let slack = (inner_row - content_h).max(0.0);
3762 cursor.y += match cell_style.vertical_align {
3763 crate::style::VerticalAlign::Middle => slack / 2.0,
3764 crate::style::VerticalAlign::Bottom => slack,
3765 crate::style::VerticalAlign::Baseline => row_bl
3770 .map(|b| {
3771 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
3772 (b - d).max(0.0)
3773 })
3774 .unwrap_or(0.0),
3775 crate::style::VerticalAlign::Top => 0.0,
3776 };
3777 }
3778
3779 let cursor_before_cell = if row_may_break {
3784 Some(cursor.clone())
3785 } else {
3786 None
3787 };
3788 let mut cell_pages: Vec<LayoutPage> = Vec::new();
3789 for child in &cell.children {
3790 self.layout_node(
3791 child,
3792 cursor,
3793 &mut cell_pages,
3794 content_x,
3795 inner_width,
3796 Some(&cell_style),
3797 font_context,
3798 None,
3799 None,
3800 );
3801 }
3802
3803 if !cell_pages.is_empty() {
3805 let post_break_elements = std::mem::take(&mut cursor.elements);
3806 if let Some(last_page) = cell_pages.last_mut() {
3807 last_page.elements.extend(post_break_elements);
3808 }
3809 if !is_header {
3815 all_overflow_pages.extend(cell_pages);
3816 }
3817 *cursor = cursor_before_cell
3823 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
3824 }
3825
3826 cursor.y = saved_y;
3827
3828 let cell_children: Vec<LayoutElement> =
3830 cursor.elements.drain(cell_snapshot..).collect();
3831
3832 cursor.elements.push(LayoutElement {
3834 x: cell_x,
3835 y: row_y,
3836 width: col_width,
3837 height: row_height,
3838 draw: if cell_style.background_color.is_some()
3839 || cell_style.border_width.horizontal() > 0.0
3840 || cell_style.border_width.vertical() > 0.0
3841 {
3842 DrawCommand::Rect {
3843 background: cell_style.background_color,
3844 border_width: cell_style.border_width,
3845 border_color: cell_style.border_color,
3846 border_style: cell_style.border_style,
3847 border_radius: cell_style.border_radius,
3848 opacity: 1.0,
3849 box_shadow: cell_style.box_shadow.map(Box::new),
3850 background_gradient: cell_style.background.clone().map(Box::new),
3851 }
3852 } else {
3853 DrawCommand::None
3854 },
3855 children: cell_children,
3856 node_type: Some("TableCell".to_string()),
3857 resolved_style: Some(cell_style.clone()),
3858 source_location: cell.source_location.clone(),
3859 href: None,
3860 bookmark: cell.bookmark.clone(),
3861 alt: None,
3862 is_header_row: is_header,
3863 col_span: span as u32,
3864 overflow: Overflow::default(),
3865 opacity: 1.0,
3866 });
3867
3868 cell_x += col_width;
3869 }
3870
3871 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
3873 let row_element = LayoutElement {
3874 x: start_x,
3875 y: row_y,
3876 width: total_width,
3877 height: row_height,
3878 draw: if let Some(bg) = row_style.background_color {
3879 DrawCommand::Rect {
3880 background: Some(bg),
3881 border_width: Edges::default(),
3882 border_color: EdgeValues::uniform(Color::BLACK),
3883 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
3884 border_radius: CornerValues::uniform(0.0),
3885 opacity: 1.0,
3886 box_shadow: row_style.box_shadow.map(Box::new),
3887 background_gradient: row_style.background.clone().map(Box::new),
3888 }
3889 } else {
3890 DrawCommand::None
3891 },
3892 children: row_children,
3893 node_type: Some("TableRow".to_string()),
3894 resolved_style: Some(row_style.clone()),
3895 source_location: row.source_location.clone(),
3896 href: None,
3897 bookmark: row.bookmark.clone(),
3898 alt: None,
3899 is_header_row: is_header,
3900 col_span: 1,
3901 overflow: row_style.overflow,
3902 opacity: row_style.opacity,
3903 };
3904
3905 if let Some(first_overflow) = all_overflow_pages.first_mut() {
3906 first_overflow.elements.push(row_element);
3916 } else {
3917 cursor.elements.push(row_element);
3918 cursor.y += row_height;
3919 }
3920
3921 pages.extend(all_overflow_pages);
3923 }
3924
3925 #[allow(clippy::too_many_arguments)]
3926 #[allow(clippy::too_many_arguments)]
3927 fn layout_text(
3928 &self,
3929 content: &str,
3930 href: Option<&str>,
3931 runs: &[TextRun],
3932 style: &ResolvedStyle,
3933 cursor: &mut PageCursor,
3934 pages: &mut Vec<LayoutPage>,
3935 x: f64,
3936 available_width: f64,
3937 font_context: &FontContext,
3938 source_location: Option<&SourceLocation>,
3939 bookmark: Option<&str>,
3940 node_type_override: Option<&str>,
3944 ) {
3945 let margin = &style.margin.to_edges();
3946 let text_x = x + margin.left;
3947 let text_width = match style.width {
3954 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
3955 SizeConstraint::Auto => available_width - margin.horizontal(),
3956 };
3957
3958 cursor.y += margin.top;
3959
3960 if !runs.is_empty() {
3962 self.layout_text_runs(
3963 runs,
3964 href,
3965 style,
3966 cursor,
3967 pages,
3968 text_x,
3969 text_width,
3970 font_context,
3971 source_location,
3972 bookmark,
3973 node_type_override,
3974 );
3975 cursor.y += margin.bottom;
3976 return;
3977 }
3978
3979 let content = substitute_page_placeholders(content);
3980 let transformed = apply_text_transform(&content, style.text_transform);
3981 let justify = matches!(style.text_align, TextAlign::Justify);
3982 let lines = match style.line_breaking {
3983 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
3984 font_context,
3985 &transformed,
3986 text_width,
3987 style.font_size,
3988 &style.font_family,
3989 style.font_weight,
3990 style.font_style,
3991 style.letter_spacing,
3992 style.hyphens,
3993 style.lang.as_deref(),
3994 justify,
3995 ),
3996 LineBreaking::Greedy => self.text_layout.break_into_lines(
3997 font_context,
3998 &transformed,
3999 text_width,
4000 style.font_size,
4001 &style.font_family,
4002 style.font_weight,
4003 style.font_style,
4004 style.letter_spacing,
4005 style.hyphens,
4006 style.lang.as_deref(),
4007 ),
4008 };
4009
4010 let lines = match style.text_overflow {
4012 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4013 font_context,
4014 lines,
4015 text_width,
4016 style.font_size,
4017 &style.font_family,
4018 style.font_weight,
4019 style.font_style,
4020 style.letter_spacing,
4021 ),
4022 TextOverflow::Clip => self.text_layout.truncate_clip(
4023 font_context,
4024 lines,
4025 text_width,
4026 style.font_size,
4027 &style.font_family,
4028 style.font_weight,
4029 style.font_style,
4030 style.letter_spacing,
4031 ),
4032 TextOverflow::Wrap => lines,
4033 };
4034
4035 let line_height = style.font_size * style.line_height;
4036
4037 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4039 let decision = page_break::decide_break(
4040 cursor.remaining_height(),
4041 &line_heights,
4042 true,
4043 style.min_orphan_lines as usize,
4044 style.min_widow_lines as usize,
4045 );
4046
4047 let mut snapshot = cursor.elements.len();
4049 let mut container_start_y = cursor.content_y + cursor.y;
4050 let mut is_first_element = true;
4051
4052 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4054 pages.push(cursor.finalize());
4055 *cursor = cursor.new_page();
4056 snapshot = cursor.elements.len();
4057 container_start_y = cursor.content_y + cursor.y;
4058 }
4059
4060 let forced_break_at = match decision {
4062 page_break::BreakDecision::Split {
4063 items_on_current_page,
4064 } => Some(items_on_current_page),
4065 _ => None,
4066 };
4067 let mut first_break_done = false;
4068
4069 for (line_idx, line) in lines.iter().enumerate() {
4070 let needs_break = if let Some(break_at) = forced_break_at {
4072 if !first_break_done && line_idx == break_at {
4073 true
4074 } else {
4075 line_height > cursor.remaining_height()
4076 }
4077 } else {
4078 line_height > cursor.remaining_height()
4079 };
4080
4081 if needs_break {
4082 first_break_done = true;
4083 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4085 if !line_elements.is_empty() {
4086 let container_height = cursor.content_y + cursor.y - container_start_y;
4087 cursor.elements.push(LayoutElement {
4088 x: text_x,
4089 y: container_start_y,
4090 width: text_width,
4091 height: container_height,
4092 draw: DrawCommand::None,
4093 children: line_elements,
4094 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4095 resolved_style: Some(style.clone()),
4096 source_location: source_location.cloned(),
4097 href: href.map(|s| s.to_string()),
4098 bookmark: if is_first_element {
4099 bookmark.map(|s| s.to_string())
4100 } else {
4101 None
4102 },
4103 alt: None,
4104 is_header_row: false,
4105 col_span: 1,
4106 overflow: Overflow::default(),
4107 opacity: 1.0,
4108 });
4109 is_first_element = false;
4110 }
4111
4112 pages.push(cursor.finalize());
4113 *cursor = cursor.new_page();
4114
4115 snapshot = cursor.elements.len();
4117 container_start_y = cursor.content_y + cursor.y;
4118 }
4119
4120 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4121
4122 let rendered_width = if glyphs.is_empty() {
4126 line.width
4127 } else {
4128 let last = &glyphs[glyphs.len() - 1];
4129 (last.x_offset + last.x_advance).max(line.width * 0.5)
4130 };
4131
4132 let line_x = match style.text_align {
4133 TextAlign::Left => text_x,
4134 TextAlign::Right => text_x + text_width - rendered_width,
4135 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4136 TextAlign::Justify => text_x,
4137 };
4138
4139 let is_last_line = line_idx == lines.len() - 1;
4147 let user_ws = style.word_spacing;
4148 let (justified_width, word_spacing) =
4149 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4150 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4151 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4152 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4153 let s = glyphs[..=idx]
4154 .iter()
4155 .filter(|g| g.char_value == ' ')
4156 .count();
4157 (w, s)
4158 } else {
4159 (0.0, 0)
4160 };
4161 let slack = text_width - natural_width;
4162 let ws = if space_count > 0 && slack.abs() > 0.01 {
4163 slack / space_count as f64
4164 } else {
4165 0.0
4166 };
4167 (text_width, user_ws + ws)
4168 } else {
4169 (rendered_width, user_ws)
4170 };
4171
4172 let text_line = TextLine {
4173 x: line_x,
4174 y: cursor.content_y + cursor.y + style.font_size,
4175 glyphs,
4176 width: justified_width,
4177 height: line_height,
4178 word_spacing,
4179 };
4180
4181 cursor.elements.push(LayoutElement {
4182 x: line_x,
4183 y: cursor.content_y + cursor.y,
4184 width: justified_width,
4185 height: line_height,
4186 draw: DrawCommand::Text {
4187 lines: vec![text_line],
4188 color: style.color,
4189 text_decoration: style.text_decoration,
4190 opacity: 1.0,
4191 },
4192 children: vec![],
4193 node_type: Some("TextLine".to_string()),
4194 resolved_style: Some(style.clone()),
4195 source_location: None,
4196 href: href.map(|s| s.to_string()),
4197 bookmark: None,
4198 alt: None,
4199 is_header_row: false,
4200 col_span: 1,
4201 overflow: Overflow::default(),
4202 opacity: 1.0,
4203 });
4204
4205 cursor.y += line_height;
4206 }
4207
4208 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4210 if !line_elements.is_empty() {
4211 let container_height = cursor.content_y + cursor.y - container_start_y;
4212 cursor.elements.push(LayoutElement {
4213 x: text_x,
4214 y: container_start_y,
4215 width: text_width,
4216 height: container_height,
4217 draw: DrawCommand::None,
4218 children: line_elements,
4219 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4220 resolved_style: Some(style.clone()),
4221 source_location: source_location.cloned(),
4222 href: href.map(|s| s.to_string()),
4223 bookmark: if is_first_element {
4224 bookmark.map(|s| s.to_string())
4225 } else {
4226 None
4227 },
4228 alt: None,
4229 is_header_row: false,
4230 col_span: 1,
4231 overflow: Overflow::default(),
4232 opacity: 1.0,
4233 });
4234 }
4235
4236 cursor.y += margin.bottom;
4237 }
4238
4239 #[allow(clippy::too_many_arguments)]
4241 #[allow(clippy::too_many_arguments)]
4242 fn layout_text_runs(
4243 &self,
4244 runs: &[TextRun],
4245 parent_href: Option<&str>,
4246 style: &ResolvedStyle,
4247 cursor: &mut PageCursor,
4248 pages: &mut Vec<LayoutPage>,
4249 text_x: f64,
4250 text_width: f64,
4251 font_context: &FontContext,
4252 source_location: Option<&SourceLocation>,
4253 bookmark: Option<&str>,
4254 node_type_override: Option<&str>,
4256 ) {
4257 let mut styled_chars: Vec<StyledChar> = Vec::new();
4259 for run in runs {
4260 let run_style = run.style.resolve(Some(style), text_width);
4261 let run_href = run.href.as_deref().or(parent_href);
4262 let transform = run_style.text_transform;
4263 let run_content = substitute_page_placeholders(&run.content);
4264 let mut prev_is_whitespace = true;
4265 for ch in run_content.chars() {
4266 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4267 prev_is_whitespace = ch.is_whitespace();
4268 styled_chars.push(StyledChar {
4269 ch: transformed_ch,
4270 font_family: run_style.font_family.clone(),
4271 font_size: run_style.font_size,
4272 font_weight: run_style.font_weight,
4273 font_style: run_style.font_style,
4274 color: run_style.color,
4275 href: run_href.map(|s| s.to_string()),
4276 text_decoration: run_style.text_decoration,
4277 letter_spacing: run_style.letter_spacing,
4278 });
4279 }
4280 }
4281
4282 let justify = matches!(style.text_align, TextAlign::Justify);
4284 let broken_lines = match style.line_breaking {
4285 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4286 font_context,
4287 &styled_chars,
4288 text_width,
4289 style.hyphens,
4290 style.lang.as_deref(),
4291 justify,
4292 ),
4293 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4294 font_context,
4295 &styled_chars,
4296 text_width,
4297 style.hyphens,
4298 style.lang.as_deref(),
4299 ),
4300 };
4301
4302 let broken_lines = match style.text_overflow {
4304 TextOverflow::Ellipsis => {
4305 self.text_layout
4306 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4307 }
4308 TextOverflow::Clip => {
4309 self.text_layout
4310 .truncate_runs_clip(font_context, broken_lines, text_width)
4311 }
4312 TextOverflow::Wrap => broken_lines,
4313 };
4314
4315 let line_height = style.font_size * style.line_height;
4316
4317 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4319 let decision = page_break::decide_break(
4320 cursor.remaining_height(),
4321 &line_heights,
4322 true,
4323 style.min_orphan_lines as usize,
4324 style.min_widow_lines as usize,
4325 );
4326
4327 let mut snapshot = cursor.elements.len();
4328 let mut container_start_y = cursor.content_y + cursor.y;
4329 let mut is_first_element = true;
4330
4331 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4332 pages.push(cursor.finalize());
4333 *cursor = cursor.new_page();
4334 snapshot = cursor.elements.len();
4335 container_start_y = cursor.content_y + cursor.y;
4336 }
4337
4338 let forced_break_at = match decision {
4339 page_break::BreakDecision::Split {
4340 items_on_current_page,
4341 } => Some(items_on_current_page),
4342 _ => None,
4343 };
4344 let mut first_break_done = false;
4345
4346 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4347 let needs_break = if let Some(break_at) = forced_break_at {
4348 if !first_break_done && line_idx == break_at {
4349 true
4350 } else {
4351 line_height > cursor.remaining_height()
4352 }
4353 } else {
4354 line_height > cursor.remaining_height()
4355 };
4356
4357 if needs_break {
4358 first_break_done = true;
4359 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4360 if !line_elements.is_empty() {
4361 let container_height = cursor.content_y + cursor.y - container_start_y;
4362 cursor.elements.push(LayoutElement {
4363 x: text_x,
4364 y: container_start_y,
4365 width: text_width,
4366 height: container_height,
4367 draw: DrawCommand::None,
4368 children: line_elements,
4369 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4370 resolved_style: Some(style.clone()),
4371 source_location: source_location.cloned(),
4372 href: parent_href.map(|s| s.to_string()),
4373 bookmark: if is_first_element {
4374 bookmark.map(|s| s.to_string())
4375 } else {
4376 None
4377 },
4378 alt: None,
4379 is_header_row: false,
4380 col_span: 1,
4381 overflow: Overflow::default(),
4382 opacity: 1.0,
4383 });
4384 is_first_element = false;
4385 }
4386
4387 pages.push(cursor.finalize());
4388 *cursor = cursor.new_page();
4389
4390 snapshot = cursor.elements.len();
4391 container_start_y = cursor.content_y + cursor.y;
4392 }
4393
4394 let line_x = match style.text_align {
4395 TextAlign::Left => text_x,
4396 TextAlign::Right => text_x + text_width - run_line.width,
4397 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
4398 TextAlign::Justify => text_x,
4399 };
4400
4401 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
4402
4403 let is_last_line = line_idx == broken_lines.len() - 1;
4410 let user_ws = style.word_spacing;
4411 let (justified_width, word_spacing) =
4412 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4413 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4414 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4415 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4416 let s = glyphs[..=idx]
4417 .iter()
4418 .filter(|g| g.char_value == ' ')
4419 .count();
4420 (w, s)
4421 } else {
4422 (0.0, 0)
4423 };
4424 let slack = text_width - natural_width;
4425 let ws = if space_count > 0 && slack.abs() > 0.01 {
4426 slack / space_count as f64
4427 } else {
4428 0.0
4429 };
4430 (text_width, user_ws + ws)
4431 } else {
4432 (run_line.width, user_ws)
4433 };
4434
4435 let text_line = TextLine {
4436 x: line_x,
4437 y: cursor.content_y + cursor.y + style.font_size,
4438 glyphs,
4439 width: justified_width,
4440 height: line_height,
4441 word_spacing,
4442 };
4443
4444 let text_dec = run_line
4446 .chars
4447 .iter()
4448 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4449 .map(|sc| sc.text_decoration)
4450 .unwrap_or(style.text_decoration);
4451
4452 cursor.elements.push(LayoutElement {
4453 x: line_x,
4454 y: cursor.content_y + cursor.y,
4455 width: justified_width,
4456 height: line_height,
4457 draw: DrawCommand::Text {
4458 lines: vec![text_line],
4459 color: style.color,
4460 text_decoration: text_dec,
4461 opacity: 1.0,
4462 },
4463 children: vec![],
4464 node_type: Some("TextLine".to_string()),
4465 resolved_style: Some(style.clone()),
4466 source_location: None,
4467 href: parent_href.map(|s| s.to_string()),
4468 bookmark: None,
4469 alt: None,
4470 is_header_row: false,
4471 col_span: 1,
4472 overflow: Overflow::default(),
4473 opacity: 1.0,
4474 });
4475
4476 cursor.y += line_height;
4477 }
4478
4479 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4480 if !line_elements.is_empty() {
4481 let container_height = cursor.content_y + cursor.y - container_start_y;
4482 cursor.elements.push(LayoutElement {
4483 x: text_x,
4484 y: container_start_y,
4485 width: text_width,
4486 height: container_height,
4487 draw: DrawCommand::None,
4488 children: line_elements,
4489 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4490 resolved_style: Some(style.clone()),
4491 source_location: source_location.cloned(),
4492 href: parent_href.map(|s| s.to_string()),
4493 bookmark: if is_first_element {
4494 bookmark.map(|s| s.to_string())
4495 } else {
4496 None
4497 },
4498 alt: None,
4499 is_header_row: false,
4500 col_span: 1,
4501 overflow: Overflow::default(),
4502 opacity: 1.0,
4503 });
4504 }
4505 }
4506
4507 fn build_positioned_glyphs_single_style(
4511 &self,
4512 line: &BrokenLine,
4513 style: &ResolvedStyle,
4514 href: Option<&str>,
4515 font_context: &FontContext,
4516 ) -> Vec<PositionedGlyph> {
4517 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4518 let line_text: String = line.chars.iter().collect();
4519 let direction = style.direction;
4520 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4522
4523 let font_runs = crate::font::fallback::segment_by_font(
4526 &line.chars,
4527 &style.font_family,
4528 style.font_weight,
4529 italic,
4530 font_context.registry(),
4531 );
4532 let needs_per_char_fallback = font_runs.len() > 1
4533 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4534
4535 if needs_per_char_fallback {
4537 let bidi_runs = if has_bidi {
4538 bidi::analyze_bidi(&line_text, direction)
4539 } else {
4540 vec![crate::text::bidi::BidiRun {
4541 char_start: 0,
4542 char_end: line.chars.len(),
4543 level: unicode_bidi::Level::ltr(),
4544 is_rtl: false,
4545 }]
4546 };
4547
4548 let mut all_glyphs = Vec::new();
4549 let mut bidi_levels = Vec::new();
4550 let mut x = 0.0_f64;
4551
4552 for bidi_run in &bidi_runs {
4554 for font_run in &font_runs {
4556 let start = font_run.start.max(bidi_run.char_start);
4558 let end = font_run.end.min(bidi_run.char_end);
4559 if start >= end {
4560 continue;
4561 }
4562
4563 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4564 let sub_text: String = sub_chars.iter().collect();
4565 let resolved_family = &font_run.family;
4566
4567 if let Some(font_data) =
4568 font_context.font_data(resolved_family, style.font_weight, italic)
4569 {
4570 if let Some(shaped) = shaping::shape_text_with_direction(
4571 &sub_text,
4572 font_data,
4573 bidi_run.is_rtl,
4574 ) {
4575 let units_per_em = font_context.units_per_em(
4576 resolved_family,
4577 style.font_weight,
4578 italic,
4579 );
4580 let scale = style.font_size / units_per_em as f64;
4581
4582 for sg in &shaped {
4583 let cluster = sg.cluster as usize;
4584 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4585
4586 let cluster_text = if shaped.len() < sub_chars.len() {
4587 let cluster_end =
4588 self.find_cluster_end(&shaped, sg, sub_chars.len());
4589 if cluster_end > cluster + 1 {
4590 Some(
4591 sub_chars[cluster..cluster_end]
4592 .iter()
4593 .collect::<String>(),
4594 )
4595 } else {
4596 None
4597 }
4598 } else {
4599 None
4600 };
4601
4602 let glyph_x = x + sg.x_offset as f64 * scale;
4603 let glyph_y = sg.y_offset as f64 * scale;
4604 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4605
4606 all_glyphs.push(PositionedGlyph {
4607 glyph_id: sg.glyph_id,
4608 x_offset: glyph_x,
4609 y_offset: glyph_y,
4610 x_advance: advance,
4611 font_size: style.font_size,
4612 font_family: Arc::from(resolved_family.as_str()),
4613 font_weight: style.font_weight,
4614 font_style: style.font_style,
4615 char_value,
4616 color: Some(style.color),
4617 href: href.map(|s| s.to_string()),
4618 text_decoration: style.text_decoration,
4619 letter_spacing: style.letter_spacing,
4620 cluster_text,
4621 });
4622 bidi_levels.push(bidi_run.level);
4623 x += advance;
4624 }
4625 continue;
4626 }
4627 }
4628
4629 for i in start..end {
4631 let ch = line.chars[i];
4632 let glyph_x = x;
4633 let char_width = font_context.char_width(
4634 ch,
4635 resolved_family,
4636 style.font_weight,
4637 italic,
4638 style.font_size,
4639 );
4640 let advance = char_width + style.letter_spacing;
4641 all_glyphs.push(PositionedGlyph {
4642 glyph_id: ch as u16,
4643 x_offset: glyph_x,
4644 y_offset: 0.0,
4645 x_advance: advance,
4646 font_size: style.font_size,
4647 font_family: Arc::from(resolved_family.as_str()),
4648 font_weight: style.font_weight,
4649 font_style: style.font_style,
4650 char_value: ch,
4651 color: Some(style.color),
4652 href: href.map(|s| s.to_string()),
4653 text_decoration: style.text_decoration,
4654 letter_spacing: style.letter_spacing,
4655 cluster_text: None,
4656 });
4657 bidi_levels.push(bidi_run.level);
4658 x += advance;
4659 }
4660 }
4661 }
4662
4663 if has_bidi && !all_glyphs.is_empty() {
4665 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4666 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
4667 }
4668 return all_glyphs;
4669 }
4670
4671 if let Some(font_data) =
4674 font_context.font_data(&style.font_family, style.font_weight, italic)
4675 {
4676 if has_bidi {
4677 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4679 let units_per_em =
4680 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4681 let scale = style.font_size / units_per_em as f64;
4682
4683 let mut all_glyphs = Vec::new();
4684 let mut bidi_levels = Vec::new();
4685 let mut x = 0.0_f64;
4686
4687 for run in &bidi_runs {
4688 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
4689 let run_text: String = run_chars.iter().collect();
4690
4691 if let Some(shaped) =
4692 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
4693 {
4694 for sg in &shaped {
4695 let cluster = sg.cluster as usize;
4696 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
4697
4698 let cluster_text = if shaped.len() < run_chars.len() {
4699 let cluster_end =
4700 self.find_cluster_end(&shaped, sg, run_chars.len());
4701 if cluster_end > cluster + 1 {
4702 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
4703 } else {
4704 None
4705 }
4706 } else {
4707 None
4708 };
4709
4710 let glyph_x = x + sg.x_offset as f64 * scale;
4711 let glyph_y = sg.y_offset as f64 * scale;
4712 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4713
4714 all_glyphs.push(PositionedGlyph {
4715 glyph_id: sg.glyph_id,
4716 x_offset: glyph_x,
4717 y_offset: glyph_y,
4718 x_advance: advance,
4719 font_size: style.font_size,
4720 font_family: Arc::from(style.font_family.as_str()),
4721 font_weight: style.font_weight,
4722 font_style: style.font_style,
4723 char_value,
4724 color: Some(style.color),
4725 href: href.map(|s| s.to_string()),
4726 text_decoration: style.text_decoration,
4727 letter_spacing: style.letter_spacing,
4728 cluster_text,
4729 });
4730 bidi_levels.push(run.level);
4731
4732 x += advance;
4733 }
4734 }
4735 }
4736
4737 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4739 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4740 return glyphs;
4741 }
4742
4743 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
4745 let units_per_em =
4746 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4747 let scale = style.font_size / units_per_em as f64;
4748
4749 return self.shaped_glyphs_to_positioned(
4750 &shaped,
4751 &line.chars,
4752 &line.char_positions,
4753 scale,
4754 style.font_size,
4755 &style.font_family,
4756 style.font_weight,
4757 style.font_style,
4758 Some(style.color),
4759 href,
4760 style.text_decoration,
4761 style.letter_spacing,
4762 );
4763 }
4764 }
4765
4766 let mut glyphs: Vec<PositionedGlyph> = line
4768 .chars
4769 .iter()
4770 .enumerate()
4771 .map(|(j, ch)| {
4772 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
4773 let char_width = font_context.char_width(
4774 *ch,
4775 &style.font_family,
4776 style.font_weight,
4777 italic,
4778 style.font_size,
4779 );
4780 PositionedGlyph {
4781 glyph_id: *ch as u16,
4782 x_offset: glyph_x,
4783 y_offset: 0.0,
4784 x_advance: char_width,
4785 font_size: style.font_size,
4786 font_family: Arc::from(style.font_family.as_str()),
4787 font_weight: style.font_weight,
4788 font_style: style.font_style,
4789 char_value: *ch,
4790 color: Some(style.color),
4791 href: href.map(|s| s.to_string()),
4792 text_decoration: style.text_decoration,
4793 letter_spacing: style.letter_spacing,
4794 cluster_text: None,
4795 }
4796 })
4797 .collect();
4798
4799 if has_bidi && !glyphs.is_empty() {
4801 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4802 let mut levels = Vec::with_capacity(glyphs.len());
4803 let mut char_idx = 0;
4804 for run in &bidi_runs {
4805 for _ in run.char_start..run.char_end {
4806 if char_idx < glyphs.len() {
4807 levels.push(run.level);
4808 char_idx += 1;
4809 }
4810 }
4811 }
4812 while levels.len() < glyphs.len() {
4814 levels.push(unicode_bidi::Level::ltr());
4815 }
4816 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
4817 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4818 }
4819
4820 glyphs
4821 }
4822
4823 fn build_positioned_glyphs_runs(
4828 &self,
4829 run_line: &RunBrokenLine,
4830 font_context: &FontContext,
4831 direction: Direction,
4832 ) -> Vec<PositionedGlyph> {
4833 let chars = &run_line.chars;
4834 if chars.is_empty() {
4835 return vec![];
4836 }
4837
4838 let resolved_families: Vec<String> = chars
4841 .iter()
4842 .map(|sc| {
4843 if !sc.font_family.contains(',') {
4844 sc.font_family.clone()
4845 } else {
4846 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4847 let (_, family) = font_context.registry().resolve_for_char(
4848 &sc.font_family,
4849 sc.ch,
4850 sc.font_weight,
4851 italic,
4852 );
4853 family
4854 }
4855 })
4856 .collect();
4857
4858 let line_text: String = chars.iter().map(|c| c.ch).collect();
4859 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4860 let bidi_runs = if has_bidi {
4861 Some(bidi::analyze_bidi(&line_text, direction))
4862 } else {
4863 None
4864 };
4865
4866 let mut glyphs = Vec::new();
4867 let mut bidi_levels = Vec::new();
4868 let mut i = 0;
4869
4870 while i < chars.len() {
4871 let sc = &chars[i];
4872 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4873 let resolved_family = &resolved_families[i];
4874
4875 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4877 runs.iter()
4878 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
4879 });
4880
4881 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
4883 {
4884 let run_start = i;
4886 let mut run_end = i + 1;
4887 while run_end < chars.len() {
4888 let next = &chars[run_end];
4889 let next_italic =
4890 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
4891 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4892 runs.iter()
4893 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
4894 });
4895 if resolved_families[run_end] == *resolved_family
4897 && next.font_weight == sc.font_weight
4898 && next_italic == italic
4899 && (next.font_size - sc.font_size).abs() < 0.001
4900 && next_is_rtl == is_rtl
4901 {
4902 run_end += 1;
4903 } else {
4904 break;
4905 }
4906 }
4907
4908 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
4909 if let Some(shaped) =
4910 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
4911 {
4912 let units_per_em =
4913 font_context.units_per_em(resolved_family, sc.font_weight, italic);
4914 let scale = sc.font_size / units_per_em as f64;
4915
4916 let run_chars: Vec<char> =
4918 chars[run_start..run_end].iter().map(|c| c.ch).collect();
4919 let run_positions: Vec<f64> = (run_start..run_end)
4920 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
4921 .collect();
4922
4923 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
4925 &shaped,
4926 &chars[run_start..run_end],
4927 &run_chars,
4928 &run_positions,
4929 scale,
4930 );
4931 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
4933 for g in &mut run_glyphs {
4934 g.font_family = resolved_family_arc.clone();
4935 }
4936 let run_level = if is_rtl {
4938 unicode_bidi::Level::rtl()
4939 } else {
4940 unicode_bidi::Level::ltr()
4941 };
4942 for _ in &run_glyphs {
4943 bidi_levels.push(run_level);
4944 }
4945 glyphs.extend(run_glyphs);
4946 i = run_end;
4947 continue;
4948 }
4949 }
4950
4951 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
4953 let char_width = font_context.char_width(
4954 sc.ch,
4955 resolved_family,
4956 sc.font_weight,
4957 italic,
4958 sc.font_size,
4959 );
4960 glyphs.push(PositionedGlyph {
4961 glyph_id: sc.ch as u16,
4962 x_offset: glyph_x,
4963 y_offset: 0.0,
4964 x_advance: char_width,
4965 font_size: sc.font_size,
4966 font_family: Arc::from(resolved_family.as_str()),
4967 font_weight: sc.font_weight,
4968 font_style: sc.font_style,
4969 char_value: sc.ch,
4970 color: Some(sc.color),
4971 href: sc.href.clone(),
4972 text_decoration: sc.text_decoration,
4973 letter_spacing: sc.letter_spacing,
4974 cluster_text: None,
4975 });
4976 bidi_levels.push(if is_rtl {
4977 unicode_bidi::Level::rtl()
4978 } else {
4979 unicode_bidi::Level::ltr()
4980 });
4981 i += 1;
4982 }
4983
4984 if has_bidi && !glyphs.is_empty() {
4986 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
4987 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4988 }
4989
4990 glyphs
4991 }
4992
4993 #[allow(clippy::too_many_arguments)]
4995 fn shaped_glyphs_to_positioned(
4996 &self,
4997 shaped: &[shaping::ShapedGlyph],
4998 chars: &[char],
4999 _char_positions: &[f64],
5000 scale: f64,
5001 font_size: f64,
5002 font_family: &str,
5003 font_weight: u32,
5004 font_style: FontStyle,
5005 color: Option<Color>,
5006 href: Option<&str>,
5007 text_decoration: TextDecoration,
5008 letter_spacing: f64,
5009 ) -> Vec<PositionedGlyph> {
5010 let mut result = Vec::with_capacity(shaped.len());
5011 let mut x = 0.0_f64;
5012
5013 for sg in shaped {
5014 let cluster = sg.cluster as usize;
5015 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5016
5017 let cluster_text = if shaped.len() < chars.len() {
5019 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5022 if cluster_end > cluster + 1 {
5023 Some(chars[cluster..cluster_end].iter().collect::<String>())
5024 } else {
5025 None
5026 }
5027 } else {
5028 None
5029 };
5030
5031 let glyph_x = x + sg.x_offset as f64 * scale;
5033 let glyph_y = sg.y_offset as f64 * scale;
5034 let advance = sg.x_advance as f64 * scale + letter_spacing;
5035
5036 result.push(PositionedGlyph {
5037 glyph_id: sg.glyph_id,
5038 x_offset: glyph_x,
5039 y_offset: glyph_y,
5040 x_advance: advance,
5041 font_size,
5042 font_family: Arc::from(font_family),
5043 font_weight,
5044 font_style,
5045 char_value,
5046 color,
5047 href: href.map(|s| s.to_string()),
5048 text_decoration,
5049 letter_spacing,
5050 cluster_text,
5051 });
5052
5053 x += advance;
5054 }
5055
5056 result
5057 }
5058
5059 fn shaped_glyphs_to_positioned_runs(
5061 &self,
5062 shaped: &[shaping::ShapedGlyph],
5063 styled_chars: &[StyledChar],
5064 chars: &[char],
5065 char_positions: &[f64],
5066 scale: f64,
5067 ) -> Vec<PositionedGlyph> {
5068 let mut result = Vec::with_capacity(shaped.len());
5069 let base_x = char_positions.first().copied().unwrap_or(0.0);
5071 let mut x = 0.0_f64;
5072
5073 for sg in shaped {
5074 let cluster = sg.cluster as usize;
5075 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5076 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5077
5078 let cluster_text = if shaped.len() < chars.len() {
5079 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5080 if cluster_end > cluster + 1 {
5081 Some(chars[cluster..cluster_end].iter().collect::<String>())
5082 } else {
5083 None
5084 }
5085 } else {
5086 None
5087 };
5088
5089 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5090 let glyph_y = sg.y_offset as f64 * scale;
5091 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5092
5093 result.push(PositionedGlyph {
5094 glyph_id: sg.glyph_id,
5095 x_offset: glyph_x,
5096 y_offset: glyph_y,
5097 x_advance: advance,
5098 font_size: sc.font_size,
5099 font_family: Arc::from(sc.font_family.as_str()),
5100 font_weight: sc.font_weight,
5101 font_style: sc.font_style,
5102 char_value,
5103 color: Some(sc.color),
5104 href: sc.href.clone(),
5105 text_decoration: sc.text_decoration,
5106 letter_spacing: sc.letter_spacing,
5107 cluster_text,
5108 });
5109
5110 x += advance;
5111 }
5112
5113 result
5114 }
5115
5116 fn find_cluster_end(
5118 &self,
5119 shaped: &[shaping::ShapedGlyph],
5120 current: &shaping::ShapedGlyph,
5121 num_chars: usize,
5122 ) -> usize {
5123 for sg in shaped {
5125 if sg.cluster > current.cluster {
5126 return sg.cluster as usize;
5127 }
5128 }
5129 num_chars
5131 }
5132
5133 fn image_display_size(
5143 &self,
5144 src: &str,
5145 style: &ResolvedStyle,
5146 explicit_width: Option<f64>,
5147 explicit_height: Option<f64>,
5148 available_width: f64,
5149 ) -> (f64, f64) {
5150 let dims = if src.is_empty() {
5151 None
5152 } else {
5153 self.get_image_dimensions(src)
5154 };
5155 let aspect = dims
5156 .map(|(w, h)| {
5157 if w > 0 {
5158 f64::from(h) / f64::from(w)
5159 } else {
5160 0.75
5161 }
5162 })
5163 .unwrap_or(0.75);
5164
5165 let style_w = match style.width {
5166 SizeConstraint::Fixed(w) => Some(w),
5167 SizeConstraint::Auto => None,
5168 };
5169 let style_h = match style.height {
5170 SizeConstraint::Fixed(h) => Some(h),
5171 SizeConstraint::Auto => None,
5172 };
5173 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5174
5175 let width_source = style_w.or(explicit_width);
5176 let height_source = style_h.or(explicit_height);
5177 match (width_source, height_source) {
5178 (Some(w), Some(h)) => (clamp(w), h),
5179 (Some(w), None) => {
5180 let w = clamp(w);
5181 (w, w * aspect)
5182 }
5183 (None, Some(h)) => (clamp(h / aspect), h),
5184 (None, None) => {
5185 let w = clamp(
5188 dims.map(|(w, _)| f64::from(w))
5189 .unwrap_or(available_width)
5190 .min(available_width),
5191 );
5192 (w, w * aspect)
5193 }
5194 }
5195 }
5196
5197 #[allow(clippy::too_many_arguments)]
5198 fn layout_image(
5199 &self,
5200 node: &Node,
5201 style: &ResolvedStyle,
5202 cursor: &mut PageCursor,
5203 pages: &mut Vec<LayoutPage>,
5204 x: f64,
5205 available_width: f64,
5206 explicit_width: Option<f64>,
5207 explicit_height: Option<f64>,
5208 ) {
5209 let margin = &style.margin.to_edges();
5210
5211 let src = match &node.kind {
5213 NodeKind::Image { src, .. } => src.as_str(),
5214 _ => "",
5215 };
5216
5217 let loaded = if !src.is_empty() {
5218 crate::image_loader::load_image(src).ok()
5219 } else {
5220 None
5221 };
5222
5223 let (img_width, img_height) = self.image_display_size(
5224 src,
5225 style,
5226 explicit_width,
5227 explicit_height,
5228 available_width - margin.horizontal(),
5229 );
5230
5231 let total_height = img_height + margin.vertical();
5232
5233 if total_height > cursor.remaining_height() {
5234 pages.push(cursor.finalize());
5235 *cursor = cursor.new_page();
5236 }
5237
5238 cursor.y += margin.top;
5239
5240 let draw = if let Some(image_data) = loaded {
5241 DrawCommand::Image { image_data }
5242 } else {
5243 DrawCommand::ImagePlaceholder
5244 };
5245
5246 cursor.elements.push(LayoutElement {
5247 x: x + margin.left,
5248 y: cursor.content_y + cursor.y,
5249 width: img_width,
5250 height: img_height,
5251 draw,
5252 children: vec![],
5253 node_type: Some(node_kind_name(&node.kind).to_string()),
5254 resolved_style: Some(style.clone()),
5255 source_location: node.source_location.clone(),
5256 href: node.href.clone(),
5257 bookmark: node.bookmark.clone(),
5258 alt: node.alt.clone(),
5259 is_header_row: false,
5260 col_span: 1,
5261 overflow: style.overflow,
5262 opacity: style.opacity,
5263 });
5264
5265 cursor.y += img_height + margin.bottom;
5266 }
5267
5268 #[allow(clippy::too_many_arguments)]
5270 fn layout_svg(
5271 &self,
5272 node: &Node,
5273 style: &ResolvedStyle,
5274 cursor: &mut PageCursor,
5275 pages: &mut Vec<LayoutPage>,
5276 x: f64,
5277 _available_width: f64,
5278 svg_width: f64,
5279 svg_height: f64,
5280 view_box: Option<&str>,
5281 content: &str,
5282 ) {
5283 let margin = &style.margin.to_edges();
5284 let total_height = svg_height + margin.vertical();
5285
5286 if total_height > cursor.remaining_height() {
5287 pages.push(cursor.finalize());
5288 *cursor = cursor.new_page();
5289 }
5290
5291 cursor.y += margin.top;
5292
5293 let vb = view_box
5294 .and_then(crate::svg::parse_view_box)
5295 .unwrap_or(crate::svg::ViewBox {
5296 min_x: 0.0,
5297 min_y: 0.0,
5298 width: svg_width,
5299 height: svg_height,
5300 });
5301
5302 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5303
5304 cursor.elements.push(LayoutElement {
5305 x: x + margin.left,
5306 y: cursor.content_y + cursor.y,
5307 width: svg_width,
5308 height: svg_height,
5309 draw: DrawCommand::Svg {
5310 commands,
5311 width: svg_width,
5312 height: svg_height,
5313 viewbox_min_x: vb.min_x,
5314 viewbox_min_y: vb.min_y,
5315 viewbox_width: vb.width,
5316 viewbox_height: vb.height,
5317 clip: false,
5318 },
5319 children: vec![],
5320 node_type: Some("Svg".to_string()),
5321 resolved_style: Some(style.clone()),
5322 source_location: node.source_location.clone(),
5323 href: node.href.clone(),
5324 bookmark: node.bookmark.clone(),
5325 alt: node.alt.clone(),
5326 is_header_row: false,
5327 col_span: 1,
5328 overflow: style.overflow,
5329 opacity: style.opacity,
5330 });
5331
5332 cursor.y += svg_height + margin.bottom;
5333 }
5334
5335 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
5337 use crate::svg::SvgCommand;
5338
5339 let mut commands = Vec::new();
5340 let mut cur_x = 0.0_f64;
5341 let mut cur_y = 0.0_f64;
5342
5343 for op in operations {
5344 match op {
5345 CanvasOp::MoveTo { x, y } => {
5346 commands.push(SvgCommand::MoveTo(*x, *y));
5347 cur_x = *x;
5348 cur_y = *y;
5349 }
5350 CanvasOp::LineTo { x, y } => {
5351 commands.push(SvgCommand::LineTo(*x, *y));
5352 cur_x = *x;
5353 cur_y = *y;
5354 }
5355 CanvasOp::BezierCurveTo {
5356 cp1x,
5357 cp1y,
5358 cp2x,
5359 cp2y,
5360 x,
5361 y,
5362 } => {
5363 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
5364 cur_x = *x;
5365 cur_y = *y;
5366 }
5367 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
5368 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
5370 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
5371 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
5372 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
5373 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
5374 cur_x = *x;
5375 cur_y = *y;
5376 }
5377 CanvasOp::ClosePath => {
5378 commands.push(SvgCommand::ClosePath);
5379 }
5380 CanvasOp::Rect {
5381 x,
5382 y,
5383 width,
5384 height,
5385 } => {
5386 commands.push(SvgCommand::MoveTo(*x, *y));
5387 commands.push(SvgCommand::LineTo(*x + *width, *y));
5388 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
5389 commands.push(SvgCommand::LineTo(*x, *y + *height));
5390 commands.push(SvgCommand::ClosePath);
5391 cur_x = *x;
5392 cur_y = *y;
5393 }
5394 CanvasOp::Circle { cx, cy, r } => {
5395 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
5396 }
5397 CanvasOp::Ellipse { cx, cy, rx, ry } => {
5398 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
5399 }
5400 CanvasOp::Arc {
5401 cx,
5402 cy,
5403 r,
5404 start_angle,
5405 end_angle,
5406 counterclockwise,
5407 } => {
5408 let steps = 32;
5412 let mut sweep = end_angle - start_angle;
5413 if !counterclockwise && sweep < 0.0 {
5414 sweep += 2.0 * std::f64::consts::PI;
5415 }
5416 if *counterclockwise && sweep > 0.0 {
5417 sweep -= 2.0 * std::f64::consts::PI;
5418 }
5419 for i in 0..=steps {
5420 let t = *start_angle + sweep * (i as f64 / steps as f64);
5421 let px = cx + r * t.cos();
5422 let py = cy + r * t.sin();
5423 if i == 0 {
5424 commands.push(SvgCommand::MoveTo(px, py));
5425 } else {
5426 commands.push(SvgCommand::LineTo(px, py));
5427 }
5428 }
5429 }
5430 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
5431 CanvasOp::Fill => commands.push(SvgCommand::Fill),
5432 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
5433 CanvasOp::SetFillColor { r, g, b } => {
5434 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
5436 }
5437 CanvasOp::SetStrokeColor { r, g, b } => {
5438 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
5439 }
5440 CanvasOp::SetLineWidth { width } => {
5441 commands.push(SvgCommand::SetStrokeWidth(*width));
5442 }
5443 CanvasOp::SetLineCap { cap } => {
5444 commands.push(SvgCommand::SetLineCap(*cap));
5445 }
5446 CanvasOp::SetLineJoin { join } => {
5447 commands.push(SvgCommand::SetLineJoin(*join));
5448 }
5449 CanvasOp::Save => commands.push(SvgCommand::SaveState),
5450 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
5451 }
5452 }
5453
5454 commands
5455 }
5456
5457 #[allow(clippy::too_many_arguments)]
5459 fn layout_canvas(
5460 &self,
5461 node: &Node,
5462 style: &ResolvedStyle,
5463 cursor: &mut PageCursor,
5464 pages: &mut Vec<LayoutPage>,
5465 x: f64,
5466 _available_width: f64,
5467 canvas_width: f64,
5468 canvas_height: f64,
5469 operations: &[CanvasOp],
5470 ) {
5471 let margin = style.margin.to_edges();
5472 let total_height = canvas_height + margin.top + margin.bottom;
5473
5474 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5476 pages.push(cursor.finalize());
5477 *cursor = cursor.new_page();
5478 }
5479
5480 cursor.y += margin.top;
5481
5482 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5483
5484 cursor.elements.push(LayoutElement {
5485 x: x + margin.left,
5486 y: cursor.content_y + cursor.y,
5487 width: canvas_width,
5488 height: canvas_height,
5489 draw: DrawCommand::Svg {
5490 commands: svg_commands,
5491 width: canvas_width,
5492 height: canvas_height,
5493 viewbox_min_x: 0.0,
5496 viewbox_min_y: 0.0,
5497 viewbox_width: canvas_width,
5498 viewbox_height: canvas_height,
5499 clip: true,
5500 },
5501 children: vec![],
5502 node_type: Some("Canvas".to_string()),
5503 resolved_style: Some(style.clone()),
5504 source_location: node.source_location.clone(),
5505 href: node.href.clone(),
5506 bookmark: node.bookmark.clone(),
5507 alt: node.alt.clone(),
5508 is_header_row: false,
5509 col_span: 1,
5510 overflow: style.overflow,
5511 opacity: style.opacity,
5512 });
5513
5514 cursor.y += canvas_height + margin.bottom;
5515 }
5516
5517 #[allow(clippy::too_many_arguments)]
5519 #[allow(clippy::too_many_arguments)]
5521 fn layout_chart(
5522 &self,
5523 node: &Node,
5524 style: &ResolvedStyle,
5525 cursor: &mut PageCursor,
5526 pages: &mut Vec<LayoutPage>,
5527 x: f64,
5528 chart_width: f64,
5529 chart_height: f64,
5530 primitives: Vec<crate::chart::ChartPrimitive>,
5531 node_type_name: &str,
5532 ) {
5533 let margin = &style.margin.to_edges();
5534 let total_height = chart_height + margin.vertical();
5535
5536 if total_height > cursor.remaining_height() {
5537 pages.push(cursor.finalize());
5538 *cursor = cursor.new_page();
5539 }
5540
5541 cursor.y += margin.top;
5542
5543 let draw = DrawCommand::Chart { primitives };
5544
5545 cursor.elements.push(LayoutElement {
5546 x: x + margin.left,
5547 y: cursor.content_y + cursor.y,
5548 width: chart_width,
5549 height: chart_height,
5550 draw,
5551 children: vec![],
5552 node_type: Some(node_type_name.to_string()),
5553 resolved_style: Some(style.clone()),
5554 source_location: node.source_location.clone(),
5555 href: node.href.clone(),
5556 bookmark: node.bookmark.clone(),
5557 alt: node.alt.clone(),
5558 is_header_row: false,
5559 col_span: 1,
5560 overflow: style.overflow,
5561 opacity: style.opacity,
5562 });
5563
5564 cursor.y += chart_height + margin.bottom;
5565 }
5566
5567 #[allow(clippy::too_many_arguments)]
5569 fn layout_form_field(
5570 &self,
5571 node: &Node,
5572 style: &ResolvedStyle,
5573 cursor: &mut PageCursor,
5574 pages: &mut Vec<LayoutPage>,
5575 x: f64,
5576 field_width: f64,
5577 field_height: f64,
5578 draw: DrawCommand,
5579 node_type_name: &str,
5580 ) {
5581 let margin = &style.margin.to_edges();
5582 let total_height = field_height + margin.vertical();
5583
5584 if total_height > cursor.remaining_height() {
5585 pages.push(cursor.finalize());
5586 *cursor = cursor.new_page();
5587 }
5588
5589 cursor.y += margin.top;
5590
5591 cursor.elements.push(LayoutElement {
5592 x: x + margin.left,
5593 y: cursor.content_y + cursor.y,
5594 width: field_width,
5595 height: field_height,
5596 draw,
5597 children: vec![],
5598 node_type: Some(node_type_name.to_string()),
5599 resolved_style: Some(style.clone()),
5600 source_location: node.source_location.clone(),
5601 href: node.href.clone(),
5602 bookmark: node.bookmark.clone(),
5603 alt: node.alt.clone(),
5604 is_header_row: false,
5605 col_span: 1,
5606 overflow: style.overflow,
5607 opacity: style.opacity,
5608 });
5609
5610 cursor.y += field_height + margin.bottom;
5611 }
5612
5613 #[allow(clippy::too_many_arguments)]
5614 fn layout_barcode(
5615 &self,
5616 node: &Node,
5617 style: &ResolvedStyle,
5618 cursor: &mut PageCursor,
5619 pages: &mut Vec<LayoutPage>,
5620 x: f64,
5621 available_width: f64,
5622 data: &str,
5623 format: crate::barcode::BarcodeFormat,
5624 explicit_width: Option<f64>,
5625 bar_height: f64,
5626 ) {
5627 let margin = &style.margin.to_edges();
5628 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5629 let total_height = bar_height + margin.vertical();
5630
5631 if total_height > cursor.remaining_height() {
5632 pages.push(cursor.finalize());
5633 *cursor = cursor.new_page();
5634 }
5635
5636 cursor.y += margin.top;
5637
5638 let draw = match crate::barcode::generate_barcode(data, format) {
5639 Ok(barcode_data) => {
5640 let bar_width = if barcode_data.bars.is_empty() {
5641 0.0
5642 } else {
5643 display_width / barcode_data.bars.len() as f64
5644 };
5645 DrawCommand::Barcode {
5646 bars: barcode_data.bars,
5647 bar_width,
5648 height: bar_height,
5649 color: style.color,
5650 }
5651 }
5652 Err(_) => DrawCommand::None,
5653 };
5654
5655 cursor.elements.push(LayoutElement {
5656 x: x + margin.left,
5657 y: cursor.content_y + cursor.y,
5658 width: display_width,
5659 height: bar_height,
5660 draw,
5661 children: vec![],
5662 node_type: Some("Barcode".to_string()),
5663 resolved_style: Some(style.clone()),
5664 source_location: node.source_location.clone(),
5665 href: node.href.clone(),
5666 bookmark: node.bookmark.clone(),
5667 alt: node.alt.clone(),
5668 is_header_row: false,
5669 col_span: 1,
5670 overflow: style.overflow,
5671 opacity: style.opacity,
5672 });
5673
5674 cursor.y += bar_height + margin.bottom;
5675 }
5676
5677 #[allow(clippy::too_many_arguments)]
5679 fn layout_qrcode(
5680 &self,
5681 node: &Node,
5682 style: &ResolvedStyle,
5683 cursor: &mut PageCursor,
5684 pages: &mut Vec<LayoutPage>,
5685 x: f64,
5686 available_width: f64,
5687 data: &str,
5688 explicit_size: Option<f64>,
5689 ) {
5690 let margin = &style.margin.to_edges();
5691 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
5692 let total_height = display_size + margin.vertical();
5693
5694 if total_height > cursor.remaining_height() {
5695 pages.push(cursor.finalize());
5696 *cursor = cursor.new_page();
5697 }
5698
5699 cursor.y += margin.top;
5700
5701 let draw = match crate::qrcode::generate_qr(data) {
5702 Ok(matrix) => {
5703 let module_size = display_size / matrix.size as f64;
5704 DrawCommand::QrCode {
5705 modules: matrix.modules,
5706 module_size,
5707 color: style.color,
5708 }
5709 }
5710 Err(_) => DrawCommand::None,
5711 };
5712
5713 cursor.elements.push(LayoutElement {
5714 x: x + margin.left,
5715 y: cursor.content_y + cursor.y,
5716 width: display_size,
5717 height: display_size,
5718 draw,
5719 children: vec![],
5720 node_type: Some("QrCode".to_string()),
5721 resolved_style: Some(style.clone()),
5722 source_location: node.source_location.clone(),
5723 href: node.href.clone(),
5724 bookmark: node.bookmark.clone(),
5725 alt: node.alt.clone(),
5726 is_header_row: false,
5727 col_span: 1,
5728 overflow: style.overflow,
5729 opacity: style.opacity,
5730 });
5731
5732 cursor.y += display_size + margin.bottom;
5733 }
5734
5735 fn measure_node_height(
5738 &self,
5739 node: &Node,
5740 available_width: f64,
5741 style: &ResolvedStyle,
5742 font_context: &FontContext,
5743 ) -> f64 {
5744 match &node.kind {
5745 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5750 let measure_width = match style.width {
5753 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
5754 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5755 };
5756 if !runs.is_empty() {
5763 let mut styled_chars: Vec<StyledChar> = Vec::new();
5764 for run in runs {
5765 let run_style = run.style.resolve(Some(style), measure_width);
5766 let transform = run_style.text_transform;
5767 let run_content = substitute_page_placeholders(&run.content);
5768 let mut prev_is_whitespace = true;
5769 for ch in run_content.chars() {
5770 let transformed_ch =
5771 apply_char_transform(ch, transform, prev_is_whitespace);
5772 prev_is_whitespace = ch.is_whitespace();
5773 styled_chars.push(StyledChar {
5774 ch: transformed_ch,
5775 font_family: run_style.font_family.clone(),
5776 font_size: run_style.font_size,
5777 font_weight: run_style.font_weight,
5778 font_style: run_style.font_style,
5779 color: run_style.color,
5780 href: None,
5781 text_decoration: run_style.text_decoration,
5782 letter_spacing: run_style.letter_spacing,
5783 });
5784 }
5785 }
5786 let justify = matches!(style.text_align, TextAlign::Justify);
5787 let broken_lines = match style.line_breaking {
5788 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
5789 font_context,
5790 &styled_chars,
5791 measure_width,
5792 style.hyphens,
5793 style.lang.as_deref(),
5794 justify,
5795 ),
5796 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
5797 font_context,
5798 &styled_chars,
5799 measure_width,
5800 style.hyphens,
5801 style.lang.as_deref(),
5802 ),
5803 };
5804 let line_height = style.font_size * style.line_height;
5805 (broken_lines.len() as f64) * line_height + style.padding.vertical()
5806 } else {
5807 let content = substitute_page_placeholders(content);
5808 let transformed = apply_text_transform(&content, style.text_transform);
5809 let justify = matches!(style.text_align, TextAlign::Justify);
5810 let lines = match style.line_breaking {
5811 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
5812 font_context,
5813 &transformed,
5814 measure_width,
5815 style.font_size,
5816 &style.font_family,
5817 style.font_weight,
5818 style.font_style,
5819 style.letter_spacing,
5820 style.hyphens,
5821 style.lang.as_deref(),
5822 justify,
5823 ),
5824 LineBreaking::Greedy => self.text_layout.break_into_lines(
5825 font_context,
5826 &transformed,
5827 measure_width,
5828 style.font_size,
5829 &style.font_family,
5830 style.font_weight,
5831 style.font_style,
5832 style.letter_spacing,
5833 style.hyphens,
5834 style.lang.as_deref(),
5835 ),
5836 };
5837 let line_height = style.font_size * style.line_height;
5838 (lines.len() as f64) * line_height + style.padding.vertical()
5839 }
5840 }
5841 NodeKind::Image {
5842 src,
5843 width: explicit_w,
5844 height: explicit_h,
5845 } => {
5846 let (_, h) = self.image_display_size(
5848 src,
5849 style,
5850 *explicit_w,
5851 *explicit_h,
5852 available_width - style.margin.horizontal(),
5853 );
5854 h + style.padding.vertical()
5855 }
5856 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
5857 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
5858 NodeKind::QrCode { size, .. } => {
5859 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
5860 display_size + style.margin.vertical()
5861 }
5862 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
5863 NodeKind::BarChart { height, .. }
5864 | NodeKind::LineChart { height, .. }
5865 | NodeKind::PieChart { height, .. }
5866 | NodeKind::AreaChart { height, .. }
5867 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
5868 NodeKind::TextField { height, .. }
5869 | NodeKind::Checkbox { height, .. }
5870 | NodeKind::Dropdown { height, .. }
5871 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
5872 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
5874 if let SizeConstraint::Fixed(h) = style.height {
5881 return h;
5882 }
5883 let outer_width = match style.width {
5884 SizeConstraint::Fixed(w) => w,
5885 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5886 };
5887 let inner_width =
5888 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5889 let col_widths = self.resolve_column_widths(
5890 columns,
5891 inner_width,
5892 &node.children,
5893 style,
5894 font_context,
5895 );
5896 let row_gap = style.row_gap;
5897 let mut total = 0.0;
5898 for (i, row) in node.children.iter().enumerate() {
5899 if i > 0 {
5900 total += row_gap;
5901 }
5902 total += self.measure_table_row_height(row, &col_widths, style, font_context);
5903 }
5904 total + style.padding.vertical() + style.border_width.vertical()
5905 }
5906 NodeKind::TableRow { .. } => {
5907 let n = node.children.len().max(1);
5912 let usable = (available_width - style.margin.horizontal()).max(0.0);
5913 let col_w = usable / n as f64;
5914 let col_widths = vec![col_w; n];
5915 self.measure_table_row_height(node, &col_widths, style, font_context)
5916 }
5917 _ => {
5918 if let SizeConstraint::Fixed(h) = style.height {
5920 return h;
5921 }
5922 let outer_width = match style.width {
5926 SizeConstraint::Fixed(w) => w,
5927 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5928 }
5929 .min(style.max_width)
5930 .max(style.min_width);
5931 let inner_width =
5932 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5933 let children_height =
5934 self.measure_children_height(&node.children, inner_width, style, font_context);
5935 (children_height + style.padding.vertical() + style.border_width.vertical())
5936 .max(style.min_height)
5937 }
5938 }
5939 }
5940
5941 fn measure_children_height(
5942 &self,
5943 children: &[Node],
5944 available_width: f64,
5945 parent_style: &ResolvedStyle,
5946 font_context: &FontContext,
5947 ) -> f64 {
5948 if children
5954 .iter()
5955 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
5956 {
5957 let flow: Vec<Node> = children
5958 .iter()
5959 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
5960 .cloned()
5961 .collect();
5962 return self.measure_children_height(
5963 &flow,
5964 available_width,
5965 parent_style,
5966 font_context,
5967 );
5968 }
5969 if matches!(parent_style.display, Display::Grid) {
5971 if let Some(template_cols) = &parent_style.grid_template_columns {
5972 let num_columns = template_cols.len();
5973 if num_columns > 0 && !children.is_empty() {
5974 let col_gap = parent_style.column_gap;
5975 let row_gap = parent_style.row_gap;
5976
5977 let content_sizes: Vec<f64> = template_cols
5978 .iter()
5979 .map(|track| {
5980 if matches!(track, GridTrackSize::Auto) {
5981 available_width / num_columns as f64
5982 } else {
5983 0.0
5984 }
5985 })
5986 .collect();
5987
5988 let col_widths = grid::resolve_tracks(
5989 template_cols,
5990 available_width,
5991 col_gap,
5992 &content_sizes,
5993 );
5994
5995 let placements: Vec<Option<&GridPlacement>> = children
5996 .iter()
5997 .map(|child| child.style.grid_placement.as_ref())
5998 .collect();
5999
6000 let item_placements = grid::place_items(&placements, num_columns);
6001 let num_rows = grid::compute_num_rows(&item_placements);
6002
6003 if num_rows == 0 {
6004 return 0.0;
6005 }
6006
6007 let mut row_heights = vec![0.0_f64; num_rows];
6008 for placement in &item_placements {
6009 let cell_width = grid::span_width(
6010 placement.col_start,
6011 placement.col_end,
6012 &col_widths,
6013 col_gap,
6014 );
6015 let child = &children[placement.child_index];
6016 let child_style = child.style.resolve(Some(parent_style), cell_width);
6017 let h =
6018 self.measure_node_height(child, cell_width, &child_style, font_context);
6019 let span = placement.row_end - placement.row_start;
6020 let per_row = h / span as f64;
6021 for rh in row_heights
6022 .iter_mut()
6023 .take(placement.row_end.min(num_rows))
6024 .skip(placement.row_start)
6025 {
6026 if per_row > *rh {
6027 *rh = per_row;
6028 }
6029 }
6030 }
6031
6032 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6033 return row_heights.iter().sum::<f64>() + total_row_gap;
6034 }
6035 }
6036 }
6037
6038 let direction = parent_style.flex_direction;
6039 let row_gap = parent_style.row_gap;
6040 let column_gap = parent_style.column_gap;
6041
6042 match direction {
6043 FlexDirection::Row | FlexDirection::RowReverse => {
6044 let styles: Vec<ResolvedStyle> = children
6047 .iter()
6048 .map(|child| child.style.resolve(Some(parent_style), available_width))
6049 .collect();
6050
6051 let base_widths: Vec<f64> = children
6052 .iter()
6053 .zip(&styles)
6054 .map(|(child, style)| match style.flex_basis {
6055 SizeConstraint::Fixed(w) => w,
6056 SizeConstraint::Auto => match style.width {
6057 SizeConstraint::Fixed(w) => w,
6058 SizeConstraint::Auto => self
6059 .measure_intrinsic_width(child, style, font_context)
6060 .min(available_width),
6061 },
6062 })
6063 .collect();
6064
6065 let lines = match parent_style.flex_wrap {
6066 FlexWrap::NoWrap => {
6067 vec![flex::WrapLine {
6068 start: 0,
6069 end: children.len(),
6070 }]
6071 }
6072 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6073 flex::partition_into_lines(&base_widths, column_gap, available_width)
6074 }
6075 };
6076
6077 let mut final_widths = base_widths.clone();
6079 for line in &lines {
6080 let line_count = line.end - line.start;
6081 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6082 let distributable = available_width - line_gap;
6083 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6084 let remaining = distributable - total_base;
6085
6086 if remaining > 0.0 {
6087 let total_grow: f64 = styles[line.start..line.end]
6088 .iter()
6089 .map(|s| s.flex_grow)
6090 .sum();
6091 if total_grow > 0.0 {
6092 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6093 final_widths[line.start + j] = base_widths[line.start + j]
6094 + remaining * (s.flex_grow / total_grow);
6095 }
6096 }
6097 } else if remaining < 0.0 {
6098 let total_shrink: f64 = styles[line.start..line.end]
6099 .iter()
6100 .enumerate()
6101 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6102 .sum();
6103 if total_shrink > 0.0 {
6104 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6105 let factor =
6106 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6107 let w = base_widths[line.start + j] + remaining * factor;
6108 final_widths[line.start + j] = w.max(s.min_width);
6109 }
6110 }
6111 }
6112 }
6113
6114 let mut total = 0.0;
6115 for (i, line) in lines.iter().enumerate() {
6116 let line_height: f64 = children[line.start..line.end]
6117 .iter()
6118 .enumerate()
6119 .map(|(j, child)| {
6120 let fw = final_widths[line.start + j];
6121 let child_style =
6130 child.style.resolve(Some(parent_style), available_width);
6131 self.measure_node_height(child, fw, &child_style, font_context)
6132 + child_style.margin.vertical()
6133 })
6134 .fold(0.0f64, f64::max);
6135 total += line_height;
6136 if i > 0 {
6137 total += row_gap;
6138 }
6139 }
6140 total
6141 }
6142 FlexDirection::Column | FlexDirection::ColumnReverse => {
6143 let mut total = 0.0;
6144 for (i, child) in children.iter().enumerate() {
6145 let child_style = child.style.resolve(Some(parent_style), available_width);
6146 let child_height = self.measure_node_height(
6147 child,
6148 available_width,
6149 &child_style,
6150 font_context,
6151 );
6152 total += child_height + child_style.margin.vertical();
6153 if i > 0 {
6154 total += row_gap;
6155 }
6156 }
6157 total
6158 }
6159 }
6160 }
6161
6162 fn measure_intrinsic_width(
6164 &self,
6165 node: &Node,
6166 style: &ResolvedStyle,
6167 font_context: &FontContext,
6168 ) -> f64 {
6169 match &node.kind {
6170 NodeKind::Svg { width, .. } => {
6171 *width + style.padding.horizontal() + style.margin.horizontal()
6172 }
6173 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6174 let text_width = if !runs.is_empty() {
6180 runs.iter()
6181 .map(|run| {
6182 let run_style = run.style.resolve(Some(style), 0.0);
6183 let run_content = substitute_page_placeholders(&run.content);
6184 let transformed =
6185 apply_text_transform(&run_content, run_style.text_transform);
6186 let italic = matches!(
6187 run_style.font_style,
6188 FontStyle::Italic | FontStyle::Oblique
6189 );
6190 transformed
6194 .split('\n')
6195 .map(|segment| {
6196 font_context.measure_string(
6197 segment,
6198 &run_style.font_family,
6199 run_style.font_weight,
6200 italic,
6201 run_style.font_size,
6202 run_style.letter_spacing,
6203 )
6204 })
6205 .fold(0.0f64, f64::max)
6206 })
6207 .sum()
6208 } else {
6209 let content = substitute_page_placeholders(content);
6210 let transformed = apply_text_transform(&content, style.text_transform);
6211 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6212 transformed
6213 .split('\n')
6214 .map(|segment| {
6215 font_context.measure_string(
6216 segment,
6217 &style.font_family,
6218 style.font_weight,
6219 italic,
6220 style.font_size,
6221 style.letter_spacing,
6222 )
6223 })
6224 .fold(0.0f64, f64::max)
6225 };
6226 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6229 }
6230 NodeKind::Image {
6231 src, width, height, ..
6232 } => {
6233 let w = if let SizeConstraint::Fixed(w) = style.width {
6234 w
6235 } else if let Some(w) = width {
6236 *w
6237 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6238 let pixel_w = iw as f64;
6239 let pixel_h = ih as f64;
6240 let aspect = if pixel_w > 0.0 {
6241 pixel_h / pixel_w
6242 } else {
6243 0.75
6244 };
6245 let constrained_h = match style.height {
6247 SizeConstraint::Fixed(h) => Some(h),
6248 SizeConstraint::Auto => *height,
6249 };
6250 if let Some(h) = constrained_h {
6251 h / aspect
6252 } else {
6253 pixel_w
6254 }
6255 } else {
6256 100.0
6257 };
6258 w + style.padding.horizontal() + style.margin.horizontal()
6259 }
6260 NodeKind::Barcode { width, .. } => {
6261 let w = width.unwrap_or(0.0);
6262 w + style.padding.horizontal() + style.margin.horizontal()
6263 }
6264 NodeKind::QrCode { size, .. } => {
6265 let display_size = size.unwrap_or(0.0);
6266 display_size + style.padding.horizontal() + style.margin.horizontal()
6267 }
6268 NodeKind::Canvas { width, .. } => {
6269 *width + style.padding.horizontal() + style.margin.horizontal()
6270 }
6271 NodeKind::BarChart { width, .. }
6272 | NodeKind::LineChart { width, .. }
6273 | NodeKind::PieChart { width, .. }
6274 | NodeKind::AreaChart { width, .. }
6275 | NodeKind::DotPlot { width, .. } => {
6276 *width + style.padding.horizontal() + style.margin.horizontal()
6277 }
6278 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6279 *width + style.padding.horizontal() + style.margin.horizontal()
6280 }
6281 NodeKind::Table { columns } => {
6282 let num_cols = node
6287 .children
6288 .iter()
6289 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6290 .max()
6291 .unwrap_or(1)
6292 .max(columns.len().max(1));
6293 let (_, col_max) =
6294 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6295 col_max.iter().sum::<f64>()
6296 + style.padding.horizontal()
6297 + style.margin.horizontal()
6298 + style.border_width.horizontal()
6299 }
6300 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
6301 *width + style.padding.horizontal() + style.margin.horizontal()
6302 }
6303 NodeKind::Watermark { .. } => 0.0, _ => {
6305 if node.children.is_empty() {
6307 style.padding.horizontal() + style.margin.horizontal()
6308 } else {
6309 let direction = style.flex_direction;
6310 let gap = style.gap;
6311 let mut total = 0.0f64;
6312 for (i, child) in node.children.iter().enumerate() {
6313 let child_style = child.style.resolve(Some(style), 0.0);
6314 let child_width =
6315 self.measure_intrinsic_width(child, &child_style, font_context);
6316 match direction {
6317 FlexDirection::Row | FlexDirection::RowReverse => {
6318 total += child_width;
6319 if i > 0 {
6320 total += gap;
6321 }
6322 }
6323 _ => {
6324 total = total.max(child_width);
6325 }
6326 }
6327 }
6328 total
6329 + style.padding.horizontal()
6330 + style.margin.horizontal()
6331 + style.border_width.horizontal()
6332 }
6333 }
6334 }
6335 }
6336
6337 pub fn measure_min_content_width(
6341 &self,
6342 node: &Node,
6343 style: &ResolvedStyle,
6344 font_context: &FontContext,
6345 ) -> f64 {
6346 match &node.kind {
6347 NodeKind::Table { columns } => {
6348 let num_cols = node
6351 .children
6352 .iter()
6353 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6354 .max()
6355 .unwrap_or(1)
6356 .max(columns.len().max(1));
6357 let (col_min, _) =
6358 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
6359 col_min.iter().sum::<f64>()
6360 + style.padding.horizontal()
6361 + style.margin.horizontal()
6362 + style.border_width.horizontal()
6363 }
6364 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6365 let word_width = if !runs.is_empty() {
6366 runs.iter()
6368 .map(|run| {
6369 let run_style = run.style.resolve(Some(style), 0.0);
6370 let run_content = substitute_page_placeholders(&run.content);
6371 let transformed =
6372 apply_text_transform(&run_content, run_style.text_transform);
6373 self.text_layout.measure_widest_word(
6374 font_context,
6375 &transformed,
6376 run_style.font_size,
6377 &run_style.font_family,
6378 run_style.font_weight,
6379 run_style.font_style,
6380 run_style.letter_spacing,
6381 style.hyphens,
6382 style.lang.as_deref(),
6383 )
6384 })
6385 .fold(0.0f64, f64::max)
6386 } else {
6387 let content = substitute_page_placeholders(content);
6388 let transformed = apply_text_transform(&content, style.text_transform);
6389 self.text_layout.measure_widest_word(
6390 font_context,
6391 &transformed,
6392 style.font_size,
6393 &style.font_family,
6394 style.font_weight,
6395 style.font_style,
6396 style.letter_spacing,
6397 style.hyphens,
6398 style.lang.as_deref(),
6399 )
6400 };
6401 word_width + style.padding.horizontal() + style.margin.horizontal()
6402 }
6403 NodeKind::Image { width, .. } => {
6404 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
6405 }
6406 NodeKind::Svg { width, .. } => {
6407 *width + style.padding.horizontal() + style.margin.horizontal()
6408 }
6409 _ => {
6410 if node.children.is_empty() {
6411 style.padding.horizontal()
6412 + style.margin.horizontal()
6413 + style.border_width.horizontal()
6414 } else {
6415 let mut max_child_min = 0.0f64;
6416 for child in &node.children {
6417 let child_style = child.style.resolve(Some(style), 0.0);
6418 let child_min =
6419 self.measure_min_content_width(child, &child_style, font_context);
6420 max_child_min = max_child_min.max(child_min);
6421 }
6422 max_child_min
6423 + style.padding.horizontal()
6424 + style.margin.horizontal()
6425 + style.border_width.horizontal()
6426 }
6427 }
6428 }
6429 }
6430
6431 fn cell_first_line_font_size(&self, cell: &Node, cell_style: &ResolvedStyle, w: f64) -> f64 {
6437 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<f64> {
6438 for ch in &node.children {
6439 let s = ch.style.resolve(Some(parent), w);
6440 match &ch.kind {
6441 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s.font_size),
6442 _ => {
6443 if let Some(f) = first(ch, &s, w) {
6444 return Some(f);
6445 }
6446 }
6447 }
6448 }
6449 None
6450 }
6451 first(cell, cell_style, w).unwrap_or(cell_style.font_size)
6452 }
6453
6454 fn cell_baseline_distance(
6457 &self,
6458 cell: &Node,
6459 cell_style: &ResolvedStyle,
6460 inner_width: f64,
6461 ) -> f64 {
6462 cell_style.padding.top
6463 + cell_style.border_width.top
6464 + self.cell_first_line_font_size(cell, cell_style, inner_width)
6465 }
6466
6467 fn row_baseline(
6470 &self,
6471 row: &Node,
6472 row_style: &ResolvedStyle,
6473 col_widths: &[f64],
6474 ) -> Option<f64> {
6475 let mut b: Option<f64> = None;
6476 let mut col_idx = 0usize;
6477 for cell in row.children.iter() {
6478 let span = match &cell.kind {
6479 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6480 _ => 1,
6481 };
6482 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
6483 col_idx += span;
6484 let cell_style = cell.style.resolve(Some(row_style), col_width);
6485 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6486 let iw = col_width
6487 - cell_style.padding.horizontal()
6488 - cell_style.border_width.horizontal();
6489 let d = self.cell_baseline_distance(cell, &cell_style, iw);
6490 b = Some(b.map_or(d, |m: f64| m.max(d)));
6491 }
6492 }
6493 b
6494 }
6495
6496 fn measure_table_row_height(
6497 &self,
6498 row: &Node,
6499 col_widths: &[f64],
6500 parent_style: &ResolvedStyle,
6501 font_context: &FontContext,
6502 ) -> f64 {
6503 let row_style = row
6504 .style
6505 .resolve(Some(parent_style), col_widths.iter().sum());
6506 let mut max_height: f64 = 0.0;
6507 let row_bl = self.row_baseline(row, &row_style, col_widths);
6510
6511 let mut col_idx = 0usize;
6512 for cell in row.children.iter() {
6513 let span = match &cell.kind {
6514 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6515 _ => 1,
6516 };
6517 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
6518 col_idx += span;
6519 let cell_style = cell.style.resolve(Some(&row_style), col_width);
6520 let inner_width =
6521 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
6522
6523 let mut cell_content_height = 0.0;
6524 for child in &cell.children {
6525 let child_style = child.style.resolve(Some(&cell_style), inner_width);
6526 cell_content_height +=
6527 self.measure_node_height(child, inner_width, &child_style, font_context);
6528 }
6529
6530 let mut total = cell_content_height
6531 + cell_style.padding.vertical()
6532 + cell_style.border_width.vertical();
6533 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6537 if let Some(b) = row_bl {
6538 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
6539 total += (b - d).max(0.0);
6540 }
6541 }
6542 if let SizeConstraint::Fixed(h) = cell_style.height {
6549 total = total.max(h);
6550 }
6551 max_height = max_height.max(total);
6552 }
6553
6554 max_height.max(row_style.min_height)
6555 }
6556
6557 fn cell_col_span(cell: &Node) -> usize {
6559 match &cell.kind {
6560 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6561 _ => 1,
6562 }
6563 }
6564
6565 fn measure_column_content(
6570 &self,
6571 children: &[Node],
6572 num_cols: usize,
6573 available_width: f64,
6574 table_style: &ResolvedStyle,
6575 font_context: &FontContext,
6576 ) -> (Vec<f64>, Vec<f64>) {
6577 let mut col_min = vec![0.0f64; num_cols];
6578 let mut col_max = vec![0.0f64; num_cols];
6579 for row_node in children {
6580 let mut col = 0usize;
6581 for cell in &row_node.children {
6582 let span = Self::cell_col_span(cell);
6583 let cell_style = cell.style.resolve(Some(table_style), available_width);
6584 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
6585 let mut cmin = 0.0f64;
6586 let mut cmax = 0.0f64;
6587 for child in &cell.children {
6588 let child_style = child.style.resolve(Some(&cell_style), 0.0);
6589 cmin =
6590 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
6591 cmax =
6592 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
6593 }
6594 cmin += chrome;
6595 let mut cmax = cmax.max(cmin) + chrome;
6596 if let SizeConstraint::Fixed(w) = cell_style.width {
6597 cmax = w.max(cmin);
6598 }
6599 let per_min = cmin / span as f64;
6600 let per_max = cmax / span as f64;
6601 for k in col..(col + span).min(num_cols) {
6602 col_min[k] = col_min[k].max(per_min);
6603 col_max[k] = col_max[k].max(per_max);
6604 }
6605 col += span;
6606 }
6607 }
6608 (col_min, col_max)
6609 }
6610
6611 fn resolve_column_widths(
6621 &self,
6622 defs: &[ColumnDef],
6623 available_width: f64,
6624 children: &[Node],
6625 table_style: &ResolvedStyle,
6626 font_context: &FontContext,
6627 ) -> Vec<f64> {
6628 if defs.is_empty() {
6629 let num_cols = children
6630 .iter()
6631 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6632 .max()
6633 .unwrap_or(1)
6634 .max(1);
6635
6636 let (col_min, col_max) = self.measure_column_content(
6637 children,
6638 num_cols,
6639 available_width,
6640 table_style,
6641 font_context,
6642 );
6643
6644 let sum_min: f64 = col_min.iter().sum();
6645 let sum_max: f64 = col_max.iter().sum();
6646 let w = available_width;
6647 return if sum_max <= w {
6648 if sum_max <= f64::EPSILON {
6652 vec![w / num_cols as f64; num_cols]
6653 } else {
6654 col_max
6655 .iter()
6656 .map(|m| m + (w - sum_max) * (m / sum_max))
6657 .collect()
6658 }
6659 } else if sum_min <= w {
6660 let denom = (sum_max - sum_min).max(f64::EPSILON);
6663 col_min
6664 .iter()
6665 .zip(&col_max)
6666 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
6667 .collect()
6668 } else {
6669 self.defect(format!(
6672 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
6673 sum_min, w
6674 ));
6675 let scale = w / sum_min.max(f64::EPSILON);
6676 col_min.iter().map(|m| m * scale).collect()
6677 };
6678 }
6679
6680 let num_cols = children
6686 .iter()
6687 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6688 .max()
6689 .unwrap_or(defs.len())
6690 .max(defs.len());
6691 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
6692 while defs_vec.len() < num_cols {
6693 defs_vec.push(ColumnDef {
6694 width: ColumnWidth::Auto,
6695 });
6696 }
6697 let defs = &defs_vec[..];
6698
6699 let mut widths = Vec::new();
6700 let mut remaining = available_width;
6701 let mut auto_count = 0;
6702
6703 for def in defs {
6704 match def.width {
6705 ColumnWidth::Fixed(w) => {
6706 widths.push(w);
6707 remaining -= w;
6708 }
6709 ColumnWidth::Fraction(f) => {
6710 let w = available_width * f;
6711 widths.push(w);
6712 remaining -= w;
6713 }
6714 ColumnWidth::Auto => {
6715 widths.push(0.0);
6716 auto_count += 1;
6717 }
6718 }
6719 }
6720
6721 if remaining < 0.0 {
6722 self.defect(format!(
6725 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
6726 available_width - remaining,
6727 available_width
6728 ));
6729 remaining = 0.0;
6730 }
6731
6732 if auto_count > 0 {
6733 let auto_width = remaining / auto_count as f64;
6734 for (i, def) in defs.iter().enumerate() {
6735 if matches!(def.width, ColumnWidth::Auto) {
6736 widths[i] = auto_width;
6737 }
6738 }
6739 }
6740
6741 let (col_min, _) = self.measure_column_content(
6748 children,
6749 widths.len(),
6750 available_width,
6751 table_style,
6752 font_context,
6753 );
6754 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
6755 if needs_floor {
6756 let sum_min: f64 = col_min.iter().sum();
6757 if sum_min > available_width {
6758 self.defect(format!(
6759 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
6760 sum_min, available_width
6761 ));
6762 let scale = available_width / sum_min.max(f64::EPSILON);
6763 return col_min.iter().map(|m| m * scale).collect();
6764 }
6765 let deficit: f64 = widths
6766 .iter()
6767 .zip(&col_min)
6768 .map(|(w, m)| (m - w).max(0.0))
6769 .sum();
6770 let surplus: f64 = widths
6771 .iter()
6772 .zip(&col_min)
6773 .map(|(w, m)| (w - m).max(0.0))
6774 .sum();
6775 let take = if surplus > 0.0 {
6776 deficit / surplus
6777 } else {
6778 0.0
6779 };
6780 widths = widths
6781 .iter()
6782 .zip(&col_min)
6783 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
6784 .collect();
6785 }
6786
6787 widths
6788 }
6789
6790 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
6791 for (page_index, page) in pages.iter_mut().enumerate() {
6792 if !page.watermarks.is_empty() {
6794 let (page_w, page_h) = page.config.size.dimensions();
6795 let cx = page_w / 2.0;
6796 let cy = page_h / 2.0;
6797
6798 let mut watermark_elements = Vec::new();
6799 for wm_node in &page.watermarks {
6800 if let NodeKind::Watermark {
6801 text,
6802 font_size,
6803 angle,
6804 } = &wm_node.kind
6805 {
6806 let style = wm_node.style.resolve(None, page_w);
6807 let color = style.color;
6808 let opacity = style.opacity;
6809 let angle_rad = angle.to_radians();
6810
6811 let italic =
6813 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6814
6815 let shaped = self.text_layout.shape_text(
6817 font_context,
6818 text,
6819 &style.font_family,
6820 style.font_weight,
6821 style.font_style,
6822 );
6823
6824 let mut glyphs = Vec::new();
6825 let mut x_pos = 0.0;
6826 let text_chars: Vec<char> = text.chars().collect();
6827
6828 if let Some(shaped_glyphs) = shaped {
6829 let units_per_em = font_context.units_per_em(
6831 &style.font_family,
6832 style.font_weight,
6833 italic,
6834 ) as f64;
6835
6836 for sg in &shaped_glyphs {
6837 let advance = sg.x_advance as f64 / units_per_em * *font_size;
6838 let cluster_idx = sg.cluster as usize;
6839 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
6840 glyphs.push(PositionedGlyph {
6841 glyph_id: sg.glyph_id,
6842 char_value: ch,
6843 x_offset: x_pos,
6844 y_offset: 0.0,
6845 x_advance: advance,
6846 font_size: *font_size,
6847 font_family: Arc::from(style.font_family.as_str()),
6848 font_weight: style.font_weight,
6849 font_style: style.font_style,
6850 color: Some(color),
6851 href: None,
6852 text_decoration: TextDecoration::None,
6853 letter_spacing: style.letter_spacing,
6854 cluster_text: None,
6855 });
6856 x_pos += advance + style.letter_spacing;
6857 }
6858 } else {
6859 for &ch in &text_chars {
6861 let w = font_context.char_width(
6862 ch,
6863 &style.font_family,
6864 style.font_weight,
6865 italic,
6866 *font_size,
6867 );
6868 glyphs.push(PositionedGlyph {
6869 glyph_id: ch as u16,
6870 char_value: ch,
6871 x_offset: x_pos,
6872 y_offset: 0.0,
6873 x_advance: w,
6874 font_size: *font_size,
6875 font_family: Arc::from(style.font_family.as_str()),
6876 font_weight: style.font_weight,
6877 font_style: style.font_style,
6878 color: Some(color),
6879 href: None,
6880 text_decoration: TextDecoration::None,
6881 letter_spacing: style.letter_spacing,
6882 cluster_text: None,
6883 });
6884 x_pos += w + style.letter_spacing;
6885 }
6886 }
6887
6888 let text_width = x_pos;
6889
6890 let line = TextLine {
6891 x: 0.0,
6892 y: 0.0,
6893 glyphs,
6894 width: text_width,
6895 height: *font_size,
6896 word_spacing: 0.0,
6897 };
6898
6899 watermark_elements.push(LayoutElement {
6900 x: cx,
6901 y: cy,
6902 width: text_width,
6903 height: *font_size,
6904 draw: DrawCommand::Watermark {
6905 lines: vec![line],
6906 color,
6907 opacity,
6908 angle_rad,
6909 font_family: style.font_family.clone(),
6910 },
6911 children: vec![],
6912 node_type: Some("Watermark".to_string()),
6913 resolved_style: None,
6914 source_location: None,
6915 href: None,
6916 bookmark: None,
6917 alt: None,
6918 is_header_row: false,
6919 col_span: 1,
6920 overflow: Overflow::default(),
6921 opacity: 1.0,
6922 });
6923 }
6924 }
6925
6926 watermark_elements.append(&mut page.elements);
6928 page.elements = watermark_elements;
6929 page.watermarks.clear();
6930 }
6931
6932 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
6933 continue;
6934 }
6935
6936 if !page.fixed_header.is_empty() {
6938 let mut hdr_cursor = PageCursor::new(&page.config);
6939 for (node, _h) in &page.fixed_header {
6940 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
6943 continue;
6944 }
6945 let cw = hdr_cursor.content_width;
6946 let cx = hdr_cursor.content_x;
6947 let style = node.style.resolve(None, cw);
6948 self.layout_view(
6949 node,
6950 &style,
6951 &mut hdr_cursor,
6952 &mut Vec::new(),
6953 cx,
6954 cw,
6955 font_context,
6956 );
6957 }
6958 let mut combined = hdr_cursor.elements;
6960 combined.append(&mut page.elements);
6961 page.elements = combined;
6962 }
6963
6964 if !page.fixed_footer.is_empty() {
6969 let mut ftr_cursor = PageCursor::new(&page.config);
6970 let total_ftr: f64 = page
6971 .fixed_footer
6972 .iter()
6973 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
6974 .map(|(_, h)| *h)
6975 .sum();
6976 let target_y = ftr_cursor.content_height - total_ftr;
6977 for (node, _h) in &page.fixed_footer {
6979 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
6980 continue;
6981 }
6982 let cw = ftr_cursor.content_width;
6983 let cx = ftr_cursor.content_x;
6984 let style = node.style.resolve(None, cw);
6985 self.layout_view(
6986 node,
6987 &style,
6988 &mut ftr_cursor,
6989 &mut Vec::new(),
6990 cx,
6991 cw,
6992 font_context,
6993 );
6994 }
6995 for el in &mut ftr_cursor.elements {
6999 offset_element_y(el, target_y);
7000 }
7001 page.elements.extend(ftr_cursor.elements);
7002 }
7003
7004 page.fixed_header.clear();
7006 page.fixed_footer.clear();
7007 }
7008 }
7009
7010 #[allow(clippy::too_many_arguments)]
7015 fn layout_grid_children(
7016 &self,
7017 children: &[Node],
7018 parent_style: &ResolvedStyle,
7019 cursor: &mut PageCursor,
7020 pages: &mut Vec<LayoutPage>,
7021 x: f64,
7022 available_width: f64,
7023 font_context: &FontContext,
7024 ) {
7025 let template_cols = match &parent_style.grid_template_columns {
7026 Some(cols) => cols,
7027 None => return, };
7029
7030 let num_columns = template_cols.len();
7031 if num_columns == 0 || children.is_empty() {
7032 return;
7033 }
7034
7035 let col_gap = parent_style.column_gap;
7036 let row_gap = parent_style.row_gap;
7037
7038 let content_sizes: Vec<f64> = template_cols
7041 .iter()
7042 .map(|track| {
7043 if matches!(track, GridTrackSize::Auto) {
7044 available_width / num_columns as f64
7047 } else {
7048 0.0
7049 }
7050 })
7051 .collect();
7052
7053 let col_widths =
7054 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7055
7056 let placements: Vec<Option<&GridPlacement>> = children
7058 .iter()
7059 .map(|child| child.style.grid_placement.as_ref())
7060 .collect();
7061
7062 let item_placements = grid::place_items(&placements, num_columns);
7064 let num_rows = grid::compute_num_rows(&item_placements);
7065
7066 if num_rows == 0 {
7067 return;
7068 }
7069
7070 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7072 for placement in &item_placements {
7073 let cell_width =
7074 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7075 let child = &children[placement.child_index];
7076 let child_style = child.style.resolve(Some(parent_style), cell_width);
7077 item_heights[placement.child_index] =
7078 self.measure_node_height(child, cell_width, &child_style, font_context);
7079 }
7080
7081 let template_rows = parent_style.grid_template_rows.as_deref();
7083 let mut row_heights = vec![0.0_f64; num_rows];
7084 for placement in &item_placements {
7085 let h = item_heights[placement.child_index];
7086 let span = placement.row_end - placement.row_start;
7087 let per_row = h / span as f64;
7088 for rh in row_heights
7089 .iter_mut()
7090 .take(placement.row_end.min(num_rows))
7091 .skip(placement.row_start)
7092 {
7093 if per_row > *rh {
7094 *rh = per_row;
7095 }
7096 }
7097 }
7098
7099 if let Some(template) = template_rows {
7101 let auto_row = parent_style.grid_auto_rows.as_ref();
7102 for (r, rh) in row_heights.iter_mut().enumerate() {
7103 let track = template.get(r).or(auto_row);
7104 if let Some(track) = track {
7105 match track {
7106 GridTrackSize::Pt(pts) => *rh = *pts,
7107 GridTrackSize::Auto => {} _ => {} }
7110 }
7111 }
7112 }
7113
7114 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7116 if row_height > cursor.remaining_height() {
7121 pages.push(cursor.finalize());
7122 *cursor = cursor.new_page();
7123 }
7124
7125 let row_start_y = cursor.y;
7126
7127 for placement in &item_placements {
7129 if placement.row_start != row {
7130 continue; }
7132
7133 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7134 let cell_width =
7135 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7136
7137 let child = &children[placement.child_index];
7138
7139 self.layout_node(
7140 child,
7141 cursor,
7142 pages,
7143 cell_x,
7144 cell_width,
7145 Some(parent_style),
7146 font_context,
7147 None,
7148 None,
7149 );
7150 cursor.y = row_start_y;
7152 }
7153
7154 cursor.y = row_start_y + row_height + row_gap;
7155 }
7156
7157 if num_rows > 0 {
7159 cursor.y -= row_gap;
7160 }
7161 }
7162}
7163
7164struct FlexItem<'a> {
7165 node: &'a Node,
7166 style: ResolvedStyle,
7167 base_width: f64,
7168 min_content_width: f64,
7169}
7170
7171#[cfg(test)]
7172mod tests {
7173 use super::*;
7174 use crate::font::FontContext;
7175
7176 fn make_text(content: &str, font_size: f64) -> Node {
7177 Node {
7178 kind: NodeKind::Text {
7179 content: content.to_string(),
7180 href: None,
7181 runs: vec![],
7182 },
7183 style: Style {
7184 font_size: Some(font_size),
7185 ..Default::default()
7186 },
7187 children: vec![],
7188 id: None,
7189 source_location: None,
7190 bookmark: None,
7191 href: None,
7192 alt: None,
7193 }
7194 }
7195
7196 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
7197 Node {
7198 kind: NodeKind::View,
7199 style,
7200 children,
7201 id: None,
7202 source_location: None,
7203 bookmark: None,
7204 href: None,
7205 alt: None,
7206 }
7207 }
7208
7209 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
7210 Node {
7211 kind: NodeKind::Text {
7212 content: String::new(),
7213 href: None,
7214 runs,
7215 },
7216 style: Style::default(),
7217 children: vec![],
7218 id: None,
7219 source_location: None,
7220 bookmark: None,
7221 href: None,
7222 alt: None,
7223 }
7224 }
7225
7226 #[test]
7227 fn intrinsic_width_measures_runs_not_just_content() {
7228 let engine = LayoutEngine::new();
7232 let font_context = FontContext::new();
7233
7234 let runs_node = make_runs_text(vec![
7235 crate::model::TextRun {
7236 content: "Hello ".to_string(),
7237 style: Style::default(),
7238 href: None,
7239 },
7240 crate::model::TextRun {
7241 content: "World".to_string(),
7242 style: Style {
7243 font_weight: Some(700),
7244 ..Default::default()
7245 },
7246 href: None,
7247 },
7248 ]);
7249 let plain_node = make_text("Hello World", 12.0);
7250
7251 let runs_style = runs_node.style.resolve(None, 0.0);
7252 let plain_style = plain_node.style.resolve(None, 0.0);
7253 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
7254 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
7255
7256 assert!(
7259 runs_w >= plain_w,
7260 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
7261 );
7262 assert!(
7263 runs_w < plain_w * 1.5,
7264 "runs width ({runs_w}) should be close to plain width ({plain_w})"
7265 );
7266 }
7267
7268 #[test]
7269 fn intrinsic_width_of_multiline_text_is_widest_line() {
7270 let engine = LayoutEngine::new();
7271 let font_context = FontContext::new();
7272
7273 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
7274 let widest = make_text("Springfield, IL 62704", 12.0);
7275
7276 let m_style = multiline.style.resolve(None, 0.0);
7277 let w_style = widest.style.resolve(None, 0.0);
7278 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
7279 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
7280
7281 assert!(
7282 (m_w - w_w).abs() < 0.01,
7283 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
7284 );
7285 }
7286
7287 #[test]
7288 fn intrinsic_width_of_heading_measures_its_text() {
7289 let engine = LayoutEngine::new();
7292 let font_context = FontContext::new();
7293
7294 let heading = Node {
7295 kind: NodeKind::Heading {
7296 level: 1,
7297 content: "Invoice #2024-001".to_string(),
7298 href: None,
7299 runs: vec![],
7300 },
7301 style: Style {
7302 font_size: Some(24.0),
7303 ..Default::default()
7304 },
7305 children: vec![],
7306 id: None,
7307 source_location: None,
7308 bookmark: None,
7309 href: None,
7310 alt: None,
7311 };
7312 let style = heading.style.resolve(None, 0.0);
7313 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
7314 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
7315 }
7316
7317 #[test]
7318 fn measure_node_height_of_wrapping_heading_matches_text() {
7319 let engine = LayoutEngine::new();
7326 let font_context = FontContext::new();
7327
7328 let content = "Annual Performance Review";
7329 let heading = Node {
7330 kind: NodeKind::Heading {
7331 level: 1,
7332 content: content.to_string(),
7333 href: None,
7334 runs: vec![],
7335 },
7336 style: Style {
7337 font_size: Some(32.0),
7338 ..Default::default()
7339 },
7340 children: vec![],
7341 id: None,
7342 source_location: None,
7343 bookmark: None,
7344 href: None,
7345 alt: None,
7346 };
7347 let text = make_text(content, 32.0);
7348
7349 let width = 200.0;
7351 let h_style = heading.style.resolve(None, width);
7352 let t_style = text.style.resolve(None, width);
7353 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
7354 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
7355
7356 assert!(
7357 h_height > 32.0,
7358 "a wrapping 32pt heading must measure more than one line, got {h_height}"
7359 );
7360 assert!(
7361 (h_height - t_height).abs() < 0.01,
7362 "heading height ({h_height}) must equal the same text's height ({t_height})"
7363 );
7364 }
7365
7366 #[test]
7367 fn intrinsic_width_flex_row_sums_children() {
7368 let engine = LayoutEngine::new();
7369 let font_context = FontContext::new();
7370
7371 let child1 = make_text("Hello", 14.0);
7372 let child2 = make_text("World", 14.0);
7373
7374 let child1_style = child1.style.resolve(None, 0.0);
7375 let child2_style = child2.style.resolve(None, 0.0);
7376 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
7377 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
7378
7379 let row = make_styled_view(
7380 Style {
7381 flex_direction: Some(FlexDirection::Row),
7382 ..Default::default()
7383 },
7384 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
7385 );
7386 let row_style = row.style.resolve(None, 0.0);
7387 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
7388
7389 assert!(
7390 (row_w - (child1_w + child2_w)).abs() < 0.01,
7391 "Row intrinsic width ({}) should equal sum of children ({} + {})",
7392 row_w,
7393 child1_w,
7394 child2_w
7395 );
7396 }
7397
7398 #[test]
7399 fn intrinsic_width_flex_column_takes_max() {
7400 let engine = LayoutEngine::new();
7401 let font_context = FontContext::new();
7402
7403 let short = make_text("Hi", 14.0);
7404 let long = make_text("Hello World", 14.0);
7405
7406 let short_style = short.style.resolve(None, 0.0);
7407 let long_style = long.style.resolve(None, 0.0);
7408 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
7409 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
7410
7411 let col = make_styled_view(
7412 Style {
7413 flex_direction: Some(FlexDirection::Column),
7414 ..Default::default()
7415 },
7416 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
7417 );
7418 let col_style = col.style.resolve(None, 0.0);
7419 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
7420
7421 assert!(
7422 (col_w - long_w).abs() < 0.01,
7423 "Column intrinsic width ({}) should equal max child ({}, short was {})",
7424 col_w,
7425 long_w,
7426 short_w
7427 );
7428 }
7429
7430 #[test]
7431 fn intrinsic_width_nested_containers() {
7432 let engine = LayoutEngine::new();
7433 let font_context = FontContext::new();
7434
7435 let inner = make_styled_view(
7436 Style {
7437 flex_direction: Some(FlexDirection::Row),
7438 ..Default::default()
7439 },
7440 vec![make_text("A", 12.0), make_text("B", 12.0)],
7441 );
7442 let inner_style = inner.style.resolve(None, 0.0);
7443 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
7444
7445 let outer = make_styled_view(
7446 Style::default(),
7447 vec![make_styled_view(
7448 Style {
7449 flex_direction: Some(FlexDirection::Row),
7450 ..Default::default()
7451 },
7452 vec![make_text("A", 12.0), make_text("B", 12.0)],
7453 )],
7454 );
7455 let outer_style = outer.style.resolve(None, 0.0);
7456 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
7457
7458 assert!(
7459 (outer_w - inner_w).abs() < 0.01,
7460 "Nested container ({}) should match inner container ({})",
7461 outer_w,
7462 inner_w
7463 );
7464 }
7465
7466 #[test]
7467 fn intrinsic_width_row_with_gap() {
7468 let engine = LayoutEngine::new();
7469 let font_context = FontContext::new();
7470
7471 let no_gap = make_styled_view(
7472 Style {
7473 flex_direction: Some(FlexDirection::Row),
7474 ..Default::default()
7475 },
7476 vec![make_text("A", 12.0), make_text("B", 12.0)],
7477 );
7478 let with_gap = make_styled_view(
7479 Style {
7480 flex_direction: Some(FlexDirection::Row),
7481 gap: Some(10.0),
7482 ..Default::default()
7483 },
7484 vec![make_text("A", 12.0), make_text("B", 12.0)],
7485 );
7486
7487 let no_gap_style = no_gap.style.resolve(None, 0.0);
7488 let with_gap_style = with_gap.style.resolve(None, 0.0);
7489 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
7490 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
7491
7492 assert!(
7493 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
7494 "Gap should add 10pt: with_gap={}, no_gap={}",
7495 with_gap_w,
7496 no_gap_w
7497 );
7498 }
7499
7500 #[test]
7501 fn intrinsic_width_empty_container() {
7502 let engine = LayoutEngine::new();
7503 let font_context = FontContext::new();
7504
7505 let padding = 8.0;
7506 let empty = make_styled_view(
7507 Style {
7508 padding: Some(Edges::uniform(padding)),
7509 ..Default::default()
7510 },
7511 vec![],
7512 );
7513 let style = empty.style.resolve(None, 0.0);
7514 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
7515
7516 assert!(
7517 (w - padding * 2.0).abs() < 0.01,
7518 "Empty container width ({}) should equal horizontal padding ({})",
7519 w,
7520 padding * 2.0
7521 );
7522 }
7523
7524 #[test]
7527 fn flex_shrink_respects_min_content_width() {
7528 let engine = LayoutEngine::new();
7532 let font_context = FontContext::new();
7533
7534 let sale_text = make_text("SALE", 12.0);
7535 let sale_style = sale_text.style.resolve(None, 0.0);
7536 let sale_word_width =
7537 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
7538 assert!(
7539 sale_word_width > 0.0,
7540 "SALE should have non-zero min-content width"
7541 );
7542
7543 let container = make_styled_view(
7546 Style {
7547 flex_direction: Some(FlexDirection::Row),
7548 width: Some(Dimension::Pt(100.0)),
7549 ..Default::default()
7550 },
7551 vec![
7552 make_styled_view(
7553 Style {
7554 width: Some(Dimension::Pt(80.0)),
7555 flex_shrink: Some(1.0),
7556 ..Default::default()
7557 },
7558 vec![],
7559 ),
7560 make_styled_view(
7561 Style {
7562 width: Some(Dimension::Pt(60.0)),
7563 flex_shrink: Some(1.0),
7564 ..Default::default()
7565 },
7566 vec![make_text("SALE", 12.0)],
7567 ),
7568 ],
7569 );
7570
7571 let doc = Document {
7572 children: vec![Node::page(
7573 PageConfig::default(),
7574 Style::default(),
7575 vec![container],
7576 )],
7577 metadata: Default::default(),
7578 default_page: PageConfig::default(),
7579 first_page: None,
7580 left_page: None,
7581 right_page: None,
7582 named_pages: Default::default(),
7583 attachments: vec![],
7584 zugferd: None,
7585 fonts: vec![],
7586 tagged: false,
7587 pdfa: None,
7588 default_style: None,
7589 embedded_data: None,
7590 flatten_forms: false,
7591 pdf_ua: false,
7592 certification: None,
7593 };
7594
7595 let pages = engine.layout(&doc, &font_context);
7596 assert!(!pages.is_empty());
7597
7598 let page = &pages[0];
7601 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
7603 assert!(
7604 container_el.is_some(),
7605 "Should find container with 2 children"
7606 );
7607 let sale_child = &container_el.unwrap().children[1];
7608 assert!(
7609 sale_child.width >= sale_word_width - 0.01,
7610 "SALE child width ({}) should be >= min-content width ({})",
7611 sale_child.width,
7612 sale_word_width
7613 );
7614 }
7615
7616 #[test]
7619 fn column_justify_content_center() {
7620 let engine = LayoutEngine::new();
7623 let font_context = FontContext::new();
7624
7625 let container = make_styled_view(
7626 Style {
7627 flex_direction: Some(FlexDirection::Column),
7628 height: Some(Dimension::Pt(200.0)),
7629 justify_content: Some(JustifyContent::Center),
7630 ..Default::default()
7631 },
7632 vec![make_text("Centered", 12.0)],
7633 );
7634
7635 let doc = Document {
7636 children: vec![Node::page(
7637 PageConfig::default(),
7638 Style::default(),
7639 vec![container],
7640 )],
7641 metadata: Default::default(),
7642 default_page: PageConfig::default(),
7643 first_page: None,
7644 left_page: None,
7645 right_page: None,
7646 named_pages: Default::default(),
7647 attachments: vec![],
7648 zugferd: None,
7649 fonts: vec![],
7650 tagged: false,
7651 pdfa: None,
7652 default_style: None,
7653 embedded_data: None,
7654 flatten_forms: false,
7655 pdf_ua: false,
7656 certification: None,
7657 };
7658
7659 let pages = engine.layout(&doc, &font_context);
7660 let page = &pages[0];
7661
7662 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7665 assert!(
7666 container_el.is_some(),
7667 "Should find container with children"
7668 );
7669 let container_el = container_el.unwrap();
7670 let child = &container_el.children[0];
7671
7672 let child_offset = child.y - container_el.y;
7674 let expected_offset = (200.0 - child.height) / 2.0;
7675 assert!(
7676 (child_offset - expected_offset).abs() < 2.0,
7677 "Child offset ({}) should be near center ({})",
7678 child_offset,
7679 expected_offset
7680 );
7681 }
7682
7683 #[test]
7684 fn column_align_items_center() {
7685 let engine = LayoutEngine::new();
7688 let font_context = FontContext::new();
7689
7690 let container = make_styled_view(
7691 Style {
7692 flex_direction: Some(FlexDirection::Column),
7693 width: Some(Dimension::Pt(300.0)),
7694 align_items: Some(AlignItems::Center),
7695 ..Default::default()
7696 },
7697 vec![make_text("Hi", 12.0)],
7698 );
7699
7700 let doc = Document {
7701 children: vec![Node::page(
7702 PageConfig::default(),
7703 Style::default(),
7704 vec![container],
7705 )],
7706 metadata: Default::default(),
7707 default_page: PageConfig::default(),
7708 first_page: None,
7709 left_page: None,
7710 right_page: None,
7711 named_pages: Default::default(),
7712 attachments: vec![],
7713 zugferd: None,
7714 fonts: vec![],
7715 tagged: false,
7716 pdfa: None,
7717 default_style: None,
7718 embedded_data: None,
7719 flatten_forms: false,
7720 pdf_ua: false,
7721 certification: None,
7722 };
7723
7724 let pages = engine.layout(&doc, &font_context);
7725 let page = &pages[0];
7726
7727 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7728 assert!(container_el.is_some());
7729 let container_el = container_el.unwrap();
7730 let child = &container_el.children[0];
7731
7732 let child_center = child.x + child.width / 2.0;
7734 let container_center = container_el.x + container_el.width / 2.0;
7735 assert!(
7736 (child_center - container_center).abs() < 2.0,
7737 "Child center ({}) should be near container center ({})",
7738 child_center,
7739 container_center
7740 );
7741 }
7742
7743 #[test]
7746 fn absolute_child_positioned_relative_to_parent() {
7747 let engine = LayoutEngine::new();
7751 let font_context = FontContext::new();
7752
7753 let parent = make_styled_view(
7754 Style {
7755 position: Some(crate::model::Position::Relative),
7756 margin: Some(MarginEdges::from_edges(Edges {
7757 top: 50.0,
7758 left: 50.0,
7759 ..Default::default()
7760 })),
7761 width: Some(Dimension::Pt(200.0)),
7762 height: Some(Dimension::Pt(200.0)),
7763 ..Default::default()
7764 },
7765 vec![make_styled_view(
7766 Style {
7767 position: Some(crate::model::Position::Absolute),
7768 top: Some(10.0),
7769 left: Some(10.0),
7770 width: Some(Dimension::Pt(50.0)),
7771 height: Some(Dimension::Pt(50.0)),
7772 ..Default::default()
7773 },
7774 vec![],
7775 )],
7776 );
7777
7778 let doc = Document {
7779 children: vec![Node::page(
7780 PageConfig::default(),
7781 Style::default(),
7782 vec![parent],
7783 )],
7784 metadata: Default::default(),
7785 default_page: PageConfig::default(),
7786 first_page: None,
7787 left_page: None,
7788 right_page: None,
7789 named_pages: Default::default(),
7790 attachments: vec![],
7791 zugferd: None,
7792 fonts: vec![],
7793 tagged: false,
7794 pdfa: None,
7795 default_style: None,
7796 embedded_data: None,
7797 flatten_forms: false,
7798 pdf_ua: false,
7799 certification: None,
7800 };
7801
7802 let pages = engine.layout(&doc, &font_context);
7803 let page = &pages[0];
7804
7805 let parent_el = page
7808 .elements
7809 .iter()
7810 .find(|e| e.width > 190.0 && e.width < 210.0);
7811 assert!(parent_el.is_some(), "Should find the 200x200 parent");
7812 let parent_el = parent_el.unwrap();
7813
7814 let abs_child = parent_el
7816 .children
7817 .iter()
7818 .find(|e| e.width > 45.0 && e.width < 55.0);
7819 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
7820 let abs_child = abs_child.unwrap();
7821
7822 let expected_x = parent_el.x + 10.0;
7823 let expected_y = parent_el.y + 10.0;
7824 assert!(
7825 (abs_child.x - expected_x).abs() < 1.0,
7826 "Absolute child x ({}) should be parent.x + 10 ({})",
7827 abs_child.x,
7828 expected_x
7829 );
7830 assert!(
7831 (abs_child.y - expected_y).abs() < 1.0,
7832 "Absolute child y ({}) should be parent.y + 10 ({})",
7833 abs_child.y,
7834 expected_y
7835 );
7836 }
7837
7838 #[test]
7839 fn absolute_escapes_unpositioned_parent_to_page() {
7840 let engine = LayoutEngine::new();
7845 let font_context = FontContext::new();
7846 let parent = make_styled_view(
7847 Style {
7848 margin: Some(MarginEdges::from_edges(Edges {
7849 top: 50.0,
7850 left: 50.0,
7851 ..Default::default()
7852 })),
7853 width: Some(Dimension::Pt(200.0)),
7854 height: Some(Dimension::Pt(200.0)),
7855 ..Default::default()
7856 },
7857 vec![make_styled_view(
7858 Style {
7859 position: Some(crate::model::Position::Absolute),
7860 top: Some(10.0),
7861 left: Some(10.0),
7862 width: Some(Dimension::Pt(50.0)),
7863 height: Some(Dimension::Pt(50.0)),
7864 ..Default::default()
7865 },
7866 vec![],
7867 )],
7868 );
7869 let doc = Document {
7870 children: vec![Node::page(
7871 PageConfig::default(),
7872 Style::default(),
7873 vec![parent],
7874 )],
7875 metadata: Default::default(),
7876 default_page: PageConfig::default(),
7877 first_page: None,
7878 left_page: None,
7879 right_page: None,
7880 named_pages: Default::default(),
7881 attachments: vec![],
7882 zugferd: None,
7883 fonts: vec![],
7884 tagged: false,
7885 pdfa: None,
7886 default_style: None,
7887 embedded_data: None,
7888 flatten_forms: false,
7889 pdf_ua: false,
7890 certification: None,
7891 };
7892 let pages = engine.layout(&doc, &font_context);
7893 let page = &pages[0];
7894 let parent_el = page
7895 .elements
7896 .iter()
7897 .find(|e| e.width > 190.0 && e.width < 210.0)
7898 .expect("parent");
7899 let abs_child = parent_el
7900 .children
7901 .iter()
7902 .find(|e| e.width > 45.0 && e.width < 55.0)
7903 .expect("abs child");
7904 let page_left = PageConfig::default().margin.left;
7905 let page_top = PageConfig::default().margin.top;
7906 assert!(
7907 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
7908 "absolute escapes to the page: x {} should be page_left + 10 ({})",
7909 abs_child.x,
7910 page_left + 10.0
7911 );
7912 assert!(
7913 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
7914 "absolute escapes to the page: y {} should be page_top + 10 ({})",
7915 abs_child.y,
7916 page_top + 10.0
7917 );
7918 assert!(
7919 abs_child.x < parent_el.x,
7920 "child must no longer be parent-relative (parent is 50pt further in)"
7921 );
7922 }
7923
7924 #[test]
7925 fn text_transform_none_passthrough() {
7926 assert_eq!(
7927 apply_text_transform("Hello World", TextTransform::None),
7928 "Hello World"
7929 );
7930 }
7931
7932 #[test]
7933 fn text_transform_uppercase() {
7934 assert_eq!(
7935 apply_text_transform("hello world", TextTransform::Uppercase),
7936 "HELLO WORLD"
7937 );
7938 }
7939
7940 #[test]
7941 fn text_transform_lowercase() {
7942 assert_eq!(
7943 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
7944 "hello world"
7945 );
7946 }
7947
7948 #[test]
7949 fn text_transform_capitalize() {
7950 assert_eq!(
7951 apply_text_transform("hello world", TextTransform::Capitalize),
7952 "Hello World"
7953 );
7954 assert_eq!(
7955 apply_text_transform(" hello world ", TextTransform::Capitalize),
7956 " Hello World "
7957 );
7958 assert_eq!(
7959 apply_text_transform("already Capitalized", TextTransform::Capitalize),
7960 "Already Capitalized"
7961 );
7962 }
7963
7964 #[test]
7965 fn text_transform_capitalize_empty() {
7966 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
7967 }
7968
7969 #[test]
7970 fn apply_char_transform_uppercase() {
7971 assert_eq!(
7972 apply_char_transform('a', TextTransform::Uppercase, false),
7973 'A'
7974 );
7975 assert_eq!(
7976 apply_char_transform('A', TextTransform::Uppercase, false),
7977 'A'
7978 );
7979 }
7980
7981 #[test]
7982 fn apply_char_transform_capitalize_word_start() {
7983 assert_eq!(
7984 apply_char_transform('h', TextTransform::Capitalize, true),
7985 'H'
7986 );
7987 assert_eq!(
7988 apply_char_transform('h', TextTransform::Capitalize, false),
7989 'h'
7990 );
7991 }
7992
7993 #[test]
7996 fn column_flex_grow_single_child_fills_container() {
7997 let engine = LayoutEngine::new();
8000 let font_context = FontContext::new();
8001
8002 let child = make_styled_view(
8003 Style {
8004 flex_grow: Some(1.0),
8005 ..Default::default()
8006 },
8007 vec![make_text("Short", 12.0)],
8008 );
8009
8010 let container = make_styled_view(
8011 Style {
8012 flex_direction: Some(FlexDirection::Column),
8013 height: Some(Dimension::Pt(300.0)),
8014 ..Default::default()
8015 },
8016 vec![child],
8017 );
8018
8019 let doc = Document {
8020 children: vec![Node::page(
8021 PageConfig::default(),
8022 Style::default(),
8023 vec![container],
8024 )],
8025 metadata: Default::default(),
8026 default_page: PageConfig::default(),
8027 first_page: None,
8028 left_page: None,
8029 right_page: None,
8030 named_pages: Default::default(),
8031 attachments: vec![],
8032 zugferd: None,
8033 fonts: vec![],
8034 tagged: false,
8035 pdfa: None,
8036 default_style: None,
8037 embedded_data: None,
8038 flatten_forms: false,
8039 pdf_ua: false,
8040 certification: None,
8041 };
8042
8043 let pages = engine.layout(&doc, &font_context);
8044 let page = &pages[0];
8045
8046 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8047 assert!(container_el.is_some());
8048 let container_el = container_el.unwrap();
8049 assert!(
8050 (container_el.height - 300.0).abs() < 1.0,
8051 "Container should be 300pt, got {}",
8052 container_el.height
8053 );
8054
8055 let child_el = &container_el.children[0];
8056 assert!(
8057 (child_el.height - 300.0).abs() < 1.0,
8058 "flex-grow child should expand to 300pt, got {}",
8059 child_el.height
8060 );
8061 }
8062
8063 #[test]
8064 fn column_flex_grow_two_children_proportional() {
8065 let engine = LayoutEngine::new();
8068 let font_context = FontContext::new();
8069
8070 let child1 = make_styled_view(
8071 Style {
8072 flex_grow: Some(1.0),
8073 ..Default::default()
8074 },
8075 vec![make_text("A", 12.0)],
8076 );
8077 let child2 = make_styled_view(
8078 Style {
8079 flex_grow: Some(2.0),
8080 ..Default::default()
8081 },
8082 vec![make_text("B", 12.0)],
8083 );
8084
8085 let container = make_styled_view(
8086 Style {
8087 flex_direction: Some(FlexDirection::Column),
8088 height: Some(Dimension::Pt(300.0)),
8089 ..Default::default()
8090 },
8091 vec![child1, child2],
8092 );
8093
8094 let doc = Document {
8095 children: vec![Node::page(
8096 PageConfig::default(),
8097 Style::default(),
8098 vec![container],
8099 )],
8100 metadata: Default::default(),
8101 default_page: PageConfig::default(),
8102 first_page: None,
8103 left_page: None,
8104 right_page: None,
8105 named_pages: Default::default(),
8106 attachments: vec![],
8107 zugferd: None,
8108 fonts: vec![],
8109 tagged: false,
8110 pdfa: None,
8111 default_style: None,
8112 embedded_data: None,
8113 flatten_forms: false,
8114 pdf_ua: false,
8115 certification: None,
8116 };
8117
8118 let pages = engine.layout(&doc, &font_context);
8119 let page = &pages[0];
8120
8121 let container_el = page
8122 .elements
8123 .iter()
8124 .find(|e| e.children.len() == 2)
8125 .expect("Should find container with two children");
8126
8127 let c1 = &container_el.children[0];
8128 let c2 = &container_el.children[1];
8129
8130 let total = c1.height + c2.height;
8134 assert!(
8135 (total - 300.0).abs() < 2.0,
8136 "Children should sum to ~300pt, got {}",
8137 total
8138 );
8139
8140 let ratio = c2.height / c1.height;
8143 assert!(
8144 ratio > 1.3 && ratio < 2.5,
8145 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
8146 ratio
8147 );
8148 }
8149
8150 #[test]
8151 fn column_flex_grow_mixed_grow_and_fixed() {
8152 let engine = LayoutEngine::new();
8155 let font_context = FontContext::new();
8156
8157 let fixed_child = make_styled_view(
8158 Style {
8159 height: Some(Dimension::Pt(50.0)),
8160 ..Default::default()
8161 },
8162 vec![make_text("Fixed", 12.0)],
8163 );
8164 let grow_child = make_styled_view(
8165 Style {
8166 flex_grow: Some(1.0),
8167 ..Default::default()
8168 },
8169 vec![make_text("Grow", 12.0)],
8170 );
8171
8172 let container = make_styled_view(
8173 Style {
8174 flex_direction: Some(FlexDirection::Column),
8175 height: Some(Dimension::Pt(300.0)),
8176 ..Default::default()
8177 },
8178 vec![fixed_child, grow_child],
8179 );
8180
8181 let doc = Document {
8182 children: vec![Node::page(
8183 PageConfig::default(),
8184 Style::default(),
8185 vec![container],
8186 )],
8187 metadata: Default::default(),
8188 default_page: PageConfig::default(),
8189 first_page: None,
8190 left_page: None,
8191 right_page: None,
8192 named_pages: Default::default(),
8193 attachments: vec![],
8194 zugferd: None,
8195 fonts: vec![],
8196 tagged: false,
8197 pdfa: None,
8198 default_style: None,
8199 embedded_data: None,
8200 flatten_forms: false,
8201 pdf_ua: false,
8202 certification: None,
8203 };
8204
8205 let pages = engine.layout(&doc, &font_context);
8206 let page = &pages[0];
8207
8208 let container_el = page
8209 .elements
8210 .iter()
8211 .find(|e| e.children.len() == 2)
8212 .expect("Should find container with two children");
8213
8214 let fixed_el = &container_el.children[0];
8215 let grow_el = &container_el.children[1];
8216
8217 assert!(
8219 (fixed_el.height - 50.0).abs() < 1.0,
8220 "Fixed child should stay at 50pt, got {}",
8221 fixed_el.height
8222 );
8223
8224 assert!(
8226 (grow_el.height - 250.0).abs() < 2.0,
8227 "Grow child should expand to ~250pt, got {}",
8228 grow_el.height
8229 );
8230 }
8231
8232 #[test]
8233 fn column_flex_grow_page_level() {
8234 let engine = LayoutEngine::new();
8236 let font_context = FontContext::new();
8237
8238 let grow_child = make_styled_view(
8239 Style {
8240 flex_grow: Some(1.0),
8241 ..Default::default()
8242 },
8243 vec![make_text("Fill page", 12.0)],
8244 );
8245
8246 let doc = Document {
8247 children: vec![Node::page(
8248 PageConfig::default(),
8249 Style::default(),
8250 vec![grow_child],
8251 )],
8252 metadata: Default::default(),
8253 default_page: PageConfig::default(),
8254 first_page: None,
8255 left_page: None,
8256 right_page: None,
8257 named_pages: Default::default(),
8258 attachments: vec![],
8259 zugferd: None,
8260 fonts: vec![],
8261 tagged: false,
8262 pdfa: None,
8263 default_style: None,
8264 embedded_data: None,
8265 flatten_forms: false,
8266 pdf_ua: false,
8267 certification: None,
8268 };
8269
8270 let pages = engine.layout(&doc, &font_context);
8271 let page = &pages[0];
8272
8273 assert!(
8275 !page.elements.is_empty(),
8276 "Page should have at least one element"
8277 );
8278
8279 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
8280 let el = &page.elements[0];
8281 assert!(
8282 (el.height - content_height).abs() < 2.0,
8283 "Page-level flex-grow child should fill content height ({}), got {}",
8284 content_height,
8285 el.height
8286 );
8287 }
8288
8289 #[test]
8290 fn column_flex_grow_with_justify_content() {
8291 let engine = LayoutEngine::new();
8295 let font_context = FontContext::new();
8296
8297 let fixed_child = make_styled_view(
8298 Style {
8299 height: Some(Dimension::Pt(50.0)),
8300 ..Default::default()
8301 },
8302 vec![make_text("Top", 12.0)],
8303 );
8304 let grow_child = make_styled_view(
8305 Style {
8306 flex_grow: Some(1.0),
8307 ..Default::default()
8308 },
8309 vec![make_text("Fill", 12.0)],
8310 );
8311
8312 let container = make_styled_view(
8313 Style {
8314 flex_direction: Some(FlexDirection::Column),
8315 height: Some(Dimension::Pt(300.0)),
8316 justify_content: Some(JustifyContent::Center),
8317 ..Default::default()
8318 },
8319 vec![fixed_child, grow_child],
8320 );
8321
8322 let doc = Document {
8323 children: vec![Node::page(
8324 PageConfig::default(),
8325 Style::default(),
8326 vec![container],
8327 )],
8328 metadata: Default::default(),
8329 default_page: PageConfig::default(),
8330 first_page: None,
8331 left_page: None,
8332 right_page: None,
8333 named_pages: Default::default(),
8334 attachments: vec![],
8335 zugferd: None,
8336 fonts: vec![],
8337 tagged: false,
8338 pdfa: None,
8339 default_style: None,
8340 embedded_data: None,
8341 flatten_forms: false,
8342 pdf_ua: false,
8343 certification: None,
8344 };
8345
8346 let pages = engine.layout(&doc, &font_context);
8347 let page = &pages[0];
8348
8349 let container_el = page
8350 .elements
8351 .iter()
8352 .find(|e| e.children.len() == 2)
8353 .expect("Should find container");
8354
8355 let first_child = &container_el.children[0];
8358 assert!(
8359 (first_child.y - container_el.y).abs() < 1.0,
8360 "First child should be at top of container"
8361 );
8362
8363 let total = container_el.children[0].height + container_el.children[1].height;
8365 assert!(
8366 (total - 300.0).abs() < 2.0,
8367 "Children should fill container, got {}",
8368 total
8369 );
8370 }
8371
8372 #[test]
8373 fn column_flex_grow_child_justify_content_center() {
8374 let engine = LayoutEngine::new();
8377 let font_context = FontContext::new();
8378
8379 let inner_box = make_styled_view(
8381 Style {
8382 height: Some(Dimension::Pt(40.0)),
8383 ..Default::default()
8384 },
8385 vec![make_text("Centered", 12.0)],
8386 );
8387
8388 let grow_child = make_styled_view(
8390 Style {
8391 flex_grow: Some(1.0),
8392 flex_direction: Some(FlexDirection::Column),
8393 justify_content: Some(JustifyContent::Center),
8394 ..Default::default()
8395 },
8396 vec![inner_box],
8397 );
8398
8399 let container = make_styled_view(
8401 Style {
8402 flex_direction: Some(FlexDirection::Column),
8403 height: Some(Dimension::Pt(400.0)),
8404 ..Default::default()
8405 },
8406 vec![grow_child],
8407 );
8408
8409 let doc = Document {
8410 children: vec![Node::page(
8411 PageConfig::default(),
8412 Style::default(),
8413 vec![container],
8414 )],
8415 metadata: Default::default(),
8416 default_page: PageConfig::default(),
8417 first_page: None,
8418 left_page: None,
8419 right_page: None,
8420 named_pages: Default::default(),
8421 attachments: vec![],
8422 zugferd: None,
8423 fonts: vec![],
8424 tagged: false,
8425 pdfa: None,
8426 default_style: None,
8427 embedded_data: None,
8428 flatten_forms: false,
8429 pdf_ua: false,
8430 certification: None,
8431 };
8432
8433 let pages = engine.layout(&doc, &font_context);
8434 let page = &pages[0];
8435
8436 let container_el = page
8438 .elements
8439 .iter()
8440 .find(|e| e.height > 350.0 && e.children.len() == 1)
8441 .expect("Should find outer container");
8442
8443 let grow_el = &container_el.children[0];
8444 assert!(
8445 (grow_el.height - 400.0).abs() < 2.0,
8446 "Grow child should expand to 400, got {}",
8447 grow_el.height
8448 );
8449
8450 let inner_el = &grow_el.children[0];
8452 let expected_center = grow_el.y + grow_el.height / 2.0;
8453 let actual_center = inner_el.y + inner_el.height / 2.0;
8454 assert!(
8455 (actual_center - expected_center).abs() < 2.0,
8456 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
8457 expected_center,
8458 actual_center
8459 );
8460 }
8461
8462 #[test]
8463 fn column_flex_grow_child_justify_content_flex_end() {
8464 let engine = LayoutEngine::new();
8466 let font_context = FontContext::new();
8467
8468 let inner_box = make_styled_view(
8469 Style {
8470 height: Some(Dimension::Pt(30.0)),
8471 ..Default::default()
8472 },
8473 vec![make_text("Bottom", 12.0)],
8474 );
8475
8476 let grow_child = make_styled_view(
8477 Style {
8478 flex_grow: Some(1.0),
8479 flex_direction: Some(FlexDirection::Column),
8480 justify_content: Some(JustifyContent::FlexEnd),
8481 ..Default::default()
8482 },
8483 vec![inner_box],
8484 );
8485
8486 let container = make_styled_view(
8487 Style {
8488 flex_direction: Some(FlexDirection::Column),
8489 height: Some(Dimension::Pt(300.0)),
8490 ..Default::default()
8491 },
8492 vec![grow_child],
8493 );
8494
8495 let doc = Document {
8496 children: vec![Node::page(
8497 PageConfig::default(),
8498 Style::default(),
8499 vec![container],
8500 )],
8501 metadata: Default::default(),
8502 default_page: PageConfig::default(),
8503 first_page: None,
8504 left_page: None,
8505 right_page: None,
8506 named_pages: Default::default(),
8507 attachments: vec![],
8508 zugferd: None,
8509 fonts: vec![],
8510 tagged: false,
8511 pdfa: None,
8512 default_style: None,
8513 embedded_data: None,
8514 flatten_forms: false,
8515 pdf_ua: false,
8516 certification: None,
8517 };
8518
8519 let pages = engine.layout(&doc, &font_context);
8520 let page = &pages[0];
8521
8522 let container_el = page
8523 .elements
8524 .iter()
8525 .find(|e| e.height > 250.0 && e.children.len() == 1)
8526 .expect("Should find outer container");
8527
8528 let grow_el = &container_el.children[0];
8529 let inner_el = &grow_el.children[0];
8530
8531 let inner_bottom = inner_el.y + inner_el.height;
8533 let grow_bottom = grow_el.y + grow_el.height;
8534 assert!(
8535 (inner_bottom - grow_bottom).abs() < 2.0,
8536 "Inner box bottom ({}) should align with grow child bottom ({})",
8537 inner_bottom,
8538 grow_bottom
8539 );
8540 }
8541
8542 #[test]
8543 fn column_flex_grow_child_no_justify_unchanged() {
8544 let engine = LayoutEngine::new();
8546 let font_context = FontContext::new();
8547
8548 let inner_box = make_styled_view(
8549 Style {
8550 height: Some(Dimension::Pt(50.0)),
8551 ..Default::default()
8552 },
8553 vec![make_text("Top", 12.0)],
8554 );
8555
8556 let grow_child = make_styled_view(
8557 Style {
8558 flex_grow: Some(1.0),
8559 flex_direction: Some(FlexDirection::Column),
8560 ..Default::default()
8562 },
8563 vec![inner_box],
8564 );
8565
8566 let container = make_styled_view(
8567 Style {
8568 flex_direction: Some(FlexDirection::Column),
8569 height: Some(Dimension::Pt(300.0)),
8570 ..Default::default()
8571 },
8572 vec![grow_child],
8573 );
8574
8575 let doc = Document {
8576 children: vec![Node::page(
8577 PageConfig::default(),
8578 Style::default(),
8579 vec![container],
8580 )],
8581 metadata: Default::default(),
8582 default_page: PageConfig::default(),
8583 first_page: None,
8584 left_page: None,
8585 right_page: None,
8586 named_pages: Default::default(),
8587 attachments: vec![],
8588 zugferd: None,
8589 fonts: vec![],
8590 tagged: false,
8591 pdfa: None,
8592 default_style: None,
8593 embedded_data: None,
8594 flatten_forms: false,
8595 pdf_ua: false,
8596 certification: None,
8597 };
8598
8599 let pages = engine.layout(&doc, &font_context);
8600 let page = &pages[0];
8601
8602 let container_el = page
8603 .elements
8604 .iter()
8605 .find(|e| e.height > 250.0 && e.children.len() == 1)
8606 .expect("Should find outer container");
8607
8608 let grow_el = &container_el.children[0];
8609 let inner_el = &grow_el.children[0];
8610
8611 assert!(
8613 (inner_el.y - grow_el.y).abs() < 2.0,
8614 "Inner box ({}) should be at top of grow child ({})",
8615 inner_el.y,
8616 grow_el.y
8617 );
8618 }
8619
8620 #[test]
8621 fn column_flex_grow_child_align_items_center() {
8622 let engine = LayoutEngine::new();
8624 let font_context = FontContext::new();
8625
8626 let text = make_text("Hello", 12.0);
8627
8628 let grow_child = make_styled_view(
8629 Style {
8630 flex_grow: Some(1.0),
8631 flex_direction: Some(FlexDirection::Column),
8632 align_items: Some(AlignItems::Center),
8633 ..Default::default()
8634 },
8635 vec![text],
8636 );
8637
8638 let container = make_styled_view(
8639 Style {
8640 flex_direction: Some(FlexDirection::Column),
8641 height: Some(Dimension::Pt(300.0)),
8642 ..Default::default()
8643 },
8644 vec![grow_child],
8645 );
8646
8647 let doc = Document {
8648 children: vec![Node::page(
8649 PageConfig::default(),
8650 Style::default(),
8651 vec![container],
8652 )],
8653 metadata: Default::default(),
8654 default_page: PageConfig::default(),
8655 first_page: None,
8656 left_page: None,
8657 right_page: None,
8658 named_pages: Default::default(),
8659 attachments: vec![],
8660 zugferd: None,
8661 fonts: vec![],
8662 tagged: false,
8663 pdfa: None,
8664 default_style: None,
8665 embedded_data: None,
8666 flatten_forms: false,
8667 pdf_ua: false,
8668 certification: None,
8669 };
8670
8671 let pages = engine.layout(&doc, &font_context);
8672 let page = &pages[0];
8673
8674 let container_el = page
8675 .elements
8676 .iter()
8677 .find(|e| e.height > 250.0 && e.children.len() == 1)
8678 .expect("Should find outer container");
8679
8680 let grow_el = &container_el.children[0];
8681 assert!(
8682 !grow_el.children.is_empty(),
8683 "Grow child should have text child"
8684 );
8685
8686 let text_el = &grow_el.children[0];
8687 let text_center = text_el.x + text_el.width / 2.0;
8688 let grow_center = grow_el.x + grow_el.width / 2.0;
8689 assert!(
8690 (text_center - grow_center).abs() < 2.0,
8691 "Text center ({}) should be near grow child center ({})",
8692 text_center,
8693 grow_center
8694 );
8695 }
8696
8697 #[test]
8698 fn image_intrinsic_width_respects_height_constraint() {
8699 let engine = LayoutEngine::new();
8703 let font_context = FontContext::new();
8704
8705 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
8707
8708 let image_node = Node {
8709 kind: NodeKind::Image {
8710 src: one_px_png.to_string(),
8711 width: None,
8712 height: Some(36.0),
8713 },
8714 style: Style::default(),
8715 children: vec![],
8716 id: None,
8717 source_location: None,
8718 bookmark: None,
8719 href: None,
8720 alt: None,
8721 };
8722
8723 let resolved = image_node.style.resolve(None, 0.0);
8724 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
8725
8726 assert!(
8728 (intrinsic - 36.0).abs() < 1.0,
8729 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
8730 intrinsic
8731 );
8732 }
8733}