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