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 base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3054 let lines = match flex_wrap {
3055 FlexWrap::NoWrap => {
3056 vec![flex::WrapLine {
3057 start: 0,
3058 end: items.len(),
3059 }]
3060 }
3061 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
3068 FlexWrap::WrapReverse => {
3069 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
3070 l.reverse();
3071 l
3072 }
3073 };
3074
3075 if lines.is_empty() {
3076 return;
3077 }
3078
3079 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3081
3082 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3084
3085 let initial_pages_count = pages.len();
3086 let flex_start_y = cursor.y;
3087 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3088
3089 for (line_idx, line) in lines.iter().enumerate() {
3090 let parallel = true;
3105 let line_items = &items[line.start..line.end];
3106 let line_count = line.end - line.start;
3107 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3108 let distributable = available_width - line_gap;
3109
3110 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
3112 let remaining = distributable - total_base;
3113
3114 if remaining > 0.0 {
3115 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3116 if total_grow > 0.0 {
3117 for (j, item) in line_items.iter().enumerate() {
3118 final_widths[line.start + j] =
3119 item.base_width + remaining * (item.style.flex_grow / total_grow);
3120 }
3121 }
3122 } else if remaining < 0.0 {
3123 let total_shrink: f64 = line_items
3124 .iter()
3125 .map(|i| i.style.flex_shrink * i.base_width)
3126 .sum();
3127 if total_shrink > 0.0 {
3128 for (j, item) in line_items.iter().enumerate() {
3129 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3130 let w = item.base_width + remaining * factor;
3131 let floor = item.style.min_width.max(item.min_content_width);
3132 final_widths[line.start + j] = w.max(floor);
3133 }
3134 }
3135 }
3136
3137 let mut line_height: f64 = line_items
3139 .iter()
3140 .enumerate()
3141 .map(|(j, item)| {
3142 let fw = final_widths[line.start + j];
3143 self.measure_node_height(item.node, fw, &item.style, font_context)
3144 + item.style.margin.vertical()
3145 })
3146 .fold(0.0f64, f64::max);
3147
3148 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3154 let baseline_ds: Vec<Option<f64>> = line_items
3155 .iter()
3156 .enumerate()
3157 .map(|(j, item)| {
3158 let align = item.style.align_self.unwrap_or(parent_align);
3159 if matches!(align, AlignItems::Baseline) {
3160 let fw = final_widths[line.start + j];
3161 Some(self.flex_item_baseline_distance(
3162 item.node,
3163 &item.style,
3164 fw,
3165 font_context,
3166 ))
3167 } else {
3168 None
3169 }
3170 })
3171 .collect();
3172 let line_baseline = baseline_ds
3173 .iter()
3174 .flatten()
3175 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3176 if let Some(bl) = line_baseline {
3177 for (j, item) in line_items.iter().enumerate() {
3178 if let Some(d) = baseline_ds[j] {
3179 let fw = final_widths[line.start + j];
3180 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3181 + item.style.margin.vertical();
3182 line_height = line_height.max(bl - d + h);
3183 }
3184 }
3185 }
3186
3187 if let Some(ps) = parent_style {
3198 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3199 if let SizeConstraint::Fixed(h) = ps.height {
3200 let inner = h - ps.padding.vertical() - ps.border_width.vertical();
3201 line_height = line_height.max(inner);
3202 }
3203 }
3204 }
3205
3206 if !parallel && line_height > cursor.remaining_height() && cursor.y > 0.0 {
3228 pages.push(cursor.finalize());
3229 *cursor = cursor.new_page();
3230 }
3231
3232 if line_idx > 0 {
3234 cursor.y += row_gap;
3235 }
3236
3237 let row_start_y = cursor.y;
3238
3239 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
3241 let slack = available_width - actual_total - line_gap;
3242
3243 let (start_offset, between_extra) = match justify {
3244 JustifyContent::FlexStart => (0.0, 0.0),
3245 JustifyContent::FlexEnd => (slack, 0.0),
3246 JustifyContent::Center => (slack / 2.0, 0.0),
3247 JustifyContent::SpaceBetween => {
3248 if line_count > 1 {
3249 (0.0, slack / (line_count as f64 - 1.0))
3250 } else {
3251 (0.0, 0.0)
3252 }
3253 }
3254 JustifyContent::SpaceAround => {
3255 let s = slack / line_count as f64;
3256 (s / 2.0, s)
3257 }
3258 JustifyContent::SpaceEvenly => {
3259 let s = slack / (line_count as f64 + 1.0);
3260 (s, s)
3261 }
3262 };
3263
3264 let line_elem_start = cursor.elements.len();
3265 let mut x = content_x + start_offset;
3266
3267 let base_page_elements = if parallel {
3274 std::mem::take(&mut cursor.elements)
3275 } else {
3276 Vec::new()
3277 };
3278 let mut item_frags: Vec<ItemFragments> = Vec::with_capacity(line_items.len());
3279
3280 let line_start_pages = pages.len();
3291
3292 for (j, item) in line_items.iter().enumerate() {
3293 if j > 0 {
3294 x += column_gap + between_extra;
3295 }
3296
3297 let fw = final_widths[line.start + j];
3298
3299 let align = item
3300 .style
3301 .align_self
3302 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3303
3304 let item_height =
3305 self.measure_node_height(item.node, fw, &item.style, font_context);
3306
3307 let has_auto_v = item.style.margin.has_auto_vertical();
3309 let y_offset = if has_auto_v {
3310 let fixed_v = item.style.margin.vertical();
3311 let slack = (line_height - item_height - fixed_v).max(0.0);
3312 let auto_top = item.style.margin.top.is_auto();
3313 let auto_bottom = item.style.margin.bottom.is_auto();
3314 match (auto_top, auto_bottom) {
3315 (true, true) => slack / 2.0,
3316 (true, false) => slack,
3317 (false, true) => 0.0,
3318 (false, false) => 0.0,
3319 }
3320 } else {
3321 match align {
3322 AlignItems::FlexStart => 0.0,
3323 AlignItems::FlexEnd => {
3324 line_height - item_height - item.style.margin.vertical()
3325 }
3326 AlignItems::Center => {
3327 (line_height - item_height - item.style.margin.vertical()) / 2.0
3328 }
3329 AlignItems::Stretch => 0.0,
3330 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3331 (Some(bl), Some(d)) => bl - d,
3332 _ => 0.0,
3333 },
3334 }
3335 };
3336
3337 let cross_h = if matches!(align, AlignItems::Stretch)
3341 && matches!(item.style.height, SizeConstraint::Auto)
3342 && !has_auto_v
3343 {
3344 let stretch_h = line_height - item.style.margin.vertical();
3345 if stretch_h > item_height {
3346 Some(stretch_h)
3347 } else {
3348 None
3349 }
3350 } else {
3351 None
3352 };
3353
3354 let saved_y = cursor.y;
3355 cursor.y = row_start_y + y_offset;
3356
3357 let mut item_cursor = cursor.clone();
3390 let mut item_pages: Vec<LayoutPage> = Vec::new();
3391
3392 self.layout_node(
3393 item.node,
3394 &mut item_cursor,
3395 &mut item_pages,
3396 x,
3397 available_width,
3398 parent_style,
3399 font_context,
3400 cross_h,
3401 Some(fw),
3402 );
3403
3404 if parallel {
3405 let final_y = item_cursor.y;
3408 let tail = std::mem::take(&mut item_cursor.elements);
3409 item_frags.push(ItemFragments {
3410 pages: item_pages,
3411 tail,
3412 cursor: item_cursor,
3413 final_y,
3414 stretches: matches!(align, AlignItems::Stretch)
3415 && matches!(item.style.height, SizeConstraint::Auto)
3416 && !has_auto_v,
3417 });
3418 } else {
3419 pages.extend(item_pages);
3421 *cursor = item_cursor;
3422 }
3423
3424 cursor.y = saved_y;
3425 x += fw;
3426 }
3427
3428 if !parallel {
3435 if line_items.len() > 1 && pages.len() > line_start_pages {
3440 let near = line_items
3441 .iter()
3442 .find_map(|it| first_text_snippet(it.node))
3443 .map(|t| format!(" (row beginning \"{t}\")"))
3444 .unwrap_or_default();
3445 self.defect(format!(
3446 "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}"
3447 ));
3448 }
3449 cursor.y = row_start_y + line_height;
3450 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3451 continue;
3452 }
3453
3454 let frag_count = item_frags
3455 .iter()
3456 .map(|f| f.pages.len() + 1)
3457 .max()
3458 .unwrap_or(1);
3459
3460 let mut empty_bands: Vec<(usize, LayoutElement)> = Vec::new();
3472 if frag_count > 1 {
3473 let page_bottom = cursor.content_y + cursor.content_height;
3474 let last = frag_count - 1;
3475 let row_end = cursor.content_y + end_y_of(&item_frags, last);
3476 for f in item_frags.iter_mut() {
3477 if !f.stretches {
3478 continue;
3479 }
3480 let own_last = f.pages.len();
3481 for (k, page) in f.pages.iter_mut().enumerate() {
3482 let bottom = if k == last { row_end } else { page_bottom };
3483 stretch_fragment(&mut page.elements, bottom);
3484 }
3485 let bottom = if own_last == last {
3486 row_end
3487 } else {
3488 page_bottom
3489 };
3490 stretch_fragment(&mut f.tail, bottom);
3491
3492 if own_last < last {
3505 let template = f
3506 .tail
3507 .iter()
3508 .chain(f.pages.iter().flat_map(|p| p.elements.iter()))
3509 .find(|el| paints_a_box(el))
3510 .cloned();
3511 if let Some(t) = template {
3512 for k in (own_last + 1)..=last {
3513 let bottom = if k == last { row_end } else { page_bottom };
3514 let mut band = t.clone();
3515 band.children.clear();
3516 band.y = cursor.content_y;
3517 band.height = (bottom - cursor.content_y).max(0.0);
3518 empty_bands.push((k, band));
3522 }
3523 }
3524 }
3525 }
3526 }
3527
3528 let mut merged: Vec<Vec<LayoutElement>> = vec![Vec::new(); frag_count];
3529 merged[0] = base_page_elements;
3531 for (k, band) in empty_bands {
3534 merged[k].push(band);
3535 }
3536 for f in item_frags.iter_mut() {
3537 for (k, page) in f.pages.iter_mut().enumerate() {
3538 merged[k].append(&mut page.elements);
3539 }
3540 let tail_index = f.pages.len();
3543 let mut tail = std::mem::take(&mut f.tail);
3544 merged[tail_index].append(&mut tail);
3545 }
3546
3547 if frag_count == 1 {
3548 cursor.elements = std::mem::take(&mut merged[0]);
3552 cursor.y = row_start_y + line_height;
3553 } else {
3554 let carrier_idx = item_frags
3565 .iter()
3566 .enumerate()
3567 .max_by_key(|(_, f)| f.pages.len())
3568 .map(|(i, _)| i)
3569 .expect("a flex line has at least one item");
3570 let mut carrier = item_frags.swap_remove(carrier_idx);
3571
3572 for (k, page) in carrier.pages.iter_mut().enumerate() {
3573 page.elements = std::mem::take(&mut merged[k]);
3574 }
3575 pages.append(&mut carrier.pages);
3576
3577 let last = frag_count - 1;
3580 let end_y = item_frags
3581 .iter()
3582 .chain(std::iter::once(&carrier))
3583 .filter(|f| f.pages.len() == last)
3584 .map(|f| f.final_y)
3585 .fold(f64::MIN, f64::max);
3586
3587 let mut merged_tail = std::mem::take(&mut merged[last]);
3588 *cursor = carrier.cursor;
3589 cursor.elements.clear();
3590 cursor.elements.append(&mut merged_tail);
3591 cursor.y = end_y;
3592 }
3593
3594 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3595 }
3596
3597 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3599 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3600 if !matches!(align_content, AlignContent::FlexStart)
3601 && !matches!(flex_wrap, FlexWrap::NoWrap)
3602 {
3603 if let Some(parent) = parent_style {
3604 if let SizeConstraint::Fixed(container_h) = parent.height {
3605 let inner_h = container_h
3606 - parent.padding.vertical()
3607 - parent.border_width.vertical();
3608 let total_used = cursor.y - flex_start_y;
3609 let slack = inner_h - total_used;
3610 if slack > 0.0 {
3611 let n = line_infos.len();
3612 let offsets: Vec<f64> = match align_content {
3613 AlignContent::FlexEnd => vec![slack; n],
3614 AlignContent::Center => vec![slack / 2.0; n],
3615 AlignContent::SpaceBetween => {
3616 if n <= 1 {
3617 vec![0.0; n]
3618 } else {
3619 let per_gap = slack / (n - 1) as f64;
3620 (0..n).map(|i| i as f64 * per_gap).collect()
3621 }
3622 }
3623 AlignContent::SpaceAround => {
3624 let space = slack / n as f64;
3625 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3626 }
3627 AlignContent::SpaceEvenly => {
3628 let space = slack / (n + 1) as f64;
3629 (0..n).map(|i| (i + 1) as f64 * space).collect()
3630 }
3631 AlignContent::Stretch => {
3632 let extra = slack / n as f64;
3633 (0..n).map(|i| i as f64 * extra).collect()
3634 }
3635 AlignContent::FlexStart => vec![0.0; n],
3636 };
3637 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3638 let dy = offsets[i];
3639 if dy.abs() > 0.001 {
3640 for j in start..end {
3641 offset_element_y(&mut cursor.elements[j], dy);
3642 }
3643 }
3644 }
3645 cursor.y += *offsets.last().unwrap_or(&0.0);
3646 }
3647 }
3648 }
3649 }
3650 }
3651 }
3652
3653 #[allow(clippy::too_many_arguments)]
3656 fn layout_list(
3657 &self,
3658 node: &Node,
3659 ordered: bool,
3660 marker_type: ListMarkerType,
3661 start: u32,
3662 style: &ResolvedStyle,
3663 cursor: &mut PageCursor,
3664 pages: &mut Vec<LayoutPage>,
3665 x: f64,
3666 available_width: f64,
3667 font_context: &FontContext,
3668 ) {
3669 let margin = &style.margin.to_edges();
3670 let padding = &style.padding;
3671
3672 cursor.y += margin.top;
3673
3674 let list_x = x + margin.left;
3675 let outer_width = available_width - margin.horizontal();
3676 let inner_width = outer_width - padding.horizontal();
3677
3678 let n_items = node
3681 .children
3682 .iter()
3683 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3684 .count() as u32;
3685
3686 let marker_gutter =
3687 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3688
3689 let list_inner_x = list_x + padding.left;
3690 let content_x = list_inner_x + marker_gutter;
3691 let content_width = (inner_width - marker_gutter).max(0.0);
3692
3693 let snapshot = cursor.elements.len();
3696 let list_start_y = cursor.content_y + cursor.y;
3697 cursor.y += padding.top;
3698
3699 let mut item_index: u32 = 0;
3700 for child in &node.children {
3701 if !matches!(child.kind, NodeKind::ListItem) {
3702 continue;
3703 }
3704 let marker_idx = start + item_index;
3705 self.layout_list_item(
3706 child,
3707 marker_idx,
3708 ordered,
3709 marker_type,
3710 marker_gutter,
3711 style,
3712 cursor,
3713 pages,
3714 list_inner_x,
3715 content_x,
3716 content_width,
3717 font_context,
3718 );
3719 item_index += 1;
3720 }
3721
3722 cursor.y += padding.bottom;
3723
3724 let item_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
3726 let list_height = cursor.content_y + cursor.y - list_start_y;
3727 cursor.elements.push(LayoutElement {
3728 x: list_x,
3729 y: list_start_y,
3730 width: outer_width,
3731 height: list_height,
3732 draw: DrawCommand::None,
3733 children: item_elements,
3734 node_type: Some("List".to_string()),
3735 resolved_style: Some(style.clone()),
3736 source_location: node.source_location.clone(),
3737 href: None,
3738 bookmark: node.bookmark.clone(),
3739 alt: None,
3740 is_header_row: false,
3741 actual_text: None,
3745 list_numbering: match marker_type {
3746 ListMarkerType::None => None,
3747 ListMarkerType::Disc => Some("Disc"),
3748 ListMarkerType::Circle => Some("Circle"),
3749 ListMarkerType::Square => Some("Square"),
3750 ListMarkerType::Decimal => Some("Decimal"),
3751 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3752 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3753 ListMarkerType::LowerRoman => Some("LowerRoman"),
3754 ListMarkerType::UpperRoman => Some("UpperRoman"),
3755 },
3756 col_span: 1,
3757 overflow: style.overflow,
3758 opacity: style.opacity,
3759 });
3760
3761 cursor.y += margin.bottom;
3762 }
3763
3764 #[allow(clippy::too_many_arguments)]
3765 fn layout_list_item(
3766 &self,
3767 item: &Node,
3768 marker_idx: u32,
3769 ordered: bool,
3770 marker_type: ListMarkerType,
3771 marker_gutter: f64,
3772 parent_style: &ResolvedStyle,
3773 cursor: &mut PageCursor,
3774 pages: &mut Vec<LayoutPage>,
3775 list_inner_x: f64,
3776 content_x: f64,
3777 content_width: f64,
3778 font_context: &FontContext,
3779 ) {
3780 let item_style = item.style.resolve(Some(parent_style), content_width);
3781 let item_margin = item_style.margin.to_edges();
3782
3783 cursor.y += item_margin.top;
3784 let item_start_y = cursor.content_y + cursor.y;
3785 let item_snapshot = cursor.elements.len();
3786
3787 let marker_str = format_marker(marker_idx, ordered, marker_type);
3791 if !marker_str.is_empty() {
3792 let saved_y = cursor.y;
3793 self.layout_text(
3794 &marker_str,
3795 None,
3796 &[],
3797 &item_style,
3798 cursor,
3799 pages,
3800 list_inner_x,
3801 marker_gutter,
3802 font_context,
3803 None,
3804 None,
3805 Some("Lbl"),
3806 );
3807 cursor.y = saved_y;
3808 }
3809
3810 self.layout_children(
3816 &item.children,
3817 &item.style,
3818 cursor,
3819 pages,
3820 content_x,
3821 content_width,
3822 Some(&item_style),
3823 font_context,
3824 );
3825
3826 let item_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, item_snapshot);
3829 let item_height = cursor.content_y + cursor.y - item_start_y;
3830 let item_width = content_x + content_width - list_inner_x;
3831 cursor.elements.push(LayoutElement {
3832 x: list_inner_x,
3833 y: item_start_y,
3834 width: item_width,
3835 height: item_height,
3836 draw: DrawCommand::None,
3837 children: item_children,
3838 node_type: Some("ListItem".to_string()),
3839 resolved_style: Some(item_style.clone()),
3840 source_location: item.source_location.clone(),
3841 href: None,
3842 bookmark: item.bookmark.clone(),
3843 alt: None,
3844 is_header_row: false,
3845 actual_text: None,
3846 list_numbering: None,
3847 col_span: 1,
3848 overflow: item_style.overflow,
3849 opacity: item_style.opacity,
3850 });
3851
3852 cursor.y += item_margin.bottom;
3853 }
3854
3855 #[allow(clippy::too_many_arguments)]
3856 fn layout_table(
3857 &self,
3858 node: &Node,
3859 style: &ResolvedStyle,
3860 column_defs: &[ColumnDef],
3861 cursor: &mut PageCursor,
3862 pages: &mut Vec<LayoutPage>,
3863 x: f64,
3864 available_width: f64,
3865 font_context: &FontContext,
3866 ) {
3867 let padding = &style.padding;
3868 let margin = &style.margin.to_edges();
3869 let border = &style.border_width;
3870
3871 let table_x = x + margin.left;
3872 let table_width = match style.width {
3873 SizeConstraint::Fixed(w) => w,
3874 SizeConstraint::Auto => available_width - margin.horizontal(),
3875 };
3876 let inner_width = table_width - padding.horizontal() - border.horizontal();
3877
3878 let col_widths = self.resolve_column_widths(
3879 column_defs,
3880 inner_width,
3881 &node.children,
3882 style,
3883 font_context,
3884 );
3885
3886 let all_offsets = Self::table_column_offsets(&node.children);
3890 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
3891 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
3892
3893 for (child, offs) in node.children.iter().zip(&all_offsets) {
3894 match &child.kind {
3895 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
3896 _ => body_rows.push((child, offs)),
3897 }
3898 }
3899
3900 if !style.breakable {
3906 let total_height: f64 = node
3907 .children
3908 .iter()
3909 .zip(&all_offsets)
3910 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3911 .sum::<f64>()
3912 + padding.vertical()
3913 + border.vertical();
3914 let fresh_page_available = cursor.content_height
3915 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3916 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3917 if total_height > cursor.remaining_height()
3918 && total_height <= fresh_page_available
3919 && cursor.y > 0.0
3920 {
3921 pages.push(cursor.finalize());
3922 *cursor = cursor.new_page();
3923 }
3924 }
3925
3926 let initial_page_count = pages.len();
3933 let snapshot = cursor.elements.len();
3934 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3935
3936 cursor.y += margin.top + padding.top + border.top;
3937
3938 let cell_x_start = table_x + padding.left + border.left;
3939
3940 if !header_rows.is_empty() {
3965 let total_header_h: f64 = header_rows
3966 .iter()
3967 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3968 .sum();
3969 let first_body_h = body_rows
3970 .first()
3971 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
3972 .unwrap_or(0.0);
3973
3974 let needed = total_header_h + first_body_h;
3975 let fresh_page_available = cursor.content_height
3976 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3977 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3978
3979 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3980 pages.push(cursor.finalize());
3981 *cursor = cursor.new_page();
3982 cursor.y += padding.top + border.top;
3983 }
3984 }
3985
3986 for (header_row, offs) in &header_rows {
3987 self.layout_table_row(
3988 header_row,
3989 &col_widths,
3990 offs,
3991 style,
3992 cursor,
3993 cell_x_start,
3994 font_context,
3995 pages,
3996 );
3997 }
3998
3999 for (body_row, offs) in &body_rows {
4000 let row_height =
4001 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
4002
4003 let fresh_page_available = cursor.content_height
4011 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
4012 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
4013 if row_height > fresh_page_available {
4014 self.defect(format!(
4015 "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",
4016 ));
4017 }
4018 if row_height > cursor.remaining_height()
4019 && cursor.remaining_height() < fresh_page_available - 0.5
4020 {
4021 pages.push(cursor.finalize());
4022 *cursor = cursor.new_page();
4023
4024 cursor.y += padding.top + border.top;
4025 for (header_row, h_offs) in &header_rows {
4026 self.layout_table_row(
4027 header_row,
4028 &col_widths,
4029 h_offs,
4030 style,
4031 cursor,
4032 cell_x_start,
4033 font_context,
4034 pages,
4035 );
4036 }
4037 }
4038
4039 self.layout_table_row(
4040 body_row,
4041 &col_widths,
4042 offs,
4043 style,
4044 cursor,
4045 cell_x_start,
4046 font_context,
4047 pages,
4048 );
4049 }
4050
4051 let has_visual = style.background_color.is_some()
4055 || style.background.is_some()
4056 || style.border_width.top > 0.0
4057 || style.border_width.right > 0.0
4058 || style.border_width.bottom > 0.0
4059 || style.border_width.left > 0.0;
4060 let draw_cmd = if has_visual {
4061 DrawCommand::Rect {
4062 background: style.background_color,
4063 border_width: style.border_width,
4064 border_color: style.border_color,
4065 border_style: style.border_style,
4066 border_radius: style.border_radius,
4067 opacity: 1.0,
4068 box_shadow: style.box_shadow.map(Box::new),
4069 background_gradient: style.background.clone().map(Box::new),
4070 }
4071 } else {
4072 DrawCommand::None
4073 };
4074 let make_wrapper =
4075 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
4076 x: table_x,
4077 y,
4078 width: table_width,
4079 height,
4080 draw,
4081 children,
4082 node_type: Some(node_kind_name(&node.kind).to_string()),
4083 resolved_style: Some(style.clone()),
4084 source_location: node.source_location.clone(),
4085 href: node.href.clone(),
4086 bookmark: None,
4087 alt: None,
4088 is_header_row: false,
4089 actual_text: None,
4090 list_numbering: None,
4091 col_span: 1,
4092 overflow: Overflow::default(),
4093 opacity: style.opacity,
4094 };
4095
4096 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
4097
4098 if pages.len() == initial_page_count {
4099 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4101 cursor.elements.push(make_wrapper(
4102 rect_start_y,
4103 table_bottom_y - rect_start_y,
4104 child_elements,
4105 draw_cmd,
4106 ));
4107 } else {
4108 let page = &mut pages[initial_page_count];
4127 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
4128 let page_content_bottom =
4129 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
4130 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
4131 if !our_elements.is_empty() {
4132 page.elements.push(make_wrapper(
4133 rect_start_y,
4134 page_content_bottom - rect_start_y,
4135 our_elements,
4136 draw_cmd.clone(),
4137 ));
4138 }
4139
4140 for page in &mut pages[initial_page_count + 1..] {
4142 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
4143 let content_top = page.config.margin.top + header_h;
4144 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
4145 let content_bottom = page.config.margin.top
4146 + (page.height - page.config.margin.vertical())
4147 - footer_h;
4148 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
4149 if !all_elements.is_empty() {
4150 page.elements.push(make_wrapper(
4151 content_top,
4152 content_bottom - content_top,
4153 all_elements,
4154 draw_cmd.clone(),
4155 ));
4156 }
4157 }
4158
4159 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
4161 if !all_elements.is_empty() {
4162 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
4163 let content_top = cursor.content_y + header_h;
4164 cursor.elements.push(make_wrapper(
4165 content_top,
4166 table_bottom_y - content_top,
4167 all_elements,
4168 draw_cmd,
4169 ));
4170 }
4171 }
4172
4173 cursor.y += padding.bottom + border.bottom + margin.bottom;
4174 }
4175
4176 fn subtree_forces_break(node: &Node) -> bool {
4186 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
4187 return true;
4188 }
4189 if node.style.break_before == Some(true) {
4190 return true;
4191 }
4192 node.children.iter().any(Self::subtree_forces_break)
4193 }
4194
4195 fn occupancy_column_count(children: &[Node]) -> usize {
4201 let offsets = Self::table_column_offsets(children);
4202 children
4203 .iter()
4204 .zip(&offsets)
4205 .map(|(row, offs)| {
4206 row.children
4207 .iter()
4208 .zip(offs)
4209 .map(|(cell, &start)| start + Self::cell_col_span(cell))
4210 .max()
4211 .unwrap_or(0)
4212 })
4213 .max()
4214 .unwrap_or(1)
4215 .max(1)
4216 }
4217
4218 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
4228 fn spans(cell: &Node) -> (usize, u32) {
4229 match &cell.kind {
4230 NodeKind::TableCell { col_span, row_span } => {
4231 ((*col_span).max(1) as usize, (*row_span).max(1))
4232 }
4233 _ => (1, 1),
4234 }
4235 }
4236 let mut pending: Vec<u32> = Vec::new();
4237 let mut out = Vec::with_capacity(rows.len());
4238 for row in rows {
4239 let mut offsets = Vec::with_capacity(row.children.len());
4240 let mut col = 0usize;
4241 for cell in &row.children {
4242 let (span, rspan) = spans(cell);
4243 while pending.get(col).copied().unwrap_or(0) > 0 {
4244 col += 1;
4245 }
4246 offsets.push(col);
4247 if rspan > 1 {
4248 if pending.len() < col + span {
4249 pending.resize(col + span, 0);
4250 }
4251 for slot in pending.iter_mut().take(col + span).skip(col) {
4252 *slot = (*slot).max(rspan);
4253 }
4254 }
4255 col += span;
4256 }
4257 out.push(offsets);
4258 for p in pending.iter_mut() {
4259 *p = p.saturating_sub(1);
4260 }
4261 }
4262 out
4263 }
4264
4265 #[allow(clippy::too_many_arguments)]
4266 fn layout_table_row(
4267 &self,
4268 row: &Node,
4269 col_widths: &[f64],
4270 col_offsets: &[usize],
4271 parent_style: &ResolvedStyle,
4272 cursor: &mut PageCursor,
4273 start_x: f64,
4274 font_context: &FontContext,
4275 pages: &mut Vec<LayoutPage>,
4276 ) {
4277 let row_style = row
4278 .style
4279 .resolve(Some(parent_style), col_widths.iter().sum());
4280
4281 let row_height =
4282 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
4283 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
4284 let row_y = cursor.content_y + cursor.y;
4285 let total_width: f64 = col_widths.iter().sum();
4286
4287 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
4288
4289 let row_snapshot = cursor.elements.len();
4291
4292 let row_may_break =
4304 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
4305
4306 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
4307 for (cell_i, cell) in row.children.iter().enumerate() {
4311 let span = match &cell.kind {
4312 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
4313 _ => 1,
4314 };
4315 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
4316 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
4317 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
4318
4319 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4320
4321 let cell_snapshot = cursor.elements.len();
4323
4324 let inner_width =
4325 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4326
4327 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4328 let saved_y = cursor.y;
4329 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4330
4331 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4335 let content_h: f64 = cell
4336 .children
4337 .iter()
4338 .map(|ch| {
4339 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4340 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4341 })
4342 .sum();
4343 let inner_row =
4344 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4345 let slack = (inner_row - content_h).max(0.0);
4346 cursor.y += match cell_style.vertical_align {
4347 crate::style::VerticalAlign::Middle => slack / 2.0,
4348 crate::style::VerticalAlign::Bottom => slack,
4349 crate::style::VerticalAlign::Baseline => row_bl
4354 .map(|b| {
4355 let d = self.cell_baseline_distance(
4356 cell,
4357 &cell_style,
4358 inner_width,
4359 font_context,
4360 );
4361 (b - d).max(0.0)
4362 })
4363 .unwrap_or(0.0),
4364 crate::style::VerticalAlign::Top => 0.0,
4365 };
4366 }
4367
4368 let cursor_before_cell = if row_may_break {
4373 Some(cursor.clone())
4374 } else {
4375 None
4376 };
4377 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4378 for child in &cell.children {
4379 self.layout_node(
4380 child,
4381 cursor,
4382 &mut cell_pages,
4383 content_x,
4384 inner_width,
4385 Some(&cell_style),
4386 font_context,
4387 None,
4388 None,
4389 );
4390 }
4391
4392 if !cell_pages.is_empty() {
4394 let post_break_elements = std::mem::take(&mut cursor.elements);
4395 if let Some(last_page) = cell_pages.last_mut() {
4396 last_page.elements.extend(post_break_elements);
4397 }
4398 if !is_header {
4404 all_overflow_pages.extend(cell_pages);
4405 }
4406 *cursor = cursor_before_cell
4412 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4413 }
4414
4415 cursor.y = saved_y;
4416
4417 let cell_children: Vec<LayoutElement> =
4419 drain_since(&mut cursor.elements, cell_snapshot);
4420
4421 cursor.elements.push(LayoutElement {
4423 x: cell_x,
4424 y: row_y,
4425 width: col_width,
4426 height: row_height,
4427 draw: if cell_style.background_color.is_some()
4428 || cell_style.border_width.horizontal() > 0.0
4429 || cell_style.border_width.vertical() > 0.0
4430 {
4431 DrawCommand::Rect {
4432 background: cell_style.background_color,
4433 border_width: cell_style.border_width,
4434 border_color: cell_style.border_color,
4435 border_style: cell_style.border_style,
4436 border_radius: cell_style.border_radius,
4437 opacity: 1.0,
4438 box_shadow: cell_style.box_shadow.map(Box::new),
4439 background_gradient: cell_style.background.clone().map(Box::new),
4440 }
4441 } else {
4442 DrawCommand::None
4443 },
4444 children: cell_children,
4445 node_type: Some("TableCell".to_string()),
4446 resolved_style: Some(cell_style.clone()),
4447 source_location: cell.source_location.clone(),
4448 href: None,
4449 bookmark: cell.bookmark.clone(),
4450 alt: None,
4451 is_header_row: is_header,
4452 actual_text: None,
4453 list_numbering: None,
4454 col_span: span as u32,
4455 overflow: Overflow::default(),
4456 opacity: 1.0,
4457 });
4458 }
4459
4460 let row_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, row_snapshot);
4462 let row_element = LayoutElement {
4463 x: start_x,
4464 y: row_y,
4465 width: total_width,
4466 height: row_height,
4467 draw: if let Some(bg) = row_style.background_color {
4468 DrawCommand::Rect {
4469 background: Some(bg),
4470 border_width: Edges::default(),
4471 border_color: EdgeValues::uniform(Color::BLACK),
4472 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4473 border_radius: CornerValues::uniform(0.0),
4474 opacity: 1.0,
4475 box_shadow: row_style.box_shadow.map(Box::new),
4476 background_gradient: row_style.background.clone().map(Box::new),
4477 }
4478 } else {
4479 DrawCommand::None
4480 },
4481 children: row_children,
4482 node_type: Some("TableRow".to_string()),
4483 resolved_style: Some(row_style.clone()),
4484 source_location: row.source_location.clone(),
4485 href: None,
4486 bookmark: row.bookmark.clone(),
4487 alt: None,
4488 is_header_row: is_header,
4489 actual_text: None,
4490 list_numbering: None,
4491 col_span: 1,
4492 overflow: row_style.overflow,
4493 opacity: row_style.opacity,
4494 };
4495
4496 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4497 first_overflow.elements.push(row_element);
4507 } else {
4508 cursor.elements.push(row_element);
4509 cursor.y += row_height;
4510 }
4511
4512 pages.extend(all_overflow_pages);
4514 }
4515
4516 #[allow(clippy::too_many_arguments)]
4517 #[allow(clippy::too_many_arguments)]
4518 fn layout_text(
4519 &self,
4520 content: &str,
4521 href: Option<&str>,
4522 runs: &[TextRun],
4523 style: &ResolvedStyle,
4524 cursor: &mut PageCursor,
4525 pages: &mut Vec<LayoutPage>,
4526 x: f64,
4527 available_width: f64,
4528 font_context: &FontContext,
4529 source_location: Option<&SourceLocation>,
4530 bookmark: Option<&str>,
4531 node_type_override: Option<&str>,
4535 ) {
4536 if style.border_width.top > 0.0
4543 || style.border_width.right > 0.0
4544 || style.border_width.bottom > 0.0
4545 || style.border_width.left > 0.0
4546 {
4547 self.defect(
4548 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4549 );
4550 }
4551 if style.background_color.is_some() {
4552 self.defect(
4553 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4554 );
4555 }
4556 let margin = &style.margin.to_edges();
4557 let text_x = x + margin.left;
4558 let text_width = match style.width {
4565 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4566 SizeConstraint::Auto => available_width - margin.horizontal(),
4567 };
4568 let text_width = text_width.min(style.max_width).max(style.min_width);
4579
4580 cursor.y += margin.top;
4581
4582 if !runs.is_empty() {
4584 self.layout_text_runs(
4585 runs,
4586 href,
4587 style,
4588 cursor,
4589 pages,
4590 text_x,
4591 text_width,
4592 font_context,
4593 source_location,
4594 bookmark,
4595 node_type_override,
4596 );
4597 cursor.y += margin.bottom;
4598 return;
4599 }
4600
4601 let content = substitute_page_placeholders(content);
4602 let transformed = apply_text_transform(&content, style.text_transform);
4603 let justify = matches!(style.text_align, TextAlign::Justify);
4604 let lines = match style.line_breaking {
4605 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4606 font_context,
4607 &transformed,
4608 text_width,
4609 style.font_size,
4610 &style.font_family,
4611 style.font_weight,
4612 style.font_style,
4613 style.letter_spacing,
4614 style.word_spacing,
4615 style.hyphens,
4616 style.lang.as_deref(),
4617 justify,
4618 ),
4619 LineBreaking::Greedy => self.text_layout.break_into_lines(
4620 font_context,
4621 &transformed,
4622 text_width,
4623 style.font_size,
4624 &style.font_family,
4625 style.font_weight,
4626 style.font_style,
4627 style.letter_spacing,
4628 style.word_spacing,
4629 style.hyphens,
4630 style.lang.as_deref(),
4631 ),
4632 };
4633
4634 let lines = match style.text_overflow {
4636 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4637 font_context,
4638 lines,
4639 text_width,
4640 style.font_size,
4641 &style.font_family,
4642 style.font_weight,
4643 style.font_style,
4644 style.letter_spacing,
4645 style.word_spacing,
4646 ),
4647 TextOverflow::Clip => self.text_layout.truncate_clip(
4648 font_context,
4649 lines,
4650 text_width,
4651 style.font_size,
4652 &style.font_family,
4653 style.font_weight,
4654 style.font_style,
4655 style.letter_spacing,
4656 style.word_spacing,
4657 ),
4658 TextOverflow::Wrap => lines,
4659 };
4660
4661 let line_height = style.font_size * style.line_height;
4662
4663 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4665 let decision = page_break::decide_break(
4666 cursor.remaining_height(),
4667 &line_heights,
4668 true,
4669 style.min_orphan_lines as usize,
4670 style.min_widow_lines as usize,
4671 );
4672
4673 let mut snapshot = cursor.elements.len();
4675 let mut container_start_y = cursor.content_y + cursor.y;
4676 let mut is_first_element = true;
4677
4678 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4680 pages.push(cursor.finalize());
4681 *cursor = cursor.new_page();
4682 snapshot = cursor.elements.len();
4683 container_start_y = cursor.content_y + cursor.y;
4684 }
4685
4686 let forced_break_at = match decision {
4688 page_break::BreakDecision::Split {
4689 items_on_current_page,
4690 } => Some(items_on_current_page),
4691 _ => None,
4692 };
4693 let mut first_break_done = false;
4694
4695 for (line_idx, line) in lines.iter().enumerate() {
4696 let needs_break = if let Some(break_at) = forced_break_at {
4698 if !first_break_done && line_idx == break_at {
4699 true
4700 } else {
4701 line_height > cursor.remaining_height()
4702 }
4703 } else {
4704 line_height > cursor.remaining_height()
4705 };
4706
4707 if needs_break {
4708 first_break_done = true;
4709 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4711 if !line_elements.is_empty() {
4712 let container_height = cursor.content_y + cursor.y - container_start_y;
4713 cursor.elements.push(LayoutElement {
4714 x: text_x,
4715 y: container_start_y,
4716 width: text_width,
4717 height: container_height,
4718 draw: DrawCommand::None,
4719 children: line_elements,
4720 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4721 resolved_style: Some(style.clone()),
4722 source_location: source_location.cloned(),
4723 href: href.map(|s| s.to_string()),
4724 bookmark: if is_first_element {
4725 bookmark.map(|s| s.to_string())
4726 } else {
4727 None
4728 },
4729 alt: None,
4730 is_header_row: false,
4731 actual_text: None,
4732 list_numbering: None,
4733 col_span: 1,
4734 overflow: Overflow::default(),
4735 opacity: 1.0,
4736 });
4737 is_first_element = false;
4738 }
4739
4740 pages.push(cursor.finalize());
4741 *cursor = cursor.new_page();
4742
4743 snapshot = cursor.elements.len();
4745 container_start_y = cursor.content_y + cursor.y;
4746 }
4747
4748 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4749
4750 let rendered_width = if glyphs.is_empty() {
4754 line.width
4755 } else {
4756 let last = &glyphs[glyphs.len() - 1];
4757 (last.x_offset + last.x_advance).max(line.width * 0.5)
4758 };
4759
4760 let line_x = match style.text_align {
4761 TextAlign::Left => text_x,
4762 TextAlign::Right => text_x + text_width - rendered_width,
4763 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4764 TextAlign::Justify => text_x,
4765 };
4766
4767 let is_last_line = line_idx == lines.len() - 1;
4775 let user_ws = style.word_spacing;
4776 let (justified_width, word_spacing) =
4777 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4778 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4779 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4780 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4781 let s = glyphs[..=idx]
4782 .iter()
4783 .filter(|g| g.char_value == ' ')
4784 .count();
4785 (w, s)
4786 } else {
4787 (0.0, 0)
4788 };
4789 let slack = text_width - natural_width;
4790 let ws = if space_count > 0 && slack.abs() > 0.01 {
4791 slack / space_count as f64
4792 } else {
4793 0.0
4794 };
4795 (text_width, user_ws + ws)
4796 } else {
4797 (rendered_width, user_ws)
4798 };
4799
4800 let text_line = TextLine {
4801 x: line_x,
4802 y: cursor.content_y
4808 + cursor.y
4809 + baseline_in_line(
4810 line_height,
4811 style.font_size,
4812 font_context.baseline_metrics(
4813 &style.font_family,
4814 style.font_weight,
4815 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
4816 ),
4817 ),
4818 glyphs,
4819 width: justified_width,
4820 height: line_height,
4821 word_spacing,
4822 };
4823
4824 cursor.elements.push(LayoutElement {
4825 x: line_x,
4826 y: cursor.content_y + cursor.y,
4827 width: justified_width,
4828 height: line_height,
4829 draw: DrawCommand::Text {
4830 lines: vec![text_line],
4831 color: style.color,
4832 text_decoration: style.text_decoration,
4833 opacity: 1.0,
4834 },
4835 children: vec![],
4836 node_type: Some("TextLine".to_string()),
4837 resolved_style: Some(style.clone()),
4838 source_location: None,
4839 href: href.map(|s| s.to_string()),
4840 bookmark: None,
4841 alt: None,
4842 is_header_row: false,
4843 actual_text: None,
4844 list_numbering: None,
4845 col_span: 1,
4846 overflow: Overflow::default(),
4847 opacity: 1.0,
4848 });
4849
4850 cursor.y += line_height;
4851 }
4852
4853 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4855 if !line_elements.is_empty() {
4856 let container_height = cursor.content_y + cursor.y - container_start_y;
4857 cursor.elements.push(LayoutElement {
4858 x: text_x,
4859 y: container_start_y,
4860 width: text_width,
4861 height: container_height,
4862 draw: DrawCommand::None,
4863 children: line_elements,
4864 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4865 resolved_style: Some(style.clone()),
4866 source_location: source_location.cloned(),
4867 href: href.map(|s| s.to_string()),
4868 bookmark: if is_first_element {
4869 bookmark.map(|s| s.to_string())
4870 } else {
4871 None
4872 },
4873 alt: None,
4874 is_header_row: false,
4875 actual_text: None,
4876 list_numbering: None,
4877 col_span: 1,
4878 overflow: Overflow::default(),
4879 opacity: 1.0,
4880 });
4881 }
4882
4883 cursor.y += margin.bottom;
4884 }
4885
4886 #[allow(clippy::too_many_arguments)]
4888 #[allow(clippy::too_many_arguments)]
4889 fn layout_text_runs(
4890 &self,
4891 runs: &[TextRun],
4892 parent_href: Option<&str>,
4893 style: &ResolvedStyle,
4894 cursor: &mut PageCursor,
4895 pages: &mut Vec<LayoutPage>,
4896 text_x: f64,
4897 text_width: f64,
4898 font_context: &FontContext,
4899 source_location: Option<&SourceLocation>,
4900 bookmark: Option<&str>,
4901 node_type_override: Option<&str>,
4903 ) {
4904 let mut styled_chars: Vec<StyledChar> = Vec::new();
4906 for run in runs {
4907 let run_style = run.style.resolve(Some(style), text_width);
4908 let run_href = run.href.as_deref().or(parent_href);
4909 let transform = run_style.text_transform;
4910 let run_content = substitute_page_placeholders(&run.content);
4911 let mut prev_is_whitespace = true;
4912 for ch in run_content.chars() {
4913 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
4914 prev_is_whitespace = ch.is_whitespace();
4915 styled_chars.push(StyledChar {
4916 ch: transformed_ch,
4917 font_family: run_style.font_family.clone(),
4918 font_size: run_style.font_size,
4919 font_weight: run_style.font_weight,
4920 font_style: run_style.font_style,
4921 color: run_style.color,
4922 href: run_href.map(|s| s.to_string()),
4923 text_decoration: run_style.text_decoration,
4924 letter_spacing: run_style.letter_spacing,
4925 word_spacing: run_style.word_spacing,
4926 });
4927 }
4928 }
4929
4930 let justify = matches!(style.text_align, TextAlign::Justify);
4932 let broken_lines = match style.line_breaking {
4933 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
4934 font_context,
4935 &styled_chars,
4936 text_width,
4937 style.hyphens,
4938 style.lang.as_deref(),
4939 justify,
4940 ),
4941 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
4942 font_context,
4943 &styled_chars,
4944 text_width,
4945 style.hyphens,
4946 style.lang.as_deref(),
4947 ),
4948 };
4949
4950 let broken_lines = match style.text_overflow {
4952 TextOverflow::Ellipsis => {
4953 self.text_layout
4954 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
4955 }
4956 TextOverflow::Clip => {
4957 self.text_layout
4958 .truncate_runs_clip(font_context, broken_lines, text_width)
4959 }
4960 TextOverflow::Wrap => broken_lines,
4961 };
4962
4963 let line_height = style.font_size * style.line_height;
4964
4965 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
4967 let decision = page_break::decide_break(
4968 cursor.remaining_height(),
4969 &line_heights,
4970 true,
4971 style.min_orphan_lines as usize,
4972 style.min_widow_lines as usize,
4973 );
4974
4975 let mut snapshot = cursor.elements.len();
4976 let mut container_start_y = cursor.content_y + cursor.y;
4977 let mut is_first_element = true;
4978
4979 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4980 pages.push(cursor.finalize());
4981 *cursor = cursor.new_page();
4982 snapshot = cursor.elements.len();
4983 container_start_y = cursor.content_y + cursor.y;
4984 }
4985
4986 let forced_break_at = match decision {
4987 page_break::BreakDecision::Split {
4988 items_on_current_page,
4989 } => Some(items_on_current_page),
4990 _ => None,
4991 };
4992 let mut first_break_done = false;
4993
4994 for (line_idx, run_line) in broken_lines.iter().enumerate() {
4995 let needs_break = if let Some(break_at) = forced_break_at {
4996 if !first_break_done && line_idx == break_at {
4997 true
4998 } else {
4999 line_height > cursor.remaining_height()
5000 }
5001 } else {
5002 line_height > cursor.remaining_height()
5003 };
5004
5005 if needs_break {
5006 first_break_done = true;
5007 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5008 if !line_elements.is_empty() {
5009 let container_height = cursor.content_y + cursor.y - container_start_y;
5010 cursor.elements.push(LayoutElement {
5011 x: text_x,
5012 y: container_start_y,
5013 width: text_width,
5014 height: container_height,
5015 draw: DrawCommand::None,
5016 children: line_elements,
5017 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5018 resolved_style: Some(style.clone()),
5019 source_location: source_location.cloned(),
5020 href: parent_href.map(|s| s.to_string()),
5021 bookmark: if is_first_element {
5022 bookmark.map(|s| s.to_string())
5023 } else {
5024 None
5025 },
5026 alt: None,
5027 is_header_row: false,
5028 actual_text: None,
5029 list_numbering: None,
5030 col_span: 1,
5031 overflow: Overflow::default(),
5032 opacity: 1.0,
5033 });
5034 is_first_element = false;
5035 }
5036
5037 pages.push(cursor.finalize());
5038 *cursor = cursor.new_page();
5039
5040 snapshot = cursor.elements.len();
5041 container_start_y = cursor.content_y + cursor.y;
5042 }
5043
5044 let line_x = match style.text_align {
5045 TextAlign::Left => text_x,
5046 TextAlign::Right => text_x + text_width - run_line.width,
5047 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
5048 TextAlign::Justify => text_x,
5049 };
5050
5051 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
5052
5053 let is_last_line = line_idx == broken_lines.len() - 1;
5060 let user_ws = style.word_spacing;
5061 let (justified_width, word_spacing) =
5062 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5063 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
5064 let (natural_width, space_count) = if let Some(idx) = last_non_space {
5065 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
5066 let s = glyphs[..=idx]
5067 .iter()
5068 .filter(|g| g.char_value == ' ')
5069 .count();
5070 (w, s)
5071 } else {
5072 (0.0, 0)
5073 };
5074 let slack = text_width - natural_width;
5075 let ws = if space_count > 0 && slack.abs() > 0.01 {
5076 slack / space_count as f64
5077 } else {
5078 0.0
5079 };
5080 (text_width, user_ws + ws)
5081 } else {
5082 (run_line.width, user_ws)
5083 };
5084
5085 let text_line = TextLine {
5086 x: line_x,
5087 y: cursor.content_y
5093 + cursor.y
5094 + baseline_in_line(
5095 line_height,
5096 style.font_size,
5097 font_context.baseline_metrics(
5098 &style.font_family,
5099 style.font_weight,
5100 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5101 ),
5102 ),
5103 glyphs,
5104 width: justified_width,
5105 height: line_height,
5106 word_spacing,
5107 };
5108
5109 let text_dec = run_line
5111 .chars
5112 .iter()
5113 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
5114 .map(|sc| sc.text_decoration)
5115 .unwrap_or(style.text_decoration);
5116
5117 cursor.elements.push(LayoutElement {
5118 x: line_x,
5119 y: cursor.content_y + cursor.y,
5120 width: justified_width,
5121 height: line_height,
5122 draw: DrawCommand::Text {
5123 lines: vec![text_line],
5124 color: style.color,
5125 text_decoration: text_dec,
5126 opacity: 1.0,
5127 },
5128 children: vec![],
5129 node_type: Some("TextLine".to_string()),
5130 resolved_style: Some(style.clone()),
5131 source_location: None,
5132 href: parent_href.map(|s| s.to_string()),
5133 bookmark: None,
5134 alt: None,
5135 is_header_row: false,
5136 actual_text: None,
5137 list_numbering: None,
5138 col_span: 1,
5139 overflow: Overflow::default(),
5140 opacity: 1.0,
5141 });
5142
5143 cursor.y += line_height;
5144 }
5145
5146 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5147 if !line_elements.is_empty() {
5148 let container_height = cursor.content_y + cursor.y - container_start_y;
5149 cursor.elements.push(LayoutElement {
5150 x: text_x,
5151 y: container_start_y,
5152 width: text_width,
5153 height: container_height,
5154 draw: DrawCommand::None,
5155 children: line_elements,
5156 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5157 resolved_style: Some(style.clone()),
5158 source_location: source_location.cloned(),
5159 href: parent_href.map(|s| s.to_string()),
5160 bookmark: if is_first_element {
5161 bookmark.map(|s| s.to_string())
5162 } else {
5163 None
5164 },
5165 alt: None,
5166 is_header_row: false,
5167 actual_text: None,
5168 list_numbering: None,
5169 col_span: 1,
5170 overflow: Overflow::default(),
5171 opacity: 1.0,
5172 });
5173 }
5174 }
5175
5176 fn build_positioned_glyphs_single_style(
5180 &self,
5181 line: &BrokenLine,
5182 style: &ResolvedStyle,
5183 href: Option<&str>,
5184 font_context: &FontContext,
5185 ) -> Vec<PositionedGlyph> {
5186 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5187 let line_text: String = line.chars.iter().collect();
5188 let direction = style.direction;
5189 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5191
5192 let font_runs = crate::font::fallback::segment_by_font(
5195 &line.chars,
5196 &style.font_family,
5197 style.font_weight,
5198 italic,
5199 font_context.registry(),
5200 );
5201 let needs_per_char_fallback = font_runs.len() > 1
5202 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
5203
5204 if needs_per_char_fallback {
5206 let bidi_runs = if has_bidi {
5207 bidi::analyze_bidi(&line_text, direction)
5208 } else {
5209 vec![crate::text::bidi::BidiRun {
5210 char_start: 0,
5211 char_end: line.chars.len(),
5212 level: unicode_bidi::Level::ltr(),
5213 is_rtl: false,
5214 }]
5215 };
5216
5217 let mut all_glyphs = Vec::new();
5218 let mut bidi_levels = Vec::new();
5219 let mut x = 0.0_f64;
5220
5221 for bidi_run in &bidi_runs {
5223 for font_run in &font_runs {
5225 let start = font_run.start.max(bidi_run.char_start);
5227 let end = font_run.end.min(bidi_run.char_end);
5228 if start >= end {
5229 continue;
5230 }
5231
5232 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
5233 let sub_text: String = sub_chars.iter().collect();
5234 let resolved_family = &font_run.family;
5235
5236 if let Some(font_data) =
5237 font_context.font_data(resolved_family, style.font_weight, italic)
5238 {
5239 if let Some(shaped) = shaping::shape_text_with_direction(
5240 &sub_text,
5241 font_data,
5242 bidi_run.is_rtl,
5243 ) {
5244 let units_per_em = font_context.units_per_em(
5245 resolved_family,
5246 style.font_weight,
5247 italic,
5248 );
5249 let scale = style.font_size / units_per_em as f64;
5250
5251 for sg in &shaped {
5252 let cluster = sg.cluster as usize;
5253 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
5254
5255 let cluster_text = if shaped.len() < sub_chars.len() {
5256 let cluster_end =
5257 self.find_cluster_end(&shaped, sg, sub_chars.len());
5258 if cluster_end > cluster + 1 {
5259 Some(
5260 sub_chars[cluster..cluster_end]
5261 .iter()
5262 .collect::<String>(),
5263 )
5264 } else {
5265 None
5266 }
5267 } else {
5268 None
5269 };
5270
5271 let glyph_x = x + sg.x_offset as f64 * scale;
5272 let glyph_y = sg.y_offset as f64 * scale;
5273 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5274
5275 all_glyphs.push(PositionedGlyph {
5276 glyph_id: sg.glyph_id,
5277 x_offset: glyph_x,
5278 y_offset: glyph_y,
5279 x_advance: advance,
5280 font_size: style.font_size,
5281 font_family: Arc::from(resolved_family.as_str()),
5282 font_weight: style.font_weight,
5283 font_style: style.font_style,
5284 char_value,
5285 color: Some(style.color),
5286 href: href.map(|s| s.to_string()),
5287 text_decoration: style.text_decoration,
5288 letter_spacing: style.letter_spacing,
5289 cluster_text,
5290 });
5291 bidi_levels.push(bidi_run.level);
5292 x += advance;
5293 }
5294 continue;
5295 }
5296 }
5297
5298 for i in start..end {
5300 let ch = line.chars[i];
5301 let glyph_x = x;
5302 let char_width = font_context.char_width(
5303 ch,
5304 resolved_family,
5305 style.font_weight,
5306 italic,
5307 style.font_size,
5308 );
5309 let advance = char_width + style.letter_spacing;
5310 all_glyphs.push(PositionedGlyph {
5311 glyph_id: ch as u16,
5312 x_offset: glyph_x,
5313 y_offset: 0.0,
5314 x_advance: advance,
5315 font_size: style.font_size,
5316 font_family: Arc::from(resolved_family.as_str()),
5317 font_weight: style.font_weight,
5318 font_style: style.font_style,
5319 char_value: ch,
5320 color: Some(style.color),
5321 href: href.map(|s| s.to_string()),
5322 text_decoration: style.text_decoration,
5323 letter_spacing: style.letter_spacing,
5324 cluster_text: None,
5325 });
5326 bidi_levels.push(bidi_run.level);
5327 x += advance;
5328 }
5329 }
5330 }
5331
5332 if has_bidi && !all_glyphs.is_empty() {
5334 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5335 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
5336 }
5337 return all_glyphs;
5338 }
5339
5340 if let Some(font_data) =
5343 font_context.font_data(&style.font_family, style.font_weight, italic)
5344 {
5345 if has_bidi {
5346 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5348 let units_per_em =
5349 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5350 let scale = style.font_size / units_per_em as f64;
5351
5352 let mut all_glyphs = Vec::new();
5353 let mut bidi_levels = Vec::new();
5354 let mut x = 0.0_f64;
5355
5356 for run in &bidi_runs {
5357 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5358 let run_text: String = run_chars.iter().collect();
5359
5360 if let Some(shaped) =
5361 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5362 {
5363 for sg in &shaped {
5364 let cluster = sg.cluster as usize;
5365 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5366
5367 let cluster_text = if shaped.len() < run_chars.len() {
5368 let cluster_end =
5369 self.find_cluster_end(&shaped, sg, run_chars.len());
5370 if cluster_end > cluster + 1 {
5371 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
5372 } else {
5373 None
5374 }
5375 } else {
5376 None
5377 };
5378
5379 let glyph_x = x + sg.x_offset as f64 * scale;
5380 let glyph_y = sg.y_offset as f64 * scale;
5381 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5382
5383 all_glyphs.push(PositionedGlyph {
5384 glyph_id: sg.glyph_id,
5385 x_offset: glyph_x,
5386 y_offset: glyph_y,
5387 x_advance: advance,
5388 font_size: style.font_size,
5389 font_family: Arc::from(style.font_family.as_str()),
5390 font_weight: style.font_weight,
5391 font_style: style.font_style,
5392 char_value,
5393 color: Some(style.color),
5394 href: href.map(|s| s.to_string()),
5395 text_decoration: style.text_decoration,
5396 letter_spacing: style.letter_spacing,
5397 cluster_text,
5398 });
5399 bidi_levels.push(run.level);
5400
5401 x += advance;
5402 }
5403 }
5404 }
5405
5406 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
5408 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5409 return glyphs;
5410 }
5411
5412 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5414 let units_per_em =
5415 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5416 let scale = style.font_size / units_per_em as f64;
5417
5418 return self.shaped_glyphs_to_positioned(
5419 &shaped,
5420 &line.chars,
5421 &line.char_positions,
5422 scale,
5423 style.font_size,
5424 &style.font_family,
5425 style.font_weight,
5426 style.font_style,
5427 Some(style.color),
5428 href,
5429 style.text_decoration,
5430 style.letter_spacing,
5431 );
5432 }
5433 }
5434
5435 let mut glyphs: Vec<PositionedGlyph> = line
5437 .chars
5438 .iter()
5439 .enumerate()
5440 .map(|(j, ch)| {
5441 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5442 let char_width = font_context.char_width(
5443 *ch,
5444 &style.font_family,
5445 style.font_weight,
5446 italic,
5447 style.font_size,
5448 );
5449 PositionedGlyph {
5450 glyph_id: *ch as u16,
5451 x_offset: glyph_x,
5452 y_offset: 0.0,
5453 x_advance: char_width,
5454 font_size: style.font_size,
5455 font_family: Arc::from(style.font_family.as_str()),
5456 font_weight: style.font_weight,
5457 font_style: style.font_style,
5458 char_value: *ch,
5459 color: Some(style.color),
5460 href: href.map(|s| s.to_string()),
5461 text_decoration: style.text_decoration,
5462 letter_spacing: style.letter_spacing,
5463 cluster_text: None,
5464 }
5465 })
5466 .collect();
5467
5468 if has_bidi && !glyphs.is_empty() {
5470 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5471 let mut levels = Vec::with_capacity(glyphs.len());
5472 let mut char_idx = 0;
5473 for run in &bidi_runs {
5474 for _ in run.char_start..run.char_end {
5475 if char_idx < glyphs.len() {
5476 levels.push(run.level);
5477 char_idx += 1;
5478 }
5479 }
5480 }
5481 while levels.len() < glyphs.len() {
5483 levels.push(unicode_bidi::Level::ltr());
5484 }
5485 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
5486 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5487 }
5488
5489 glyphs
5490 }
5491
5492 fn build_positioned_glyphs_runs(
5497 &self,
5498 run_line: &RunBrokenLine,
5499 font_context: &FontContext,
5500 direction: Direction,
5501 ) -> Vec<PositionedGlyph> {
5502 let chars = &run_line.chars;
5503 if chars.is_empty() {
5504 return vec![];
5505 }
5506
5507 let resolved_families: Vec<String> = chars
5516 .iter()
5517 .map(|sc| {
5518 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5519 if !sc.font_family.contains(',') {
5520 let primary =
5521 font_context
5522 .registry()
5523 .resolve(&sc.font_family, sc.font_weight, italic);
5524 if sc.ch.is_whitespace()
5525 || sc.ch == PAGE_NUMBER_SENTINEL
5526 || sc.ch == TOTAL_PAGES_SENTINEL
5527 || primary.has_char(sc.ch)
5528 {
5529 return sc.font_family.clone();
5530 }
5531 }
5532 let (_, family) = font_context.registry().resolve_for_char(
5533 &sc.font_family,
5534 sc.ch,
5535 sc.font_weight,
5536 italic,
5537 );
5538 family
5539 })
5540 .collect();
5541
5542 let line_text: String = chars.iter().map(|c| c.ch).collect();
5543 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5544 let bidi_runs = if has_bidi {
5545 Some(bidi::analyze_bidi(&line_text, direction))
5546 } else {
5547 None
5548 };
5549
5550 let mut glyphs = Vec::new();
5551 let mut bidi_levels = Vec::new();
5552 let mut i = 0;
5553
5554 while i < chars.len() {
5555 let sc = &chars[i];
5556 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5557 let resolved_family = &resolved_families[i];
5558
5559 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5561 runs.iter()
5562 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
5563 });
5564
5565 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5567 {
5568 let run_start = i;
5570 let mut run_end = i + 1;
5571 while run_end < chars.len() {
5572 let next = &chars[run_end];
5573 let next_italic =
5574 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5575 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
5576 runs.iter()
5577 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
5578 });
5579 if resolved_families[run_end] == *resolved_family
5581 && next.font_weight == sc.font_weight
5582 && next_italic == italic
5583 && (next.font_size - sc.font_size).abs() < 0.001
5584 && next_is_rtl == is_rtl
5585 {
5586 run_end += 1;
5587 } else {
5588 break;
5589 }
5590 }
5591
5592 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5593 if let Some(shaped) =
5594 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5595 {
5596 let units_per_em =
5597 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5598 let scale = sc.font_size / units_per_em as f64;
5599
5600 let run_chars: Vec<char> =
5602 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5603 let run_positions: Vec<f64> = (run_start..run_end)
5604 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5605 .collect();
5606
5607 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5609 &shaped,
5610 &chars[run_start..run_end],
5611 &run_chars,
5612 &run_positions,
5613 scale,
5614 );
5615 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5617 for g in &mut run_glyphs {
5618 g.font_family = resolved_family_arc.clone();
5619 }
5620 let run_level = if is_rtl {
5622 unicode_bidi::Level::rtl()
5623 } else {
5624 unicode_bidi::Level::ltr()
5625 };
5626 for _ in &run_glyphs {
5627 bidi_levels.push(run_level);
5628 }
5629 glyphs.extend(run_glyphs);
5630 i = run_end;
5631 continue;
5632 }
5633 }
5634
5635 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5637 let char_width = font_context.char_width(
5638 sc.ch,
5639 resolved_family,
5640 sc.font_weight,
5641 italic,
5642 sc.font_size,
5643 );
5644 glyphs.push(PositionedGlyph {
5645 glyph_id: sc.ch as u16,
5646 x_offset: glyph_x,
5647 y_offset: 0.0,
5648 x_advance: char_width,
5649 font_size: sc.font_size,
5650 font_family: Arc::from(resolved_family.as_str()),
5651 font_weight: sc.font_weight,
5652 font_style: sc.font_style,
5653 char_value: sc.ch,
5654 color: Some(sc.color),
5655 href: sc.href.clone(),
5656 text_decoration: sc.text_decoration,
5657 letter_spacing: sc.letter_spacing,
5658 cluster_text: None,
5659 });
5660 bidi_levels.push(if is_rtl {
5661 unicode_bidi::Level::rtl()
5662 } else {
5663 unicode_bidi::Level::ltr()
5664 });
5665 i += 1;
5666 }
5667
5668 if has_bidi && !glyphs.is_empty() {
5670 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
5671 bidi::reposition_after_reorder(&mut glyphs, 0.0);
5672 }
5673
5674 glyphs
5675 }
5676
5677 #[allow(clippy::too_many_arguments)]
5679 fn shaped_glyphs_to_positioned(
5680 &self,
5681 shaped: &[shaping::ShapedGlyph],
5682 chars: &[char],
5683 _char_positions: &[f64],
5684 scale: f64,
5685 font_size: f64,
5686 font_family: &str,
5687 font_weight: u32,
5688 font_style: FontStyle,
5689 color: Option<Color>,
5690 href: Option<&str>,
5691 text_decoration: TextDecoration,
5692 letter_spacing: f64,
5693 ) -> Vec<PositionedGlyph> {
5694 let mut result = Vec::with_capacity(shaped.len());
5695 let mut x = 0.0_f64;
5696
5697 for sg in shaped {
5698 let cluster = sg.cluster as usize;
5699 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5700
5701 let cluster_text = if shaped.len() < chars.len() {
5703 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5706 if cluster_end > cluster + 1 {
5707 Some(chars[cluster..cluster_end].iter().collect::<String>())
5708 } else {
5709 None
5710 }
5711 } else {
5712 None
5713 };
5714
5715 let glyph_x = x + sg.x_offset as f64 * scale;
5717 let glyph_y = sg.y_offset as f64 * scale;
5718 let advance = sg.x_advance as f64 * scale + letter_spacing;
5719
5720 result.push(PositionedGlyph {
5721 glyph_id: sg.glyph_id,
5722 x_offset: glyph_x,
5723 y_offset: glyph_y,
5724 x_advance: advance,
5725 font_size,
5726 font_family: Arc::from(font_family),
5727 font_weight,
5728 font_style,
5729 char_value,
5730 color,
5731 href: href.map(|s| s.to_string()),
5732 text_decoration,
5733 letter_spacing,
5734 cluster_text,
5735 });
5736
5737 x += advance;
5738 }
5739
5740 result
5741 }
5742
5743 fn shaped_glyphs_to_positioned_runs(
5745 &self,
5746 shaped: &[shaping::ShapedGlyph],
5747 styled_chars: &[StyledChar],
5748 chars: &[char],
5749 char_positions: &[f64],
5750 scale: f64,
5751 ) -> Vec<PositionedGlyph> {
5752 let mut result = Vec::with_capacity(shaped.len());
5753 let base_x = char_positions.first().copied().unwrap_or(0.0);
5755 let mut x = 0.0_f64;
5756
5757 for sg in shaped {
5758 let cluster = sg.cluster as usize;
5759 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5760 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5761
5762 let cluster_text = if shaped.len() < chars.len() {
5763 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
5764 if cluster_end > cluster + 1 {
5765 Some(chars[cluster..cluster_end].iter().collect::<String>())
5766 } else {
5767 None
5768 }
5769 } else {
5770 None
5771 };
5772
5773 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
5774 let glyph_y = sg.y_offset as f64 * scale;
5775 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5776
5777 result.push(PositionedGlyph {
5778 glyph_id: sg.glyph_id,
5779 x_offset: glyph_x,
5780 y_offset: glyph_y,
5781 x_advance: advance,
5782 font_size: sc.font_size,
5783 font_family: Arc::from(sc.font_family.as_str()),
5784 font_weight: sc.font_weight,
5785 font_style: sc.font_style,
5786 char_value,
5787 color: Some(sc.color),
5788 href: sc.href.clone(),
5789 text_decoration: sc.text_decoration,
5790 letter_spacing: sc.letter_spacing,
5791 cluster_text,
5792 });
5793
5794 x += advance;
5795 }
5796
5797 result
5798 }
5799
5800 fn find_cluster_end(
5802 &self,
5803 shaped: &[shaping::ShapedGlyph],
5804 current: &shaping::ShapedGlyph,
5805 num_chars: usize,
5806 ) -> usize {
5807 for sg in shaped {
5809 if sg.cluster > current.cluster {
5810 return sg.cluster as usize;
5811 }
5812 }
5813 num_chars
5815 }
5816
5817 fn image_display_size(
5827 &self,
5828 src: &str,
5829 style: &ResolvedStyle,
5830 explicit_width: Option<f64>,
5831 explicit_height: Option<f64>,
5832 available_width: f64,
5833 ) -> (f64, f64) {
5834 let dims = if src.is_empty() {
5835 None
5836 } else {
5837 self.get_image_dimensions(src)
5838 };
5839 let aspect = dims
5840 .map(|(w, h)| {
5841 if w > 0 {
5842 f64::from(h) / f64::from(w)
5843 } else {
5844 0.75
5845 }
5846 })
5847 .unwrap_or(0.75);
5848
5849 let style_w = match style.width {
5850 SizeConstraint::Fixed(w) => Some(w),
5851 SizeConstraint::Auto => None,
5852 };
5853 let style_h = match style.height {
5854 SizeConstraint::Fixed(h) => Some(h),
5855 SizeConstraint::Auto => None,
5856 };
5857 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5858
5859 let width_source = style_w.or(explicit_width);
5860 let height_source = style_h.or(explicit_height);
5861 match (width_source, height_source) {
5862 (Some(w), Some(h)) => (clamp(w), h),
5863 (Some(w), None) => {
5864 let w = clamp(w);
5865 (w, w * aspect)
5866 }
5867 (None, Some(h)) => (clamp(h / aspect), h),
5868 (None, None) => {
5869 let w = clamp(
5872 dims.map(|(w, _)| f64::from(w))
5873 .unwrap_or(available_width)
5874 .min(available_width),
5875 );
5876 (w, w * aspect)
5877 }
5878 }
5879 }
5880
5881 #[allow(clippy::too_many_arguments)]
5882 fn layout_image(
5883 &self,
5884 node: &Node,
5885 style: &ResolvedStyle,
5886 cursor: &mut PageCursor,
5887 pages: &mut Vec<LayoutPage>,
5888 x: f64,
5889 available_width: f64,
5890 explicit_width: Option<f64>,
5891 explicit_height: Option<f64>,
5892 ) {
5893 let margin = &style.margin.to_edges();
5894
5895 let src = match &node.kind {
5897 NodeKind::Image { src, .. } => src.as_str(),
5898 _ => "",
5899 };
5900
5901 let loaded = if !src.is_empty() {
5902 crate::image_loader::load_image(src).ok()
5903 } else {
5904 None
5905 };
5906
5907 let (img_width, img_height) = self.image_display_size(
5908 src,
5909 style,
5910 explicit_width,
5911 explicit_height,
5912 available_width - margin.horizontal(),
5913 );
5914
5915 let total_height = img_height + margin.vertical();
5916
5917 if total_height > cursor.remaining_height() {
5918 pages.push(cursor.finalize());
5919 *cursor = cursor.new_page();
5920 }
5921
5922 cursor.y += margin.top;
5923
5924 let draw = if let Some(image_data) = loaded {
5925 DrawCommand::Image { image_data }
5926 } else {
5927 DrawCommand::ImagePlaceholder
5928 };
5929
5930 cursor.elements.push(LayoutElement {
5931 x: x + margin.left,
5932 y: cursor.content_y + cursor.y,
5933 width: img_width,
5934 height: img_height,
5935 draw,
5936 children: vec![],
5937 node_type: Some(node_kind_name(&node.kind).to_string()),
5938 resolved_style: Some(style.clone()),
5939 source_location: node.source_location.clone(),
5940 href: node.href.clone(),
5941 bookmark: node.bookmark.clone(),
5942 alt: node.alt.clone(),
5943 is_header_row: false,
5944 actual_text: None,
5945 list_numbering: None,
5946 col_span: 1,
5947 overflow: style.overflow,
5948 opacity: style.opacity,
5949 });
5950
5951 cursor.y += img_height + margin.bottom;
5952 }
5953
5954 #[allow(clippy::too_many_arguments)]
5956 fn layout_svg(
5957 &self,
5958 node: &Node,
5959 style: &ResolvedStyle,
5960 cursor: &mut PageCursor,
5961 pages: &mut Vec<LayoutPage>,
5962 x: f64,
5963 _available_width: f64,
5964 svg_width: f64,
5965 svg_height: f64,
5966 view_box: Option<&str>,
5967 content: &str,
5968 ) {
5969 let margin = &style.margin.to_edges();
5970 let total_height = svg_height + margin.vertical();
5971
5972 if total_height > cursor.remaining_height() {
5973 pages.push(cursor.finalize());
5974 *cursor = cursor.new_page();
5975 }
5976
5977 cursor.y += margin.top;
5978
5979 let vb = view_box
5980 .and_then(crate::svg::parse_view_box)
5981 .unwrap_or(crate::svg::ViewBox {
5982 min_x: 0.0,
5983 min_y: 0.0,
5984 width: svg_width,
5985 height: svg_height,
5986 });
5987
5988 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
5989
5990 cursor.elements.push(LayoutElement {
5991 x: x + margin.left,
5992 y: cursor.content_y + cursor.y,
5993 width: svg_width,
5994 height: svg_height,
5995 draw: DrawCommand::Svg {
5996 commands,
5997 width: svg_width,
5998 height: svg_height,
5999 viewbox_min_x: vb.min_x,
6000 viewbox_min_y: vb.min_y,
6001 viewbox_width: vb.width,
6002 viewbox_height: vb.height,
6003 clip: false,
6004 },
6005 children: vec![],
6006 node_type: Some("Svg".to_string()),
6007 resolved_style: Some(style.clone()),
6008 source_location: node.source_location.clone(),
6009 href: node.href.clone(),
6010 bookmark: node.bookmark.clone(),
6011 alt: node.alt.clone(),
6012 is_header_row: false,
6013 actual_text: None,
6014 list_numbering: None,
6015 col_span: 1,
6016 overflow: style.overflow,
6017 opacity: style.opacity,
6018 });
6019
6020 cursor.y += svg_height + margin.bottom;
6021 }
6022
6023 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
6025 use crate::svg::SvgCommand;
6026
6027 let mut commands = Vec::new();
6028 let mut cur_x = 0.0_f64;
6029 let mut cur_y = 0.0_f64;
6030
6031 for op in operations {
6032 match op {
6033 CanvasOp::MoveTo { x, y } => {
6034 commands.push(SvgCommand::MoveTo(*x, *y));
6035 cur_x = *x;
6036 cur_y = *y;
6037 }
6038 CanvasOp::LineTo { x, y } => {
6039 commands.push(SvgCommand::LineTo(*x, *y));
6040 cur_x = *x;
6041 cur_y = *y;
6042 }
6043 CanvasOp::BezierCurveTo {
6044 cp1x,
6045 cp1y,
6046 cp2x,
6047 cp2y,
6048 x,
6049 y,
6050 } => {
6051 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
6052 cur_x = *x;
6053 cur_y = *y;
6054 }
6055 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
6056 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
6058 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
6059 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
6060 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
6061 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
6062 cur_x = *x;
6063 cur_y = *y;
6064 }
6065 CanvasOp::ClosePath => {
6066 commands.push(SvgCommand::ClosePath);
6067 }
6068 CanvasOp::Rect {
6069 x,
6070 y,
6071 width,
6072 height,
6073 } => {
6074 commands.push(SvgCommand::MoveTo(*x, *y));
6075 commands.push(SvgCommand::LineTo(*x + *width, *y));
6076 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
6077 commands.push(SvgCommand::LineTo(*x, *y + *height));
6078 commands.push(SvgCommand::ClosePath);
6079 cur_x = *x;
6080 cur_y = *y;
6081 }
6082 CanvasOp::Circle { cx, cy, r } => {
6083 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
6084 }
6085 CanvasOp::Ellipse { cx, cy, rx, ry } => {
6086 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
6087 }
6088 CanvasOp::Arc {
6089 cx,
6090 cy,
6091 r,
6092 start_angle,
6093 end_angle,
6094 counterclockwise,
6095 } => {
6096 let steps = 32;
6100 let mut sweep = end_angle - start_angle;
6101 if !counterclockwise && sweep < 0.0 {
6102 sweep += 2.0 * std::f64::consts::PI;
6103 }
6104 if *counterclockwise && sweep > 0.0 {
6105 sweep -= 2.0 * std::f64::consts::PI;
6106 }
6107 for i in 0..=steps {
6108 let t = *start_angle + sweep * (i as f64 / steps as f64);
6109 let px = cx + r * t.cos();
6110 let py = cy + r * t.sin();
6111 if i == 0 {
6112 commands.push(SvgCommand::MoveTo(px, py));
6113 } else {
6114 commands.push(SvgCommand::LineTo(px, py));
6115 }
6116 }
6117 }
6118 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
6119 CanvasOp::Fill => commands.push(SvgCommand::Fill),
6120 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
6121 CanvasOp::SetFillColor { r, g, b } => {
6122 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
6124 }
6125 CanvasOp::SetStrokeColor { r, g, b } => {
6126 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
6127 }
6128 CanvasOp::SetLineWidth { width } => {
6129 commands.push(SvgCommand::SetStrokeWidth(*width));
6130 }
6131 CanvasOp::SetLineCap { cap } => {
6132 commands.push(SvgCommand::SetLineCap(*cap));
6133 }
6134 CanvasOp::SetLineJoin { join } => {
6135 commands.push(SvgCommand::SetLineJoin(*join));
6136 }
6137 CanvasOp::Save => commands.push(SvgCommand::SaveState),
6138 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
6139 }
6140 }
6141
6142 commands
6143 }
6144
6145 #[allow(clippy::too_many_arguments)]
6147 fn layout_canvas(
6148 &self,
6149 node: &Node,
6150 style: &ResolvedStyle,
6151 cursor: &mut PageCursor,
6152 pages: &mut Vec<LayoutPage>,
6153 x: f64,
6154 _available_width: f64,
6155 canvas_width: f64,
6156 canvas_height: f64,
6157 operations: &[CanvasOp],
6158 ) {
6159 let margin = style.margin.to_edges();
6160 let total_height = canvas_height + margin.top + margin.bottom;
6161
6162 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
6164 pages.push(cursor.finalize());
6165 *cursor = cursor.new_page();
6166 }
6167
6168 cursor.y += margin.top;
6169
6170 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
6171
6172 cursor.elements.push(LayoutElement {
6173 x: x + margin.left,
6174 y: cursor.content_y + cursor.y,
6175 width: canvas_width,
6176 height: canvas_height,
6177 draw: DrawCommand::Svg {
6178 commands: svg_commands,
6179 width: canvas_width,
6180 height: canvas_height,
6181 viewbox_min_x: 0.0,
6184 viewbox_min_y: 0.0,
6185 viewbox_width: canvas_width,
6186 viewbox_height: canvas_height,
6187 clip: true,
6188 },
6189 children: vec![],
6190 node_type: Some("Canvas".to_string()),
6191 resolved_style: Some(style.clone()),
6192 source_location: node.source_location.clone(),
6193 href: node.href.clone(),
6194 bookmark: node.bookmark.clone(),
6195 alt: node.alt.clone(),
6196 is_header_row: false,
6197 actual_text: None,
6198 list_numbering: None,
6199 col_span: 1,
6200 overflow: style.overflow,
6201 opacity: style.opacity,
6202 });
6203
6204 cursor.y += canvas_height + margin.bottom;
6205 }
6206
6207 #[allow(clippy::too_many_arguments)]
6209 #[allow(clippy::too_many_arguments)]
6211 fn layout_chart(
6212 &self,
6213 node: &Node,
6214 style: &ResolvedStyle,
6215 cursor: &mut PageCursor,
6216 pages: &mut Vec<LayoutPage>,
6217 x: f64,
6218 chart_width: f64,
6219 chart_height: f64,
6220 primitives: Vec<crate::chart::ChartPrimitive>,
6221 node_type_name: &str,
6222 ) {
6223 let margin = &style.margin.to_edges();
6224 let total_height = chart_height + margin.vertical();
6225
6226 if total_height > cursor.remaining_height() {
6227 pages.push(cursor.finalize());
6228 *cursor = cursor.new_page();
6229 }
6230
6231 cursor.y += margin.top;
6232
6233 let draw = DrawCommand::Chart { primitives };
6234
6235 cursor.elements.push(LayoutElement {
6236 x: x + margin.left,
6237 y: cursor.content_y + cursor.y,
6238 width: chart_width,
6239 height: chart_height,
6240 draw,
6241 children: vec![],
6242 node_type: Some(node_type_name.to_string()),
6243 resolved_style: Some(style.clone()),
6244 source_location: node.source_location.clone(),
6245 href: node.href.clone(),
6246 bookmark: node.bookmark.clone(),
6247 alt: node.alt.clone(),
6248 is_header_row: false,
6249 actual_text: None,
6250 list_numbering: None,
6251 col_span: 1,
6252 overflow: style.overflow,
6253 opacity: style.opacity,
6254 });
6255
6256 cursor.y += chart_height + margin.bottom;
6257 }
6258
6259 #[allow(clippy::too_many_arguments)]
6261 fn layout_form_field(
6262 &self,
6263 node: &Node,
6264 style: &ResolvedStyle,
6265 cursor: &mut PageCursor,
6266 pages: &mut Vec<LayoutPage>,
6267 x: f64,
6268 field_width: f64,
6269 field_height: f64,
6270 draw: DrawCommand,
6271 node_type_name: &str,
6272 ) {
6273 let margin = &style.margin.to_edges();
6274 let total_height = field_height + margin.vertical();
6275
6276 if total_height > cursor.remaining_height() {
6277 pages.push(cursor.finalize());
6278 *cursor = cursor.new_page();
6279 }
6280
6281 cursor.y += margin.top;
6282
6283 cursor.elements.push(LayoutElement {
6284 x: x + margin.left,
6285 y: cursor.content_y + cursor.y,
6286 width: field_width,
6287 height: field_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 += field_height + margin.bottom;
6305 }
6306
6307 #[allow(clippy::too_many_arguments)]
6308 fn layout_barcode(
6309 &self,
6310 node: &Node,
6311 style: &ResolvedStyle,
6312 cursor: &mut PageCursor,
6313 pages: &mut Vec<LayoutPage>,
6314 x: f64,
6315 available_width: f64,
6316 data: &str,
6317 format: crate::barcode::BarcodeFormat,
6318 explicit_width: Option<f64>,
6319 bar_height: f64,
6320 ) {
6321 let margin = &style.margin.to_edges();
6322 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
6323 let total_height = bar_height + margin.vertical();
6324
6325 if total_height > cursor.remaining_height() {
6326 pages.push(cursor.finalize());
6327 *cursor = cursor.new_page();
6328 }
6329
6330 cursor.y += margin.top;
6331
6332 let draw = match crate::barcode::generate_barcode(data, format) {
6333 Ok(barcode_data) => {
6334 let bar_width = if barcode_data.bars.is_empty() {
6335 0.0
6336 } else {
6337 display_width / barcode_data.bars.len() as f64
6338 };
6339 DrawCommand::Barcode {
6340 bars: barcode_data.bars,
6341 bar_width,
6342 height: bar_height,
6343 color: style.color,
6344 }
6345 }
6346 Err(_) => DrawCommand::None,
6347 };
6348
6349 cursor.elements.push(LayoutElement {
6350 x: x + margin.left,
6351 y: cursor.content_y + cursor.y,
6352 width: display_width,
6353 height: bar_height,
6354 draw,
6355 children: vec![],
6356 node_type: Some("Barcode".to_string()),
6357 resolved_style: Some(style.clone()),
6358 source_location: node.source_location.clone(),
6359 href: node.href.clone(),
6360 bookmark: node.bookmark.clone(),
6361 alt: node.alt.clone(),
6362 is_header_row: false,
6363 actual_text: Some(data.to_string()),
6364 list_numbering: None,
6365 col_span: 1,
6366 overflow: style.overflow,
6367 opacity: style.opacity,
6368 });
6369
6370 cursor.y += bar_height + margin.bottom;
6371 }
6372
6373 #[allow(clippy::too_many_arguments)]
6375 fn layout_qrcode(
6376 &self,
6377 node: &Node,
6378 style: &ResolvedStyle,
6379 cursor: &mut PageCursor,
6380 pages: &mut Vec<LayoutPage>,
6381 x: f64,
6382 available_width: f64,
6383 data: &str,
6384 explicit_size: Option<f64>,
6385 ) {
6386 let margin = &style.margin.to_edges();
6387 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6388 let total_height = display_size + margin.vertical();
6389
6390 if total_height > cursor.remaining_height() {
6391 pages.push(cursor.finalize());
6392 *cursor = cursor.new_page();
6393 }
6394
6395 cursor.y += margin.top;
6396
6397 let draw = match crate::qrcode::generate_qr(data) {
6398 Ok(matrix) => {
6399 let module_size = display_size / matrix.size as f64;
6400 DrawCommand::QrCode {
6401 modules: matrix.modules,
6402 module_size,
6403 color: style.color,
6404 }
6405 }
6406 Err(_) => DrawCommand::None,
6407 };
6408
6409 cursor.elements.push(LayoutElement {
6410 x: x + margin.left,
6411 y: cursor.content_y + cursor.y,
6412 width: display_size,
6413 height: display_size,
6414 draw,
6415 children: vec![],
6416 node_type: Some("QrCode".to_string()),
6417 resolved_style: Some(style.clone()),
6418 source_location: node.source_location.clone(),
6419 href: node.href.clone(),
6420 bookmark: node.bookmark.clone(),
6421 alt: node.alt.clone(),
6422 is_header_row: false,
6423 actual_text: Some(data.to_string()),
6424 list_numbering: None,
6425 col_span: 1,
6426 overflow: style.overflow,
6427 opacity: style.opacity,
6428 });
6429
6430 cursor.y += display_size + margin.bottom;
6431 }
6432
6433 fn measure_node_height(
6436 &self,
6437 node: &Node,
6438 available_width: f64,
6439 style: &ResolvedStyle,
6440 font_context: &FontContext,
6441 ) -> f64 {
6442 match &node.kind {
6443 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6448 let measure_width = match style.width {
6451 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6452 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6453 };
6454 if !runs.is_empty() {
6461 let mut styled_chars: Vec<StyledChar> = Vec::new();
6462 for run in runs {
6463 let run_style = run.style.resolve(Some(style), measure_width);
6464 let transform = run_style.text_transform;
6465 let run_content = substitute_page_placeholders(&run.content);
6466 let mut prev_is_whitespace = true;
6467 for ch in run_content.chars() {
6468 let transformed_ch =
6469 apply_char_transform(ch, transform, prev_is_whitespace);
6470 prev_is_whitespace = ch.is_whitespace();
6471 styled_chars.push(StyledChar {
6472 ch: transformed_ch,
6473 font_family: run_style.font_family.clone(),
6474 font_size: run_style.font_size,
6475 font_weight: run_style.font_weight,
6476 font_style: run_style.font_style,
6477 color: run_style.color,
6478 href: None,
6479 text_decoration: run_style.text_decoration,
6480 letter_spacing: run_style.letter_spacing,
6481 word_spacing: run_style.word_spacing,
6482 });
6483 }
6484 }
6485 let justify = matches!(style.text_align, TextAlign::Justify);
6486 let broken_lines = match style.line_breaking {
6487 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6488 font_context,
6489 &styled_chars,
6490 measure_width,
6491 style.hyphens,
6492 style.lang.as_deref(),
6493 justify,
6494 ),
6495 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6496 font_context,
6497 &styled_chars,
6498 measure_width,
6499 style.hyphens,
6500 style.lang.as_deref(),
6501 ),
6502 };
6503 let line_height = style.font_size * style.line_height;
6504 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6505 } else {
6506 let content = substitute_page_placeholders(content);
6507 let transformed = apply_text_transform(&content, style.text_transform);
6508 let justify = matches!(style.text_align, TextAlign::Justify);
6509 let lines = match style.line_breaking {
6510 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6511 font_context,
6512 &transformed,
6513 measure_width,
6514 style.font_size,
6515 &style.font_family,
6516 style.font_weight,
6517 style.font_style,
6518 style.letter_spacing,
6519 style.word_spacing,
6520 style.hyphens,
6521 style.lang.as_deref(),
6522 justify,
6523 ),
6524 LineBreaking::Greedy => self.text_layout.break_into_lines(
6525 font_context,
6526 &transformed,
6527 measure_width,
6528 style.font_size,
6529 &style.font_family,
6530 style.font_weight,
6531 style.font_style,
6532 style.letter_spacing,
6533 style.word_spacing,
6534 style.hyphens,
6535 style.lang.as_deref(),
6536 ),
6537 };
6538 let line_height = style.font_size * style.line_height;
6539 (lines.len() as f64) * line_height + style.padding.vertical()
6540 }
6541 }
6542 NodeKind::Image {
6543 src,
6544 width: explicit_w,
6545 height: explicit_h,
6546 } => {
6547 let (_, h) = self.image_display_size(
6549 src,
6550 style,
6551 *explicit_w,
6552 *explicit_h,
6553 available_width - style.margin.horizontal(),
6554 );
6555 h + style.padding.vertical()
6556 }
6557 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6558 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6559 NodeKind::QrCode { size, .. } => {
6560 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6561 display_size + style.margin.vertical()
6562 }
6563 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6564 NodeKind::BarChart { height, .. }
6565 | NodeKind::LineChart { height, .. }
6566 | NodeKind::PieChart { height, .. }
6567 | NodeKind::AreaChart { height, .. }
6568 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6569 NodeKind::TextField { height, .. }
6570 | NodeKind::Checkbox { height, .. }
6571 | NodeKind::Dropdown { height, .. }
6572 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6573 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6575 if let SizeConstraint::Fixed(h) = style.height {
6582 return h;
6583 }
6584 let outer_width = match style.width {
6585 SizeConstraint::Fixed(w) => w,
6586 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6587 };
6588 let inner_width =
6589 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6590 let col_widths = self.resolve_column_widths(
6591 columns,
6592 inner_width,
6593 &node.children,
6594 style,
6595 font_context,
6596 );
6597 let row_gap = style.row_gap;
6598 let offsets = Self::table_column_offsets(&node.children);
6599 let mut total = 0.0;
6600 for (i, row) in node.children.iter().enumerate() {
6601 if i > 0 {
6602 total += row_gap;
6603 }
6604 total += self.measure_table_row_height(
6605 row,
6606 &col_widths,
6607 &offsets[i],
6608 style,
6609 font_context,
6610 );
6611 }
6612 total + style.padding.vertical() + style.border_width.vertical()
6613 }
6614 NodeKind::TableRow { .. } => {
6615 let n = node.children.len().max(1);
6620 let usable = (available_width - style.margin.horizontal()).max(0.0);
6621 let col_w = usable / n as f64;
6622 let col_widths = vec![col_w; n];
6623 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6624 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6625 }
6626 _ => {
6627 if let SizeConstraint::Fixed(h) = style.height {
6629 return h;
6630 }
6631 let outer_width = match style.width {
6635 SizeConstraint::Fixed(w) => w,
6636 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6637 }
6638 .min(style.max_width)
6639 .max(style.min_width);
6640 let inner_width =
6641 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6642 let children_height =
6643 self.measure_children_height(&node.children, inner_width, style, font_context);
6644 (children_height + style.padding.vertical() + style.border_width.vertical())
6645 .max(style.min_height)
6646 }
6647 }
6648 }
6649
6650 fn measure_children_height(
6651 &self,
6652 children: &[Node],
6653 available_width: f64,
6654 parent_style: &ResolvedStyle,
6655 font_context: &FontContext,
6656 ) -> f64 {
6657 if children
6663 .iter()
6664 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6665 {
6666 let flow: Vec<Node> = children
6667 .iter()
6668 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6669 .cloned()
6670 .collect();
6671 return self.measure_children_height(
6672 &flow,
6673 available_width,
6674 parent_style,
6675 font_context,
6676 );
6677 }
6678 if matches!(parent_style.display, Display::Grid) {
6680 if let Some(template_cols) = &parent_style.grid_template_columns {
6681 let num_columns = template_cols.len();
6682 if num_columns > 0 && !children.is_empty() {
6683 let col_gap = parent_style.column_gap;
6684 let row_gap = parent_style.row_gap;
6685
6686 let content_sizes: Vec<f64> = template_cols
6687 .iter()
6688 .map(|track| {
6689 if matches!(track, GridTrackSize::Auto) {
6690 available_width / num_columns as f64
6691 } else {
6692 0.0
6693 }
6694 })
6695 .collect();
6696
6697 let col_widths = grid::resolve_tracks(
6698 template_cols,
6699 available_width,
6700 col_gap,
6701 &content_sizes,
6702 );
6703
6704 let placements: Vec<Option<&GridPlacement>> = children
6705 .iter()
6706 .map(|child| child.style.grid_placement.as_ref())
6707 .collect();
6708
6709 let item_placements = grid::place_items(&placements, num_columns);
6710 let num_rows = grid::compute_num_rows(&item_placements);
6711
6712 if num_rows == 0 {
6713 return 0.0;
6714 }
6715
6716 let mut row_heights = vec![0.0_f64; num_rows];
6717 for placement in &item_placements {
6718 let cell_width = grid::span_width(
6719 placement.col_start,
6720 placement.col_end,
6721 &col_widths,
6722 col_gap,
6723 );
6724 let child = &children[placement.child_index];
6725 let child_style = child.style.resolve(Some(parent_style), cell_width);
6726 let h =
6727 self.measure_node_height(child, cell_width, &child_style, font_context);
6728 let span = placement.row_end - placement.row_start;
6729 let per_row = h / span as f64;
6730 for rh in row_heights
6731 .iter_mut()
6732 .take(placement.row_end.min(num_rows))
6733 .skip(placement.row_start)
6734 {
6735 if per_row > *rh {
6736 *rh = per_row;
6737 }
6738 }
6739 }
6740
6741 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6742 return row_heights.iter().sum::<f64>() + total_row_gap;
6743 }
6744 }
6745 }
6746
6747 let direction = parent_style.flex_direction;
6748 let row_gap = parent_style.row_gap;
6749 let column_gap = parent_style.column_gap;
6750
6751 match direction {
6752 FlexDirection::Row | FlexDirection::RowReverse => {
6753 let styles: Vec<ResolvedStyle> = children
6756 .iter()
6757 .map(|child| child.style.resolve(Some(parent_style), available_width))
6758 .collect();
6759
6760 let base_widths: Vec<f64> = children
6761 .iter()
6762 .zip(&styles)
6763 .map(|(child, style)| match style.flex_basis {
6764 SizeConstraint::Fixed(w) => w,
6765 SizeConstraint::Auto => match style.width {
6766 SizeConstraint::Fixed(w) => w,
6767 SizeConstraint::Auto => self
6768 .measure_intrinsic_width(child, style, font_context)
6769 .min(available_width),
6770 },
6771 })
6772 .collect();
6773
6774 let lines = match parent_style.flex_wrap {
6775 FlexWrap::NoWrap => {
6776 vec![flex::WrapLine {
6777 start: 0,
6778 end: children.len(),
6779 }]
6780 }
6781 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6782 flex::partition_into_lines(&base_widths, column_gap, available_width)
6783 }
6784 };
6785
6786 let mut final_widths = base_widths.clone();
6788 for line in &lines {
6789 let line_count = line.end - line.start;
6790 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6791 let distributable = available_width - line_gap;
6792 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6793 let remaining = distributable - total_base;
6794
6795 if remaining > 0.0 {
6796 let total_grow: f64 = styles[line.start..line.end]
6797 .iter()
6798 .map(|s| s.flex_grow)
6799 .sum();
6800 if total_grow > 0.0 {
6801 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6802 final_widths[line.start + j] = base_widths[line.start + j]
6803 + remaining * (s.flex_grow / total_grow);
6804 }
6805 }
6806 } else if remaining < 0.0 {
6807 let total_shrink: f64 = styles[line.start..line.end]
6808 .iter()
6809 .enumerate()
6810 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6811 .sum();
6812 if total_shrink > 0.0 {
6813 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6814 let factor =
6815 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6816 let w = base_widths[line.start + j] + remaining * factor;
6817 final_widths[line.start + j] = w.max(s.min_width);
6818 }
6819 }
6820 }
6821 }
6822
6823 let mut total = 0.0;
6824 for (i, line) in lines.iter().enumerate() {
6825 let line_height: f64 = children[line.start..line.end]
6826 .iter()
6827 .enumerate()
6828 .map(|(j, child)| {
6829 let fw = final_widths[line.start + j];
6830 let child_style =
6839 child.style.resolve(Some(parent_style), available_width);
6840 self.measure_node_height(child, fw, &child_style, font_context)
6841 + child_style.margin.vertical()
6842 })
6843 .fold(0.0f64, f64::max);
6844 total += line_height;
6845 if i > 0 {
6846 total += row_gap;
6847 }
6848 }
6849 total
6850 }
6851 FlexDirection::Column | FlexDirection::ColumnReverse => {
6852 let mut total = 0.0;
6853 for (i, child) in children.iter().enumerate() {
6854 let child_style = child.style.resolve(Some(parent_style), available_width);
6855 let child_height = self.measure_node_height(
6856 child,
6857 available_width,
6858 &child_style,
6859 font_context,
6860 );
6861 total += child_height + child_style.margin.vertical();
6862 if i > 0 {
6863 total += row_gap;
6864 }
6865 }
6866 total
6867 }
6868 }
6869 }
6870
6871 fn measure_intrinsic_width(
6873 &self,
6874 node: &Node,
6875 style: &ResolvedStyle,
6876 font_context: &FontContext,
6877 ) -> f64 {
6878 match &node.kind {
6879 NodeKind::Svg { width, .. } => {
6880 *width + style.padding.horizontal() + style.margin.horizontal()
6881 }
6882 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6883 let text_width = if !runs.is_empty() {
6889 runs.iter()
6890 .map(|run| {
6891 let run_style = run.style.resolve(Some(style), 0.0);
6892 let run_content = substitute_page_placeholders(&run.content);
6893 let transformed =
6894 apply_text_transform(&run_content, run_style.text_transform);
6895 let italic = matches!(
6896 run_style.font_style,
6897 FontStyle::Italic | FontStyle::Oblique
6898 );
6899 transformed
6903 .split('\n')
6904 .map(|segment| {
6905 font_context.measure_string(
6906 segment,
6907 &run_style.font_family,
6908 run_style.font_weight,
6909 italic,
6910 run_style.font_size,
6911 run_style.letter_spacing,
6912 )
6913 })
6914 .fold(0.0f64, f64::max)
6915 })
6916 .sum()
6917 } else {
6918 let content = substitute_page_placeholders(content);
6919 let transformed = apply_text_transform(&content, style.text_transform);
6920 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6921 transformed
6922 .split('\n')
6923 .map(|segment| {
6924 font_context.measure_string(
6925 segment,
6926 &style.font_family,
6927 style.font_weight,
6928 italic,
6929 style.font_size,
6930 style.letter_spacing,
6931 )
6932 })
6933 .fold(0.0f64, f64::max)
6934 };
6935 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
6938 }
6939 NodeKind::Image {
6940 src, width, height, ..
6941 } => {
6942 let w = if let SizeConstraint::Fixed(w) = style.width {
6943 w
6944 } else if let Some(w) = width {
6945 *w
6946 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
6947 let pixel_w = iw as f64;
6948 let pixel_h = ih as f64;
6949 let aspect = if pixel_w > 0.0 {
6950 pixel_h / pixel_w
6951 } else {
6952 0.75
6953 };
6954 let constrained_h = match style.height {
6956 SizeConstraint::Fixed(h) => Some(h),
6957 SizeConstraint::Auto => *height,
6958 };
6959 if let Some(h) = constrained_h {
6960 h / aspect
6961 } else {
6962 pixel_w
6963 }
6964 } else {
6965 100.0
6966 };
6967 w + style.padding.horizontal() + style.margin.horizontal()
6968 }
6969 NodeKind::Barcode { width, .. } => {
6970 let w = width.unwrap_or(0.0);
6971 w + style.padding.horizontal() + style.margin.horizontal()
6972 }
6973 NodeKind::QrCode { size, .. } => {
6974 let display_size = size.unwrap_or(0.0);
6975 display_size + style.padding.horizontal() + style.margin.horizontal()
6976 }
6977 NodeKind::Canvas { width, .. } => {
6978 *width + style.padding.horizontal() + style.margin.horizontal()
6979 }
6980 NodeKind::BarChart { width, .. }
6981 | NodeKind::LineChart { width, .. }
6982 | NodeKind::PieChart { width, .. }
6983 | NodeKind::AreaChart { width, .. }
6984 | NodeKind::DotPlot { width, .. } => {
6985 *width + style.padding.horizontal() + style.margin.horizontal()
6986 }
6987 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
6988 *width + style.padding.horizontal() + style.margin.horizontal()
6989 }
6990 NodeKind::Table { columns } => {
6991 let num_cols = node
6996 .children
6997 .iter()
6998 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
6999 .max()
7000 .unwrap_or(1)
7001 .max(columns.len().max(1));
7002 let (_, col_max) =
7003 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7004 col_max.iter().sum::<f64>()
7005 + style.padding.horizontal()
7006 + style.margin.horizontal()
7007 + style.border_width.horizontal()
7008 }
7009 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
7010 *width + style.padding.horizontal() + style.margin.horizontal()
7011 }
7012 NodeKind::Watermark { .. } => 0.0, _ => {
7014 if let SizeConstraint::Fixed(w) = style.width {
7022 return w
7023 + style.padding.horizontal()
7024 + style.border_width.horizontal()
7025 + style.margin.horizontal();
7026 }
7027 if node.children.is_empty() {
7029 style.padding.horizontal() + style.margin.horizontal()
7030 } else {
7031 let direction = style.flex_direction;
7032 let gap = style.column_gap;
7044 let mut total = 0.0f64;
7045 for (i, child) in node.children.iter().enumerate() {
7046 let child_style = child.style.resolve(Some(style), 0.0);
7047 let child_width =
7048 self.measure_intrinsic_width(child, &child_style, font_context);
7049 match direction {
7050 FlexDirection::Row | FlexDirection::RowReverse => {
7051 total += child_width;
7052 if i > 0 {
7053 total += gap;
7054 }
7055 }
7056 _ => {
7057 total = total.max(child_width);
7058 }
7059 }
7060 }
7061 total
7062 + style.padding.horizontal()
7063 + style.margin.horizontal()
7064 + style.border_width.horizontal()
7065 }
7066 }
7067 }
7068 }
7069
7070 pub fn measure_min_content_width(
7074 &self,
7075 node: &Node,
7076 style: &ResolvedStyle,
7077 font_context: &FontContext,
7078 ) -> f64 {
7079 match &node.kind {
7080 NodeKind::Table { columns } => {
7081 let num_cols = node
7084 .children
7085 .iter()
7086 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7087 .max()
7088 .unwrap_or(1)
7089 .max(columns.len().max(1));
7090 let (col_min, _) =
7091 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7092 col_min.iter().sum::<f64>()
7093 + style.padding.horizontal()
7094 + style.margin.horizontal()
7095 + style.border_width.horizontal()
7096 }
7097 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7098 let word_width = if !runs.is_empty() {
7099 runs.iter()
7101 .map(|run| {
7102 let run_style = run.style.resolve(Some(style), 0.0);
7103 let run_content = substitute_page_placeholders(&run.content);
7104 let transformed =
7105 apply_text_transform(&run_content, run_style.text_transform);
7106 self.text_layout.measure_widest_word(
7107 font_context,
7108 &transformed,
7109 run_style.font_size,
7110 &run_style.font_family,
7111 run_style.font_weight,
7112 run_style.font_style,
7113 run_style.letter_spacing,
7114 run_style.word_spacing,
7115 style.hyphens,
7116 style.lang.as_deref(),
7117 )
7118 })
7119 .fold(0.0f64, f64::max)
7120 } else {
7121 let content = substitute_page_placeholders(content);
7122 let transformed = apply_text_transform(&content, style.text_transform);
7123 self.text_layout.measure_widest_word(
7124 font_context,
7125 &transformed,
7126 style.font_size,
7127 &style.font_family,
7128 style.font_weight,
7129 style.font_style,
7130 style.letter_spacing,
7131 style.word_spacing,
7132 style.hyphens,
7133 style.lang.as_deref(),
7134 )
7135 };
7136 word_width + style.padding.horizontal() + style.margin.horizontal()
7137 }
7138 NodeKind::Image { width, .. } => {
7139 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
7140 }
7141 NodeKind::Svg { width, .. } => {
7142 *width + style.padding.horizontal() + style.margin.horizontal()
7143 }
7144 _ => {
7145 if node.children.is_empty() {
7146 style.padding.horizontal()
7147 + style.margin.horizontal()
7148 + style.border_width.horizontal()
7149 } else {
7150 let mut max_child_min = 0.0f64;
7151 for child in &node.children {
7152 let child_style = child.style.resolve(Some(style), 0.0);
7153 let child_min =
7154 self.measure_min_content_width(child, &child_style, font_context);
7155 max_child_min = max_child_min.max(child_min);
7156 }
7157 max_child_min
7158 + style.padding.horizontal()
7159 + style.margin.horizontal()
7160 + style.border_width.horizontal()
7161 }
7162 }
7163 }
7164 }
7165
7166 fn flex_item_baseline_distance(
7173 &self,
7174 item: &Node,
7175 style: &ResolvedStyle,
7176 w: f64,
7177 font_context: &FontContext,
7178 ) -> f64 {
7179 let first_line = match &item.kind {
7180 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
7181 let metrics = font_context.baseline_metrics(
7182 &style.font_family,
7183 style.font_weight,
7184 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
7185 );
7186 baseline_in_line(
7187 style.font_size * style.line_height,
7188 style.font_size,
7189 metrics,
7190 )
7191 }
7192 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
7193 };
7194 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
7195 }
7196
7197 fn cell_first_baseline_in_line(
7204 &self,
7205 cell: &Node,
7206 cell_style: &ResolvedStyle,
7207 w: f64,
7208 font_context: &FontContext,
7209 ) -> f64 {
7210 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
7211 for ch in &node.children {
7212 let s = ch.style.resolve(Some(parent), w);
7213 match &ch.kind {
7214 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
7215 _ => {
7216 if let Some(f) = first(ch, &s, w) {
7217 return Some(f);
7218 }
7219 }
7220 }
7221 }
7222 None
7223 }
7224 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
7225 let metrics = font_context.baseline_metrics(
7226 &s.font_family,
7227 s.font_weight,
7228 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
7229 );
7230 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
7231 }
7232
7233 fn cell_baseline_distance(
7238 &self,
7239 cell: &Node,
7240 cell_style: &ResolvedStyle,
7241 inner_width: f64,
7242 font_context: &FontContext,
7243 ) -> f64 {
7244 cell_style.padding.top
7245 + cell_style.border_width.top
7246 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
7247 }
7248
7249 fn row_baseline(
7252 &self,
7253 row: &Node,
7254 row_style: &ResolvedStyle,
7255 col_widths: &[f64],
7256 col_offsets: &[usize],
7257 font_context: &FontContext,
7258 ) -> Option<f64> {
7259 let mut b: Option<f64> = None;
7260 for (cell_i, cell) in row.children.iter().enumerate() {
7261 let span = match &cell.kind {
7262 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7263 _ => 1,
7264 };
7265 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7266 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7267 let cell_style = cell.style.resolve(Some(row_style), col_width);
7268 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7269 let iw = col_width
7270 - cell_style.padding.horizontal()
7271 - cell_style.border_width.horizontal();
7272 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
7273 b = Some(b.map_or(d, |m: f64| m.max(d)));
7274 }
7275 }
7276 b
7277 }
7278
7279 fn measure_table_row_height(
7280 &self,
7281 row: &Node,
7282 col_widths: &[f64],
7283 col_offsets: &[usize],
7284 parent_style: &ResolvedStyle,
7285 font_context: &FontContext,
7286 ) -> f64 {
7287 let row_style = row
7288 .style
7289 .resolve(Some(parent_style), col_widths.iter().sum());
7290 let mut max_height: f64 = 0.0;
7291 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
7294
7295 for (cell_i, cell) in row.children.iter().enumerate() {
7296 let span = match &cell.kind {
7297 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7298 _ => 1,
7299 };
7300 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7301 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7302 let cell_style = cell.style.resolve(Some(&row_style), col_width);
7303 let inner_width =
7304 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
7305
7306 let mut cell_content_height = 0.0;
7307 for child in &cell.children {
7308 let child_style = child.style.resolve(Some(&cell_style), inner_width);
7309 cell_content_height +=
7310 self.measure_node_height(child, inner_width, &child_style, font_context);
7311 }
7312
7313 let mut total = cell_content_height
7314 + cell_style.padding.vertical()
7315 + cell_style.border_width.vertical();
7316 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7320 if let Some(b) = row_bl {
7321 let d =
7322 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
7323 total += (b - d).max(0.0);
7324 }
7325 }
7326 if let SizeConstraint::Fixed(h) = cell_style.height {
7333 total = total.max(h);
7334 }
7335 max_height = max_height.max(total);
7336 }
7337
7338 max_height.max(row_style.min_height)
7339 }
7340
7341 fn cell_col_span(cell: &Node) -> usize {
7343 match &cell.kind {
7344 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7345 _ => 1,
7346 }
7347 }
7348
7349 fn measure_column_content(
7354 &self,
7355 children: &[Node],
7356 num_cols: usize,
7357 available_width: f64,
7358 table_style: &ResolvedStyle,
7359 font_context: &FontContext,
7360 ) -> (Vec<f64>, Vec<f64>) {
7361 let mut col_min = vec![0.0f64; num_cols];
7362 let mut col_max = vec![0.0f64; num_cols];
7363 let offsets = Self::table_column_offsets(children);
7364 for (row_i, row_node) in children.iter().enumerate() {
7365 for (cell_i, cell) in row_node.children.iter().enumerate() {
7366 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7367 let span = Self::cell_col_span(cell);
7368 let cell_style = cell.style.resolve(Some(table_style), available_width);
7369 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7370 let mut cmin = 0.0f64;
7371 let mut cmax = 0.0f64;
7372 for child in &cell.children {
7373 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7374 cmin =
7375 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7376 cmax =
7377 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7378 }
7379 cmin += chrome;
7380 let mut cmax = cmax.max(cmin) + chrome;
7381 if let SizeConstraint::Fixed(w) = cell_style.width {
7382 cmax = w.max(cmin);
7383 }
7384 let per_min = cmin / span as f64;
7385 let per_max = cmax / span as f64;
7386 for k in col..(col + span).min(num_cols) {
7387 col_min[k] = col_min[k].max(per_min);
7388 col_max[k] = col_max[k].max(per_max);
7389 }
7390 }
7391 }
7392 (col_min, col_max)
7393 }
7394
7395 fn resolve_column_widths(
7405 &self,
7406 defs: &[ColumnDef],
7407 available_width: f64,
7408 children: &[Node],
7409 table_style: &ResolvedStyle,
7410 font_context: &FontContext,
7411 ) -> Vec<f64> {
7412 if defs.is_empty() {
7413 let num_cols = Self::occupancy_column_count(children);
7414
7415 let (col_min, col_max) = self.measure_column_content(
7416 children,
7417 num_cols,
7418 available_width,
7419 table_style,
7420 font_context,
7421 );
7422
7423 let sum_min: f64 = col_min.iter().sum();
7424 let sum_max: f64 = col_max.iter().sum();
7425 let w = available_width;
7426 return if sum_max <= w {
7427 if sum_max <= f64::EPSILON {
7431 vec![w / num_cols as f64; num_cols]
7432 } else {
7433 col_max
7434 .iter()
7435 .map(|m| m + (w - sum_max) * (m / sum_max))
7436 .collect()
7437 }
7438 } else if sum_min <= w {
7439 let denom = (sum_max - sum_min).max(f64::EPSILON);
7442 col_min
7443 .iter()
7444 .zip(&col_max)
7445 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7446 .collect()
7447 } else {
7448 self.defect(format!(
7451 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7452 sum_min, w
7453 ));
7454 let scale = w / sum_min.max(f64::EPSILON);
7455 col_min.iter().map(|m| m * scale).collect()
7456 };
7457 }
7458
7459 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7465 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7466 while defs_vec.len() < num_cols {
7467 defs_vec.push(ColumnDef {
7468 width: ColumnWidth::Auto,
7469 });
7470 }
7471 let defs = &defs_vec[..];
7472
7473 let mut widths = Vec::new();
7474 let mut remaining = available_width;
7475 let mut auto_count = 0;
7476
7477 for def in defs {
7478 match def.width {
7479 ColumnWidth::Fixed(w) => {
7480 widths.push(w);
7481 remaining -= w;
7482 }
7483 ColumnWidth::Fraction(f) => {
7484 let w = available_width * f;
7485 widths.push(w);
7486 remaining -= w;
7487 }
7488 ColumnWidth::Auto => {
7489 widths.push(0.0);
7490 auto_count += 1;
7491 }
7492 }
7493 }
7494
7495 const OVERFULL_EPS: f64 = 0.01;
7504 if remaining < -OVERFULL_EPS {
7505 self.defect(format!(
7508 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7509 available_width - remaining,
7510 available_width
7511 ));
7512 remaining = 0.0;
7513 }
7514 remaining = remaining.max(0.0);
7517
7518 if auto_count > 0 {
7519 let auto_width = remaining / auto_count as f64;
7520 for (i, def) in defs.iter().enumerate() {
7521 if matches!(def.width, ColumnWidth::Auto) {
7522 widths[i] = auto_width;
7523 }
7524 }
7525 }
7526
7527 let (col_min, _) = self.measure_column_content(
7534 children,
7535 widths.len(),
7536 available_width,
7537 table_style,
7538 font_context,
7539 );
7540 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7541 if needs_floor {
7542 let sum_min: f64 = col_min.iter().sum();
7543 if sum_min > available_width {
7544 self.defect(format!(
7545 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7546 sum_min, available_width
7547 ));
7548 let scale = available_width / sum_min.max(f64::EPSILON);
7549 return col_min.iter().map(|m| m * scale).collect();
7550 }
7551 let deficit: f64 = widths
7552 .iter()
7553 .zip(&col_min)
7554 .map(|(w, m)| (m - w).max(0.0))
7555 .sum();
7556 let surplus: f64 = widths
7557 .iter()
7558 .zip(&col_min)
7559 .map(|(w, m)| (w - m).max(0.0))
7560 .sum();
7561 let take = if surplus > 0.0 {
7562 deficit / surplus
7563 } else {
7564 0.0
7565 };
7566 widths = widths
7567 .iter()
7568 .zip(&col_min)
7569 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7570 .collect();
7571 }
7572
7573 widths
7574 }
7575
7576 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7577 for (page_index, page) in pages.iter_mut().enumerate() {
7578 if !page.watermarks.is_empty() {
7580 let (page_w, page_h) = page.config.size.dimensions();
7581 let cx = page_w / 2.0;
7582 let cy = page_h / 2.0;
7583
7584 let mut watermark_elements = Vec::new();
7585 for wm_node in &page.watermarks {
7586 if let NodeKind::Watermark {
7587 text,
7588 font_size,
7589 angle,
7590 } = &wm_node.kind
7591 {
7592 let style = wm_node.style.resolve(None, page_w);
7593 let color = style.color;
7594 let opacity = style.opacity;
7595 let angle_rad = angle.to_radians();
7596
7597 let italic =
7599 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7600
7601 let shaped = self.text_layout.shape_text(
7603 font_context,
7604 text,
7605 &style.font_family,
7606 style.font_weight,
7607 style.font_style,
7608 );
7609
7610 let mut glyphs = Vec::new();
7611 let mut x_pos = 0.0;
7612 let text_chars: Vec<char> = text.chars().collect();
7613
7614 if let Some(shaped_glyphs) = shaped {
7615 let units_per_em = font_context.units_per_em(
7617 &style.font_family,
7618 style.font_weight,
7619 italic,
7620 ) as f64;
7621
7622 for sg in &shaped_glyphs {
7623 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7624 let cluster_idx = sg.cluster as usize;
7625 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7626 glyphs.push(PositionedGlyph {
7627 glyph_id: sg.glyph_id,
7628 char_value: ch,
7629 x_offset: x_pos,
7630 y_offset: 0.0,
7631 x_advance: advance,
7632 font_size: *font_size,
7633 font_family: Arc::from(style.font_family.as_str()),
7634 font_weight: style.font_weight,
7635 font_style: style.font_style,
7636 color: Some(color),
7637 href: None,
7638 text_decoration: TextDecoration::None,
7639 letter_spacing: style.letter_spacing,
7640 cluster_text: None,
7641 });
7642 x_pos += advance + style.letter_spacing;
7643 }
7644 } else {
7645 for &ch in &text_chars {
7647 let w = font_context.char_width(
7648 ch,
7649 &style.font_family,
7650 style.font_weight,
7651 italic,
7652 *font_size,
7653 );
7654 glyphs.push(PositionedGlyph {
7655 glyph_id: ch as u16,
7656 char_value: ch,
7657 x_offset: x_pos,
7658 y_offset: 0.0,
7659 x_advance: w,
7660 font_size: *font_size,
7661 font_family: Arc::from(style.font_family.as_str()),
7662 font_weight: style.font_weight,
7663 font_style: style.font_style,
7664 color: Some(color),
7665 href: None,
7666 text_decoration: TextDecoration::None,
7667 letter_spacing: style.letter_spacing,
7668 cluster_text: None,
7669 });
7670 x_pos += w + style.letter_spacing;
7671 }
7672 }
7673
7674 let text_width = x_pos;
7675
7676 let line = TextLine {
7677 x: 0.0,
7678 y: 0.0,
7679 glyphs,
7680 width: text_width,
7681 height: *font_size,
7682 word_spacing: 0.0,
7683 };
7684
7685 watermark_elements.push(LayoutElement {
7686 x: cx,
7687 y: cy,
7688 width: text_width,
7689 height: *font_size,
7690 draw: DrawCommand::Watermark {
7691 lines: vec![line],
7692 color,
7693 opacity,
7694 angle_rad,
7695 font_family: style.font_family.clone(),
7696 },
7697 children: vec![],
7698 node_type: Some("Watermark".to_string()),
7699 resolved_style: None,
7700 source_location: None,
7701 href: None,
7702 bookmark: None,
7703 alt: None,
7704 is_header_row: false,
7705 actual_text: None,
7706 list_numbering: None,
7707 col_span: 1,
7708 overflow: Overflow::default(),
7709 opacity: 1.0,
7710 });
7711 }
7712 }
7713
7714 watermark_elements.append(&mut page.elements);
7716 page.elements = watermark_elements;
7717 page.watermarks.clear();
7718 }
7719
7720 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7721 continue;
7722 }
7723
7724 if !page.fixed_header.is_empty() {
7726 let mut hdr_cursor = PageCursor::new(&page.config);
7727 for (node, _h) in &page.fixed_header {
7728 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7731 continue;
7732 }
7733 let cw = hdr_cursor.content_width;
7734 let cx = hdr_cursor.content_x;
7735 let style = node.style.resolve(None, cw);
7736 self.layout_view(
7737 node,
7738 &style,
7739 &mut hdr_cursor,
7740 &mut Vec::new(),
7741 cx,
7742 cw,
7743 font_context,
7744 );
7745 }
7746 let mut combined = hdr_cursor.elements;
7748 combined.append(&mut page.elements);
7749 page.elements = combined;
7750 }
7751
7752 if !page.fixed_footer.is_empty() {
7757 let mut ftr_cursor = PageCursor::new(&page.config);
7758 let total_ftr: f64 = page
7759 .fixed_footer
7760 .iter()
7761 .filter(|(n, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7762 .map(|(_, h)| *h)
7763 .sum();
7764 let target_y = ftr_cursor.content_height - total_ftr;
7765 for (node, _h) in &page.fixed_footer {
7767 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7768 continue;
7769 }
7770 let cw = ftr_cursor.content_width;
7771 let cx = ftr_cursor.content_x;
7772 let style = node.style.resolve(None, cw);
7773 self.layout_view(
7774 node,
7775 &style,
7776 &mut ftr_cursor,
7777 &mut Vec::new(),
7778 cx,
7779 cw,
7780 font_context,
7781 );
7782 }
7783 for el in &mut ftr_cursor.elements {
7787 offset_element_y(el, target_y);
7788 }
7789 page.elements.extend(ftr_cursor.elements);
7790 }
7791
7792 page.fixed_header.clear();
7794 page.fixed_footer.clear();
7795 }
7796 }
7797
7798 #[allow(clippy::too_many_arguments)]
7803 fn layout_grid_children(
7804 &self,
7805 children: &[Node],
7806 parent_style: &ResolvedStyle,
7807 cursor: &mut PageCursor,
7808 pages: &mut Vec<LayoutPage>,
7809 x: f64,
7810 available_width: f64,
7811 font_context: &FontContext,
7812 ) {
7813 let template_cols = match &parent_style.grid_template_columns {
7814 Some(cols) => cols,
7815 None => return, };
7817
7818 let num_columns = template_cols.len();
7819 if num_columns == 0 || children.is_empty() {
7820 return;
7821 }
7822
7823 let col_gap = parent_style.column_gap;
7824 let row_gap = parent_style.row_gap;
7825
7826 let content_sizes: Vec<f64> = template_cols
7829 .iter()
7830 .map(|track| {
7831 if matches!(track, GridTrackSize::Auto) {
7832 available_width / num_columns as f64
7835 } else {
7836 0.0
7837 }
7838 })
7839 .collect();
7840
7841 let col_widths =
7842 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7843
7844 let placements: Vec<Option<&GridPlacement>> = children
7846 .iter()
7847 .map(|child| child.style.grid_placement.as_ref())
7848 .collect();
7849
7850 let item_placements = grid::place_items(&placements, num_columns);
7852 let num_rows = grid::compute_num_rows(&item_placements);
7853
7854 if num_rows == 0 {
7855 return;
7856 }
7857
7858 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7860 for placement in &item_placements {
7861 let cell_width =
7862 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7863 let child = &children[placement.child_index];
7864 let child_style = child.style.resolve(Some(parent_style), cell_width);
7865 item_heights[placement.child_index] =
7866 self.measure_node_height(child, cell_width, &child_style, font_context);
7867 }
7868
7869 let template_rows = parent_style.grid_template_rows.as_deref();
7871 let mut row_heights = vec![0.0_f64; num_rows];
7872 for placement in &item_placements {
7873 let h = item_heights[placement.child_index];
7874 let span = placement.row_end - placement.row_start;
7875 let per_row = h / span as f64;
7876 for rh in row_heights
7877 .iter_mut()
7878 .take(placement.row_end.min(num_rows))
7879 .skip(placement.row_start)
7880 {
7881 if per_row > *rh {
7882 *rh = per_row;
7883 }
7884 }
7885 }
7886
7887 if let Some(template) = template_rows {
7889 let auto_row = parent_style.grid_auto_rows.as_ref();
7890 for (r, rh) in row_heights.iter_mut().enumerate() {
7891 let track = template.get(r).or(auto_row);
7892 if let Some(track) = track {
7893 match track {
7894 GridTrackSize::Pt(pts) => *rh = *pts,
7895 GridTrackSize::Auto => {} _ => {} }
7898 }
7899 }
7900 }
7901
7902 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
7904 if row_height > cursor.remaining_height() {
7909 pages.push(cursor.finalize());
7910 *cursor = cursor.new_page();
7911 }
7912
7913 let row_start_y = cursor.y;
7914
7915 for placement in &item_placements {
7917 if placement.row_start != row {
7918 continue; }
7920
7921 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
7922 let cell_width =
7923 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
7924
7925 let child = &children[placement.child_index];
7926
7927 self.layout_node(
7928 child,
7929 cursor,
7930 pages,
7931 cell_x,
7932 cell_width,
7933 Some(parent_style),
7934 font_context,
7935 None,
7936 None,
7937 );
7938 cursor.y = row_start_y;
7940 }
7941
7942 cursor.y = row_start_y + row_height + row_gap;
7943 }
7944
7945 if num_rows > 0 {
7947 cursor.y -= row_gap;
7948 }
7949 }
7950}
7951
7952fn end_y_of(frags: &[ItemFragments], last: usize) -> f64 {
7956 frags
7957 .iter()
7958 .filter(|f| f.pages.len() == last)
7959 .map(|f| f.final_y)
7960 .fold(f64::MIN, f64::max)
7961}
7962
7963fn paints_a_box(el: &LayoutElement) -> bool {
7971 match &el.draw {
7972 DrawCommand::Rect {
7973 background,
7974 border_width,
7975 ..
7976 } => {
7977 background.is_some()
7978 || border_width.top > 0.0
7979 || border_width.right > 0.0
7980 || border_width.bottom > 0.0
7981 || border_width.left > 0.0
7982 }
7983 _ => false,
7984 }
7985}
7986
7987fn drain_since(elements: &mut Vec<LayoutElement>, snapshot: usize) -> Vec<LayoutElement> {
8004 let start = snapshot.min(elements.len());
8005 elements.drain(start..).collect()
8006}
8007
8008fn stretch_fragment(elements: &mut [LayoutElement], bottom: f64) {
8009 for el in elements.iter_mut() {
8010 let grown = bottom - el.y;
8011 if grown > el.height {
8012 el.height = grown;
8013 }
8014 }
8015}
8016
8017struct ItemFragments {
8023 pages: Vec<LayoutPage>,
8024 tail: Vec<LayoutElement>,
8025 cursor: PageCursor,
8026 final_y: f64,
8027 stretches: bool,
8032}
8033
8034struct FlexItem<'a> {
8035 node: &'a Node,
8036 style: ResolvedStyle,
8037 base_width: f64,
8038 min_content_width: f64,
8039}
8040
8041fn first_text_snippet(node: &Node) -> Option<String> {
8044 fn walk(n: &Node) -> Option<&str> {
8045 match &n.kind {
8046 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
8047 if !content.trim().is_empty() {
8048 return Some(content.trim());
8049 }
8050 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
8051 return Some(r.content.trim());
8052 }
8053 None
8054 }
8055 _ => n.children.iter().find_map(walk),
8056 }
8057 }
8058 walk(node).map(|t| {
8059 let mut s: String = t.chars().take(32).collect();
8060 if t.chars().count() > 32 {
8061 s.push('…');
8062 }
8063 s
8064 })
8065}
8066
8067fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
8077 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
8078}
8079
8080#[cfg(test)]
8081mod tests {
8082 use super::*;
8083 use crate::font::FontContext;
8084
8085 fn make_text(content: &str, font_size: f64) -> Node {
8086 Node {
8087 kind: NodeKind::Text {
8088 content: content.to_string(),
8089 href: None,
8090 runs: vec![],
8091 },
8092 style: Style {
8093 font_size: Some(font_size),
8094 ..Default::default()
8095 },
8096 children: vec![],
8097 id: None,
8098 source_location: None,
8099 bookmark: None,
8100 href: None,
8101 alt: None,
8102 }
8103 }
8104
8105 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
8106 Node {
8107 kind: NodeKind::View,
8108 style,
8109 children,
8110 id: None,
8111 source_location: None,
8112 bookmark: None,
8113 href: None,
8114 alt: None,
8115 }
8116 }
8117
8118 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
8119 Node {
8120 kind: NodeKind::Text {
8121 content: String::new(),
8122 href: None,
8123 runs,
8124 },
8125 style: Style::default(),
8126 children: vec![],
8127 id: None,
8128 source_location: None,
8129 bookmark: None,
8130 href: None,
8131 alt: None,
8132 }
8133 }
8134
8135 #[test]
8136 fn intrinsic_width_measures_runs_not_just_content() {
8137 let engine = LayoutEngine::new();
8141 let font_context = FontContext::new();
8142
8143 let runs_node = make_runs_text(vec![
8144 crate::model::TextRun {
8145 content: "Hello ".to_string(),
8146 style: Style::default(),
8147 href: None,
8148 },
8149 crate::model::TextRun {
8150 content: "World".to_string(),
8151 style: Style {
8152 font_weight: Some(700),
8153 ..Default::default()
8154 },
8155 href: None,
8156 },
8157 ]);
8158 let plain_node = make_text("Hello World", 12.0);
8159
8160 let runs_style = runs_node.style.resolve(None, 0.0);
8161 let plain_style = plain_node.style.resolve(None, 0.0);
8162 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
8163 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
8164
8165 assert!(
8168 runs_w >= plain_w,
8169 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
8170 );
8171 assert!(
8172 runs_w < plain_w * 1.5,
8173 "runs width ({runs_w}) should be close to plain width ({plain_w})"
8174 );
8175 }
8176
8177 #[test]
8178 fn intrinsic_width_of_multiline_text_is_widest_line() {
8179 let engine = LayoutEngine::new();
8180 let font_context = FontContext::new();
8181
8182 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
8183 let widest = make_text("Springfield, IL 62704", 12.0);
8184
8185 let m_style = multiline.style.resolve(None, 0.0);
8186 let w_style = widest.style.resolve(None, 0.0);
8187 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
8188 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
8189
8190 assert!(
8191 (m_w - w_w).abs() < 0.01,
8192 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
8193 );
8194 }
8195
8196 #[test]
8197 fn intrinsic_width_of_heading_measures_its_text() {
8198 let engine = LayoutEngine::new();
8201 let font_context = FontContext::new();
8202
8203 let heading = Node {
8204 kind: NodeKind::Heading {
8205 level: 1,
8206 content: "Invoice #2024-001".to_string(),
8207 href: None,
8208 runs: vec![],
8209 },
8210 style: Style {
8211 font_size: Some(24.0),
8212 ..Default::default()
8213 },
8214 children: vec![],
8215 id: None,
8216 source_location: None,
8217 bookmark: None,
8218 href: None,
8219 alt: None,
8220 };
8221 let style = heading.style.resolve(None, 0.0);
8222 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
8223 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
8224 }
8225
8226 #[test]
8227 fn measure_node_height_of_wrapping_heading_matches_text() {
8228 let engine = LayoutEngine::new();
8235 let font_context = FontContext::new();
8236
8237 let content = "Annual Performance Review";
8238 let heading = Node {
8239 kind: NodeKind::Heading {
8240 level: 1,
8241 content: content.to_string(),
8242 href: None,
8243 runs: vec![],
8244 },
8245 style: Style {
8246 font_size: Some(32.0),
8247 ..Default::default()
8248 },
8249 children: vec![],
8250 id: None,
8251 source_location: None,
8252 bookmark: None,
8253 href: None,
8254 alt: None,
8255 };
8256 let text = make_text(content, 32.0);
8257
8258 let width = 200.0;
8260 let h_style = heading.style.resolve(None, width);
8261 let t_style = text.style.resolve(None, width);
8262 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
8263 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
8264
8265 assert!(
8266 h_height > 32.0,
8267 "a wrapping 32pt heading must measure more than one line, got {h_height}"
8268 );
8269 assert!(
8270 (h_height - t_height).abs() < 0.01,
8271 "heading height ({h_height}) must equal the same text's height ({t_height})"
8272 );
8273 }
8274
8275 #[test]
8276 fn intrinsic_width_flex_row_sums_children() {
8277 let engine = LayoutEngine::new();
8278 let font_context = FontContext::new();
8279
8280 let child1 = make_text("Hello", 14.0);
8281 let child2 = make_text("World", 14.0);
8282
8283 let child1_style = child1.style.resolve(None, 0.0);
8284 let child2_style = child2.style.resolve(None, 0.0);
8285 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
8286 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
8287
8288 let row = make_styled_view(
8289 Style {
8290 flex_direction: Some(FlexDirection::Row),
8291 ..Default::default()
8292 },
8293 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
8294 );
8295 let row_style = row.style.resolve(None, 0.0);
8296 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
8297
8298 assert!(
8299 (row_w - (child1_w + child2_w)).abs() < 0.01,
8300 "Row intrinsic width ({}) should equal sum of children ({} + {})",
8301 row_w,
8302 child1_w,
8303 child2_w
8304 );
8305 }
8306
8307 #[test]
8308 fn intrinsic_width_flex_column_takes_max() {
8309 let engine = LayoutEngine::new();
8310 let font_context = FontContext::new();
8311
8312 let short = make_text("Hi", 14.0);
8313 let long = make_text("Hello World", 14.0);
8314
8315 let short_style = short.style.resolve(None, 0.0);
8316 let long_style = long.style.resolve(None, 0.0);
8317 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
8318 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
8319
8320 let col = make_styled_view(
8321 Style {
8322 flex_direction: Some(FlexDirection::Column),
8323 ..Default::default()
8324 },
8325 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
8326 );
8327 let col_style = col.style.resolve(None, 0.0);
8328 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
8329
8330 assert!(
8331 (col_w - long_w).abs() < 0.01,
8332 "Column intrinsic width ({}) should equal max child ({}, short was {})",
8333 col_w,
8334 long_w,
8335 short_w
8336 );
8337 }
8338
8339 #[test]
8340 fn intrinsic_width_nested_containers() {
8341 let engine = LayoutEngine::new();
8342 let font_context = FontContext::new();
8343
8344 let inner = make_styled_view(
8345 Style {
8346 flex_direction: Some(FlexDirection::Row),
8347 ..Default::default()
8348 },
8349 vec![make_text("A", 12.0), make_text("B", 12.0)],
8350 );
8351 let inner_style = inner.style.resolve(None, 0.0);
8352 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
8353
8354 let outer = make_styled_view(
8355 Style::default(),
8356 vec![make_styled_view(
8357 Style {
8358 flex_direction: Some(FlexDirection::Row),
8359 ..Default::default()
8360 },
8361 vec![make_text("A", 12.0), make_text("B", 12.0)],
8362 )],
8363 );
8364 let outer_style = outer.style.resolve(None, 0.0);
8365 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
8366
8367 assert!(
8368 (outer_w - inner_w).abs() < 0.01,
8369 "Nested container ({}) should match inner container ({})",
8370 outer_w,
8371 inner_w
8372 );
8373 }
8374
8375 #[test]
8376 fn intrinsic_width_row_with_gap() {
8377 let engine = LayoutEngine::new();
8378 let font_context = FontContext::new();
8379
8380 let no_gap = make_styled_view(
8381 Style {
8382 flex_direction: Some(FlexDirection::Row),
8383 ..Default::default()
8384 },
8385 vec![make_text("A", 12.0), make_text("B", 12.0)],
8386 );
8387 let with_gap = make_styled_view(
8388 Style {
8389 flex_direction: Some(FlexDirection::Row),
8390 gap: Some(10.0),
8391 ..Default::default()
8392 },
8393 vec![make_text("A", 12.0), make_text("B", 12.0)],
8394 );
8395
8396 let no_gap_style = no_gap.style.resolve(None, 0.0);
8397 let with_gap_style = with_gap.style.resolve(None, 0.0);
8398 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
8399 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
8400
8401 assert!(
8402 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
8403 "Gap should add 10pt: with_gap={}, no_gap={}",
8404 with_gap_w,
8405 no_gap_w
8406 );
8407 }
8408
8409 #[test]
8410 fn intrinsic_width_empty_container() {
8411 let engine = LayoutEngine::new();
8412 let font_context = FontContext::new();
8413
8414 let padding = 8.0;
8415 let empty = make_styled_view(
8416 Style {
8417 padding: Some(Edges::uniform(padding)),
8418 ..Default::default()
8419 },
8420 vec![],
8421 );
8422 let style = empty.style.resolve(None, 0.0);
8423 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
8424
8425 assert!(
8426 (w - padding * 2.0).abs() < 0.01,
8427 "Empty container width ({}) should equal horizontal padding ({})",
8428 w,
8429 padding * 2.0
8430 );
8431 }
8432
8433 #[test]
8436 fn flex_shrink_respects_min_content_width() {
8437 let engine = LayoutEngine::new();
8441 let font_context = FontContext::new();
8442
8443 let sale_text = make_text("SALE", 12.0);
8444 let sale_style = sale_text.style.resolve(None, 0.0);
8445 let sale_word_width =
8446 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8447 assert!(
8448 sale_word_width > 0.0,
8449 "SALE should have non-zero min-content width"
8450 );
8451
8452 let container = make_styled_view(
8455 Style {
8456 flex_direction: Some(FlexDirection::Row),
8457 width: Some(Dimension::Pt(100.0)),
8458 ..Default::default()
8459 },
8460 vec![
8461 make_styled_view(
8462 Style {
8463 width: Some(Dimension::Pt(80.0)),
8464 flex_shrink: Some(1.0),
8465 ..Default::default()
8466 },
8467 vec![],
8468 ),
8469 make_styled_view(
8470 Style {
8471 width: Some(Dimension::Pt(60.0)),
8472 flex_shrink: Some(1.0),
8473 ..Default::default()
8474 },
8475 vec![make_text("SALE", 12.0)],
8476 ),
8477 ],
8478 );
8479
8480 let doc = Document {
8481 children: vec![Node::page(
8482 PageConfig::default(),
8483 Style::default(),
8484 vec![container],
8485 )],
8486 metadata: Default::default(),
8487 default_page: PageConfig::default(),
8488 first_page: None,
8489 left_page: None,
8490 right_page: None,
8491 named_pages: Default::default(),
8492 attachments: vec![],
8493 zugferd: None,
8494 fonts: vec![],
8495 tagged: false,
8496 pdfa: None,
8497 default_style: None,
8498 embedded_data: None,
8499 flatten_forms: false,
8500 pdf_ua: false,
8501 certification: None,
8502 pdf_version: Default::default(),
8503 pdf_ua2: false,
8504 };
8505
8506 let pages = engine.layout(&doc, &font_context);
8507 assert!(!pages.is_empty());
8508
8509 let page = &pages[0];
8512 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8514 assert!(
8515 container_el.is_some(),
8516 "Should find container with 2 children"
8517 );
8518 let sale_child = &container_el.unwrap().children[1];
8519 assert!(
8520 sale_child.width >= sale_word_width - 0.01,
8521 "SALE child width ({}) should be >= min-content width ({})",
8522 sale_child.width,
8523 sale_word_width
8524 );
8525 }
8526
8527 #[test]
8530 fn column_justify_content_center() {
8531 let engine = LayoutEngine::new();
8534 let font_context = FontContext::new();
8535
8536 let container = make_styled_view(
8537 Style {
8538 flex_direction: Some(FlexDirection::Column),
8539 height: Some(Dimension::Pt(200.0)),
8540 justify_content: Some(JustifyContent::Center),
8541 ..Default::default()
8542 },
8543 vec![make_text("Centered", 12.0)],
8544 );
8545
8546 let doc = Document {
8547 children: vec![Node::page(
8548 PageConfig::default(),
8549 Style::default(),
8550 vec![container],
8551 )],
8552 metadata: Default::default(),
8553 default_page: PageConfig::default(),
8554 first_page: None,
8555 left_page: None,
8556 right_page: None,
8557 named_pages: Default::default(),
8558 attachments: vec![],
8559 zugferd: None,
8560 fonts: vec![],
8561 tagged: false,
8562 pdfa: None,
8563 default_style: None,
8564 embedded_data: None,
8565 flatten_forms: false,
8566 pdf_ua: false,
8567 certification: None,
8568 pdf_version: Default::default(),
8569 pdf_ua2: false,
8570 };
8571
8572 let pages = engine.layout(&doc, &font_context);
8573 let page = &pages[0];
8574
8575 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8578 assert!(
8579 container_el.is_some(),
8580 "Should find container with children"
8581 );
8582 let container_el = container_el.unwrap();
8583 let child = &container_el.children[0];
8584
8585 let child_offset = child.y - container_el.y;
8587 let expected_offset = (200.0 - child.height) / 2.0;
8588 assert!(
8589 (child_offset - expected_offset).abs() < 2.0,
8590 "Child offset ({}) should be near center ({})",
8591 child_offset,
8592 expected_offset
8593 );
8594 }
8595
8596 #[test]
8597 fn column_align_items_center() {
8598 let engine = LayoutEngine::new();
8601 let font_context = FontContext::new();
8602
8603 let container = make_styled_view(
8604 Style {
8605 flex_direction: Some(FlexDirection::Column),
8606 width: Some(Dimension::Pt(300.0)),
8607 align_items: Some(AlignItems::Center),
8608 ..Default::default()
8609 },
8610 vec![make_text("Hi", 12.0)],
8611 );
8612
8613 let doc = Document {
8614 children: vec![Node::page(
8615 PageConfig::default(),
8616 Style::default(),
8617 vec![container],
8618 )],
8619 metadata: Default::default(),
8620 default_page: PageConfig::default(),
8621 first_page: None,
8622 left_page: None,
8623 right_page: None,
8624 named_pages: Default::default(),
8625 attachments: vec![],
8626 zugferd: None,
8627 fonts: vec![],
8628 tagged: false,
8629 pdfa: None,
8630 default_style: None,
8631 embedded_data: None,
8632 flatten_forms: false,
8633 pdf_ua: false,
8634 certification: None,
8635 pdf_version: Default::default(),
8636 pdf_ua2: false,
8637 };
8638
8639 let pages = engine.layout(&doc, &font_context);
8640 let page = &pages[0];
8641
8642 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8643 assert!(container_el.is_some());
8644 let container_el = container_el.unwrap();
8645 let child = &container_el.children[0];
8646
8647 let child_center = child.x + child.width / 2.0;
8649 let container_center = container_el.x + container_el.width / 2.0;
8650 assert!(
8651 (child_center - container_center).abs() < 2.0,
8652 "Child center ({}) should be near container center ({})",
8653 child_center,
8654 container_center
8655 );
8656 }
8657
8658 #[test]
8661 fn absolute_child_positioned_relative_to_parent() {
8662 let engine = LayoutEngine::new();
8666 let font_context = FontContext::new();
8667
8668 let parent = make_styled_view(
8669 Style {
8670 position: Some(crate::model::Position::Relative),
8671 margin: Some(MarginEdges::from_edges(Edges {
8672 top: 50.0,
8673 left: 50.0,
8674 ..Default::default()
8675 })),
8676 width: Some(Dimension::Pt(200.0)),
8677 height: Some(Dimension::Pt(200.0)),
8678 ..Default::default()
8679 },
8680 vec![make_styled_view(
8681 Style {
8682 position: Some(crate::model::Position::Absolute),
8683 top: Some(10.0),
8684 left: Some(10.0),
8685 width: Some(Dimension::Pt(50.0)),
8686 height: Some(Dimension::Pt(50.0)),
8687 ..Default::default()
8688 },
8689 vec![],
8690 )],
8691 );
8692
8693 let doc = Document {
8694 children: vec![Node::page(
8695 PageConfig::default(),
8696 Style::default(),
8697 vec![parent],
8698 )],
8699 metadata: Default::default(),
8700 default_page: PageConfig::default(),
8701 first_page: None,
8702 left_page: None,
8703 right_page: None,
8704 named_pages: Default::default(),
8705 attachments: vec![],
8706 zugferd: None,
8707 fonts: vec![],
8708 tagged: false,
8709 pdfa: None,
8710 default_style: None,
8711 embedded_data: None,
8712 flatten_forms: false,
8713 pdf_ua: false,
8714 certification: None,
8715 pdf_version: Default::default(),
8716 pdf_ua2: false,
8717 };
8718
8719 let pages = engine.layout(&doc, &font_context);
8720 let page = &pages[0];
8721
8722 let parent_el = page
8725 .elements
8726 .iter()
8727 .find(|e| e.width > 190.0 && e.width < 210.0);
8728 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8729 let parent_el = parent_el.unwrap();
8730
8731 let abs_child = parent_el
8733 .children
8734 .iter()
8735 .find(|e| e.width > 45.0 && e.width < 55.0);
8736 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8737 let abs_child = abs_child.unwrap();
8738
8739 let expected_x = parent_el.x + 10.0;
8740 let expected_y = parent_el.y + 10.0;
8741 assert!(
8742 (abs_child.x - expected_x).abs() < 1.0,
8743 "Absolute child x ({}) should be parent.x + 10 ({})",
8744 abs_child.x,
8745 expected_x
8746 );
8747 assert!(
8748 (abs_child.y - expected_y).abs() < 1.0,
8749 "Absolute child y ({}) should be parent.y + 10 ({})",
8750 abs_child.y,
8751 expected_y
8752 );
8753 }
8754
8755 #[test]
8756 fn absolute_escapes_unpositioned_parent_to_page() {
8757 let engine = LayoutEngine::new();
8762 let font_context = FontContext::new();
8763 let parent = make_styled_view(
8764 Style {
8765 margin: Some(MarginEdges::from_edges(Edges {
8766 top: 50.0,
8767 left: 50.0,
8768 ..Default::default()
8769 })),
8770 width: Some(Dimension::Pt(200.0)),
8771 height: Some(Dimension::Pt(200.0)),
8772 ..Default::default()
8773 },
8774 vec![make_styled_view(
8775 Style {
8776 position: Some(crate::model::Position::Absolute),
8777 top: Some(10.0),
8778 left: Some(10.0),
8779 width: Some(Dimension::Pt(50.0)),
8780 height: Some(Dimension::Pt(50.0)),
8781 ..Default::default()
8782 },
8783 vec![],
8784 )],
8785 );
8786 let doc = Document {
8787 children: vec![Node::page(
8788 PageConfig::default(),
8789 Style::default(),
8790 vec![parent],
8791 )],
8792 metadata: Default::default(),
8793 default_page: PageConfig::default(),
8794 first_page: None,
8795 left_page: None,
8796 right_page: None,
8797 named_pages: Default::default(),
8798 attachments: vec![],
8799 zugferd: None,
8800 fonts: vec![],
8801 tagged: false,
8802 pdfa: None,
8803 default_style: None,
8804 embedded_data: None,
8805 flatten_forms: false,
8806 pdf_ua: false,
8807 certification: None,
8808 pdf_version: Default::default(),
8809 pdf_ua2: false,
8810 };
8811 let pages = engine.layout(&doc, &font_context);
8812 let page = &pages[0];
8813 let parent_el = page
8814 .elements
8815 .iter()
8816 .find(|e| e.width > 190.0 && e.width < 210.0)
8817 .expect("parent");
8818 let abs_child = parent_el
8819 .children
8820 .iter()
8821 .find(|e| e.width > 45.0 && e.width < 55.0)
8822 .expect("abs child");
8823 let page_left = PageConfig::default().margin.left;
8824 let page_top = PageConfig::default().margin.top;
8825 assert!(
8826 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
8827 "absolute escapes to the page: x {} should be page_left + 10 ({})",
8828 abs_child.x,
8829 page_left + 10.0
8830 );
8831 assert!(
8832 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
8833 "absolute escapes to the page: y {} should be page_top + 10 ({})",
8834 abs_child.y,
8835 page_top + 10.0
8836 );
8837 assert!(
8838 abs_child.x < parent_el.x,
8839 "child must no longer be parent-relative (parent is 50pt further in)"
8840 );
8841 }
8842
8843 #[test]
8844 fn text_transform_none_passthrough() {
8845 assert_eq!(
8846 apply_text_transform("Hello World", TextTransform::None),
8847 "Hello World"
8848 );
8849 }
8850
8851 #[test]
8852 fn text_transform_uppercase() {
8853 assert_eq!(
8854 apply_text_transform("hello world", TextTransform::Uppercase),
8855 "HELLO WORLD"
8856 );
8857 }
8858
8859 #[test]
8860 fn text_transform_lowercase() {
8861 assert_eq!(
8862 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
8863 "hello world"
8864 );
8865 }
8866
8867 #[test]
8868 fn text_transform_capitalize() {
8869 assert_eq!(
8870 apply_text_transform("hello world", TextTransform::Capitalize),
8871 "Hello World"
8872 );
8873 assert_eq!(
8874 apply_text_transform(" hello world ", TextTransform::Capitalize),
8875 " Hello World "
8876 );
8877 assert_eq!(
8878 apply_text_transform("already Capitalized", TextTransform::Capitalize),
8879 "Already Capitalized"
8880 );
8881 }
8882
8883 #[test]
8884 fn text_transform_capitalize_empty() {
8885 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
8886 }
8887
8888 #[test]
8889 fn apply_char_transform_uppercase() {
8890 assert_eq!(
8891 apply_char_transform('a', TextTransform::Uppercase, false),
8892 'A'
8893 );
8894 assert_eq!(
8895 apply_char_transform('A', TextTransform::Uppercase, false),
8896 'A'
8897 );
8898 }
8899
8900 #[test]
8901 fn apply_char_transform_capitalize_word_start() {
8902 assert_eq!(
8903 apply_char_transform('h', TextTransform::Capitalize, true),
8904 'H'
8905 );
8906 assert_eq!(
8907 apply_char_transform('h', TextTransform::Capitalize, false),
8908 'h'
8909 );
8910 }
8911
8912 #[test]
8915 fn column_flex_grow_single_child_fills_container() {
8916 let engine = LayoutEngine::new();
8919 let font_context = FontContext::new();
8920
8921 let child = make_styled_view(
8922 Style {
8923 flex_grow: Some(1.0),
8924 ..Default::default()
8925 },
8926 vec![make_text("Short", 12.0)],
8927 );
8928
8929 let container = make_styled_view(
8930 Style {
8931 flex_direction: Some(FlexDirection::Column),
8932 height: Some(Dimension::Pt(300.0)),
8933 ..Default::default()
8934 },
8935 vec![child],
8936 );
8937
8938 let doc = Document {
8939 children: vec![Node::page(
8940 PageConfig::default(),
8941 Style::default(),
8942 vec![container],
8943 )],
8944 metadata: Default::default(),
8945 default_page: PageConfig::default(),
8946 first_page: None,
8947 left_page: None,
8948 right_page: None,
8949 named_pages: Default::default(),
8950 attachments: vec![],
8951 zugferd: None,
8952 fonts: vec![],
8953 tagged: false,
8954 pdfa: None,
8955 default_style: None,
8956 embedded_data: None,
8957 flatten_forms: false,
8958 pdf_ua: false,
8959 certification: None,
8960 pdf_version: Default::default(),
8961 pdf_ua2: false,
8962 };
8963
8964 let pages = engine.layout(&doc, &font_context);
8965 let page = &pages[0];
8966
8967 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8968 assert!(container_el.is_some());
8969 let container_el = container_el.unwrap();
8970 assert!(
8971 (container_el.height - 300.0).abs() < 1.0,
8972 "Container should be 300pt, got {}",
8973 container_el.height
8974 );
8975
8976 let child_el = &container_el.children[0];
8977 assert!(
8978 (child_el.height - 300.0).abs() < 1.0,
8979 "flex-grow child should expand to 300pt, got {}",
8980 child_el.height
8981 );
8982 }
8983
8984 #[test]
8985 fn column_flex_grow_two_children_proportional() {
8986 let engine = LayoutEngine::new();
8989 let font_context = FontContext::new();
8990
8991 let child1 = make_styled_view(
8992 Style {
8993 flex_grow: Some(1.0),
8994 ..Default::default()
8995 },
8996 vec![make_text("A", 12.0)],
8997 );
8998 let child2 = make_styled_view(
8999 Style {
9000 flex_grow: Some(2.0),
9001 ..Default::default()
9002 },
9003 vec![make_text("B", 12.0)],
9004 );
9005
9006 let container = make_styled_view(
9007 Style {
9008 flex_direction: Some(FlexDirection::Column),
9009 height: Some(Dimension::Pt(300.0)),
9010 ..Default::default()
9011 },
9012 vec![child1, child2],
9013 );
9014
9015 let doc = Document {
9016 children: vec![Node::page(
9017 PageConfig::default(),
9018 Style::default(),
9019 vec![container],
9020 )],
9021 metadata: Default::default(),
9022 default_page: PageConfig::default(),
9023 first_page: None,
9024 left_page: None,
9025 right_page: None,
9026 named_pages: Default::default(),
9027 attachments: vec![],
9028 zugferd: None,
9029 fonts: vec![],
9030 tagged: false,
9031 pdfa: None,
9032 default_style: None,
9033 embedded_data: None,
9034 flatten_forms: false,
9035 pdf_ua: false,
9036 certification: None,
9037 pdf_version: Default::default(),
9038 pdf_ua2: false,
9039 };
9040
9041 let pages = engine.layout(&doc, &font_context);
9042 let page = &pages[0];
9043
9044 let container_el = page
9045 .elements
9046 .iter()
9047 .find(|e| e.children.len() == 2)
9048 .expect("Should find container with two children");
9049
9050 let c1 = &container_el.children[0];
9051 let c2 = &container_el.children[1];
9052
9053 let total = c1.height + c2.height;
9057 assert!(
9058 (total - 300.0).abs() < 2.0,
9059 "Children should sum to ~300pt, got {}",
9060 total
9061 );
9062
9063 let ratio = c2.height / c1.height;
9066 assert!(
9067 ratio > 1.3 && ratio < 2.5,
9068 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
9069 ratio
9070 );
9071 }
9072
9073 #[test]
9074 fn column_flex_grow_mixed_grow_and_fixed() {
9075 let engine = LayoutEngine::new();
9078 let font_context = FontContext::new();
9079
9080 let fixed_child = make_styled_view(
9081 Style {
9082 height: Some(Dimension::Pt(50.0)),
9083 ..Default::default()
9084 },
9085 vec![make_text("Fixed", 12.0)],
9086 );
9087 let grow_child = make_styled_view(
9088 Style {
9089 flex_grow: Some(1.0),
9090 ..Default::default()
9091 },
9092 vec![make_text("Grow", 12.0)],
9093 );
9094
9095 let container = make_styled_view(
9096 Style {
9097 flex_direction: Some(FlexDirection::Column),
9098 height: Some(Dimension::Pt(300.0)),
9099 ..Default::default()
9100 },
9101 vec![fixed_child, grow_child],
9102 );
9103
9104 let doc = Document {
9105 children: vec![Node::page(
9106 PageConfig::default(),
9107 Style::default(),
9108 vec![container],
9109 )],
9110 metadata: Default::default(),
9111 default_page: PageConfig::default(),
9112 first_page: None,
9113 left_page: None,
9114 right_page: None,
9115 named_pages: Default::default(),
9116 attachments: vec![],
9117 zugferd: None,
9118 fonts: vec![],
9119 tagged: false,
9120 pdfa: None,
9121 default_style: None,
9122 embedded_data: None,
9123 flatten_forms: false,
9124 pdf_ua: false,
9125 certification: None,
9126 pdf_version: Default::default(),
9127 pdf_ua2: false,
9128 };
9129
9130 let pages = engine.layout(&doc, &font_context);
9131 let page = &pages[0];
9132
9133 let container_el = page
9134 .elements
9135 .iter()
9136 .find(|e| e.children.len() == 2)
9137 .expect("Should find container with two children");
9138
9139 let fixed_el = &container_el.children[0];
9140 let grow_el = &container_el.children[1];
9141
9142 assert!(
9144 (fixed_el.height - 50.0).abs() < 1.0,
9145 "Fixed child should stay at 50pt, got {}",
9146 fixed_el.height
9147 );
9148
9149 assert!(
9151 (grow_el.height - 250.0).abs() < 2.0,
9152 "Grow child should expand to ~250pt, got {}",
9153 grow_el.height
9154 );
9155 }
9156
9157 #[test]
9158 fn column_flex_grow_page_level() {
9159 let engine = LayoutEngine::new();
9161 let font_context = FontContext::new();
9162
9163 let grow_child = make_styled_view(
9164 Style {
9165 flex_grow: Some(1.0),
9166 ..Default::default()
9167 },
9168 vec![make_text("Fill page", 12.0)],
9169 );
9170
9171 let doc = Document {
9172 children: vec![Node::page(
9173 PageConfig::default(),
9174 Style::default(),
9175 vec![grow_child],
9176 )],
9177 metadata: Default::default(),
9178 default_page: PageConfig::default(),
9179 first_page: None,
9180 left_page: None,
9181 right_page: None,
9182 named_pages: Default::default(),
9183 attachments: vec![],
9184 zugferd: None,
9185 fonts: vec![],
9186 tagged: false,
9187 pdfa: None,
9188 default_style: None,
9189 embedded_data: None,
9190 flatten_forms: false,
9191 pdf_ua: false,
9192 certification: None,
9193 pdf_version: Default::default(),
9194 pdf_ua2: false,
9195 };
9196
9197 let pages = engine.layout(&doc, &font_context);
9198 let page = &pages[0];
9199
9200 assert!(
9202 !page.elements.is_empty(),
9203 "Page should have at least one element"
9204 );
9205
9206 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
9207 let el = &page.elements[0];
9208 assert!(
9209 (el.height - content_height).abs() < 2.0,
9210 "Page-level flex-grow child should fill content height ({}), got {}",
9211 content_height,
9212 el.height
9213 );
9214 }
9215
9216 #[test]
9217 fn column_flex_grow_with_justify_content() {
9218 let engine = LayoutEngine::new();
9222 let font_context = FontContext::new();
9223
9224 let fixed_child = make_styled_view(
9225 Style {
9226 height: Some(Dimension::Pt(50.0)),
9227 ..Default::default()
9228 },
9229 vec![make_text("Top", 12.0)],
9230 );
9231 let grow_child = make_styled_view(
9232 Style {
9233 flex_grow: Some(1.0),
9234 ..Default::default()
9235 },
9236 vec![make_text("Fill", 12.0)],
9237 );
9238
9239 let container = make_styled_view(
9240 Style {
9241 flex_direction: Some(FlexDirection::Column),
9242 height: Some(Dimension::Pt(300.0)),
9243 justify_content: Some(JustifyContent::Center),
9244 ..Default::default()
9245 },
9246 vec![fixed_child, grow_child],
9247 );
9248
9249 let doc = Document {
9250 children: vec![Node::page(
9251 PageConfig::default(),
9252 Style::default(),
9253 vec![container],
9254 )],
9255 metadata: Default::default(),
9256 default_page: PageConfig::default(),
9257 first_page: None,
9258 left_page: None,
9259 right_page: None,
9260 named_pages: Default::default(),
9261 attachments: vec![],
9262 zugferd: None,
9263 fonts: vec![],
9264 tagged: false,
9265 pdfa: None,
9266 default_style: None,
9267 embedded_data: None,
9268 flatten_forms: false,
9269 pdf_ua: false,
9270 certification: None,
9271 pdf_version: Default::default(),
9272 pdf_ua2: false,
9273 };
9274
9275 let pages = engine.layout(&doc, &font_context);
9276 let page = &pages[0];
9277
9278 let container_el = page
9279 .elements
9280 .iter()
9281 .find(|e| e.children.len() == 2)
9282 .expect("Should find container");
9283
9284 let first_child = &container_el.children[0];
9287 assert!(
9288 (first_child.y - container_el.y).abs() < 1.0,
9289 "First child should be at top of container"
9290 );
9291
9292 let total = container_el.children[0].height + container_el.children[1].height;
9294 assert!(
9295 (total - 300.0).abs() < 2.0,
9296 "Children should fill container, got {}",
9297 total
9298 );
9299 }
9300
9301 #[test]
9302 fn column_flex_grow_child_justify_content_center() {
9303 let engine = LayoutEngine::new();
9306 let font_context = FontContext::new();
9307
9308 let inner_box = make_styled_view(
9310 Style {
9311 height: Some(Dimension::Pt(40.0)),
9312 ..Default::default()
9313 },
9314 vec![make_text("Centered", 12.0)],
9315 );
9316
9317 let grow_child = make_styled_view(
9319 Style {
9320 flex_grow: Some(1.0),
9321 flex_direction: Some(FlexDirection::Column),
9322 justify_content: Some(JustifyContent::Center),
9323 ..Default::default()
9324 },
9325 vec![inner_box],
9326 );
9327
9328 let container = make_styled_view(
9330 Style {
9331 flex_direction: Some(FlexDirection::Column),
9332 height: Some(Dimension::Pt(400.0)),
9333 ..Default::default()
9334 },
9335 vec![grow_child],
9336 );
9337
9338 let doc = Document {
9339 children: vec![Node::page(
9340 PageConfig::default(),
9341 Style::default(),
9342 vec![container],
9343 )],
9344 metadata: Default::default(),
9345 default_page: PageConfig::default(),
9346 first_page: None,
9347 left_page: None,
9348 right_page: None,
9349 named_pages: Default::default(),
9350 attachments: vec![],
9351 zugferd: None,
9352 fonts: vec![],
9353 tagged: false,
9354 pdfa: None,
9355 default_style: None,
9356 embedded_data: None,
9357 flatten_forms: false,
9358 pdf_ua: false,
9359 certification: None,
9360 pdf_version: Default::default(),
9361 pdf_ua2: false,
9362 };
9363
9364 let pages = engine.layout(&doc, &font_context);
9365 let page = &pages[0];
9366
9367 let container_el = page
9369 .elements
9370 .iter()
9371 .find(|e| e.height > 350.0 && e.children.len() == 1)
9372 .expect("Should find outer container");
9373
9374 let grow_el = &container_el.children[0];
9375 assert!(
9376 (grow_el.height - 400.0).abs() < 2.0,
9377 "Grow child should expand to 400, got {}",
9378 grow_el.height
9379 );
9380
9381 let inner_el = &grow_el.children[0];
9383 let expected_center = grow_el.y + grow_el.height / 2.0;
9384 let actual_center = inner_el.y + inner_el.height / 2.0;
9385 assert!(
9386 (actual_center - expected_center).abs() < 2.0,
9387 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
9388 expected_center,
9389 actual_center
9390 );
9391 }
9392
9393 #[test]
9394 fn column_flex_grow_child_justify_content_flex_end() {
9395 let engine = LayoutEngine::new();
9397 let font_context = FontContext::new();
9398
9399 let inner_box = make_styled_view(
9400 Style {
9401 height: Some(Dimension::Pt(30.0)),
9402 ..Default::default()
9403 },
9404 vec![make_text("Bottom", 12.0)],
9405 );
9406
9407 let grow_child = make_styled_view(
9408 Style {
9409 flex_grow: Some(1.0),
9410 flex_direction: Some(FlexDirection::Column),
9411 justify_content: Some(JustifyContent::FlexEnd),
9412 ..Default::default()
9413 },
9414 vec![inner_box],
9415 );
9416
9417 let container = make_styled_view(
9418 Style {
9419 flex_direction: Some(FlexDirection::Column),
9420 height: Some(Dimension::Pt(300.0)),
9421 ..Default::default()
9422 },
9423 vec![grow_child],
9424 );
9425
9426 let doc = Document {
9427 children: vec![Node::page(
9428 PageConfig::default(),
9429 Style::default(),
9430 vec![container],
9431 )],
9432 metadata: Default::default(),
9433 default_page: PageConfig::default(),
9434 first_page: None,
9435 left_page: None,
9436 right_page: None,
9437 named_pages: Default::default(),
9438 attachments: vec![],
9439 zugferd: None,
9440 fonts: vec![],
9441 tagged: false,
9442 pdfa: None,
9443 default_style: None,
9444 embedded_data: None,
9445 flatten_forms: false,
9446 pdf_ua: false,
9447 certification: None,
9448 pdf_version: Default::default(),
9449 pdf_ua2: false,
9450 };
9451
9452 let pages = engine.layout(&doc, &font_context);
9453 let page = &pages[0];
9454
9455 let container_el = page
9456 .elements
9457 .iter()
9458 .find(|e| e.height > 250.0 && e.children.len() == 1)
9459 .expect("Should find outer container");
9460
9461 let grow_el = &container_el.children[0];
9462 let inner_el = &grow_el.children[0];
9463
9464 let inner_bottom = inner_el.y + inner_el.height;
9466 let grow_bottom = grow_el.y + grow_el.height;
9467 assert!(
9468 (inner_bottom - grow_bottom).abs() < 2.0,
9469 "Inner box bottom ({}) should align with grow child bottom ({})",
9470 inner_bottom,
9471 grow_bottom
9472 );
9473 }
9474
9475 #[test]
9476 fn column_flex_grow_child_no_justify_unchanged() {
9477 let engine = LayoutEngine::new();
9479 let font_context = FontContext::new();
9480
9481 let inner_box = make_styled_view(
9482 Style {
9483 height: Some(Dimension::Pt(50.0)),
9484 ..Default::default()
9485 },
9486 vec![make_text("Top", 12.0)],
9487 );
9488
9489 let grow_child = make_styled_view(
9490 Style {
9491 flex_grow: Some(1.0),
9492 flex_direction: Some(FlexDirection::Column),
9493 ..Default::default()
9495 },
9496 vec![inner_box],
9497 );
9498
9499 let container = make_styled_view(
9500 Style {
9501 flex_direction: Some(FlexDirection::Column),
9502 height: Some(Dimension::Pt(300.0)),
9503 ..Default::default()
9504 },
9505 vec![grow_child],
9506 );
9507
9508 let doc = Document {
9509 children: vec![Node::page(
9510 PageConfig::default(),
9511 Style::default(),
9512 vec![container],
9513 )],
9514 metadata: Default::default(),
9515 default_page: PageConfig::default(),
9516 first_page: None,
9517 left_page: None,
9518 right_page: None,
9519 named_pages: Default::default(),
9520 attachments: vec![],
9521 zugferd: None,
9522 fonts: vec![],
9523 tagged: false,
9524 pdfa: None,
9525 default_style: None,
9526 embedded_data: None,
9527 flatten_forms: false,
9528 pdf_ua: false,
9529 certification: None,
9530 pdf_version: Default::default(),
9531 pdf_ua2: false,
9532 };
9533
9534 let pages = engine.layout(&doc, &font_context);
9535 let page = &pages[0];
9536
9537 let container_el = page
9538 .elements
9539 .iter()
9540 .find(|e| e.height > 250.0 && e.children.len() == 1)
9541 .expect("Should find outer container");
9542
9543 let grow_el = &container_el.children[0];
9544 let inner_el = &grow_el.children[0];
9545
9546 assert!(
9548 (inner_el.y - grow_el.y).abs() < 2.0,
9549 "Inner box ({}) should be at top of grow child ({})",
9550 inner_el.y,
9551 grow_el.y
9552 );
9553 }
9554
9555 #[test]
9556 fn column_flex_grow_child_align_items_center() {
9557 let engine = LayoutEngine::new();
9559 let font_context = FontContext::new();
9560
9561 let text = make_text("Hello", 12.0);
9562
9563 let grow_child = make_styled_view(
9564 Style {
9565 flex_grow: Some(1.0),
9566 flex_direction: Some(FlexDirection::Column),
9567 align_items: Some(AlignItems::Center),
9568 ..Default::default()
9569 },
9570 vec![text],
9571 );
9572
9573 let container = make_styled_view(
9574 Style {
9575 flex_direction: Some(FlexDirection::Column),
9576 height: Some(Dimension::Pt(300.0)),
9577 ..Default::default()
9578 },
9579 vec![grow_child],
9580 );
9581
9582 let doc = Document {
9583 children: vec![Node::page(
9584 PageConfig::default(),
9585 Style::default(),
9586 vec![container],
9587 )],
9588 metadata: Default::default(),
9589 default_page: PageConfig::default(),
9590 first_page: None,
9591 left_page: None,
9592 right_page: None,
9593 named_pages: Default::default(),
9594 attachments: vec![],
9595 zugferd: None,
9596 fonts: vec![],
9597 tagged: false,
9598 pdfa: None,
9599 default_style: None,
9600 embedded_data: None,
9601 flatten_forms: false,
9602 pdf_ua: false,
9603 certification: None,
9604 pdf_version: Default::default(),
9605 pdf_ua2: false,
9606 };
9607
9608 let pages = engine.layout(&doc, &font_context);
9609 let page = &pages[0];
9610
9611 let container_el = page
9612 .elements
9613 .iter()
9614 .find(|e| e.height > 250.0 && e.children.len() == 1)
9615 .expect("Should find outer container");
9616
9617 let grow_el = &container_el.children[0];
9618 assert!(
9619 !grow_el.children.is_empty(),
9620 "Grow child should have text child"
9621 );
9622
9623 let text_el = &grow_el.children[0];
9624 let text_center = text_el.x + text_el.width / 2.0;
9625 let grow_center = grow_el.x + grow_el.width / 2.0;
9626 assert!(
9627 (text_center - grow_center).abs() < 2.0,
9628 "Text center ({}) should be near grow child center ({})",
9629 text_center,
9630 grow_center
9631 );
9632 }
9633
9634 #[test]
9635 fn image_intrinsic_width_respects_height_constraint() {
9636 let engine = LayoutEngine::new();
9640 let font_context = FontContext::new();
9641
9642 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9644
9645 let image_node = Node {
9646 kind: NodeKind::Image {
9647 src: one_px_png.to_string(),
9648 width: None,
9649 height: Some(36.0),
9650 },
9651 style: Style::default(),
9652 children: vec![],
9653 id: None,
9654 source_location: None,
9655 bookmark: None,
9656 href: None,
9657 alt: None,
9658 };
9659
9660 let resolved = image_node.style.resolve(None, 0.0);
9661 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9662
9663 assert!(
9665 (intrinsic - 36.0).abs() < 1.0,
9666 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9667 intrinsic
9668 );
9669 }
9670}