1pub mod audit;
35pub mod flex;
36pub mod grid;
37pub mod page_break;
38
39use std::cell::RefCell;
40use std::collections::HashMap;
41use std::sync::Arc;
42
43use serde::Serialize;
44
45use crate::font::FontContext;
46use crate::model::*;
47use crate::style::*;
48use crate::text::bidi;
49use crate::text::shaping;
50use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
51
52#[derive(Debug, Clone, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookmarkEntry {
56 pub title: String,
57 pub page_index: usize,
58 pub y: f64,
59}
60
61#[derive(Debug, Clone, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct LayoutInfo {
67 pub pages: Vec<PageInfo>,
68}
69
70#[derive(Debug, Clone, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct PageInfo {
74 pub width: f64,
75 pub height: f64,
76 pub content_x: f64,
77 pub content_y: f64,
78 pub content_width: f64,
79 pub content_height: f64,
80 pub elements: Vec<ElementInfo>,
81}
82
83#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct ElementStyleInfo {
87 pub margin: Edges,
89 pub padding: Edges,
90 pub border_width: Edges,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub width: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub height: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_width: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub min_height: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_width: Option<f64>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub max_height: Option<f64>,
103 pub flex_direction: FlexDirection,
105 pub justify_content: JustifyContent,
106 pub align_items: AlignItems,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub align_self: Option<AlignItems>,
109 pub flex_wrap: FlexWrap,
110 pub align_content: AlignContent,
111 pub flex_grow: f64,
112 pub flex_shrink: f64,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub flex_basis: Option<String>,
115 pub gap: f64,
116 pub row_gap: f64,
117 pub column_gap: f64,
118 pub font_family: String,
120 pub font_size: f64,
121 pub font_weight: u32,
122 pub font_style: FontStyle,
123 pub line_height: f64,
124 pub text_align: TextAlign,
125 pub letter_spacing: f64,
126 pub word_spacing: f64,
127 pub text_decoration: TextDecoration,
128 pub text_transform: TextTransform,
129 pub color: Color,
131 pub background_color: Option<Color>,
132 pub border_color: EdgeValues<Color>,
133 pub border_style: EdgeValues<crate::style::BorderStyle>,
134 pub border_radius: CornerValues,
135 pub opacity: f64,
136 pub position: Position,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub top: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub right: Option<f64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub bottom: Option<f64>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub left: Option<f64>,
146 pub overflow: Overflow,
148 pub breakable: bool,
150 pub break_before: bool,
151 pub min_widow_lines: u32,
152 pub min_orphan_lines: u32,
153}
154
155fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
156 match sc {
157 SizeConstraint::Auto => None,
158 SizeConstraint::Fixed(v) => Some(format!("{v}")),
159 }
160}
161
162impl ElementStyleInfo {
163 fn from_resolved(style: &ResolvedStyle) -> Self {
164 ElementStyleInfo {
165 margin: style.margin.to_edges(),
166 padding: style.padding,
167 border_width: style.border_width,
168 width: size_constraint_to_str(&style.width),
169 height: size_constraint_to_str(&style.height),
170 min_width: if style.min_width > 0.0 {
171 Some(style.min_width)
172 } else {
173 None
174 },
175 min_height: if style.min_height > 0.0 {
176 Some(style.min_height)
177 } else {
178 None
179 },
180 max_width: if style.max_width.is_finite() {
181 Some(style.max_width)
182 } else {
183 None
184 },
185 max_height: if style.max_height.is_finite() {
186 Some(style.max_height)
187 } else {
188 None
189 },
190 flex_direction: style.flex_direction,
191 justify_content: style.justify_content,
192 align_items: style.align_items,
193 align_self: style.align_self,
194 flex_wrap: style.flex_wrap,
195 align_content: style.align_content,
196 flex_grow: style.flex_grow,
197 flex_shrink: style.flex_shrink,
198 flex_basis: size_constraint_to_str(&style.flex_basis),
199 gap: style.gap,
200 row_gap: style.row_gap,
201 column_gap: style.column_gap,
202 font_family: style.font_family.clone(),
203 font_size: style.font_size,
204 font_weight: style.font_weight,
205 font_style: style.font_style,
206 line_height: style.line_height,
207 text_align: style.text_align,
208 letter_spacing: style.letter_spacing,
209 word_spacing: style.word_spacing,
210 text_decoration: style.text_decoration,
211 text_transform: style.text_transform,
212 color: style.color,
213 background_color: style.background_color,
214 border_color: style.border_color,
215 border_style: style.border_style,
216 border_radius: style.border_radius,
217 opacity: style.opacity,
218 position: style.position,
219 top: style.top,
220 right: style.right,
221 bottom: style.bottom,
222 left: style.left,
223 overflow: style.overflow,
224 breakable: style.breakable,
225 break_before: style.break_before,
226 min_widow_lines: style.min_widow_lines,
227 min_orphan_lines: style.min_orphan_lines,
228 }
229 }
230}
231
232impl Default for ElementStyleInfo {
233 fn default() -> Self {
234 ElementStyleInfo {
235 margin: Edges::default(),
236 padding: Edges::default(),
237 border_width: Edges::default(),
238 width: None,
239 height: None,
240 min_width: None,
241 min_height: None,
242 max_width: None,
243 max_height: None,
244 flex_direction: FlexDirection::default(),
245 justify_content: JustifyContent::default(),
246 align_items: AlignItems::default(),
247 align_self: None,
248 flex_wrap: FlexWrap::default(),
249 align_content: AlignContent::default(),
250 flex_grow: 0.0,
251 flex_shrink: 1.0,
252 flex_basis: None,
253 gap: 0.0,
254 row_gap: 0.0,
255 column_gap: 0.0,
256 font_family: "Helvetica".to_string(),
257 font_size: 12.0,
258 font_weight: 400,
259 font_style: FontStyle::default(),
260 line_height: 1.4,
261 text_align: TextAlign::default(),
262 letter_spacing: 0.0,
263 word_spacing: 0.0,
264 text_decoration: TextDecoration::None,
265 text_transform: TextTransform::None,
266 color: Color::BLACK,
267 background_color: None,
268 border_color: EdgeValues::uniform(Color::BLACK),
269 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
270 border_radius: CornerValues::uniform(0.0),
271 opacity: 1.0,
272 position: Position::default(),
273 top: None,
274 right: None,
275 bottom: None,
276 left: None,
277 overflow: Overflow::default(),
278 breakable: false,
279 break_before: false,
280 min_widow_lines: 2,
281 min_orphan_lines: 2,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct ElementInfo {
290 pub x: f64,
291 pub y: f64,
292 pub width: f64,
293 pub height: f64,
294 pub kind: String,
296 pub node_type: String,
298 pub style: ElementStyleInfo,
300 pub children: Vec<ElementInfo>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub source_location: Option<SourceLocation>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub text_content: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub href: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub bookmark: Option<String>,
314}
315
316impl LayoutInfo {
317 pub fn from_pages(pages: &[LayoutPage]) -> Self {
319 LayoutInfo {
320 pages: pages
321 .iter()
322 .map(|page| {
323 let (page_w, page_h) = page.config.size.dimensions();
324 let content_x = page.config.margin.left;
325 let content_y = page.config.margin.top;
326 let content_width = page_w - page.config.margin.horizontal();
327 let content_height = page_h - page.config.margin.vertical();
328
329 let elements = Self::build_element_tree(&page.elements);
330
331 PageInfo {
332 width: page_w,
333 height: page_h,
334 content_x,
335 content_y,
336 content_width,
337 content_height,
338 elements,
339 }
340 })
341 .collect(),
342 }
343 }
344
345 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
346 elems
347 .iter()
348 .map(|elem| {
349 let kind = match &elem.draw {
350 DrawCommand::None => "None",
351 DrawCommand::Rect { .. } => "Rect",
352 DrawCommand::Text { .. } => "Text",
353 DrawCommand::Image { .. } => "Image",
354 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
355 DrawCommand::Svg { .. } => "Svg",
356 DrawCommand::Barcode { .. } => "Barcode",
357 DrawCommand::QrCode { .. } => "QrCode",
358 DrawCommand::Chart { .. } => "Chart",
359 DrawCommand::Watermark { .. } => "Watermark",
360 DrawCommand::FormField { .. } => "FormField",
361 };
362 let text_content = match &elem.draw {
363 DrawCommand::Text { lines, .. } => {
364 let text: String = lines
365 .iter()
366 .flat_map(|line| {
367 line.glyphs.iter().flat_map(|g| {
368 g.cluster_text.as_deref().unwrap_or("").chars().chain(
370 if g.cluster_text.is_none() {
371 Some(g.char_value)
372 } else {
373 None
374 },
375 )
376 })
377 })
378 .collect();
379 if text.is_empty() {
380 None
381 } else {
382 Some(text)
383 }
384 }
385 _ => None,
386 };
387 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
388 let style = elem
389 .resolved_style
390 .as_ref()
391 .map(ElementStyleInfo::from_resolved)
392 .unwrap_or_default();
393 ElementInfo {
394 x: elem.x,
395 y: elem.y,
396 width: elem.width,
397 height: elem.height,
398 kind: kind.to_string(),
399 node_type,
400 style,
401 children: Self::build_element_tree(&elem.children),
402 source_location: elem.source_location.clone(),
403 text_content,
404 href: elem.href.clone(),
405 bookmark: elem.bookmark.clone(),
406 }
407 })
408 .collect()
409 }
410}
411
412#[derive(Debug, Clone)]
414pub struct LayoutPage {
415 pub width: f64,
416 pub height: f64,
417 pub elements: Vec<LayoutElement>,
418 pub(crate) fixed_header: Vec<(Node, f64)>,
420 pub(crate) fixed_footer: Vec<(Node, f64)>,
422 pub(crate) watermarks: Vec<Node>,
424 pub(crate) config: PageConfig,
426 pub(crate) page_name: Option<String>,
428}
429
430#[derive(Debug, Clone)]
432pub struct LayoutElement {
433 pub x: f64,
435 pub y: f64,
436 pub width: f64,
438 pub height: f64,
439 pub draw: DrawCommand,
441 pub children: Vec<LayoutElement>,
443 pub node_type: Option<String>,
445 pub resolved_style: Option<ResolvedStyle>,
447 pub source_location: Option<SourceLocation>,
449 pub href: Option<String>,
451 pub bookmark: Option<String>,
453 pub alt: Option<String>,
455 pub is_header_row: bool,
457 pub list_numbering: Option<&'static str>,
462 pub actual_text: Option<String>,
467 pub col_span: u32,
470 pub overflow: Overflow,
472 pub opacity: f64,
477}
478
479fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
482 match &node.kind {
483 NodeKind::Fixed {
484 pages,
485 page_name: only,
486 exclude_page_names,
487 ..
488 } => {
489 pages.applies(page_index)
490 && match only {
491 Some(n) => page_name == Some(n.as_str()),
492 None => true,
493 }
494 && !exclude_page_names
495 .iter()
496 .any(|n| page_name == Some(n.as_str()))
497 }
498 _ => true,
499 }
500}
501
502fn node_kind_name(kind: &NodeKind) -> &'static str {
504 match kind {
505 NodeKind::View => "View",
506 NodeKind::Text { .. } => "Text",
507 NodeKind::Heading { level: 1, .. } => "H1",
508 NodeKind::Heading { level: 2, .. } => "H2",
509 NodeKind::Heading { level: 3, .. } => "H3",
510 NodeKind::Heading { level: 4, .. } => "H4",
511 NodeKind::Heading { level: 5, .. } => "H5",
512 NodeKind::Heading { .. } => "H6",
516 NodeKind::List { .. } => "List",
517 NodeKind::ListItem => "ListItem",
518 NodeKind::Image { .. } => "Image",
519 NodeKind::Table { .. } => "Table",
520 NodeKind::TableRow { .. } => "TableRow",
521 NodeKind::TableCell { .. } => "TableCell",
522 NodeKind::Fixed {
523 position: FixedPosition::Header,
524 ..
525 } => "FixedHeader",
526 NodeKind::Fixed {
527 position: FixedPosition::Footer,
528 ..
529 } => "FixedFooter",
530 NodeKind::Page { .. } => "Page",
531 NodeKind::PageBreak => "PageBreak",
532 NodeKind::PageName { .. } => "PageName",
533 NodeKind::Svg { .. } => "Svg",
534 NodeKind::Canvas { .. } => "Canvas",
535 NodeKind::Barcode { .. } => "Barcode",
536 NodeKind::QrCode { .. } => "QrCode",
537 NodeKind::BarChart { .. } => "BarChart",
538 NodeKind::LineChart { .. } => "LineChart",
539 NodeKind::PieChart { .. } => "PieChart",
540 NodeKind::AreaChart { .. } => "AreaChart",
541 NodeKind::DotPlot { .. } => "DotPlot",
542 NodeKind::Watermark { .. } => "Watermark",
543 NodeKind::TextField { .. } => "TextField",
544 NodeKind::Checkbox { .. } => "Checkbox",
545 NodeKind::Dropdown { .. } => "Dropdown",
546 NodeKind::RadioButton { .. } => "RadioButton",
547 }
548}
549
550fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
569 node.bookmark.as_ref().map(|title| LayoutElement {
570 x,
571 y,
572 width: 0.0,
573 height: 0.0,
574 draw: DrawCommand::None,
575 children: vec![],
576 node_type: Some("Bookmark".to_string()),
577 resolved_style: None,
578 source_location: None,
579 href: None,
580 bookmark: Some(title.clone()),
581 alt: None,
582 is_header_row: false,
583 actual_text: None,
584 list_numbering: None,
585 col_span: 1,
586 overflow: Overflow::default(),
587 opacity: 1.0,
588 })
589}
590
591fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
598 if !ordered {
599 return match marker_type {
603 ListMarkerType::None => String::new(),
604 _ => "•".to_string(),
605 };
606 }
607 let body = match marker_type {
608 ListMarkerType::LowerAlpha => to_alpha(idx, false),
609 ListMarkerType::UpperAlpha => to_alpha(idx, true),
610 ListMarkerType::LowerRoman => to_roman(idx, false),
611 ListMarkerType::UpperRoman => to_roman(idx, true),
612 ListMarkerType::None => return String::new(),
613 _ => idx.to_string(),
616 };
617 format!("{body}.")
618}
619
620fn to_alpha(mut n: u32, upper: bool) -> String {
623 if n == 0 {
624 return String::new();
625 }
626 let base = if upper { b'A' } else { b'a' };
627 let mut bytes: Vec<u8> = Vec::new();
628 while n > 0 {
629 let rem = ((n - 1) % 26) as u8;
630 bytes.push(base + rem);
631 n = (n - 1) / 26;
632 }
633 bytes.reverse();
634 String::from_utf8(bytes).unwrap_or_default()
635}
636
637fn to_roman(n: u32, upper: bool) -> String {
641 if n == 0 || n > 3999 {
642 return n.to_string();
643 }
644 const UPPER: &[(&str, u32)] = &[
645 ("M", 1000),
646 ("CM", 900),
647 ("D", 500),
648 ("CD", 400),
649 ("C", 100),
650 ("XC", 90),
651 ("L", 50),
652 ("XL", 40),
653 ("X", 10),
654 ("IX", 9),
655 ("V", 5),
656 ("IV", 4),
657 ("I", 1),
658 ];
659 const LOWER: &[(&str, u32)] = &[
660 ("m", 1000),
661 ("cm", 900),
662 ("d", 500),
663 ("cd", 400),
664 ("c", 100),
665 ("xc", 90),
666 ("l", 50),
667 ("xl", 40),
668 ("x", 10),
669 ("ix", 9),
670 ("v", 5),
671 ("iv", 4),
672 ("i", 1),
673 ];
674 let table = if upper { UPPER } else { LOWER };
675 let mut out = String::new();
676 let mut remaining = n;
677 for &(sym, val) in table {
678 while remaining >= val {
679 out.push_str(sym);
680 remaining -= val;
681 }
682 }
683 out
684}
685
686fn compute_marker_gutter_width(
693 ordered: bool,
694 marker_type: ListMarkerType,
695 start: u32,
696 n_items: u32,
697 style: &ResolvedStyle,
698) -> f64 {
699 if matches!(marker_type, ListMarkerType::None) {
700 return 0.0;
701 }
702 let approx_char_w = style.font_size * 0.6;
704 let gap = 6.0_f64;
705 if !ordered {
706 return approx_char_w + gap;
708 }
709 let last_idx = start + n_items.saturating_sub(1);
711 let widest = match marker_type {
712 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
713 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
714 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
715 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
716 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
717 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
718 };
719 widest.chars().count() as f64 * approx_char_w + gap
720}
721
722#[derive(Debug, Clone)]
724pub enum FormFieldType {
725 TextField {
726 value: Option<String>,
727 placeholder: Option<String>,
728 multiline: bool,
729 password: bool,
730 read_only: bool,
731 max_length: Option<u32>,
732 font_size: f64,
733 },
734 Checkbox {
735 checked: bool,
736 read_only: bool,
737 },
738 Dropdown {
739 options: Vec<String>,
740 value: Option<String>,
741 read_only: bool,
742 font_size: f64,
743 },
744 RadioButton {
745 value: String,
746 checked: bool,
747 read_only: bool,
748 },
749}
750
751#[derive(Debug, Clone)]
753pub enum DrawCommand {
754 None,
756 Rect {
758 background: Option<Color>,
759 border_width: Edges,
760 border_color: EdgeValues<Color>,
761 border_style: EdgeValues<crate::style::BorderStyle>,
762 border_radius: CornerValues,
763 opacity: f64,
764 box_shadow: Option<Box<crate::style::BoxShadow>>,
767 background_gradient: Option<Box<crate::style::Background>>,
771 },
772 Text {
774 lines: Vec<TextLine>,
775 color: Color,
776 text_decoration: TextDecoration,
777 opacity: f64,
778 },
779 Image {
781 image_data: crate::image_loader::LoadedImage,
782 },
783 ImagePlaceholder,
785 Svg {
787 commands: Vec<crate::svg::SvgCommand>,
788 width: f64,
790 height: f64,
792 viewbox_min_x: f64,
796 viewbox_min_y: f64,
797 viewbox_width: f64,
798 viewbox_height: f64,
799 clip: bool,
801 },
802 Barcode {
804 bars: Vec<u8>,
805 bar_width: f64,
806 height: f64,
807 color: Color,
808 },
809 QrCode {
811 modules: Vec<Vec<bool>>,
812 module_size: f64,
813 color: Color,
814 },
815 Chart {
817 primitives: Vec<crate::chart::ChartPrimitive>,
818 },
819 Watermark {
821 lines: Vec<TextLine>,
822 color: Color,
823 opacity: f64,
824 angle_rad: f64,
825 font_family: String,
827 },
828 FormField {
830 field_type: FormFieldType,
831 name: String,
832 },
833}
834
835#[derive(Debug, Clone)]
836pub struct TextLine {
837 pub x: f64,
838 pub y: f64,
839 pub glyphs: Vec<PositionedGlyph>,
840 pub width: f64,
841 pub height: f64,
842 pub word_spacing: f64,
844}
845
846#[derive(Debug, Clone)]
847pub struct PositionedGlyph {
848 pub glyph_id: u16,
851 pub x_offset: f64,
853 pub y_offset: f64,
855 pub x_advance: f64,
857 pub font_size: f64,
858 pub font_family: Arc<str>,
864 pub font_weight: u32,
865 pub font_style: FontStyle,
866 pub char_value: char,
868 pub color: Option<Color>,
870 pub href: Option<String>,
872 pub text_decoration: TextDecoration,
874 pub letter_spacing: f64,
876 pub cluster_text: Option<String>,
879}
880
881fn offset_element_y(el: &mut LayoutElement, dy: f64) {
884 el.y += dy;
885 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
886 for line in lines.iter_mut() {
887 line.y += dy;
888 }
889 }
890 for child in &mut el.children {
891 offset_element_y(child, dy);
892 }
893}
894
895fn offset_element_x(el: &mut LayoutElement, dx: f64) {
897 el.x += dx;
898 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
899 for line in lines.iter_mut() {
900 line.x += dx;
901 }
902 }
903 for child in &mut el.children {
904 offset_element_x(child, dx);
905 }
906}
907
908fn reapply_justify_content(elem: &mut LayoutElement) {
912 let style = match elem.resolved_style {
913 Some(ref s) => s,
914 None => return,
915 };
916 if matches!(style.justify_content, JustifyContent::FlexStart) {
917 return;
918 }
919 if elem.children.is_empty() {
920 return;
921 }
922
923 let padding_top = style.padding.top + style.border_width.top;
924 let padding_bottom = style.padding.bottom + style.border_width.bottom;
925 let inner_h = elem.height - padding_top - padding_bottom;
926 let content_top = elem.y + padding_top;
927
928 let last_child = &elem.children[elem.children.len() - 1];
930 let children_bottom = last_child.y + last_child.height;
931 let children_span = children_bottom - content_top;
932 let slack = inner_h - children_span;
933 if slack < 0.001 {
934 return;
935 }
936
937 let n = elem.children.len();
938 let offsets: Vec<f64> = match style.justify_content {
939 JustifyContent::FlexEnd => vec![slack; n],
940 JustifyContent::Center => vec![slack / 2.0; n],
941 JustifyContent::SpaceBetween => {
942 if n <= 1 {
943 vec![0.0; n]
944 } else {
945 let per_gap = slack / (n - 1) as f64;
946 (0..n).map(|i| i as f64 * per_gap).collect()
947 }
948 }
949 JustifyContent::SpaceAround => {
950 let space = slack / n as f64;
951 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
952 }
953 JustifyContent::SpaceEvenly => {
954 let space = slack / (n + 1) as f64;
955 (0..n).map(|i| (i + 1) as f64 * space).collect()
956 }
957 JustifyContent::FlexStart => unreachable!(),
958 };
959
960 for (i, child) in elem.children.iter_mut().enumerate() {
961 let dy = offsets[i];
962 if dy.abs() > 0.001 {
963 offset_element_y(child, dy);
964 }
965 }
966}
967
968fn apply_text_transform(text: &str, transform: TextTransform) -> String {
970 match transform {
971 TextTransform::None => text.to_string(),
972 TextTransform::Uppercase => text.to_uppercase(),
973 TextTransform::Lowercase => text.to_lowercase(),
974 TextTransform::Capitalize => {
975 let mut result = String::with_capacity(text.len());
976 let mut prev_is_whitespace = true;
977 for ch in text.chars() {
978 if prev_is_whitespace && ch.is_alphabetic() {
979 for upper in ch.to_uppercase() {
980 result.push(upper);
981 }
982 } else {
983 result.push(ch);
984 }
985 prev_is_whitespace = ch.is_whitespace();
986 }
987 result
988 }
989 }
990}
991
992pub const PAGE_NUMBER_SENTINEL: char = '\x02';
996
997pub const TOTAL_PAGES_SENTINEL: char = '\x03';
999
1000fn substitute_page_placeholders(text: &str) -> String {
1005 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
1006 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
1007 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
1008 } else {
1009 text.to_string()
1010 }
1011}
1012
1013fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1016 match transform {
1017 TextTransform::None => ch,
1018 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1019 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1020 TextTransform::Capitalize => {
1021 if is_word_start && ch.is_alphabetic() {
1022 ch.to_uppercase().next().unwrap_or(ch)
1023 } else {
1024 ch
1025 }
1026 }
1027 }
1028}
1029
1030pub struct LayoutEngine {
1032 text_layout: TextLayout,
1033 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1034 warnings: RefCell<Vec<String>>,
1046 measure_check: bool,
1060}
1061
1062#[derive(Debug, Clone)]
1064struct PageCursor {
1065 config: PageConfig,
1066 base_config: PageConfig,
1069 page_index: usize,
1074 content_width: f64,
1075 content_height: f64,
1076 y: f64,
1077 elements: Vec<LayoutElement>,
1078 fixed_header: Vec<(Node, f64)>,
1079 fixed_footer: Vec<(Node, f64)>,
1080 watermarks: Vec<Node>,
1082 content_x: f64,
1083 content_y: f64,
1084 continuation_top_offset: f64,
1086 containing_block: (f64, f64, f64, f64),
1091 left_config: Option<PageConfig>,
1099 right_config: Option<PageConfig>,
1100 display_config: Option<PageConfig>,
1103 flow_anchor_left: f64,
1108 page_name: Option<String>,
1112 base_display: Option<PageConfig>,
1116 first_display: Option<PageConfig>,
1120 doc_base: PageConfig,
1123 doc_left: Option<PageConfig>,
1124 doc_right: Option<PageConfig>,
1125 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1127}
1128
1129impl PageCursor {
1130 fn new(config: &PageConfig) -> Self {
1131 let (page_w, page_h) = config.size.dimensions();
1132 let content_width = page_w - config.margin.horizontal();
1133 let content_height = page_h - config.margin.vertical();
1134
1135 Self {
1136 config: config.clone(),
1137 base_config: config.clone(),
1138 page_index: 0,
1139 content_width,
1140 content_height,
1141 y: 0.0,
1142 elements: Vec::new(),
1143 fixed_header: Vec::new(),
1144 fixed_footer: Vec::new(),
1145 watermarks: Vec::new(),
1146 content_x: config.margin.left,
1147 content_y: config.margin.top,
1148 continuation_top_offset: 0.0,
1149 containing_block: (
1150 config.margin.left,
1151 config.margin.top,
1152 content_width,
1153 content_height,
1154 ),
1155 left_config: None,
1156 right_config: None,
1157 display_config: None,
1158 flow_anchor_left: config.margin.left,
1159 page_name: None,
1160 base_display: None,
1161 first_display: None,
1162 doc_base: config.clone(),
1163 doc_left: None,
1164 doc_right: None,
1165 named_sets: std::collections::HashMap::new(),
1166 }
1167 }
1168
1169 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1176 if page_number == 1 {
1177 if let Some(first) = &self.first_display {
1178 return Some(first.clone());
1179 }
1180 }
1181 let parity = if page_number % 2 == 1 {
1182 self.right_config.clone()
1183 } else {
1184 self.left_config.clone()
1185 };
1186 parity.or_else(|| self.base_display.clone())
1187 }
1188
1189 fn fixed_applies(&self, node: &Node) -> bool {
1192 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1193 }
1194
1195 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1202 let set = name
1203 .as_deref()
1204 .and_then(|n| self.named_sets.get(n))
1205 .cloned();
1206 let real = set
1207 .as_ref()
1208 .map(|s| s.base.clone())
1209 .unwrap_or_else(|| self.doc_base.clone());
1210 let mut cursor = PageCursor::new(&real);
1211 cursor.page_index = if in_place {
1212 self.page_index
1213 } else {
1214 self.page_index + 1
1215 };
1216 cursor.page_name = name;
1217 cursor.doc_base = self.doc_base.clone();
1218 cursor.doc_left = self.doc_left.clone();
1219 cursor.doc_right = self.doc_right.clone();
1220 cursor.named_sets = self.named_sets.clone();
1221 match &set {
1222 Some(s) => {
1223 cursor.left_config = s.display_left.clone();
1224 cursor.right_config = s.display_right.clone();
1225 cursor.base_display = s.display.clone();
1226 cursor.first_display = s.display_first.clone();
1227 }
1228 None => {
1229 cursor.left_config = self.doc_left.clone();
1233 cursor.right_config = self.doc_right.clone();
1234 }
1235 }
1236 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1237 cursor.flow_anchor_left = self.flow_anchor_left;
1238 cursor.fixed_header = self.fixed_header.clone();
1239 cursor.fixed_footer = self.fixed_footer.clone();
1240 cursor.watermarks = self.watermarks.clone();
1241 cursor.continuation_top_offset = self.continuation_top_offset;
1242
1243 let header_height: f64 = cursor
1244 .fixed_header
1245 .iter()
1246 .filter(|(n, _)| cursor.fixed_applies(n))
1247 .map(|(_, h)| *h)
1248 .sum();
1249 cursor.y = header_height + cursor.continuation_top_offset;
1250 cursor
1251 }
1252
1253 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1256 let mut cursor = PageCursor::new(first);
1257 cursor.base_config = base.clone();
1258 cursor
1259 }
1260
1261 fn remaining_height(&self) -> f64 {
1262 let footer_height: f64 = self
1263 .fixed_footer
1264 .iter()
1265 .filter(|(n, _)| self.fixed_applies(n))
1266 .map(|(_, h)| *h)
1267 .sum();
1268 (self.content_height - self.y - footer_height).max(0.0)
1269 }
1270
1271 fn finalize(&self) -> LayoutPage {
1272 let (page_w, page_h) = self.config.size.dimensions();
1273
1274 let (elements, config) = match &self.display_config {
1280 Some(display) => {
1281 let dx = display.margin.left - self.flow_anchor_left;
1282 let mut elements = self.elements.clone();
1283 if dx != 0.0 {
1284 fn shift(els: &mut [LayoutElement], dx: f64) {
1285 for el in els {
1286 el.x += dx;
1287 shift(&mut el.children, dx);
1288 }
1289 }
1290 shift(&mut elements, dx);
1291 }
1292 (elements, display.clone())
1293 }
1294 None => (self.elements.clone(), self.config.clone()),
1295 };
1296
1297 LayoutPage {
1298 width: page_w,
1299 height: page_h,
1300 elements,
1301 fixed_header: self.fixed_header.clone(),
1302 fixed_footer: self.fixed_footer.clone(),
1303 watermarks: self.watermarks.clone(),
1304 config,
1305 page_name: self.page_name.clone(),
1306 }
1307 }
1308
1309 fn new_page(&self) -> Self {
1310 let mut cursor = PageCursor::new(&self.base_config);
1313 cursor.page_index = self.page_index + 1;
1314 cursor.page_name = self.page_name.clone();
1315 cursor.left_config = self.left_config.clone();
1316 cursor.right_config = self.right_config.clone();
1317 cursor.base_display = self.base_display.clone();
1318 cursor.first_display = self.first_display.clone();
1319 cursor.doc_base = self.doc_base.clone();
1320 cursor.doc_left = self.doc_left.clone();
1321 cursor.doc_right = self.doc_right.clone();
1322 cursor.named_sets = self.named_sets.clone();
1323 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1324 cursor.flow_anchor_left = self.flow_anchor_left;
1325 cursor.fixed_header = self.fixed_header.clone();
1326 cursor.fixed_footer = self.fixed_footer.clone();
1327 cursor.watermarks = self.watermarks.clone();
1328 cursor.continuation_top_offset = self.continuation_top_offset;
1329
1330 let header_height: f64 = cursor
1331 .fixed_header
1332 .iter()
1333 .filter(|(n, _)| cursor.fixed_applies(n))
1334 .map(|(_, h)| *h)
1335 .sum();
1336 cursor.y = header_height + cursor.continuation_top_offset;
1337
1338 cursor
1339 }
1340}
1341
1342impl Default for LayoutEngine {
1343 fn default() -> Self {
1344 Self::new()
1345 }
1346}
1347
1348impl LayoutEngine {
1349 pub fn new() -> Self {
1350 Self {
1351 text_layout: TextLayout::new(),
1352 image_dim_cache: RefCell::new(HashMap::new()),
1353 warnings: RefCell::new(Vec::new()),
1354 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1355 }
1356 }
1357
1358 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1360 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1361 return Some(*dims);
1362 }
1363 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1364 self.image_dim_cache
1365 .borrow_mut()
1366 .insert(src.to_string(), dims);
1367 Some(dims)
1368 } else {
1369 None
1370 }
1371 }
1372
1373 fn defect(&self, msg: String) {
1376 let mut w = self.warnings.borrow_mut();
1377 if !w.contains(&msg) {
1380 w.push(msg);
1381 }
1382 }
1383
1384 pub fn take_warnings(&self) -> Vec<String> {
1388 std::mem::take(&mut *self.warnings.borrow_mut())
1389 }
1390
1391 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1393 self.warnings.borrow_mut().clear();
1394 let mut pages: Vec<LayoutPage> = Vec::new();
1395 let mut cursor = match &document.first_page {
1396 Some(first) => PageCursor::new_first(first, &document.default_page),
1397 None => PageCursor::new(&document.default_page),
1398 };
1399 cursor.left_config = document.left_page.clone();
1405 cursor.right_config = document.right_page.clone();
1406 cursor.doc_base = document.default_page.clone();
1408 cursor.doc_left = document.left_page.clone();
1409 cursor.doc_right = document.right_page.clone();
1410 cursor.named_sets = document.named_pages.clone();
1411 if document.first_page.is_none() {
1412 cursor.display_config = cursor.display_for(1);
1413 }
1414
1415 let base = document.default_style.clone().unwrap_or_default();
1417 let root_style = Style {
1418 lang: base.lang.clone().or(document.metadata.lang.clone()),
1419 ..base
1420 }
1421 .resolve(None, cursor.content_width);
1422
1423 for node in &document.children {
1424 match &node.kind {
1425 NodeKind::Page { config } => {
1426 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1427 pages.push(cursor.finalize());
1428 }
1429 cursor = PageCursor::new(config);
1430 cursor.page_index = pages.len();
1431
1432 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1439 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1440
1441 let cx = cursor.content_x;
1442 let cw = cursor.content_width;
1443 self.layout_children(
1444 &node.children,
1445 &node.style,
1446 &mut cursor,
1447 &mut pages,
1448 cx,
1449 cw,
1450 Some(&page_root),
1451 font_context,
1452 );
1453 }
1454 NodeKind::PageBreak => {
1455 pages.push(cursor.finalize());
1456 cursor = cursor.new_page();
1457 }
1458 _ => {
1459 let cx = cursor.content_x;
1460 let cw = cursor.content_width;
1461 self.layout_node(
1462 node,
1463 &mut cursor,
1464 &mut pages,
1465 cx,
1466 cw,
1467 Some(&root_style),
1468 font_context,
1469 None,
1470 None,
1471 );
1472 }
1473 }
1474 }
1475
1476 if !cursor.elements.is_empty() {
1482 pages.push(cursor.finalize());
1483 }
1484
1485 fn has_visible_ink(els: &[LayoutElement]) -> bool {
1492 els.iter().any(|e| {
1493 let paints = match &e.draw {
1494 DrawCommand::None => false,
1495 DrawCommand::Rect {
1499 background,
1500 border_width,
1501 ..
1502 } => {
1503 background.is_some()
1504 || border_width.top > 0.0
1505 || border_width.right > 0.0
1506 || border_width.bottom > 0.0
1507 || border_width.left > 0.0
1508 }
1509 _ => true,
1510 };
1511 paints || has_visible_ink(&e.children)
1512 })
1513 }
1514 while pages.len() > 1 && !has_visible_ink(&pages.last().unwrap().elements) {
1515 pages.pop();
1516 }
1517
1518 self.inject_fixed_elements(&mut pages, font_context);
1519
1520 pages
1521 }
1522
1523 #[allow(clippy::too_many_arguments)]
1524 fn layout_node(
1525 &self,
1526 node: &Node,
1527 cursor: &mut PageCursor,
1528 pages: &mut Vec<LayoutPage>,
1529 x: f64,
1530 available_width: f64,
1531 parent_style: Option<&ResolvedStyle>,
1532 font_context: &FontContext,
1533 cross_axis_height: Option<f64>,
1534 forced_outer_width: Option<f64>,
1535 ) {
1536 let mut style = node.style.resolve(parent_style, available_width);
1537
1538 if let Some(h) = cross_axis_height {
1541 if matches!(style.height, SizeConstraint::Auto) {
1542 style.height = SizeConstraint::Fixed(h);
1543 }
1544 }
1545
1546 if let Some(w) = forced_outer_width {
1551 style.width = SizeConstraint::Fixed(w);
1552 }
1553
1554 if style.break_before && !cursor.elements.is_empty() {
1565 pages.push(cursor.finalize());
1566 *cursor = cursor.new_page();
1567 }
1568
1569 let elem_start = cursor.elements.len();
1572
1573 match &node.kind {
1574 NodeKind::PageBreak => {
1575 pages.push(cursor.finalize());
1576 *cursor = cursor.new_page();
1577 }
1578
1579 NodeKind::PageName { name } => {
1580 if cursor.page_name.as_deref() != name.as_deref() {
1586 let has_content = !cursor.elements.is_empty();
1593 if has_content {
1594 pages.push(cursor.finalize());
1595 }
1596 *cursor = cursor.renamed_page(name.clone(), !has_content);
1597 }
1598 }
1599
1600 NodeKind::Fixed {
1601 position, pages, ..
1602 } => {
1603 let height = self.measure_node_height(node, available_width, &style, font_context);
1604 match position {
1605 FixedPosition::Header => {
1606 cursor.fixed_header.push((node.clone(), height));
1607 let _ = pages;
1611 if cursor.fixed_applies(node) {
1612 cursor.y += height;
1613 }
1614 }
1615 FixedPosition::Footer => {
1616 cursor.fixed_footer.push((node.clone(), height));
1617 }
1618 }
1619 }
1620
1621 NodeKind::Watermark { .. } => {
1622 cursor.watermarks.push(node.clone());
1624 }
1625
1626 NodeKind::TextField {
1627 name,
1628 value,
1629 placeholder,
1630 width: field_w,
1631 height: field_h,
1632 multiline,
1633 password,
1634 read_only,
1635 max_length,
1636 font_size,
1637 } => {
1638 self.layout_form_field(
1639 node,
1640 &style,
1641 cursor,
1642 pages,
1643 x,
1644 *field_w,
1645 *field_h,
1646 DrawCommand::FormField {
1647 field_type: FormFieldType::TextField {
1648 value: value.clone(),
1649 placeholder: placeholder.clone(),
1650 multiline: *multiline,
1651 password: *password,
1652 read_only: *read_only,
1653 max_length: *max_length,
1654 font_size: *font_size,
1655 },
1656 name: name.clone(),
1657 },
1658 "TextField",
1659 );
1660 }
1661
1662 NodeKind::Checkbox {
1663 name,
1664 checked,
1665 width: field_w,
1666 height: field_h,
1667 read_only,
1668 } => {
1669 self.layout_form_field(
1670 node,
1671 &style,
1672 cursor,
1673 pages,
1674 x,
1675 *field_w,
1676 *field_h,
1677 DrawCommand::FormField {
1678 field_type: FormFieldType::Checkbox {
1679 checked: *checked,
1680 read_only: *read_only,
1681 },
1682 name: name.clone(),
1683 },
1684 "Checkbox",
1685 );
1686 }
1687
1688 NodeKind::Dropdown {
1689 name,
1690 options,
1691 value,
1692 width: field_w,
1693 height: field_h,
1694 read_only,
1695 font_size,
1696 } => {
1697 self.layout_form_field(
1698 node,
1699 &style,
1700 cursor,
1701 pages,
1702 x,
1703 *field_w,
1704 *field_h,
1705 DrawCommand::FormField {
1706 field_type: FormFieldType::Dropdown {
1707 options: options.clone(),
1708 value: value.clone(),
1709 read_only: *read_only,
1710 font_size: *font_size,
1711 },
1712 name: name.clone(),
1713 },
1714 "Dropdown",
1715 );
1716 }
1717
1718 NodeKind::RadioButton {
1719 name,
1720 value,
1721 checked,
1722 width: field_w,
1723 height: field_h,
1724 read_only,
1725 } => {
1726 self.layout_form_field(
1727 node,
1728 &style,
1729 cursor,
1730 pages,
1731 x,
1732 *field_w,
1733 *field_h,
1734 DrawCommand::FormField {
1735 field_type: FormFieldType::RadioButton {
1736 value: value.clone(),
1737 checked: *checked,
1738 read_only: *read_only,
1739 },
1740 name: name.clone(),
1741 },
1742 "RadioButton",
1743 );
1744 }
1745
1746 NodeKind::Text {
1747 content,
1748 href,
1749 runs,
1750 } => {
1751 self.layout_text(
1752 content,
1753 href.as_deref(),
1754 runs,
1755 &style,
1756 cursor,
1757 pages,
1758 x,
1759 available_width,
1760 font_context,
1761 node.source_location.as_ref(),
1762 node.bookmark.as_deref(),
1763 None,
1764 );
1765 }
1766
1767 NodeKind::Heading {
1768 content,
1769 href,
1770 runs,
1771 ..
1772 } => {
1773 let heading_role = node_kind_name(&node.kind); self.layout_text(
1779 content,
1780 href.as_deref(),
1781 runs,
1782 &style,
1783 cursor,
1784 pages,
1785 x,
1786 available_width,
1787 font_context,
1788 node.source_location.as_ref(),
1789 node.bookmark.as_deref(),
1790 Some(heading_role),
1791 );
1792 }
1793
1794 NodeKind::List {
1795 ordered,
1796 marker_type,
1797 start,
1798 } => {
1799 self.layout_list(
1800 node,
1801 *ordered,
1802 *marker_type,
1803 *start,
1804 &style,
1805 cursor,
1806 pages,
1807 x,
1808 available_width,
1809 font_context,
1810 );
1811 }
1812
1813 NodeKind::ListItem => {
1814 self.layout_view(
1819 node,
1820 &style,
1821 cursor,
1822 pages,
1823 x,
1824 available_width,
1825 font_context,
1826 );
1827 }
1828
1829 NodeKind::Image { width, height, .. } => {
1830 self.layout_image(
1831 node,
1832 &style,
1833 cursor,
1834 pages,
1835 x,
1836 available_width,
1837 *width,
1838 *height,
1839 );
1840 }
1841
1842 NodeKind::Table { columns } => {
1843 self.layout_table(
1844 node,
1845 &style,
1846 columns,
1847 cursor,
1848 pages,
1849 x,
1850 available_width,
1851 font_context,
1852 );
1853 }
1854
1855 NodeKind::View | NodeKind::Page { .. } => {
1856 self.layout_view(
1857 node,
1858 &style,
1859 cursor,
1860 pages,
1861 x,
1862 available_width,
1863 font_context,
1864 );
1865 }
1866
1867 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1868 self.layout_view(
1869 node,
1870 &style,
1871 cursor,
1872 pages,
1873 x,
1874 available_width,
1875 font_context,
1876 );
1877 }
1878
1879 NodeKind::Svg {
1880 width: svg_w,
1881 height: svg_h,
1882 view_box,
1883 content,
1884 } => {
1885 self.layout_svg(
1886 node,
1887 &style,
1888 cursor,
1889 pages,
1890 x,
1891 available_width,
1892 *svg_w,
1893 *svg_h,
1894 view_box.as_deref(),
1895 content,
1896 );
1897 }
1898
1899 NodeKind::Barcode {
1900 data,
1901 format,
1902 width: explicit_width,
1903 height: bar_height,
1904 } => {
1905 self.layout_barcode(
1906 node,
1907 &style,
1908 cursor,
1909 pages,
1910 x,
1911 available_width,
1912 data,
1913 *format,
1914 *explicit_width,
1915 *bar_height,
1916 );
1917 }
1918
1919 NodeKind::QrCode {
1920 data,
1921 size: explicit_size,
1922 } => {
1923 self.layout_qrcode(
1924 node,
1925 &style,
1926 cursor,
1927 pages,
1928 x,
1929 available_width,
1930 data,
1931 *explicit_size,
1932 );
1933 }
1934
1935 NodeKind::Canvas {
1936 width: canvas_w,
1937 height: canvas_h,
1938 operations,
1939 } => {
1940 self.layout_canvas(
1941 node,
1942 &style,
1943 cursor,
1944 pages,
1945 x,
1946 available_width,
1947 *canvas_w,
1948 *canvas_h,
1949 operations,
1950 );
1951 }
1952
1953 NodeKind::BarChart {
1954 data,
1955 width: chart_w,
1956 height: chart_h,
1957 color,
1958 show_labels,
1959 show_values,
1960 show_grid,
1961 title,
1962 } => {
1963 let config = crate::chart::bar::BarChartConfig {
1964 color: color.clone(),
1965 show_labels: *show_labels,
1966 show_values: *show_values,
1967 show_grid: *show_grid,
1968 title: title.clone(),
1969 };
1970 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1971 self.layout_chart(
1972 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1973 );
1974 }
1975
1976 NodeKind::LineChart {
1977 series,
1978 labels,
1979 width: chart_w,
1980 height: chart_h,
1981 show_points,
1982 show_grid,
1983 title,
1984 } => {
1985 let config = crate::chart::line::LineChartConfig {
1986 show_points: *show_points,
1987 show_grid: *show_grid,
1988 title: title.clone(),
1989 };
1990 let primitives =
1991 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1992 self.layout_chart(
1993 node,
1994 &style,
1995 cursor,
1996 pages,
1997 x,
1998 *chart_w,
1999 *chart_h,
2000 primitives,
2001 "LineChart",
2002 );
2003 }
2004
2005 NodeKind::PieChart {
2006 data,
2007 width: chart_w,
2008 height: chart_h,
2009 donut,
2010 show_legend,
2011 title,
2012 } => {
2013 let config = crate::chart::pie::PieChartConfig {
2014 donut: *donut,
2015 show_legend: *show_legend,
2016 title: title.clone(),
2017 };
2018 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
2019 self.layout_chart(
2020 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
2021 );
2022 }
2023
2024 NodeKind::AreaChart {
2025 series,
2026 labels,
2027 width: chart_w,
2028 height: chart_h,
2029 show_grid,
2030 title,
2031 } => {
2032 let config = crate::chart::area::AreaChartConfig {
2033 show_grid: *show_grid,
2034 title: title.clone(),
2035 };
2036 let primitives =
2037 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
2038 self.layout_chart(
2039 node,
2040 &style,
2041 cursor,
2042 pages,
2043 x,
2044 *chart_w,
2045 *chart_h,
2046 primitives,
2047 "AreaChart",
2048 );
2049 }
2050
2051 NodeKind::DotPlot {
2052 groups,
2053 width: chart_w,
2054 height: chart_h,
2055 x_min,
2056 x_max,
2057 y_min,
2058 y_max,
2059 x_label,
2060 y_label,
2061 show_legend,
2062 dot_size,
2063 } => {
2064 let config = crate::chart::dot::DotPlotConfig {
2065 x_min: *x_min,
2066 x_max: *x_max,
2067 y_min: *y_min,
2068 y_max: *y_max,
2069 x_label: x_label.clone(),
2070 y_label: y_label.clone(),
2071 show_legend: *show_legend,
2072 dot_size: *dot_size,
2073 };
2074 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2075 self.layout_chart(
2076 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2077 );
2078 }
2079 }
2080
2081 if matches!(style.position, Position::Relative)
2089 && (style.top.is_some()
2090 || style.left.is_some()
2091 || style.right.is_some()
2092 || style.bottom.is_some())
2093 {
2094 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2095 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2096 for el in &mut cursor.elements[elem_start..] {
2097 if dx != 0.0 {
2098 offset_element_x(el, dx);
2099 }
2100 if dy != 0.0 {
2101 offset_element_y(el, dy);
2102 }
2103 }
2104 }
2105 }
2106
2107 #[allow(clippy::too_many_arguments)]
2108 fn layout_view(
2109 &self,
2110 node: &Node,
2111 style: &ResolvedStyle,
2112 cursor: &mut PageCursor,
2113 pages: &mut Vec<LayoutPage>,
2114 x: f64,
2115 available_width: f64,
2116 font_context: &FontContext,
2117 ) {
2118 let padding = &style.padding;
2119 let margin = &style.margin.to_edges();
2120 let border = &style.border_width;
2121
2122 let outer_width = match style.width {
2123 SizeConstraint::Fixed(w) => w,
2124 SizeConstraint::Auto => available_width - margin.horizontal(),
2125 }
2126 .min(style.max_width)
2128 .max(style.min_width);
2129 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2130
2131 let children_height =
2132 self.measure_children_height(&node.children, inner_width, style, font_context);
2133 let total_height = match style.height {
2134 SizeConstraint::Fixed(h) => h,
2135 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2136 }
2137 .max(style.min_height);
2138
2139 let node_x = x + margin.left;
2140
2141 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2142
2143 if fits || !style.breakable {
2144 if !fits && !style.breakable {
2145 pages.push(cursor.finalize());
2146 *cursor = cursor.new_page();
2147 }
2148
2149 let rect_y = cursor.content_y + cursor.y + margin.top;
2151 let snapshot = cursor.elements.len();
2152
2153 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2158 cursor.elements.push(marker);
2159 }
2160
2161 let saved_y = cursor.y;
2162 cursor.y += margin.top + padding.top + border.top;
2163
2164 let pages_before = pages.len();
2165 let children_x = node_x + padding.left + border.left;
2166 let is_grid =
2167 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2168 if is_grid {
2169 self.layout_grid_children(
2170 &node.children,
2171 style,
2172 cursor,
2173 pages,
2174 children_x,
2175 inner_width,
2176 font_context,
2177 );
2178 } else {
2179 self.layout_children(
2180 &node.children,
2181 &node.style,
2182 cursor,
2183 pages,
2184 children_x,
2185 inner_width,
2186 Some(style),
2187 font_context,
2188 );
2189 }
2190
2191 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2193
2194 if self.measure_check
2201 && matches!(style.height, SizeConstraint::Auto)
2202 && pages.len() == pages_before
2203 && !child_elements.is_empty()
2204 {
2205 let content_top = rect_y + padding.top + border.top;
2206 let extent = child_elements
2207 .iter()
2208 .map(|el| el.y + el.height)
2209 .fold(f64::NEG_INFINITY, f64::max)
2210 - content_top;
2211 if extent > 0.0 && children_height - extent > 2.0 {
2212 self.defect(format!(
2213 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2214 node_kind_name(&node.kind),
2215 children_height - extent,
2216 ));
2217 }
2218 }
2219
2220 let rect_element = LayoutElement {
2221 x: node_x,
2222 y: rect_y,
2223 width: outer_width,
2224 height: total_height,
2225 draw: DrawCommand::Rect {
2226 background: style.background_color,
2227 border_width: style.border_width,
2228 border_color: style.border_color,
2229 border_style: style.border_style,
2230 border_radius: style.border_radius,
2231 opacity: 1.0,
2232 box_shadow: style.box_shadow.map(Box::new),
2233 background_gradient: style.background.clone().map(Box::new),
2234 },
2235 children: child_elements,
2236 node_type: Some(node_kind_name(&node.kind).to_string()),
2237 resolved_style: Some(style.clone()),
2238 source_location: node.source_location.clone(),
2239 href: node.href.clone(),
2240 bookmark: None,
2243 alt: None,
2244 is_header_row: false,
2245 actual_text: None,
2246 list_numbering: None,
2247 col_span: 1,
2248 overflow: style.overflow,
2249 opacity: style.opacity,
2250 };
2251 cursor.elements.push(rect_element);
2252
2253 cursor.y = saved_y + total_height + margin.vertical();
2254 } else {
2255 self.layout_breakable_view(
2256 node,
2257 style,
2258 cursor,
2259 pages,
2260 node_x,
2261 outer_width,
2262 inner_width,
2263 font_context,
2264 );
2265 }
2266 }
2267
2268 #[allow(clippy::too_many_arguments)]
2269 fn layout_breakable_view(
2270 &self,
2271 node: &Node,
2272 style: &ResolvedStyle,
2273 cursor: &mut PageCursor,
2274 pages: &mut Vec<LayoutPage>,
2275 node_x: f64,
2276 outer_width: f64,
2277 inner_width: f64,
2278 font_context: &FontContext,
2279 ) {
2280 let padding = &style.padding;
2281 let border = &style.border_width;
2282 let margin = &style.margin.to_edges();
2283
2284 let initial_page_count = pages.len();
2286 let snapshot = cursor.elements.len();
2287 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2288
2289 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2297 cursor.elements.push(marker);
2298 }
2299
2300 cursor.y += margin.top + padding.top + border.top;
2301 let prev_continuation_offset = cursor.continuation_top_offset;
2302 cursor.continuation_top_offset = padding.top + border.top;
2303
2304 let children_x = node_x + padding.left + border.left;
2305 let is_grid =
2306 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2307 if is_grid {
2308 self.layout_grid_children(
2309 &node.children,
2310 style,
2311 cursor,
2312 pages,
2313 children_x,
2314 inner_width,
2315 font_context,
2316 );
2317 } else {
2318 self.layout_children(
2319 &node.children,
2320 &node.style,
2321 cursor,
2322 pages,
2323 children_x,
2324 inner_width,
2325 Some(style),
2326 font_context,
2327 );
2328 }
2329
2330 cursor.continuation_top_offset = prev_continuation_offset;
2331
2332 let mut spans_by_height = false;
2361 if pages.len() == initial_page_count {
2362 if let SizeConstraint::Fixed(h) = style.height {
2363 let consumed =
2364 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2365 let mut owed = h - consumed;
2366 while owed > 0.0 {
2367 let room = cursor.remaining_height();
2368 if owed <= room {
2369 cursor.y += owed;
2370 break;
2371 }
2372 owed -= room;
2373 cursor.y += room;
2374 pages.push(cursor.finalize());
2375 *cursor = cursor.new_page();
2376 spans_by_height = true;
2377 }
2378 }
2379 }
2380
2381 let has_visual = style.background_color.is_some()
2383 || style.border_width.top > 0.0
2384 || style.border_width.right > 0.0
2385 || style.border_width.bottom > 0.0
2386 || style.border_width.left > 0.0;
2387 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2389
2390 if !needs_wrapper {
2391 cursor.y += padding.bottom + border.bottom + margin.bottom;
2393 return;
2394 }
2395
2396 let draw_cmd = DrawCommand::Rect {
2397 background: style.background_color,
2398 border_width: style.border_width,
2399 border_color: style.border_color,
2400 border_style: style.border_style,
2401 border_radius: style.border_radius,
2402 opacity: 1.0,
2403 box_shadow: style.box_shadow.map(Box::new),
2404 background_gradient: style.background.clone().map(Box::new),
2405 };
2406
2407 if pages.len() == initial_page_count {
2408 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2410 let rect_height =
2411 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2412 cursor.elements.push(LayoutElement {
2413 x: node_x,
2414 y: rect_start_y,
2415 width: outer_width,
2416 height: rect_height,
2417 draw: draw_cmd,
2418 children: child_elements,
2419 node_type: Some(node_kind_name(&node.kind).to_string()),
2420 resolved_style: Some(style.clone()),
2421 source_location: node.source_location.clone(),
2422 href: node.href.clone(),
2423 bookmark: None,
2428 alt: None,
2429 is_header_row: false,
2430 actual_text: None,
2431 list_numbering: None,
2432 col_span: 1,
2433 overflow: style.overflow,
2434 opacity: style.opacity,
2435 });
2436 } else {
2437 let page = &mut pages[initial_page_count];
2441 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2442 let page_content_bottom =
2443 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2444 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
2445 if !our_elements.is_empty() {
2446 let rect_height = page_content_bottom - rect_start_y;
2447 page.elements.push(LayoutElement {
2448 x: node_x,
2449 y: rect_start_y,
2450 width: outer_width,
2451 height: rect_height,
2452 draw: draw_cmd.clone(),
2453 children: our_elements,
2454 node_type: Some(node_kind_name(&node.kind).to_string()),
2455 resolved_style: Some(style.clone()),
2456 source_location: node.source_location.clone(),
2457 href: node.href.clone(),
2458 bookmark: None,
2460 alt: None,
2461 is_header_row: false,
2462 actual_text: None,
2463 list_numbering: None,
2464 col_span: 1,
2465 overflow: Overflow::default(),
2466 opacity: 1.0,
2467 });
2468 }
2469
2470 for page in &mut pages[initial_page_count + 1..] {
2472 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2473 let content_top = page.config.margin.top + header_h;
2474 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2475 let content_bottom = page.config.margin.top
2476 + (page.height - page.config.margin.vertical())
2477 - footer_h;
2478 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2479 if !all_elements.is_empty() || spans_by_height {
2480 page.elements.push(LayoutElement {
2481 x: node_x,
2482 y: content_top,
2483 width: outer_width,
2484 height: content_bottom - content_top,
2485 draw: draw_cmd.clone(),
2486 children: all_elements,
2487 node_type: Some(node_kind_name(&node.kind).to_string()),
2488 resolved_style: Some(style.clone()),
2489 source_location: node.source_location.clone(),
2490 href: None,
2491 bookmark: None,
2492 alt: None,
2493 is_header_row: false,
2494 actual_text: None,
2495 list_numbering: None,
2496 col_span: 1,
2497 overflow: Overflow::default(),
2498 opacity: 1.0,
2499 });
2500 }
2501 }
2502
2503 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2505 if !all_elements.is_empty() || spans_by_height {
2506 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2507 let content_top = cursor.content_y + header_h;
2508 let rect_height =
2509 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2510 cursor.elements.push(LayoutElement {
2511 x: node_x,
2512 y: content_top,
2513 width: outer_width,
2514 height: rect_height,
2515 draw: draw_cmd,
2516 children: all_elements,
2517 node_type: Some(node_kind_name(&node.kind).to_string()),
2518 resolved_style: Some(style.clone()),
2519 source_location: node.source_location.clone(),
2520 href: None,
2521 bookmark: None,
2522 alt: None,
2523 is_header_row: false,
2524 actual_text: None,
2525 list_numbering: None,
2526 col_span: 1,
2527 overflow: Overflow::default(),
2528 opacity: 1.0,
2529 });
2530 }
2531 }
2532
2533 cursor.y += padding.bottom + border.bottom + margin.bottom;
2534 }
2535
2536 #[allow(clippy::too_many_arguments)]
2537 fn layout_children(
2538 &self,
2539 children: &[Node],
2540 _parent_raw_style: &Style,
2541 cursor: &mut PageCursor,
2542 pages: &mut Vec<LayoutPage>,
2543 content_x: f64,
2544 available_width: f64,
2545 parent_style: Option<&ResolvedStyle>,
2546 font_context: &FontContext,
2547 ) {
2548 let parent_box_y = cursor.content_y + cursor.y;
2550 let parent_box_x = content_x;
2551 let entry_page_index = pages.len();
2555
2556 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2562 let saved_cb = cursor.containing_block;
2563 if parent_positioned {
2564 let cb_height = parent_style
2565 .and_then(|ps| match ps.height {
2566 SizeConstraint::Fixed(h) => {
2567 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2568 }
2569 SizeConstraint::Auto => None,
2570 })
2571 .unwrap_or(saved_cb.3);
2572 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2573 }
2574
2575 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2577 .iter()
2578 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2579
2580 let direction = parent_style
2581 .map(|s| s.flex_direction)
2582 .unwrap_or(FlexDirection::Column);
2583
2584 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2585 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2586
2587 match direction {
2589 FlexDirection::Column | FlexDirection::ColumnReverse => {
2590 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2591 flow_children.into_iter().rev().collect()
2592 } else {
2593 flow_children
2594 };
2595
2596 let justify = parent_style
2597 .map(|s| s.justify_content)
2598 .unwrap_or(JustifyContent::FlexStart);
2599 let align = parent_style
2600 .map(|s| s.align_items)
2601 .unwrap_or(AlignItems::Stretch);
2602
2603 let start_y = cursor.y;
2604 let initial_pages = pages.len();
2605
2606 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2608
2609 for (i, child) in items.iter().enumerate() {
2610 if i > 0 {
2611 cursor.y += row_gap;
2612 }
2613 let child_start = cursor.elements.len();
2614
2615 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2618 let has_auto_h = child_margin.has_auto_horizontal();
2619
2620 let (child_x, layout_w) = if has_auto_h {
2627 let child_style = child.style.resolve(parent_style, available_width);
2628 let has_explicit_width =
2629 matches!(child_style.width, SizeConstraint::Fixed(_));
2630 let intrinsic = self
2631 .measure_intrinsic_width(child, &child_style, font_context)
2632 .min(available_width);
2633 let w = match child_style.width {
2634 SizeConstraint::Fixed(fw) => fw,
2635 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2640 (available_width - child_margin.horizontal())
2641 .min(child_style.max_width)
2642 }
2643 SizeConstraint::Auto => intrinsic,
2644 }
2645 .min(child_style.max_width)
2646 .max(child_style.min_width);
2647 let lw = if has_explicit_width {
2648 available_width
2649 } else {
2650 w
2651 };
2652 let fixed_h = child_margin.horizontal();
2653 let slack = (available_width - w - fixed_h).max(0.0);
2654 let auto_left = child_margin.left.is_auto();
2655 let auto_right = child_margin.right.is_auto();
2656 let ml = match (auto_left, auto_right) {
2657 (true, true) => slack / 2.0,
2658 (true, false) => slack,
2659 (false, true) => 0.0,
2660 (false, false) => 0.0,
2661 };
2662 (content_x + child_margin.left.resolve() + ml, lw)
2663 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2664 let child_style = child.style.resolve(parent_style, available_width);
2665 let has_explicit_width =
2666 matches!(child_style.width, SizeConstraint::Fixed(_));
2667 let intrinsic = self
2668 .measure_intrinsic_width(child, &child_style, font_context)
2669 .min(available_width);
2670 let w = match child_style.width {
2679 SizeConstraint::Fixed(fw) => fw,
2680 SizeConstraint::Auto => intrinsic,
2681 }
2682 .min(child_style.max_width)
2683 .max(child_style.min_width);
2684 let lw = if has_explicit_width {
2685 available_width
2686 } else {
2687 w
2688 };
2689 match align {
2690 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2691 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2692 _ => (content_x, available_width),
2693 }
2694 } else {
2695 (content_x, available_width)
2696 };
2697
2698 self.layout_node(
2699 child,
2700 cursor,
2701 pages,
2702 child_x,
2703 layout_w,
2704 parent_style,
2705 font_context,
2706 None,
2707 None,
2708 );
2709
2710 child_ranges.push((child_start, cursor.elements.len()));
2711 }
2712
2713 let container_inner_h: Option<f64> = parent_style
2716 .and_then(|ps| match ps.height {
2717 SizeConstraint::Fixed(h) => {
2718 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2719 }
2720 SizeConstraint::Auto => None,
2721 })
2722 .or_else(|| {
2723 if parent_style.is_none() {
2725 Some(cursor.content_height - start_y)
2726 } else {
2727 None
2728 }
2729 });
2730
2731 if let Some(inner_h) = container_inner_h {
2732 if pages.len() == initial_pages {
2733 let child_styles: Vec<ResolvedStyle> = items
2734 .iter()
2735 .map(|child| child.style.resolve(parent_style, available_width))
2736 .collect();
2737 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2738 if total_grow > 0.0 {
2739 let children_total = cursor.y - start_y;
2740 let slack = (inner_h - children_total).max(0.0);
2741 if slack > 0.0 {
2742 let mut cumulative_shift = 0.0_f64;
2743 for (i, cs) in child_styles.iter().enumerate() {
2744 let (start, end) = child_ranges[i];
2745 if cumulative_shift > 0.001 {
2746 for j in start..end {
2747 offset_element_y(
2748 &mut cursor.elements[j],
2749 cumulative_shift,
2750 );
2751 }
2752 }
2753 if cs.flex_grow > 0.0 {
2754 let extra = slack * (cs.flex_grow / total_grow);
2755 if start < end {
2757 let elem = &mut cursor.elements[end - 1];
2758 elem.height += extra;
2759 reapply_justify_content(elem);
2760 }
2761 cumulative_shift += extra;
2762 }
2763 }
2764 cursor.y += cumulative_shift;
2765 }
2766 }
2767 }
2768 }
2769
2770 if let Some(inner_h) = container_inner_h {
2777 if pages.len() == initial_pages {
2778 let auto_styles: Vec<ResolvedStyle> = items
2779 .iter()
2780 .map(|child| child.style.resolve(parent_style, available_width))
2781 .collect();
2782 let total_autos: usize = auto_styles
2783 .iter()
2784 .map(|s| {
2785 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2786 })
2787 .sum();
2788 if total_autos > 0 {
2789 let children_total = cursor.y - start_y;
2790 let total_slack = (inner_h - children_total).max(0.0);
2791 if total_slack > 0.0 {
2792 let per_auto = total_slack / total_autos as f64;
2793 let mut cumulative_shift = 0.0_f64;
2794 for (i, cs) in auto_styles.iter().enumerate() {
2795 let (start, end) = child_ranges[i];
2796 let mt_auto = cs.margin.top.is_auto();
2797 let mb_auto = cs.margin.bottom.is_auto();
2798 let this_child_shift =
2802 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2803 if this_child_shift > 0.001 {
2804 for j in start..end {
2805 offset_element_y(
2806 &mut cursor.elements[j],
2807 this_child_shift,
2808 );
2809 }
2810 }
2811 cumulative_shift =
2814 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2815 }
2816 cursor.y += cumulative_shift;
2817 }
2818 }
2819 }
2820 }
2821
2822 let needs_justify =
2824 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2825 if needs_justify {
2826 let justify_inner_h = container_inner_h.or_else(|| {
2828 parent_style.and_then(|ps| match ps.height {
2829 SizeConstraint::Fixed(h) => {
2830 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2831 }
2832 SizeConstraint::Auto => None,
2833 })
2834 });
2835 if let Some(inner_h) = justify_inner_h {
2836 let children_total = cursor.y - start_y;
2837 let slack = inner_h - children_total;
2838 if slack > 0.0 {
2839 let n = child_ranges.len();
2840 let offsets: Vec<f64> = match justify {
2841 JustifyContent::FlexEnd => vec![slack; n],
2842 JustifyContent::Center => vec![slack / 2.0; n],
2843 JustifyContent::SpaceBetween => {
2844 if n <= 1 {
2845 vec![0.0; n]
2846 } else {
2847 let per_gap = slack / (n - 1) as f64;
2848 (0..n).map(|i| i as f64 * per_gap).collect()
2849 }
2850 }
2851 JustifyContent::SpaceAround => {
2852 let space = slack / n as f64;
2853 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2854 }
2855 JustifyContent::SpaceEvenly => {
2856 let space = slack / (n + 1) as f64;
2857 (0..n).map(|i| (i + 1) as f64 * space).collect()
2858 }
2859 JustifyContent::FlexStart => vec![0.0; n],
2860 };
2861 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2862 let dy = offsets[i];
2863 if dy.abs() > 0.001 {
2864 for j in start..end {
2865 offset_element_y(&mut cursor.elements[j], dy);
2866 }
2867 }
2868 }
2869 cursor.y += *offsets.last().unwrap_or(&0.0);
2870 }
2871 }
2872 }
2873 }
2874
2875 FlexDirection::Row | FlexDirection::RowReverse => {
2876 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2877 self.layout_flex_row(
2878 &flow_owned,
2879 cursor,
2880 pages,
2881 content_x,
2882 available_width,
2883 parent_style,
2884 column_gap,
2885 row_gap,
2886 font_context,
2887 );
2888 }
2889 }
2890
2891 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
2905 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2906 let ph = parent_style
2907 .and_then(|ps| match ps.height {
2908 SizeConstraint::Fixed(h) => {
2909 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2910 }
2911 SizeConstraint::Auto => None,
2912 })
2913 .unwrap_or_else(|| {
2914 if parent_fragmented {
2915 let first = &pages[entry_page_index];
2916 (first.height - first.config.margin.bottom) - parent_box_y
2917 } else {
2918 cursor.content_y + cursor.y - parent_box_y
2919 }
2920 });
2921 (parent_box_x, parent_box_y, available_width, ph)
2922 } else {
2923 cursor.containing_block
2924 };
2925
2926 for abs_child in &abs_children {
2928 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2929
2930 let child_width = match abs_style.width {
2932 SizeConstraint::Fixed(w) => w,
2933 SizeConstraint::Auto => {
2934 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2936 (cb_w - l - r).max(0.0)
2937 } else {
2938 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2939 }
2940 }
2941 };
2942
2943 let child_height = match abs_style.height {
2944 SizeConstraint::Fixed(h) => h,
2945 SizeConstraint::Auto => {
2946 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2947 }
2948 };
2949
2950 let abs_margin = abs_style.margin.to_edges();
2958 let abs_x = if let Some(l) = abs_style.left {
2959 cb_x + l
2960 } else if let Some(r) = abs_style.right {
2961 cb_x + cb_w - r - child_width - abs_margin.horizontal()
2962 } else {
2963 cb_x
2964 };
2965
2966 let abs_y = if let Some(t) = abs_style.top {
2967 cb_y + t
2968 } else if let Some(b) = abs_style.bottom {
2969 cb_y + cb_h - b - child_height - abs_margin.vertical()
2970 } else {
2971 cb_y
2972 };
2973
2974 let mut abs_cursor = PageCursor::new(&cursor.config);
2976 abs_cursor.y = 0.0;
2977 abs_cursor.content_x = abs_x;
2978 abs_cursor.content_y = abs_y;
2979
2980 self.layout_node(
2981 abs_child,
2982 &mut abs_cursor,
2983 &mut Vec::new(),
2984 abs_x,
2985 child_width,
2986 parent_style,
2987 font_context,
2988 None,
2989 None,
2990 );
2991
2992 if parent_fragmented {
2996 pages[entry_page_index].elements.extend(abs_cursor.elements);
2997 } else {
2998 cursor.elements.extend(abs_cursor.elements);
2999 }
3000 }
3001
3002 cursor.containing_block = saved_cb;
3004 }
3005
3006 #[allow(clippy::too_many_arguments)]
3007 fn layout_flex_row(
3008 &self,
3009 children: &[Node],
3010 cursor: &mut PageCursor,
3011 pages: &mut Vec<LayoutPage>,
3012 content_x: f64,
3013 available_width: f64,
3014 parent_style: Option<&ResolvedStyle>,
3015 column_gap: f64,
3016 row_gap: f64,
3017 font_context: &FontContext,
3018 ) {
3019 if children.is_empty() {
3020 return;
3021 }
3022
3023 let flex_wrap = parent_style
3024 .map(|s| s.flex_wrap)
3025 .unwrap_or(FlexWrap::NoWrap);
3026
3027 let items: Vec<FlexItem> = children
3030 .iter()
3031 .map(|child| {
3032 let style = child.style.resolve(parent_style, available_width);
3033 let base_width = match style.flex_basis {
3034 SizeConstraint::Fixed(w) => w,
3035 SizeConstraint::Auto => match style.width {
3036 SizeConstraint::Fixed(w) => w,
3037 SizeConstraint::Auto => {
3038 self.measure_intrinsic_width(child, &style, font_context)
3039 }
3040 },
3041 };
3042 let min_content_width = self.measure_min_content_width(child, &style, font_context);
3043 FlexItem {
3044 node: child,
3045 style,
3046 base_width,
3047 min_content_width,
3048 }
3049 })
3050 .collect();
3051
3052 let outer_widths: Vec<f64> = items
3061 .iter()
3062 .map(|i| i.base_width + i.style.margin.horizontal())
3063 .collect();
3064 let lines = match flex_wrap {
3065 FlexWrap::NoWrap => {
3066 vec![flex::WrapLine {
3067 start: 0,
3068 end: items.len(),
3069 }]
3070 }
3071 FlexWrap::Wrap => {
3078 flex::partition_into_lines(&outer_widths, column_gap, available_width)
3079 }
3080 FlexWrap::WrapReverse => {
3081 let mut l = flex::partition_into_lines(&outer_widths, column_gap, available_width);
3082 l.reverse();
3083 l
3084 }
3085 };
3086
3087 if lines.is_empty() {
3088 return;
3089 }
3090
3091 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3093
3094 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3096
3097 let initial_pages_count = pages.len();
3098 let flex_start_y = cursor.y;
3099 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3100
3101 for (line_idx, line) in lines.iter().enumerate() {
3102 let parallel = true;
3117 let line_items = &items[line.start..line.end];
3118 let line_count = line.end - line.start;
3119 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3120 let distributable = available_width - line_gap;
3121
3122 let total_base: f64 = line_items
3136 .iter()
3137 .map(|i| i.base_width + i.style.margin.horizontal())
3138 .sum();
3139 let remaining = distributable - total_base;
3140
3141 if remaining > 0.0 {
3142 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3143 if total_grow > 0.0 {
3144 for (j, item) in line_items.iter().enumerate() {
3145 final_widths[line.start + j] =
3146 item.base_width + remaining * (item.style.flex_grow / total_grow);
3147 }
3148 }
3149 } else if remaining < 0.0 {
3150 let total_shrink: f64 = line_items
3151 .iter()
3152 .map(|i| i.style.flex_shrink * i.base_width)
3153 .sum();
3154 if total_shrink > 0.0 {
3155 for (j, item) in line_items.iter().enumerate() {
3156 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3157 let w = item.base_width + remaining * factor;
3158 let floor = item.style.min_width.max(item.min_content_width);
3159 final_widths[line.start + j] = w.max(floor);
3160 }
3161 }
3162 }
3163
3164 let mut line_height: f64 = line_items
3166 .iter()
3167 .enumerate()
3168 .map(|(j, item)| {
3169 let fw = final_widths[line.start + j];
3170 self.measure_node_height(item.node, fw, &item.style, font_context)
3171 + item.style.margin.vertical()
3172 })
3173 .fold(0.0f64, f64::max);
3174
3175 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3181 let baseline_ds: Vec<Option<f64>> = line_items
3182 .iter()
3183 .enumerate()
3184 .map(|(j, item)| {
3185 let align = item.style.align_self.unwrap_or(parent_align);
3186 if matches!(align, AlignItems::Baseline) {
3187 let fw = final_widths[line.start + j];
3188 Some(self.flex_item_baseline_distance(
3189 item.node,
3190 &item.style,
3191 fw,
3192 font_context,
3193 ))
3194 } else {
3195 None
3196 }
3197 })
3198 .collect();
3199 let line_baseline = baseline_ds
3200 .iter()
3201 .flatten()
3202 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3203 if let Some(bl) = line_baseline {
3204 for (j, item) in line_items.iter().enumerate() {
3205 if let Some(d) = baseline_ds[j] {
3206 let fw = final_widths[line.start + j];
3207 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3208 + item.style.margin.vertical();
3209 line_height = line_height.max(bl - d + h);
3210 }
3211 }
3212 }
3213
3214 if let Some(ps) = parent_style {
3232 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3233 if let SizeConstraint::Fixed(h) = ps.height {
3234 line_height = h - ps.padding.vertical() - ps.border_width.vertical();
3235 }
3236 }
3237 }
3238
3239 if !parallel && line_height > cursor.remaining_height() && cursor.y > 0.0 {
3261 pages.push(cursor.finalize());
3262 *cursor = cursor.new_page();
3263 }
3264
3265 if line_idx > 0 {
3267 cursor.y += row_gap;
3268 }
3269
3270 let row_start_y = cursor.y;
3271
3272 let actual_total: f64 = (line.start..line.end)
3281 .map(|i| final_widths[i] + items[i].style.margin.horizontal())
3282 .sum();
3283 let slack = available_width - actual_total - line_gap;
3284
3285 let (start_offset, between_extra) = match justify {
3286 JustifyContent::FlexStart => (0.0, 0.0),
3287 JustifyContent::FlexEnd => (slack, 0.0),
3288 JustifyContent::Center => (slack / 2.0, 0.0),
3289 JustifyContent::SpaceBetween => {
3290 if line_count > 1 {
3291 (0.0, slack / (line_count as f64 - 1.0))
3292 } else {
3293 (0.0, 0.0)
3294 }
3295 }
3296 JustifyContent::SpaceAround => {
3297 let s = slack / line_count as f64;
3298 (s / 2.0, s)
3299 }
3300 JustifyContent::SpaceEvenly => {
3301 let s = slack / (line_count as f64 + 1.0);
3302 (s, s)
3303 }
3304 };
3305
3306 let line_elem_start = cursor.elements.len();
3307 let mut x = content_x + start_offset;
3308
3309 let base_page_elements = if parallel {
3316 std::mem::take(&mut cursor.elements)
3317 } else {
3318 Vec::new()
3319 };
3320 let mut item_frags: Vec<ItemFragments> = Vec::with_capacity(line_items.len());
3321
3322 let line_start_pages = pages.len();
3333
3334 for (j, item) in line_items.iter().enumerate() {
3335 if j > 0 {
3336 x += column_gap + between_extra;
3337 }
3338
3339 let fw = final_widths[line.start + j];
3340
3341 let align = item
3342 .style
3343 .align_self
3344 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3345
3346 let item_height =
3347 self.measure_node_height(item.node, fw, &item.style, font_context);
3348
3349 let has_auto_v = item.style.margin.has_auto_vertical();
3351 let y_offset = if has_auto_v {
3352 let fixed_v = item.style.margin.vertical();
3353 let slack = (line_height - item_height - fixed_v).max(0.0);
3354 let auto_top = item.style.margin.top.is_auto();
3355 let auto_bottom = item.style.margin.bottom.is_auto();
3356 match (auto_top, auto_bottom) {
3357 (true, true) => slack / 2.0,
3358 (true, false) => slack,
3359 (false, true) => 0.0,
3360 (false, false) => 0.0,
3361 }
3362 } else {
3363 match align {
3364 AlignItems::FlexStart => 0.0,
3365 AlignItems::FlexEnd => {
3366 line_height - item_height - item.style.margin.vertical()
3367 }
3368 AlignItems::Center => {
3369 (line_height - item_height - item.style.margin.vertical()) / 2.0
3370 }
3371 AlignItems::Stretch => 0.0,
3372 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3373 (Some(bl), Some(d)) => bl - d,
3374 _ => 0.0,
3375 },
3376 }
3377 };
3378
3379 let cross_h = if matches!(align, AlignItems::Stretch)
3383 && matches!(item.style.height, SizeConstraint::Auto)
3384 && !has_auto_v
3385 {
3386 let stretch_h = line_height - item.style.margin.vertical();
3387 if stretch_h > item_height {
3388 Some(stretch_h)
3389 } else {
3390 None
3391 }
3392 } else {
3393 None
3394 };
3395
3396 let saved_y = cursor.y;
3397 cursor.y = row_start_y + y_offset;
3398
3399 let mut item_cursor = cursor.clone();
3432 let mut item_pages: Vec<LayoutPage> = Vec::new();
3433
3434 self.layout_node(
3435 item.node,
3436 &mut item_cursor,
3437 &mut item_pages,
3438 x,
3439 available_width,
3440 parent_style,
3441 font_context,
3442 cross_h,
3443 Some(fw),
3444 );
3445
3446 if parallel {
3447 let final_y = item_cursor.y;
3450 let tail = std::mem::take(&mut item_cursor.elements);
3451 item_frags.push(ItemFragments {
3452 pages: item_pages,
3453 tail,
3454 cursor: item_cursor,
3455 final_y,
3456 stretches: matches!(align, AlignItems::Stretch)
3457 && matches!(item.style.height, SizeConstraint::Auto)
3458 && !has_auto_v,
3459 });
3460 } else {
3461 pages.extend(item_pages);
3463 *cursor = item_cursor;
3464 }
3465
3466 cursor.y = saved_y;
3467 x += fw + item.style.margin.horizontal();
3474 }
3475
3476 if !parallel {
3483 if line_items.len() > 1 && pages.len() > line_start_pages {
3488 let near = line_items
3489 .iter()
3490 .find_map(|it| first_text_snippet(it.node))
3491 .map(|t| format!(" (row beginning \"{t}\")"))
3492 .unwrap_or_default();
3493 self.defect(format!(
3494 "render defect: a wrapped flex row crossing a page boundary lays its children sequentially — a column taller than the page does not continue side by side{near}"
3495 ));
3496 }
3497 cursor.y = row_start_y + line_height;
3498 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3499 continue;
3500 }
3501
3502 let frag_count = item_frags
3503 .iter()
3504 .map(|f| f.pages.len() + 1)
3505 .max()
3506 .unwrap_or(1);
3507
3508 let mut empty_bands: Vec<(usize, LayoutElement)> = Vec::new();
3520 if frag_count > 1 {
3521 let page_bottom = cursor.content_y + cursor.content_height;
3522 let last = frag_count - 1;
3523 let row_end = cursor.content_y + end_y_of(&item_frags, last);
3524 for f in item_frags.iter_mut() {
3525 if !f.stretches {
3526 continue;
3527 }
3528 let own_last = f.pages.len();
3529 for (k, page) in f.pages.iter_mut().enumerate() {
3530 let bottom = if k == last { row_end } else { page_bottom };
3531 stretch_fragment(&mut page.elements, bottom);
3532 }
3533 let bottom = if own_last == last {
3534 row_end
3535 } else {
3536 page_bottom
3537 };
3538 stretch_fragment(&mut f.tail, bottom);
3539
3540 if own_last < last {
3553 let template = f
3554 .tail
3555 .iter()
3556 .chain(f.pages.iter().flat_map(|p| p.elements.iter()))
3557 .find(|el| paints_a_box(el))
3558 .cloned();
3559 if let Some(t) = template {
3560 for k in (own_last + 1)..=last {
3561 let bottom = if k == last { row_end } else { page_bottom };
3562 let mut band = t.clone();
3563 band.children.clear();
3564 band.y = cursor.content_y;
3565 band.height = (bottom - cursor.content_y).max(0.0);
3566 empty_bands.push((k, band));
3570 }
3571 }
3572 }
3573 }
3574 }
3575
3576 let mut merged: Vec<Vec<LayoutElement>> = vec![Vec::new(); frag_count];
3577 merged[0] = base_page_elements;
3579 for (k, band) in empty_bands {
3582 merged[k].push(band);
3583 }
3584 for f in item_frags.iter_mut() {
3585 for (k, page) in f.pages.iter_mut().enumerate() {
3586 merged[k].append(&mut page.elements);
3587 }
3588 let tail_index = f.pages.len();
3591 let mut tail = std::mem::take(&mut f.tail);
3592 merged[tail_index].append(&mut tail);
3593 }
3594
3595 if frag_count == 1 {
3596 cursor.elements = std::mem::take(&mut merged[0]);
3600 cursor.y = row_start_y + line_height;
3601 } else {
3602 let carrier_idx = item_frags
3613 .iter()
3614 .enumerate()
3615 .max_by_key(|(_, f)| f.pages.len())
3616 .map(|(i, _)| i)
3617 .expect("a flex line has at least one item");
3618 let mut carrier = item_frags.swap_remove(carrier_idx);
3619
3620 for (k, page) in carrier.pages.iter_mut().enumerate() {
3621 page.elements = std::mem::take(&mut merged[k]);
3622 }
3623 pages.append(&mut carrier.pages);
3624
3625 let last = frag_count - 1;
3628 let end_y = item_frags
3629 .iter()
3630 .chain(std::iter::once(&carrier))
3631 .filter(|f| f.pages.len() == last)
3632 .map(|f| f.final_y)
3633 .fold(f64::MIN, f64::max);
3634
3635 let mut merged_tail = std::mem::take(&mut merged[last]);
3636 *cursor = carrier.cursor;
3637 cursor.elements.clear();
3638 cursor.elements.append(&mut merged_tail);
3639 cursor.y = end_y;
3640 }
3641
3642 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3643 }
3644
3645 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3647 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3648 if !matches!(align_content, AlignContent::FlexStart)
3649 && !matches!(flex_wrap, FlexWrap::NoWrap)
3650 {
3651 if let Some(parent) = parent_style {
3652 if let SizeConstraint::Fixed(container_h) = parent.height {
3653 let inner_h = container_h
3654 - parent.padding.vertical()
3655 - parent.border_width.vertical();
3656 let total_used = cursor.y - flex_start_y;
3657 let slack = inner_h - total_used;
3658 if slack > 0.0 {
3659 let n = line_infos.len();
3660 let offsets: Vec<f64> = match align_content {
3661 AlignContent::FlexEnd => vec![slack; n],
3662 AlignContent::Center => vec![slack / 2.0; n],
3663 AlignContent::SpaceBetween => {
3664 if n <= 1 {
3665 vec![0.0; n]
3666 } else {
3667 let per_gap = slack / (n - 1) as f64;
3668 (0..n).map(|i| i as f64 * per_gap).collect()
3669 }
3670 }
3671 AlignContent::SpaceAround => {
3672 let space = slack / n as f64;
3673 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3674 }
3675 AlignContent::SpaceEvenly => {
3676 let space = slack / (n + 1) as f64;
3677 (0..n).map(|i| (i + 1) as f64 * space).collect()
3678 }
3679 AlignContent::Stretch => {
3680 let extra = slack / n as f64;
3681 (0..n).map(|i| i as f64 * extra).collect()
3682 }
3683 AlignContent::FlexStart => vec![0.0; n],
3684 };
3685 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3686 let dy = offsets[i];
3687 if dy.abs() > 0.001 {
3688 for j in start..end {
3689 offset_element_y(&mut cursor.elements[j], dy);
3690 }
3691 }
3692 }
3693 cursor.y += *offsets.last().unwrap_or(&0.0);
3694 }
3695 }
3696 }
3697 }
3698 }
3699 }
3700
3701 #[allow(clippy::too_many_arguments)]
3704 fn layout_list(
3705 &self,
3706 node: &Node,
3707 ordered: bool,
3708 marker_type: ListMarkerType,
3709 start: u32,
3710 style: &ResolvedStyle,
3711 cursor: &mut PageCursor,
3712 pages: &mut Vec<LayoutPage>,
3713 x: f64,
3714 available_width: f64,
3715 font_context: &FontContext,
3716 ) {
3717 let margin = &style.margin.to_edges();
3718 let padding = &style.padding;
3719
3720 cursor.y += margin.top;
3721
3722 let list_x = x + margin.left;
3723 let outer_width = available_width - margin.horizontal();
3724 let inner_width = outer_width - padding.horizontal();
3725
3726 let n_items = node
3729 .children
3730 .iter()
3731 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3732 .count() as u32;
3733
3734 let marker_gutter =
3735 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3736
3737 let list_inner_x = list_x + padding.left;
3738 let content_x = list_inner_x + marker_gutter;
3739 let content_width = (inner_width - marker_gutter).max(0.0);
3740
3741 let snapshot = cursor.elements.len();
3744 let list_start_y = cursor.content_y + cursor.y;
3745 cursor.y += padding.top;
3746
3747 let mut item_index: u32 = 0;
3748 for child in &node.children {
3749 if !matches!(child.kind, NodeKind::ListItem) {
3750 continue;
3751 }
3752 let marker_idx = start + item_index;
3753 self.layout_list_item(
3754 child,
3755 marker_idx,
3756 ordered,
3757 marker_type,
3758 marker_gutter,
3759 style,
3760 cursor,
3761 pages,
3762 list_inner_x,
3763 content_x,
3764 content_width,
3765 font_context,
3766 );
3767 item_index += 1;
3768 }
3769
3770 cursor.y += padding.bottom;
3771
3772 let item_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
3774 let list_height = cursor.content_y + cursor.y - list_start_y;
3775 cursor.elements.push(LayoutElement {
3776 x: list_x,
3777 y: list_start_y,
3778 width: outer_width,
3779 height: list_height,
3780 draw: DrawCommand::None,
3781 children: item_elements,
3782 node_type: Some("List".to_string()),
3783 resolved_style: Some(style.clone()),
3784 source_location: node.source_location.clone(),
3785 href: None,
3786 bookmark: node.bookmark.clone(),
3787 alt: None,
3788 is_header_row: false,
3789 actual_text: None,
3793 list_numbering: match marker_type {
3794 ListMarkerType::None => None,
3795 ListMarkerType::Disc => Some("Disc"),
3796 ListMarkerType::Circle => Some("Circle"),
3797 ListMarkerType::Square => Some("Square"),
3798 ListMarkerType::Decimal => Some("Decimal"),
3799 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3800 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3801 ListMarkerType::LowerRoman => Some("LowerRoman"),
3802 ListMarkerType::UpperRoman => Some("UpperRoman"),
3803 },
3804 col_span: 1,
3805 overflow: style.overflow,
3806 opacity: style.opacity,
3807 });
3808
3809 cursor.y += margin.bottom;
3810 }
3811
3812 #[allow(clippy::too_many_arguments)]
3813 fn layout_list_item(
3814 &self,
3815 item: &Node,
3816 marker_idx: u32,
3817 ordered: bool,
3818 marker_type: ListMarkerType,
3819 marker_gutter: f64,
3820 parent_style: &ResolvedStyle,
3821 cursor: &mut PageCursor,
3822 pages: &mut Vec<LayoutPage>,
3823 list_inner_x: f64,
3824 content_x: f64,
3825 content_width: f64,
3826 font_context: &FontContext,
3827 ) {
3828 let item_style = item.style.resolve(Some(parent_style), content_width);
3829 let item_margin = item_style.margin.to_edges();
3830
3831 cursor.y += item_margin.top;
3832 let item_start_y = cursor.content_y + cursor.y;
3833 let item_snapshot = cursor.elements.len();
3834
3835 let marker_str = format_marker(marker_idx, ordered, marker_type);
3839 if !marker_str.is_empty() {
3840 let saved_y = cursor.y;
3841 self.layout_text(
3842 &marker_str,
3843 None,
3844 &[],
3845 &item_style,
3846 cursor,
3847 pages,
3848 list_inner_x,
3849 marker_gutter,
3850 font_context,
3851 None,
3852 None,
3853 Some("Lbl"),
3854 );
3855 cursor.y = saved_y;
3856 }
3857
3858 self.layout_children(
3864 &item.children,
3865 &item.style,
3866 cursor,
3867 pages,
3868 content_x,
3869 content_width,
3870 Some(&item_style),
3871 font_context,
3872 );
3873
3874 let item_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, item_snapshot);
3877 let item_height = cursor.content_y + cursor.y - item_start_y;
3878 let item_width = content_x + content_width - list_inner_x;
3879 cursor.elements.push(LayoutElement {
3880 x: list_inner_x,
3881 y: item_start_y,
3882 width: item_width,
3883 height: item_height,
3884 draw: DrawCommand::None,
3885 children: item_children,
3886 node_type: Some("ListItem".to_string()),
3887 resolved_style: Some(item_style.clone()),
3888 source_location: item.source_location.clone(),
3889 href: None,
3890 bookmark: item.bookmark.clone(),
3891 alt: None,
3892 is_header_row: false,
3893 actual_text: None,
3894 list_numbering: None,
3895 col_span: 1,
3896 overflow: item_style.overflow,
3897 opacity: item_style.opacity,
3898 });
3899
3900 cursor.y += item_margin.bottom;
3901 }
3902
3903 #[allow(clippy::too_many_arguments)]
3904 fn layout_table(
3905 &self,
3906 node: &Node,
3907 style: &ResolvedStyle,
3908 column_defs: &[ColumnDef],
3909 cursor: &mut PageCursor,
3910 pages: &mut Vec<LayoutPage>,
3911 x: f64,
3912 available_width: f64,
3913 font_context: &FontContext,
3914 ) {
3915 let padding = &style.padding;
3916 let margin = &style.margin.to_edges();
3917 let border = &style.border_width;
3918
3919 let table_x = x + margin.left;
3920 let table_width = match style.width {
3921 SizeConstraint::Fixed(w) => w,
3922 SizeConstraint::Auto => available_width - margin.horizontal(),
3923 };
3924 let inner_width = table_width - padding.horizontal() - border.horizontal();
3925
3926 let col_widths = self.resolve_column_widths(
3927 column_defs,
3928 inner_width,
3929 &node.children,
3930 style,
3931 font_context,
3932 );
3933
3934 let all_offsets = Self::table_column_offsets(&node.children);
3938 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
3939 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
3940
3941 for (child, offs) in node.children.iter().zip(&all_offsets) {
3942 match &child.kind {
3943 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
3944 _ => body_rows.push((child, offs)),
3945 }
3946 }
3947
3948 if !style.breakable {
3954 let total_height: f64 = node
3955 .children
3956 .iter()
3957 .zip(&all_offsets)
3958 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3959 .sum::<f64>()
3960 + padding.vertical()
3961 + border.vertical();
3962 let fresh_page_available = cursor.content_height
3963 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3964 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3965 if total_height > cursor.remaining_height()
3966 && total_height <= fresh_page_available
3967 && cursor.y > 0.0
3968 {
3969 pages.push(cursor.finalize());
3970 *cursor = cursor.new_page();
3971 }
3972 }
3973
3974 let initial_page_count = pages.len();
3981 let snapshot = cursor.elements.len();
3982 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3983
3984 cursor.y += margin.top + padding.top + border.top;
3985
3986 let cell_x_start = table_x + padding.left + border.left;
3987
3988 if !header_rows.is_empty() {
4013 let total_header_h: f64 = header_rows
4014 .iter()
4015 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4016 .sum();
4017 let first_body_h = body_rows
4018 .first()
4019 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4020 .unwrap_or(0.0);
4021
4022 let needed = total_header_h + first_body_h;
4023 let fresh_page_available = cursor.content_height
4024 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
4025 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
4026
4027 if needed > cursor.remaining_height() && needed <= fresh_page_available {
4028 pages.push(cursor.finalize());
4029 *cursor = cursor.new_page();
4030 cursor.y += padding.top + border.top;
4031 }
4032 }
4033
4034 for (header_row, offs) in &header_rows {
4035 self.layout_table_row(
4036 header_row,
4037 &col_widths,
4038 offs,
4039 style,
4040 cursor,
4041 cell_x_start,
4042 font_context,
4043 pages,
4044 );
4045 }
4046
4047 for (body_row, offs) in &body_rows {
4048 let row_height =
4049 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
4050
4051 let fresh_page_available = cursor.content_height
4059 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
4060 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
4061 if row_height > fresh_page_available {
4062 self.defect(format!(
4063 "render defect: table row needs {row_height:.0}pt but a page holds {fresh_page_available:.0}pt — rows are atomic, so it is placed whole and overflows",
4064 ));
4065 }
4066 if row_height > cursor.remaining_height()
4067 && cursor.remaining_height() < fresh_page_available - 0.5
4068 {
4069 pages.push(cursor.finalize());
4070 *cursor = cursor.new_page();
4071
4072 cursor.y += padding.top + border.top;
4073 for (header_row, h_offs) in &header_rows {
4074 self.layout_table_row(
4075 header_row,
4076 &col_widths,
4077 h_offs,
4078 style,
4079 cursor,
4080 cell_x_start,
4081 font_context,
4082 pages,
4083 );
4084 }
4085 }
4086
4087 self.layout_table_row(
4088 body_row,
4089 &col_widths,
4090 offs,
4091 style,
4092 cursor,
4093 cell_x_start,
4094 font_context,
4095 pages,
4096 );
4097 }
4098
4099 let has_visual = style.background_color.is_some()
4103 || style.background.is_some()
4104 || style.border_width.top > 0.0
4105 || style.border_width.right > 0.0
4106 || style.border_width.bottom > 0.0
4107 || style.border_width.left > 0.0;
4108 let draw_cmd = if has_visual {
4109 DrawCommand::Rect {
4110 background: style.background_color,
4111 border_width: style.border_width,
4112 border_color: style.border_color,
4113 border_style: style.border_style,
4114 border_radius: style.border_radius,
4115 opacity: 1.0,
4116 box_shadow: style.box_shadow.map(Box::new),
4117 background_gradient: style.background.clone().map(Box::new),
4118 }
4119 } else {
4120 DrawCommand::None
4121 };
4122 let make_wrapper =
4123 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
4124 x: table_x,
4125 y,
4126 width: table_width,
4127 height,
4128 draw,
4129 children,
4130 node_type: Some(node_kind_name(&node.kind).to_string()),
4131 resolved_style: Some(style.clone()),
4132 source_location: node.source_location.clone(),
4133 href: node.href.clone(),
4134 bookmark: None,
4135 alt: None,
4136 is_header_row: false,
4137 actual_text: None,
4138 list_numbering: None,
4139 col_span: 1,
4140 overflow: Overflow::default(),
4141 opacity: style.opacity,
4142 };
4143
4144 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
4145
4146 if pages.len() == initial_page_count {
4147 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4149 cursor.elements.push(make_wrapper(
4150 rect_start_y,
4151 table_bottom_y - rect_start_y,
4152 child_elements,
4153 draw_cmd,
4154 ));
4155 } else {
4156 let page = &mut pages[initial_page_count];
4175 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
4176 let page_content_bottom =
4177 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
4178 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
4179 if !our_elements.is_empty() {
4180 page.elements.push(make_wrapper(
4181 rect_start_y,
4182 page_content_bottom - rect_start_y,
4183 our_elements,
4184 draw_cmd.clone(),
4185 ));
4186 }
4187
4188 for page in &mut pages[initial_page_count + 1..] {
4190 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
4191 let content_top = page.config.margin.top + header_h;
4192 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
4193 let content_bottom = page.config.margin.top
4194 + (page.height - page.config.margin.vertical())
4195 - footer_h;
4196 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
4197 if !all_elements.is_empty() {
4198 page.elements.push(make_wrapper(
4199 content_top,
4200 content_bottom - content_top,
4201 all_elements,
4202 draw_cmd.clone(),
4203 ));
4204 }
4205 }
4206
4207 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
4209 if !all_elements.is_empty() {
4210 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
4211 let content_top = cursor.content_y + header_h;
4212 cursor.elements.push(make_wrapper(
4213 content_top,
4214 table_bottom_y - content_top,
4215 all_elements,
4216 draw_cmd,
4217 ));
4218 }
4219 }
4220
4221 cursor.y += padding.bottom + border.bottom + margin.bottom;
4222 }
4223
4224 fn subtree_forces_break(node: &Node) -> bool {
4234 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
4235 return true;
4236 }
4237 if node.style.break_before == Some(true) {
4238 return true;
4239 }
4240 node.children.iter().any(Self::subtree_forces_break)
4241 }
4242
4243 fn occupancy_column_count(children: &[Node]) -> usize {
4249 let offsets = Self::table_column_offsets(children);
4250 children
4251 .iter()
4252 .zip(&offsets)
4253 .map(|(row, offs)| {
4254 row.children
4255 .iter()
4256 .zip(offs)
4257 .map(|(cell, &start)| start + Self::cell_col_span(cell))
4258 .max()
4259 .unwrap_or(0)
4260 })
4261 .max()
4262 .unwrap_or(1)
4263 .max(1)
4264 }
4265
4266 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
4276 fn spans(cell: &Node) -> (usize, u32) {
4277 match &cell.kind {
4278 NodeKind::TableCell { col_span, row_span } => {
4279 ((*col_span).max(1) as usize, (*row_span).max(1))
4280 }
4281 _ => (1, 1),
4282 }
4283 }
4284 let mut pending: Vec<u32> = Vec::new();
4285 let mut out = Vec::with_capacity(rows.len());
4286 for row in rows {
4287 let mut offsets = Vec::with_capacity(row.children.len());
4288 let mut col = 0usize;
4289 for cell in &row.children {
4290 let (span, rspan) = spans(cell);
4291 while pending.get(col).copied().unwrap_or(0) > 0 {
4292 col += 1;
4293 }
4294 offsets.push(col);
4295 if rspan > 1 {
4296 if pending.len() < col + span {
4297 pending.resize(col + span, 0);
4298 }
4299 for slot in pending.iter_mut().take(col + span).skip(col) {
4300 *slot = (*slot).max(rspan);
4301 }
4302 }
4303 col += span;
4304 }
4305 out.push(offsets);
4306 for p in pending.iter_mut() {
4307 *p = p.saturating_sub(1);
4308 }
4309 }
4310 out
4311 }
4312
4313 #[allow(clippy::too_many_arguments)]
4314 fn layout_table_row(
4315 &self,
4316 row: &Node,
4317 col_widths: &[f64],
4318 col_offsets: &[usize],
4319 parent_style: &ResolvedStyle,
4320 cursor: &mut PageCursor,
4321 start_x: f64,
4322 font_context: &FontContext,
4323 pages: &mut Vec<LayoutPage>,
4324 ) {
4325 let row_style = row
4326 .style
4327 .resolve(Some(parent_style), col_widths.iter().sum());
4328
4329 let row_height =
4330 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
4331 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
4332 let row_y = cursor.content_y + cursor.y;
4333 let total_width: f64 = col_widths.iter().sum();
4334
4335 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
4336
4337 let row_snapshot = cursor.elements.len();
4339
4340 let row_may_break =
4352 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
4353
4354 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
4355 for (cell_i, cell) in row.children.iter().enumerate() {
4359 let span = match &cell.kind {
4360 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
4361 _ => 1,
4362 };
4363 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
4364 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
4365 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
4366
4367 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4368
4369 let cell_snapshot = cursor.elements.len();
4371
4372 let inner_width =
4373 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4374
4375 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4376 let saved_y = cursor.y;
4377 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4378
4379 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4383 let content_h: f64 = cell
4384 .children
4385 .iter()
4386 .map(|ch| {
4387 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4388 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4389 })
4390 .sum();
4391 let inner_row =
4392 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4393 let slack = (inner_row - content_h).max(0.0);
4394 cursor.y += match cell_style.vertical_align {
4395 crate::style::VerticalAlign::Middle => slack / 2.0,
4396 crate::style::VerticalAlign::Bottom => slack,
4397 crate::style::VerticalAlign::Baseline => row_bl
4402 .map(|b| {
4403 let d = self.cell_baseline_distance(
4404 cell,
4405 &cell_style,
4406 inner_width,
4407 font_context,
4408 );
4409 (b - d).max(0.0)
4410 })
4411 .unwrap_or(0.0),
4412 crate::style::VerticalAlign::Top => 0.0,
4413 };
4414 }
4415
4416 let cursor_before_cell = if row_may_break {
4421 Some(cursor.clone())
4422 } else {
4423 None
4424 };
4425 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4426 for child in &cell.children {
4427 self.layout_node(
4428 child,
4429 cursor,
4430 &mut cell_pages,
4431 content_x,
4432 inner_width,
4433 Some(&cell_style),
4434 font_context,
4435 None,
4436 None,
4437 );
4438 }
4439
4440 if !cell_pages.is_empty() {
4442 let post_break_elements = std::mem::take(&mut cursor.elements);
4443 if let Some(last_page) = cell_pages.last_mut() {
4444 last_page.elements.extend(post_break_elements);
4445 }
4446 if !is_header {
4452 all_overflow_pages.extend(cell_pages);
4453 }
4454 *cursor = cursor_before_cell
4460 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4461 }
4462
4463 cursor.y = saved_y;
4464
4465 let cell_children: Vec<LayoutElement> =
4467 drain_since(&mut cursor.elements, cell_snapshot);
4468
4469 cursor.elements.push(LayoutElement {
4471 x: cell_x,
4472 y: row_y,
4473 width: col_width,
4474 height: row_height,
4475 draw: if cell_style.background_color.is_some()
4476 || cell_style.border_width.horizontal() > 0.0
4477 || cell_style.border_width.vertical() > 0.0
4478 {
4479 DrawCommand::Rect {
4480 background: cell_style.background_color,
4481 border_width: cell_style.border_width,
4482 border_color: cell_style.border_color,
4483 border_style: cell_style.border_style,
4484 border_radius: cell_style.border_radius,
4485 opacity: 1.0,
4486 box_shadow: cell_style.box_shadow.map(Box::new),
4487 background_gradient: cell_style.background.clone().map(Box::new),
4488 }
4489 } else {
4490 DrawCommand::None
4491 },
4492 children: cell_children,
4493 node_type: Some("TableCell".to_string()),
4494 resolved_style: Some(cell_style.clone()),
4495 source_location: cell.source_location.clone(),
4496 href: None,
4497 bookmark: cell.bookmark.clone(),
4498 alt: None,
4499 is_header_row: is_header,
4500 actual_text: None,
4501 list_numbering: None,
4502 col_span: span as u32,
4503 overflow: Overflow::default(),
4504 opacity: 1.0,
4505 });
4506 }
4507
4508 let row_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, row_snapshot);
4510 let row_element = LayoutElement {
4511 x: start_x,
4512 y: row_y,
4513 width: total_width,
4514 height: row_height,
4515 draw: if let Some(bg) = row_style.background_color {
4516 DrawCommand::Rect {
4517 background: Some(bg),
4518 border_width: Edges::default(),
4519 border_color: EdgeValues::uniform(Color::BLACK),
4520 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4521 border_radius: CornerValues::uniform(0.0),
4522 opacity: 1.0,
4523 box_shadow: row_style.box_shadow.map(Box::new),
4524 background_gradient: row_style.background.clone().map(Box::new),
4525 }
4526 } else {
4527 DrawCommand::None
4528 },
4529 children: row_children,
4530 node_type: Some("TableRow".to_string()),
4531 resolved_style: Some(row_style.clone()),
4532 source_location: row.source_location.clone(),
4533 href: None,
4534 bookmark: row.bookmark.clone(),
4535 alt: None,
4536 is_header_row: is_header,
4537 actual_text: None,
4538 list_numbering: None,
4539 col_span: 1,
4540 overflow: row_style.overflow,
4541 opacity: row_style.opacity,
4542 };
4543
4544 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4545 first_overflow.elements.push(row_element);
4555 } else {
4556 cursor.elements.push(row_element);
4557 cursor.y += row_height;
4558 }
4559
4560 pages.extend(all_overflow_pages);
4562 }
4563
4564 #[allow(clippy::too_many_arguments)]
4565 #[allow(clippy::too_many_arguments)]
4566 fn layout_text(
4567 &self,
4568 content: &str,
4569 href: Option<&str>,
4570 runs: &[TextRun],
4571 style: &ResolvedStyle,
4572 cursor: &mut PageCursor,
4573 pages: &mut Vec<LayoutPage>,
4574 x: f64,
4575 available_width: f64,
4576 font_context: &FontContext,
4577 source_location: Option<&SourceLocation>,
4578 bookmark: Option<&str>,
4579 node_type_override: Option<&str>,
4583 ) {
4584 if style.border_width.top > 0.0
4591 || style.border_width.right > 0.0
4592 || style.border_width.bottom > 0.0
4593 || style.border_width.left > 0.0
4594 {
4595 self.defect(
4596 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4597 );
4598 }
4599 if style.background_color.is_some() {
4600 self.defect(
4601 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4602 );
4603 }
4604 let margin = &style.margin.to_edges();
4605 let text_x = x + margin.left;
4606 let text_width = match style.width {
4613 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4614 SizeConstraint::Auto => available_width - margin.horizontal(),
4615 };
4616 let text_width = text_width.min(style.max_width).max(style.min_width);
4627
4628 cursor.y += margin.top;
4629
4630 if !runs.is_empty() {
4632 self.layout_text_runs(
4633 runs,
4634 href,
4635 style,
4636 cursor,
4637 pages,
4638 text_x,
4639 text_width,
4640 font_context,
4641 source_location,
4642 bookmark,
4643 node_type_override,
4644 );
4645 cursor.y += margin.bottom;
4646 return;
4647 }
4648
4649 let content = substitute_page_placeholders(content);
4650 let transformed = apply_text_transform(&content, style.text_transform);
4651 let justify = matches!(style.text_align, TextAlign::Justify);
4652 let lines = match style.line_breaking {
4653 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4654 font_context,
4655 &transformed,
4656 text_width,
4657 style.font_size,
4658 &style.font_family,
4659 style.font_weight,
4660 style.font_style,
4661 style.letter_spacing,
4662 style.word_spacing,
4663 style.hyphens,
4664 style.lang.as_deref(),
4665 justify,
4666 ),
4667 LineBreaking::Greedy => self.text_layout.break_into_lines(
4668 font_context,
4669 &transformed,
4670 text_width,
4671 style.font_size,
4672 &style.font_family,
4673 style.font_weight,
4674 style.font_style,
4675 style.letter_spacing,
4676 style.word_spacing,
4677 style.hyphens,
4678 style.lang.as_deref(),
4679 ),
4680 };
4681
4682 let lines = match style.text_overflow {
4684 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4685 font_context,
4686 lines,
4687 text_width,
4688 style.font_size,
4689 &style.font_family,
4690 style.font_weight,
4691 style.font_style,
4692 style.letter_spacing,
4693 style.word_spacing,
4694 ),
4695 TextOverflow::Clip => self.text_layout.truncate_clip(
4696 font_context,
4697 lines,
4698 text_width,
4699 style.font_size,
4700 &style.font_family,
4701 style.font_weight,
4702 style.font_style,
4703 style.letter_spacing,
4704 style.word_spacing,
4705 ),
4706 TextOverflow::Wrap => lines,
4707 };
4708
4709 let line_height = style.font_size * style.line_height;
4710
4711 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4713 let decision = page_break::decide_break(
4714 cursor.remaining_height(),
4715 &line_heights,
4716 true,
4717 style.min_orphan_lines as usize,
4718 style.min_widow_lines as usize,
4719 );
4720
4721 let mut snapshot = cursor.elements.len();
4723 let mut container_start_y = cursor.content_y + cursor.y;
4724 let mut is_first_element = true;
4725
4726 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4728 pages.push(cursor.finalize());
4729 *cursor = cursor.new_page();
4730 snapshot = cursor.elements.len();
4731 container_start_y = cursor.content_y + cursor.y;
4732 }
4733
4734 let forced_break_at = match decision {
4736 page_break::BreakDecision::Split {
4737 items_on_current_page,
4738 } => Some(items_on_current_page),
4739 _ => None,
4740 };
4741 let mut first_break_done = false;
4742
4743 for (line_idx, line) in lines.iter().enumerate() {
4744 let needs_break = if let Some(break_at) = forced_break_at {
4746 if !first_break_done && line_idx == break_at {
4747 true
4748 } else {
4749 line_height > cursor.remaining_height()
4750 }
4751 } else {
4752 line_height > cursor.remaining_height()
4753 };
4754
4755 if needs_break {
4756 first_break_done = true;
4757 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4759 if !line_elements.is_empty() {
4760 let container_height = cursor.content_y + cursor.y - container_start_y;
4761 cursor.elements.push(LayoutElement {
4762 x: text_x,
4763 y: container_start_y,
4764 width: text_width,
4765 height: container_height,
4766 draw: DrawCommand::None,
4767 children: line_elements,
4768 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4769 resolved_style: Some(style.clone()),
4770 source_location: source_location.cloned(),
4771 href: href.map(|s| s.to_string()),
4772 bookmark: if is_first_element {
4773 bookmark.map(|s| s.to_string())
4774 } else {
4775 None
4776 },
4777 alt: None,
4778 is_header_row: false,
4779 actual_text: None,
4780 list_numbering: None,
4781 col_span: 1,
4782 overflow: Overflow::default(),
4783 opacity: 1.0,
4784 });
4785 is_first_element = false;
4786 }
4787
4788 pages.push(cursor.finalize());
4789 *cursor = cursor.new_page();
4790
4791 snapshot = cursor.elements.len();
4793 container_start_y = cursor.content_y + cursor.y;
4794 }
4795
4796 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4797
4798 let rendered_width = if glyphs.is_empty() {
4802 line.width
4803 } else {
4804 let last = &glyphs[glyphs.len() - 1];
4805 (last.x_offset + last.x_advance).max(line.width * 0.5)
4806 };
4807
4808 let line_x = match style.text_align {
4809 TextAlign::Left => text_x,
4810 TextAlign::Right => text_x + text_width - rendered_width,
4811 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4812 TextAlign::Justify => text_x,
4813 };
4814
4815 let is_last_line = line_idx == lines.len() - 1;
4823 let user_ws = style.word_spacing;
4824 let (justified_width, word_spacing) =
4825 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4826 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4827 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4828 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4829 let s = glyphs[..=idx]
4830 .iter()
4831 .filter(|g| g.char_value == ' ')
4832 .count();
4833 (w, s)
4834 } else {
4835 (0.0, 0)
4836 };
4837 let slack = text_width - natural_width;
4838 let ws = if space_count > 0 && slack.abs() > 0.01 {
4839 slack / space_count as f64
4840 } else {
4841 0.0
4842 };
4843 (text_width, user_ws + ws)
4844 } else {
4845 (rendered_width, user_ws)
4846 };
4847
4848 let text_line = TextLine {
4849 x: line_x,
4850 y: cursor.content_y
4856 + cursor.y
4857 + baseline_in_line(
4858 line_height,
4859 style.font_size,
4860 font_context.baseline_metrics(
4861 &style.font_family,
4862 style.font_weight,
4863 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4864 ),
4865 ),
4866 glyphs,
4867 width: justified_width,
4868 height: line_height,
4869 word_spacing,
4870 };
4871
4872 cursor.elements.push(LayoutElement {
4873 x: line_x,
4874 y: cursor.content_y + cursor.y,
4875 width: justified_width,
4876 height: line_height,
4877 draw: DrawCommand::Text {
4878 lines: vec![text_line],
4879 color: style.color,
4880 text_decoration: style.text_decoration,
4881 opacity: 1.0,
4882 },
4883 children: vec![],
4884 node_type: Some("TextLine".to_string()),
4885 resolved_style: Some(style.clone()),
4886 source_location: None,
4887 href: href.map(|s| s.to_string()),
4888 bookmark: None,
4889 alt: None,
4890 is_header_row: false,
4891 actual_text: None,
4892 list_numbering: None,
4893 col_span: 1,
4894 overflow: Overflow::default(),
4895 opacity: 1.0,
4896 });
4897
4898 cursor.y += line_height;
4899 }
4900
4901 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4903 if !line_elements.is_empty() {
4904 let container_height = cursor.content_y + cursor.y - container_start_y;
4905 cursor.elements.push(LayoutElement {
4906 x: text_x,
4907 y: container_start_y,
4908 width: text_width,
4909 height: container_height,
4910 draw: DrawCommand::None,
4911 children: line_elements,
4912 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4913 resolved_style: Some(style.clone()),
4914 source_location: source_location.cloned(),
4915 href: href.map(|s| s.to_string()),
4916 bookmark: if is_first_element {
4917 bookmark.map(|s| s.to_string())
4918 } else {
4919 None
4920 },
4921 alt: None,
4922 is_header_row: false,
4923 actual_text: None,
4924 list_numbering: None,
4925 col_span: 1,
4926 overflow: Overflow::default(),
4927 opacity: 1.0,
4928 });
4929 }
4930
4931 cursor.y += margin.bottom;
4932 }
4933
4934 #[allow(clippy::too_many_arguments)]
4936 #[allow(clippy::too_many_arguments)]
4937 fn layout_text_runs(
4938 &self,
4939 runs: &[TextRun],
4940 parent_href: Option<&str>,
4941 style: &ResolvedStyle,
4942 cursor: &mut PageCursor,
4943 pages: &mut Vec<LayoutPage>,
4944 text_x: f64,
4945 text_width: f64,
4946 font_context: &FontContext,
4947 source_location: Option<&SourceLocation>,
4948 bookmark: Option<&str>,
4949 node_type_override: Option<&str>,
4951 ) {
4952 let mut styled_chars: Vec<StyledChar> = Vec::new();
4954 for run in runs {
4955 let run_style = run.style.resolve(Some(style), text_width);
4956 let run_href = run.href.as_deref().or(parent_href);
4957 let transform = run_style.text_transform;
4958 let run_content = substitute_page_placeholders(&run.content);
4959 let mut prev_is_whitespace = true;
4960 for ch in run_content.chars() {
4961 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4962 prev_is_whitespace = ch.is_whitespace();
4963 styled_chars.push(StyledChar {
4964 ch: transformed_ch,
4965 font_family: run_style.font_family.clone(),
4966 font_size: run_style.font_size,
4967 font_weight: run_style.font_weight,
4968 font_style: run_style.font_style,
4969 color: run_style.color,
4970 href: run_href.map(|s| s.to_string()),
4971 text_decoration: run_style.text_decoration,
4972 letter_spacing: run_style.letter_spacing,
4973 word_spacing: run_style.word_spacing,
4974 });
4975 }
4976 }
4977
4978 let justify = matches!(style.text_align, TextAlign::Justify);
4980 let broken_lines = match style.line_breaking {
4981 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4982 font_context,
4983 &styled_chars,
4984 text_width,
4985 style.hyphens,
4986 style.lang.as_deref(),
4987 justify,
4988 ),
4989 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4990 font_context,
4991 &styled_chars,
4992 text_width,
4993 style.hyphens,
4994 style.lang.as_deref(),
4995 ),
4996 };
4997
4998 let broken_lines = match style.text_overflow {
5000 TextOverflow::Ellipsis => {
5001 self.text_layout
5002 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
5003 }
5004 TextOverflow::Clip => {
5005 self.text_layout
5006 .truncate_runs_clip(font_context, broken_lines, text_width)
5007 }
5008 TextOverflow::Wrap => broken_lines,
5009 };
5010
5011 let line_height = style.font_size * style.line_height;
5012
5013 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
5015 let decision = page_break::decide_break(
5016 cursor.remaining_height(),
5017 &line_heights,
5018 true,
5019 style.min_orphan_lines as usize,
5020 style.min_widow_lines as usize,
5021 );
5022
5023 let mut snapshot = cursor.elements.len();
5024 let mut container_start_y = cursor.content_y + cursor.y;
5025 let mut is_first_element = true;
5026
5027 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
5028 pages.push(cursor.finalize());
5029 *cursor = cursor.new_page();
5030 snapshot = cursor.elements.len();
5031 container_start_y = cursor.content_y + cursor.y;
5032 }
5033
5034 let forced_break_at = match decision {
5035 page_break::BreakDecision::Split {
5036 items_on_current_page,
5037 } => Some(items_on_current_page),
5038 _ => None,
5039 };
5040 let mut first_break_done = false;
5041
5042 for (line_idx, run_line) in broken_lines.iter().enumerate() {
5043 let needs_break = if let Some(break_at) = forced_break_at {
5044 if !first_break_done && line_idx == break_at {
5045 true
5046 } else {
5047 line_height > cursor.remaining_height()
5048 }
5049 } else {
5050 line_height > cursor.remaining_height()
5051 };
5052
5053 if needs_break {
5054 first_break_done = true;
5055 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5056 if !line_elements.is_empty() {
5057 let container_height = cursor.content_y + cursor.y - container_start_y;
5058 cursor.elements.push(LayoutElement {
5059 x: text_x,
5060 y: container_start_y,
5061 width: text_width,
5062 height: container_height,
5063 draw: DrawCommand::None,
5064 children: line_elements,
5065 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5066 resolved_style: Some(style.clone()),
5067 source_location: source_location.cloned(),
5068 href: parent_href.map(|s| s.to_string()),
5069 bookmark: if is_first_element {
5070 bookmark.map(|s| s.to_string())
5071 } else {
5072 None
5073 },
5074 alt: None,
5075 is_header_row: false,
5076 actual_text: None,
5077 list_numbering: None,
5078 col_span: 1,
5079 overflow: Overflow::default(),
5080 opacity: 1.0,
5081 });
5082 is_first_element = false;
5083 }
5084
5085 pages.push(cursor.finalize());
5086 *cursor = cursor.new_page();
5087
5088 snapshot = cursor.elements.len();
5089 container_start_y = cursor.content_y + cursor.y;
5090 }
5091
5092 let line_x = match style.text_align {
5093 TextAlign::Left => text_x,
5094 TextAlign::Right => text_x + text_width - run_line.width,
5095 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
5096 TextAlign::Justify => text_x,
5097 };
5098
5099 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
5100
5101 let is_last_line = line_idx == broken_lines.len() - 1;
5108 let user_ws = style.word_spacing;
5109 let (justified_width, word_spacing) =
5110 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5111 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
5112 let (natural_width, space_count) = if let Some(idx) = last_non_space {
5113 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
5114 let s = glyphs[..=idx]
5115 .iter()
5116 .filter(|g| g.char_value == ' ')
5117 .count();
5118 (w, s)
5119 } else {
5120 (0.0, 0)
5121 };
5122 let slack = text_width - natural_width;
5123 let ws = if space_count > 0 && slack.abs() > 0.01 {
5124 slack / space_count as f64
5125 } else {
5126 0.0
5127 };
5128 (text_width, user_ws + ws)
5129 } else {
5130 (run_line.width, user_ws)
5131 };
5132
5133 let text_line = TextLine {
5134 x: line_x,
5135 y: cursor.content_y
5141 + cursor.y
5142 + baseline_in_line(
5143 line_height,
5144 style.font_size,
5145 font_context.baseline_metrics(
5146 &style.font_family,
5147 style.font_weight,
5148 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5149 ),
5150 ),
5151 glyphs,
5152 width: justified_width,
5153 height: line_height,
5154 word_spacing,
5155 };
5156
5157 let text_dec = run_line
5159 .chars
5160 .iter()
5161 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
5162 .map(|sc| sc.text_decoration)
5163 .unwrap_or(style.text_decoration);
5164
5165 cursor.elements.push(LayoutElement {
5166 x: line_x,
5167 y: cursor.content_y + cursor.y,
5168 width: justified_width,
5169 height: line_height,
5170 draw: DrawCommand::Text {
5171 lines: vec![text_line],
5172 color: style.color,
5173 text_decoration: text_dec,
5174 opacity: 1.0,
5175 },
5176 children: vec![],
5177 node_type: Some("TextLine".to_string()),
5178 resolved_style: Some(style.clone()),
5179 source_location: None,
5180 href: parent_href.map(|s| s.to_string()),
5181 bookmark: None,
5182 alt: None,
5183 is_header_row: false,
5184 actual_text: None,
5185 list_numbering: None,
5186 col_span: 1,
5187 overflow: Overflow::default(),
5188 opacity: 1.0,
5189 });
5190
5191 cursor.y += line_height;
5192 }
5193
5194 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5195 if !line_elements.is_empty() {
5196 let container_height = cursor.content_y + cursor.y - container_start_y;
5197 cursor.elements.push(LayoutElement {
5198 x: text_x,
5199 y: container_start_y,
5200 width: text_width,
5201 height: container_height,
5202 draw: DrawCommand::None,
5203 children: line_elements,
5204 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5205 resolved_style: Some(style.clone()),
5206 source_location: source_location.cloned(),
5207 href: parent_href.map(|s| s.to_string()),
5208 bookmark: if is_first_element {
5209 bookmark.map(|s| s.to_string())
5210 } else {
5211 None
5212 },
5213 alt: None,
5214 is_header_row: false,
5215 actual_text: None,
5216 list_numbering: None,
5217 col_span: 1,
5218 overflow: Overflow::default(),
5219 opacity: 1.0,
5220 });
5221 }
5222 }
5223
5224 fn build_positioned_glyphs_single_style(
5228 &self,
5229 line: &BrokenLine,
5230 style: &ResolvedStyle,
5231 href: Option<&str>,
5232 font_context: &FontContext,
5233 ) -> Vec<PositionedGlyph> {
5234 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5235 let line_text: String = line.chars.iter().collect();
5236 let direction = style.direction;
5237 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5239
5240 let font_runs = crate::font::fallback::segment_by_font(
5243 &line.chars,
5244 &style.font_family,
5245 style.font_weight,
5246 italic,
5247 font_context.registry(),
5248 );
5249 let needs_per_char_fallback = font_runs.len() > 1
5250 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
5251
5252 if needs_per_char_fallback {
5254 let bidi_runs = if has_bidi {
5255 bidi::analyze_bidi(&line_text, direction)
5256 } else {
5257 vec![crate::text::bidi::BidiRun {
5258 char_start: 0,
5259 char_end: line.chars.len(),
5260 level: unicode_bidi::Level::ltr(),
5261 is_rtl: false,
5262 }]
5263 };
5264
5265 let mut all_glyphs = Vec::new();
5266 let mut bidi_levels = Vec::new();
5267 let mut x = 0.0_f64;
5268
5269 for bidi_run in &bidi_runs {
5271 for font_run in &font_runs {
5273 let start = font_run.start.max(bidi_run.char_start);
5275 let end = font_run.end.min(bidi_run.char_end);
5276 if start >= end {
5277 continue;
5278 }
5279
5280 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
5281 let sub_text: String = sub_chars.iter().collect();
5282 let resolved_family = &font_run.family;
5283
5284 if let Some(font_data) =
5285 font_context.font_data(resolved_family, style.font_weight, italic)
5286 {
5287 if let Some(shaped) = shaping::shape_text_with_direction(
5288 &sub_text,
5289 font_data,
5290 bidi_run.is_rtl,
5291 ) {
5292 let units_per_em = font_context.units_per_em(
5293 resolved_family,
5294 style.font_weight,
5295 italic,
5296 );
5297 let scale = style.font_size / units_per_em as f64;
5298
5299 for sg in &shaped {
5300 let cluster = sg.cluster as usize;
5301 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
5302
5303 let cluster_text = if shaped.len() < sub_chars.len() {
5304 let cluster_end =
5305 self.find_cluster_end(&shaped, sg, sub_chars.len());
5306 if cluster_end > cluster + 1 {
5307 Some(
5308 sub_chars[cluster..cluster_end]
5309 .iter()
5310 .collect::<String>(),
5311 )
5312 } else {
5313 None
5314 }
5315 } else {
5316 None
5317 };
5318
5319 let glyph_x = x + sg.x_offset as f64 * scale;
5320 let glyph_y = sg.y_offset as f64 * scale;
5321 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5322
5323 all_glyphs.push(PositionedGlyph {
5324 glyph_id: sg.glyph_id,
5325 x_offset: glyph_x,
5326 y_offset: glyph_y,
5327 x_advance: advance,
5328 font_size: style.font_size,
5329 font_family: Arc::from(resolved_family.as_str()),
5330 font_weight: style.font_weight,
5331 font_style: style.font_style,
5332 char_value,
5333 color: Some(style.color),
5334 href: href.map(|s| s.to_string()),
5335 text_decoration: style.text_decoration,
5336 letter_spacing: style.letter_spacing,
5337 cluster_text,
5338 });
5339 bidi_levels.push(bidi_run.level);
5340 x += advance;
5341 }
5342 continue;
5343 }
5344 }
5345
5346 for i in start..end {
5348 let ch = line.chars[i];
5349 let glyph_x = x;
5350 let char_width = font_context.char_width(
5351 ch,
5352 resolved_family,
5353 style.font_weight,
5354 italic,
5355 style.font_size,
5356 );
5357 let advance = char_width + style.letter_spacing;
5358 all_glyphs.push(PositionedGlyph {
5359 glyph_id: ch as u16,
5360 x_offset: glyph_x,
5361 y_offset: 0.0,
5362 x_advance: advance,
5363 font_size: style.font_size,
5364 font_family: Arc::from(resolved_family.as_str()),
5365 font_weight: style.font_weight,
5366 font_style: style.font_style,
5367 char_value: ch,
5368 color: Some(style.color),
5369 href: href.map(|s| s.to_string()),
5370 text_decoration: style.text_decoration,
5371 letter_spacing: style.letter_spacing,
5372 cluster_text: None,
5373 });
5374 bidi_levels.push(bidi_run.level);
5375 x += advance;
5376 }
5377 }
5378 }
5379
5380 if has_bidi && !all_glyphs.is_empty() {
5382 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5383 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
5384 }
5385 return all_glyphs;
5386 }
5387
5388 if let Some(font_data) =
5391 font_context.font_data(&style.font_family, style.font_weight, italic)
5392 {
5393 if has_bidi {
5394 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5396 let units_per_em =
5397 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5398 let scale = style.font_size / units_per_em as f64;
5399
5400 let mut all_glyphs = Vec::new();
5401 let mut bidi_levels = Vec::new();
5402 let mut x = 0.0_f64;
5403
5404 for run in &bidi_runs {
5405 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5406 let run_text: String = run_chars.iter().collect();
5407
5408 if let Some(shaped) =
5409 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5410 {
5411 for sg in &shaped {
5412 let cluster = sg.cluster as usize;
5413 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5414
5415 let cluster_text = if shaped.len() < run_chars.len() {
5416 let cluster_end =
5417 self.find_cluster_end(&shaped, sg, run_chars.len());
5418 if cluster_end > cluster + 1 {
5419 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
5420 } else {
5421 None
5422 }
5423 } else {
5424 None
5425 };
5426
5427 let glyph_x = x + sg.x_offset as f64 * scale;
5428 let glyph_y = sg.y_offset as f64 * scale;
5429 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5430
5431 all_glyphs.push(PositionedGlyph {
5432 glyph_id: sg.glyph_id,
5433 x_offset: glyph_x,
5434 y_offset: glyph_y,
5435 x_advance: advance,
5436 font_size: style.font_size,
5437 font_family: Arc::from(style.font_family.as_str()),
5438 font_weight: style.font_weight,
5439 font_style: style.font_style,
5440 char_value,
5441 color: Some(style.color),
5442 href: href.map(|s| s.to_string()),
5443 text_decoration: style.text_decoration,
5444 letter_spacing: style.letter_spacing,
5445 cluster_text,
5446 });
5447 bidi_levels.push(run.level);
5448
5449 x += advance;
5450 }
5451 }
5452 }
5453
5454 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5456 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5457 return glyphs;
5458 }
5459
5460 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5462 let units_per_em =
5463 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5464 let scale = style.font_size / units_per_em as f64;
5465
5466 return self.shaped_glyphs_to_positioned(
5467 &shaped,
5468 &line.chars,
5469 &line.char_positions,
5470 scale,
5471 style.font_size,
5472 &style.font_family,
5473 style.font_weight,
5474 style.font_style,
5475 Some(style.color),
5476 href,
5477 style.text_decoration,
5478 style.letter_spacing,
5479 );
5480 }
5481 }
5482
5483 let mut glyphs: Vec<PositionedGlyph> = line
5485 .chars
5486 .iter()
5487 .enumerate()
5488 .map(|(j, ch)| {
5489 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5490 let char_width = font_context.char_width(
5491 *ch,
5492 &style.font_family,
5493 style.font_weight,
5494 italic,
5495 style.font_size,
5496 );
5497 PositionedGlyph {
5498 glyph_id: *ch as u16,
5499 x_offset: glyph_x,
5500 y_offset: 0.0,
5501 x_advance: char_width,
5502 font_size: style.font_size,
5503 font_family: Arc::from(style.font_family.as_str()),
5504 font_weight: style.font_weight,
5505 font_style: style.font_style,
5506 char_value: *ch,
5507 color: Some(style.color),
5508 href: href.map(|s| s.to_string()),
5509 text_decoration: style.text_decoration,
5510 letter_spacing: style.letter_spacing,
5511 cluster_text: None,
5512 }
5513 })
5514 .collect();
5515
5516 if has_bidi && !glyphs.is_empty() {
5518 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5519 let mut levels = Vec::with_capacity(glyphs.len());
5520 let mut char_idx = 0;
5521 for run in &bidi_runs {
5522 for _ in run.char_start..run.char_end {
5523 if char_idx < glyphs.len() {
5524 levels.push(run.level);
5525 char_idx += 1;
5526 }
5527 }
5528 }
5529 while levels.len() < glyphs.len() {
5531 levels.push(unicode_bidi::Level::ltr());
5532 }
5533 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
5534 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5535 }
5536
5537 glyphs
5538 }
5539
5540 fn build_positioned_glyphs_runs(
5545 &self,
5546 run_line: &RunBrokenLine,
5547 font_context: &FontContext,
5548 direction: Direction,
5549 ) -> Vec<PositionedGlyph> {
5550 let chars = &run_line.chars;
5551 if chars.is_empty() {
5552 return vec![];
5553 }
5554
5555 let resolved_families: Vec<String> = chars
5564 .iter()
5565 .map(|sc| {
5566 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5567 if !sc.font_family.contains(',') {
5568 let primary =
5569 font_context
5570 .registry()
5571 .resolve(&sc.font_family, sc.font_weight, italic);
5572 if sc.ch.is_whitespace()
5573 || sc.ch == PAGE_NUMBER_SENTINEL
5574 || sc.ch == TOTAL_PAGES_SENTINEL
5575 || primary.has_char(sc.ch)
5576 {
5577 return sc.font_family.clone();
5578 }
5579 }
5580 let (_, family) = font_context.registry().resolve_for_char(
5581 &sc.font_family,
5582 sc.ch,
5583 sc.font_weight,
5584 italic,
5585 );
5586 family
5587 })
5588 .collect();
5589
5590 let line_text: String = chars.iter().map(|c| c.ch).collect();
5591 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5592 let bidi_runs = if has_bidi {
5593 Some(bidi::analyze_bidi(&line_text, direction))
5594 } else {
5595 None
5596 };
5597
5598 let mut glyphs = Vec::new();
5599 let mut bidi_levels = Vec::new();
5600 let mut i = 0;
5601
5602 while i < chars.len() {
5603 let sc = &chars[i];
5604 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5605 let resolved_family = &resolved_families[i];
5606
5607 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5609 runs.iter()
5610 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5611 });
5612
5613 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5615 {
5616 let run_start = i;
5618 let mut run_end = i + 1;
5619 while run_end < chars.len() {
5620 let next = &chars[run_end];
5621 let next_italic =
5622 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5623 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5624 runs.iter()
5625 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5626 });
5627 if resolved_families[run_end] == *resolved_family
5629 && next.font_weight == sc.font_weight
5630 && next_italic == italic
5631 && (next.font_size - sc.font_size).abs() < 0.001
5632 && next_is_rtl == is_rtl
5633 {
5634 run_end += 1;
5635 } else {
5636 break;
5637 }
5638 }
5639
5640 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5641 if let Some(shaped) =
5642 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5643 {
5644 let units_per_em =
5645 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5646 let scale = sc.font_size / units_per_em as f64;
5647
5648 let run_chars: Vec<char> =
5650 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5651 let run_positions: Vec<f64> = (run_start..run_end)
5652 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5653 .collect();
5654
5655 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5657 &shaped,
5658 &chars[run_start..run_end],
5659 &run_chars,
5660 &run_positions,
5661 scale,
5662 );
5663 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5665 for g in &mut run_glyphs {
5666 g.font_family = resolved_family_arc.clone();
5667 }
5668 let run_level = if is_rtl {
5670 unicode_bidi::Level::rtl()
5671 } else {
5672 unicode_bidi::Level::ltr()
5673 };
5674 for _ in &run_glyphs {
5675 bidi_levels.push(run_level);
5676 }
5677 glyphs.extend(run_glyphs);
5678 i = run_end;
5679 continue;
5680 }
5681 }
5682
5683 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5685 let char_width = font_context.char_width(
5686 sc.ch,
5687 resolved_family,
5688 sc.font_weight,
5689 italic,
5690 sc.font_size,
5691 );
5692 glyphs.push(PositionedGlyph {
5693 glyph_id: sc.ch as u16,
5694 x_offset: glyph_x,
5695 y_offset: 0.0,
5696 x_advance: char_width,
5697 font_size: sc.font_size,
5698 font_family: Arc::from(resolved_family.as_str()),
5699 font_weight: sc.font_weight,
5700 font_style: sc.font_style,
5701 char_value: sc.ch,
5702 color: Some(sc.color),
5703 href: sc.href.clone(),
5704 text_decoration: sc.text_decoration,
5705 letter_spacing: sc.letter_spacing,
5706 cluster_text: None,
5707 });
5708 bidi_levels.push(if is_rtl {
5709 unicode_bidi::Level::rtl()
5710 } else {
5711 unicode_bidi::Level::ltr()
5712 });
5713 i += 1;
5714 }
5715
5716 if has_bidi && !glyphs.is_empty() {
5718 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
5719 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5720 }
5721
5722 glyphs
5723 }
5724
5725 #[allow(clippy::too_many_arguments)]
5727 fn shaped_glyphs_to_positioned(
5728 &self,
5729 shaped: &[shaping::ShapedGlyph],
5730 chars: &[char],
5731 _char_positions: &[f64],
5732 scale: f64,
5733 font_size: f64,
5734 font_family: &str,
5735 font_weight: u32,
5736 font_style: FontStyle,
5737 color: Option<Color>,
5738 href: Option<&str>,
5739 text_decoration: TextDecoration,
5740 letter_spacing: f64,
5741 ) -> Vec<PositionedGlyph> {
5742 let mut result = Vec::with_capacity(shaped.len());
5743 let mut x = 0.0_f64;
5744
5745 for sg in shaped {
5746 let cluster = sg.cluster as usize;
5747 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5748
5749 let cluster_text = if shaped.len() < chars.len() {
5751 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5754 if cluster_end > cluster + 1 {
5755 Some(chars[cluster..cluster_end].iter().collect::<String>())
5756 } else {
5757 None
5758 }
5759 } else {
5760 None
5761 };
5762
5763 let glyph_x = x + sg.x_offset as f64 * scale;
5765 let glyph_y = sg.y_offset as f64 * scale;
5766 let advance = sg.x_advance as f64 * scale + letter_spacing;
5767
5768 result.push(PositionedGlyph {
5769 glyph_id: sg.glyph_id,
5770 x_offset: glyph_x,
5771 y_offset: glyph_y,
5772 x_advance: advance,
5773 font_size,
5774 font_family: Arc::from(font_family),
5775 font_weight,
5776 font_style,
5777 char_value,
5778 color,
5779 href: href.map(|s| s.to_string()),
5780 text_decoration,
5781 letter_spacing,
5782 cluster_text,
5783 });
5784
5785 x += advance;
5786 }
5787
5788 result
5789 }
5790
5791 fn shaped_glyphs_to_positioned_runs(
5793 &self,
5794 shaped: &[shaping::ShapedGlyph],
5795 styled_chars: &[StyledChar],
5796 chars: &[char],
5797 char_positions: &[f64],
5798 scale: f64,
5799 ) -> Vec<PositionedGlyph> {
5800 let mut result = Vec::with_capacity(shaped.len());
5801 let base_x = char_positions.first().copied().unwrap_or(0.0);
5803 let mut x = 0.0_f64;
5804
5805 for sg in shaped {
5806 let cluster = sg.cluster as usize;
5807 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5808 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5809
5810 let cluster_text = if shaped.len() < chars.len() {
5811 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5812 if cluster_end > cluster + 1 {
5813 Some(chars[cluster..cluster_end].iter().collect::<String>())
5814 } else {
5815 None
5816 }
5817 } else {
5818 None
5819 };
5820
5821 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5822 let glyph_y = sg.y_offset as f64 * scale;
5823 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5824
5825 result.push(PositionedGlyph {
5826 glyph_id: sg.glyph_id,
5827 x_offset: glyph_x,
5828 y_offset: glyph_y,
5829 x_advance: advance,
5830 font_size: sc.font_size,
5831 font_family: Arc::from(sc.font_family.as_str()),
5832 font_weight: sc.font_weight,
5833 font_style: sc.font_style,
5834 char_value,
5835 color: Some(sc.color),
5836 href: sc.href.clone(),
5837 text_decoration: sc.text_decoration,
5838 letter_spacing: sc.letter_spacing,
5839 cluster_text,
5840 });
5841
5842 x += advance;
5843 }
5844
5845 result
5846 }
5847
5848 fn find_cluster_end(
5850 &self,
5851 shaped: &[shaping::ShapedGlyph],
5852 current: &shaping::ShapedGlyph,
5853 num_chars: usize,
5854 ) -> usize {
5855 for sg in shaped {
5857 if sg.cluster > current.cluster {
5858 return sg.cluster as usize;
5859 }
5860 }
5861 num_chars
5863 }
5864
5865 fn image_display_size(
5875 &self,
5876 src: &str,
5877 style: &ResolvedStyle,
5878 explicit_width: Option<f64>,
5879 explicit_height: Option<f64>,
5880 available_width: f64,
5881 ) -> (f64, f64) {
5882 let dims = if src.is_empty() {
5883 None
5884 } else {
5885 self.get_image_dimensions(src)
5886 };
5887 let aspect = dims
5888 .map(|(w, h)| {
5889 if w > 0 {
5890 f64::from(h) / f64::from(w)
5891 } else {
5892 0.75
5893 }
5894 })
5895 .unwrap_or(0.75);
5896
5897 let style_w = match style.width {
5898 SizeConstraint::Fixed(w) => Some(w),
5899 SizeConstraint::Auto => None,
5900 };
5901 let style_h = match style.height {
5902 SizeConstraint::Fixed(h) => Some(h),
5903 SizeConstraint::Auto => None,
5904 };
5905 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5906
5907 let width_source = style_w.or(explicit_width);
5908 let height_source = style_h.or(explicit_height);
5909 match (width_source, height_source) {
5910 (Some(w), Some(h)) => (clamp(w), h),
5911 (Some(w), None) => {
5912 let w = clamp(w);
5913 (w, w * aspect)
5914 }
5915 (None, Some(h)) => (clamp(h / aspect), h),
5916 (None, None) => {
5917 let w = clamp(
5920 dims.map(|(w, _)| f64::from(w))
5921 .unwrap_or(available_width)
5922 .min(available_width),
5923 );
5924 (w, w * aspect)
5925 }
5926 }
5927 }
5928
5929 #[allow(clippy::too_many_arguments)]
5930 fn layout_image(
5931 &self,
5932 node: &Node,
5933 style: &ResolvedStyle,
5934 cursor: &mut PageCursor,
5935 pages: &mut Vec<LayoutPage>,
5936 x: f64,
5937 available_width: f64,
5938 explicit_width: Option<f64>,
5939 explicit_height: Option<f64>,
5940 ) {
5941 let margin = &style.margin.to_edges();
5942
5943 let src = match &node.kind {
5945 NodeKind::Image { src, .. } => src.as_str(),
5946 _ => "",
5947 };
5948
5949 let loaded = if !src.is_empty() {
5950 crate::image_loader::load_image(src).ok()
5951 } else {
5952 None
5953 };
5954
5955 let (img_width, img_height) = self.image_display_size(
5956 src,
5957 style,
5958 explicit_width,
5959 explicit_height,
5960 available_width - margin.horizontal(),
5961 );
5962
5963 let total_height = img_height + margin.vertical();
5964
5965 if total_height > cursor.remaining_height() {
5966 pages.push(cursor.finalize());
5967 *cursor = cursor.new_page();
5968 }
5969
5970 cursor.y += margin.top;
5971
5972 let draw = if let Some(image_data) = loaded {
5973 DrawCommand::Image { image_data }
5974 } else {
5975 DrawCommand::ImagePlaceholder
5976 };
5977
5978 cursor.elements.push(LayoutElement {
5979 x: x + margin.left,
5980 y: cursor.content_y + cursor.y,
5981 width: img_width,
5982 height: img_height,
5983 draw,
5984 children: vec![],
5985 node_type: Some(node_kind_name(&node.kind).to_string()),
5986 resolved_style: Some(style.clone()),
5987 source_location: node.source_location.clone(),
5988 href: node.href.clone(),
5989 bookmark: node.bookmark.clone(),
5990 alt: node.alt.clone(),
5991 is_header_row: false,
5992 actual_text: None,
5993 list_numbering: None,
5994 col_span: 1,
5995 overflow: style.overflow,
5996 opacity: style.opacity,
5997 });
5998
5999 cursor.y += img_height + margin.bottom;
6000 }
6001
6002 #[allow(clippy::too_many_arguments)]
6004 fn layout_svg(
6005 &self,
6006 node: &Node,
6007 style: &ResolvedStyle,
6008 cursor: &mut PageCursor,
6009 pages: &mut Vec<LayoutPage>,
6010 x: f64,
6011 _available_width: f64,
6012 svg_width: f64,
6013 svg_height: f64,
6014 view_box: Option<&str>,
6015 content: &str,
6016 ) {
6017 let margin = &style.margin.to_edges();
6018 let total_height = svg_height + margin.vertical();
6019
6020 if total_height > cursor.remaining_height() {
6021 pages.push(cursor.finalize());
6022 *cursor = cursor.new_page();
6023 }
6024
6025 cursor.y += margin.top;
6026
6027 let vb = view_box
6028 .and_then(crate::svg::parse_view_box)
6029 .unwrap_or(crate::svg::ViewBox {
6030 min_x: 0.0,
6031 min_y: 0.0,
6032 width: svg_width,
6033 height: svg_height,
6034 });
6035
6036 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
6037
6038 cursor.elements.push(LayoutElement {
6039 x: x + margin.left,
6040 y: cursor.content_y + cursor.y,
6041 width: svg_width,
6042 height: svg_height,
6043 draw: DrawCommand::Svg {
6044 commands,
6045 width: svg_width,
6046 height: svg_height,
6047 viewbox_min_x: vb.min_x,
6048 viewbox_min_y: vb.min_y,
6049 viewbox_width: vb.width,
6050 viewbox_height: vb.height,
6051 clip: false,
6052 },
6053 children: vec![],
6054 node_type: Some("Svg".to_string()),
6055 resolved_style: Some(style.clone()),
6056 source_location: node.source_location.clone(),
6057 href: node.href.clone(),
6058 bookmark: node.bookmark.clone(),
6059 alt: node.alt.clone(),
6060 is_header_row: false,
6061 actual_text: None,
6062 list_numbering: None,
6063 col_span: 1,
6064 overflow: style.overflow,
6065 opacity: style.opacity,
6066 });
6067
6068 cursor.y += svg_height + margin.bottom;
6069 }
6070
6071 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
6073 use crate::svg::SvgCommand;
6074
6075 let mut commands = Vec::new();
6076 let mut cur_x = 0.0_f64;
6077 let mut cur_y = 0.0_f64;
6078
6079 for op in operations {
6080 match op {
6081 CanvasOp::MoveTo { x, y } => {
6082 commands.push(SvgCommand::MoveTo(*x, *y));
6083 cur_x = *x;
6084 cur_y = *y;
6085 }
6086 CanvasOp::LineTo { x, y } => {
6087 commands.push(SvgCommand::LineTo(*x, *y));
6088 cur_x = *x;
6089 cur_y = *y;
6090 }
6091 CanvasOp::BezierCurveTo {
6092 cp1x,
6093 cp1y,
6094 cp2x,
6095 cp2y,
6096 x,
6097 y,
6098 } => {
6099 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
6100 cur_x = *x;
6101 cur_y = *y;
6102 }
6103 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
6104 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
6106 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
6107 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
6108 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
6109 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
6110 cur_x = *x;
6111 cur_y = *y;
6112 }
6113 CanvasOp::ClosePath => {
6114 commands.push(SvgCommand::ClosePath);
6115 }
6116 CanvasOp::Rect {
6117 x,
6118 y,
6119 width,
6120 height,
6121 } => {
6122 commands.push(SvgCommand::MoveTo(*x, *y));
6123 commands.push(SvgCommand::LineTo(*x + *width, *y));
6124 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
6125 commands.push(SvgCommand::LineTo(*x, *y + *height));
6126 commands.push(SvgCommand::ClosePath);
6127 cur_x = *x;
6128 cur_y = *y;
6129 }
6130 CanvasOp::Circle { cx, cy, r } => {
6131 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
6132 }
6133 CanvasOp::Ellipse { cx, cy, rx, ry } => {
6134 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
6135 }
6136 CanvasOp::Arc {
6137 cx,
6138 cy,
6139 r,
6140 start_angle,
6141 end_angle,
6142 counterclockwise,
6143 } => {
6144 let steps = 32;
6148 let mut sweep = end_angle - start_angle;
6149 if !counterclockwise && sweep < 0.0 {
6150 sweep += 2.0 * std::f64::consts::PI;
6151 }
6152 if *counterclockwise && sweep > 0.0 {
6153 sweep -= 2.0 * std::f64::consts::PI;
6154 }
6155 for i in 0..=steps {
6156 let t = *start_angle + sweep * (i as f64 / steps as f64);
6157 let px = cx + r * t.cos();
6158 let py = cy + r * t.sin();
6159 if i == 0 {
6160 commands.push(SvgCommand::MoveTo(px, py));
6161 } else {
6162 commands.push(SvgCommand::LineTo(px, py));
6163 }
6164 }
6165 }
6166 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
6167 CanvasOp::Fill => commands.push(SvgCommand::Fill),
6168 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
6169 CanvasOp::SetFillColor { r, g, b } => {
6170 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
6172 }
6173 CanvasOp::SetStrokeColor { r, g, b } => {
6174 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
6175 }
6176 CanvasOp::SetLineWidth { width } => {
6177 commands.push(SvgCommand::SetStrokeWidth(*width));
6178 }
6179 CanvasOp::SetLineCap { cap } => {
6180 commands.push(SvgCommand::SetLineCap(*cap));
6181 }
6182 CanvasOp::SetLineJoin { join } => {
6183 commands.push(SvgCommand::SetLineJoin(*join));
6184 }
6185 CanvasOp::Save => commands.push(SvgCommand::SaveState),
6186 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
6187 }
6188 }
6189
6190 commands
6191 }
6192
6193 #[allow(clippy::too_many_arguments)]
6195 fn layout_canvas(
6196 &self,
6197 node: &Node,
6198 style: &ResolvedStyle,
6199 cursor: &mut PageCursor,
6200 pages: &mut Vec<LayoutPage>,
6201 x: f64,
6202 _available_width: f64,
6203 canvas_width: f64,
6204 canvas_height: f64,
6205 operations: &[CanvasOp],
6206 ) {
6207 let margin = style.margin.to_edges();
6208 let total_height = canvas_height + margin.top + margin.bottom;
6209
6210 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
6212 pages.push(cursor.finalize());
6213 *cursor = cursor.new_page();
6214 }
6215
6216 cursor.y += margin.top;
6217
6218 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
6219
6220 cursor.elements.push(LayoutElement {
6221 x: x + margin.left,
6222 y: cursor.content_y + cursor.y,
6223 width: canvas_width,
6224 height: canvas_height,
6225 draw: DrawCommand::Svg {
6226 commands: svg_commands,
6227 width: canvas_width,
6228 height: canvas_height,
6229 viewbox_min_x: 0.0,
6232 viewbox_min_y: 0.0,
6233 viewbox_width: canvas_width,
6234 viewbox_height: canvas_height,
6235 clip: true,
6236 },
6237 children: vec![],
6238 node_type: Some("Canvas".to_string()),
6239 resolved_style: Some(style.clone()),
6240 source_location: node.source_location.clone(),
6241 href: node.href.clone(),
6242 bookmark: node.bookmark.clone(),
6243 alt: node.alt.clone(),
6244 is_header_row: false,
6245 actual_text: None,
6246 list_numbering: None,
6247 col_span: 1,
6248 overflow: style.overflow,
6249 opacity: style.opacity,
6250 });
6251
6252 cursor.y += canvas_height + margin.bottom;
6253 }
6254
6255 #[allow(clippy::too_many_arguments)]
6257 #[allow(clippy::too_many_arguments)]
6259 fn layout_chart(
6260 &self,
6261 node: &Node,
6262 style: &ResolvedStyle,
6263 cursor: &mut PageCursor,
6264 pages: &mut Vec<LayoutPage>,
6265 x: f64,
6266 chart_width: f64,
6267 chart_height: f64,
6268 primitives: Vec<crate::chart::ChartPrimitive>,
6269 node_type_name: &str,
6270 ) {
6271 let margin = &style.margin.to_edges();
6272 let total_height = chart_height + margin.vertical();
6273
6274 if total_height > cursor.remaining_height() {
6275 pages.push(cursor.finalize());
6276 *cursor = cursor.new_page();
6277 }
6278
6279 cursor.y += margin.top;
6280
6281 let draw = DrawCommand::Chart { primitives };
6282
6283 cursor.elements.push(LayoutElement {
6284 x: x + margin.left,
6285 y: cursor.content_y + cursor.y,
6286 width: chart_width,
6287 height: chart_height,
6288 draw,
6289 children: vec![],
6290 node_type: Some(node_type_name.to_string()),
6291 resolved_style: Some(style.clone()),
6292 source_location: node.source_location.clone(),
6293 href: node.href.clone(),
6294 bookmark: node.bookmark.clone(),
6295 alt: node.alt.clone(),
6296 is_header_row: false,
6297 actual_text: None,
6298 list_numbering: None,
6299 col_span: 1,
6300 overflow: style.overflow,
6301 opacity: style.opacity,
6302 });
6303
6304 cursor.y += chart_height + margin.bottom;
6305 }
6306
6307 #[allow(clippy::too_many_arguments)]
6309 fn layout_form_field(
6310 &self,
6311 node: &Node,
6312 style: &ResolvedStyle,
6313 cursor: &mut PageCursor,
6314 pages: &mut Vec<LayoutPage>,
6315 x: f64,
6316 field_width: f64,
6317 field_height: f64,
6318 draw: DrawCommand,
6319 node_type_name: &str,
6320 ) {
6321 let margin = &style.margin.to_edges();
6322 let total_height = field_height + margin.vertical();
6323
6324 if total_height > cursor.remaining_height() {
6325 pages.push(cursor.finalize());
6326 *cursor = cursor.new_page();
6327 }
6328
6329 cursor.y += margin.top;
6330
6331 cursor.elements.push(LayoutElement {
6332 x: x + margin.left,
6333 y: cursor.content_y + cursor.y,
6334 width: field_width,
6335 height: field_height,
6336 draw,
6337 children: vec![],
6338 node_type: Some(node_type_name.to_string()),
6339 resolved_style: Some(style.clone()),
6340 source_location: node.source_location.clone(),
6341 href: node.href.clone(),
6342 bookmark: node.bookmark.clone(),
6343 alt: node.alt.clone(),
6344 is_header_row: false,
6345 actual_text: None,
6346 list_numbering: None,
6347 col_span: 1,
6348 overflow: style.overflow,
6349 opacity: style.opacity,
6350 });
6351
6352 cursor.y += field_height + margin.bottom;
6353 }
6354
6355 #[allow(clippy::too_many_arguments)]
6356 fn layout_barcode(
6357 &self,
6358 node: &Node,
6359 style: &ResolvedStyle,
6360 cursor: &mut PageCursor,
6361 pages: &mut Vec<LayoutPage>,
6362 x: f64,
6363 available_width: f64,
6364 data: &str,
6365 format: crate::barcode::BarcodeFormat,
6366 explicit_width: Option<f64>,
6367 bar_height: f64,
6368 ) {
6369 let margin = &style.margin.to_edges();
6370 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
6371 let total_height = bar_height + margin.vertical();
6372
6373 if total_height > cursor.remaining_height() {
6374 pages.push(cursor.finalize());
6375 *cursor = cursor.new_page();
6376 }
6377
6378 cursor.y += margin.top;
6379
6380 let draw = match crate::barcode::generate_barcode(data, format) {
6381 Ok(barcode_data) => {
6382 let bar_width = if barcode_data.bars.is_empty() {
6383 0.0
6384 } else {
6385 display_width / barcode_data.bars.len() as f64
6386 };
6387 DrawCommand::Barcode {
6388 bars: barcode_data.bars,
6389 bar_width,
6390 height: bar_height,
6391 color: style.color,
6392 }
6393 }
6394 Err(_) => DrawCommand::None,
6395 };
6396
6397 cursor.elements.push(LayoutElement {
6398 x: x + margin.left,
6399 y: cursor.content_y + cursor.y,
6400 width: display_width,
6401 height: bar_height,
6402 draw,
6403 children: vec![],
6404 node_type: Some("Barcode".to_string()),
6405 resolved_style: Some(style.clone()),
6406 source_location: node.source_location.clone(),
6407 href: node.href.clone(),
6408 bookmark: node.bookmark.clone(),
6409 alt: node.alt.clone(),
6410 is_header_row: false,
6411 actual_text: Some(data.to_string()),
6412 list_numbering: None,
6413 col_span: 1,
6414 overflow: style.overflow,
6415 opacity: style.opacity,
6416 });
6417
6418 cursor.y += bar_height + margin.bottom;
6419 }
6420
6421 #[allow(clippy::too_many_arguments)]
6423 fn layout_qrcode(
6424 &self,
6425 node: &Node,
6426 style: &ResolvedStyle,
6427 cursor: &mut PageCursor,
6428 pages: &mut Vec<LayoutPage>,
6429 x: f64,
6430 available_width: f64,
6431 data: &str,
6432 explicit_size: Option<f64>,
6433 ) {
6434 let margin = &style.margin.to_edges();
6435 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6436 let total_height = display_size + margin.vertical();
6437
6438 if total_height > cursor.remaining_height() {
6439 pages.push(cursor.finalize());
6440 *cursor = cursor.new_page();
6441 }
6442
6443 cursor.y += margin.top;
6444
6445 let draw = match crate::qrcode::generate_qr(data) {
6446 Ok(matrix) => {
6447 let module_size = display_size / matrix.size as f64;
6448 DrawCommand::QrCode {
6449 modules: matrix.modules,
6450 module_size,
6451 color: style.color,
6452 }
6453 }
6454 Err(_) => DrawCommand::None,
6455 };
6456
6457 cursor.elements.push(LayoutElement {
6458 x: x + margin.left,
6459 y: cursor.content_y + cursor.y,
6460 width: display_size,
6461 height: display_size,
6462 draw,
6463 children: vec![],
6464 node_type: Some("QrCode".to_string()),
6465 resolved_style: Some(style.clone()),
6466 source_location: node.source_location.clone(),
6467 href: node.href.clone(),
6468 bookmark: node.bookmark.clone(),
6469 alt: node.alt.clone(),
6470 is_header_row: false,
6471 actual_text: Some(data.to_string()),
6472 list_numbering: None,
6473 col_span: 1,
6474 overflow: style.overflow,
6475 opacity: style.opacity,
6476 });
6477
6478 cursor.y += display_size + margin.bottom;
6479 }
6480
6481 fn measure_node_height(
6484 &self,
6485 node: &Node,
6486 available_width: f64,
6487 style: &ResolvedStyle,
6488 font_context: &FontContext,
6489 ) -> f64 {
6490 match &node.kind {
6491 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6496 let measure_width = match style.width {
6499 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6500 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6501 };
6502 if !runs.is_empty() {
6509 let mut styled_chars: Vec<StyledChar> = Vec::new();
6510 for run in runs {
6511 let run_style = run.style.resolve(Some(style), measure_width);
6512 let transform = run_style.text_transform;
6513 let run_content = substitute_page_placeholders(&run.content);
6514 let mut prev_is_whitespace = true;
6515 for ch in run_content.chars() {
6516 let transformed_ch =
6517 apply_char_transform(ch, transform, prev_is_whitespace);
6518 prev_is_whitespace = ch.is_whitespace();
6519 styled_chars.push(StyledChar {
6520 ch: transformed_ch,
6521 font_family: run_style.font_family.clone(),
6522 font_size: run_style.font_size,
6523 font_weight: run_style.font_weight,
6524 font_style: run_style.font_style,
6525 color: run_style.color,
6526 href: None,
6527 text_decoration: run_style.text_decoration,
6528 letter_spacing: run_style.letter_spacing,
6529 word_spacing: run_style.word_spacing,
6530 });
6531 }
6532 }
6533 let justify = matches!(style.text_align, TextAlign::Justify);
6534 let broken_lines = match style.line_breaking {
6535 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6536 font_context,
6537 &styled_chars,
6538 measure_width,
6539 style.hyphens,
6540 style.lang.as_deref(),
6541 justify,
6542 ),
6543 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6544 font_context,
6545 &styled_chars,
6546 measure_width,
6547 style.hyphens,
6548 style.lang.as_deref(),
6549 ),
6550 };
6551 let line_height = style.font_size * style.line_height;
6552 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6553 } else {
6554 let content = substitute_page_placeholders(content);
6555 let transformed = apply_text_transform(&content, style.text_transform);
6556 let justify = matches!(style.text_align, TextAlign::Justify);
6557 let lines = match style.line_breaking {
6558 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6559 font_context,
6560 &transformed,
6561 measure_width,
6562 style.font_size,
6563 &style.font_family,
6564 style.font_weight,
6565 style.font_style,
6566 style.letter_spacing,
6567 style.word_spacing,
6568 style.hyphens,
6569 style.lang.as_deref(),
6570 justify,
6571 ),
6572 LineBreaking::Greedy => self.text_layout.break_into_lines(
6573 font_context,
6574 &transformed,
6575 measure_width,
6576 style.font_size,
6577 &style.font_family,
6578 style.font_weight,
6579 style.font_style,
6580 style.letter_spacing,
6581 style.word_spacing,
6582 style.hyphens,
6583 style.lang.as_deref(),
6584 ),
6585 };
6586 let line_height = style.font_size * style.line_height;
6587 (lines.len() as f64) * line_height + style.padding.vertical()
6588 }
6589 }
6590 NodeKind::Image {
6591 src,
6592 width: explicit_w,
6593 height: explicit_h,
6594 } => {
6595 let (_, h) = self.image_display_size(
6597 src,
6598 style,
6599 *explicit_w,
6600 *explicit_h,
6601 available_width - style.margin.horizontal(),
6602 );
6603 h + style.padding.vertical()
6604 }
6605 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6606 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6607 NodeKind::QrCode { size, .. } => {
6608 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6609 display_size + style.margin.vertical()
6610 }
6611 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6612 NodeKind::BarChart { height, .. }
6613 | NodeKind::LineChart { height, .. }
6614 | NodeKind::PieChart { height, .. }
6615 | NodeKind::AreaChart { height, .. }
6616 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6617 NodeKind::TextField { height, .. }
6618 | NodeKind::Checkbox { height, .. }
6619 | NodeKind::Dropdown { height, .. }
6620 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6621 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6623 if let SizeConstraint::Fixed(h) = style.height {
6630 return h;
6631 }
6632 let outer_width = match style.width {
6633 SizeConstraint::Fixed(w) => w,
6634 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6635 };
6636 let inner_width =
6637 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6638 let col_widths = self.resolve_column_widths(
6639 columns,
6640 inner_width,
6641 &node.children,
6642 style,
6643 font_context,
6644 );
6645 let row_gap = style.row_gap;
6646 let offsets = Self::table_column_offsets(&node.children);
6647 let mut total = 0.0;
6648 for (i, row) in node.children.iter().enumerate() {
6649 if i > 0 {
6650 total += row_gap;
6651 }
6652 total += self.measure_table_row_height(
6653 row,
6654 &col_widths,
6655 &offsets[i],
6656 style,
6657 font_context,
6658 );
6659 }
6660 total + style.padding.vertical() + style.border_width.vertical()
6661 }
6662 NodeKind::TableRow { .. } => {
6663 let n = node.children.len().max(1);
6668 let usable = (available_width - style.margin.horizontal()).max(0.0);
6669 let col_w = usable / n as f64;
6670 let col_widths = vec![col_w; n];
6671 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6672 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6673 }
6674 _ => {
6675 if let SizeConstraint::Fixed(h) = style.height {
6677 return h;
6678 }
6679 let outer_width = match style.width {
6683 SizeConstraint::Fixed(w) => w,
6684 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6685 }
6686 .min(style.max_width)
6687 .max(style.min_width);
6688 let inner_width =
6689 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6690 let children_height =
6691 self.measure_children_height(&node.children, inner_width, style, font_context);
6692 (children_height + style.padding.vertical() + style.border_width.vertical())
6693 .max(style.min_height)
6694 }
6695 }
6696 }
6697
6698 fn measure_children_height(
6699 &self,
6700 children: &[Node],
6701 available_width: f64,
6702 parent_style: &ResolvedStyle,
6703 font_context: &FontContext,
6704 ) -> f64 {
6705 if children
6711 .iter()
6712 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6713 {
6714 let flow: Vec<Node> = children
6715 .iter()
6716 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6717 .cloned()
6718 .collect();
6719 return self.measure_children_height(
6720 &flow,
6721 available_width,
6722 parent_style,
6723 font_context,
6724 );
6725 }
6726 if matches!(parent_style.display, Display::Grid) {
6728 if let Some(template_cols) = &parent_style.grid_template_columns {
6729 let num_columns = template_cols.len();
6730 if num_columns > 0 && !children.is_empty() {
6731 let col_gap = parent_style.column_gap;
6732 let row_gap = parent_style.row_gap;
6733
6734 let content_sizes: Vec<f64> = template_cols
6735 .iter()
6736 .map(|track| {
6737 if matches!(track, GridTrackSize::Auto) {
6738 available_width / num_columns as f64
6739 } else {
6740 0.0
6741 }
6742 })
6743 .collect();
6744
6745 let col_widths = grid::resolve_tracks(
6746 template_cols,
6747 available_width,
6748 col_gap,
6749 &content_sizes,
6750 );
6751
6752 let placements: Vec<Option<&GridPlacement>> = children
6753 .iter()
6754 .map(|child| child.style.grid_placement.as_ref())
6755 .collect();
6756
6757 let item_placements = grid::place_items(&placements, num_columns);
6758 let num_rows = grid::compute_num_rows(&item_placements);
6759
6760 if num_rows == 0 {
6761 return 0.0;
6762 }
6763
6764 let mut row_heights = vec![0.0_f64; num_rows];
6765 for placement in &item_placements {
6766 let cell_width = grid::span_width(
6767 placement.col_start,
6768 placement.col_end,
6769 &col_widths,
6770 col_gap,
6771 );
6772 let child = &children[placement.child_index];
6773 let child_style = child.style.resolve(Some(parent_style), cell_width);
6774 let h =
6775 self.measure_node_height(child, cell_width, &child_style, font_context);
6776 let span = placement.row_end - placement.row_start;
6777 let per_row = h / span as f64;
6778 for rh in row_heights
6779 .iter_mut()
6780 .take(placement.row_end.min(num_rows))
6781 .skip(placement.row_start)
6782 {
6783 if per_row > *rh {
6784 *rh = per_row;
6785 }
6786 }
6787 }
6788
6789 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6790 return row_heights.iter().sum::<f64>() + total_row_gap;
6791 }
6792 }
6793 }
6794
6795 let direction = parent_style.flex_direction;
6796 let row_gap = parent_style.row_gap;
6797 let column_gap = parent_style.column_gap;
6798
6799 match direction {
6800 FlexDirection::Row | FlexDirection::RowReverse => {
6801 let styles: Vec<ResolvedStyle> = children
6804 .iter()
6805 .map(|child| child.style.resolve(Some(parent_style), available_width))
6806 .collect();
6807
6808 let base_widths: Vec<f64> = children
6809 .iter()
6810 .zip(&styles)
6811 .map(|(child, style)| match style.flex_basis {
6812 SizeConstraint::Fixed(w) => w,
6813 SizeConstraint::Auto => match style.width {
6814 SizeConstraint::Fixed(w) => w,
6815 SizeConstraint::Auto => self
6816 .measure_intrinsic_width(child, style, font_context)
6817 .min(available_width),
6818 },
6819 })
6820 .collect();
6821
6822 let lines = match parent_style.flex_wrap {
6823 FlexWrap::NoWrap => {
6824 vec![flex::WrapLine {
6825 start: 0,
6826 end: children.len(),
6827 }]
6828 }
6829 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6830 flex::partition_into_lines(&base_widths, column_gap, available_width)
6831 }
6832 };
6833
6834 let mut final_widths = base_widths.clone();
6836 for line in &lines {
6837 let line_count = line.end - line.start;
6838 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6839 let distributable = available_width - line_gap;
6840 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6841 let remaining = distributable - total_base;
6842
6843 if remaining > 0.0 {
6844 let total_grow: f64 = styles[line.start..line.end]
6845 .iter()
6846 .map(|s| s.flex_grow)
6847 .sum();
6848 if total_grow > 0.0 {
6849 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6850 final_widths[line.start + j] = base_widths[line.start + j]
6851 + remaining * (s.flex_grow / total_grow);
6852 }
6853 }
6854 } else if remaining < 0.0 {
6855 let total_shrink: f64 = styles[line.start..line.end]
6856 .iter()
6857 .enumerate()
6858 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6859 .sum();
6860 if total_shrink > 0.0 {
6861 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6862 let factor =
6863 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6864 let w = base_widths[line.start + j] + remaining * factor;
6865 final_widths[line.start + j] = w.max(s.min_width);
6866 }
6867 }
6868 }
6869 }
6870
6871 let mut total = 0.0;
6872 for (i, line) in lines.iter().enumerate() {
6873 let line_height: f64 = children[line.start..line.end]
6874 .iter()
6875 .enumerate()
6876 .map(|(j, child)| {
6877 let fw = final_widths[line.start + j];
6878 let child_style =
6887 child.style.resolve(Some(parent_style), available_width);
6888 self.measure_node_height(child, fw, &child_style, font_context)
6889 + child_style.margin.vertical()
6890 })
6891 .fold(0.0f64, f64::max);
6892 total += line_height;
6893 if i > 0 {
6894 total += row_gap;
6895 }
6896 }
6897 total
6898 }
6899 FlexDirection::Column | FlexDirection::ColumnReverse => {
6900 let mut total = 0.0;
6901 for (i, child) in children.iter().enumerate() {
6902 let child_style = child.style.resolve(Some(parent_style), available_width);
6903 let child_height = self.measure_node_height(
6904 child,
6905 available_width,
6906 &child_style,
6907 font_context,
6908 );
6909 total += child_height + child_style.margin.vertical();
6910 if i > 0 {
6911 total += row_gap;
6912 }
6913 }
6914 total
6915 }
6916 }
6917 }
6918
6919 fn measure_intrinsic_width(
6921 &self,
6922 node: &Node,
6923 style: &ResolvedStyle,
6924 font_context: &FontContext,
6925 ) -> f64 {
6926 match &node.kind {
6927 NodeKind::Svg { width, .. } => {
6928 *width + style.padding.horizontal() + style.margin.horizontal()
6929 }
6930 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6931 let text_width = if !runs.is_empty() {
6937 runs.iter()
6938 .map(|run| {
6939 let run_style = run.style.resolve(Some(style), 0.0);
6940 let run_content = substitute_page_placeholders(&run.content);
6941 let transformed =
6942 apply_text_transform(&run_content, run_style.text_transform);
6943 let italic = matches!(
6944 run_style.font_style,
6945 FontStyle::Italic | FontStyle::Oblique
6946 );
6947 transformed
6951 .split('\n')
6952 .map(|segment| {
6953 font_context.measure_string(
6954 segment,
6955 &run_style.font_family,
6956 run_style.font_weight,
6957 italic,
6958 run_style.font_size,
6959 run_style.letter_spacing,
6960 )
6961 })
6962 .fold(0.0f64, f64::max)
6963 })
6964 .sum()
6965 } else {
6966 let content = substitute_page_placeholders(content);
6967 let transformed = apply_text_transform(&content, style.text_transform);
6968 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6969 transformed
6970 .split('\n')
6971 .map(|segment| {
6972 font_context.measure_string(
6973 segment,
6974 &style.font_family,
6975 style.font_weight,
6976 italic,
6977 style.font_size,
6978 style.letter_spacing,
6979 )
6980 })
6981 .fold(0.0f64, f64::max)
6982 };
6983 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6986 }
6987 NodeKind::Image {
6988 src, width, height, ..
6989 } => {
6990 let w = if let SizeConstraint::Fixed(w) = style.width {
6991 w
6992 } else if let Some(w) = width {
6993 *w
6994 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6995 let pixel_w = iw as f64;
6996 let pixel_h = ih as f64;
6997 let aspect = if pixel_w > 0.0 {
6998 pixel_h / pixel_w
6999 } else {
7000 0.75
7001 };
7002 let constrained_h = match style.height {
7004 SizeConstraint::Fixed(h) => Some(h),
7005 SizeConstraint::Auto => *height,
7006 };
7007 if let Some(h) = constrained_h {
7008 h / aspect
7009 } else {
7010 pixel_w
7011 }
7012 } else {
7013 100.0
7014 };
7015 w + style.padding.horizontal() + style.margin.horizontal()
7016 }
7017 NodeKind::Barcode { width, .. } => {
7018 let w = width.unwrap_or(0.0);
7019 w + style.padding.horizontal() + style.margin.horizontal()
7020 }
7021 NodeKind::QrCode { size, .. } => {
7022 let display_size = size.unwrap_or(0.0);
7023 display_size + style.padding.horizontal() + style.margin.horizontal()
7024 }
7025 NodeKind::Canvas { width, .. } => {
7026 *width + style.padding.horizontal() + style.margin.horizontal()
7027 }
7028 NodeKind::BarChart { width, .. }
7029 | NodeKind::LineChart { width, .. }
7030 | NodeKind::PieChart { width, .. }
7031 | NodeKind::AreaChart { width, .. }
7032 | NodeKind::DotPlot { width, .. } => {
7033 *width + style.padding.horizontal() + style.margin.horizontal()
7034 }
7035 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
7036 *width + style.padding.horizontal() + style.margin.horizontal()
7037 }
7038 NodeKind::Table { columns } => {
7039 let num_cols = node
7044 .children
7045 .iter()
7046 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7047 .max()
7048 .unwrap_or(1)
7049 .max(columns.len().max(1));
7050 let (_, col_max) =
7051 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7052 col_max.iter().sum::<f64>()
7053 + style.padding.horizontal()
7054 + style.margin.horizontal()
7055 + style.border_width.horizontal()
7056 }
7057 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
7058 *width + style.padding.horizontal() + style.margin.horizontal()
7059 }
7060 NodeKind::Watermark { .. } => 0.0, _ => {
7062 if let SizeConstraint::Fixed(w) = style.width {
7070 return w
7071 + style.padding.horizontal()
7072 + style.border_width.horizontal()
7073 + style.margin.horizontal();
7074 }
7075 if node.children.is_empty() {
7077 style.padding.horizontal() + style.margin.horizontal()
7078 } else {
7079 let direction = style.flex_direction;
7080 let gap = style.column_gap;
7092 let mut total = 0.0f64;
7093 for (i, child) in node.children.iter().enumerate() {
7094 let child_style = child.style.resolve(Some(style), 0.0);
7095 let child_width =
7096 self.measure_intrinsic_width(child, &child_style, font_context);
7097 match direction {
7098 FlexDirection::Row | FlexDirection::RowReverse => {
7099 total += child_width;
7100 if i > 0 {
7101 total += gap;
7102 }
7103 }
7104 _ => {
7105 total = total.max(child_width);
7106 }
7107 }
7108 }
7109 total
7110 + style.padding.horizontal()
7111 + style.margin.horizontal()
7112 + style.border_width.horizontal()
7113 }
7114 }
7115 }
7116 }
7117
7118 pub fn measure_min_content_width(
7122 &self,
7123 node: &Node,
7124 style: &ResolvedStyle,
7125 font_context: &FontContext,
7126 ) -> f64 {
7127 match &node.kind {
7128 NodeKind::Table { columns } => {
7129 let num_cols = node
7132 .children
7133 .iter()
7134 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7135 .max()
7136 .unwrap_or(1)
7137 .max(columns.len().max(1));
7138 let (col_min, _) =
7139 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7140 col_min.iter().sum::<f64>()
7141 + style.padding.horizontal()
7142 + style.margin.horizontal()
7143 + style.border_width.horizontal()
7144 }
7145 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7146 let word_width = if !runs.is_empty() {
7147 runs.iter()
7149 .map(|run| {
7150 let run_style = run.style.resolve(Some(style), 0.0);
7151 let run_content = substitute_page_placeholders(&run.content);
7152 let transformed =
7153 apply_text_transform(&run_content, run_style.text_transform);
7154 self.text_layout.measure_widest_word(
7155 font_context,
7156 &transformed,
7157 run_style.font_size,
7158 &run_style.font_family,
7159 run_style.font_weight,
7160 run_style.font_style,
7161 run_style.letter_spacing,
7162 run_style.word_spacing,
7163 style.hyphens,
7164 style.lang.as_deref(),
7165 )
7166 })
7167 .fold(0.0f64, f64::max)
7168 } else {
7169 let content = substitute_page_placeholders(content);
7170 let transformed = apply_text_transform(&content, style.text_transform);
7171 self.text_layout.measure_widest_word(
7172 font_context,
7173 &transformed,
7174 style.font_size,
7175 &style.font_family,
7176 style.font_weight,
7177 style.font_style,
7178 style.letter_spacing,
7179 style.word_spacing,
7180 style.hyphens,
7181 style.lang.as_deref(),
7182 )
7183 };
7184 word_width + style.padding.horizontal() + style.margin.horizontal()
7185 }
7186 NodeKind::Image { width, .. } => {
7187 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
7188 }
7189 NodeKind::Svg { width, .. } => {
7190 *width + style.padding.horizontal() + style.margin.horizontal()
7191 }
7192 _ => {
7193 if node.children.is_empty() {
7194 style.padding.horizontal()
7195 + style.margin.horizontal()
7196 + style.border_width.horizontal()
7197 } else {
7198 let mut max_child_min = 0.0f64;
7199 for child in &node.children {
7200 let child_style = child.style.resolve(Some(style), 0.0);
7201 let child_min =
7202 self.measure_min_content_width(child, &child_style, font_context);
7203 max_child_min = max_child_min.max(child_min);
7204 }
7205 max_child_min
7206 + style.padding.horizontal()
7207 + style.margin.horizontal()
7208 + style.border_width.horizontal()
7209 }
7210 }
7211 }
7212 }
7213
7214 fn flex_item_baseline_distance(
7221 &self,
7222 item: &Node,
7223 style: &ResolvedStyle,
7224 w: f64,
7225 font_context: &FontContext,
7226 ) -> f64 {
7227 let first_line = match &item.kind {
7228 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
7229 let metrics = font_context.baseline_metrics(
7230 &style.font_family,
7231 style.font_weight,
7232 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
7233 );
7234 baseline_in_line(
7235 style.font_size * style.line_height,
7236 style.font_size,
7237 metrics,
7238 )
7239 }
7240 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
7241 };
7242 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
7243 }
7244
7245 fn cell_first_baseline_in_line(
7252 &self,
7253 cell: &Node,
7254 cell_style: &ResolvedStyle,
7255 w: f64,
7256 font_context: &FontContext,
7257 ) -> f64 {
7258 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
7259 for ch in &node.children {
7260 let s = ch.style.resolve(Some(parent), w);
7261 match &ch.kind {
7262 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
7263 _ => {
7264 if let Some(f) = first(ch, &s, w) {
7265 return Some(f);
7266 }
7267 }
7268 }
7269 }
7270 None
7271 }
7272 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
7273 let metrics = font_context.baseline_metrics(
7274 &s.font_family,
7275 s.font_weight,
7276 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
7277 );
7278 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
7279 }
7280
7281 fn cell_baseline_distance(
7286 &self,
7287 cell: &Node,
7288 cell_style: &ResolvedStyle,
7289 inner_width: f64,
7290 font_context: &FontContext,
7291 ) -> f64 {
7292 cell_style.padding.top
7293 + cell_style.border_width.top
7294 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
7295 }
7296
7297 fn row_baseline(
7300 &self,
7301 row: &Node,
7302 row_style: &ResolvedStyle,
7303 col_widths: &[f64],
7304 col_offsets: &[usize],
7305 font_context: &FontContext,
7306 ) -> Option<f64> {
7307 let mut b: Option<f64> = None;
7308 for (cell_i, cell) in row.children.iter().enumerate() {
7309 let span = match &cell.kind {
7310 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7311 _ => 1,
7312 };
7313 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7314 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7315 let cell_style = cell.style.resolve(Some(row_style), col_width);
7316 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7317 let iw = col_width
7318 - cell_style.padding.horizontal()
7319 - cell_style.border_width.horizontal();
7320 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
7321 b = Some(b.map_or(d, |m: f64| m.max(d)));
7322 }
7323 }
7324 b
7325 }
7326
7327 fn measure_table_row_height(
7328 &self,
7329 row: &Node,
7330 col_widths: &[f64],
7331 col_offsets: &[usize],
7332 parent_style: &ResolvedStyle,
7333 font_context: &FontContext,
7334 ) -> f64 {
7335 let row_style = row
7336 .style
7337 .resolve(Some(parent_style), col_widths.iter().sum());
7338 let mut max_height: f64 = 0.0;
7339 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
7342
7343 for (cell_i, cell) in row.children.iter().enumerate() {
7344 let span = match &cell.kind {
7345 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7346 _ => 1,
7347 };
7348 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7349 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7350 let cell_style = cell.style.resolve(Some(&row_style), col_width);
7351 let inner_width =
7352 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
7353
7354 let mut cell_content_height = 0.0;
7355 for child in &cell.children {
7356 let child_style = child.style.resolve(Some(&cell_style), inner_width);
7357 cell_content_height +=
7358 self.measure_node_height(child, inner_width, &child_style, font_context);
7359 }
7360
7361 let mut total = cell_content_height
7362 + cell_style.padding.vertical()
7363 + cell_style.border_width.vertical();
7364 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7368 if let Some(b) = row_bl {
7369 let d =
7370 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
7371 total += (b - d).max(0.0);
7372 }
7373 }
7374 if let SizeConstraint::Fixed(h) = cell_style.height {
7381 total = total.max(h);
7382 }
7383 max_height = max_height.max(total);
7384 }
7385
7386 max_height.max(row_style.min_height)
7387 }
7388
7389 fn cell_col_span(cell: &Node) -> usize {
7391 match &cell.kind {
7392 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7393 _ => 1,
7394 }
7395 }
7396
7397 fn measure_column_content(
7402 &self,
7403 children: &[Node],
7404 num_cols: usize,
7405 available_width: f64,
7406 table_style: &ResolvedStyle,
7407 font_context: &FontContext,
7408 ) -> (Vec<f64>, Vec<f64>) {
7409 let mut col_min = vec![0.0f64; num_cols];
7410 let mut col_max = vec![0.0f64; num_cols];
7411 let offsets = Self::table_column_offsets(children);
7412 for (row_i, row_node) in children.iter().enumerate() {
7413 for (cell_i, cell) in row_node.children.iter().enumerate() {
7414 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7415 let span = Self::cell_col_span(cell);
7416 let cell_style = cell.style.resolve(Some(table_style), available_width);
7417 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7418 let mut cmin = 0.0f64;
7419 let mut cmax = 0.0f64;
7420 for child in &cell.children {
7421 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7422 cmin =
7423 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7424 cmax =
7425 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7426 }
7427 cmin += chrome;
7428 let mut cmax = cmax.max(cmin) + chrome;
7429 if let SizeConstraint::Fixed(w) = cell_style.width {
7430 cmax = w.max(cmin);
7431 }
7432 let per_min = cmin / span as f64;
7433 let per_max = cmax / span as f64;
7434 for k in col..(col + span).min(num_cols) {
7435 col_min[k] = col_min[k].max(per_min);
7436 col_max[k] = col_max[k].max(per_max);
7437 }
7438 }
7439 }
7440 (col_min, col_max)
7441 }
7442
7443 fn resolve_column_widths(
7453 &self,
7454 defs: &[ColumnDef],
7455 available_width: f64,
7456 children: &[Node],
7457 table_style: &ResolvedStyle,
7458 font_context: &FontContext,
7459 ) -> Vec<f64> {
7460 if defs.is_empty() {
7461 let num_cols = Self::occupancy_column_count(children);
7462
7463 let (col_min, col_max) = self.measure_column_content(
7464 children,
7465 num_cols,
7466 available_width,
7467 table_style,
7468 font_context,
7469 );
7470
7471 let sum_min: f64 = col_min.iter().sum();
7472 let sum_max: f64 = col_max.iter().sum();
7473 let w = available_width;
7474 return if sum_max <= w {
7475 if sum_max <= f64::EPSILON {
7479 vec![w / num_cols as f64; num_cols]
7480 } else {
7481 col_max
7482 .iter()
7483 .map(|m| m + (w - sum_max) * (m / sum_max))
7484 .collect()
7485 }
7486 } else if sum_min <= w {
7487 let denom = (sum_max - sum_min).max(f64::EPSILON);
7490 col_min
7491 .iter()
7492 .zip(&col_max)
7493 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7494 .collect()
7495 } else {
7496 self.defect(format!(
7499 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7500 sum_min, w
7501 ));
7502 let scale = w / sum_min.max(f64::EPSILON);
7503 col_min.iter().map(|m| m * scale).collect()
7504 };
7505 }
7506
7507 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7513 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7514 while defs_vec.len() < num_cols {
7515 defs_vec.push(ColumnDef {
7516 width: ColumnWidth::Auto,
7517 });
7518 }
7519 let defs = &defs_vec[..];
7520
7521 let mut widths = Vec::new();
7522 let mut remaining = available_width;
7523 let mut auto_count = 0;
7524
7525 for def in defs {
7526 match def.width {
7527 ColumnWidth::Fixed(w) => {
7528 widths.push(w);
7529 remaining -= w;
7530 }
7531 ColumnWidth::Fraction(f) => {
7532 let w = available_width * f;
7533 widths.push(w);
7534 remaining -= w;
7535 }
7536 ColumnWidth::Auto => {
7537 widths.push(0.0);
7538 auto_count += 1;
7539 }
7540 }
7541 }
7542
7543 const OVERFULL_EPS: f64 = 0.01;
7552 if remaining < -OVERFULL_EPS {
7553 self.defect(format!(
7556 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7557 available_width - remaining,
7558 available_width
7559 ));
7560 remaining = 0.0;
7561 }
7562 remaining = remaining.max(0.0);
7565
7566 if auto_count > 0 {
7567 let auto_width = remaining / auto_count as f64;
7568 for (i, def) in defs.iter().enumerate() {
7569 if matches!(def.width, ColumnWidth::Auto) {
7570 widths[i] = auto_width;
7571 }
7572 }
7573 }
7574
7575 let (col_min, _) = self.measure_column_content(
7582 children,
7583 widths.len(),
7584 available_width,
7585 table_style,
7586 font_context,
7587 );
7588 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7589 if needs_floor {
7590 let sum_min: f64 = col_min.iter().sum();
7591 if sum_min > available_width {
7592 self.defect(format!(
7593 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7594 sum_min, available_width
7595 ));
7596 let scale = available_width / sum_min.max(f64::EPSILON);
7597 return col_min.iter().map(|m| m * scale).collect();
7598 }
7599 let deficit: f64 = widths
7600 .iter()
7601 .zip(&col_min)
7602 .map(|(w, m)| (m - w).max(0.0))
7603 .sum();
7604 let surplus: f64 = widths
7605 .iter()
7606 .zip(&col_min)
7607 .map(|(w, m)| (w - m).max(0.0))
7608 .sum();
7609 let take = if surplus > 0.0 {
7610 deficit / surplus
7611 } else {
7612 0.0
7613 };
7614 widths = widths
7615 .iter()
7616 .zip(&col_min)
7617 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7618 .collect();
7619 }
7620
7621 widths
7622 }
7623
7624 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7625 for (page_index, page) in pages.iter_mut().enumerate() {
7626 if !page.watermarks.is_empty() {
7628 let (page_w, page_h) = page.config.size.dimensions();
7629 let cx = page_w / 2.0;
7630 let cy = page_h / 2.0;
7631
7632 let mut watermark_elements = Vec::new();
7633 for wm_node in &page.watermarks {
7634 if let NodeKind::Watermark {
7635 text,
7636 font_size,
7637 angle,
7638 } = &wm_node.kind
7639 {
7640 let style = wm_node.style.resolve(None, page_w);
7641 let color = style.color;
7642 let opacity = style.opacity;
7643 let angle_rad = angle.to_radians();
7644
7645 let italic =
7647 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7648
7649 let shaped = self.text_layout.shape_text(
7651 font_context,
7652 text,
7653 &style.font_family,
7654 style.font_weight,
7655 style.font_style,
7656 );
7657
7658 let mut glyphs = Vec::new();
7659 let mut x_pos = 0.0;
7660 let text_chars: Vec<char> = text.chars().collect();
7661
7662 if let Some(shaped_glyphs) = shaped {
7663 let units_per_em = font_context.units_per_em(
7665 &style.font_family,
7666 style.font_weight,
7667 italic,
7668 ) as f64;
7669
7670 for sg in &shaped_glyphs {
7671 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7672 let cluster_idx = sg.cluster as usize;
7673 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7674 glyphs.push(PositionedGlyph {
7675 glyph_id: sg.glyph_id,
7676 char_value: ch,
7677 x_offset: x_pos,
7678 y_offset: 0.0,
7679 x_advance: advance,
7680 font_size: *font_size,
7681 font_family: Arc::from(style.font_family.as_str()),
7682 font_weight: style.font_weight,
7683 font_style: style.font_style,
7684 color: Some(color),
7685 href: None,
7686 text_decoration: TextDecoration::None,
7687 letter_spacing: style.letter_spacing,
7688 cluster_text: None,
7689 });
7690 x_pos += advance + style.letter_spacing;
7691 }
7692 } else {
7693 for &ch in &text_chars {
7695 let w = font_context.char_width(
7696 ch,
7697 &style.font_family,
7698 style.font_weight,
7699 italic,
7700 *font_size,
7701 );
7702 glyphs.push(PositionedGlyph {
7703 glyph_id: ch as u16,
7704 char_value: ch,
7705 x_offset: x_pos,
7706 y_offset: 0.0,
7707 x_advance: w,
7708 font_size: *font_size,
7709 font_family: Arc::from(style.font_family.as_str()),
7710 font_weight: style.font_weight,
7711 font_style: style.font_style,
7712 color: Some(color),
7713 href: None,
7714 text_decoration: TextDecoration::None,
7715 letter_spacing: style.letter_spacing,
7716 cluster_text: None,
7717 });
7718 x_pos += w + style.letter_spacing;
7719 }
7720 }
7721
7722 let text_width = x_pos;
7723
7724 let line = TextLine {
7725 x: 0.0,
7726 y: 0.0,
7727 glyphs,
7728 width: text_width,
7729 height: *font_size,
7730 word_spacing: 0.0,
7731 };
7732
7733 watermark_elements.push(LayoutElement {
7734 x: cx,
7735 y: cy,
7736 width: text_width,
7737 height: *font_size,
7738 draw: DrawCommand::Watermark {
7739 lines: vec![line],
7740 color,
7741 opacity,
7742 angle_rad,
7743 font_family: style.font_family.clone(),
7744 },
7745 children: vec![],
7746 node_type: Some("Watermark".to_string()),
7747 resolved_style: None,
7748 source_location: None,
7749 href: None,
7750 bookmark: None,
7751 alt: None,
7752 is_header_row: false,
7753 actual_text: None,
7754 list_numbering: None,
7755 col_span: 1,
7756 overflow: Overflow::default(),
7757 opacity: 1.0,
7758 });
7759 }
7760 }
7761
7762 watermark_elements.append(&mut page.elements);
7764 page.elements = watermark_elements;
7765 page.watermarks.clear();
7766 }
7767
7768 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7769 continue;
7770 }
7771
7772 if !page.fixed_header.is_empty() {
7774 let mut hdr_cursor = PageCursor::new(&page.config);
7775 for (node, _h) in &page.fixed_header {
7776 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7779 continue;
7780 }
7781 let cw = hdr_cursor.content_width;
7782 let cx = hdr_cursor.content_x;
7783 let style = node.style.resolve(None, cw);
7784 self.layout_view(
7785 node,
7786 &style,
7787 &mut hdr_cursor,
7788 &mut Vec::new(),
7789 cx,
7790 cw,
7791 font_context,
7792 );
7793 }
7794 let mut combined = hdr_cursor.elements;
7796 combined.append(&mut page.elements);
7797 page.elements = combined;
7798 }
7799
7800 if !page.fixed_footer.is_empty() {
7805 let mut ftr_cursor = PageCursor::new(&page.config);
7806 let total_ftr: f64 = page
7807 .fixed_footer
7808 .iter()
7809 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7810 .map(|(_, h)| *h)
7811 .sum();
7812 let target_y = ftr_cursor.content_height - total_ftr;
7813 for (node, _h) in &page.fixed_footer {
7815 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7816 continue;
7817 }
7818 let cw = ftr_cursor.content_width;
7819 let cx = ftr_cursor.content_x;
7820 let style = node.style.resolve(None, cw);
7821 self.layout_view(
7822 node,
7823 &style,
7824 &mut ftr_cursor,
7825 &mut Vec::new(),
7826 cx,
7827 cw,
7828 font_context,
7829 );
7830 }
7831 for el in &mut ftr_cursor.elements {
7835 offset_element_y(el, target_y);
7836 }
7837 page.elements.extend(ftr_cursor.elements);
7838 }
7839
7840 page.fixed_header.clear();
7842 page.fixed_footer.clear();
7843 }
7844 }
7845
7846 #[allow(clippy::too_many_arguments)]
7851 fn layout_grid_children(
7852 &self,
7853 children: &[Node],
7854 parent_style: &ResolvedStyle,
7855 cursor: &mut PageCursor,
7856 pages: &mut Vec<LayoutPage>,
7857 x: f64,
7858 available_width: f64,
7859 font_context: &FontContext,
7860 ) {
7861 let template_cols = match &parent_style.grid_template_columns {
7862 Some(cols) => cols,
7863 None => return, };
7865
7866 let num_columns = template_cols.len();
7867 if num_columns == 0 || children.is_empty() {
7868 return;
7869 }
7870
7871 let col_gap = parent_style.column_gap;
7872 let row_gap = parent_style.row_gap;
7873
7874 let content_sizes: Vec<f64> = template_cols
7877 .iter()
7878 .map(|track| {
7879 if matches!(track, GridTrackSize::Auto) {
7880 available_width / num_columns as f64
7883 } else {
7884 0.0
7885 }
7886 })
7887 .collect();
7888
7889 let col_widths =
7890 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7891
7892 let placements: Vec<Option<&GridPlacement>> = children
7894 .iter()
7895 .map(|child| child.style.grid_placement.as_ref())
7896 .collect();
7897
7898 let item_placements = grid::place_items(&placements, num_columns);
7900 let num_rows = grid::compute_num_rows(&item_placements);
7901
7902 if num_rows == 0 {
7903 return;
7904 }
7905
7906 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7908 for placement in &item_placements {
7909 let cell_width =
7910 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7911 let child = &children[placement.child_index];
7912 let child_style = child.style.resolve(Some(parent_style), cell_width);
7913 item_heights[placement.child_index] =
7914 self.measure_node_height(child, cell_width, &child_style, font_context);
7915 }
7916
7917 let template_rows = parent_style.grid_template_rows.as_deref();
7919 let mut row_heights = vec![0.0_f64; num_rows];
7920 for placement in &item_placements {
7921 let h = item_heights[placement.child_index];
7922 let span = placement.row_end - placement.row_start;
7923 let per_row = h / span as f64;
7924 for rh in row_heights
7925 .iter_mut()
7926 .take(placement.row_end.min(num_rows))
7927 .skip(placement.row_start)
7928 {
7929 if per_row > *rh {
7930 *rh = per_row;
7931 }
7932 }
7933 }
7934
7935 if let Some(template) = template_rows {
7937 let auto_row = parent_style.grid_auto_rows.as_ref();
7938 for (r, rh) in row_heights.iter_mut().enumerate() {
7939 let track = template.get(r).or(auto_row);
7940 if let Some(track) = track {
7941 match track {
7942 GridTrackSize::Pt(pts) => *rh = *pts,
7943 GridTrackSize::Auto => {} _ => {} }
7946 }
7947 }
7948 }
7949
7950 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7952 if row_height > cursor.remaining_height() {
7957 pages.push(cursor.finalize());
7958 *cursor = cursor.new_page();
7959 }
7960
7961 let row_start_y = cursor.y;
7962
7963 for placement in &item_placements {
7965 if placement.row_start != row {
7966 continue; }
7968
7969 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7970 let cell_width =
7971 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7972
7973 let child = &children[placement.child_index];
7974
7975 self.layout_node(
7976 child,
7977 cursor,
7978 pages,
7979 cell_x,
7980 cell_width,
7981 Some(parent_style),
7982 font_context,
7983 None,
7984 None,
7985 );
7986 cursor.y = row_start_y;
7988 }
7989
7990 cursor.y = row_start_y + row_height + row_gap;
7991 }
7992
7993 if num_rows > 0 {
7995 cursor.y -= row_gap;
7996 }
7997 }
7998}
7999
8000fn end_y_of(frags: &[ItemFragments], last: usize) -> f64 {
8004 frags
8005 .iter()
8006 .filter(|f| f.pages.len() == last)
8007 .map(|f| f.final_y)
8008 .fold(f64::MIN, f64::max)
8009}
8010
8011fn paints_a_box(el: &LayoutElement) -> bool {
8019 match &el.draw {
8020 DrawCommand::Rect {
8021 background,
8022 border_width,
8023 ..
8024 } => {
8025 background.is_some()
8026 || border_width.top > 0.0
8027 || border_width.right > 0.0
8028 || border_width.bottom > 0.0
8029 || border_width.left > 0.0
8030 }
8031 _ => false,
8032 }
8033}
8034
8035fn drain_since(elements: &mut Vec<LayoutElement>, snapshot: usize) -> Vec<LayoutElement> {
8052 let start = snapshot.min(elements.len());
8053 elements.drain(start..).collect()
8054}
8055
8056fn stretch_fragment(elements: &mut [LayoutElement], bottom: f64) {
8057 for el in elements.iter_mut() {
8058 let grown = bottom - el.y;
8059 if grown > el.height {
8060 el.height = grown;
8061 }
8062 }
8063}
8064
8065struct ItemFragments {
8071 pages: Vec<LayoutPage>,
8072 tail: Vec<LayoutElement>,
8073 cursor: PageCursor,
8074 final_y: f64,
8075 stretches: bool,
8080}
8081
8082struct FlexItem<'a> {
8083 node: &'a Node,
8084 style: ResolvedStyle,
8085 base_width: f64,
8086 min_content_width: f64,
8087}
8088
8089fn first_text_snippet(node: &Node) -> Option<String> {
8092 fn walk(n: &Node) -> Option<&str> {
8093 match &n.kind {
8094 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
8095 if !content.trim().is_empty() {
8096 return Some(content.trim());
8097 }
8098 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
8099 return Some(r.content.trim());
8100 }
8101 None
8102 }
8103 _ => n.children.iter().find_map(walk),
8104 }
8105 }
8106 walk(node).map(|t| {
8107 let mut s: String = t.chars().take(32).collect();
8108 if t.chars().count() > 32 {
8109 s.push('…');
8110 }
8111 s
8112 })
8113}
8114
8115fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
8125 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
8126}
8127
8128#[cfg(test)]
8129mod tests {
8130 use super::*;
8131 use crate::font::FontContext;
8132
8133 fn make_text(content: &str, font_size: f64) -> Node {
8134 Node {
8135 kind: NodeKind::Text {
8136 content: content.to_string(),
8137 href: None,
8138 runs: vec![],
8139 },
8140 style: Style {
8141 font_size: Some(font_size),
8142 ..Default::default()
8143 },
8144 children: vec![],
8145 id: None,
8146 source_location: None,
8147 bookmark: None,
8148 href: None,
8149 alt: None,
8150 }
8151 }
8152
8153 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
8154 Node {
8155 kind: NodeKind::View,
8156 style,
8157 children,
8158 id: None,
8159 source_location: None,
8160 bookmark: None,
8161 href: None,
8162 alt: None,
8163 }
8164 }
8165
8166 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
8167 Node {
8168 kind: NodeKind::Text {
8169 content: String::new(),
8170 href: None,
8171 runs,
8172 },
8173 style: Style::default(),
8174 children: vec![],
8175 id: None,
8176 source_location: None,
8177 bookmark: None,
8178 href: None,
8179 alt: None,
8180 }
8181 }
8182
8183 #[test]
8184 fn intrinsic_width_measures_runs_not_just_content() {
8185 let engine = LayoutEngine::new();
8189 let font_context = FontContext::new();
8190
8191 let runs_node = make_runs_text(vec![
8192 crate::model::TextRun {
8193 content: "Hello ".to_string(),
8194 style: Style::default(),
8195 href: None,
8196 },
8197 crate::model::TextRun {
8198 content: "World".to_string(),
8199 style: Style {
8200 font_weight: Some(700),
8201 ..Default::default()
8202 },
8203 href: None,
8204 },
8205 ]);
8206 let plain_node = make_text("Hello World", 12.0);
8207
8208 let runs_style = runs_node.style.resolve(None, 0.0);
8209 let plain_style = plain_node.style.resolve(None, 0.0);
8210 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
8211 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
8212
8213 assert!(
8216 runs_w >= plain_w,
8217 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
8218 );
8219 assert!(
8220 runs_w < plain_w * 1.5,
8221 "runs width ({runs_w}) should be close to plain width ({plain_w})"
8222 );
8223 }
8224
8225 #[test]
8226 fn intrinsic_width_of_multiline_text_is_widest_line() {
8227 let engine = LayoutEngine::new();
8228 let font_context = FontContext::new();
8229
8230 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
8231 let widest = make_text("Springfield, IL 62704", 12.0);
8232
8233 let m_style = multiline.style.resolve(None, 0.0);
8234 let w_style = widest.style.resolve(None, 0.0);
8235 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
8236 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
8237
8238 assert!(
8239 (m_w - w_w).abs() < 0.01,
8240 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
8241 );
8242 }
8243
8244 #[test]
8245 fn intrinsic_width_of_heading_measures_its_text() {
8246 let engine = LayoutEngine::new();
8249 let font_context = FontContext::new();
8250
8251 let heading = Node {
8252 kind: NodeKind::Heading {
8253 level: 1,
8254 content: "Invoice #2024-001".to_string(),
8255 href: None,
8256 runs: vec![],
8257 },
8258 style: Style {
8259 font_size: Some(24.0),
8260 ..Default::default()
8261 },
8262 children: vec![],
8263 id: None,
8264 source_location: None,
8265 bookmark: None,
8266 href: None,
8267 alt: None,
8268 };
8269 let style = heading.style.resolve(None, 0.0);
8270 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
8271 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
8272 }
8273
8274 #[test]
8275 fn measure_node_height_of_wrapping_heading_matches_text() {
8276 let engine = LayoutEngine::new();
8283 let font_context = FontContext::new();
8284
8285 let content = "Annual Performance Review";
8286 let heading = Node {
8287 kind: NodeKind::Heading {
8288 level: 1,
8289 content: content.to_string(),
8290 href: None,
8291 runs: vec![],
8292 },
8293 style: Style {
8294 font_size: Some(32.0),
8295 ..Default::default()
8296 },
8297 children: vec![],
8298 id: None,
8299 source_location: None,
8300 bookmark: None,
8301 href: None,
8302 alt: None,
8303 };
8304 let text = make_text(content, 32.0);
8305
8306 let width = 200.0;
8308 let h_style = heading.style.resolve(None, width);
8309 let t_style = text.style.resolve(None, width);
8310 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
8311 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
8312
8313 assert!(
8314 h_height > 32.0,
8315 "a wrapping 32pt heading must measure more than one line, got {h_height}"
8316 );
8317 assert!(
8318 (h_height - t_height).abs() < 0.01,
8319 "heading height ({h_height}) must equal the same text's height ({t_height})"
8320 );
8321 }
8322
8323 #[test]
8324 fn intrinsic_width_flex_row_sums_children() {
8325 let engine = LayoutEngine::new();
8326 let font_context = FontContext::new();
8327
8328 let child1 = make_text("Hello", 14.0);
8329 let child2 = make_text("World", 14.0);
8330
8331 let child1_style = child1.style.resolve(None, 0.0);
8332 let child2_style = child2.style.resolve(None, 0.0);
8333 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
8334 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
8335
8336 let row = make_styled_view(
8337 Style {
8338 flex_direction: Some(FlexDirection::Row),
8339 ..Default::default()
8340 },
8341 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
8342 );
8343 let row_style = row.style.resolve(None, 0.0);
8344 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
8345
8346 assert!(
8347 (row_w - (child1_w + child2_w)).abs() < 0.01,
8348 "Row intrinsic width ({}) should equal sum of children ({} + {})",
8349 row_w,
8350 child1_w,
8351 child2_w
8352 );
8353 }
8354
8355 #[test]
8356 fn intrinsic_width_flex_column_takes_max() {
8357 let engine = LayoutEngine::new();
8358 let font_context = FontContext::new();
8359
8360 let short = make_text("Hi", 14.0);
8361 let long = make_text("Hello World", 14.0);
8362
8363 let short_style = short.style.resolve(None, 0.0);
8364 let long_style = long.style.resolve(None, 0.0);
8365 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
8366 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
8367
8368 let col = make_styled_view(
8369 Style {
8370 flex_direction: Some(FlexDirection::Column),
8371 ..Default::default()
8372 },
8373 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
8374 );
8375 let col_style = col.style.resolve(None, 0.0);
8376 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
8377
8378 assert!(
8379 (col_w - long_w).abs() < 0.01,
8380 "Column intrinsic width ({}) should equal max child ({}, short was {})",
8381 col_w,
8382 long_w,
8383 short_w
8384 );
8385 }
8386
8387 #[test]
8388 fn intrinsic_width_nested_containers() {
8389 let engine = LayoutEngine::new();
8390 let font_context = FontContext::new();
8391
8392 let inner = make_styled_view(
8393 Style {
8394 flex_direction: Some(FlexDirection::Row),
8395 ..Default::default()
8396 },
8397 vec![make_text("A", 12.0), make_text("B", 12.0)],
8398 );
8399 let inner_style = inner.style.resolve(None, 0.0);
8400 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
8401
8402 let outer = make_styled_view(
8403 Style::default(),
8404 vec![make_styled_view(
8405 Style {
8406 flex_direction: Some(FlexDirection::Row),
8407 ..Default::default()
8408 },
8409 vec![make_text("A", 12.0), make_text("B", 12.0)],
8410 )],
8411 );
8412 let outer_style = outer.style.resolve(None, 0.0);
8413 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
8414
8415 assert!(
8416 (outer_w - inner_w).abs() < 0.01,
8417 "Nested container ({}) should match inner container ({})",
8418 outer_w,
8419 inner_w
8420 );
8421 }
8422
8423 #[test]
8424 fn intrinsic_width_row_with_gap() {
8425 let engine = LayoutEngine::new();
8426 let font_context = FontContext::new();
8427
8428 let no_gap = make_styled_view(
8429 Style {
8430 flex_direction: Some(FlexDirection::Row),
8431 ..Default::default()
8432 },
8433 vec![make_text("A", 12.0), make_text("B", 12.0)],
8434 );
8435 let with_gap = make_styled_view(
8436 Style {
8437 flex_direction: Some(FlexDirection::Row),
8438 gap: Some(10.0),
8439 ..Default::default()
8440 },
8441 vec![make_text("A", 12.0), make_text("B", 12.0)],
8442 );
8443
8444 let no_gap_style = no_gap.style.resolve(None, 0.0);
8445 let with_gap_style = with_gap.style.resolve(None, 0.0);
8446 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
8447 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
8448
8449 assert!(
8450 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
8451 "Gap should add 10pt: with_gap={}, no_gap={}",
8452 with_gap_w,
8453 no_gap_w
8454 );
8455 }
8456
8457 #[test]
8458 fn intrinsic_width_empty_container() {
8459 let engine = LayoutEngine::new();
8460 let font_context = FontContext::new();
8461
8462 let padding = 8.0;
8463 let empty = make_styled_view(
8464 Style {
8465 padding: Some(Edges::uniform(padding)),
8466 ..Default::default()
8467 },
8468 vec![],
8469 );
8470 let style = empty.style.resolve(None, 0.0);
8471 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
8472
8473 assert!(
8474 (w - padding * 2.0).abs() < 0.01,
8475 "Empty container width ({}) should equal horizontal padding ({})",
8476 w,
8477 padding * 2.0
8478 );
8479 }
8480
8481 #[test]
8484 fn flex_shrink_respects_min_content_width() {
8485 let engine = LayoutEngine::new();
8489 let font_context = FontContext::new();
8490
8491 let sale_text = make_text("SALE", 12.0);
8492 let sale_style = sale_text.style.resolve(None, 0.0);
8493 let sale_word_width =
8494 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8495 assert!(
8496 sale_word_width > 0.0,
8497 "SALE should have non-zero min-content width"
8498 );
8499
8500 let container = make_styled_view(
8503 Style {
8504 flex_direction: Some(FlexDirection::Row),
8505 width: Some(Dimension::Pt(100.0)),
8506 ..Default::default()
8507 },
8508 vec![
8509 make_styled_view(
8510 Style {
8511 width: Some(Dimension::Pt(80.0)),
8512 flex_shrink: Some(1.0),
8513 ..Default::default()
8514 },
8515 vec![],
8516 ),
8517 make_styled_view(
8518 Style {
8519 width: Some(Dimension::Pt(60.0)),
8520 flex_shrink: Some(1.0),
8521 ..Default::default()
8522 },
8523 vec![make_text("SALE", 12.0)],
8524 ),
8525 ],
8526 );
8527
8528 let doc = Document {
8529 children: vec![Node::page(
8530 PageConfig::default(),
8531 Style::default(),
8532 vec![container],
8533 )],
8534 metadata: Default::default(),
8535 default_page: PageConfig::default(),
8536 first_page: None,
8537 left_page: None,
8538 right_page: None,
8539 named_pages: Default::default(),
8540 attachments: vec![],
8541 zugferd: None,
8542 fonts: vec![],
8543 tagged: false,
8544 pdfa: None,
8545 default_style: None,
8546 embedded_data: None,
8547 flatten_forms: false,
8548 pdf_ua: false,
8549 certification: None,
8550 pdf_version: Default::default(),
8551 pdf_ua2: false,
8552 };
8553
8554 let pages = engine.layout(&doc, &font_context);
8555 assert!(!pages.is_empty());
8556
8557 let page = &pages[0];
8560 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8562 assert!(
8563 container_el.is_some(),
8564 "Should find container with 2 children"
8565 );
8566 let sale_child = &container_el.unwrap().children[1];
8567 assert!(
8568 sale_child.width >= sale_word_width - 0.01,
8569 "SALE child width ({}) should be >= min-content width ({})",
8570 sale_child.width,
8571 sale_word_width
8572 );
8573 }
8574
8575 #[test]
8578 fn column_justify_content_center() {
8579 let engine = LayoutEngine::new();
8582 let font_context = FontContext::new();
8583
8584 let container = make_styled_view(
8585 Style {
8586 flex_direction: Some(FlexDirection::Column),
8587 height: Some(Dimension::Pt(200.0)),
8588 justify_content: Some(JustifyContent::Center),
8589 ..Default::default()
8590 },
8591 vec![make_text("Centered", 12.0)],
8592 );
8593
8594 let doc = Document {
8595 children: vec![Node::page(
8596 PageConfig::default(),
8597 Style::default(),
8598 vec![container],
8599 )],
8600 metadata: Default::default(),
8601 default_page: PageConfig::default(),
8602 first_page: None,
8603 left_page: None,
8604 right_page: None,
8605 named_pages: Default::default(),
8606 attachments: vec![],
8607 zugferd: None,
8608 fonts: vec![],
8609 tagged: false,
8610 pdfa: None,
8611 default_style: None,
8612 embedded_data: None,
8613 flatten_forms: false,
8614 pdf_ua: false,
8615 certification: None,
8616 pdf_version: Default::default(),
8617 pdf_ua2: false,
8618 };
8619
8620 let pages = engine.layout(&doc, &font_context);
8621 let page = &pages[0];
8622
8623 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8626 assert!(
8627 container_el.is_some(),
8628 "Should find container with children"
8629 );
8630 let container_el = container_el.unwrap();
8631 let child = &container_el.children[0];
8632
8633 let child_offset = child.y - container_el.y;
8635 let expected_offset = (200.0 - child.height) / 2.0;
8636 assert!(
8637 (child_offset - expected_offset).abs() < 2.0,
8638 "Child offset ({}) should be near center ({})",
8639 child_offset,
8640 expected_offset
8641 );
8642 }
8643
8644 #[test]
8645 fn column_align_items_center() {
8646 let engine = LayoutEngine::new();
8649 let font_context = FontContext::new();
8650
8651 let container = make_styled_view(
8652 Style {
8653 flex_direction: Some(FlexDirection::Column),
8654 width: Some(Dimension::Pt(300.0)),
8655 align_items: Some(AlignItems::Center),
8656 ..Default::default()
8657 },
8658 vec![make_text("Hi", 12.0)],
8659 );
8660
8661 let doc = Document {
8662 children: vec![Node::page(
8663 PageConfig::default(),
8664 Style::default(),
8665 vec![container],
8666 )],
8667 metadata: Default::default(),
8668 default_page: PageConfig::default(),
8669 first_page: None,
8670 left_page: None,
8671 right_page: None,
8672 named_pages: Default::default(),
8673 attachments: vec![],
8674 zugferd: None,
8675 fonts: vec![],
8676 tagged: false,
8677 pdfa: None,
8678 default_style: None,
8679 embedded_data: None,
8680 flatten_forms: false,
8681 pdf_ua: false,
8682 certification: None,
8683 pdf_version: Default::default(),
8684 pdf_ua2: false,
8685 };
8686
8687 let pages = engine.layout(&doc, &font_context);
8688 let page = &pages[0];
8689
8690 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8691 assert!(container_el.is_some());
8692 let container_el = container_el.unwrap();
8693 let child = &container_el.children[0];
8694
8695 let child_center = child.x + child.width / 2.0;
8697 let container_center = container_el.x + container_el.width / 2.0;
8698 assert!(
8699 (child_center - container_center).abs() < 2.0,
8700 "Child center ({}) should be near container center ({})",
8701 child_center,
8702 container_center
8703 );
8704 }
8705
8706 #[test]
8709 fn absolute_child_positioned_relative_to_parent() {
8710 let engine = LayoutEngine::new();
8714 let font_context = FontContext::new();
8715
8716 let parent = make_styled_view(
8717 Style {
8718 position: Some(crate::model::Position::Relative),
8719 margin: Some(MarginEdges::from_edges(Edges {
8720 top: 50.0,
8721 left: 50.0,
8722 ..Default::default()
8723 })),
8724 width: Some(Dimension::Pt(200.0)),
8725 height: Some(Dimension::Pt(200.0)),
8726 ..Default::default()
8727 },
8728 vec![make_styled_view(
8729 Style {
8730 position: Some(crate::model::Position::Absolute),
8731 top: Some(10.0),
8732 left: Some(10.0),
8733 width: Some(Dimension::Pt(50.0)),
8734 height: Some(Dimension::Pt(50.0)),
8735 ..Default::default()
8736 },
8737 vec![],
8738 )],
8739 );
8740
8741 let doc = Document {
8742 children: vec![Node::page(
8743 PageConfig::default(),
8744 Style::default(),
8745 vec![parent],
8746 )],
8747 metadata: Default::default(),
8748 default_page: PageConfig::default(),
8749 first_page: None,
8750 left_page: None,
8751 right_page: None,
8752 named_pages: Default::default(),
8753 attachments: vec![],
8754 zugferd: None,
8755 fonts: vec![],
8756 tagged: false,
8757 pdfa: None,
8758 default_style: None,
8759 embedded_data: None,
8760 flatten_forms: false,
8761 pdf_ua: false,
8762 certification: None,
8763 pdf_version: Default::default(),
8764 pdf_ua2: false,
8765 };
8766
8767 let pages = engine.layout(&doc, &font_context);
8768 let page = &pages[0];
8769
8770 let parent_el = page
8773 .elements
8774 .iter()
8775 .find(|e| e.width > 190.0 && e.width < 210.0);
8776 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8777 let parent_el = parent_el.unwrap();
8778
8779 let abs_child = parent_el
8781 .children
8782 .iter()
8783 .find(|e| e.width > 45.0 && e.width < 55.0);
8784 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8785 let abs_child = abs_child.unwrap();
8786
8787 let expected_x = parent_el.x + 10.0;
8788 let expected_y = parent_el.y + 10.0;
8789 assert!(
8790 (abs_child.x - expected_x).abs() < 1.0,
8791 "Absolute child x ({}) should be parent.x + 10 ({})",
8792 abs_child.x,
8793 expected_x
8794 );
8795 assert!(
8796 (abs_child.y - expected_y).abs() < 1.0,
8797 "Absolute child y ({}) should be parent.y + 10 ({})",
8798 abs_child.y,
8799 expected_y
8800 );
8801 }
8802
8803 #[test]
8804 fn absolute_escapes_unpositioned_parent_to_page() {
8805 let engine = LayoutEngine::new();
8810 let font_context = FontContext::new();
8811 let parent = make_styled_view(
8812 Style {
8813 margin: Some(MarginEdges::from_edges(Edges {
8814 top: 50.0,
8815 left: 50.0,
8816 ..Default::default()
8817 })),
8818 width: Some(Dimension::Pt(200.0)),
8819 height: Some(Dimension::Pt(200.0)),
8820 ..Default::default()
8821 },
8822 vec![make_styled_view(
8823 Style {
8824 position: Some(crate::model::Position::Absolute),
8825 top: Some(10.0),
8826 left: Some(10.0),
8827 width: Some(Dimension::Pt(50.0)),
8828 height: Some(Dimension::Pt(50.0)),
8829 ..Default::default()
8830 },
8831 vec![],
8832 )],
8833 );
8834 let doc = Document {
8835 children: vec![Node::page(
8836 PageConfig::default(),
8837 Style::default(),
8838 vec![parent],
8839 )],
8840 metadata: Default::default(),
8841 default_page: PageConfig::default(),
8842 first_page: None,
8843 left_page: None,
8844 right_page: None,
8845 named_pages: Default::default(),
8846 attachments: vec![],
8847 zugferd: None,
8848 fonts: vec![],
8849 tagged: false,
8850 pdfa: None,
8851 default_style: None,
8852 embedded_data: None,
8853 flatten_forms: false,
8854 pdf_ua: false,
8855 certification: None,
8856 pdf_version: Default::default(),
8857 pdf_ua2: false,
8858 };
8859 let pages = engine.layout(&doc, &font_context);
8860 let page = &pages[0];
8861 let parent_el = page
8862 .elements
8863 .iter()
8864 .find(|e| e.width > 190.0 && e.width < 210.0)
8865 .expect("parent");
8866 let abs_child = parent_el
8867 .children
8868 .iter()
8869 .find(|e| e.width > 45.0 && e.width < 55.0)
8870 .expect("abs child");
8871 let page_left = PageConfig::default().margin.left;
8872 let page_top = PageConfig::default().margin.top;
8873 assert!(
8874 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
8875 "absolute escapes to the page: x {} should be page_left + 10 ({})",
8876 abs_child.x,
8877 page_left + 10.0
8878 );
8879 assert!(
8880 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
8881 "absolute escapes to the page: y {} should be page_top + 10 ({})",
8882 abs_child.y,
8883 page_top + 10.0
8884 );
8885 assert!(
8886 abs_child.x < parent_el.x,
8887 "child must no longer be parent-relative (parent is 50pt further in)"
8888 );
8889 }
8890
8891 #[test]
8892 fn text_transform_none_passthrough() {
8893 assert_eq!(
8894 apply_text_transform("Hello World", TextTransform::None),
8895 "Hello World"
8896 );
8897 }
8898
8899 #[test]
8900 fn text_transform_uppercase() {
8901 assert_eq!(
8902 apply_text_transform("hello world", TextTransform::Uppercase),
8903 "HELLO WORLD"
8904 );
8905 }
8906
8907 #[test]
8908 fn text_transform_lowercase() {
8909 assert_eq!(
8910 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
8911 "hello world"
8912 );
8913 }
8914
8915 #[test]
8916 fn text_transform_capitalize() {
8917 assert_eq!(
8918 apply_text_transform("hello world", TextTransform::Capitalize),
8919 "Hello World"
8920 );
8921 assert_eq!(
8922 apply_text_transform(" hello world ", TextTransform::Capitalize),
8923 " Hello World "
8924 );
8925 assert_eq!(
8926 apply_text_transform("already Capitalized", TextTransform::Capitalize),
8927 "Already Capitalized"
8928 );
8929 }
8930
8931 #[test]
8932 fn text_transform_capitalize_empty() {
8933 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
8934 }
8935
8936 #[test]
8937 fn apply_char_transform_uppercase() {
8938 assert_eq!(
8939 apply_char_transform('a', TextTransform::Uppercase, false),
8940 'A'
8941 );
8942 assert_eq!(
8943 apply_char_transform('A', TextTransform::Uppercase, false),
8944 'A'
8945 );
8946 }
8947
8948 #[test]
8949 fn apply_char_transform_capitalize_word_start() {
8950 assert_eq!(
8951 apply_char_transform('h', TextTransform::Capitalize, true),
8952 'H'
8953 );
8954 assert_eq!(
8955 apply_char_transform('h', TextTransform::Capitalize, false),
8956 'h'
8957 );
8958 }
8959
8960 #[test]
8963 fn column_flex_grow_single_child_fills_container() {
8964 let engine = LayoutEngine::new();
8967 let font_context = FontContext::new();
8968
8969 let child = make_styled_view(
8970 Style {
8971 flex_grow: Some(1.0),
8972 ..Default::default()
8973 },
8974 vec![make_text("Short", 12.0)],
8975 );
8976
8977 let container = make_styled_view(
8978 Style {
8979 flex_direction: Some(FlexDirection::Column),
8980 height: Some(Dimension::Pt(300.0)),
8981 ..Default::default()
8982 },
8983 vec![child],
8984 );
8985
8986 let doc = Document {
8987 children: vec![Node::page(
8988 PageConfig::default(),
8989 Style::default(),
8990 vec![container],
8991 )],
8992 metadata: Default::default(),
8993 default_page: PageConfig::default(),
8994 first_page: None,
8995 left_page: None,
8996 right_page: None,
8997 named_pages: Default::default(),
8998 attachments: vec![],
8999 zugferd: None,
9000 fonts: vec![],
9001 tagged: false,
9002 pdfa: None,
9003 default_style: None,
9004 embedded_data: None,
9005 flatten_forms: false,
9006 pdf_ua: false,
9007 certification: None,
9008 pdf_version: Default::default(),
9009 pdf_ua2: false,
9010 };
9011
9012 let pages = engine.layout(&doc, &font_context);
9013 let page = &pages[0];
9014
9015 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
9016 assert!(container_el.is_some());
9017 let container_el = container_el.unwrap();
9018 assert!(
9019 (container_el.height - 300.0).abs() < 1.0,
9020 "Container should be 300pt, got {}",
9021 container_el.height
9022 );
9023
9024 let child_el = &container_el.children[0];
9025 assert!(
9026 (child_el.height - 300.0).abs() < 1.0,
9027 "flex-grow child should expand to 300pt, got {}",
9028 child_el.height
9029 );
9030 }
9031
9032 #[test]
9033 fn column_flex_grow_two_children_proportional() {
9034 let engine = LayoutEngine::new();
9037 let font_context = FontContext::new();
9038
9039 let child1 = make_styled_view(
9040 Style {
9041 flex_grow: Some(1.0),
9042 ..Default::default()
9043 },
9044 vec![make_text("A", 12.0)],
9045 );
9046 let child2 = make_styled_view(
9047 Style {
9048 flex_grow: Some(2.0),
9049 ..Default::default()
9050 },
9051 vec![make_text("B", 12.0)],
9052 );
9053
9054 let container = make_styled_view(
9055 Style {
9056 flex_direction: Some(FlexDirection::Column),
9057 height: Some(Dimension::Pt(300.0)),
9058 ..Default::default()
9059 },
9060 vec![child1, child2],
9061 );
9062
9063 let doc = Document {
9064 children: vec![Node::page(
9065 PageConfig::default(),
9066 Style::default(),
9067 vec![container],
9068 )],
9069 metadata: Default::default(),
9070 default_page: PageConfig::default(),
9071 first_page: None,
9072 left_page: None,
9073 right_page: None,
9074 named_pages: Default::default(),
9075 attachments: vec![],
9076 zugferd: None,
9077 fonts: vec![],
9078 tagged: false,
9079 pdfa: None,
9080 default_style: None,
9081 embedded_data: None,
9082 flatten_forms: false,
9083 pdf_ua: false,
9084 certification: None,
9085 pdf_version: Default::default(),
9086 pdf_ua2: false,
9087 };
9088
9089 let pages = engine.layout(&doc, &font_context);
9090 let page = &pages[0];
9091
9092 let container_el = page
9093 .elements
9094 .iter()
9095 .find(|e| e.children.len() == 2)
9096 .expect("Should find container with two children");
9097
9098 let c1 = &container_el.children[0];
9099 let c2 = &container_el.children[1];
9100
9101 let total = c1.height + c2.height;
9105 assert!(
9106 (total - 300.0).abs() < 2.0,
9107 "Children should sum to ~300pt, got {}",
9108 total
9109 );
9110
9111 let ratio = c2.height / c1.height;
9114 assert!(
9115 ratio > 1.3 && ratio < 2.5,
9116 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
9117 ratio
9118 );
9119 }
9120
9121 #[test]
9122 fn column_flex_grow_mixed_grow_and_fixed() {
9123 let engine = LayoutEngine::new();
9126 let font_context = FontContext::new();
9127
9128 let fixed_child = make_styled_view(
9129 Style {
9130 height: Some(Dimension::Pt(50.0)),
9131 ..Default::default()
9132 },
9133 vec![make_text("Fixed", 12.0)],
9134 );
9135 let grow_child = make_styled_view(
9136 Style {
9137 flex_grow: Some(1.0),
9138 ..Default::default()
9139 },
9140 vec![make_text("Grow", 12.0)],
9141 );
9142
9143 let container = make_styled_view(
9144 Style {
9145 flex_direction: Some(FlexDirection::Column),
9146 height: Some(Dimension::Pt(300.0)),
9147 ..Default::default()
9148 },
9149 vec![fixed_child, grow_child],
9150 );
9151
9152 let doc = Document {
9153 children: vec![Node::page(
9154 PageConfig::default(),
9155 Style::default(),
9156 vec![container],
9157 )],
9158 metadata: Default::default(),
9159 default_page: PageConfig::default(),
9160 first_page: None,
9161 left_page: None,
9162 right_page: None,
9163 named_pages: Default::default(),
9164 attachments: vec![],
9165 zugferd: None,
9166 fonts: vec![],
9167 tagged: false,
9168 pdfa: None,
9169 default_style: None,
9170 embedded_data: None,
9171 flatten_forms: false,
9172 pdf_ua: false,
9173 certification: None,
9174 pdf_version: Default::default(),
9175 pdf_ua2: false,
9176 };
9177
9178 let pages = engine.layout(&doc, &font_context);
9179 let page = &pages[0];
9180
9181 let container_el = page
9182 .elements
9183 .iter()
9184 .find(|e| e.children.len() == 2)
9185 .expect("Should find container with two children");
9186
9187 let fixed_el = &container_el.children[0];
9188 let grow_el = &container_el.children[1];
9189
9190 assert!(
9192 (fixed_el.height - 50.0).abs() < 1.0,
9193 "Fixed child should stay at 50pt, got {}",
9194 fixed_el.height
9195 );
9196
9197 assert!(
9199 (grow_el.height - 250.0).abs() < 2.0,
9200 "Grow child should expand to ~250pt, got {}",
9201 grow_el.height
9202 );
9203 }
9204
9205 #[test]
9206 fn column_flex_grow_page_level() {
9207 let engine = LayoutEngine::new();
9209 let font_context = FontContext::new();
9210
9211 let grow_child = make_styled_view(
9212 Style {
9213 flex_grow: Some(1.0),
9214 ..Default::default()
9215 },
9216 vec![make_text("Fill page", 12.0)],
9217 );
9218
9219 let doc = Document {
9220 children: vec![Node::page(
9221 PageConfig::default(),
9222 Style::default(),
9223 vec![grow_child],
9224 )],
9225 metadata: Default::default(),
9226 default_page: PageConfig::default(),
9227 first_page: None,
9228 left_page: None,
9229 right_page: None,
9230 named_pages: Default::default(),
9231 attachments: vec![],
9232 zugferd: None,
9233 fonts: vec![],
9234 tagged: false,
9235 pdfa: None,
9236 default_style: None,
9237 embedded_data: None,
9238 flatten_forms: false,
9239 pdf_ua: false,
9240 certification: None,
9241 pdf_version: Default::default(),
9242 pdf_ua2: false,
9243 };
9244
9245 let pages = engine.layout(&doc, &font_context);
9246 let page = &pages[0];
9247
9248 assert!(
9250 !page.elements.is_empty(),
9251 "Page should have at least one element"
9252 );
9253
9254 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
9255 let el = &page.elements[0];
9256 assert!(
9257 (el.height - content_height).abs() < 2.0,
9258 "Page-level flex-grow child should fill content height ({}), got {}",
9259 content_height,
9260 el.height
9261 );
9262 }
9263
9264 #[test]
9265 fn column_flex_grow_with_justify_content() {
9266 let engine = LayoutEngine::new();
9270 let font_context = FontContext::new();
9271
9272 let fixed_child = make_styled_view(
9273 Style {
9274 height: Some(Dimension::Pt(50.0)),
9275 ..Default::default()
9276 },
9277 vec![make_text("Top", 12.0)],
9278 );
9279 let grow_child = make_styled_view(
9280 Style {
9281 flex_grow: Some(1.0),
9282 ..Default::default()
9283 },
9284 vec![make_text("Fill", 12.0)],
9285 );
9286
9287 let container = make_styled_view(
9288 Style {
9289 flex_direction: Some(FlexDirection::Column),
9290 height: Some(Dimension::Pt(300.0)),
9291 justify_content: Some(JustifyContent::Center),
9292 ..Default::default()
9293 },
9294 vec![fixed_child, grow_child],
9295 );
9296
9297 let doc = Document {
9298 children: vec![Node::page(
9299 PageConfig::default(),
9300 Style::default(),
9301 vec![container],
9302 )],
9303 metadata: Default::default(),
9304 default_page: PageConfig::default(),
9305 first_page: None,
9306 left_page: None,
9307 right_page: None,
9308 named_pages: Default::default(),
9309 attachments: vec![],
9310 zugferd: None,
9311 fonts: vec![],
9312 tagged: false,
9313 pdfa: None,
9314 default_style: None,
9315 embedded_data: None,
9316 flatten_forms: false,
9317 pdf_ua: false,
9318 certification: None,
9319 pdf_version: Default::default(),
9320 pdf_ua2: false,
9321 };
9322
9323 let pages = engine.layout(&doc, &font_context);
9324 let page = &pages[0];
9325
9326 let container_el = page
9327 .elements
9328 .iter()
9329 .find(|e| e.children.len() == 2)
9330 .expect("Should find container");
9331
9332 let first_child = &container_el.children[0];
9335 assert!(
9336 (first_child.y - container_el.y).abs() < 1.0,
9337 "First child should be at top of container"
9338 );
9339
9340 let total = container_el.children[0].height + container_el.children[1].height;
9342 assert!(
9343 (total - 300.0).abs() < 2.0,
9344 "Children should fill container, got {}",
9345 total
9346 );
9347 }
9348
9349 #[test]
9350 fn column_flex_grow_child_justify_content_center() {
9351 let engine = LayoutEngine::new();
9354 let font_context = FontContext::new();
9355
9356 let inner_box = make_styled_view(
9358 Style {
9359 height: Some(Dimension::Pt(40.0)),
9360 ..Default::default()
9361 },
9362 vec![make_text("Centered", 12.0)],
9363 );
9364
9365 let grow_child = make_styled_view(
9367 Style {
9368 flex_grow: Some(1.0),
9369 flex_direction: Some(FlexDirection::Column),
9370 justify_content: Some(JustifyContent::Center),
9371 ..Default::default()
9372 },
9373 vec![inner_box],
9374 );
9375
9376 let container = make_styled_view(
9378 Style {
9379 flex_direction: Some(FlexDirection::Column),
9380 height: Some(Dimension::Pt(400.0)),
9381 ..Default::default()
9382 },
9383 vec![grow_child],
9384 );
9385
9386 let doc = Document {
9387 children: vec![Node::page(
9388 PageConfig::default(),
9389 Style::default(),
9390 vec![container],
9391 )],
9392 metadata: Default::default(),
9393 default_page: PageConfig::default(),
9394 first_page: None,
9395 left_page: None,
9396 right_page: None,
9397 named_pages: Default::default(),
9398 attachments: vec![],
9399 zugferd: None,
9400 fonts: vec![],
9401 tagged: false,
9402 pdfa: None,
9403 default_style: None,
9404 embedded_data: None,
9405 flatten_forms: false,
9406 pdf_ua: false,
9407 certification: None,
9408 pdf_version: Default::default(),
9409 pdf_ua2: false,
9410 };
9411
9412 let pages = engine.layout(&doc, &font_context);
9413 let page = &pages[0];
9414
9415 let container_el = page
9417 .elements
9418 .iter()
9419 .find(|e| e.height > 350.0 && e.children.len() == 1)
9420 .expect("Should find outer container");
9421
9422 let grow_el = &container_el.children[0];
9423 assert!(
9424 (grow_el.height - 400.0).abs() < 2.0,
9425 "Grow child should expand to 400, got {}",
9426 grow_el.height
9427 );
9428
9429 let inner_el = &grow_el.children[0];
9431 let expected_center = grow_el.y + grow_el.height / 2.0;
9432 let actual_center = inner_el.y + inner_el.height / 2.0;
9433 assert!(
9434 (actual_center - expected_center).abs() < 2.0,
9435 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
9436 expected_center,
9437 actual_center
9438 );
9439 }
9440
9441 #[test]
9442 fn column_flex_grow_child_justify_content_flex_end() {
9443 let engine = LayoutEngine::new();
9445 let font_context = FontContext::new();
9446
9447 let inner_box = make_styled_view(
9448 Style {
9449 height: Some(Dimension::Pt(30.0)),
9450 ..Default::default()
9451 },
9452 vec![make_text("Bottom", 12.0)],
9453 );
9454
9455 let grow_child = make_styled_view(
9456 Style {
9457 flex_grow: Some(1.0),
9458 flex_direction: Some(FlexDirection::Column),
9459 justify_content: Some(JustifyContent::FlexEnd),
9460 ..Default::default()
9461 },
9462 vec![inner_box],
9463 );
9464
9465 let container = make_styled_view(
9466 Style {
9467 flex_direction: Some(FlexDirection::Column),
9468 height: Some(Dimension::Pt(300.0)),
9469 ..Default::default()
9470 },
9471 vec![grow_child],
9472 );
9473
9474 let doc = Document {
9475 children: vec![Node::page(
9476 PageConfig::default(),
9477 Style::default(),
9478 vec![container],
9479 )],
9480 metadata: Default::default(),
9481 default_page: PageConfig::default(),
9482 first_page: None,
9483 left_page: None,
9484 right_page: None,
9485 named_pages: Default::default(),
9486 attachments: vec![],
9487 zugferd: None,
9488 fonts: vec![],
9489 tagged: false,
9490 pdfa: None,
9491 default_style: None,
9492 embedded_data: None,
9493 flatten_forms: false,
9494 pdf_ua: false,
9495 certification: None,
9496 pdf_version: Default::default(),
9497 pdf_ua2: false,
9498 };
9499
9500 let pages = engine.layout(&doc, &font_context);
9501 let page = &pages[0];
9502
9503 let container_el = page
9504 .elements
9505 .iter()
9506 .find(|e| e.height > 250.0 && e.children.len() == 1)
9507 .expect("Should find outer container");
9508
9509 let grow_el = &container_el.children[0];
9510 let inner_el = &grow_el.children[0];
9511
9512 let inner_bottom = inner_el.y + inner_el.height;
9514 let grow_bottom = grow_el.y + grow_el.height;
9515 assert!(
9516 (inner_bottom - grow_bottom).abs() < 2.0,
9517 "Inner box bottom ({}) should align with grow child bottom ({})",
9518 inner_bottom,
9519 grow_bottom
9520 );
9521 }
9522
9523 #[test]
9524 fn column_flex_grow_child_no_justify_unchanged() {
9525 let engine = LayoutEngine::new();
9527 let font_context = FontContext::new();
9528
9529 let inner_box = make_styled_view(
9530 Style {
9531 height: Some(Dimension::Pt(50.0)),
9532 ..Default::default()
9533 },
9534 vec![make_text("Top", 12.0)],
9535 );
9536
9537 let grow_child = make_styled_view(
9538 Style {
9539 flex_grow: Some(1.0),
9540 flex_direction: Some(FlexDirection::Column),
9541 ..Default::default()
9543 },
9544 vec![inner_box],
9545 );
9546
9547 let container = make_styled_view(
9548 Style {
9549 flex_direction: Some(FlexDirection::Column),
9550 height: Some(Dimension::Pt(300.0)),
9551 ..Default::default()
9552 },
9553 vec![grow_child],
9554 );
9555
9556 let doc = Document {
9557 children: vec![Node::page(
9558 PageConfig::default(),
9559 Style::default(),
9560 vec![container],
9561 )],
9562 metadata: Default::default(),
9563 default_page: PageConfig::default(),
9564 first_page: None,
9565 left_page: None,
9566 right_page: None,
9567 named_pages: Default::default(),
9568 attachments: vec![],
9569 zugferd: None,
9570 fonts: vec![],
9571 tagged: false,
9572 pdfa: None,
9573 default_style: None,
9574 embedded_data: None,
9575 flatten_forms: false,
9576 pdf_ua: false,
9577 certification: None,
9578 pdf_version: Default::default(),
9579 pdf_ua2: false,
9580 };
9581
9582 let pages = engine.layout(&doc, &font_context);
9583 let page = &pages[0];
9584
9585 let container_el = page
9586 .elements
9587 .iter()
9588 .find(|e| e.height > 250.0 && e.children.len() == 1)
9589 .expect("Should find outer container");
9590
9591 let grow_el = &container_el.children[0];
9592 let inner_el = &grow_el.children[0];
9593
9594 assert!(
9596 (inner_el.y - grow_el.y).abs() < 2.0,
9597 "Inner box ({}) should be at top of grow child ({})",
9598 inner_el.y,
9599 grow_el.y
9600 );
9601 }
9602
9603 #[test]
9604 fn column_flex_grow_child_align_items_center() {
9605 let engine = LayoutEngine::new();
9607 let font_context = FontContext::new();
9608
9609 let text = make_text("Hello", 12.0);
9610
9611 let grow_child = make_styled_view(
9612 Style {
9613 flex_grow: Some(1.0),
9614 flex_direction: Some(FlexDirection::Column),
9615 align_items: Some(AlignItems::Center),
9616 ..Default::default()
9617 },
9618 vec![text],
9619 );
9620
9621 let container = make_styled_view(
9622 Style {
9623 flex_direction: Some(FlexDirection::Column),
9624 height: Some(Dimension::Pt(300.0)),
9625 ..Default::default()
9626 },
9627 vec![grow_child],
9628 );
9629
9630 let doc = Document {
9631 children: vec![Node::page(
9632 PageConfig::default(),
9633 Style::default(),
9634 vec![container],
9635 )],
9636 metadata: Default::default(),
9637 default_page: PageConfig::default(),
9638 first_page: None,
9639 left_page: None,
9640 right_page: None,
9641 named_pages: Default::default(),
9642 attachments: vec![],
9643 zugferd: None,
9644 fonts: vec![],
9645 tagged: false,
9646 pdfa: None,
9647 default_style: None,
9648 embedded_data: None,
9649 flatten_forms: false,
9650 pdf_ua: false,
9651 certification: None,
9652 pdf_version: Default::default(),
9653 pdf_ua2: false,
9654 };
9655
9656 let pages = engine.layout(&doc, &font_context);
9657 let page = &pages[0];
9658
9659 let container_el = page
9660 .elements
9661 .iter()
9662 .find(|e| e.height > 250.0 && e.children.len() == 1)
9663 .expect("Should find outer container");
9664
9665 let grow_el = &container_el.children[0];
9666 assert!(
9667 !grow_el.children.is_empty(),
9668 "Grow child should have text child"
9669 );
9670
9671 let text_el = &grow_el.children[0];
9672 let text_center = text_el.x + text_el.width / 2.0;
9673 let grow_center = grow_el.x + grow_el.width / 2.0;
9674 assert!(
9675 (text_center - grow_center).abs() < 2.0,
9676 "Text center ({}) should be near grow child center ({})",
9677 text_center,
9678 grow_center
9679 );
9680 }
9681
9682 #[test]
9683 fn image_intrinsic_width_respects_height_constraint() {
9684 let engine = LayoutEngine::new();
9688 let font_context = FontContext::new();
9689
9690 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9692
9693 let image_node = Node {
9694 kind: NodeKind::Image {
9695 src: one_px_png.to_string(),
9696 width: None,
9697 height: Some(36.0),
9698 },
9699 style: Style::default(),
9700 children: vec![],
9701 id: None,
9702 source_location: None,
9703 bookmark: None,
9704 href: None,
9705 alt: None,
9706 };
9707
9708 let resolved = image_node.style.resolve(None, 0.0);
9709 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9710
9711 assert!(
9713 (intrinsic - 36.0).abs() < 1.0,
9714 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9715 intrinsic
9716 );
9717 }
9718}