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}
1019
1020#[derive(Debug, Clone)]
1022struct PageCursor {
1023 config: PageConfig,
1024 base_config: PageConfig,
1027 page_index: usize,
1032 content_width: f64,
1033 content_height: f64,
1034 y: f64,
1035 elements: Vec<LayoutElement>,
1036 fixed_header: Vec<(Node, f64)>,
1037 fixed_footer: Vec<(Node, f64)>,
1038 watermarks: Vec<Node>,
1040 content_x: f64,
1041 content_y: f64,
1042 continuation_top_offset: f64,
1044 containing_block: (f64, f64, f64, f64),
1049 left_config: Option<PageConfig>,
1057 right_config: Option<PageConfig>,
1058 display_config: Option<PageConfig>,
1061 flow_anchor_left: f64,
1066 page_name: Option<String>,
1070 base_display: Option<PageConfig>,
1074 first_display: Option<PageConfig>,
1078 doc_base: PageConfig,
1081 doc_left: Option<PageConfig>,
1082 doc_right: Option<PageConfig>,
1083 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1085}
1086
1087impl PageCursor {
1088 fn new(config: &PageConfig) -> Self {
1089 let (page_w, page_h) = config.size.dimensions();
1090 let content_width = page_w - config.margin.horizontal();
1091 let content_height = page_h - config.margin.vertical();
1092
1093 Self {
1094 config: config.clone(),
1095 base_config: config.clone(),
1096 page_index: 0,
1097 content_width,
1098 content_height,
1099 y: 0.0,
1100 elements: Vec::new(),
1101 fixed_header: Vec::new(),
1102 fixed_footer: Vec::new(),
1103 watermarks: Vec::new(),
1104 content_x: config.margin.left,
1105 content_y: config.margin.top,
1106 continuation_top_offset: 0.0,
1107 containing_block: (
1108 config.margin.left,
1109 config.margin.top,
1110 content_width,
1111 content_height,
1112 ),
1113 left_config: None,
1114 right_config: None,
1115 display_config: None,
1116 flow_anchor_left: config.margin.left,
1117 page_name: None,
1118 base_display: None,
1119 first_display: None,
1120 doc_base: config.clone(),
1121 doc_left: None,
1122 doc_right: None,
1123 named_sets: std::collections::HashMap::new(),
1124 }
1125 }
1126
1127 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1134 if page_number == 1 {
1135 if let Some(first) = &self.first_display {
1136 return Some(first.clone());
1137 }
1138 }
1139 let parity = if page_number % 2 == 1 {
1140 self.right_config.clone()
1141 } else {
1142 self.left_config.clone()
1143 };
1144 parity.or_else(|| self.base_display.clone())
1145 }
1146
1147 fn fixed_applies(&self, node: &Node) -> bool {
1150 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1151 }
1152
1153 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1160 let set = name
1161 .as_deref()
1162 .and_then(|n| self.named_sets.get(n))
1163 .cloned();
1164 let real = set
1165 .as_ref()
1166 .map(|s| s.base.clone())
1167 .unwrap_or_else(|| self.doc_base.clone());
1168 let mut cursor = PageCursor::new(&real);
1169 cursor.page_index = if in_place {
1170 self.page_index
1171 } else {
1172 self.page_index + 1
1173 };
1174 cursor.page_name = name;
1175 cursor.doc_base = self.doc_base.clone();
1176 cursor.doc_left = self.doc_left.clone();
1177 cursor.doc_right = self.doc_right.clone();
1178 cursor.named_sets = self.named_sets.clone();
1179 match &set {
1180 Some(s) => {
1181 cursor.left_config = s.display_left.clone();
1182 cursor.right_config = s.display_right.clone();
1183 cursor.base_display = s.display.clone();
1184 cursor.first_display = s.display_first.clone();
1185 }
1186 None => {
1187 cursor.left_config = self.doc_left.clone();
1191 cursor.right_config = self.doc_right.clone();
1192 }
1193 }
1194 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1195 cursor.flow_anchor_left = self.flow_anchor_left;
1196 cursor.fixed_header = self.fixed_header.clone();
1197 cursor.fixed_footer = self.fixed_footer.clone();
1198 cursor.watermarks = self.watermarks.clone();
1199 cursor.continuation_top_offset = self.continuation_top_offset;
1200
1201 let header_height: f64 = cursor
1202 .fixed_header
1203 .iter()
1204 .filter(|(n, _)| cursor.fixed_applies(n))
1205 .map(|(_, h)| *h)
1206 .sum();
1207 cursor.y = header_height + cursor.continuation_top_offset;
1208 cursor
1209 }
1210
1211 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1214 let mut cursor = PageCursor::new(first);
1215 cursor.base_config = base.clone();
1216 cursor
1217 }
1218
1219 fn remaining_height(&self) -> f64 {
1220 let footer_height: f64 = self
1221 .fixed_footer
1222 .iter()
1223 .filter(|(n, _)| self.fixed_applies(n))
1224 .map(|(_, h)| *h)
1225 .sum();
1226 (self.content_height - self.y - footer_height).max(0.0)
1227 }
1228
1229 fn finalize(&self) -> LayoutPage {
1230 let (page_w, page_h) = self.config.size.dimensions();
1231
1232 let (elements, config) = match &self.display_config {
1238 Some(display) => {
1239 let dx = display.margin.left - self.flow_anchor_left;
1240 let mut elements = self.elements.clone();
1241 if dx != 0.0 {
1242 fn shift(els: &mut [LayoutElement], dx: f64) {
1243 for el in els {
1244 el.x += dx;
1245 shift(&mut el.children, dx);
1246 }
1247 }
1248 shift(&mut elements, dx);
1249 }
1250 (elements, display.clone())
1251 }
1252 None => (self.elements.clone(), self.config.clone()),
1253 };
1254
1255 LayoutPage {
1256 width: page_w,
1257 height: page_h,
1258 elements,
1259 fixed_header: self.fixed_header.clone(),
1260 fixed_footer: self.fixed_footer.clone(),
1261 watermarks: self.watermarks.clone(),
1262 config,
1263 page_name: self.page_name.clone(),
1264 }
1265 }
1266
1267 fn new_page(&self) -> Self {
1268 let mut cursor = PageCursor::new(&self.base_config);
1271 cursor.page_index = self.page_index + 1;
1272 cursor.page_name = self.page_name.clone();
1273 cursor.left_config = self.left_config.clone();
1274 cursor.right_config = self.right_config.clone();
1275 cursor.base_display = self.base_display.clone();
1276 cursor.first_display = self.first_display.clone();
1277 cursor.doc_base = self.doc_base.clone();
1278 cursor.doc_left = self.doc_left.clone();
1279 cursor.doc_right = self.doc_right.clone();
1280 cursor.named_sets = self.named_sets.clone();
1281 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1282 cursor.flow_anchor_left = self.flow_anchor_left;
1283 cursor.fixed_header = self.fixed_header.clone();
1284 cursor.fixed_footer = self.fixed_footer.clone();
1285 cursor.watermarks = self.watermarks.clone();
1286 cursor.continuation_top_offset = self.continuation_top_offset;
1287
1288 let header_height: f64 = cursor
1289 .fixed_header
1290 .iter()
1291 .filter(|(n, _)| cursor.fixed_applies(n))
1292 .map(|(_, h)| *h)
1293 .sum();
1294 cursor.y = header_height + cursor.continuation_top_offset;
1295
1296 cursor
1297 }
1298}
1299
1300impl Default for LayoutEngine {
1301 fn default() -> Self {
1302 Self::new()
1303 }
1304}
1305
1306impl LayoutEngine {
1307 pub fn new() -> Self {
1308 Self {
1309 text_layout: TextLayout::new(),
1310 image_dim_cache: RefCell::new(HashMap::new()),
1311 }
1312 }
1313
1314 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1316 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1317 return Some(*dims);
1318 }
1319 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1320 self.image_dim_cache
1321 .borrow_mut()
1322 .insert(src.to_string(), dims);
1323 Some(dims)
1324 } else {
1325 None
1326 }
1327 }
1328
1329 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1331 let mut pages: Vec<LayoutPage> = Vec::new();
1332 let mut cursor = match &document.first_page {
1333 Some(first) => PageCursor::new_first(first, &document.default_page),
1334 None => PageCursor::new(&document.default_page),
1335 };
1336 cursor.left_config = document.left_page.clone();
1342 cursor.right_config = document.right_page.clone();
1343 cursor.doc_base = document.default_page.clone();
1345 cursor.doc_left = document.left_page.clone();
1346 cursor.doc_right = document.right_page.clone();
1347 cursor.named_sets = document.named_pages.clone();
1348 if document.first_page.is_none() {
1349 cursor.display_config = cursor.display_for(1);
1350 }
1351
1352 let base = document.default_style.clone().unwrap_or_default();
1354 let root_style = Style {
1355 lang: base.lang.clone().or(document.metadata.lang.clone()),
1356 ..base
1357 }
1358 .resolve(None, cursor.content_width);
1359
1360 for node in &document.children {
1361 match &node.kind {
1362 NodeKind::Page { config } => {
1363 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1364 pages.push(cursor.finalize());
1365 }
1366 cursor = PageCursor::new(config);
1367 cursor.page_index = pages.len();
1368
1369 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1376 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1377
1378 let cx = cursor.content_x;
1379 let cw = cursor.content_width;
1380 self.layout_children(
1381 &node.children,
1382 &node.style,
1383 &mut cursor,
1384 &mut pages,
1385 cx,
1386 cw,
1387 Some(&page_root),
1388 font_context,
1389 );
1390 }
1391 NodeKind::PageBreak => {
1392 pages.push(cursor.finalize());
1393 cursor = cursor.new_page();
1394 }
1395 _ => {
1396 let cx = cursor.content_x;
1397 let cw = cursor.content_width;
1398 self.layout_node(
1399 node,
1400 &mut cursor,
1401 &mut pages,
1402 cx,
1403 cw,
1404 Some(&root_style),
1405 font_context,
1406 None,
1407 None,
1408 );
1409 }
1410 }
1411 }
1412
1413 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1414 pages.push(cursor.finalize());
1415 }
1416
1417 self.inject_fixed_elements(&mut pages, font_context);
1418
1419 pages
1420 }
1421
1422 #[allow(clippy::too_many_arguments)]
1423 fn layout_node(
1424 &self,
1425 node: &Node,
1426 cursor: &mut PageCursor,
1427 pages: &mut Vec<LayoutPage>,
1428 x: f64,
1429 available_width: f64,
1430 parent_style: Option<&ResolvedStyle>,
1431 font_context: &FontContext,
1432 cross_axis_height: Option<f64>,
1433 forced_outer_width: Option<f64>,
1434 ) {
1435 let mut style = node.style.resolve(parent_style, available_width);
1436
1437 if let Some(h) = cross_axis_height {
1440 if matches!(style.height, SizeConstraint::Auto) {
1441 style.height = SizeConstraint::Fixed(h);
1442 }
1443 }
1444
1445 if let Some(w) = forced_outer_width {
1450 style.width = SizeConstraint::Fixed(w);
1451 }
1452
1453 if style.break_before && !cursor.elements.is_empty() {
1464 pages.push(cursor.finalize());
1465 *cursor = cursor.new_page();
1466 }
1467
1468 let elem_start = cursor.elements.len();
1471
1472 match &node.kind {
1473 NodeKind::PageBreak => {
1474 pages.push(cursor.finalize());
1475 *cursor = cursor.new_page();
1476 }
1477
1478 NodeKind::PageName { name } => {
1479 if cursor.page_name.as_deref() != name.as_deref() {
1485 let fresh_y: f64 = cursor
1488 .fixed_header
1489 .iter()
1490 .filter(|(n, _)| cursor.fixed_applies(n))
1491 .map(|(_, h)| *h)
1492 .sum::<f64>()
1493 + cursor.continuation_top_offset;
1494 let has_content = !cursor.elements.is_empty() || cursor.y > fresh_y + 0.01;
1495 if has_content {
1496 pages.push(cursor.finalize());
1497 }
1498 *cursor = cursor.renamed_page(name.clone(), !has_content);
1499 }
1500 }
1501
1502 NodeKind::Fixed {
1503 position, pages, ..
1504 } => {
1505 let height = self.measure_node_height(node, available_width, &style, font_context);
1506 match position {
1507 FixedPosition::Header => {
1508 cursor.fixed_header.push((node.clone(), height));
1509 let _ = pages;
1513 if cursor.fixed_applies(node) {
1514 cursor.y += height;
1515 }
1516 }
1517 FixedPosition::Footer => {
1518 cursor.fixed_footer.push((node.clone(), height));
1519 }
1520 }
1521 }
1522
1523 NodeKind::Watermark { .. } => {
1524 cursor.watermarks.push(node.clone());
1526 }
1527
1528 NodeKind::TextField {
1529 name,
1530 value,
1531 placeholder,
1532 width: field_w,
1533 height: field_h,
1534 multiline,
1535 password,
1536 read_only,
1537 max_length,
1538 font_size,
1539 } => {
1540 self.layout_form_field(
1541 node,
1542 &style,
1543 cursor,
1544 pages,
1545 x,
1546 *field_w,
1547 *field_h,
1548 DrawCommand::FormField {
1549 field_type: FormFieldType::TextField {
1550 value: value.clone(),
1551 placeholder: placeholder.clone(),
1552 multiline: *multiline,
1553 password: *password,
1554 read_only: *read_only,
1555 max_length: *max_length,
1556 font_size: *font_size,
1557 },
1558 name: name.clone(),
1559 },
1560 "TextField",
1561 );
1562 }
1563
1564 NodeKind::Checkbox {
1565 name,
1566 checked,
1567 width: field_w,
1568 height: field_h,
1569 read_only,
1570 } => {
1571 self.layout_form_field(
1572 node,
1573 &style,
1574 cursor,
1575 pages,
1576 x,
1577 *field_w,
1578 *field_h,
1579 DrawCommand::FormField {
1580 field_type: FormFieldType::Checkbox {
1581 checked: *checked,
1582 read_only: *read_only,
1583 },
1584 name: name.clone(),
1585 },
1586 "Checkbox",
1587 );
1588 }
1589
1590 NodeKind::Dropdown {
1591 name,
1592 options,
1593 value,
1594 width: field_w,
1595 height: field_h,
1596 read_only,
1597 font_size,
1598 } => {
1599 self.layout_form_field(
1600 node,
1601 &style,
1602 cursor,
1603 pages,
1604 x,
1605 *field_w,
1606 *field_h,
1607 DrawCommand::FormField {
1608 field_type: FormFieldType::Dropdown {
1609 options: options.clone(),
1610 value: value.clone(),
1611 read_only: *read_only,
1612 font_size: *font_size,
1613 },
1614 name: name.clone(),
1615 },
1616 "Dropdown",
1617 );
1618 }
1619
1620 NodeKind::RadioButton {
1621 name,
1622 value,
1623 checked,
1624 width: field_w,
1625 height: field_h,
1626 read_only,
1627 } => {
1628 self.layout_form_field(
1629 node,
1630 &style,
1631 cursor,
1632 pages,
1633 x,
1634 *field_w,
1635 *field_h,
1636 DrawCommand::FormField {
1637 field_type: FormFieldType::RadioButton {
1638 value: value.clone(),
1639 checked: *checked,
1640 read_only: *read_only,
1641 },
1642 name: name.clone(),
1643 },
1644 "RadioButton",
1645 );
1646 }
1647
1648 NodeKind::Text {
1649 content,
1650 href,
1651 runs,
1652 } => {
1653 self.layout_text(
1654 content,
1655 href.as_deref(),
1656 runs,
1657 &style,
1658 cursor,
1659 pages,
1660 x,
1661 available_width,
1662 font_context,
1663 node.source_location.as_ref(),
1664 node.bookmark.as_deref(),
1665 None,
1666 );
1667 }
1668
1669 NodeKind::Heading {
1670 content,
1671 href,
1672 runs,
1673 ..
1674 } => {
1675 let heading_role = node_kind_name(&node.kind); self.layout_text(
1681 content,
1682 href.as_deref(),
1683 runs,
1684 &style,
1685 cursor,
1686 pages,
1687 x,
1688 available_width,
1689 font_context,
1690 node.source_location.as_ref(),
1691 node.bookmark.as_deref(),
1692 Some(heading_role),
1693 );
1694 }
1695
1696 NodeKind::List {
1697 ordered,
1698 marker_type,
1699 start,
1700 } => {
1701 self.layout_list(
1702 node,
1703 *ordered,
1704 *marker_type,
1705 *start,
1706 &style,
1707 cursor,
1708 pages,
1709 x,
1710 available_width,
1711 font_context,
1712 );
1713 }
1714
1715 NodeKind::ListItem => {
1716 self.layout_view(
1721 node,
1722 &style,
1723 cursor,
1724 pages,
1725 x,
1726 available_width,
1727 font_context,
1728 );
1729 }
1730
1731 NodeKind::Image { width, height, .. } => {
1732 self.layout_image(
1733 node,
1734 &style,
1735 cursor,
1736 pages,
1737 x,
1738 available_width,
1739 *width,
1740 *height,
1741 );
1742 }
1743
1744 NodeKind::Table { columns } => {
1745 self.layout_table(
1746 node,
1747 &style,
1748 columns,
1749 cursor,
1750 pages,
1751 x,
1752 available_width,
1753 font_context,
1754 );
1755 }
1756
1757 NodeKind::View | NodeKind::Page { .. } => {
1758 self.layout_view(
1759 node,
1760 &style,
1761 cursor,
1762 pages,
1763 x,
1764 available_width,
1765 font_context,
1766 );
1767 }
1768
1769 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1770 self.layout_view(
1771 node,
1772 &style,
1773 cursor,
1774 pages,
1775 x,
1776 available_width,
1777 font_context,
1778 );
1779 }
1780
1781 NodeKind::Svg {
1782 width: svg_w,
1783 height: svg_h,
1784 view_box,
1785 content,
1786 } => {
1787 self.layout_svg(
1788 node,
1789 &style,
1790 cursor,
1791 pages,
1792 x,
1793 available_width,
1794 *svg_w,
1795 *svg_h,
1796 view_box.as_deref(),
1797 content,
1798 );
1799 }
1800
1801 NodeKind::Barcode {
1802 data,
1803 format,
1804 width: explicit_width,
1805 height: bar_height,
1806 } => {
1807 self.layout_barcode(
1808 node,
1809 &style,
1810 cursor,
1811 pages,
1812 x,
1813 available_width,
1814 data,
1815 *format,
1816 *explicit_width,
1817 *bar_height,
1818 );
1819 }
1820
1821 NodeKind::QrCode {
1822 data,
1823 size: explicit_size,
1824 } => {
1825 self.layout_qrcode(
1826 node,
1827 &style,
1828 cursor,
1829 pages,
1830 x,
1831 available_width,
1832 data,
1833 *explicit_size,
1834 );
1835 }
1836
1837 NodeKind::Canvas {
1838 width: canvas_w,
1839 height: canvas_h,
1840 operations,
1841 } => {
1842 self.layout_canvas(
1843 node,
1844 &style,
1845 cursor,
1846 pages,
1847 x,
1848 available_width,
1849 *canvas_w,
1850 *canvas_h,
1851 operations,
1852 );
1853 }
1854
1855 NodeKind::BarChart {
1856 data,
1857 width: chart_w,
1858 height: chart_h,
1859 color,
1860 show_labels,
1861 show_values,
1862 show_grid,
1863 title,
1864 } => {
1865 let config = crate::chart::bar::BarChartConfig {
1866 color: color.clone(),
1867 show_labels: *show_labels,
1868 show_values: *show_values,
1869 show_grid: *show_grid,
1870 title: title.clone(),
1871 };
1872 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1873 self.layout_chart(
1874 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1875 );
1876 }
1877
1878 NodeKind::LineChart {
1879 series,
1880 labels,
1881 width: chart_w,
1882 height: chart_h,
1883 show_points,
1884 show_grid,
1885 title,
1886 } => {
1887 let config = crate::chart::line::LineChartConfig {
1888 show_points: *show_points,
1889 show_grid: *show_grid,
1890 title: title.clone(),
1891 };
1892 let primitives =
1893 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1894 self.layout_chart(
1895 node,
1896 &style,
1897 cursor,
1898 pages,
1899 x,
1900 *chart_w,
1901 *chart_h,
1902 primitives,
1903 "LineChart",
1904 );
1905 }
1906
1907 NodeKind::PieChart {
1908 data,
1909 width: chart_w,
1910 height: chart_h,
1911 donut,
1912 show_legend,
1913 title,
1914 } => {
1915 let config = crate::chart::pie::PieChartConfig {
1916 donut: *donut,
1917 show_legend: *show_legend,
1918 title: title.clone(),
1919 };
1920 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1921 self.layout_chart(
1922 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1923 );
1924 }
1925
1926 NodeKind::AreaChart {
1927 series,
1928 labels,
1929 width: chart_w,
1930 height: chart_h,
1931 show_grid,
1932 title,
1933 } => {
1934 let config = crate::chart::area::AreaChartConfig {
1935 show_grid: *show_grid,
1936 title: title.clone(),
1937 };
1938 let primitives =
1939 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1940 self.layout_chart(
1941 node,
1942 &style,
1943 cursor,
1944 pages,
1945 x,
1946 *chart_w,
1947 *chart_h,
1948 primitives,
1949 "AreaChart",
1950 );
1951 }
1952
1953 NodeKind::DotPlot {
1954 groups,
1955 width: chart_w,
1956 height: chart_h,
1957 x_min,
1958 x_max,
1959 y_min,
1960 y_max,
1961 x_label,
1962 y_label,
1963 show_legend,
1964 dot_size,
1965 } => {
1966 let config = crate::chart::dot::DotPlotConfig {
1967 x_min: *x_min,
1968 x_max: *x_max,
1969 y_min: *y_min,
1970 y_max: *y_max,
1971 x_label: x_label.clone(),
1972 y_label: y_label.clone(),
1973 show_legend: *show_legend,
1974 dot_size: *dot_size,
1975 };
1976 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
1977 self.layout_chart(
1978 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
1979 );
1980 }
1981 }
1982
1983 if matches!(style.position, Position::Relative)
1991 && (style.top.is_some()
1992 || style.left.is_some()
1993 || style.right.is_some()
1994 || style.bottom.is_some())
1995 {
1996 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
1997 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
1998 for el in &mut cursor.elements[elem_start..] {
1999 if dx != 0.0 {
2000 offset_element_x(el, dx);
2001 }
2002 if dy != 0.0 {
2003 offset_element_y(el, dy);
2004 }
2005 }
2006 }
2007 }
2008
2009 #[allow(clippy::too_many_arguments)]
2010 fn layout_view(
2011 &self,
2012 node: &Node,
2013 style: &ResolvedStyle,
2014 cursor: &mut PageCursor,
2015 pages: &mut Vec<LayoutPage>,
2016 x: f64,
2017 available_width: f64,
2018 font_context: &FontContext,
2019 ) {
2020 let padding = &style.padding;
2021 let margin = &style.margin.to_edges();
2022 let border = &style.border_width;
2023
2024 let outer_width = match style.width {
2025 SizeConstraint::Fixed(w) => w,
2026 SizeConstraint::Auto => available_width - margin.horizontal(),
2027 }
2028 .min(style.max_width)
2030 .max(style.min_width);
2031 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2032
2033 let children_height =
2034 self.measure_children_height(&node.children, inner_width, style, font_context);
2035 let total_height = match style.height {
2036 SizeConstraint::Fixed(h) => h,
2037 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2038 }
2039 .max(style.min_height);
2040
2041 let node_x = x + margin.left;
2042
2043 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2044
2045 if fits || !style.breakable {
2046 if !fits && !style.breakable {
2047 pages.push(cursor.finalize());
2048 *cursor = cursor.new_page();
2049 }
2050
2051 let rect_y = cursor.content_y + cursor.y + margin.top;
2053 let snapshot = cursor.elements.len();
2054
2055 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2060 cursor.elements.push(marker);
2061 }
2062
2063 let saved_y = cursor.y;
2064 cursor.y += margin.top + padding.top + border.top;
2065
2066 let children_x = node_x + padding.left + border.left;
2067 let is_grid =
2068 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2069 if is_grid {
2070 self.layout_grid_children(
2071 &node.children,
2072 style,
2073 cursor,
2074 pages,
2075 children_x,
2076 inner_width,
2077 font_context,
2078 );
2079 } else {
2080 self.layout_children(
2081 &node.children,
2082 &node.style,
2083 cursor,
2084 pages,
2085 children_x,
2086 inner_width,
2087 Some(style),
2088 font_context,
2089 );
2090 }
2091
2092 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2094
2095 let rect_element = LayoutElement {
2096 x: node_x,
2097 y: rect_y,
2098 width: outer_width,
2099 height: total_height,
2100 draw: DrawCommand::Rect {
2101 background: style.background_color,
2102 border_width: style.border_width,
2103 border_color: style.border_color,
2104 border_style: style.border_style,
2105 border_radius: style.border_radius,
2106 opacity: 1.0,
2107 box_shadow: style.box_shadow.map(Box::new),
2108 background_gradient: style.background.clone().map(Box::new),
2109 },
2110 children: child_elements,
2111 node_type: Some(node_kind_name(&node.kind).to_string()),
2112 resolved_style: Some(style.clone()),
2113 source_location: node.source_location.clone(),
2114 href: node.href.clone(),
2115 bookmark: None,
2118 alt: None,
2119 is_header_row: false,
2120 col_span: 1,
2121 overflow: style.overflow,
2122 opacity: style.opacity,
2123 };
2124 cursor.elements.push(rect_element);
2125
2126 cursor.y = saved_y + total_height + margin.vertical();
2127 } else {
2128 self.layout_breakable_view(
2129 node,
2130 style,
2131 cursor,
2132 pages,
2133 node_x,
2134 outer_width,
2135 inner_width,
2136 font_context,
2137 );
2138 }
2139 }
2140
2141 #[allow(clippy::too_many_arguments)]
2142 fn layout_breakable_view(
2143 &self,
2144 node: &Node,
2145 style: &ResolvedStyle,
2146 cursor: &mut PageCursor,
2147 pages: &mut Vec<LayoutPage>,
2148 node_x: f64,
2149 outer_width: f64,
2150 inner_width: f64,
2151 font_context: &FontContext,
2152 ) {
2153 let padding = &style.padding;
2154 let border = &style.border_width;
2155 let margin = &style.margin.to_edges();
2156
2157 let initial_page_count = pages.len();
2159 let snapshot = cursor.elements.len();
2160 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2161
2162 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2170 cursor.elements.push(marker);
2171 }
2172
2173 cursor.y += margin.top + padding.top + border.top;
2174 let prev_continuation_offset = cursor.continuation_top_offset;
2175 cursor.continuation_top_offset = padding.top + border.top;
2176
2177 let children_x = node_x + padding.left + border.left;
2178 let is_grid =
2179 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2180 if is_grid {
2181 self.layout_grid_children(
2182 &node.children,
2183 style,
2184 cursor,
2185 pages,
2186 children_x,
2187 inner_width,
2188 font_context,
2189 );
2190 } else {
2191 self.layout_children(
2192 &node.children,
2193 &node.style,
2194 cursor,
2195 pages,
2196 children_x,
2197 inner_width,
2198 Some(style),
2199 font_context,
2200 );
2201 }
2202
2203 cursor.continuation_top_offset = prev_continuation_offset;
2204
2205 let has_visual = style.background_color.is_some()
2207 || style.border_width.top > 0.0
2208 || style.border_width.right > 0.0
2209 || style.border_width.bottom > 0.0
2210 || style.border_width.left > 0.0;
2211 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2213
2214 if !needs_wrapper {
2215 cursor.y += padding.bottom + border.bottom + margin.bottom;
2217 return;
2218 }
2219
2220 let draw_cmd = DrawCommand::Rect {
2221 background: style.background_color,
2222 border_width: style.border_width,
2223 border_color: style.border_color,
2224 border_style: style.border_style,
2225 border_radius: style.border_radius,
2226 opacity: 1.0,
2227 box_shadow: style.box_shadow.map(Box::new),
2228 background_gradient: style.background.clone().map(Box::new),
2229 };
2230
2231 if pages.len() == initial_page_count {
2232 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2234 let rect_height =
2235 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2236 cursor.elements.push(LayoutElement {
2237 x: node_x,
2238 y: rect_start_y,
2239 width: outer_width,
2240 height: rect_height,
2241 draw: draw_cmd,
2242 children: child_elements,
2243 node_type: Some(node_kind_name(&node.kind).to_string()),
2244 resolved_style: Some(style.clone()),
2245 source_location: node.source_location.clone(),
2246 href: node.href.clone(),
2247 bookmark: None,
2252 alt: None,
2253 is_header_row: false,
2254 col_span: 1,
2255 overflow: style.overflow,
2256 opacity: style.opacity,
2257 });
2258 } else {
2259 let page = &mut pages[initial_page_count];
2263 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2264 let page_content_bottom =
2265 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2266 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
2267 if !our_elements.is_empty() {
2268 let rect_height = page_content_bottom - rect_start_y;
2269 page.elements.push(LayoutElement {
2270 x: node_x,
2271 y: rect_start_y,
2272 width: outer_width,
2273 height: rect_height,
2274 draw: draw_cmd.clone(),
2275 children: our_elements,
2276 node_type: Some(node_kind_name(&node.kind).to_string()),
2277 resolved_style: Some(style.clone()),
2278 source_location: node.source_location.clone(),
2279 href: node.href.clone(),
2280 bookmark: None,
2282 alt: None,
2283 is_header_row: false,
2284 col_span: 1,
2285 overflow: Overflow::default(),
2286 opacity: 1.0,
2287 });
2288 }
2289
2290 for page in &mut pages[initial_page_count + 1..] {
2292 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2293 let content_top = page.config.margin.top + header_h;
2294 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2295 let content_bottom = page.config.margin.top
2296 + (page.height - page.config.margin.vertical())
2297 - footer_h;
2298 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2299 if !all_elements.is_empty() {
2300 page.elements.push(LayoutElement {
2301 x: node_x,
2302 y: content_top,
2303 width: outer_width,
2304 height: content_bottom - content_top,
2305 draw: draw_cmd.clone(),
2306 children: all_elements,
2307 node_type: Some(node_kind_name(&node.kind).to_string()),
2308 resolved_style: Some(style.clone()),
2309 source_location: node.source_location.clone(),
2310 href: None,
2311 bookmark: None,
2312 alt: None,
2313 is_header_row: false,
2314 col_span: 1,
2315 overflow: Overflow::default(),
2316 opacity: 1.0,
2317 });
2318 }
2319 }
2320
2321 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2323 if !all_elements.is_empty() {
2324 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2325 let content_top = cursor.content_y + header_h;
2326 let rect_height =
2327 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2328 cursor.elements.push(LayoutElement {
2329 x: node_x,
2330 y: content_top,
2331 width: outer_width,
2332 height: rect_height,
2333 draw: draw_cmd,
2334 children: all_elements,
2335 node_type: Some(node_kind_name(&node.kind).to_string()),
2336 resolved_style: Some(style.clone()),
2337 source_location: node.source_location.clone(),
2338 href: None,
2339 bookmark: None,
2340 alt: None,
2341 is_header_row: false,
2342 col_span: 1,
2343 overflow: Overflow::default(),
2344 opacity: 1.0,
2345 });
2346 }
2347 }
2348
2349 cursor.y += padding.bottom + border.bottom + margin.bottom;
2350 }
2351
2352 #[allow(clippy::too_many_arguments)]
2353 fn layout_children(
2354 &self,
2355 children: &[Node],
2356 _parent_raw_style: &Style,
2357 cursor: &mut PageCursor,
2358 pages: &mut Vec<LayoutPage>,
2359 content_x: f64,
2360 available_width: f64,
2361 parent_style: Option<&ResolvedStyle>,
2362 font_context: &FontContext,
2363 ) {
2364 let parent_box_y = cursor.content_y + cursor.y;
2366 let parent_box_x = content_x;
2367
2368 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2374 let saved_cb = cursor.containing_block;
2375 if parent_positioned {
2376 let cb_height = parent_style
2377 .and_then(|ps| match ps.height {
2378 SizeConstraint::Fixed(h) => {
2379 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2380 }
2381 SizeConstraint::Auto => None,
2382 })
2383 .unwrap_or(saved_cb.3);
2384 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2385 }
2386
2387 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2389 .iter()
2390 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2391
2392 let direction = parent_style
2393 .map(|s| s.flex_direction)
2394 .unwrap_or(FlexDirection::Column);
2395
2396 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2397 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2398
2399 match direction {
2401 FlexDirection::Column | FlexDirection::ColumnReverse => {
2402 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2403 flow_children.into_iter().rev().collect()
2404 } else {
2405 flow_children
2406 };
2407
2408 let justify = parent_style
2409 .map(|s| s.justify_content)
2410 .unwrap_or(JustifyContent::FlexStart);
2411 let align = parent_style
2412 .map(|s| s.align_items)
2413 .unwrap_or(AlignItems::Stretch);
2414
2415 let start_y = cursor.y;
2416 let initial_pages = pages.len();
2417
2418 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2420
2421 for (i, child) in items.iter().enumerate() {
2422 if i > 0 {
2423 cursor.y += row_gap;
2424 }
2425 let child_start = cursor.elements.len();
2426
2427 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2430 let has_auto_h = child_margin.has_auto_horizontal();
2431
2432 let (child_x, layout_w) = if has_auto_h {
2439 let child_style = child.style.resolve(parent_style, available_width);
2440 let has_explicit_width =
2441 matches!(child_style.width, SizeConstraint::Fixed(_));
2442 let intrinsic = self
2443 .measure_intrinsic_width(child, &child_style, font_context)
2444 .min(available_width);
2445 let w = match child_style.width {
2446 SizeConstraint::Fixed(fw) => fw,
2447 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2452 (available_width - child_margin.horizontal())
2453 .min(child_style.max_width)
2454 }
2455 SizeConstraint::Auto => intrinsic,
2456 }
2457 .min(child_style.max_width)
2458 .max(child_style.min_width);
2459 let lw = if has_explicit_width {
2460 available_width
2461 } else {
2462 w
2463 };
2464 let fixed_h = child_margin.horizontal();
2465 let slack = (available_width - w - fixed_h).max(0.0);
2466 let auto_left = child_margin.left.is_auto();
2467 let auto_right = child_margin.right.is_auto();
2468 let ml = match (auto_left, auto_right) {
2469 (true, true) => slack / 2.0,
2470 (true, false) => slack,
2471 (false, true) => 0.0,
2472 (false, false) => 0.0,
2473 };
2474 (content_x + child_margin.left.resolve() + ml, lw)
2475 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2476 let child_style = child.style.resolve(parent_style, available_width);
2477 let has_explicit_width =
2478 matches!(child_style.width, SizeConstraint::Fixed(_));
2479 let intrinsic = self
2480 .measure_intrinsic_width(child, &child_style, font_context)
2481 .min(available_width);
2482 let w = match child_style.width {
2483 SizeConstraint::Fixed(fw) => fw,
2484 SizeConstraint::Auto => intrinsic,
2485 };
2486 let lw = if has_explicit_width {
2487 available_width
2488 } else {
2489 w
2490 };
2491 match align {
2492 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2493 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2494 _ => (content_x, available_width),
2495 }
2496 } else {
2497 (content_x, available_width)
2498 };
2499
2500 self.layout_node(
2501 child,
2502 cursor,
2503 pages,
2504 child_x,
2505 layout_w,
2506 parent_style,
2507 font_context,
2508 None,
2509 None,
2510 );
2511
2512 child_ranges.push((child_start, cursor.elements.len()));
2513 }
2514
2515 let container_inner_h: Option<f64> = parent_style
2518 .and_then(|ps| match ps.height {
2519 SizeConstraint::Fixed(h) => {
2520 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2521 }
2522 SizeConstraint::Auto => None,
2523 })
2524 .or_else(|| {
2525 if parent_style.is_none() {
2527 Some(cursor.content_height - start_y)
2528 } else {
2529 None
2530 }
2531 });
2532
2533 if let Some(inner_h) = container_inner_h {
2534 if pages.len() == initial_pages {
2535 let child_styles: Vec<ResolvedStyle> = items
2536 .iter()
2537 .map(|child| child.style.resolve(parent_style, available_width))
2538 .collect();
2539 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2540 if total_grow > 0.0 {
2541 let children_total = cursor.y - start_y;
2542 let slack = (inner_h - children_total).max(0.0);
2543 if slack > 0.0 {
2544 let mut cumulative_shift = 0.0_f64;
2545 for (i, cs) in child_styles.iter().enumerate() {
2546 let (start, end) = child_ranges[i];
2547 if cumulative_shift > 0.001 {
2548 for j in start..end {
2549 offset_element_y(
2550 &mut cursor.elements[j],
2551 cumulative_shift,
2552 );
2553 }
2554 }
2555 if cs.flex_grow > 0.0 {
2556 let extra = slack * (cs.flex_grow / total_grow);
2557 if start < end {
2559 let elem = &mut cursor.elements[end - 1];
2560 elem.height += extra;
2561 reapply_justify_content(elem);
2562 }
2563 cumulative_shift += extra;
2564 }
2565 }
2566 cursor.y += cumulative_shift;
2567 }
2568 }
2569 }
2570 }
2571
2572 if let Some(inner_h) = container_inner_h {
2579 if pages.len() == initial_pages {
2580 let auto_styles: Vec<ResolvedStyle> = items
2581 .iter()
2582 .map(|child| child.style.resolve(parent_style, available_width))
2583 .collect();
2584 let total_autos: usize = auto_styles
2585 .iter()
2586 .map(|s| {
2587 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2588 })
2589 .sum();
2590 if total_autos > 0 {
2591 let children_total = cursor.y - start_y;
2592 let total_slack = (inner_h - children_total).max(0.0);
2593 if total_slack > 0.0 {
2594 let per_auto = total_slack / total_autos as f64;
2595 let mut cumulative_shift = 0.0_f64;
2596 for (i, cs) in auto_styles.iter().enumerate() {
2597 let (start, end) = child_ranges[i];
2598 let mt_auto = cs.margin.top.is_auto();
2599 let mb_auto = cs.margin.bottom.is_auto();
2600 let this_child_shift =
2604 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2605 if this_child_shift > 0.001 {
2606 for j in start..end {
2607 offset_element_y(
2608 &mut cursor.elements[j],
2609 this_child_shift,
2610 );
2611 }
2612 }
2613 cumulative_shift =
2616 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2617 }
2618 cursor.y += cumulative_shift;
2619 }
2620 }
2621 }
2622 }
2623
2624 let needs_justify =
2626 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2627 if needs_justify {
2628 let justify_inner_h = container_inner_h.or_else(|| {
2630 parent_style.and_then(|ps| match ps.height {
2631 SizeConstraint::Fixed(h) => {
2632 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2633 }
2634 SizeConstraint::Auto => None,
2635 })
2636 });
2637 if let Some(inner_h) = justify_inner_h {
2638 let children_total = cursor.y - start_y;
2639 let slack = inner_h - children_total;
2640 if slack > 0.0 {
2641 let n = child_ranges.len();
2642 let offsets: Vec<f64> = match justify {
2643 JustifyContent::FlexEnd => vec![slack; n],
2644 JustifyContent::Center => vec![slack / 2.0; n],
2645 JustifyContent::SpaceBetween => {
2646 if n <= 1 {
2647 vec![0.0; n]
2648 } else {
2649 let per_gap = slack / (n - 1) as f64;
2650 (0..n).map(|i| i as f64 * per_gap).collect()
2651 }
2652 }
2653 JustifyContent::SpaceAround => {
2654 let space = slack / n as f64;
2655 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2656 }
2657 JustifyContent::SpaceEvenly => {
2658 let space = slack / (n + 1) as f64;
2659 (0..n).map(|i| (i + 1) as f64 * space).collect()
2660 }
2661 JustifyContent::FlexStart => vec![0.0; n],
2662 };
2663 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2664 let dy = offsets[i];
2665 if dy.abs() > 0.001 {
2666 for j in start..end {
2667 offset_element_y(&mut cursor.elements[j], dy);
2668 }
2669 }
2670 }
2671 cursor.y += *offsets.last().unwrap_or(&0.0);
2672 }
2673 }
2674 }
2675 }
2676
2677 FlexDirection::Row | FlexDirection::RowReverse => {
2678 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2679 self.layout_flex_row(
2680 &flow_owned,
2681 cursor,
2682 pages,
2683 content_x,
2684 available_width,
2685 parent_style,
2686 column_gap,
2687 row_gap,
2688 font_context,
2689 );
2690 }
2691 }
2692
2693 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2699 let ph = parent_style
2700 .and_then(|ps| match ps.height {
2701 SizeConstraint::Fixed(h) => {
2702 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2703 }
2704 SizeConstraint::Auto => None,
2705 })
2706 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
2707 (parent_box_x, parent_box_y, available_width, ph)
2708 } else {
2709 cursor.containing_block
2710 };
2711
2712 for abs_child in &abs_children {
2714 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2715
2716 let child_width = match abs_style.width {
2718 SizeConstraint::Fixed(w) => w,
2719 SizeConstraint::Auto => {
2720 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2722 (cb_w - l - r).max(0.0)
2723 } else {
2724 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2725 }
2726 }
2727 };
2728
2729 let child_height = match abs_style.height {
2730 SizeConstraint::Fixed(h) => h,
2731 SizeConstraint::Auto => {
2732 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2733 }
2734 };
2735
2736 let abs_x = if let Some(l) = abs_style.left {
2738 cb_x + l
2739 } else if let Some(r) = abs_style.right {
2740 cb_x + cb_w - r - child_width
2741 } else {
2742 cb_x
2743 };
2744
2745 let abs_y = if let Some(t) = abs_style.top {
2746 cb_y + t
2747 } else if let Some(b) = abs_style.bottom {
2748 cb_y + cb_h - b - child_height
2749 } else {
2750 cb_y
2751 };
2752
2753 let mut abs_cursor = PageCursor::new(&cursor.config);
2755 abs_cursor.y = 0.0;
2756 abs_cursor.content_x = abs_x;
2757 abs_cursor.content_y = abs_y;
2758
2759 self.layout_node(
2760 abs_child,
2761 &mut abs_cursor,
2762 &mut Vec::new(),
2763 abs_x,
2764 child_width,
2765 parent_style,
2766 font_context,
2767 None,
2768 None,
2769 );
2770
2771 cursor.elements.extend(abs_cursor.elements);
2773 }
2774
2775 cursor.containing_block = saved_cb;
2777 }
2778
2779 #[allow(clippy::too_many_arguments)]
2780 fn layout_flex_row(
2781 &self,
2782 children: &[Node],
2783 cursor: &mut PageCursor,
2784 pages: &mut Vec<LayoutPage>,
2785 content_x: f64,
2786 available_width: f64,
2787 parent_style: Option<&ResolvedStyle>,
2788 column_gap: f64,
2789 row_gap: f64,
2790 font_context: &FontContext,
2791 ) {
2792 if children.is_empty() {
2793 return;
2794 }
2795
2796 let flex_wrap = parent_style
2797 .map(|s| s.flex_wrap)
2798 .unwrap_or(FlexWrap::NoWrap);
2799
2800 let items: Vec<FlexItem> = children
2803 .iter()
2804 .map(|child| {
2805 let style = child.style.resolve(parent_style, available_width);
2806 let base_width = match style.flex_basis {
2807 SizeConstraint::Fixed(w) => w,
2808 SizeConstraint::Auto => match style.width {
2809 SizeConstraint::Fixed(w) => w,
2810 SizeConstraint::Auto => {
2811 self.measure_intrinsic_width(child, &style, font_context)
2812 }
2813 },
2814 };
2815 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2816 FlexItem {
2817 node: child,
2818 style,
2819 base_width,
2820 min_content_width,
2821 }
2822 })
2823 .collect();
2824
2825 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2827 let lines = match flex_wrap {
2828 FlexWrap::NoWrap => {
2829 vec![flex::WrapLine {
2830 start: 0,
2831 end: items.len(),
2832 }]
2833 }
2834 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2835 FlexWrap::WrapReverse => {
2836 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2837 l.reverse();
2838 l
2839 }
2840 };
2841
2842 if lines.is_empty() {
2843 return;
2844 }
2845
2846 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2848
2849 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2851
2852 let initial_pages_count = pages.len();
2853 let flex_start_y = cursor.y;
2854 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2855
2856 for (line_idx, line) in lines.iter().enumerate() {
2857 let line_items = &items[line.start..line.end];
2858 let line_count = line.end - line.start;
2859 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
2860 let distributable = available_width - line_gap;
2861
2862 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
2864 let remaining = distributable - total_base;
2865
2866 if remaining > 0.0 {
2867 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
2868 if total_grow > 0.0 {
2869 for (j, item) in line_items.iter().enumerate() {
2870 final_widths[line.start + j] =
2871 item.base_width + remaining * (item.style.flex_grow / total_grow);
2872 }
2873 }
2874 } else if remaining < 0.0 {
2875 let total_shrink: f64 = line_items
2876 .iter()
2877 .map(|i| i.style.flex_shrink * i.base_width)
2878 .sum();
2879 if total_shrink > 0.0 {
2880 for (j, item) in line_items.iter().enumerate() {
2881 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
2882 let w = item.base_width + remaining * factor;
2883 let floor = item.style.min_width.max(item.min_content_width);
2884 final_widths[line.start + j] = w.max(floor);
2885 }
2886 }
2887 }
2888
2889 let line_height: f64 = line_items
2891 .iter()
2892 .enumerate()
2893 .map(|(j, item)| {
2894 let fw = final_widths[line.start + j];
2895 self.measure_node_height(item.node, fw, &item.style, font_context)
2896 + item.style.margin.vertical()
2897 })
2898 .fold(0.0f64, f64::max);
2899
2900 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
2907 pages.push(cursor.finalize());
2908 *cursor = cursor.new_page();
2909 }
2910
2911 if line_idx > 0 {
2913 cursor.y += row_gap;
2914 }
2915
2916 let row_start_y = cursor.y;
2917
2918 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
2920 let slack = available_width - actual_total - line_gap;
2921
2922 let (start_offset, between_extra) = match justify {
2923 JustifyContent::FlexStart => (0.0, 0.0),
2924 JustifyContent::FlexEnd => (slack, 0.0),
2925 JustifyContent::Center => (slack / 2.0, 0.0),
2926 JustifyContent::SpaceBetween => {
2927 if line_count > 1 {
2928 (0.0, slack / (line_count as f64 - 1.0))
2929 } else {
2930 (0.0, 0.0)
2931 }
2932 }
2933 JustifyContent::SpaceAround => {
2934 let s = slack / line_count as f64;
2935 (s / 2.0, s)
2936 }
2937 JustifyContent::SpaceEvenly => {
2938 let s = slack / (line_count as f64 + 1.0);
2939 (s, s)
2940 }
2941 };
2942
2943 let line_elem_start = cursor.elements.len();
2944 let mut x = content_x + start_offset;
2945
2946 for (j, item) in line_items.iter().enumerate() {
2947 if j > 0 {
2948 x += column_gap + between_extra;
2949 }
2950
2951 let fw = final_widths[line.start + j];
2952
2953 let align = item
2954 .style
2955 .align_self
2956 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
2957
2958 let item_height =
2959 self.measure_node_height(item.node, fw, &item.style, font_context);
2960
2961 let has_auto_v = item.style.margin.has_auto_vertical();
2963 let y_offset = if has_auto_v {
2964 let fixed_v = item.style.margin.vertical();
2965 let slack = (line_height - item_height - fixed_v).max(0.0);
2966 let auto_top = item.style.margin.top.is_auto();
2967 let auto_bottom = item.style.margin.bottom.is_auto();
2968 match (auto_top, auto_bottom) {
2969 (true, true) => slack / 2.0,
2970 (true, false) => slack,
2971 (false, true) => 0.0,
2972 (false, false) => 0.0,
2973 }
2974 } else {
2975 match align {
2976 AlignItems::FlexStart => 0.0,
2977 AlignItems::FlexEnd => {
2978 line_height - item_height - item.style.margin.vertical()
2979 }
2980 AlignItems::Center => {
2981 (line_height - item_height - item.style.margin.vertical()) / 2.0
2982 }
2983 AlignItems::Stretch => 0.0,
2984 AlignItems::Baseline => 0.0,
2985 }
2986 };
2987
2988 let cross_h = if matches!(align, AlignItems::Stretch)
2992 && matches!(item.style.height, SizeConstraint::Auto)
2993 && !has_auto_v
2994 {
2995 let stretch_h = line_height - item.style.margin.vertical();
2996 if stretch_h > item_height {
2997 Some(stretch_h)
2998 } else {
2999 None
3000 }
3001 } else {
3002 None
3003 };
3004
3005 let saved_y = cursor.y;
3006 cursor.y = row_start_y + y_offset;
3007
3008 self.layout_node(
3009 item.node,
3010 cursor,
3011 pages,
3012 x,
3013 available_width,
3014 parent_style,
3015 font_context,
3016 cross_h,
3017 Some(fw),
3018 );
3019
3020 cursor.y = saved_y;
3021 x += fw;
3022 }
3023
3024 cursor.y = row_start_y + line_height;
3025 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3026 }
3027
3028 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3030 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3031 if !matches!(align_content, AlignContent::FlexStart)
3032 && !matches!(flex_wrap, FlexWrap::NoWrap)
3033 {
3034 if let Some(parent) = parent_style {
3035 if let SizeConstraint::Fixed(container_h) = parent.height {
3036 let inner_h = container_h
3037 - parent.padding.vertical()
3038 - parent.border_width.vertical();
3039 let total_used = cursor.y - flex_start_y;
3040 let slack = inner_h - total_used;
3041 if slack > 0.0 {
3042 let n = line_infos.len();
3043 let offsets: Vec<f64> = match align_content {
3044 AlignContent::FlexEnd => vec![slack; n],
3045 AlignContent::Center => vec![slack / 2.0; n],
3046 AlignContent::SpaceBetween => {
3047 if n <= 1 {
3048 vec![0.0; n]
3049 } else {
3050 let per_gap = slack / (n - 1) as f64;
3051 (0..n).map(|i| i as f64 * per_gap).collect()
3052 }
3053 }
3054 AlignContent::SpaceAround => {
3055 let space = slack / n as f64;
3056 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3057 }
3058 AlignContent::SpaceEvenly => {
3059 let space = slack / (n + 1) as f64;
3060 (0..n).map(|i| (i + 1) as f64 * space).collect()
3061 }
3062 AlignContent::Stretch => {
3063 let extra = slack / n as f64;
3064 (0..n).map(|i| i as f64 * extra).collect()
3065 }
3066 AlignContent::FlexStart => vec![0.0; n],
3067 };
3068 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3069 let dy = offsets[i];
3070 if dy.abs() > 0.001 {
3071 for j in start..end {
3072 offset_element_y(&mut cursor.elements[j], dy);
3073 }
3074 }
3075 }
3076 cursor.y += *offsets.last().unwrap_or(&0.0);
3077 }
3078 }
3079 }
3080 }
3081 }
3082 }
3083
3084 #[allow(clippy::too_many_arguments)]
3087 fn layout_list(
3088 &self,
3089 node: &Node,
3090 ordered: bool,
3091 marker_type: ListMarkerType,
3092 start: u32,
3093 style: &ResolvedStyle,
3094 cursor: &mut PageCursor,
3095 pages: &mut Vec<LayoutPage>,
3096 x: f64,
3097 available_width: f64,
3098 font_context: &FontContext,
3099 ) {
3100 let margin = &style.margin.to_edges();
3101 let padding = &style.padding;
3102
3103 cursor.y += margin.top;
3104
3105 let list_x = x + margin.left;
3106 let outer_width = available_width - margin.horizontal();
3107 let inner_width = outer_width - padding.horizontal();
3108
3109 let n_items = node
3112 .children
3113 .iter()
3114 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3115 .count() as u32;
3116
3117 let marker_gutter =
3118 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3119
3120 let list_inner_x = list_x + padding.left;
3121 let content_x = list_inner_x + marker_gutter;
3122 let content_width = (inner_width - marker_gutter).max(0.0);
3123
3124 let snapshot = cursor.elements.len();
3127 let list_start_y = cursor.content_y + cursor.y;
3128 cursor.y += padding.top;
3129
3130 let mut item_index: u32 = 0;
3131 for child in &node.children {
3132 if !matches!(child.kind, NodeKind::ListItem) {
3133 continue;
3134 }
3135 let marker_idx = start + item_index;
3136 self.layout_list_item(
3137 child,
3138 marker_idx,
3139 ordered,
3140 marker_type,
3141 marker_gutter,
3142 style,
3143 cursor,
3144 pages,
3145 list_inner_x,
3146 content_x,
3147 content_width,
3148 font_context,
3149 );
3150 item_index += 1;
3151 }
3152
3153 cursor.y += padding.bottom;
3154
3155 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3157 let list_height = cursor.content_y + cursor.y - list_start_y;
3158 cursor.elements.push(LayoutElement {
3159 x: list_x,
3160 y: list_start_y,
3161 width: outer_width,
3162 height: list_height,
3163 draw: DrawCommand::None,
3164 children: item_elements,
3165 node_type: Some("List".to_string()),
3166 resolved_style: Some(style.clone()),
3167 source_location: node.source_location.clone(),
3168 href: None,
3169 bookmark: node.bookmark.clone(),
3170 alt: None,
3171 is_header_row: false,
3172 col_span: 1,
3173 overflow: style.overflow,
3174 opacity: style.opacity,
3175 });
3176
3177 cursor.y += margin.bottom;
3178 }
3179
3180 #[allow(clippy::too_many_arguments)]
3181 fn layout_list_item(
3182 &self,
3183 item: &Node,
3184 marker_idx: u32,
3185 ordered: bool,
3186 marker_type: ListMarkerType,
3187 marker_gutter: f64,
3188 parent_style: &ResolvedStyle,
3189 cursor: &mut PageCursor,
3190 pages: &mut Vec<LayoutPage>,
3191 list_inner_x: f64,
3192 content_x: f64,
3193 content_width: f64,
3194 font_context: &FontContext,
3195 ) {
3196 let item_style = item.style.resolve(Some(parent_style), content_width);
3197 let item_margin = item_style.margin.to_edges();
3198
3199 cursor.y += item_margin.top;
3200 let item_start_y = cursor.content_y + cursor.y;
3201 let item_snapshot = cursor.elements.len();
3202
3203 let marker_str = format_marker(marker_idx, ordered, marker_type);
3207 if !marker_str.is_empty() {
3208 let saved_y = cursor.y;
3209 self.layout_text(
3210 &marker_str,
3211 None,
3212 &[],
3213 &item_style,
3214 cursor,
3215 pages,
3216 list_inner_x,
3217 marker_gutter,
3218 font_context,
3219 None,
3220 None,
3221 Some("Lbl"),
3222 );
3223 cursor.y = saved_y;
3224 }
3225
3226 self.layout_children(
3232 &item.children,
3233 &item.style,
3234 cursor,
3235 pages,
3236 content_x,
3237 content_width,
3238 Some(&item_style),
3239 font_context,
3240 );
3241
3242 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3245 let item_height = cursor.content_y + cursor.y - item_start_y;
3246 let item_width = content_x + content_width - list_inner_x;
3247 cursor.elements.push(LayoutElement {
3248 x: list_inner_x,
3249 y: item_start_y,
3250 width: item_width,
3251 height: item_height,
3252 draw: DrawCommand::None,
3253 children: item_children,
3254 node_type: Some("ListItem".to_string()),
3255 resolved_style: Some(item_style.clone()),
3256 source_location: item.source_location.clone(),
3257 href: None,
3258 bookmark: item.bookmark.clone(),
3259 alt: None,
3260 is_header_row: false,
3261 col_span: 1,
3262 overflow: item_style.overflow,
3263 opacity: item_style.opacity,
3264 });
3265
3266 cursor.y += item_margin.bottom;
3267 }
3268
3269 #[allow(clippy::too_many_arguments)]
3270 fn layout_table(
3271 &self,
3272 node: &Node,
3273 style: &ResolvedStyle,
3274 column_defs: &[ColumnDef],
3275 cursor: &mut PageCursor,
3276 pages: &mut Vec<LayoutPage>,
3277 x: f64,
3278 available_width: f64,
3279 font_context: &FontContext,
3280 ) {
3281 let padding = &style.padding;
3282 let margin = &style.margin.to_edges();
3283 let border = &style.border_width;
3284
3285 let table_x = x + margin.left;
3286 let table_width = match style.width {
3287 SizeConstraint::Fixed(w) => w,
3288 SizeConstraint::Auto => available_width - margin.horizontal(),
3289 };
3290 let inner_width = table_width - padding.horizontal() - border.horizontal();
3291
3292 let col_widths = self.resolve_column_widths(column_defs, inner_width, &node.children);
3293
3294 let mut header_rows: Vec<&Node> = Vec::new();
3295 let mut body_rows: Vec<&Node> = Vec::new();
3296
3297 for child in &node.children {
3298 match &child.kind {
3299 NodeKind::TableRow { is_header: true } => header_rows.push(child),
3300 _ => body_rows.push(child),
3301 }
3302 }
3303
3304 if !style.breakable {
3310 let total_height: f64 = node
3311 .children
3312 .iter()
3313 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3314 .sum::<f64>()
3315 + padding.vertical()
3316 + border.vertical();
3317 let fresh_page_available = cursor.content_height
3318 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3319 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3320 if total_height > cursor.remaining_height()
3321 && total_height <= fresh_page_available
3322 && cursor.y > 0.0
3323 {
3324 pages.push(cursor.finalize());
3325 *cursor = cursor.new_page();
3326 }
3327 }
3328
3329 let initial_page_count = pages.len();
3336 let snapshot = cursor.elements.len();
3337 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3338
3339 cursor.y += margin.top + padding.top + border.top;
3340
3341 let cell_x_start = table_x + padding.left + border.left;
3342
3343 if !header_rows.is_empty() {
3368 let total_header_h: f64 = header_rows
3369 .iter()
3370 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3371 .sum();
3372 let first_body_h = body_rows
3373 .first()
3374 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3375 .unwrap_or(0.0);
3376
3377 let needed = total_header_h + first_body_h;
3378 let fresh_page_available = cursor.content_height
3379 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3380 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3381
3382 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3383 pages.push(cursor.finalize());
3384 *cursor = cursor.new_page();
3385 cursor.y += padding.top + border.top;
3386 }
3387 }
3388
3389 for header_row in &header_rows {
3390 self.layout_table_row(
3391 header_row,
3392 &col_widths,
3393 style,
3394 cursor,
3395 cell_x_start,
3396 font_context,
3397 pages,
3398 );
3399 }
3400
3401 for body_row in &body_rows {
3402 let row_height =
3403 self.measure_table_row_height(body_row, &col_widths, style, font_context);
3404
3405 if row_height > cursor.remaining_height() {
3406 pages.push(cursor.finalize());
3407 *cursor = cursor.new_page();
3408
3409 cursor.y += padding.top + border.top;
3410 for header_row in &header_rows {
3411 self.layout_table_row(
3412 header_row,
3413 &col_widths,
3414 style,
3415 cursor,
3416 cell_x_start,
3417 font_context,
3418 pages,
3419 );
3420 }
3421 }
3422
3423 self.layout_table_row(
3424 body_row,
3425 &col_widths,
3426 style,
3427 cursor,
3428 cell_x_start,
3429 font_context,
3430 pages,
3431 );
3432 }
3433
3434 let has_visual = style.background_color.is_some()
3438 || style.background.is_some()
3439 || style.border_width.top > 0.0
3440 || style.border_width.right > 0.0
3441 || style.border_width.bottom > 0.0
3442 || style.border_width.left > 0.0;
3443 let draw_cmd = if has_visual {
3444 DrawCommand::Rect {
3445 background: style.background_color,
3446 border_width: style.border_width,
3447 border_color: style.border_color,
3448 border_style: style.border_style,
3449 border_radius: style.border_radius,
3450 opacity: 1.0,
3451 box_shadow: style.box_shadow.map(Box::new),
3452 background_gradient: style.background.clone().map(Box::new),
3453 }
3454 } else {
3455 DrawCommand::None
3456 };
3457 let make_wrapper =
3458 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3459 x: table_x,
3460 y,
3461 width: table_width,
3462 height,
3463 draw,
3464 children,
3465 node_type: Some(node_kind_name(&node.kind).to_string()),
3466 resolved_style: Some(style.clone()),
3467 source_location: node.source_location.clone(),
3468 href: node.href.clone(),
3469 bookmark: None,
3470 alt: None,
3471 is_header_row: false,
3472 col_span: 1,
3473 overflow: Overflow::default(),
3474 opacity: style.opacity,
3475 };
3476
3477 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3478
3479 if pages.len() == initial_page_count {
3480 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3482 cursor.elements.push(make_wrapper(
3483 rect_start_y,
3484 table_bottom_y - rect_start_y,
3485 child_elements,
3486 draw_cmd,
3487 ));
3488 } else {
3489 let page = &mut pages[initial_page_count];
3508 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3509 let page_content_bottom =
3510 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3511 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3512 if !our_elements.is_empty() {
3513 page.elements.push(make_wrapper(
3514 rect_start_y,
3515 page_content_bottom - rect_start_y,
3516 our_elements,
3517 draw_cmd.clone(),
3518 ));
3519 }
3520
3521 for page in &mut pages[initial_page_count + 1..] {
3523 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3524 let content_top = page.config.margin.top + header_h;
3525 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3526 let content_bottom = page.config.margin.top
3527 + (page.height - page.config.margin.vertical())
3528 - footer_h;
3529 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3530 if !all_elements.is_empty() {
3531 page.elements.push(make_wrapper(
3532 content_top,
3533 content_bottom - content_top,
3534 all_elements,
3535 draw_cmd.clone(),
3536 ));
3537 }
3538 }
3539
3540 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3542 if !all_elements.is_empty() {
3543 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3544 let content_top = cursor.content_y + header_h;
3545 cursor.elements.push(make_wrapper(
3546 content_top,
3547 table_bottom_y - content_top,
3548 all_elements,
3549 draw_cmd,
3550 ));
3551 }
3552 }
3553
3554 cursor.y += padding.bottom + border.bottom + margin.bottom;
3555 }
3556
3557 fn subtree_forces_break(node: &Node) -> bool {
3567 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
3568 return true;
3569 }
3570 if node.style.break_before == Some(true) {
3571 return true;
3572 }
3573 node.children.iter().any(Self::subtree_forces_break)
3574 }
3575
3576 #[allow(clippy::too_many_arguments)]
3577 fn layout_table_row(
3578 &self,
3579 row: &Node,
3580 col_widths: &[f64],
3581 parent_style: &ResolvedStyle,
3582 cursor: &mut PageCursor,
3583 start_x: f64,
3584 font_context: &FontContext,
3585 pages: &mut Vec<LayoutPage>,
3586 ) {
3587 let row_style = row
3588 .style
3589 .resolve(Some(parent_style), col_widths.iter().sum());
3590
3591 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
3592 let row_bl = self.row_baseline(row, &row_style, col_widths);
3593 let row_y = cursor.content_y + cursor.y;
3594 let total_width: f64 = col_widths.iter().sum();
3595
3596 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3597
3598 let row_snapshot = cursor.elements.len();
3600
3601 let row_may_break =
3613 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
3614
3615 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3616 let mut cell_x = start_x;
3617 let mut col_idx = 0usize;
3623 for cell in row.children.iter() {
3624 let span = match &cell.kind {
3625 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
3626 _ => 1,
3627 };
3628 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
3629 col_idx += span;
3630
3631 let cell_style = cell.style.resolve(Some(&row_style), col_width);
3632
3633 let cell_snapshot = cursor.elements.len();
3635
3636 let inner_width =
3637 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
3638
3639 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
3640 let saved_y = cursor.y;
3641 cursor.y += cell_style.padding.top + cell_style.border_width.top;
3642
3643 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
3647 let content_h: f64 = cell
3648 .children
3649 .iter()
3650 .map(|ch| {
3651 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
3652 self.measure_node_height(ch, inner_width, &ch_style, font_context)
3653 })
3654 .sum();
3655 let inner_row =
3656 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
3657 let slack = (inner_row - content_h).max(0.0);
3658 cursor.y += match cell_style.vertical_align {
3659 crate::style::VerticalAlign::Middle => slack / 2.0,
3660 crate::style::VerticalAlign::Bottom => slack,
3661 crate::style::VerticalAlign::Baseline => row_bl
3666 .map(|b| {
3667 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
3668 (b - d).max(0.0)
3669 })
3670 .unwrap_or(0.0),
3671 crate::style::VerticalAlign::Top => 0.0,
3672 };
3673 }
3674
3675 let cursor_before_cell = if row_may_break {
3680 Some(cursor.clone())
3681 } else {
3682 None
3683 };
3684 let mut cell_pages: Vec<LayoutPage> = Vec::new();
3685 for child in &cell.children {
3686 self.layout_node(
3687 child,
3688 cursor,
3689 &mut cell_pages,
3690 content_x,
3691 inner_width,
3692 Some(&cell_style),
3693 font_context,
3694 None,
3695 None,
3696 );
3697 }
3698
3699 if !cell_pages.is_empty() {
3701 let post_break_elements = std::mem::take(&mut cursor.elements);
3702 if let Some(last_page) = cell_pages.last_mut() {
3703 last_page.elements.extend(post_break_elements);
3704 }
3705 if !is_header {
3711 all_overflow_pages.extend(cell_pages);
3712 }
3713 *cursor = cursor_before_cell
3719 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
3720 }
3721
3722 cursor.y = saved_y;
3723
3724 let cell_children: Vec<LayoutElement> =
3726 cursor.elements.drain(cell_snapshot..).collect();
3727
3728 cursor.elements.push(LayoutElement {
3730 x: cell_x,
3731 y: row_y,
3732 width: col_width,
3733 height: row_height,
3734 draw: if cell_style.background_color.is_some()
3735 || cell_style.border_width.horizontal() > 0.0
3736 || cell_style.border_width.vertical() > 0.0
3737 {
3738 DrawCommand::Rect {
3739 background: cell_style.background_color,
3740 border_width: cell_style.border_width,
3741 border_color: cell_style.border_color,
3742 border_style: cell_style.border_style,
3743 border_radius: cell_style.border_radius,
3744 opacity: 1.0,
3745 box_shadow: cell_style.box_shadow.map(Box::new),
3746 background_gradient: cell_style.background.clone().map(Box::new),
3747 }
3748 } else {
3749 DrawCommand::None
3750 },
3751 children: cell_children,
3752 node_type: Some("TableCell".to_string()),
3753 resolved_style: Some(cell_style.clone()),
3754 source_location: cell.source_location.clone(),
3755 href: None,
3756 bookmark: cell.bookmark.clone(),
3757 alt: None,
3758 is_header_row: is_header,
3759 col_span: span as u32,
3760 overflow: Overflow::default(),
3761 opacity: 1.0,
3762 });
3763
3764 cell_x += col_width;
3765 }
3766
3767 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
3769 cursor.elements.push(LayoutElement {
3770 x: start_x,
3771 y: row_y,
3772 width: total_width,
3773 height: row_height,
3774 draw: if let Some(bg) = row_style.background_color {
3775 DrawCommand::Rect {
3776 background: Some(bg),
3777 border_width: Edges::default(),
3778 border_color: EdgeValues::uniform(Color::BLACK),
3779 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
3780 border_radius: CornerValues::uniform(0.0),
3781 opacity: 1.0,
3782 box_shadow: row_style.box_shadow.map(Box::new),
3783 background_gradient: row_style.background.clone().map(Box::new),
3784 }
3785 } else {
3786 DrawCommand::None
3787 },
3788 children: row_children,
3789 node_type: Some("TableRow".to_string()),
3790 resolved_style: Some(row_style.clone()),
3791 source_location: row.source_location.clone(),
3792 href: None,
3793 bookmark: row.bookmark.clone(),
3794 alt: None,
3795 is_header_row: is_header,
3796 col_span: 1,
3797 overflow: row_style.overflow,
3798 opacity: row_style.opacity,
3799 });
3800
3801 pages.extend(all_overflow_pages);
3803
3804 cursor.y += row_height;
3805 }
3806
3807 #[allow(clippy::too_many_arguments)]
3808 #[allow(clippy::too_many_arguments)]
3809 fn layout_text(
3810 &self,
3811 content: &str,
3812 href: Option<&str>,
3813 runs: &[TextRun],
3814 style: &ResolvedStyle,
3815 cursor: &mut PageCursor,
3816 pages: &mut Vec<LayoutPage>,
3817 x: f64,
3818 available_width: f64,
3819 font_context: &FontContext,
3820 source_location: Option<&SourceLocation>,
3821 bookmark: Option<&str>,
3822 node_type_override: Option<&str>,
3826 ) {
3827 let margin = &style.margin.to_edges();
3828 let text_x = x + margin.left;
3829 let text_width = match style.width {
3836 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
3837 SizeConstraint::Auto => available_width - margin.horizontal(),
3838 };
3839
3840 cursor.y += margin.top;
3841
3842 if !runs.is_empty() {
3844 self.layout_text_runs(
3845 runs,
3846 href,
3847 style,
3848 cursor,
3849 pages,
3850 text_x,
3851 text_width,
3852 font_context,
3853 source_location,
3854 bookmark,
3855 node_type_override,
3856 );
3857 cursor.y += margin.bottom;
3858 return;
3859 }
3860
3861 let content = substitute_page_placeholders(content);
3862 let transformed = apply_text_transform(&content, style.text_transform);
3863 let justify = matches!(style.text_align, TextAlign::Justify);
3864 let lines = match style.line_breaking {
3865 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
3866 font_context,
3867 &transformed,
3868 text_width,
3869 style.font_size,
3870 &style.font_family,
3871 style.font_weight,
3872 style.font_style,
3873 style.letter_spacing,
3874 style.hyphens,
3875 style.lang.as_deref(),
3876 justify,
3877 ),
3878 LineBreaking::Greedy => self.text_layout.break_into_lines(
3879 font_context,
3880 &transformed,
3881 text_width,
3882 style.font_size,
3883 &style.font_family,
3884 style.font_weight,
3885 style.font_style,
3886 style.letter_spacing,
3887 style.hyphens,
3888 style.lang.as_deref(),
3889 ),
3890 };
3891
3892 let lines = match style.text_overflow {
3894 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
3895 font_context,
3896 lines,
3897 text_width,
3898 style.font_size,
3899 &style.font_family,
3900 style.font_weight,
3901 style.font_style,
3902 style.letter_spacing,
3903 ),
3904 TextOverflow::Clip => self.text_layout.truncate_clip(
3905 font_context,
3906 lines,
3907 text_width,
3908 style.font_size,
3909 &style.font_family,
3910 style.font_weight,
3911 style.font_style,
3912 style.letter_spacing,
3913 ),
3914 TextOverflow::Wrap => lines,
3915 };
3916
3917 let line_height = style.font_size * style.line_height;
3918
3919 let line_heights: Vec<f64> = vec![line_height; lines.len()];
3921 let decision = page_break::decide_break(
3922 cursor.remaining_height(),
3923 &line_heights,
3924 true,
3925 style.min_orphan_lines as usize,
3926 style.min_widow_lines as usize,
3927 );
3928
3929 let mut snapshot = cursor.elements.len();
3931 let mut container_start_y = cursor.content_y + cursor.y;
3932 let mut is_first_element = true;
3933
3934 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
3936 pages.push(cursor.finalize());
3937 *cursor = cursor.new_page();
3938 snapshot = cursor.elements.len();
3939 container_start_y = cursor.content_y + cursor.y;
3940 }
3941
3942 let forced_break_at = match decision {
3944 page_break::BreakDecision::Split {
3945 items_on_current_page,
3946 } => Some(items_on_current_page),
3947 _ => None,
3948 };
3949 let mut first_break_done = false;
3950
3951 for (line_idx, line) in lines.iter().enumerate() {
3952 let needs_break = if let Some(break_at) = forced_break_at {
3954 if !first_break_done && line_idx == break_at {
3955 true
3956 } else {
3957 line_height > cursor.remaining_height()
3958 }
3959 } else {
3960 line_height > cursor.remaining_height()
3961 };
3962
3963 if needs_break {
3964 first_break_done = true;
3965 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3967 if !line_elements.is_empty() {
3968 let container_height = cursor.content_y + cursor.y - container_start_y;
3969 cursor.elements.push(LayoutElement {
3970 x: text_x,
3971 y: container_start_y,
3972 width: text_width,
3973 height: container_height,
3974 draw: DrawCommand::None,
3975 children: line_elements,
3976 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3977 resolved_style: Some(style.clone()),
3978 source_location: source_location.cloned(),
3979 href: href.map(|s| s.to_string()),
3980 bookmark: if is_first_element {
3981 bookmark.map(|s| s.to_string())
3982 } else {
3983 None
3984 },
3985 alt: None,
3986 is_header_row: false,
3987 col_span: 1,
3988 overflow: Overflow::default(),
3989 opacity: 1.0,
3990 });
3991 is_first_element = false;
3992 }
3993
3994 pages.push(cursor.finalize());
3995 *cursor = cursor.new_page();
3996
3997 snapshot = cursor.elements.len();
3999 container_start_y = cursor.content_y + cursor.y;
4000 }
4001
4002 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4003
4004 let rendered_width = if glyphs.is_empty() {
4008 line.width
4009 } else {
4010 let last = &glyphs[glyphs.len() - 1];
4011 (last.x_offset + last.x_advance).max(line.width * 0.5)
4012 };
4013
4014 let line_x = match style.text_align {
4015 TextAlign::Left => text_x,
4016 TextAlign::Right => text_x + text_width - rendered_width,
4017 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4018 TextAlign::Justify => text_x,
4019 };
4020
4021 let is_last_line = line_idx == lines.len() - 1;
4029 let user_ws = style.word_spacing;
4030 let (justified_width, word_spacing) =
4031 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4032 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4033 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4034 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4035 let s = glyphs[..=idx]
4036 .iter()
4037 .filter(|g| g.char_value == ' ')
4038 .count();
4039 (w, s)
4040 } else {
4041 (0.0, 0)
4042 };
4043 let slack = text_width - natural_width;
4044 let ws = if space_count > 0 && slack.abs() > 0.01 {
4045 slack / space_count as f64
4046 } else {
4047 0.0
4048 };
4049 (text_width, user_ws + ws)
4050 } else {
4051 (rendered_width, user_ws)
4052 };
4053
4054 let text_line = TextLine {
4055 x: line_x,
4056 y: cursor.content_y + cursor.y + style.font_size,
4057 glyphs,
4058 width: justified_width,
4059 height: line_height,
4060 word_spacing,
4061 };
4062
4063 cursor.elements.push(LayoutElement {
4064 x: line_x,
4065 y: cursor.content_y + cursor.y,
4066 width: justified_width,
4067 height: line_height,
4068 draw: DrawCommand::Text {
4069 lines: vec![text_line],
4070 color: style.color,
4071 text_decoration: style.text_decoration,
4072 opacity: 1.0,
4073 },
4074 children: vec![],
4075 node_type: Some("TextLine".to_string()),
4076 resolved_style: Some(style.clone()),
4077 source_location: None,
4078 href: href.map(|s| s.to_string()),
4079 bookmark: None,
4080 alt: None,
4081 is_header_row: false,
4082 col_span: 1,
4083 overflow: Overflow::default(),
4084 opacity: 1.0,
4085 });
4086
4087 cursor.y += line_height;
4088 }
4089
4090 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4092 if !line_elements.is_empty() {
4093 let container_height = cursor.content_y + cursor.y - container_start_y;
4094 cursor.elements.push(LayoutElement {
4095 x: text_x,
4096 y: container_start_y,
4097 width: text_width,
4098 height: container_height,
4099 draw: DrawCommand::None,
4100 children: line_elements,
4101 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4102 resolved_style: Some(style.clone()),
4103 source_location: source_location.cloned(),
4104 href: href.map(|s| s.to_string()),
4105 bookmark: if is_first_element {
4106 bookmark.map(|s| s.to_string())
4107 } else {
4108 None
4109 },
4110 alt: None,
4111 is_header_row: false,
4112 col_span: 1,
4113 overflow: Overflow::default(),
4114 opacity: 1.0,
4115 });
4116 }
4117
4118 cursor.y += margin.bottom;
4119 }
4120
4121 #[allow(clippy::too_many_arguments)]
4123 #[allow(clippy::too_many_arguments)]
4124 fn layout_text_runs(
4125 &self,
4126 runs: &[TextRun],
4127 parent_href: Option<&str>,
4128 style: &ResolvedStyle,
4129 cursor: &mut PageCursor,
4130 pages: &mut Vec<LayoutPage>,
4131 text_x: f64,
4132 text_width: f64,
4133 font_context: &FontContext,
4134 source_location: Option<&SourceLocation>,
4135 bookmark: Option<&str>,
4136 node_type_override: Option<&str>,
4138 ) {
4139 let mut styled_chars: Vec<StyledChar> = Vec::new();
4141 for run in runs {
4142 let run_style = run.style.resolve(Some(style), text_width);
4143 let run_href = run.href.as_deref().or(parent_href);
4144 let transform = run_style.text_transform;
4145 let run_content = substitute_page_placeholders(&run.content);
4146 let mut prev_is_whitespace = true;
4147 for ch in run_content.chars() {
4148 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4149 prev_is_whitespace = ch.is_whitespace();
4150 styled_chars.push(StyledChar {
4151 ch: transformed_ch,
4152 font_family: run_style.font_family.clone(),
4153 font_size: run_style.font_size,
4154 font_weight: run_style.font_weight,
4155 font_style: run_style.font_style,
4156 color: run_style.color,
4157 href: run_href.map(|s| s.to_string()),
4158 text_decoration: run_style.text_decoration,
4159 letter_spacing: run_style.letter_spacing,
4160 });
4161 }
4162 }
4163
4164 let justify = matches!(style.text_align, TextAlign::Justify);
4166 let broken_lines = match style.line_breaking {
4167 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4168 font_context,
4169 &styled_chars,
4170 text_width,
4171 style.hyphens,
4172 style.lang.as_deref(),
4173 justify,
4174 ),
4175 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4176 font_context,
4177 &styled_chars,
4178 text_width,
4179 style.hyphens,
4180 style.lang.as_deref(),
4181 ),
4182 };
4183
4184 let broken_lines = match style.text_overflow {
4186 TextOverflow::Ellipsis => {
4187 self.text_layout
4188 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4189 }
4190 TextOverflow::Clip => {
4191 self.text_layout
4192 .truncate_runs_clip(font_context, broken_lines, text_width)
4193 }
4194 TextOverflow::Wrap => broken_lines,
4195 };
4196
4197 let line_height = style.font_size * style.line_height;
4198
4199 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4201 let decision = page_break::decide_break(
4202 cursor.remaining_height(),
4203 &line_heights,
4204 true,
4205 style.min_orphan_lines as usize,
4206 style.min_widow_lines as usize,
4207 );
4208
4209 let mut snapshot = cursor.elements.len();
4210 let mut container_start_y = cursor.content_y + cursor.y;
4211 let mut is_first_element = true;
4212
4213 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4214 pages.push(cursor.finalize());
4215 *cursor = cursor.new_page();
4216 snapshot = cursor.elements.len();
4217 container_start_y = cursor.content_y + cursor.y;
4218 }
4219
4220 let forced_break_at = match decision {
4221 page_break::BreakDecision::Split {
4222 items_on_current_page,
4223 } => Some(items_on_current_page),
4224 _ => None,
4225 };
4226 let mut first_break_done = false;
4227
4228 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4229 let needs_break = if let Some(break_at) = forced_break_at {
4230 if !first_break_done && line_idx == break_at {
4231 true
4232 } else {
4233 line_height > cursor.remaining_height()
4234 }
4235 } else {
4236 line_height > cursor.remaining_height()
4237 };
4238
4239 if needs_break {
4240 first_break_done = true;
4241 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4242 if !line_elements.is_empty() {
4243 let container_height = cursor.content_y + cursor.y - container_start_y;
4244 cursor.elements.push(LayoutElement {
4245 x: text_x,
4246 y: container_start_y,
4247 width: text_width,
4248 height: container_height,
4249 draw: DrawCommand::None,
4250 children: line_elements,
4251 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4252 resolved_style: Some(style.clone()),
4253 source_location: source_location.cloned(),
4254 href: parent_href.map(|s| s.to_string()),
4255 bookmark: if is_first_element {
4256 bookmark.map(|s| s.to_string())
4257 } else {
4258 None
4259 },
4260 alt: None,
4261 is_header_row: false,
4262 col_span: 1,
4263 overflow: Overflow::default(),
4264 opacity: 1.0,
4265 });
4266 is_first_element = false;
4267 }
4268
4269 pages.push(cursor.finalize());
4270 *cursor = cursor.new_page();
4271
4272 snapshot = cursor.elements.len();
4273 container_start_y = cursor.content_y + cursor.y;
4274 }
4275
4276 let line_x = match style.text_align {
4277 TextAlign::Left => text_x,
4278 TextAlign::Right => text_x + text_width - run_line.width,
4279 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
4280 TextAlign::Justify => text_x,
4281 };
4282
4283 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
4284
4285 let is_last_line = line_idx == broken_lines.len() - 1;
4292 let user_ws = style.word_spacing;
4293 let (justified_width, word_spacing) =
4294 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4295 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4296 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4297 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4298 let s = glyphs[..=idx]
4299 .iter()
4300 .filter(|g| g.char_value == ' ')
4301 .count();
4302 (w, s)
4303 } else {
4304 (0.0, 0)
4305 };
4306 let slack = text_width - natural_width;
4307 let ws = if space_count > 0 && slack.abs() > 0.01 {
4308 slack / space_count as f64
4309 } else {
4310 0.0
4311 };
4312 (text_width, user_ws + ws)
4313 } else {
4314 (run_line.width, user_ws)
4315 };
4316
4317 let text_line = TextLine {
4318 x: line_x,
4319 y: cursor.content_y + cursor.y + style.font_size,
4320 glyphs,
4321 width: justified_width,
4322 height: line_height,
4323 word_spacing,
4324 };
4325
4326 let text_dec = run_line
4328 .chars
4329 .iter()
4330 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4331 .map(|sc| sc.text_decoration)
4332 .unwrap_or(style.text_decoration);
4333
4334 cursor.elements.push(LayoutElement {
4335 x: line_x,
4336 y: cursor.content_y + cursor.y,
4337 width: justified_width,
4338 height: line_height,
4339 draw: DrawCommand::Text {
4340 lines: vec![text_line],
4341 color: style.color,
4342 text_decoration: text_dec,
4343 opacity: 1.0,
4344 },
4345 children: vec![],
4346 node_type: Some("TextLine".to_string()),
4347 resolved_style: Some(style.clone()),
4348 source_location: None,
4349 href: parent_href.map(|s| s.to_string()),
4350 bookmark: None,
4351 alt: None,
4352 is_header_row: false,
4353 col_span: 1,
4354 overflow: Overflow::default(),
4355 opacity: 1.0,
4356 });
4357
4358 cursor.y += line_height;
4359 }
4360
4361 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4362 if !line_elements.is_empty() {
4363 let container_height = cursor.content_y + cursor.y - container_start_y;
4364 cursor.elements.push(LayoutElement {
4365 x: text_x,
4366 y: container_start_y,
4367 width: text_width,
4368 height: container_height,
4369 draw: DrawCommand::None,
4370 children: line_elements,
4371 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4372 resolved_style: Some(style.clone()),
4373 source_location: source_location.cloned(),
4374 href: parent_href.map(|s| s.to_string()),
4375 bookmark: if is_first_element {
4376 bookmark.map(|s| s.to_string())
4377 } else {
4378 None
4379 },
4380 alt: None,
4381 is_header_row: false,
4382 col_span: 1,
4383 overflow: Overflow::default(),
4384 opacity: 1.0,
4385 });
4386 }
4387 }
4388
4389 fn build_positioned_glyphs_single_style(
4393 &self,
4394 line: &BrokenLine,
4395 style: &ResolvedStyle,
4396 href: Option<&str>,
4397 font_context: &FontContext,
4398 ) -> Vec<PositionedGlyph> {
4399 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4400 let line_text: String = line.chars.iter().collect();
4401 let direction = style.direction;
4402 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4404
4405 let font_runs = crate::font::fallback::segment_by_font(
4408 &line.chars,
4409 &style.font_family,
4410 style.font_weight,
4411 italic,
4412 font_context.registry(),
4413 );
4414 let needs_per_char_fallback = font_runs.len() > 1
4415 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4416
4417 if needs_per_char_fallback {
4419 let bidi_runs = if has_bidi {
4420 bidi::analyze_bidi(&line_text, direction)
4421 } else {
4422 vec![crate::text::bidi::BidiRun {
4423 char_start: 0,
4424 char_end: line.chars.len(),
4425 level: unicode_bidi::Level::ltr(),
4426 is_rtl: false,
4427 }]
4428 };
4429
4430 let mut all_glyphs = Vec::new();
4431 let mut bidi_levels = Vec::new();
4432 let mut x = 0.0_f64;
4433
4434 for bidi_run in &bidi_runs {
4436 for font_run in &font_runs {
4438 let start = font_run.start.max(bidi_run.char_start);
4440 let end = font_run.end.min(bidi_run.char_end);
4441 if start >= end {
4442 continue;
4443 }
4444
4445 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4446 let sub_text: String = sub_chars.iter().collect();
4447 let resolved_family = &font_run.family;
4448
4449 if let Some(font_data) =
4450 font_context.font_data(resolved_family, style.font_weight, italic)
4451 {
4452 if let Some(shaped) = shaping::shape_text_with_direction(
4453 &sub_text,
4454 font_data,
4455 bidi_run.is_rtl,
4456 ) {
4457 let units_per_em = font_context.units_per_em(
4458 resolved_family,
4459 style.font_weight,
4460 italic,
4461 );
4462 let scale = style.font_size / units_per_em as f64;
4463
4464 for sg in &shaped {
4465 let cluster = sg.cluster as usize;
4466 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4467
4468 let cluster_text = if shaped.len() < sub_chars.len() {
4469 let cluster_end =
4470 self.find_cluster_end(&shaped, sg, sub_chars.len());
4471 if cluster_end > cluster + 1 {
4472 Some(
4473 sub_chars[cluster..cluster_end]
4474 .iter()
4475 .collect::<String>(),
4476 )
4477 } else {
4478 None
4479 }
4480 } else {
4481 None
4482 };
4483
4484 let glyph_x = x + sg.x_offset as f64 * scale;
4485 let glyph_y = sg.y_offset as f64 * scale;
4486 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4487
4488 all_glyphs.push(PositionedGlyph {
4489 glyph_id: sg.glyph_id,
4490 x_offset: glyph_x,
4491 y_offset: glyph_y,
4492 x_advance: advance,
4493 font_size: style.font_size,
4494 font_family: Arc::from(resolved_family.as_str()),
4495 font_weight: style.font_weight,
4496 font_style: style.font_style,
4497 char_value,
4498 color: Some(style.color),
4499 href: href.map(|s| s.to_string()),
4500 text_decoration: style.text_decoration,
4501 letter_spacing: style.letter_spacing,
4502 cluster_text,
4503 });
4504 bidi_levels.push(bidi_run.level);
4505 x += advance;
4506 }
4507 continue;
4508 }
4509 }
4510
4511 for i in start..end {
4513 let ch = line.chars[i];
4514 let glyph_x = x;
4515 let char_width = font_context.char_width(
4516 ch,
4517 resolved_family,
4518 style.font_weight,
4519 italic,
4520 style.font_size,
4521 );
4522 let advance = char_width + style.letter_spacing;
4523 all_glyphs.push(PositionedGlyph {
4524 glyph_id: ch as u16,
4525 x_offset: glyph_x,
4526 y_offset: 0.0,
4527 x_advance: advance,
4528 font_size: style.font_size,
4529 font_family: Arc::from(resolved_family.as_str()),
4530 font_weight: style.font_weight,
4531 font_style: style.font_style,
4532 char_value: ch,
4533 color: Some(style.color),
4534 href: href.map(|s| s.to_string()),
4535 text_decoration: style.text_decoration,
4536 letter_spacing: style.letter_spacing,
4537 cluster_text: None,
4538 });
4539 bidi_levels.push(bidi_run.level);
4540 x += advance;
4541 }
4542 }
4543 }
4544
4545 if has_bidi && !all_glyphs.is_empty() {
4547 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4548 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
4549 }
4550 return all_glyphs;
4551 }
4552
4553 if let Some(font_data) =
4556 font_context.font_data(&style.font_family, style.font_weight, italic)
4557 {
4558 if has_bidi {
4559 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4561 let units_per_em =
4562 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4563 let scale = style.font_size / units_per_em as f64;
4564
4565 let mut all_glyphs = Vec::new();
4566 let mut bidi_levels = Vec::new();
4567 let mut x = 0.0_f64;
4568
4569 for run in &bidi_runs {
4570 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
4571 let run_text: String = run_chars.iter().collect();
4572
4573 if let Some(shaped) =
4574 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
4575 {
4576 for sg in &shaped {
4577 let cluster = sg.cluster as usize;
4578 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
4579
4580 let cluster_text = if shaped.len() < run_chars.len() {
4581 let cluster_end =
4582 self.find_cluster_end(&shaped, sg, run_chars.len());
4583 if cluster_end > cluster + 1 {
4584 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
4585 } else {
4586 None
4587 }
4588 } else {
4589 None
4590 };
4591
4592 let glyph_x = x + sg.x_offset as f64 * scale;
4593 let glyph_y = sg.y_offset as f64 * scale;
4594 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4595
4596 all_glyphs.push(PositionedGlyph {
4597 glyph_id: sg.glyph_id,
4598 x_offset: glyph_x,
4599 y_offset: glyph_y,
4600 x_advance: advance,
4601 font_size: style.font_size,
4602 font_family: Arc::from(style.font_family.as_str()),
4603 font_weight: style.font_weight,
4604 font_style: style.font_style,
4605 char_value,
4606 color: Some(style.color),
4607 href: href.map(|s| s.to_string()),
4608 text_decoration: style.text_decoration,
4609 letter_spacing: style.letter_spacing,
4610 cluster_text,
4611 });
4612 bidi_levels.push(run.level);
4613
4614 x += advance;
4615 }
4616 }
4617 }
4618
4619 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4621 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4622 return glyphs;
4623 }
4624
4625 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
4627 let units_per_em =
4628 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4629 let scale = style.font_size / units_per_em as f64;
4630
4631 return self.shaped_glyphs_to_positioned(
4632 &shaped,
4633 &line.chars,
4634 &line.char_positions,
4635 scale,
4636 style.font_size,
4637 &style.font_family,
4638 style.font_weight,
4639 style.font_style,
4640 Some(style.color),
4641 href,
4642 style.text_decoration,
4643 style.letter_spacing,
4644 );
4645 }
4646 }
4647
4648 let mut glyphs: Vec<PositionedGlyph> = line
4650 .chars
4651 .iter()
4652 .enumerate()
4653 .map(|(j, ch)| {
4654 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
4655 let char_width = font_context.char_width(
4656 *ch,
4657 &style.font_family,
4658 style.font_weight,
4659 italic,
4660 style.font_size,
4661 );
4662 PositionedGlyph {
4663 glyph_id: *ch as u16,
4664 x_offset: glyph_x,
4665 y_offset: 0.0,
4666 x_advance: char_width,
4667 font_size: style.font_size,
4668 font_family: Arc::from(style.font_family.as_str()),
4669 font_weight: style.font_weight,
4670 font_style: style.font_style,
4671 char_value: *ch,
4672 color: Some(style.color),
4673 href: href.map(|s| s.to_string()),
4674 text_decoration: style.text_decoration,
4675 letter_spacing: style.letter_spacing,
4676 cluster_text: None,
4677 }
4678 })
4679 .collect();
4680
4681 if has_bidi && !glyphs.is_empty() {
4683 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4684 let mut levels = Vec::with_capacity(glyphs.len());
4685 let mut char_idx = 0;
4686 for run in &bidi_runs {
4687 for _ in run.char_start..run.char_end {
4688 if char_idx < glyphs.len() {
4689 levels.push(run.level);
4690 char_idx += 1;
4691 }
4692 }
4693 }
4694 while levels.len() < glyphs.len() {
4696 levels.push(unicode_bidi::Level::ltr());
4697 }
4698 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
4699 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4700 }
4701
4702 glyphs
4703 }
4704
4705 fn build_positioned_glyphs_runs(
4710 &self,
4711 run_line: &RunBrokenLine,
4712 font_context: &FontContext,
4713 direction: Direction,
4714 ) -> Vec<PositionedGlyph> {
4715 let chars = &run_line.chars;
4716 if chars.is_empty() {
4717 return vec![];
4718 }
4719
4720 let resolved_families: Vec<String> = chars
4723 .iter()
4724 .map(|sc| {
4725 if !sc.font_family.contains(',') {
4726 sc.font_family.clone()
4727 } else {
4728 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4729 let (_, family) = font_context.registry().resolve_for_char(
4730 &sc.font_family,
4731 sc.ch,
4732 sc.font_weight,
4733 italic,
4734 );
4735 family
4736 }
4737 })
4738 .collect();
4739
4740 let line_text: String = chars.iter().map(|c| c.ch).collect();
4741 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4742 let bidi_runs = if has_bidi {
4743 Some(bidi::analyze_bidi(&line_text, direction))
4744 } else {
4745 None
4746 };
4747
4748 let mut glyphs = Vec::new();
4749 let mut bidi_levels = Vec::new();
4750 let mut i = 0;
4751
4752 while i < chars.len() {
4753 let sc = &chars[i];
4754 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4755 let resolved_family = &resolved_families[i];
4756
4757 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4759 runs.iter()
4760 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
4761 });
4762
4763 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
4765 {
4766 let run_start = i;
4768 let mut run_end = i + 1;
4769 while run_end < chars.len() {
4770 let next = &chars[run_end];
4771 let next_italic =
4772 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
4773 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4774 runs.iter()
4775 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
4776 });
4777 if resolved_families[run_end] == *resolved_family
4779 && next.font_weight == sc.font_weight
4780 && next_italic == italic
4781 && (next.font_size - sc.font_size).abs() < 0.001
4782 && next_is_rtl == is_rtl
4783 {
4784 run_end += 1;
4785 } else {
4786 break;
4787 }
4788 }
4789
4790 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
4791 if let Some(shaped) =
4792 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
4793 {
4794 let units_per_em =
4795 font_context.units_per_em(resolved_family, sc.font_weight, italic);
4796 let scale = sc.font_size / units_per_em as f64;
4797
4798 let run_chars: Vec<char> =
4800 chars[run_start..run_end].iter().map(|c| c.ch).collect();
4801 let run_positions: Vec<f64> = (run_start..run_end)
4802 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
4803 .collect();
4804
4805 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
4807 &shaped,
4808 &chars[run_start..run_end],
4809 &run_chars,
4810 &run_positions,
4811 scale,
4812 );
4813 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
4815 for g in &mut run_glyphs {
4816 g.font_family = resolved_family_arc.clone();
4817 }
4818 let run_level = if is_rtl {
4820 unicode_bidi::Level::rtl()
4821 } else {
4822 unicode_bidi::Level::ltr()
4823 };
4824 for _ in &run_glyphs {
4825 bidi_levels.push(run_level);
4826 }
4827 glyphs.extend(run_glyphs);
4828 i = run_end;
4829 continue;
4830 }
4831 }
4832
4833 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
4835 let char_width = font_context.char_width(
4836 sc.ch,
4837 resolved_family,
4838 sc.font_weight,
4839 italic,
4840 sc.font_size,
4841 );
4842 glyphs.push(PositionedGlyph {
4843 glyph_id: sc.ch as u16,
4844 x_offset: glyph_x,
4845 y_offset: 0.0,
4846 x_advance: char_width,
4847 font_size: sc.font_size,
4848 font_family: Arc::from(resolved_family.as_str()),
4849 font_weight: sc.font_weight,
4850 font_style: sc.font_style,
4851 char_value: sc.ch,
4852 color: Some(sc.color),
4853 href: sc.href.clone(),
4854 text_decoration: sc.text_decoration,
4855 letter_spacing: sc.letter_spacing,
4856 cluster_text: None,
4857 });
4858 bidi_levels.push(if is_rtl {
4859 unicode_bidi::Level::rtl()
4860 } else {
4861 unicode_bidi::Level::ltr()
4862 });
4863 i += 1;
4864 }
4865
4866 if has_bidi && !glyphs.is_empty() {
4868 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
4869 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4870 }
4871
4872 glyphs
4873 }
4874
4875 #[allow(clippy::too_many_arguments)]
4877 fn shaped_glyphs_to_positioned(
4878 &self,
4879 shaped: &[shaping::ShapedGlyph],
4880 chars: &[char],
4881 _char_positions: &[f64],
4882 scale: f64,
4883 font_size: f64,
4884 font_family: &str,
4885 font_weight: u32,
4886 font_style: FontStyle,
4887 color: Option<Color>,
4888 href: Option<&str>,
4889 text_decoration: TextDecoration,
4890 letter_spacing: f64,
4891 ) -> Vec<PositionedGlyph> {
4892 let mut result = Vec::with_capacity(shaped.len());
4893 let mut x = 0.0_f64;
4894
4895 for sg in shaped {
4896 let cluster = sg.cluster as usize;
4897 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4898
4899 let cluster_text = if shaped.len() < chars.len() {
4901 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4904 if cluster_end > cluster + 1 {
4905 Some(chars[cluster..cluster_end].iter().collect::<String>())
4906 } else {
4907 None
4908 }
4909 } else {
4910 None
4911 };
4912
4913 let glyph_x = x + sg.x_offset as f64 * scale;
4915 let glyph_y = sg.y_offset as f64 * scale;
4916 let advance = sg.x_advance as f64 * scale + letter_spacing;
4917
4918 result.push(PositionedGlyph {
4919 glyph_id: sg.glyph_id,
4920 x_offset: glyph_x,
4921 y_offset: glyph_y,
4922 x_advance: advance,
4923 font_size,
4924 font_family: Arc::from(font_family),
4925 font_weight,
4926 font_style,
4927 char_value,
4928 color,
4929 href: href.map(|s| s.to_string()),
4930 text_decoration,
4931 letter_spacing,
4932 cluster_text,
4933 });
4934
4935 x += advance;
4936 }
4937
4938 result
4939 }
4940
4941 fn shaped_glyphs_to_positioned_runs(
4943 &self,
4944 shaped: &[shaping::ShapedGlyph],
4945 styled_chars: &[StyledChar],
4946 chars: &[char],
4947 char_positions: &[f64],
4948 scale: f64,
4949 ) -> Vec<PositionedGlyph> {
4950 let mut result = Vec::with_capacity(shaped.len());
4951 let base_x = char_positions.first().copied().unwrap_or(0.0);
4953 let mut x = 0.0_f64;
4954
4955 for sg in shaped {
4956 let cluster = sg.cluster as usize;
4957 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
4958 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4959
4960 let cluster_text = if shaped.len() < chars.len() {
4961 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4962 if cluster_end > cluster + 1 {
4963 Some(chars[cluster..cluster_end].iter().collect::<String>())
4964 } else {
4965 None
4966 }
4967 } else {
4968 None
4969 };
4970
4971 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
4972 let glyph_y = sg.y_offset as f64 * scale;
4973 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
4974
4975 result.push(PositionedGlyph {
4976 glyph_id: sg.glyph_id,
4977 x_offset: glyph_x,
4978 y_offset: glyph_y,
4979 x_advance: advance,
4980 font_size: sc.font_size,
4981 font_family: Arc::from(sc.font_family.as_str()),
4982 font_weight: sc.font_weight,
4983 font_style: sc.font_style,
4984 char_value,
4985 color: Some(sc.color),
4986 href: sc.href.clone(),
4987 text_decoration: sc.text_decoration,
4988 letter_spacing: sc.letter_spacing,
4989 cluster_text,
4990 });
4991
4992 x += advance;
4993 }
4994
4995 result
4996 }
4997
4998 fn find_cluster_end(
5000 &self,
5001 shaped: &[shaping::ShapedGlyph],
5002 current: &shaping::ShapedGlyph,
5003 num_chars: usize,
5004 ) -> usize {
5005 for sg in shaped {
5007 if sg.cluster > current.cluster {
5008 return sg.cluster as usize;
5009 }
5010 }
5011 num_chars
5013 }
5014
5015 #[allow(clippy::too_many_arguments)]
5016 fn layout_image(
5017 &self,
5018 node: &Node,
5019 style: &ResolvedStyle,
5020 cursor: &mut PageCursor,
5021 pages: &mut Vec<LayoutPage>,
5022 x: f64,
5023 available_width: f64,
5024 explicit_width: Option<f64>,
5025 explicit_height: Option<f64>,
5026 ) {
5027 let margin = &style.margin.to_edges();
5028
5029 let src = match &node.kind {
5031 NodeKind::Image { src, .. } => src.as_str(),
5032 _ => "",
5033 };
5034
5035 let loaded = if !src.is_empty() {
5036 crate::image_loader::load_image(src).ok()
5037 } else {
5038 None
5039 };
5040
5041 let (img_width, img_height) = if let Some(ref img) = loaded {
5043 let intrinsic_w = img.width_px as f64;
5044 let intrinsic_h = img.height_px as f64;
5045 let aspect = if intrinsic_w > 0.0 {
5046 intrinsic_h / intrinsic_w
5047 } else {
5048 0.75
5049 };
5050
5051 match (explicit_width, explicit_height) {
5052 (Some(w), Some(h)) => (w, h),
5053 (Some(w), None) => (w, w * aspect),
5054 (None, Some(h)) => (h / aspect, h),
5055 (None, None) => {
5056 let max_w = available_width - margin.horizontal();
5057 let w = intrinsic_w.min(max_w);
5058 (w, w * aspect)
5059 }
5060 }
5061 } else {
5062 let w = explicit_width.unwrap_or(available_width - margin.horizontal());
5064 let h = explicit_height.unwrap_or(w * 0.75);
5065 (w, h)
5066 };
5067
5068 let total_height = img_height + margin.vertical();
5069
5070 if total_height > cursor.remaining_height() {
5071 pages.push(cursor.finalize());
5072 *cursor = cursor.new_page();
5073 }
5074
5075 cursor.y += margin.top;
5076
5077 let draw = if let Some(image_data) = loaded {
5078 DrawCommand::Image { image_data }
5079 } else {
5080 DrawCommand::ImagePlaceholder
5081 };
5082
5083 cursor.elements.push(LayoutElement {
5084 x: x + margin.left,
5085 y: cursor.content_y + cursor.y,
5086 width: img_width,
5087 height: img_height,
5088 draw,
5089 children: vec![],
5090 node_type: Some(node_kind_name(&node.kind).to_string()),
5091 resolved_style: Some(style.clone()),
5092 source_location: node.source_location.clone(),
5093 href: node.href.clone(),
5094 bookmark: node.bookmark.clone(),
5095 alt: node.alt.clone(),
5096 is_header_row: false,
5097 col_span: 1,
5098 overflow: style.overflow,
5099 opacity: style.opacity,
5100 });
5101
5102 cursor.y += img_height + margin.bottom;
5103 }
5104
5105 #[allow(clippy::too_many_arguments)]
5107 fn layout_svg(
5108 &self,
5109 node: &Node,
5110 style: &ResolvedStyle,
5111 cursor: &mut PageCursor,
5112 pages: &mut Vec<LayoutPage>,
5113 x: f64,
5114 _available_width: f64,
5115 svg_width: f64,
5116 svg_height: f64,
5117 view_box: Option<&str>,
5118 content: &str,
5119 ) {
5120 let margin = &style.margin.to_edges();
5121 let total_height = svg_height + margin.vertical();
5122
5123 if total_height > cursor.remaining_height() {
5124 pages.push(cursor.finalize());
5125 *cursor = cursor.new_page();
5126 }
5127
5128 cursor.y += margin.top;
5129
5130 let vb = view_box
5131 .and_then(crate::svg::parse_view_box)
5132 .unwrap_or(crate::svg::ViewBox {
5133 min_x: 0.0,
5134 min_y: 0.0,
5135 width: svg_width,
5136 height: svg_height,
5137 });
5138
5139 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5140
5141 cursor.elements.push(LayoutElement {
5142 x: x + margin.left,
5143 y: cursor.content_y + cursor.y,
5144 width: svg_width,
5145 height: svg_height,
5146 draw: DrawCommand::Svg {
5147 commands,
5148 width: svg_width,
5149 height: svg_height,
5150 viewbox_min_x: vb.min_x,
5151 viewbox_min_y: vb.min_y,
5152 viewbox_width: vb.width,
5153 viewbox_height: vb.height,
5154 clip: false,
5155 },
5156 children: vec![],
5157 node_type: Some("Svg".to_string()),
5158 resolved_style: Some(style.clone()),
5159 source_location: node.source_location.clone(),
5160 href: node.href.clone(),
5161 bookmark: node.bookmark.clone(),
5162 alt: node.alt.clone(),
5163 is_header_row: false,
5164 col_span: 1,
5165 overflow: style.overflow,
5166 opacity: style.opacity,
5167 });
5168
5169 cursor.y += svg_height + margin.bottom;
5170 }
5171
5172 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
5174 use crate::svg::SvgCommand;
5175
5176 let mut commands = Vec::new();
5177 let mut cur_x = 0.0_f64;
5178 let mut cur_y = 0.0_f64;
5179
5180 for op in operations {
5181 match op {
5182 CanvasOp::MoveTo { x, y } => {
5183 commands.push(SvgCommand::MoveTo(*x, *y));
5184 cur_x = *x;
5185 cur_y = *y;
5186 }
5187 CanvasOp::LineTo { x, y } => {
5188 commands.push(SvgCommand::LineTo(*x, *y));
5189 cur_x = *x;
5190 cur_y = *y;
5191 }
5192 CanvasOp::BezierCurveTo {
5193 cp1x,
5194 cp1y,
5195 cp2x,
5196 cp2y,
5197 x,
5198 y,
5199 } => {
5200 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
5201 cur_x = *x;
5202 cur_y = *y;
5203 }
5204 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
5205 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
5207 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
5208 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
5209 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
5210 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
5211 cur_x = *x;
5212 cur_y = *y;
5213 }
5214 CanvasOp::ClosePath => {
5215 commands.push(SvgCommand::ClosePath);
5216 }
5217 CanvasOp::Rect {
5218 x,
5219 y,
5220 width,
5221 height,
5222 } => {
5223 commands.push(SvgCommand::MoveTo(*x, *y));
5224 commands.push(SvgCommand::LineTo(*x + *width, *y));
5225 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
5226 commands.push(SvgCommand::LineTo(*x, *y + *height));
5227 commands.push(SvgCommand::ClosePath);
5228 cur_x = *x;
5229 cur_y = *y;
5230 }
5231 CanvasOp::Circle { cx, cy, r } => {
5232 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
5233 }
5234 CanvasOp::Ellipse { cx, cy, rx, ry } => {
5235 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
5236 }
5237 CanvasOp::Arc {
5238 cx,
5239 cy,
5240 r,
5241 start_angle,
5242 end_angle,
5243 counterclockwise,
5244 } => {
5245 let steps = 32;
5249 let mut sweep = end_angle - start_angle;
5250 if !counterclockwise && sweep < 0.0 {
5251 sweep += 2.0 * std::f64::consts::PI;
5252 }
5253 if *counterclockwise && sweep > 0.0 {
5254 sweep -= 2.0 * std::f64::consts::PI;
5255 }
5256 for i in 0..=steps {
5257 let t = *start_angle + sweep * (i as f64 / steps as f64);
5258 let px = cx + r * t.cos();
5259 let py = cy + r * t.sin();
5260 if i == 0 {
5261 commands.push(SvgCommand::MoveTo(px, py));
5262 } else {
5263 commands.push(SvgCommand::LineTo(px, py));
5264 }
5265 }
5266 }
5267 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
5268 CanvasOp::Fill => commands.push(SvgCommand::Fill),
5269 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
5270 CanvasOp::SetFillColor { r, g, b } => {
5271 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
5273 }
5274 CanvasOp::SetStrokeColor { r, g, b } => {
5275 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
5276 }
5277 CanvasOp::SetLineWidth { width } => {
5278 commands.push(SvgCommand::SetStrokeWidth(*width));
5279 }
5280 CanvasOp::SetLineCap { cap } => {
5281 commands.push(SvgCommand::SetLineCap(*cap));
5282 }
5283 CanvasOp::SetLineJoin { join } => {
5284 commands.push(SvgCommand::SetLineJoin(*join));
5285 }
5286 CanvasOp::Save => commands.push(SvgCommand::SaveState),
5287 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
5288 }
5289 }
5290
5291 commands
5292 }
5293
5294 #[allow(clippy::too_many_arguments)]
5296 fn layout_canvas(
5297 &self,
5298 node: &Node,
5299 style: &ResolvedStyle,
5300 cursor: &mut PageCursor,
5301 pages: &mut Vec<LayoutPage>,
5302 x: f64,
5303 _available_width: f64,
5304 canvas_width: f64,
5305 canvas_height: f64,
5306 operations: &[CanvasOp],
5307 ) {
5308 let margin = style.margin.to_edges();
5309 let total_height = canvas_height + margin.top + margin.bottom;
5310
5311 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5313 pages.push(cursor.finalize());
5314 *cursor = cursor.new_page();
5315 }
5316
5317 cursor.y += margin.top;
5318
5319 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5320
5321 cursor.elements.push(LayoutElement {
5322 x: x + margin.left,
5323 y: cursor.content_y + cursor.y,
5324 width: canvas_width,
5325 height: canvas_height,
5326 draw: DrawCommand::Svg {
5327 commands: svg_commands,
5328 width: canvas_width,
5329 height: canvas_height,
5330 viewbox_min_x: 0.0,
5333 viewbox_min_y: 0.0,
5334 viewbox_width: canvas_width,
5335 viewbox_height: canvas_height,
5336 clip: true,
5337 },
5338 children: vec![],
5339 node_type: Some("Canvas".to_string()),
5340 resolved_style: Some(style.clone()),
5341 source_location: node.source_location.clone(),
5342 href: node.href.clone(),
5343 bookmark: node.bookmark.clone(),
5344 alt: node.alt.clone(),
5345 is_header_row: false,
5346 col_span: 1,
5347 overflow: style.overflow,
5348 opacity: style.opacity,
5349 });
5350
5351 cursor.y += canvas_height + margin.bottom;
5352 }
5353
5354 #[allow(clippy::too_many_arguments)]
5356 #[allow(clippy::too_many_arguments)]
5358 fn layout_chart(
5359 &self,
5360 node: &Node,
5361 style: &ResolvedStyle,
5362 cursor: &mut PageCursor,
5363 pages: &mut Vec<LayoutPage>,
5364 x: f64,
5365 chart_width: f64,
5366 chart_height: f64,
5367 primitives: Vec<crate::chart::ChartPrimitive>,
5368 node_type_name: &str,
5369 ) {
5370 let margin = &style.margin.to_edges();
5371 let total_height = chart_height + margin.vertical();
5372
5373 if total_height > cursor.remaining_height() {
5374 pages.push(cursor.finalize());
5375 *cursor = cursor.new_page();
5376 }
5377
5378 cursor.y += margin.top;
5379
5380 let draw = DrawCommand::Chart { primitives };
5381
5382 cursor.elements.push(LayoutElement {
5383 x: x + margin.left,
5384 y: cursor.content_y + cursor.y,
5385 width: chart_width,
5386 height: chart_height,
5387 draw,
5388 children: vec![],
5389 node_type: Some(node_type_name.to_string()),
5390 resolved_style: Some(style.clone()),
5391 source_location: node.source_location.clone(),
5392 href: node.href.clone(),
5393 bookmark: node.bookmark.clone(),
5394 alt: node.alt.clone(),
5395 is_header_row: false,
5396 col_span: 1,
5397 overflow: style.overflow,
5398 opacity: style.opacity,
5399 });
5400
5401 cursor.y += chart_height + margin.bottom;
5402 }
5403
5404 #[allow(clippy::too_many_arguments)]
5406 fn layout_form_field(
5407 &self,
5408 node: &Node,
5409 style: &ResolvedStyle,
5410 cursor: &mut PageCursor,
5411 pages: &mut Vec<LayoutPage>,
5412 x: f64,
5413 field_width: f64,
5414 field_height: f64,
5415 draw: DrawCommand,
5416 node_type_name: &str,
5417 ) {
5418 let margin = &style.margin.to_edges();
5419 let total_height = field_height + margin.vertical();
5420
5421 if total_height > cursor.remaining_height() {
5422 pages.push(cursor.finalize());
5423 *cursor = cursor.new_page();
5424 }
5425
5426 cursor.y += margin.top;
5427
5428 cursor.elements.push(LayoutElement {
5429 x: x + margin.left,
5430 y: cursor.content_y + cursor.y,
5431 width: field_width,
5432 height: field_height,
5433 draw,
5434 children: vec![],
5435 node_type: Some(node_type_name.to_string()),
5436 resolved_style: Some(style.clone()),
5437 source_location: node.source_location.clone(),
5438 href: node.href.clone(),
5439 bookmark: node.bookmark.clone(),
5440 alt: node.alt.clone(),
5441 is_header_row: false,
5442 col_span: 1,
5443 overflow: style.overflow,
5444 opacity: style.opacity,
5445 });
5446
5447 cursor.y += field_height + margin.bottom;
5448 }
5449
5450 #[allow(clippy::too_many_arguments)]
5451 fn layout_barcode(
5452 &self,
5453 node: &Node,
5454 style: &ResolvedStyle,
5455 cursor: &mut PageCursor,
5456 pages: &mut Vec<LayoutPage>,
5457 x: f64,
5458 available_width: f64,
5459 data: &str,
5460 format: crate::barcode::BarcodeFormat,
5461 explicit_width: Option<f64>,
5462 bar_height: f64,
5463 ) {
5464 let margin = &style.margin.to_edges();
5465 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5466 let total_height = bar_height + margin.vertical();
5467
5468 if total_height > cursor.remaining_height() {
5469 pages.push(cursor.finalize());
5470 *cursor = cursor.new_page();
5471 }
5472
5473 cursor.y += margin.top;
5474
5475 let draw = match crate::barcode::generate_barcode(data, format) {
5476 Ok(barcode_data) => {
5477 let bar_width = if barcode_data.bars.is_empty() {
5478 0.0
5479 } else {
5480 display_width / barcode_data.bars.len() as f64
5481 };
5482 DrawCommand::Barcode {
5483 bars: barcode_data.bars,
5484 bar_width,
5485 height: bar_height,
5486 color: style.color,
5487 }
5488 }
5489 Err(_) => DrawCommand::None,
5490 };
5491
5492 cursor.elements.push(LayoutElement {
5493 x: x + margin.left,
5494 y: cursor.content_y + cursor.y,
5495 width: display_width,
5496 height: bar_height,
5497 draw,
5498 children: vec![],
5499 node_type: Some("Barcode".to_string()),
5500 resolved_style: Some(style.clone()),
5501 source_location: node.source_location.clone(),
5502 href: node.href.clone(),
5503 bookmark: node.bookmark.clone(),
5504 alt: node.alt.clone(),
5505 is_header_row: false,
5506 col_span: 1,
5507 overflow: style.overflow,
5508 opacity: style.opacity,
5509 });
5510
5511 cursor.y += bar_height + margin.bottom;
5512 }
5513
5514 #[allow(clippy::too_many_arguments)]
5516 fn layout_qrcode(
5517 &self,
5518 node: &Node,
5519 style: &ResolvedStyle,
5520 cursor: &mut PageCursor,
5521 pages: &mut Vec<LayoutPage>,
5522 x: f64,
5523 available_width: f64,
5524 data: &str,
5525 explicit_size: Option<f64>,
5526 ) {
5527 let margin = &style.margin.to_edges();
5528 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
5529 let total_height = display_size + margin.vertical();
5530
5531 if total_height > cursor.remaining_height() {
5532 pages.push(cursor.finalize());
5533 *cursor = cursor.new_page();
5534 }
5535
5536 cursor.y += margin.top;
5537
5538 let draw = match crate::qrcode::generate_qr(data) {
5539 Ok(matrix) => {
5540 let module_size = display_size / matrix.size as f64;
5541 DrawCommand::QrCode {
5542 modules: matrix.modules,
5543 module_size,
5544 color: style.color,
5545 }
5546 }
5547 Err(_) => DrawCommand::None,
5548 };
5549
5550 cursor.elements.push(LayoutElement {
5551 x: x + margin.left,
5552 y: cursor.content_y + cursor.y,
5553 width: display_size,
5554 height: display_size,
5555 draw,
5556 children: vec![],
5557 node_type: Some("QrCode".to_string()),
5558 resolved_style: Some(style.clone()),
5559 source_location: node.source_location.clone(),
5560 href: node.href.clone(),
5561 bookmark: node.bookmark.clone(),
5562 alt: node.alt.clone(),
5563 is_header_row: false,
5564 col_span: 1,
5565 overflow: style.overflow,
5566 opacity: style.opacity,
5567 });
5568
5569 cursor.y += display_size + margin.bottom;
5570 }
5571
5572 fn measure_node_height(
5575 &self,
5576 node: &Node,
5577 available_width: f64,
5578 style: &ResolvedStyle,
5579 font_context: &FontContext,
5580 ) -> f64 {
5581 match &node.kind {
5582 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5587 let measure_width = match style.width {
5590 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
5591 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5592 };
5593 if !runs.is_empty() {
5594 let mut styled_chars: Vec<StyledChar> = Vec::new();
5596 for run in runs {
5597 let run_style = run.style.resolve(Some(style), measure_width);
5598 let run_content = substitute_page_placeholders(&run.content);
5599 for ch in run_content.chars() {
5600 styled_chars.push(StyledChar {
5601 ch,
5602 font_family: run_style.font_family.clone(),
5603 font_size: run_style.font_size,
5604 font_weight: run_style.font_weight,
5605 font_style: run_style.font_style,
5606 color: run_style.color,
5607 href: None,
5608 text_decoration: run_style.text_decoration,
5609 letter_spacing: run_style.letter_spacing,
5610 });
5611 }
5612 }
5613 let broken_lines = self.text_layout.break_runs_into_lines(
5614 font_context,
5615 &styled_chars,
5616 measure_width,
5617 style.hyphens,
5618 style.lang.as_deref(),
5619 );
5620 let line_height = style.font_size * style.line_height;
5621 (broken_lines.len() as f64) * line_height + style.padding.vertical()
5622 } else {
5623 let content = substitute_page_placeholders(content);
5624 let lines = self.text_layout.break_into_lines(
5625 font_context,
5626 &content,
5627 measure_width,
5628 style.font_size,
5629 &style.font_family,
5630 style.font_weight,
5631 style.font_style,
5632 style.letter_spacing,
5633 style.hyphens,
5634 style.lang.as_deref(),
5635 );
5636 let line_height = style.font_size * style.line_height;
5637 (lines.len() as f64) * line_height + style.padding.vertical()
5638 }
5639 }
5640 NodeKind::Image {
5641 src,
5642 width: explicit_w,
5643 height: explicit_h,
5644 } => {
5645 if let SizeConstraint::Fixed(h) = style.height {
5647 return h + style.padding.vertical();
5648 }
5649 if let Some(h) = explicit_h {
5651 return *h + style.padding.vertical();
5652 }
5653 let aspect = self
5655 .get_image_dimensions(src)
5656 .map(|(w, h)| if w > 0 { h as f64 / w as f64 } else { 0.75 })
5657 .unwrap_or(0.75);
5658 let w = if let SizeConstraint::Fixed(w) = style.width {
5659 w
5660 } else {
5661 explicit_w.unwrap_or(available_width - style.margin.horizontal())
5662 };
5663 w * aspect + style.padding.vertical()
5664 }
5665 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
5666 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
5667 NodeKind::QrCode { size, .. } => {
5668 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
5669 display_size + style.margin.vertical()
5670 }
5671 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
5672 NodeKind::BarChart { height, .. }
5673 | NodeKind::LineChart { height, .. }
5674 | NodeKind::PieChart { height, .. }
5675 | NodeKind::AreaChart { height, .. }
5676 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
5677 NodeKind::TextField { height, .. }
5678 | NodeKind::Checkbox { height, .. }
5679 | NodeKind::Dropdown { height, .. }
5680 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
5681 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
5683 if let SizeConstraint::Fixed(h) = style.height {
5690 return h;
5691 }
5692 let outer_width = match style.width {
5693 SizeConstraint::Fixed(w) => w,
5694 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5695 };
5696 let inner_width =
5697 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5698 let col_widths = self.resolve_column_widths(columns, inner_width, &node.children);
5699 let row_gap = style.row_gap;
5700 let mut total = 0.0;
5701 for (i, row) in node.children.iter().enumerate() {
5702 if i > 0 {
5703 total += row_gap;
5704 }
5705 total += self.measure_table_row_height(row, &col_widths, style, font_context);
5706 }
5707 total + style.padding.vertical() + style.border_width.vertical()
5708 }
5709 NodeKind::TableRow { .. } => {
5710 let n = node.children.len().max(1);
5715 let usable = (available_width - style.margin.horizontal()).max(0.0);
5716 let col_w = usable / n as f64;
5717 let col_widths = vec![col_w; n];
5718 self.measure_table_row_height(node, &col_widths, style, font_context)
5719 }
5720 _ => {
5721 if let SizeConstraint::Fixed(h) = style.height {
5723 return h;
5724 }
5725 let outer_width = match style.width {
5729 SizeConstraint::Fixed(w) => w,
5730 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5731 }
5732 .min(style.max_width)
5733 .max(style.min_width);
5734 let inner_width =
5735 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5736 let children_height =
5737 self.measure_children_height(&node.children, inner_width, style, font_context);
5738 (children_height + style.padding.vertical() + style.border_width.vertical())
5739 .max(style.min_height)
5740 }
5741 }
5742 }
5743
5744 fn measure_children_height(
5745 &self,
5746 children: &[Node],
5747 available_width: f64,
5748 parent_style: &ResolvedStyle,
5749 font_context: &FontContext,
5750 ) -> f64 {
5751 if matches!(parent_style.display, Display::Grid) {
5753 if let Some(template_cols) = &parent_style.grid_template_columns {
5754 let num_columns = template_cols.len();
5755 if num_columns > 0 && !children.is_empty() {
5756 let col_gap = parent_style.column_gap;
5757 let row_gap = parent_style.row_gap;
5758
5759 let content_sizes: Vec<f64> = template_cols
5760 .iter()
5761 .map(|track| {
5762 if matches!(track, GridTrackSize::Auto) {
5763 available_width / num_columns as f64
5764 } else {
5765 0.0
5766 }
5767 })
5768 .collect();
5769
5770 let col_widths = grid::resolve_tracks(
5771 template_cols,
5772 available_width,
5773 col_gap,
5774 &content_sizes,
5775 );
5776
5777 let placements: Vec<Option<&GridPlacement>> = children
5778 .iter()
5779 .map(|child| child.style.grid_placement.as_ref())
5780 .collect();
5781
5782 let item_placements = grid::place_items(&placements, num_columns);
5783 let num_rows = grid::compute_num_rows(&item_placements);
5784
5785 if num_rows == 0 {
5786 return 0.0;
5787 }
5788
5789 let mut row_heights = vec![0.0_f64; num_rows];
5790 for placement in &item_placements {
5791 let cell_width = grid::span_width(
5792 placement.col_start,
5793 placement.col_end,
5794 &col_widths,
5795 col_gap,
5796 );
5797 let child = &children[placement.child_index];
5798 let child_style = child.style.resolve(Some(parent_style), cell_width);
5799 let h =
5800 self.measure_node_height(child, cell_width, &child_style, font_context);
5801 let span = placement.row_end - placement.row_start;
5802 let per_row = h / span as f64;
5803 for rh in row_heights
5804 .iter_mut()
5805 .take(placement.row_end.min(num_rows))
5806 .skip(placement.row_start)
5807 {
5808 if per_row > *rh {
5809 *rh = per_row;
5810 }
5811 }
5812 }
5813
5814 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
5815 return row_heights.iter().sum::<f64>() + total_row_gap;
5816 }
5817 }
5818 }
5819
5820 let direction = parent_style.flex_direction;
5821 let row_gap = parent_style.row_gap;
5822 let column_gap = parent_style.column_gap;
5823
5824 match direction {
5825 FlexDirection::Row | FlexDirection::RowReverse => {
5826 let styles: Vec<ResolvedStyle> = children
5829 .iter()
5830 .map(|child| child.style.resolve(Some(parent_style), available_width))
5831 .collect();
5832
5833 let base_widths: Vec<f64> = children
5834 .iter()
5835 .zip(&styles)
5836 .map(|(child, style)| match style.flex_basis {
5837 SizeConstraint::Fixed(w) => w,
5838 SizeConstraint::Auto => match style.width {
5839 SizeConstraint::Fixed(w) => w,
5840 SizeConstraint::Auto => self
5841 .measure_intrinsic_width(child, style, font_context)
5842 .min(available_width),
5843 },
5844 })
5845 .collect();
5846
5847 let lines = match parent_style.flex_wrap {
5848 FlexWrap::NoWrap => {
5849 vec![flex::WrapLine {
5850 start: 0,
5851 end: children.len(),
5852 }]
5853 }
5854 FlexWrap::Wrap | FlexWrap::WrapReverse => {
5855 flex::partition_into_lines(&base_widths, column_gap, available_width)
5856 }
5857 };
5858
5859 let mut final_widths = base_widths.clone();
5861 for line in &lines {
5862 let line_count = line.end - line.start;
5863 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
5864 let distributable = available_width - line_gap;
5865 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
5866 let remaining = distributable - total_base;
5867
5868 if remaining > 0.0 {
5869 let total_grow: f64 = styles[line.start..line.end]
5870 .iter()
5871 .map(|s| s.flex_grow)
5872 .sum();
5873 if total_grow > 0.0 {
5874 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5875 final_widths[line.start + j] = base_widths[line.start + j]
5876 + remaining * (s.flex_grow / total_grow);
5877 }
5878 }
5879 } else if remaining < 0.0 {
5880 let total_shrink: f64 = styles[line.start..line.end]
5881 .iter()
5882 .enumerate()
5883 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
5884 .sum();
5885 if total_shrink > 0.0 {
5886 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5887 let factor =
5888 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
5889 let w = base_widths[line.start + j] + remaining * factor;
5890 final_widths[line.start + j] = w.max(s.min_width);
5891 }
5892 }
5893 }
5894 }
5895
5896 let mut total = 0.0;
5897 for (i, line) in lines.iter().enumerate() {
5898 let line_height: f64 = children[line.start..line.end]
5899 .iter()
5900 .enumerate()
5901 .map(|(j, child)| {
5902 let fw = final_widths[line.start + j];
5903 let child_style = child.style.resolve(Some(parent_style), fw);
5904 self.measure_node_height(child, fw, &child_style, font_context)
5905 + child_style.margin.vertical()
5906 })
5907 .fold(0.0f64, f64::max);
5908 total += line_height;
5909 if i > 0 {
5910 total += row_gap;
5911 }
5912 }
5913 total
5914 }
5915 FlexDirection::Column | FlexDirection::ColumnReverse => {
5916 let mut total = 0.0;
5917 for (i, child) in children.iter().enumerate() {
5918 let child_style = child.style.resolve(Some(parent_style), available_width);
5919 let child_height = self.measure_node_height(
5920 child,
5921 available_width,
5922 &child_style,
5923 font_context,
5924 );
5925 total += child_height + child_style.margin.vertical();
5926 if i > 0 {
5927 total += row_gap;
5928 }
5929 }
5930 total
5931 }
5932 }
5933 }
5934
5935 fn measure_intrinsic_width(
5937 &self,
5938 node: &Node,
5939 style: &ResolvedStyle,
5940 font_context: &FontContext,
5941 ) -> f64 {
5942 match &node.kind {
5943 NodeKind::Svg { width, .. } => {
5944 *width + style.padding.horizontal() + style.margin.horizontal()
5945 }
5946 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5947 let text_width = if !runs.is_empty() {
5953 runs.iter()
5954 .map(|run| {
5955 let run_style = run.style.resolve(Some(style), 0.0);
5956 let run_content = substitute_page_placeholders(&run.content);
5957 let transformed =
5958 apply_text_transform(&run_content, run_style.text_transform);
5959 let italic = matches!(
5960 run_style.font_style,
5961 FontStyle::Italic | FontStyle::Oblique
5962 );
5963 transformed
5967 .split('\n')
5968 .map(|segment| {
5969 font_context.measure_string(
5970 segment,
5971 &run_style.font_family,
5972 run_style.font_weight,
5973 italic,
5974 run_style.font_size,
5975 run_style.letter_spacing,
5976 )
5977 })
5978 .fold(0.0f64, f64::max)
5979 })
5980 .sum()
5981 } else {
5982 let content = substitute_page_placeholders(content);
5983 let transformed = apply_text_transform(&content, style.text_transform);
5984 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5985 transformed
5986 .split('\n')
5987 .map(|segment| {
5988 font_context.measure_string(
5989 segment,
5990 &style.font_family,
5991 style.font_weight,
5992 italic,
5993 style.font_size,
5994 style.letter_spacing,
5995 )
5996 })
5997 .fold(0.0f64, f64::max)
5998 };
5999 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6002 }
6003 NodeKind::Image {
6004 src, width, height, ..
6005 } => {
6006 let w = if let SizeConstraint::Fixed(w) = style.width {
6007 w
6008 } else if let Some(w) = width {
6009 *w
6010 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6011 let pixel_w = iw as f64;
6012 let pixel_h = ih as f64;
6013 let aspect = if pixel_w > 0.0 {
6014 pixel_h / pixel_w
6015 } else {
6016 0.75
6017 };
6018 let constrained_h = match style.height {
6020 SizeConstraint::Fixed(h) => Some(h),
6021 SizeConstraint::Auto => *height,
6022 };
6023 if let Some(h) = constrained_h {
6024 h / aspect
6025 } else {
6026 pixel_w
6027 }
6028 } else {
6029 100.0
6030 };
6031 w + style.padding.horizontal() + style.margin.horizontal()
6032 }
6033 NodeKind::Barcode { width, .. } => {
6034 let w = width.unwrap_or(0.0);
6035 w + style.padding.horizontal() + style.margin.horizontal()
6036 }
6037 NodeKind::QrCode { size, .. } => {
6038 let display_size = size.unwrap_or(0.0);
6039 display_size + style.padding.horizontal() + style.margin.horizontal()
6040 }
6041 NodeKind::Canvas { width, .. } => {
6042 *width + style.padding.horizontal() + style.margin.horizontal()
6043 }
6044 NodeKind::BarChart { width, .. }
6045 | NodeKind::LineChart { width, .. }
6046 | NodeKind::PieChart { width, .. }
6047 | NodeKind::AreaChart { width, .. }
6048 | NodeKind::DotPlot { width, .. } => {
6049 *width + style.padding.horizontal() + style.margin.horizontal()
6050 }
6051 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6052 *width + style.padding.horizontal() + style.margin.horizontal()
6053 }
6054 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
6055 *width + style.padding.horizontal() + style.margin.horizontal()
6056 }
6057 NodeKind::Watermark { .. } => 0.0, _ => {
6059 if node.children.is_empty() {
6061 style.padding.horizontal() + style.margin.horizontal()
6062 } else {
6063 let direction = style.flex_direction;
6064 let gap = style.gap;
6065 let mut total = 0.0f64;
6066 for (i, child) in node.children.iter().enumerate() {
6067 let child_style = child.style.resolve(Some(style), 0.0);
6068 let child_width =
6069 self.measure_intrinsic_width(child, &child_style, font_context);
6070 match direction {
6071 FlexDirection::Row | FlexDirection::RowReverse => {
6072 total += child_width;
6073 if i > 0 {
6074 total += gap;
6075 }
6076 }
6077 _ => {
6078 total = total.max(child_width);
6079 }
6080 }
6081 }
6082 total
6083 + style.padding.horizontal()
6084 + style.margin.horizontal()
6085 + style.border_width.horizontal()
6086 }
6087 }
6088 }
6089 }
6090
6091 pub fn measure_min_content_width(
6095 &self,
6096 node: &Node,
6097 style: &ResolvedStyle,
6098 font_context: &FontContext,
6099 ) -> f64 {
6100 match &node.kind {
6101 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6102 let word_width = if !runs.is_empty() {
6103 runs.iter()
6105 .map(|run| {
6106 let run_style = run.style.resolve(Some(style), 0.0);
6107 let run_content = substitute_page_placeholders(&run.content);
6108 let transformed =
6109 apply_text_transform(&run_content, run_style.text_transform);
6110 self.text_layout.measure_widest_word(
6111 font_context,
6112 &transformed,
6113 run_style.font_size,
6114 &run_style.font_family,
6115 run_style.font_weight,
6116 run_style.font_style,
6117 run_style.letter_spacing,
6118 style.hyphens,
6119 style.lang.as_deref(),
6120 )
6121 })
6122 .fold(0.0f64, f64::max)
6123 } else {
6124 let content = substitute_page_placeholders(content);
6125 let transformed = apply_text_transform(&content, style.text_transform);
6126 self.text_layout.measure_widest_word(
6127 font_context,
6128 &transformed,
6129 style.font_size,
6130 &style.font_family,
6131 style.font_weight,
6132 style.font_style,
6133 style.letter_spacing,
6134 style.hyphens,
6135 style.lang.as_deref(),
6136 )
6137 };
6138 word_width + style.padding.horizontal() + style.margin.horizontal()
6139 }
6140 NodeKind::Image { width, .. } => {
6141 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
6142 }
6143 NodeKind::Svg { width, .. } => {
6144 *width + style.padding.horizontal() + style.margin.horizontal()
6145 }
6146 _ => {
6147 if node.children.is_empty() {
6148 style.padding.horizontal()
6149 + style.margin.horizontal()
6150 + style.border_width.horizontal()
6151 } else {
6152 let mut max_child_min = 0.0f64;
6153 for child in &node.children {
6154 let child_style = child.style.resolve(Some(style), 0.0);
6155 let child_min =
6156 self.measure_min_content_width(child, &child_style, font_context);
6157 max_child_min = max_child_min.max(child_min);
6158 }
6159 max_child_min
6160 + style.padding.horizontal()
6161 + style.margin.horizontal()
6162 + style.border_width.horizontal()
6163 }
6164 }
6165 }
6166 }
6167
6168 fn cell_first_line_font_size(&self, cell: &Node, cell_style: &ResolvedStyle, w: f64) -> f64 {
6174 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<f64> {
6175 for ch in &node.children {
6176 let s = ch.style.resolve(Some(parent), w);
6177 match &ch.kind {
6178 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s.font_size),
6179 _ => {
6180 if let Some(f) = first(ch, &s, w) {
6181 return Some(f);
6182 }
6183 }
6184 }
6185 }
6186 None
6187 }
6188 first(cell, cell_style, w).unwrap_or(cell_style.font_size)
6189 }
6190
6191 fn cell_baseline_distance(
6194 &self,
6195 cell: &Node,
6196 cell_style: &ResolvedStyle,
6197 inner_width: f64,
6198 ) -> f64 {
6199 cell_style.padding.top
6200 + cell_style.border_width.top
6201 + self.cell_first_line_font_size(cell, cell_style, inner_width)
6202 }
6203
6204 fn row_baseline(
6207 &self,
6208 row: &Node,
6209 row_style: &ResolvedStyle,
6210 col_widths: &[f64],
6211 ) -> Option<f64> {
6212 let mut b: Option<f64> = None;
6213 let mut col_idx = 0usize;
6214 for cell in row.children.iter() {
6215 let span = match &cell.kind {
6216 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6217 _ => 1,
6218 };
6219 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
6220 col_idx += span;
6221 let cell_style = cell.style.resolve(Some(row_style), col_width);
6222 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6223 let iw = col_width
6224 - cell_style.padding.horizontal()
6225 - cell_style.border_width.horizontal();
6226 let d = self.cell_baseline_distance(cell, &cell_style, iw);
6227 b = Some(b.map_or(d, |m: f64| m.max(d)));
6228 }
6229 }
6230 b
6231 }
6232
6233 fn measure_table_row_height(
6234 &self,
6235 row: &Node,
6236 col_widths: &[f64],
6237 parent_style: &ResolvedStyle,
6238 font_context: &FontContext,
6239 ) -> f64 {
6240 let row_style = row
6241 .style
6242 .resolve(Some(parent_style), col_widths.iter().sum());
6243 let mut max_height: f64 = 0.0;
6244 let row_bl = self.row_baseline(row, &row_style, col_widths);
6247
6248 let mut col_idx = 0usize;
6249 for cell in row.children.iter() {
6250 let span = match &cell.kind {
6251 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
6252 _ => 1,
6253 };
6254 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
6255 col_idx += span;
6256 let cell_style = cell.style.resolve(Some(&row_style), col_width);
6257 let inner_width =
6258 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
6259
6260 let mut cell_content_height = 0.0;
6261 for child in &cell.children {
6262 let child_style = child.style.resolve(Some(&cell_style), inner_width);
6263 cell_content_height +=
6264 self.measure_node_height(child, inner_width, &child_style, font_context);
6265 }
6266
6267 let mut total = cell_content_height
6268 + cell_style.padding.vertical()
6269 + cell_style.border_width.vertical();
6270 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
6274 if let Some(b) = row_bl {
6275 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
6276 total += (b - d).max(0.0);
6277 }
6278 }
6279 if let SizeConstraint::Fixed(h) = cell_style.height {
6286 total = total.max(h);
6287 }
6288 max_height = max_height.max(total);
6289 }
6290
6291 max_height.max(row_style.min_height)
6292 }
6293
6294 fn resolve_column_widths(
6295 &self,
6296 defs: &[ColumnDef],
6297 available_width: f64,
6298 children: &[Node],
6299 ) -> Vec<f64> {
6300 if defs.is_empty() {
6301 let num_cols = children.first().map(|row| row.children.len()).unwrap_or(1);
6302 return vec![available_width / num_cols as f64; num_cols];
6303 }
6304
6305 let mut widths = Vec::new();
6306 let mut remaining = available_width;
6307 let mut auto_count = 0;
6308
6309 for def in defs {
6310 match def.width {
6311 ColumnWidth::Fixed(w) => {
6312 widths.push(w);
6313 remaining -= w;
6314 }
6315 ColumnWidth::Fraction(f) => {
6316 let w = available_width * f;
6317 widths.push(w);
6318 remaining -= w;
6319 }
6320 ColumnWidth::Auto => {
6321 widths.push(0.0);
6322 auto_count += 1;
6323 }
6324 }
6325 }
6326
6327 if auto_count > 0 {
6328 let auto_width = remaining / auto_count as f64;
6329 for (i, def) in defs.iter().enumerate() {
6330 if matches!(def.width, ColumnWidth::Auto) {
6331 widths[i] = auto_width;
6332 }
6333 }
6334 }
6335
6336 widths
6337 }
6338
6339 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
6340 for (page_index, page) in pages.iter_mut().enumerate() {
6341 if !page.watermarks.is_empty() {
6343 let (page_w, page_h) = page.config.size.dimensions();
6344 let cx = page_w / 2.0;
6345 let cy = page_h / 2.0;
6346
6347 let mut watermark_elements = Vec::new();
6348 for wm_node in &page.watermarks {
6349 if let NodeKind::Watermark {
6350 text,
6351 font_size,
6352 angle,
6353 } = &wm_node.kind
6354 {
6355 let style = wm_node.style.resolve(None, page_w);
6356 let color = style.color;
6357 let opacity = style.opacity;
6358 let angle_rad = angle.to_radians();
6359
6360 let italic =
6362 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6363
6364 let shaped = self.text_layout.shape_text(
6366 font_context,
6367 text,
6368 &style.font_family,
6369 style.font_weight,
6370 style.font_style,
6371 );
6372
6373 let mut glyphs = Vec::new();
6374 let mut x_pos = 0.0;
6375 let text_chars: Vec<char> = text.chars().collect();
6376
6377 if let Some(shaped_glyphs) = shaped {
6378 let units_per_em = font_context.units_per_em(
6380 &style.font_family,
6381 style.font_weight,
6382 italic,
6383 ) as f64;
6384
6385 for sg in &shaped_glyphs {
6386 let advance = sg.x_advance as f64 / units_per_em * *font_size;
6387 let cluster_idx = sg.cluster as usize;
6388 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
6389 glyphs.push(PositionedGlyph {
6390 glyph_id: sg.glyph_id,
6391 char_value: ch,
6392 x_offset: x_pos,
6393 y_offset: 0.0,
6394 x_advance: advance,
6395 font_size: *font_size,
6396 font_family: Arc::from(style.font_family.as_str()),
6397 font_weight: style.font_weight,
6398 font_style: style.font_style,
6399 color: Some(color),
6400 href: None,
6401 text_decoration: TextDecoration::None,
6402 letter_spacing: style.letter_spacing,
6403 cluster_text: None,
6404 });
6405 x_pos += advance + style.letter_spacing;
6406 }
6407 } else {
6408 for &ch in &text_chars {
6410 let w = font_context.char_width(
6411 ch,
6412 &style.font_family,
6413 style.font_weight,
6414 italic,
6415 *font_size,
6416 );
6417 glyphs.push(PositionedGlyph {
6418 glyph_id: ch as u16,
6419 char_value: ch,
6420 x_offset: x_pos,
6421 y_offset: 0.0,
6422 x_advance: w,
6423 font_size: *font_size,
6424 font_family: Arc::from(style.font_family.as_str()),
6425 font_weight: style.font_weight,
6426 font_style: style.font_style,
6427 color: Some(color),
6428 href: None,
6429 text_decoration: TextDecoration::None,
6430 letter_spacing: style.letter_spacing,
6431 cluster_text: None,
6432 });
6433 x_pos += w + style.letter_spacing;
6434 }
6435 }
6436
6437 let text_width = x_pos;
6438
6439 let line = TextLine {
6440 x: 0.0,
6441 y: 0.0,
6442 glyphs,
6443 width: text_width,
6444 height: *font_size,
6445 word_spacing: 0.0,
6446 };
6447
6448 watermark_elements.push(LayoutElement {
6449 x: cx,
6450 y: cy,
6451 width: text_width,
6452 height: *font_size,
6453 draw: DrawCommand::Watermark {
6454 lines: vec![line],
6455 color,
6456 opacity,
6457 angle_rad,
6458 font_family: style.font_family.clone(),
6459 },
6460 children: vec![],
6461 node_type: Some("Watermark".to_string()),
6462 resolved_style: None,
6463 source_location: None,
6464 href: None,
6465 bookmark: None,
6466 alt: None,
6467 is_header_row: false,
6468 col_span: 1,
6469 overflow: Overflow::default(),
6470 opacity: 1.0,
6471 });
6472 }
6473 }
6474
6475 watermark_elements.append(&mut page.elements);
6477 page.elements = watermark_elements;
6478 page.watermarks.clear();
6479 }
6480
6481 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
6482 continue;
6483 }
6484
6485 if !page.fixed_header.is_empty() {
6487 let mut hdr_cursor = PageCursor::new(&page.config);
6488 for (node, _h) in &page.fixed_header {
6489 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
6492 continue;
6493 }
6494 let cw = hdr_cursor.content_width;
6495 let cx = hdr_cursor.content_x;
6496 let style = node.style.resolve(None, cw);
6497 self.layout_view(
6498 node,
6499 &style,
6500 &mut hdr_cursor,
6501 &mut Vec::new(),
6502 cx,
6503 cw,
6504 font_context,
6505 );
6506 }
6507 let mut combined = hdr_cursor.elements;
6509 combined.append(&mut page.elements);
6510 page.elements = combined;
6511 }
6512
6513 if !page.fixed_footer.is_empty() {
6518 let mut ftr_cursor = PageCursor::new(&page.config);
6519 let total_ftr: f64 = page
6520 .fixed_footer
6521 .iter()
6522 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
6523 .map(|(_, h)| *h)
6524 .sum();
6525 let target_y = ftr_cursor.content_height - total_ftr;
6526 for (node, _h) in &page.fixed_footer {
6528 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
6529 continue;
6530 }
6531 let cw = ftr_cursor.content_width;
6532 let cx = ftr_cursor.content_x;
6533 let style = node.style.resolve(None, cw);
6534 self.layout_view(
6535 node,
6536 &style,
6537 &mut ftr_cursor,
6538 &mut Vec::new(),
6539 cx,
6540 cw,
6541 font_context,
6542 );
6543 }
6544 for el in &mut ftr_cursor.elements {
6548 offset_element_y(el, target_y);
6549 }
6550 page.elements.extend(ftr_cursor.elements);
6551 }
6552
6553 page.fixed_header.clear();
6555 page.fixed_footer.clear();
6556 }
6557 }
6558
6559 #[allow(clippy::too_many_arguments)]
6564 fn layout_grid_children(
6565 &self,
6566 children: &[Node],
6567 parent_style: &ResolvedStyle,
6568 cursor: &mut PageCursor,
6569 pages: &mut Vec<LayoutPage>,
6570 x: f64,
6571 available_width: f64,
6572 font_context: &FontContext,
6573 ) {
6574 let template_cols = match &parent_style.grid_template_columns {
6575 Some(cols) => cols,
6576 None => return, };
6578
6579 let num_columns = template_cols.len();
6580 if num_columns == 0 || children.is_empty() {
6581 return;
6582 }
6583
6584 let col_gap = parent_style.column_gap;
6585 let row_gap = parent_style.row_gap;
6586
6587 let content_sizes: Vec<f64> = template_cols
6590 .iter()
6591 .map(|track| {
6592 if matches!(track, GridTrackSize::Auto) {
6593 available_width / num_columns as f64
6596 } else {
6597 0.0
6598 }
6599 })
6600 .collect();
6601
6602 let col_widths =
6603 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
6604
6605 let placements: Vec<Option<&GridPlacement>> = children
6607 .iter()
6608 .map(|child| child.style.grid_placement.as_ref())
6609 .collect();
6610
6611 let item_placements = grid::place_items(&placements, num_columns);
6613 let num_rows = grid::compute_num_rows(&item_placements);
6614
6615 if num_rows == 0 {
6616 return;
6617 }
6618
6619 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
6621 for placement in &item_placements {
6622 let cell_width =
6623 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
6624 let child = &children[placement.child_index];
6625 let child_style = child.style.resolve(Some(parent_style), cell_width);
6626 item_heights[placement.child_index] =
6627 self.measure_node_height(child, cell_width, &child_style, font_context);
6628 }
6629
6630 let template_rows = parent_style.grid_template_rows.as_deref();
6632 let mut row_heights = vec![0.0_f64; num_rows];
6633 for placement in &item_placements {
6634 let h = item_heights[placement.child_index];
6635 let span = placement.row_end - placement.row_start;
6636 let per_row = h / span as f64;
6637 for rh in row_heights
6638 .iter_mut()
6639 .take(placement.row_end.min(num_rows))
6640 .skip(placement.row_start)
6641 {
6642 if per_row > *rh {
6643 *rh = per_row;
6644 }
6645 }
6646 }
6647
6648 if let Some(template) = template_rows {
6650 let auto_row = parent_style.grid_auto_rows.as_ref();
6651 for (r, rh) in row_heights.iter_mut().enumerate() {
6652 let track = template.get(r).or(auto_row);
6653 if let Some(track) = track {
6654 match track {
6655 GridTrackSize::Pt(pts) => *rh = *pts,
6656 GridTrackSize::Auto => {} _ => {} }
6659 }
6660 }
6661 }
6662
6663 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
6665 if row_height > cursor.remaining_height() {
6670 pages.push(cursor.finalize());
6671 *cursor = cursor.new_page();
6672 }
6673
6674 let row_start_y = cursor.y;
6675
6676 for placement in &item_placements {
6678 if placement.row_start != row {
6679 continue; }
6681
6682 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
6683 let cell_width =
6684 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
6685
6686 let child = &children[placement.child_index];
6687
6688 self.layout_node(
6689 child,
6690 cursor,
6691 pages,
6692 cell_x,
6693 cell_width,
6694 Some(parent_style),
6695 font_context,
6696 None,
6697 None,
6698 );
6699 cursor.y = row_start_y;
6701 }
6702
6703 cursor.y = row_start_y + row_height + row_gap;
6704 }
6705
6706 if num_rows > 0 {
6708 cursor.y -= row_gap;
6709 }
6710 }
6711}
6712
6713struct FlexItem<'a> {
6714 node: &'a Node,
6715 style: ResolvedStyle,
6716 base_width: f64,
6717 min_content_width: f64,
6718}
6719
6720#[cfg(test)]
6721mod tests {
6722 use super::*;
6723 use crate::font::FontContext;
6724
6725 fn make_text(content: &str, font_size: f64) -> Node {
6726 Node {
6727 kind: NodeKind::Text {
6728 content: content.to_string(),
6729 href: None,
6730 runs: vec![],
6731 },
6732 style: Style {
6733 font_size: Some(font_size),
6734 ..Default::default()
6735 },
6736 children: vec![],
6737 id: None,
6738 source_location: None,
6739 bookmark: None,
6740 href: None,
6741 alt: None,
6742 }
6743 }
6744
6745 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
6746 Node {
6747 kind: NodeKind::View,
6748 style,
6749 children,
6750 id: None,
6751 source_location: None,
6752 bookmark: None,
6753 href: None,
6754 alt: None,
6755 }
6756 }
6757
6758 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
6759 Node {
6760 kind: NodeKind::Text {
6761 content: String::new(),
6762 href: None,
6763 runs,
6764 },
6765 style: Style::default(),
6766 children: vec![],
6767 id: None,
6768 source_location: None,
6769 bookmark: None,
6770 href: None,
6771 alt: None,
6772 }
6773 }
6774
6775 #[test]
6776 fn intrinsic_width_measures_runs_not_just_content() {
6777 let engine = LayoutEngine::new();
6781 let font_context = FontContext::new();
6782
6783 let runs_node = make_runs_text(vec![
6784 crate::model::TextRun {
6785 content: "Hello ".to_string(),
6786 style: Style::default(),
6787 href: None,
6788 },
6789 crate::model::TextRun {
6790 content: "World".to_string(),
6791 style: Style {
6792 font_weight: Some(700),
6793 ..Default::default()
6794 },
6795 href: None,
6796 },
6797 ]);
6798 let plain_node = make_text("Hello World", 12.0);
6799
6800 let runs_style = runs_node.style.resolve(None, 0.0);
6801 let plain_style = plain_node.style.resolve(None, 0.0);
6802 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
6803 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
6804
6805 assert!(
6808 runs_w >= plain_w,
6809 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
6810 );
6811 assert!(
6812 runs_w < plain_w * 1.5,
6813 "runs width ({runs_w}) should be close to plain width ({plain_w})"
6814 );
6815 }
6816
6817 #[test]
6818 fn intrinsic_width_of_multiline_text_is_widest_line() {
6819 let engine = LayoutEngine::new();
6820 let font_context = FontContext::new();
6821
6822 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
6823 let widest = make_text("Springfield, IL 62704", 12.0);
6824
6825 let m_style = multiline.style.resolve(None, 0.0);
6826 let w_style = widest.style.resolve(None, 0.0);
6827 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
6828 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
6829
6830 assert!(
6831 (m_w - w_w).abs() < 0.01,
6832 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
6833 );
6834 }
6835
6836 #[test]
6837 fn intrinsic_width_of_heading_measures_its_text() {
6838 let engine = LayoutEngine::new();
6841 let font_context = FontContext::new();
6842
6843 let heading = Node {
6844 kind: NodeKind::Heading {
6845 level: 1,
6846 content: "Invoice #2024-001".to_string(),
6847 href: None,
6848 runs: vec![],
6849 },
6850 style: Style {
6851 font_size: Some(24.0),
6852 ..Default::default()
6853 },
6854 children: vec![],
6855 id: None,
6856 source_location: None,
6857 bookmark: None,
6858 href: None,
6859 alt: None,
6860 };
6861 let style = heading.style.resolve(None, 0.0);
6862 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
6863 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
6864 }
6865
6866 #[test]
6867 fn measure_node_height_of_wrapping_heading_matches_text() {
6868 let engine = LayoutEngine::new();
6875 let font_context = FontContext::new();
6876
6877 let content = "Annual Performance Review";
6878 let heading = Node {
6879 kind: NodeKind::Heading {
6880 level: 1,
6881 content: content.to_string(),
6882 href: None,
6883 runs: vec![],
6884 },
6885 style: Style {
6886 font_size: Some(32.0),
6887 ..Default::default()
6888 },
6889 children: vec![],
6890 id: None,
6891 source_location: None,
6892 bookmark: None,
6893 href: None,
6894 alt: None,
6895 };
6896 let text = make_text(content, 32.0);
6897
6898 let width = 200.0;
6900 let h_style = heading.style.resolve(None, width);
6901 let t_style = text.style.resolve(None, width);
6902 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
6903 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
6904
6905 assert!(
6906 h_height > 32.0,
6907 "a wrapping 32pt heading must measure more than one line, got {h_height}"
6908 );
6909 assert!(
6910 (h_height - t_height).abs() < 0.01,
6911 "heading height ({h_height}) must equal the same text's height ({t_height})"
6912 );
6913 }
6914
6915 #[test]
6916 fn intrinsic_width_flex_row_sums_children() {
6917 let engine = LayoutEngine::new();
6918 let font_context = FontContext::new();
6919
6920 let child1 = make_text("Hello", 14.0);
6921 let child2 = make_text("World", 14.0);
6922
6923 let child1_style = child1.style.resolve(None, 0.0);
6924 let child2_style = child2.style.resolve(None, 0.0);
6925 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
6926 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
6927
6928 let row = make_styled_view(
6929 Style {
6930 flex_direction: Some(FlexDirection::Row),
6931 ..Default::default()
6932 },
6933 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
6934 );
6935 let row_style = row.style.resolve(None, 0.0);
6936 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
6937
6938 assert!(
6939 (row_w - (child1_w + child2_w)).abs() < 0.01,
6940 "Row intrinsic width ({}) should equal sum of children ({} + {})",
6941 row_w,
6942 child1_w,
6943 child2_w
6944 );
6945 }
6946
6947 #[test]
6948 fn intrinsic_width_flex_column_takes_max() {
6949 let engine = LayoutEngine::new();
6950 let font_context = FontContext::new();
6951
6952 let short = make_text("Hi", 14.0);
6953 let long = make_text("Hello World", 14.0);
6954
6955 let short_style = short.style.resolve(None, 0.0);
6956 let long_style = long.style.resolve(None, 0.0);
6957 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
6958 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
6959
6960 let col = make_styled_view(
6961 Style {
6962 flex_direction: Some(FlexDirection::Column),
6963 ..Default::default()
6964 },
6965 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
6966 );
6967 let col_style = col.style.resolve(None, 0.0);
6968 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
6969
6970 assert!(
6971 (col_w - long_w).abs() < 0.01,
6972 "Column intrinsic width ({}) should equal max child ({}, short was {})",
6973 col_w,
6974 long_w,
6975 short_w
6976 );
6977 }
6978
6979 #[test]
6980 fn intrinsic_width_nested_containers() {
6981 let engine = LayoutEngine::new();
6982 let font_context = FontContext::new();
6983
6984 let inner = make_styled_view(
6985 Style {
6986 flex_direction: Some(FlexDirection::Row),
6987 ..Default::default()
6988 },
6989 vec![make_text("A", 12.0), make_text("B", 12.0)],
6990 );
6991 let inner_style = inner.style.resolve(None, 0.0);
6992 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
6993
6994 let outer = make_styled_view(
6995 Style::default(),
6996 vec![make_styled_view(
6997 Style {
6998 flex_direction: Some(FlexDirection::Row),
6999 ..Default::default()
7000 },
7001 vec![make_text("A", 12.0), make_text("B", 12.0)],
7002 )],
7003 );
7004 let outer_style = outer.style.resolve(None, 0.0);
7005 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
7006
7007 assert!(
7008 (outer_w - inner_w).abs() < 0.01,
7009 "Nested container ({}) should match inner container ({})",
7010 outer_w,
7011 inner_w
7012 );
7013 }
7014
7015 #[test]
7016 fn intrinsic_width_row_with_gap() {
7017 let engine = LayoutEngine::new();
7018 let font_context = FontContext::new();
7019
7020 let no_gap = make_styled_view(
7021 Style {
7022 flex_direction: Some(FlexDirection::Row),
7023 ..Default::default()
7024 },
7025 vec![make_text("A", 12.0), make_text("B", 12.0)],
7026 );
7027 let with_gap = make_styled_view(
7028 Style {
7029 flex_direction: Some(FlexDirection::Row),
7030 gap: Some(10.0),
7031 ..Default::default()
7032 },
7033 vec![make_text("A", 12.0), make_text("B", 12.0)],
7034 );
7035
7036 let no_gap_style = no_gap.style.resolve(None, 0.0);
7037 let with_gap_style = with_gap.style.resolve(None, 0.0);
7038 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
7039 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
7040
7041 assert!(
7042 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
7043 "Gap should add 10pt: with_gap={}, no_gap={}",
7044 with_gap_w,
7045 no_gap_w
7046 );
7047 }
7048
7049 #[test]
7050 fn intrinsic_width_empty_container() {
7051 let engine = LayoutEngine::new();
7052 let font_context = FontContext::new();
7053
7054 let padding = 8.0;
7055 let empty = make_styled_view(
7056 Style {
7057 padding: Some(Edges::uniform(padding)),
7058 ..Default::default()
7059 },
7060 vec![],
7061 );
7062 let style = empty.style.resolve(None, 0.0);
7063 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
7064
7065 assert!(
7066 (w - padding * 2.0).abs() < 0.01,
7067 "Empty container width ({}) should equal horizontal padding ({})",
7068 w,
7069 padding * 2.0
7070 );
7071 }
7072
7073 #[test]
7076 fn flex_shrink_respects_min_content_width() {
7077 let engine = LayoutEngine::new();
7081 let font_context = FontContext::new();
7082
7083 let sale_text = make_text("SALE", 12.0);
7084 let sale_style = sale_text.style.resolve(None, 0.0);
7085 let sale_word_width =
7086 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
7087 assert!(
7088 sale_word_width > 0.0,
7089 "SALE should have non-zero min-content width"
7090 );
7091
7092 let container = make_styled_view(
7095 Style {
7096 flex_direction: Some(FlexDirection::Row),
7097 width: Some(Dimension::Pt(100.0)),
7098 ..Default::default()
7099 },
7100 vec![
7101 make_styled_view(
7102 Style {
7103 width: Some(Dimension::Pt(80.0)),
7104 flex_shrink: Some(1.0),
7105 ..Default::default()
7106 },
7107 vec![],
7108 ),
7109 make_styled_view(
7110 Style {
7111 width: Some(Dimension::Pt(60.0)),
7112 flex_shrink: Some(1.0),
7113 ..Default::default()
7114 },
7115 vec![make_text("SALE", 12.0)],
7116 ),
7117 ],
7118 );
7119
7120 let doc = Document {
7121 children: vec![Node::page(
7122 PageConfig::default(),
7123 Style::default(),
7124 vec![container],
7125 )],
7126 metadata: Default::default(),
7127 default_page: PageConfig::default(),
7128 first_page: None,
7129 left_page: None,
7130 right_page: None,
7131 named_pages: Default::default(),
7132 fonts: vec![],
7133 tagged: false,
7134 pdfa: None,
7135 default_style: None,
7136 embedded_data: None,
7137 flatten_forms: false,
7138 pdf_ua: false,
7139 certification: None,
7140 };
7141
7142 let pages = engine.layout(&doc, &font_context);
7143 assert!(!pages.is_empty());
7144
7145 let page = &pages[0];
7148 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
7150 assert!(
7151 container_el.is_some(),
7152 "Should find container with 2 children"
7153 );
7154 let sale_child = &container_el.unwrap().children[1];
7155 assert!(
7156 sale_child.width >= sale_word_width - 0.01,
7157 "SALE child width ({}) should be >= min-content width ({})",
7158 sale_child.width,
7159 sale_word_width
7160 );
7161 }
7162
7163 #[test]
7166 fn column_justify_content_center() {
7167 let engine = LayoutEngine::new();
7170 let font_context = FontContext::new();
7171
7172 let container = make_styled_view(
7173 Style {
7174 flex_direction: Some(FlexDirection::Column),
7175 height: Some(Dimension::Pt(200.0)),
7176 justify_content: Some(JustifyContent::Center),
7177 ..Default::default()
7178 },
7179 vec![make_text("Centered", 12.0)],
7180 );
7181
7182 let doc = Document {
7183 children: vec![Node::page(
7184 PageConfig::default(),
7185 Style::default(),
7186 vec![container],
7187 )],
7188 metadata: Default::default(),
7189 default_page: PageConfig::default(),
7190 first_page: None,
7191 left_page: None,
7192 right_page: None,
7193 named_pages: Default::default(),
7194 fonts: vec![],
7195 tagged: false,
7196 pdfa: None,
7197 default_style: None,
7198 embedded_data: None,
7199 flatten_forms: false,
7200 pdf_ua: false,
7201 certification: None,
7202 };
7203
7204 let pages = engine.layout(&doc, &font_context);
7205 let page = &pages[0];
7206
7207 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7210 assert!(
7211 container_el.is_some(),
7212 "Should find container with children"
7213 );
7214 let container_el = container_el.unwrap();
7215 let child = &container_el.children[0];
7216
7217 let child_offset = child.y - container_el.y;
7219 let expected_offset = (200.0 - child.height) / 2.0;
7220 assert!(
7221 (child_offset - expected_offset).abs() < 2.0,
7222 "Child offset ({}) should be near center ({})",
7223 child_offset,
7224 expected_offset
7225 );
7226 }
7227
7228 #[test]
7229 fn column_align_items_center() {
7230 let engine = LayoutEngine::new();
7233 let font_context = FontContext::new();
7234
7235 let container = make_styled_view(
7236 Style {
7237 flex_direction: Some(FlexDirection::Column),
7238 width: Some(Dimension::Pt(300.0)),
7239 align_items: Some(AlignItems::Center),
7240 ..Default::default()
7241 },
7242 vec![make_text("Hi", 12.0)],
7243 );
7244
7245 let doc = Document {
7246 children: vec![Node::page(
7247 PageConfig::default(),
7248 Style::default(),
7249 vec![container],
7250 )],
7251 metadata: Default::default(),
7252 default_page: PageConfig::default(),
7253 first_page: None,
7254 left_page: None,
7255 right_page: None,
7256 named_pages: Default::default(),
7257 fonts: vec![],
7258 tagged: false,
7259 pdfa: None,
7260 default_style: None,
7261 embedded_data: None,
7262 flatten_forms: false,
7263 pdf_ua: false,
7264 certification: None,
7265 };
7266
7267 let pages = engine.layout(&doc, &font_context);
7268 let page = &pages[0];
7269
7270 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7271 assert!(container_el.is_some());
7272 let container_el = container_el.unwrap();
7273 let child = &container_el.children[0];
7274
7275 let child_center = child.x + child.width / 2.0;
7277 let container_center = container_el.x + container_el.width / 2.0;
7278 assert!(
7279 (child_center - container_center).abs() < 2.0,
7280 "Child center ({}) should be near container center ({})",
7281 child_center,
7282 container_center
7283 );
7284 }
7285
7286 #[test]
7289 fn absolute_child_positioned_relative_to_parent() {
7290 let engine = LayoutEngine::new();
7294 let font_context = FontContext::new();
7295
7296 let parent = make_styled_view(
7297 Style {
7298 position: Some(crate::model::Position::Relative),
7299 margin: Some(MarginEdges::from_edges(Edges {
7300 top: 50.0,
7301 left: 50.0,
7302 ..Default::default()
7303 })),
7304 width: Some(Dimension::Pt(200.0)),
7305 height: Some(Dimension::Pt(200.0)),
7306 ..Default::default()
7307 },
7308 vec![make_styled_view(
7309 Style {
7310 position: Some(crate::model::Position::Absolute),
7311 top: Some(10.0),
7312 left: Some(10.0),
7313 width: Some(Dimension::Pt(50.0)),
7314 height: Some(Dimension::Pt(50.0)),
7315 ..Default::default()
7316 },
7317 vec![],
7318 )],
7319 );
7320
7321 let doc = Document {
7322 children: vec![Node::page(
7323 PageConfig::default(),
7324 Style::default(),
7325 vec![parent],
7326 )],
7327 metadata: Default::default(),
7328 default_page: PageConfig::default(),
7329 first_page: None,
7330 left_page: None,
7331 right_page: None,
7332 named_pages: Default::default(),
7333 fonts: vec![],
7334 tagged: false,
7335 pdfa: None,
7336 default_style: None,
7337 embedded_data: None,
7338 flatten_forms: false,
7339 pdf_ua: false,
7340 certification: None,
7341 };
7342
7343 let pages = engine.layout(&doc, &font_context);
7344 let page = &pages[0];
7345
7346 let parent_el = page
7349 .elements
7350 .iter()
7351 .find(|e| e.width > 190.0 && e.width < 210.0);
7352 assert!(parent_el.is_some(), "Should find the 200x200 parent");
7353 let parent_el = parent_el.unwrap();
7354
7355 let abs_child = parent_el
7357 .children
7358 .iter()
7359 .find(|e| e.width > 45.0 && e.width < 55.0);
7360 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
7361 let abs_child = abs_child.unwrap();
7362
7363 let expected_x = parent_el.x + 10.0;
7364 let expected_y = parent_el.y + 10.0;
7365 assert!(
7366 (abs_child.x - expected_x).abs() < 1.0,
7367 "Absolute child x ({}) should be parent.x + 10 ({})",
7368 abs_child.x,
7369 expected_x
7370 );
7371 assert!(
7372 (abs_child.y - expected_y).abs() < 1.0,
7373 "Absolute child y ({}) should be parent.y + 10 ({})",
7374 abs_child.y,
7375 expected_y
7376 );
7377 }
7378
7379 #[test]
7380 fn absolute_escapes_unpositioned_parent_to_page() {
7381 let engine = LayoutEngine::new();
7386 let font_context = FontContext::new();
7387 let parent = make_styled_view(
7388 Style {
7389 margin: Some(MarginEdges::from_edges(Edges {
7390 top: 50.0,
7391 left: 50.0,
7392 ..Default::default()
7393 })),
7394 width: Some(Dimension::Pt(200.0)),
7395 height: Some(Dimension::Pt(200.0)),
7396 ..Default::default()
7397 },
7398 vec![make_styled_view(
7399 Style {
7400 position: Some(crate::model::Position::Absolute),
7401 top: Some(10.0),
7402 left: Some(10.0),
7403 width: Some(Dimension::Pt(50.0)),
7404 height: Some(Dimension::Pt(50.0)),
7405 ..Default::default()
7406 },
7407 vec![],
7408 )],
7409 );
7410 let doc = Document {
7411 children: vec![Node::page(
7412 PageConfig::default(),
7413 Style::default(),
7414 vec![parent],
7415 )],
7416 metadata: Default::default(),
7417 default_page: PageConfig::default(),
7418 first_page: None,
7419 left_page: None,
7420 right_page: None,
7421 named_pages: Default::default(),
7422 fonts: vec![],
7423 tagged: false,
7424 pdfa: None,
7425 default_style: None,
7426 embedded_data: None,
7427 flatten_forms: false,
7428 pdf_ua: false,
7429 certification: None,
7430 };
7431 let pages = engine.layout(&doc, &font_context);
7432 let page = &pages[0];
7433 let parent_el = page
7434 .elements
7435 .iter()
7436 .find(|e| e.width > 190.0 && e.width < 210.0)
7437 .expect("parent");
7438 let abs_child = parent_el
7439 .children
7440 .iter()
7441 .find(|e| e.width > 45.0 && e.width < 55.0)
7442 .expect("abs child");
7443 let page_left = PageConfig::default().margin.left;
7444 let page_top = PageConfig::default().margin.top;
7445 assert!(
7446 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
7447 "absolute escapes to the page: x {} should be page_left + 10 ({})",
7448 abs_child.x,
7449 page_left + 10.0
7450 );
7451 assert!(
7452 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
7453 "absolute escapes to the page: y {} should be page_top + 10 ({})",
7454 abs_child.y,
7455 page_top + 10.0
7456 );
7457 assert!(
7458 abs_child.x < parent_el.x,
7459 "child must no longer be parent-relative (parent is 50pt further in)"
7460 );
7461 }
7462
7463 #[test]
7464 fn text_transform_none_passthrough() {
7465 assert_eq!(
7466 apply_text_transform("Hello World", TextTransform::None),
7467 "Hello World"
7468 );
7469 }
7470
7471 #[test]
7472 fn text_transform_uppercase() {
7473 assert_eq!(
7474 apply_text_transform("hello world", TextTransform::Uppercase),
7475 "HELLO WORLD"
7476 );
7477 }
7478
7479 #[test]
7480 fn text_transform_lowercase() {
7481 assert_eq!(
7482 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
7483 "hello world"
7484 );
7485 }
7486
7487 #[test]
7488 fn text_transform_capitalize() {
7489 assert_eq!(
7490 apply_text_transform("hello world", TextTransform::Capitalize),
7491 "Hello World"
7492 );
7493 assert_eq!(
7494 apply_text_transform(" hello world ", TextTransform::Capitalize),
7495 " Hello World "
7496 );
7497 assert_eq!(
7498 apply_text_transform("already Capitalized", TextTransform::Capitalize),
7499 "Already Capitalized"
7500 );
7501 }
7502
7503 #[test]
7504 fn text_transform_capitalize_empty() {
7505 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
7506 }
7507
7508 #[test]
7509 fn apply_char_transform_uppercase() {
7510 assert_eq!(
7511 apply_char_transform('a', TextTransform::Uppercase, false),
7512 'A'
7513 );
7514 assert_eq!(
7515 apply_char_transform('A', TextTransform::Uppercase, false),
7516 'A'
7517 );
7518 }
7519
7520 #[test]
7521 fn apply_char_transform_capitalize_word_start() {
7522 assert_eq!(
7523 apply_char_transform('h', TextTransform::Capitalize, true),
7524 'H'
7525 );
7526 assert_eq!(
7527 apply_char_transform('h', TextTransform::Capitalize, false),
7528 'h'
7529 );
7530 }
7531
7532 #[test]
7535 fn column_flex_grow_single_child_fills_container() {
7536 let engine = LayoutEngine::new();
7539 let font_context = FontContext::new();
7540
7541 let child = make_styled_view(
7542 Style {
7543 flex_grow: Some(1.0),
7544 ..Default::default()
7545 },
7546 vec![make_text("Short", 12.0)],
7547 );
7548
7549 let container = make_styled_view(
7550 Style {
7551 flex_direction: Some(FlexDirection::Column),
7552 height: Some(Dimension::Pt(300.0)),
7553 ..Default::default()
7554 },
7555 vec![child],
7556 );
7557
7558 let doc = Document {
7559 children: vec![Node::page(
7560 PageConfig::default(),
7561 Style::default(),
7562 vec![container],
7563 )],
7564 metadata: Default::default(),
7565 default_page: PageConfig::default(),
7566 first_page: None,
7567 left_page: None,
7568 right_page: None,
7569 named_pages: Default::default(),
7570 fonts: vec![],
7571 tagged: false,
7572 pdfa: None,
7573 default_style: None,
7574 embedded_data: None,
7575 flatten_forms: false,
7576 pdf_ua: false,
7577 certification: None,
7578 };
7579
7580 let pages = engine.layout(&doc, &font_context);
7581 let page = &pages[0];
7582
7583 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7584 assert!(container_el.is_some());
7585 let container_el = container_el.unwrap();
7586 assert!(
7587 (container_el.height - 300.0).abs() < 1.0,
7588 "Container should be 300pt, got {}",
7589 container_el.height
7590 );
7591
7592 let child_el = &container_el.children[0];
7593 assert!(
7594 (child_el.height - 300.0).abs() < 1.0,
7595 "flex-grow child should expand to 300pt, got {}",
7596 child_el.height
7597 );
7598 }
7599
7600 #[test]
7601 fn column_flex_grow_two_children_proportional() {
7602 let engine = LayoutEngine::new();
7605 let font_context = FontContext::new();
7606
7607 let child1 = make_styled_view(
7608 Style {
7609 flex_grow: Some(1.0),
7610 ..Default::default()
7611 },
7612 vec![make_text("A", 12.0)],
7613 );
7614 let child2 = make_styled_view(
7615 Style {
7616 flex_grow: Some(2.0),
7617 ..Default::default()
7618 },
7619 vec![make_text("B", 12.0)],
7620 );
7621
7622 let container = make_styled_view(
7623 Style {
7624 flex_direction: Some(FlexDirection::Column),
7625 height: Some(Dimension::Pt(300.0)),
7626 ..Default::default()
7627 },
7628 vec![child1, child2],
7629 );
7630
7631 let doc = Document {
7632 children: vec![Node::page(
7633 PageConfig::default(),
7634 Style::default(),
7635 vec![container],
7636 )],
7637 metadata: Default::default(),
7638 default_page: PageConfig::default(),
7639 first_page: None,
7640 left_page: None,
7641 right_page: None,
7642 named_pages: Default::default(),
7643 fonts: vec![],
7644 tagged: false,
7645 pdfa: None,
7646 default_style: None,
7647 embedded_data: None,
7648 flatten_forms: false,
7649 pdf_ua: false,
7650 certification: None,
7651 };
7652
7653 let pages = engine.layout(&doc, &font_context);
7654 let page = &pages[0];
7655
7656 let container_el = page
7657 .elements
7658 .iter()
7659 .find(|e| e.children.len() == 2)
7660 .expect("Should find container with two children");
7661
7662 let c1 = &container_el.children[0];
7663 let c2 = &container_el.children[1];
7664
7665 let total = c1.height + c2.height;
7669 assert!(
7670 (total - 300.0).abs() < 2.0,
7671 "Children should sum to ~300pt, got {}",
7672 total
7673 );
7674
7675 let ratio = c2.height / c1.height;
7678 assert!(
7679 ratio > 1.3 && ratio < 2.5,
7680 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
7681 ratio
7682 );
7683 }
7684
7685 #[test]
7686 fn column_flex_grow_mixed_grow_and_fixed() {
7687 let engine = LayoutEngine::new();
7690 let font_context = FontContext::new();
7691
7692 let fixed_child = make_styled_view(
7693 Style {
7694 height: Some(Dimension::Pt(50.0)),
7695 ..Default::default()
7696 },
7697 vec![make_text("Fixed", 12.0)],
7698 );
7699 let grow_child = make_styled_view(
7700 Style {
7701 flex_grow: Some(1.0),
7702 ..Default::default()
7703 },
7704 vec![make_text("Grow", 12.0)],
7705 );
7706
7707 let container = make_styled_view(
7708 Style {
7709 flex_direction: Some(FlexDirection::Column),
7710 height: Some(Dimension::Pt(300.0)),
7711 ..Default::default()
7712 },
7713 vec![fixed_child, grow_child],
7714 );
7715
7716 let doc = Document {
7717 children: vec![Node::page(
7718 PageConfig::default(),
7719 Style::default(),
7720 vec![container],
7721 )],
7722 metadata: Default::default(),
7723 default_page: PageConfig::default(),
7724 first_page: None,
7725 left_page: None,
7726 right_page: None,
7727 named_pages: Default::default(),
7728 fonts: vec![],
7729 tagged: false,
7730 pdfa: None,
7731 default_style: None,
7732 embedded_data: None,
7733 flatten_forms: false,
7734 pdf_ua: false,
7735 certification: None,
7736 };
7737
7738 let pages = engine.layout(&doc, &font_context);
7739 let page = &pages[0];
7740
7741 let container_el = page
7742 .elements
7743 .iter()
7744 .find(|e| e.children.len() == 2)
7745 .expect("Should find container with two children");
7746
7747 let fixed_el = &container_el.children[0];
7748 let grow_el = &container_el.children[1];
7749
7750 assert!(
7752 (fixed_el.height - 50.0).abs() < 1.0,
7753 "Fixed child should stay at 50pt, got {}",
7754 fixed_el.height
7755 );
7756
7757 assert!(
7759 (grow_el.height - 250.0).abs() < 2.0,
7760 "Grow child should expand to ~250pt, got {}",
7761 grow_el.height
7762 );
7763 }
7764
7765 #[test]
7766 fn column_flex_grow_page_level() {
7767 let engine = LayoutEngine::new();
7769 let font_context = FontContext::new();
7770
7771 let grow_child = make_styled_view(
7772 Style {
7773 flex_grow: Some(1.0),
7774 ..Default::default()
7775 },
7776 vec![make_text("Fill page", 12.0)],
7777 );
7778
7779 let doc = Document {
7780 children: vec![Node::page(
7781 PageConfig::default(),
7782 Style::default(),
7783 vec![grow_child],
7784 )],
7785 metadata: Default::default(),
7786 default_page: PageConfig::default(),
7787 first_page: None,
7788 left_page: None,
7789 right_page: None,
7790 named_pages: Default::default(),
7791 fonts: vec![],
7792 tagged: false,
7793 pdfa: None,
7794 default_style: None,
7795 embedded_data: None,
7796 flatten_forms: false,
7797 pdf_ua: false,
7798 certification: None,
7799 };
7800
7801 let pages = engine.layout(&doc, &font_context);
7802 let page = &pages[0];
7803
7804 assert!(
7806 !page.elements.is_empty(),
7807 "Page should have at least one element"
7808 );
7809
7810 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
7811 let el = &page.elements[0];
7812 assert!(
7813 (el.height - content_height).abs() < 2.0,
7814 "Page-level flex-grow child should fill content height ({}), got {}",
7815 content_height,
7816 el.height
7817 );
7818 }
7819
7820 #[test]
7821 fn column_flex_grow_with_justify_content() {
7822 let engine = LayoutEngine::new();
7826 let font_context = FontContext::new();
7827
7828 let fixed_child = make_styled_view(
7829 Style {
7830 height: Some(Dimension::Pt(50.0)),
7831 ..Default::default()
7832 },
7833 vec![make_text("Top", 12.0)],
7834 );
7835 let grow_child = make_styled_view(
7836 Style {
7837 flex_grow: Some(1.0),
7838 ..Default::default()
7839 },
7840 vec![make_text("Fill", 12.0)],
7841 );
7842
7843 let container = make_styled_view(
7844 Style {
7845 flex_direction: Some(FlexDirection::Column),
7846 height: Some(Dimension::Pt(300.0)),
7847 justify_content: Some(JustifyContent::Center),
7848 ..Default::default()
7849 },
7850 vec![fixed_child, grow_child],
7851 );
7852
7853 let doc = Document {
7854 children: vec![Node::page(
7855 PageConfig::default(),
7856 Style::default(),
7857 vec![container],
7858 )],
7859 metadata: Default::default(),
7860 default_page: PageConfig::default(),
7861 first_page: None,
7862 left_page: None,
7863 right_page: None,
7864 named_pages: Default::default(),
7865 fonts: vec![],
7866 tagged: false,
7867 pdfa: None,
7868 default_style: None,
7869 embedded_data: None,
7870 flatten_forms: false,
7871 pdf_ua: false,
7872 certification: None,
7873 };
7874
7875 let pages = engine.layout(&doc, &font_context);
7876 let page = &pages[0];
7877
7878 let container_el = page
7879 .elements
7880 .iter()
7881 .find(|e| e.children.len() == 2)
7882 .expect("Should find container");
7883
7884 let first_child = &container_el.children[0];
7887 assert!(
7888 (first_child.y - container_el.y).abs() < 1.0,
7889 "First child should be at top of container"
7890 );
7891
7892 let total = container_el.children[0].height + container_el.children[1].height;
7894 assert!(
7895 (total - 300.0).abs() < 2.0,
7896 "Children should fill container, got {}",
7897 total
7898 );
7899 }
7900
7901 #[test]
7902 fn column_flex_grow_child_justify_content_center() {
7903 let engine = LayoutEngine::new();
7906 let font_context = FontContext::new();
7907
7908 let inner_box = make_styled_view(
7910 Style {
7911 height: Some(Dimension::Pt(40.0)),
7912 ..Default::default()
7913 },
7914 vec![make_text("Centered", 12.0)],
7915 );
7916
7917 let grow_child = make_styled_view(
7919 Style {
7920 flex_grow: Some(1.0),
7921 flex_direction: Some(FlexDirection::Column),
7922 justify_content: Some(JustifyContent::Center),
7923 ..Default::default()
7924 },
7925 vec![inner_box],
7926 );
7927
7928 let container = make_styled_view(
7930 Style {
7931 flex_direction: Some(FlexDirection::Column),
7932 height: Some(Dimension::Pt(400.0)),
7933 ..Default::default()
7934 },
7935 vec![grow_child],
7936 );
7937
7938 let doc = Document {
7939 children: vec![Node::page(
7940 PageConfig::default(),
7941 Style::default(),
7942 vec![container],
7943 )],
7944 metadata: Default::default(),
7945 default_page: PageConfig::default(),
7946 first_page: None,
7947 left_page: None,
7948 right_page: None,
7949 named_pages: Default::default(),
7950 fonts: vec![],
7951 tagged: false,
7952 pdfa: None,
7953 default_style: None,
7954 embedded_data: None,
7955 flatten_forms: false,
7956 pdf_ua: false,
7957 certification: None,
7958 };
7959
7960 let pages = engine.layout(&doc, &font_context);
7961 let page = &pages[0];
7962
7963 let container_el = page
7965 .elements
7966 .iter()
7967 .find(|e| e.height > 350.0 && e.children.len() == 1)
7968 .expect("Should find outer container");
7969
7970 let grow_el = &container_el.children[0];
7971 assert!(
7972 (grow_el.height - 400.0).abs() < 2.0,
7973 "Grow child should expand to 400, got {}",
7974 grow_el.height
7975 );
7976
7977 let inner_el = &grow_el.children[0];
7979 let expected_center = grow_el.y + grow_el.height / 2.0;
7980 let actual_center = inner_el.y + inner_el.height / 2.0;
7981 assert!(
7982 (actual_center - expected_center).abs() < 2.0,
7983 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
7984 expected_center,
7985 actual_center
7986 );
7987 }
7988
7989 #[test]
7990 fn column_flex_grow_child_justify_content_flex_end() {
7991 let engine = LayoutEngine::new();
7993 let font_context = FontContext::new();
7994
7995 let inner_box = make_styled_view(
7996 Style {
7997 height: Some(Dimension::Pt(30.0)),
7998 ..Default::default()
7999 },
8000 vec![make_text("Bottom", 12.0)],
8001 );
8002
8003 let grow_child = make_styled_view(
8004 Style {
8005 flex_grow: Some(1.0),
8006 flex_direction: Some(FlexDirection::Column),
8007 justify_content: Some(JustifyContent::FlexEnd),
8008 ..Default::default()
8009 },
8010 vec![inner_box],
8011 );
8012
8013 let container = make_styled_view(
8014 Style {
8015 flex_direction: Some(FlexDirection::Column),
8016 height: Some(Dimension::Pt(300.0)),
8017 ..Default::default()
8018 },
8019 vec![grow_child],
8020 );
8021
8022 let doc = Document {
8023 children: vec![Node::page(
8024 PageConfig::default(),
8025 Style::default(),
8026 vec![container],
8027 )],
8028 metadata: Default::default(),
8029 default_page: PageConfig::default(),
8030 first_page: None,
8031 left_page: None,
8032 right_page: None,
8033 named_pages: Default::default(),
8034 fonts: vec![],
8035 tagged: false,
8036 pdfa: None,
8037 default_style: None,
8038 embedded_data: None,
8039 flatten_forms: false,
8040 pdf_ua: false,
8041 certification: None,
8042 };
8043
8044 let pages = engine.layout(&doc, &font_context);
8045 let page = &pages[0];
8046
8047 let container_el = page
8048 .elements
8049 .iter()
8050 .find(|e| e.height > 250.0 && e.children.len() == 1)
8051 .expect("Should find outer container");
8052
8053 let grow_el = &container_el.children[0];
8054 let inner_el = &grow_el.children[0];
8055
8056 let inner_bottom = inner_el.y + inner_el.height;
8058 let grow_bottom = grow_el.y + grow_el.height;
8059 assert!(
8060 (inner_bottom - grow_bottom).abs() < 2.0,
8061 "Inner box bottom ({}) should align with grow child bottom ({})",
8062 inner_bottom,
8063 grow_bottom
8064 );
8065 }
8066
8067 #[test]
8068 fn column_flex_grow_child_no_justify_unchanged() {
8069 let engine = LayoutEngine::new();
8071 let font_context = FontContext::new();
8072
8073 let inner_box = make_styled_view(
8074 Style {
8075 height: Some(Dimension::Pt(50.0)),
8076 ..Default::default()
8077 },
8078 vec![make_text("Top", 12.0)],
8079 );
8080
8081 let grow_child = make_styled_view(
8082 Style {
8083 flex_grow: Some(1.0),
8084 flex_direction: Some(FlexDirection::Column),
8085 ..Default::default()
8087 },
8088 vec![inner_box],
8089 );
8090
8091 let container = make_styled_view(
8092 Style {
8093 flex_direction: Some(FlexDirection::Column),
8094 height: Some(Dimension::Pt(300.0)),
8095 ..Default::default()
8096 },
8097 vec![grow_child],
8098 );
8099
8100 let doc = Document {
8101 children: vec![Node::page(
8102 PageConfig::default(),
8103 Style::default(),
8104 vec![container],
8105 )],
8106 metadata: Default::default(),
8107 default_page: PageConfig::default(),
8108 first_page: None,
8109 left_page: None,
8110 right_page: None,
8111 named_pages: Default::default(),
8112 fonts: vec![],
8113 tagged: false,
8114 pdfa: None,
8115 default_style: None,
8116 embedded_data: None,
8117 flatten_forms: false,
8118 pdf_ua: false,
8119 certification: None,
8120 };
8121
8122 let pages = engine.layout(&doc, &font_context);
8123 let page = &pages[0];
8124
8125 let container_el = page
8126 .elements
8127 .iter()
8128 .find(|e| e.height > 250.0 && e.children.len() == 1)
8129 .expect("Should find outer container");
8130
8131 let grow_el = &container_el.children[0];
8132 let inner_el = &grow_el.children[0];
8133
8134 assert!(
8136 (inner_el.y - grow_el.y).abs() < 2.0,
8137 "Inner box ({}) should be at top of grow child ({})",
8138 inner_el.y,
8139 grow_el.y
8140 );
8141 }
8142
8143 #[test]
8144 fn column_flex_grow_child_align_items_center() {
8145 let engine = LayoutEngine::new();
8147 let font_context = FontContext::new();
8148
8149 let text = make_text("Hello", 12.0);
8150
8151 let grow_child = make_styled_view(
8152 Style {
8153 flex_grow: Some(1.0),
8154 flex_direction: Some(FlexDirection::Column),
8155 align_items: Some(AlignItems::Center),
8156 ..Default::default()
8157 },
8158 vec![text],
8159 );
8160
8161 let container = make_styled_view(
8162 Style {
8163 flex_direction: Some(FlexDirection::Column),
8164 height: Some(Dimension::Pt(300.0)),
8165 ..Default::default()
8166 },
8167 vec![grow_child],
8168 );
8169
8170 let doc = Document {
8171 children: vec![Node::page(
8172 PageConfig::default(),
8173 Style::default(),
8174 vec![container],
8175 )],
8176 metadata: Default::default(),
8177 default_page: PageConfig::default(),
8178 first_page: None,
8179 left_page: None,
8180 right_page: None,
8181 named_pages: Default::default(),
8182 fonts: vec![],
8183 tagged: false,
8184 pdfa: None,
8185 default_style: None,
8186 embedded_data: None,
8187 flatten_forms: false,
8188 pdf_ua: false,
8189 certification: None,
8190 };
8191
8192 let pages = engine.layout(&doc, &font_context);
8193 let page = &pages[0];
8194
8195 let container_el = page
8196 .elements
8197 .iter()
8198 .find(|e| e.height > 250.0 && e.children.len() == 1)
8199 .expect("Should find outer container");
8200
8201 let grow_el = &container_el.children[0];
8202 assert!(
8203 !grow_el.children.is_empty(),
8204 "Grow child should have text child"
8205 );
8206
8207 let text_el = &grow_el.children[0];
8208 let text_center = text_el.x + text_el.width / 2.0;
8209 let grow_center = grow_el.x + grow_el.width / 2.0;
8210 assert!(
8211 (text_center - grow_center).abs() < 2.0,
8212 "Text center ({}) should be near grow child center ({})",
8213 text_center,
8214 grow_center
8215 );
8216 }
8217
8218 #[test]
8219 fn image_intrinsic_width_respects_height_constraint() {
8220 let engine = LayoutEngine::new();
8224 let font_context = FontContext::new();
8225
8226 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
8228
8229 let image_node = Node {
8230 kind: NodeKind::Image {
8231 src: one_px_png.to_string(),
8232 width: None,
8233 height: Some(36.0),
8234 },
8235 style: Style::default(),
8236 children: vec![],
8237 id: None,
8238 source_location: None,
8239 bookmark: None,
8240 href: None,
8241 alt: None,
8242 };
8243
8244 let resolved = image_node.style.resolve(None, 0.0);
8245 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
8246
8247 assert!(
8249 (intrinsic - 36.0).abs() < 1.0,
8250 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
8251 intrinsic
8252 );
8253 }
8254}