1pub mod flex;
35pub mod grid;
36pub mod page_break;
37
38use std::cell::RefCell;
39use std::collections::HashMap;
40
41use serde::Serialize;
42
43use crate::font::FontContext;
44use crate::model::*;
45use crate::style::*;
46use crate::text::bidi;
47use crate::text::shaping;
48use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
49
50#[derive(Debug, Clone, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BookmarkEntry {
54 pub title: String,
55 pub page_index: usize,
56 pub y: f64,
57}
58
59#[derive(Debug, Clone, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct LayoutInfo {
65 pub pages: Vec<PageInfo>,
66}
67
68#[derive(Debug, Clone, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct PageInfo {
72 pub width: f64,
73 pub height: f64,
74 pub content_x: f64,
75 pub content_y: f64,
76 pub content_width: f64,
77 pub content_height: f64,
78 pub elements: Vec<ElementInfo>,
79}
80
81#[derive(Debug, Clone, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct ElementStyleInfo {
85 pub margin: Edges,
87 pub padding: Edges,
88 pub border_width: Edges,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub width: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub height: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub min_width: Option<f64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_height: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub max_width: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_height: Option<f64>,
101 pub flex_direction: FlexDirection,
103 pub justify_content: JustifyContent,
104 pub align_items: AlignItems,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub align_self: Option<AlignItems>,
107 pub flex_wrap: FlexWrap,
108 pub align_content: AlignContent,
109 pub flex_grow: f64,
110 pub flex_shrink: f64,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub flex_basis: Option<String>,
113 pub gap: f64,
114 pub row_gap: f64,
115 pub column_gap: f64,
116 pub font_family: String,
118 pub font_size: f64,
119 pub font_weight: u32,
120 pub font_style: FontStyle,
121 pub line_height: f64,
122 pub text_align: TextAlign,
123 pub letter_spacing: f64,
124 pub text_decoration: TextDecoration,
125 pub text_transform: TextTransform,
126 pub color: Color,
128 pub background_color: Option<Color>,
129 pub border_color: EdgeValues<Color>,
130 pub border_style: EdgeValues<crate::style::BorderStyle>,
131 pub border_radius: CornerValues,
132 pub opacity: f64,
133 pub position: Position,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub top: Option<f64>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub right: Option<f64>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub bottom: Option<f64>,
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub left: Option<f64>,
143 pub overflow: Overflow,
145 pub breakable: bool,
147 pub break_before: bool,
148 pub min_widow_lines: u32,
149 pub min_orphan_lines: u32,
150}
151
152fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
153 match sc {
154 SizeConstraint::Auto => None,
155 SizeConstraint::Fixed(v) => Some(format!("{v}")),
156 }
157}
158
159impl ElementStyleInfo {
160 fn from_resolved(style: &ResolvedStyle) -> Self {
161 ElementStyleInfo {
162 margin: style.margin.to_edges(),
163 padding: style.padding,
164 border_width: style.border_width,
165 width: size_constraint_to_str(&style.width),
166 height: size_constraint_to_str(&style.height),
167 min_width: if style.min_width > 0.0 {
168 Some(style.min_width)
169 } else {
170 None
171 },
172 min_height: if style.min_height > 0.0 {
173 Some(style.min_height)
174 } else {
175 None
176 },
177 max_width: if style.max_width.is_finite() {
178 Some(style.max_width)
179 } else {
180 None
181 },
182 max_height: if style.max_height.is_finite() {
183 Some(style.max_height)
184 } else {
185 None
186 },
187 flex_direction: style.flex_direction,
188 justify_content: style.justify_content,
189 align_items: style.align_items,
190 align_self: style.align_self,
191 flex_wrap: style.flex_wrap,
192 align_content: style.align_content,
193 flex_grow: style.flex_grow,
194 flex_shrink: style.flex_shrink,
195 flex_basis: size_constraint_to_str(&style.flex_basis),
196 gap: style.gap,
197 row_gap: style.row_gap,
198 column_gap: style.column_gap,
199 font_family: style.font_family.clone(),
200 font_size: style.font_size,
201 font_weight: style.font_weight,
202 font_style: style.font_style,
203 line_height: style.line_height,
204 text_align: style.text_align,
205 letter_spacing: style.letter_spacing,
206 text_decoration: style.text_decoration,
207 text_transform: style.text_transform,
208 color: style.color,
209 background_color: style.background_color,
210 border_color: style.border_color,
211 border_style: style.border_style,
212 border_radius: style.border_radius,
213 opacity: style.opacity,
214 position: style.position,
215 top: style.top,
216 right: style.right,
217 bottom: style.bottom,
218 left: style.left,
219 overflow: style.overflow,
220 breakable: style.breakable,
221 break_before: style.break_before,
222 min_widow_lines: style.min_widow_lines,
223 min_orphan_lines: style.min_orphan_lines,
224 }
225 }
226}
227
228impl Default for ElementStyleInfo {
229 fn default() -> Self {
230 ElementStyleInfo {
231 margin: Edges::default(),
232 padding: Edges::default(),
233 border_width: Edges::default(),
234 width: None,
235 height: None,
236 min_width: None,
237 min_height: None,
238 max_width: None,
239 max_height: None,
240 flex_direction: FlexDirection::default(),
241 justify_content: JustifyContent::default(),
242 align_items: AlignItems::default(),
243 align_self: None,
244 flex_wrap: FlexWrap::default(),
245 align_content: AlignContent::default(),
246 flex_grow: 0.0,
247 flex_shrink: 1.0,
248 flex_basis: None,
249 gap: 0.0,
250 row_gap: 0.0,
251 column_gap: 0.0,
252 font_family: "Helvetica".to_string(),
253 font_size: 12.0,
254 font_weight: 400,
255 font_style: FontStyle::default(),
256 line_height: 1.4,
257 text_align: TextAlign::default(),
258 letter_spacing: 0.0,
259 text_decoration: TextDecoration::None,
260 text_transform: TextTransform::None,
261 color: Color::BLACK,
262 background_color: None,
263 border_color: EdgeValues::uniform(Color::BLACK),
264 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
265 border_radius: CornerValues::uniform(0.0),
266 opacity: 1.0,
267 position: Position::default(),
268 top: None,
269 right: None,
270 bottom: None,
271 left: None,
272 overflow: Overflow::default(),
273 breakable: false,
274 break_before: false,
275 min_widow_lines: 2,
276 min_orphan_lines: 2,
277 }
278 }
279}
280
281#[derive(Debug, Clone, Serialize)]
283#[serde(rename_all = "camelCase")]
284pub struct ElementInfo {
285 pub x: f64,
286 pub y: f64,
287 pub width: f64,
288 pub height: f64,
289 pub kind: String,
291 pub node_type: String,
293 pub style: ElementStyleInfo,
295 pub children: Vec<ElementInfo>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub source_location: Option<SourceLocation>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub text_content: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub href: Option<String>,
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub bookmark: Option<String>,
309}
310
311impl LayoutInfo {
312 pub fn from_pages(pages: &[LayoutPage]) -> Self {
314 LayoutInfo {
315 pages: pages
316 .iter()
317 .map(|page| {
318 let (page_w, page_h) = page.config.size.dimensions();
319 let content_x = page.config.margin.left;
320 let content_y = page.config.margin.top;
321 let content_width = page_w - page.config.margin.horizontal();
322 let content_height = page_h - page.config.margin.vertical();
323
324 let elements = Self::build_element_tree(&page.elements);
325
326 PageInfo {
327 width: page_w,
328 height: page_h,
329 content_x,
330 content_y,
331 content_width,
332 content_height,
333 elements,
334 }
335 })
336 .collect(),
337 }
338 }
339
340 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
341 elems
342 .iter()
343 .map(|elem| {
344 let kind = match &elem.draw {
345 DrawCommand::None => "None",
346 DrawCommand::Rect { .. } => "Rect",
347 DrawCommand::Text { .. } => "Text",
348 DrawCommand::Image { .. } => "Image",
349 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
350 DrawCommand::Svg { .. } => "Svg",
351 DrawCommand::Barcode { .. } => "Barcode",
352 DrawCommand::QrCode { .. } => "QrCode",
353 DrawCommand::Chart { .. } => "Chart",
354 DrawCommand::Watermark { .. } => "Watermark",
355 DrawCommand::FormField { .. } => "FormField",
356 };
357 let text_content = match &elem.draw {
358 DrawCommand::Text { lines, .. } => {
359 let text: String = lines
360 .iter()
361 .flat_map(|line| {
362 line.glyphs.iter().flat_map(|g| {
363 g.cluster_text.as_deref().unwrap_or("").chars().chain(
365 if g.cluster_text.is_none() {
366 Some(g.char_value)
367 } else {
368 None
369 },
370 )
371 })
372 })
373 .collect();
374 if text.is_empty() {
375 None
376 } else {
377 Some(text)
378 }
379 }
380 _ => None,
381 };
382 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
383 let style = elem
384 .resolved_style
385 .as_ref()
386 .map(ElementStyleInfo::from_resolved)
387 .unwrap_or_default();
388 ElementInfo {
389 x: elem.x,
390 y: elem.y,
391 width: elem.width,
392 height: elem.height,
393 kind: kind.to_string(),
394 node_type,
395 style,
396 children: Self::build_element_tree(&elem.children),
397 source_location: elem.source_location.clone(),
398 text_content,
399 href: elem.href.clone(),
400 bookmark: elem.bookmark.clone(),
401 }
402 })
403 .collect()
404 }
405}
406
407#[derive(Debug, Clone)]
409pub struct LayoutPage {
410 pub width: f64,
411 pub height: f64,
412 pub elements: Vec<LayoutElement>,
413 pub(crate) fixed_header: Vec<(Node, f64)>,
415 pub(crate) fixed_footer: Vec<(Node, f64)>,
417 pub(crate) watermarks: Vec<Node>,
419 pub(crate) config: PageConfig,
421}
422
423#[derive(Debug, Clone)]
425pub struct LayoutElement {
426 pub x: f64,
428 pub y: f64,
429 pub width: f64,
431 pub height: f64,
432 pub draw: DrawCommand,
434 pub children: Vec<LayoutElement>,
436 pub node_type: Option<String>,
438 pub resolved_style: Option<ResolvedStyle>,
440 pub source_location: Option<SourceLocation>,
442 pub href: Option<String>,
444 pub bookmark: Option<String>,
446 pub alt: Option<String>,
448 pub is_header_row: bool,
450 pub col_span: u32,
453 pub overflow: Overflow,
455 pub opacity: f64,
460}
461
462fn node_kind_name(kind: &NodeKind) -> &'static str {
464 match kind {
465 NodeKind::View => "View",
466 NodeKind::Text { .. } => "Text",
467 NodeKind::Heading { level: 1, .. } => "H1",
468 NodeKind::Heading { level: 2, .. } => "H2",
469 NodeKind::Heading { level: 3, .. } => "H3",
470 NodeKind::Heading { level: 4, .. } => "H4",
471 NodeKind::Heading { level: 5, .. } => "H5",
472 NodeKind::Heading { .. } => "H6",
476 NodeKind::List { .. } => "List",
477 NodeKind::ListItem => "ListItem",
478 NodeKind::Image { .. } => "Image",
479 NodeKind::Table { .. } => "Table",
480 NodeKind::TableRow { .. } => "TableRow",
481 NodeKind::TableCell { .. } => "TableCell",
482 NodeKind::Fixed {
483 position: FixedPosition::Header,
484 ..
485 } => "FixedHeader",
486 NodeKind::Fixed {
487 position: FixedPosition::Footer,
488 ..
489 } => "FixedFooter",
490 NodeKind::Page { .. } => "Page",
491 NodeKind::PageBreak => "PageBreak",
492 NodeKind::Svg { .. } => "Svg",
493 NodeKind::Canvas { .. } => "Canvas",
494 NodeKind::Barcode { .. } => "Barcode",
495 NodeKind::QrCode { .. } => "QrCode",
496 NodeKind::BarChart { .. } => "BarChart",
497 NodeKind::LineChart { .. } => "LineChart",
498 NodeKind::PieChart { .. } => "PieChart",
499 NodeKind::AreaChart { .. } => "AreaChart",
500 NodeKind::DotPlot { .. } => "DotPlot",
501 NodeKind::Watermark { .. } => "Watermark",
502 NodeKind::TextField { .. } => "TextField",
503 NodeKind::Checkbox { .. } => "Checkbox",
504 NodeKind::Dropdown { .. } => "Dropdown",
505 NodeKind::RadioButton { .. } => "RadioButton",
506 }
507}
508
509fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
528 node.bookmark.as_ref().map(|title| LayoutElement {
529 x,
530 y,
531 width: 0.0,
532 height: 0.0,
533 draw: DrawCommand::None,
534 children: vec![],
535 node_type: Some("Bookmark".to_string()),
536 resolved_style: None,
537 source_location: None,
538 href: None,
539 bookmark: Some(title.clone()),
540 alt: None,
541 is_header_row: false,
542 col_span: 1,
543 overflow: Overflow::default(),
544 opacity: 1.0,
545 })
546}
547
548fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
555 if !ordered {
556 return match marker_type {
560 ListMarkerType::None => String::new(),
561 _ => "•".to_string(),
562 };
563 }
564 let body = match marker_type {
565 ListMarkerType::LowerAlpha => to_alpha(idx, false),
566 ListMarkerType::UpperAlpha => to_alpha(idx, true),
567 ListMarkerType::LowerRoman => to_roman(idx, false),
568 ListMarkerType::UpperRoman => to_roman(idx, true),
569 ListMarkerType::None => return String::new(),
570 _ => idx.to_string(),
573 };
574 format!("{body}.")
575}
576
577fn to_alpha(mut n: u32, upper: bool) -> String {
580 if n == 0 {
581 return String::new();
582 }
583 let base = if upper { b'A' } else { b'a' };
584 let mut bytes: Vec<u8> = Vec::new();
585 while n > 0 {
586 let rem = ((n - 1) % 26) as u8;
587 bytes.push(base + rem);
588 n = (n - 1) / 26;
589 }
590 bytes.reverse();
591 String::from_utf8(bytes).unwrap_or_default()
592}
593
594fn to_roman(n: u32, upper: bool) -> String {
598 if n == 0 || n > 3999 {
599 return n.to_string();
600 }
601 const UPPER: &[(&str, u32)] = &[
602 ("M", 1000),
603 ("CM", 900),
604 ("D", 500),
605 ("CD", 400),
606 ("C", 100),
607 ("XC", 90),
608 ("L", 50),
609 ("XL", 40),
610 ("X", 10),
611 ("IX", 9),
612 ("V", 5),
613 ("IV", 4),
614 ("I", 1),
615 ];
616 const LOWER: &[(&str, u32)] = &[
617 ("m", 1000),
618 ("cm", 900),
619 ("d", 500),
620 ("cd", 400),
621 ("c", 100),
622 ("xc", 90),
623 ("l", 50),
624 ("xl", 40),
625 ("x", 10),
626 ("ix", 9),
627 ("v", 5),
628 ("iv", 4),
629 ("i", 1),
630 ];
631 let table = if upper { UPPER } else { LOWER };
632 let mut out = String::new();
633 let mut remaining = n;
634 for &(sym, val) in table {
635 while remaining >= val {
636 out.push_str(sym);
637 remaining -= val;
638 }
639 }
640 out
641}
642
643fn compute_marker_gutter_width(
650 ordered: bool,
651 marker_type: ListMarkerType,
652 start: u32,
653 n_items: u32,
654 style: &ResolvedStyle,
655) -> f64 {
656 if matches!(marker_type, ListMarkerType::None) {
657 return 0.0;
658 }
659 let approx_char_w = style.font_size * 0.6;
661 let gap = 6.0_f64;
662 if !ordered {
663 return approx_char_w + gap;
665 }
666 let last_idx = start + n_items.saturating_sub(1);
668 let widest = match marker_type {
669 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
670 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
671 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
672 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
673 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
674 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
675 };
676 widest.chars().count() as f64 * approx_char_w + gap
677}
678
679#[derive(Debug, Clone)]
681pub enum FormFieldType {
682 TextField {
683 value: Option<String>,
684 placeholder: Option<String>,
685 multiline: bool,
686 password: bool,
687 read_only: bool,
688 max_length: Option<u32>,
689 font_size: f64,
690 },
691 Checkbox {
692 checked: bool,
693 read_only: bool,
694 },
695 Dropdown {
696 options: Vec<String>,
697 value: Option<String>,
698 read_only: bool,
699 font_size: f64,
700 },
701 RadioButton {
702 value: String,
703 checked: bool,
704 read_only: bool,
705 },
706}
707
708#[derive(Debug, Clone)]
710pub enum DrawCommand {
711 None,
713 Rect {
715 background: Option<Color>,
716 border_width: Edges,
717 border_color: EdgeValues<Color>,
718 border_style: EdgeValues<crate::style::BorderStyle>,
719 border_radius: CornerValues,
720 opacity: f64,
721 box_shadow: Option<Box<crate::style::BoxShadow>>,
724 background_gradient: Option<Box<crate::style::Background>>,
728 },
729 Text {
731 lines: Vec<TextLine>,
732 color: Color,
733 text_decoration: TextDecoration,
734 opacity: f64,
735 },
736 Image {
738 image_data: crate::image_loader::LoadedImage,
739 },
740 ImagePlaceholder,
742 Svg {
744 commands: Vec<crate::svg::SvgCommand>,
745 width: f64,
747 height: f64,
749 viewbox_min_x: f64,
753 viewbox_min_y: f64,
754 viewbox_width: f64,
755 viewbox_height: f64,
756 clip: bool,
758 },
759 Barcode {
761 bars: Vec<u8>,
762 bar_width: f64,
763 height: f64,
764 color: Color,
765 },
766 QrCode {
768 modules: Vec<Vec<bool>>,
769 module_size: f64,
770 color: Color,
771 },
772 Chart {
774 primitives: Vec<crate::chart::ChartPrimitive>,
775 },
776 Watermark {
778 lines: Vec<TextLine>,
779 color: Color,
780 opacity: f64,
781 angle_rad: f64,
782 font_family: String,
784 },
785 FormField {
787 field_type: FormFieldType,
788 name: String,
789 },
790}
791
792#[derive(Debug, Clone)]
793pub struct TextLine {
794 pub x: f64,
795 pub y: f64,
796 pub glyphs: Vec<PositionedGlyph>,
797 pub width: f64,
798 pub height: f64,
799 pub word_spacing: f64,
801}
802
803#[derive(Debug, Clone)]
804pub struct PositionedGlyph {
805 pub glyph_id: u16,
808 pub x_offset: f64,
810 pub y_offset: f64,
812 pub x_advance: f64,
814 pub font_size: f64,
815 pub font_family: String,
816 pub font_weight: u32,
817 pub font_style: FontStyle,
818 pub char_value: char,
820 pub color: Option<Color>,
822 pub href: Option<String>,
824 pub text_decoration: TextDecoration,
826 pub letter_spacing: f64,
828 pub cluster_text: Option<String>,
831}
832
833fn offset_element_y(el: &mut LayoutElement, dy: f64) {
836 el.y += dy;
837 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
838 for line in lines.iter_mut() {
839 line.y += dy;
840 }
841 }
842 for child in &mut el.children {
843 offset_element_y(child, dy);
844 }
845}
846
847fn offset_element_x(el: &mut LayoutElement, dx: f64) {
849 el.x += dx;
850 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
851 for line in lines.iter_mut() {
852 line.x += dx;
853 }
854 }
855 for child in &mut el.children {
856 offset_element_x(child, dx);
857 }
858}
859
860fn reapply_justify_content(elem: &mut LayoutElement) {
864 let style = match elem.resolved_style {
865 Some(ref s) => s,
866 None => return,
867 };
868 if matches!(style.justify_content, JustifyContent::FlexStart) {
869 return;
870 }
871 if elem.children.is_empty() {
872 return;
873 }
874
875 let padding_top = style.padding.top + style.border_width.top;
876 let padding_bottom = style.padding.bottom + style.border_width.bottom;
877 let inner_h = elem.height - padding_top - padding_bottom;
878 let content_top = elem.y + padding_top;
879
880 let last_child = &elem.children[elem.children.len() - 1];
882 let children_bottom = last_child.y + last_child.height;
883 let children_span = children_bottom - content_top;
884 let slack = inner_h - children_span;
885 if slack < 0.001 {
886 return;
887 }
888
889 let n = elem.children.len();
890 let offsets: Vec<f64> = match style.justify_content {
891 JustifyContent::FlexEnd => vec![slack; n],
892 JustifyContent::Center => vec![slack / 2.0; n],
893 JustifyContent::SpaceBetween => {
894 if n <= 1 {
895 vec![0.0; n]
896 } else {
897 let per_gap = slack / (n - 1) as f64;
898 (0..n).map(|i| i as f64 * per_gap).collect()
899 }
900 }
901 JustifyContent::SpaceAround => {
902 let space = slack / n as f64;
903 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
904 }
905 JustifyContent::SpaceEvenly => {
906 let space = slack / (n + 1) as f64;
907 (0..n).map(|i| (i + 1) as f64 * space).collect()
908 }
909 JustifyContent::FlexStart => unreachable!(),
910 };
911
912 for (i, child) in elem.children.iter_mut().enumerate() {
913 let dy = offsets[i];
914 if dy.abs() > 0.001 {
915 offset_element_y(child, dy);
916 }
917 }
918}
919
920fn apply_text_transform(text: &str, transform: TextTransform) -> String {
922 match transform {
923 TextTransform::None => text.to_string(),
924 TextTransform::Uppercase => text.to_uppercase(),
925 TextTransform::Lowercase => text.to_lowercase(),
926 TextTransform::Capitalize => {
927 let mut result = String::with_capacity(text.len());
928 let mut prev_is_whitespace = true;
929 for ch in text.chars() {
930 if prev_is_whitespace && ch.is_alphabetic() {
931 for upper in ch.to_uppercase() {
932 result.push(upper);
933 }
934 } else {
935 result.push(ch);
936 }
937 prev_is_whitespace = ch.is_whitespace();
938 }
939 result
940 }
941 }
942}
943
944pub const PAGE_NUMBER_SENTINEL: char = '\x02';
948
949pub const TOTAL_PAGES_SENTINEL: char = '\x03';
951
952fn substitute_page_placeholders(text: &str) -> String {
957 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
958 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
959 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
960 } else {
961 text.to_string()
962 }
963}
964
965fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
968 match transform {
969 TextTransform::None => ch,
970 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
971 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
972 TextTransform::Capitalize => {
973 if is_word_start && ch.is_alphabetic() {
974 ch.to_uppercase().next().unwrap_or(ch)
975 } else {
976 ch
977 }
978 }
979 }
980}
981
982pub struct LayoutEngine {
984 text_layout: TextLayout,
985 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
986}
987
988#[derive(Debug, Clone)]
990struct PageCursor {
991 config: PageConfig,
992 base_config: PageConfig,
995 page_index: usize,
1000 content_width: f64,
1001 content_height: f64,
1002 y: f64,
1003 elements: Vec<LayoutElement>,
1004 fixed_header: Vec<(Node, f64)>,
1005 fixed_footer: Vec<(Node, f64)>,
1006 watermarks: Vec<Node>,
1008 content_x: f64,
1009 content_y: f64,
1010 continuation_top_offset: f64,
1012 containing_block: (f64, f64, f64, f64),
1017}
1018
1019impl PageCursor {
1020 fn new(config: &PageConfig) -> Self {
1021 let (page_w, page_h) = config.size.dimensions();
1022 let content_width = page_w - config.margin.horizontal();
1023 let content_height = page_h - config.margin.vertical();
1024
1025 Self {
1026 config: config.clone(),
1027 base_config: config.clone(),
1028 page_index: 0,
1029 content_width,
1030 content_height,
1031 y: 0.0,
1032 elements: Vec::new(),
1033 fixed_header: Vec::new(),
1034 fixed_footer: Vec::new(),
1035 watermarks: Vec::new(),
1036 content_x: config.margin.left,
1037 content_y: config.margin.top,
1038 continuation_top_offset: 0.0,
1039 containing_block: (
1040 config.margin.left,
1041 config.margin.top,
1042 content_width,
1043 content_height,
1044 ),
1045 }
1046 }
1047
1048 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1051 let mut cursor = PageCursor::new(first);
1052 cursor.base_config = base.clone();
1053 cursor
1054 }
1055
1056 fn fixed_page_filter(node: &Node) -> crate::model::FixedPageFilter {
1057 match &node.kind {
1058 NodeKind::Fixed { pages, .. } => *pages,
1059 _ => crate::model::FixedPageFilter::All,
1060 }
1061 }
1062
1063 fn remaining_height(&self) -> f64 {
1064 let footer_height: f64 = self
1065 .fixed_footer
1066 .iter()
1067 .filter(|(n, _)| Self::fixed_page_filter(n).applies(self.page_index))
1068 .map(|(_, h)| *h)
1069 .sum();
1070 (self.content_height - self.y - footer_height).max(0.0)
1071 }
1072
1073 fn finalize(&self) -> LayoutPage {
1074 let (page_w, page_h) = self.config.size.dimensions();
1075 LayoutPage {
1076 width: page_w,
1077 height: page_h,
1078 elements: self.elements.clone(),
1079 fixed_header: self.fixed_header.clone(),
1080 fixed_footer: self.fixed_footer.clone(),
1081 watermarks: self.watermarks.clone(),
1082 config: self.config.clone(),
1083 }
1084 }
1085
1086 fn new_page(&self) -> Self {
1087 let mut cursor = PageCursor::new(&self.base_config);
1090 cursor.page_index = self.page_index + 1;
1091 cursor.fixed_header = self.fixed_header.clone();
1092 cursor.fixed_footer = self.fixed_footer.clone();
1093 cursor.watermarks = self.watermarks.clone();
1094 cursor.continuation_top_offset = self.continuation_top_offset;
1095
1096 let header_height: f64 = cursor
1097 .fixed_header
1098 .iter()
1099 .filter(|(n, _)| Self::fixed_page_filter(n).applies(cursor.page_index))
1100 .map(|(_, h)| *h)
1101 .sum();
1102 cursor.y = header_height + cursor.continuation_top_offset;
1103
1104 cursor
1105 }
1106}
1107
1108impl Default for LayoutEngine {
1109 fn default() -> Self {
1110 Self::new()
1111 }
1112}
1113
1114impl LayoutEngine {
1115 pub fn new() -> Self {
1116 Self {
1117 text_layout: TextLayout::new(),
1118 image_dim_cache: RefCell::new(HashMap::new()),
1119 }
1120 }
1121
1122 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1124 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1125 return Some(*dims);
1126 }
1127 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1128 self.image_dim_cache
1129 .borrow_mut()
1130 .insert(src.to_string(), dims);
1131 Some(dims)
1132 } else {
1133 None
1134 }
1135 }
1136
1137 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1139 let mut pages: Vec<LayoutPage> = Vec::new();
1140 let mut cursor = match &document.first_page {
1141 Some(first) => PageCursor::new_first(first, &document.default_page),
1142 None => PageCursor::new(&document.default_page),
1143 };
1144
1145 let base = document.default_style.clone().unwrap_or_default();
1147 let root_style = Style {
1148 lang: base.lang.clone().or(document.metadata.lang.clone()),
1149 ..base
1150 }
1151 .resolve(None, cursor.content_width);
1152
1153 for node in &document.children {
1154 match &node.kind {
1155 NodeKind::Page { config } => {
1156 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1157 pages.push(cursor.finalize());
1158 }
1159 cursor = PageCursor::new(config);
1160 cursor.page_index = pages.len();
1161
1162 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1169 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1170
1171 let cx = cursor.content_x;
1172 let cw = cursor.content_width;
1173 self.layout_children(
1174 &node.children,
1175 &node.style,
1176 &mut cursor,
1177 &mut pages,
1178 cx,
1179 cw,
1180 Some(&page_root),
1181 font_context,
1182 );
1183 }
1184 NodeKind::PageBreak => {
1185 pages.push(cursor.finalize());
1186 cursor = cursor.new_page();
1187 }
1188 _ => {
1189 let cx = cursor.content_x;
1190 let cw = cursor.content_width;
1191 self.layout_node(
1192 node,
1193 &mut cursor,
1194 &mut pages,
1195 cx,
1196 cw,
1197 Some(&root_style),
1198 font_context,
1199 None,
1200 None,
1201 );
1202 }
1203 }
1204 }
1205
1206 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1207 pages.push(cursor.finalize());
1208 }
1209
1210 self.inject_fixed_elements(&mut pages, font_context);
1211
1212 pages
1213 }
1214
1215 #[allow(clippy::too_many_arguments)]
1216 fn layout_node(
1217 &self,
1218 node: &Node,
1219 cursor: &mut PageCursor,
1220 pages: &mut Vec<LayoutPage>,
1221 x: f64,
1222 available_width: f64,
1223 parent_style: Option<&ResolvedStyle>,
1224 font_context: &FontContext,
1225 cross_axis_height: Option<f64>,
1226 forced_outer_width: Option<f64>,
1227 ) {
1228 let mut style = node.style.resolve(parent_style, available_width);
1229
1230 if let Some(h) = cross_axis_height {
1233 if matches!(style.height, SizeConstraint::Auto) {
1234 style.height = SizeConstraint::Fixed(h);
1235 }
1236 }
1237
1238 if let Some(w) = forced_outer_width {
1243 style.width = SizeConstraint::Fixed(w);
1244 }
1245
1246 if style.break_before && !cursor.elements.is_empty() {
1257 pages.push(cursor.finalize());
1258 *cursor = cursor.new_page();
1259 }
1260
1261 let elem_start = cursor.elements.len();
1264
1265 match &node.kind {
1266 NodeKind::PageBreak => {
1267 pages.push(cursor.finalize());
1268 *cursor = cursor.new_page();
1269 }
1270
1271 NodeKind::Fixed { position, pages } => {
1272 let height = self.measure_node_height(node, available_width, &style, font_context);
1273 match position {
1274 FixedPosition::Header => {
1275 cursor.fixed_header.push((node.clone(), height));
1276 if pages.applies(cursor.page_index) {
1279 cursor.y += height;
1280 }
1281 }
1282 FixedPosition::Footer => {
1283 cursor.fixed_footer.push((node.clone(), height));
1284 }
1285 }
1286 }
1287
1288 NodeKind::Watermark { .. } => {
1289 cursor.watermarks.push(node.clone());
1291 }
1292
1293 NodeKind::TextField {
1294 name,
1295 value,
1296 placeholder,
1297 width: field_w,
1298 height: field_h,
1299 multiline,
1300 password,
1301 read_only,
1302 max_length,
1303 font_size,
1304 } => {
1305 self.layout_form_field(
1306 node,
1307 &style,
1308 cursor,
1309 pages,
1310 x,
1311 *field_w,
1312 *field_h,
1313 DrawCommand::FormField {
1314 field_type: FormFieldType::TextField {
1315 value: value.clone(),
1316 placeholder: placeholder.clone(),
1317 multiline: *multiline,
1318 password: *password,
1319 read_only: *read_only,
1320 max_length: *max_length,
1321 font_size: *font_size,
1322 },
1323 name: name.clone(),
1324 },
1325 "TextField",
1326 );
1327 }
1328
1329 NodeKind::Checkbox {
1330 name,
1331 checked,
1332 width: field_w,
1333 height: field_h,
1334 read_only,
1335 } => {
1336 self.layout_form_field(
1337 node,
1338 &style,
1339 cursor,
1340 pages,
1341 x,
1342 *field_w,
1343 *field_h,
1344 DrawCommand::FormField {
1345 field_type: FormFieldType::Checkbox {
1346 checked: *checked,
1347 read_only: *read_only,
1348 },
1349 name: name.clone(),
1350 },
1351 "Checkbox",
1352 );
1353 }
1354
1355 NodeKind::Dropdown {
1356 name,
1357 options,
1358 value,
1359 width: field_w,
1360 height: field_h,
1361 read_only,
1362 font_size,
1363 } => {
1364 self.layout_form_field(
1365 node,
1366 &style,
1367 cursor,
1368 pages,
1369 x,
1370 *field_w,
1371 *field_h,
1372 DrawCommand::FormField {
1373 field_type: FormFieldType::Dropdown {
1374 options: options.clone(),
1375 value: value.clone(),
1376 read_only: *read_only,
1377 font_size: *font_size,
1378 },
1379 name: name.clone(),
1380 },
1381 "Dropdown",
1382 );
1383 }
1384
1385 NodeKind::RadioButton {
1386 name,
1387 value,
1388 checked,
1389 width: field_w,
1390 height: field_h,
1391 read_only,
1392 } => {
1393 self.layout_form_field(
1394 node,
1395 &style,
1396 cursor,
1397 pages,
1398 x,
1399 *field_w,
1400 *field_h,
1401 DrawCommand::FormField {
1402 field_type: FormFieldType::RadioButton {
1403 value: value.clone(),
1404 checked: *checked,
1405 read_only: *read_only,
1406 },
1407 name: name.clone(),
1408 },
1409 "RadioButton",
1410 );
1411 }
1412
1413 NodeKind::Text {
1414 content,
1415 href,
1416 runs,
1417 } => {
1418 self.layout_text(
1419 content,
1420 href.as_deref(),
1421 runs,
1422 &style,
1423 cursor,
1424 pages,
1425 x,
1426 available_width,
1427 font_context,
1428 node.source_location.as_ref(),
1429 node.bookmark.as_deref(),
1430 None,
1431 );
1432 }
1433
1434 NodeKind::Heading {
1435 content,
1436 href,
1437 runs,
1438 ..
1439 } => {
1440 let heading_role = node_kind_name(&node.kind); self.layout_text(
1446 content,
1447 href.as_deref(),
1448 runs,
1449 &style,
1450 cursor,
1451 pages,
1452 x,
1453 available_width,
1454 font_context,
1455 node.source_location.as_ref(),
1456 node.bookmark.as_deref(),
1457 Some(heading_role),
1458 );
1459 }
1460
1461 NodeKind::List {
1462 ordered,
1463 marker_type,
1464 start,
1465 } => {
1466 self.layout_list(
1467 node,
1468 *ordered,
1469 *marker_type,
1470 *start,
1471 &style,
1472 cursor,
1473 pages,
1474 x,
1475 available_width,
1476 font_context,
1477 );
1478 }
1479
1480 NodeKind::ListItem => {
1481 self.layout_view(
1486 node,
1487 &style,
1488 cursor,
1489 pages,
1490 x,
1491 available_width,
1492 font_context,
1493 );
1494 }
1495
1496 NodeKind::Image { width, height, .. } => {
1497 self.layout_image(
1498 node,
1499 &style,
1500 cursor,
1501 pages,
1502 x,
1503 available_width,
1504 *width,
1505 *height,
1506 );
1507 }
1508
1509 NodeKind::Table { columns } => {
1510 self.layout_table(
1511 node,
1512 &style,
1513 columns,
1514 cursor,
1515 pages,
1516 x,
1517 available_width,
1518 font_context,
1519 );
1520 }
1521
1522 NodeKind::View | NodeKind::Page { .. } => {
1523 self.layout_view(
1524 node,
1525 &style,
1526 cursor,
1527 pages,
1528 x,
1529 available_width,
1530 font_context,
1531 );
1532 }
1533
1534 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
1535 self.layout_view(
1536 node,
1537 &style,
1538 cursor,
1539 pages,
1540 x,
1541 available_width,
1542 font_context,
1543 );
1544 }
1545
1546 NodeKind::Svg {
1547 width: svg_w,
1548 height: svg_h,
1549 view_box,
1550 content,
1551 } => {
1552 self.layout_svg(
1553 node,
1554 &style,
1555 cursor,
1556 pages,
1557 x,
1558 available_width,
1559 *svg_w,
1560 *svg_h,
1561 view_box.as_deref(),
1562 content,
1563 );
1564 }
1565
1566 NodeKind::Barcode {
1567 data,
1568 format,
1569 width: explicit_width,
1570 height: bar_height,
1571 } => {
1572 self.layout_barcode(
1573 node,
1574 &style,
1575 cursor,
1576 pages,
1577 x,
1578 available_width,
1579 data,
1580 *format,
1581 *explicit_width,
1582 *bar_height,
1583 );
1584 }
1585
1586 NodeKind::QrCode {
1587 data,
1588 size: explicit_size,
1589 } => {
1590 self.layout_qrcode(
1591 node,
1592 &style,
1593 cursor,
1594 pages,
1595 x,
1596 available_width,
1597 data,
1598 *explicit_size,
1599 );
1600 }
1601
1602 NodeKind::Canvas {
1603 width: canvas_w,
1604 height: canvas_h,
1605 operations,
1606 } => {
1607 self.layout_canvas(
1608 node,
1609 &style,
1610 cursor,
1611 pages,
1612 x,
1613 available_width,
1614 *canvas_w,
1615 *canvas_h,
1616 operations,
1617 );
1618 }
1619
1620 NodeKind::BarChart {
1621 data,
1622 width: chart_w,
1623 height: chart_h,
1624 color,
1625 show_labels,
1626 show_values,
1627 show_grid,
1628 title,
1629 } => {
1630 let config = crate::chart::bar::BarChartConfig {
1631 color: color.clone(),
1632 show_labels: *show_labels,
1633 show_values: *show_values,
1634 show_grid: *show_grid,
1635 title: title.clone(),
1636 };
1637 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1638 self.layout_chart(
1639 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1640 );
1641 }
1642
1643 NodeKind::LineChart {
1644 series,
1645 labels,
1646 width: chart_w,
1647 height: chart_h,
1648 show_points,
1649 show_grid,
1650 title,
1651 } => {
1652 let config = crate::chart::line::LineChartConfig {
1653 show_points: *show_points,
1654 show_grid: *show_grid,
1655 title: title.clone(),
1656 };
1657 let primitives =
1658 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1659 self.layout_chart(
1660 node,
1661 &style,
1662 cursor,
1663 pages,
1664 x,
1665 *chart_w,
1666 *chart_h,
1667 primitives,
1668 "LineChart",
1669 );
1670 }
1671
1672 NodeKind::PieChart {
1673 data,
1674 width: chart_w,
1675 height: chart_h,
1676 donut,
1677 show_legend,
1678 title,
1679 } => {
1680 let config = crate::chart::pie::PieChartConfig {
1681 donut: *donut,
1682 show_legend: *show_legend,
1683 title: title.clone(),
1684 };
1685 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1686 self.layout_chart(
1687 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1688 );
1689 }
1690
1691 NodeKind::AreaChart {
1692 series,
1693 labels,
1694 width: chart_w,
1695 height: chart_h,
1696 show_grid,
1697 title,
1698 } => {
1699 let config = crate::chart::area::AreaChartConfig {
1700 show_grid: *show_grid,
1701 title: title.clone(),
1702 };
1703 let primitives =
1704 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1705 self.layout_chart(
1706 node,
1707 &style,
1708 cursor,
1709 pages,
1710 x,
1711 *chart_w,
1712 *chart_h,
1713 primitives,
1714 "AreaChart",
1715 );
1716 }
1717
1718 NodeKind::DotPlot {
1719 groups,
1720 width: chart_w,
1721 height: chart_h,
1722 x_min,
1723 x_max,
1724 y_min,
1725 y_max,
1726 x_label,
1727 y_label,
1728 show_legend,
1729 dot_size,
1730 } => {
1731 let config = crate::chart::dot::DotPlotConfig {
1732 x_min: *x_min,
1733 x_max: *x_max,
1734 y_min: *y_min,
1735 y_max: *y_max,
1736 x_label: x_label.clone(),
1737 y_label: y_label.clone(),
1738 show_legend: *show_legend,
1739 dot_size: *dot_size,
1740 };
1741 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
1742 self.layout_chart(
1743 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
1744 );
1745 }
1746 }
1747
1748 if matches!(style.position, Position::Relative)
1756 && (style.top.is_some()
1757 || style.left.is_some()
1758 || style.right.is_some()
1759 || style.bottom.is_some())
1760 {
1761 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
1762 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
1763 for el in &mut cursor.elements[elem_start..] {
1764 if dx != 0.0 {
1765 offset_element_x(el, dx);
1766 }
1767 if dy != 0.0 {
1768 offset_element_y(el, dy);
1769 }
1770 }
1771 }
1772 }
1773
1774 #[allow(clippy::too_many_arguments)]
1775 fn layout_view(
1776 &self,
1777 node: &Node,
1778 style: &ResolvedStyle,
1779 cursor: &mut PageCursor,
1780 pages: &mut Vec<LayoutPage>,
1781 x: f64,
1782 available_width: f64,
1783 font_context: &FontContext,
1784 ) {
1785 let padding = &style.padding;
1786 let margin = &style.margin.to_edges();
1787 let border = &style.border_width;
1788
1789 let outer_width = match style.width {
1790 SizeConstraint::Fixed(w) => w,
1791 SizeConstraint::Auto => available_width - margin.horizontal(),
1792 }
1793 .min(style.max_width)
1795 .max(style.min_width);
1796 let inner_width = outer_width - padding.horizontal() - border.horizontal();
1797
1798 let children_height =
1799 self.measure_children_height(&node.children, inner_width, style, font_context);
1800 let total_height = match style.height {
1801 SizeConstraint::Fixed(h) => h,
1802 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
1803 }
1804 .max(style.min_height);
1805
1806 let node_x = x + margin.left;
1807
1808 let fits = total_height <= cursor.remaining_height() - margin.vertical();
1809
1810 if fits || !style.breakable {
1811 if !fits && !style.breakable {
1812 pages.push(cursor.finalize());
1813 *cursor = cursor.new_page();
1814 }
1815
1816 let rect_y = cursor.content_y + cursor.y + margin.top;
1818 let snapshot = cursor.elements.len();
1819
1820 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
1825 cursor.elements.push(marker);
1826 }
1827
1828 let saved_y = cursor.y;
1829 cursor.y += margin.top + padding.top + border.top;
1830
1831 let children_x = node_x + padding.left + border.left;
1832 let is_grid =
1833 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1834 if is_grid {
1835 self.layout_grid_children(
1836 &node.children,
1837 style,
1838 cursor,
1839 pages,
1840 children_x,
1841 inner_width,
1842 font_context,
1843 );
1844 } else {
1845 self.layout_children(
1846 &node.children,
1847 &node.style,
1848 cursor,
1849 pages,
1850 children_x,
1851 inner_width,
1852 Some(style),
1853 font_context,
1854 );
1855 }
1856
1857 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1859
1860 let rect_element = LayoutElement {
1861 x: node_x,
1862 y: rect_y,
1863 width: outer_width,
1864 height: total_height,
1865 draw: DrawCommand::Rect {
1866 background: style.background_color,
1867 border_width: style.border_width,
1868 border_color: style.border_color,
1869 border_style: style.border_style,
1870 border_radius: style.border_radius,
1871 opacity: 1.0,
1872 box_shadow: style.box_shadow.map(Box::new),
1873 background_gradient: style.background.clone().map(Box::new),
1874 },
1875 children: child_elements,
1876 node_type: Some(node_kind_name(&node.kind).to_string()),
1877 resolved_style: Some(style.clone()),
1878 source_location: node.source_location.clone(),
1879 href: node.href.clone(),
1880 bookmark: None,
1883 alt: None,
1884 is_header_row: false,
1885 col_span: 1,
1886 overflow: style.overflow,
1887 opacity: style.opacity,
1888 };
1889 cursor.elements.push(rect_element);
1890
1891 cursor.y = saved_y + total_height + margin.vertical();
1892 } else {
1893 self.layout_breakable_view(
1894 node,
1895 style,
1896 cursor,
1897 pages,
1898 node_x,
1899 outer_width,
1900 inner_width,
1901 font_context,
1902 );
1903 }
1904 }
1905
1906 #[allow(clippy::too_many_arguments)]
1907 fn layout_breakable_view(
1908 &self,
1909 node: &Node,
1910 style: &ResolvedStyle,
1911 cursor: &mut PageCursor,
1912 pages: &mut Vec<LayoutPage>,
1913 node_x: f64,
1914 outer_width: f64,
1915 inner_width: f64,
1916 font_context: &FontContext,
1917 ) {
1918 let padding = &style.padding;
1919 let border = &style.border_width;
1920 let margin = &style.margin.to_edges();
1921
1922 let initial_page_count = pages.len();
1924 let snapshot = cursor.elements.len();
1925 let rect_start_y = cursor.content_y + cursor.y + margin.top;
1926
1927 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
1935 cursor.elements.push(marker);
1936 }
1937
1938 cursor.y += margin.top + padding.top + border.top;
1939 let prev_continuation_offset = cursor.continuation_top_offset;
1940 cursor.continuation_top_offset = padding.top + border.top;
1941
1942 let children_x = node_x + padding.left + border.left;
1943 let is_grid =
1944 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1945 if is_grid {
1946 self.layout_grid_children(
1947 &node.children,
1948 style,
1949 cursor,
1950 pages,
1951 children_x,
1952 inner_width,
1953 font_context,
1954 );
1955 } else {
1956 self.layout_children(
1957 &node.children,
1958 &node.style,
1959 cursor,
1960 pages,
1961 children_x,
1962 inner_width,
1963 Some(style),
1964 font_context,
1965 );
1966 }
1967
1968 cursor.continuation_top_offset = prev_continuation_offset;
1969
1970 let has_visual = style.background_color.is_some()
1972 || style.border_width.top > 0.0
1973 || style.border_width.right > 0.0
1974 || style.border_width.bottom > 0.0
1975 || style.border_width.left > 0.0;
1976 let needs_wrapper = has_visual || style.flex_grow > 0.0;
1978
1979 if !needs_wrapper {
1980 cursor.y += padding.bottom + border.bottom + margin.bottom;
1982 return;
1983 }
1984
1985 let draw_cmd = DrawCommand::Rect {
1986 background: style.background_color,
1987 border_width: style.border_width,
1988 border_color: style.border_color,
1989 border_style: style.border_style,
1990 border_radius: style.border_radius,
1991 opacity: 1.0,
1992 box_shadow: style.box_shadow.map(Box::new),
1993 background_gradient: style.background.clone().map(Box::new),
1994 };
1995
1996 if pages.len() == initial_page_count {
1997 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1999 let rect_height =
2000 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2001 cursor.elements.push(LayoutElement {
2002 x: node_x,
2003 y: rect_start_y,
2004 width: outer_width,
2005 height: rect_height,
2006 draw: draw_cmd,
2007 children: child_elements,
2008 node_type: Some(node_kind_name(&node.kind).to_string()),
2009 resolved_style: Some(style.clone()),
2010 source_location: node.source_location.clone(),
2011 href: node.href.clone(),
2012 bookmark: None,
2017 alt: None,
2018 is_header_row: false,
2019 col_span: 1,
2020 overflow: style.overflow,
2021 opacity: style.opacity,
2022 });
2023 } else {
2024 let page = &mut pages[initial_page_count];
2028 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2029 let page_content_bottom =
2030 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2031 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
2032 if !our_elements.is_empty() {
2033 let rect_height = page_content_bottom - rect_start_y;
2034 page.elements.push(LayoutElement {
2035 x: node_x,
2036 y: rect_start_y,
2037 width: outer_width,
2038 height: rect_height,
2039 draw: draw_cmd.clone(),
2040 children: our_elements,
2041 node_type: Some(node_kind_name(&node.kind).to_string()),
2042 resolved_style: Some(style.clone()),
2043 source_location: node.source_location.clone(),
2044 href: node.href.clone(),
2045 bookmark: None,
2047 alt: None,
2048 is_header_row: false,
2049 col_span: 1,
2050 overflow: Overflow::default(),
2051 opacity: 1.0,
2052 });
2053 }
2054
2055 for page in &mut pages[initial_page_count + 1..] {
2057 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
2058 let content_top = page.config.margin.top + header_h;
2059 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
2060 let content_bottom = page.config.margin.top
2061 + (page.height - page.config.margin.vertical())
2062 - footer_h;
2063 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2064 if !all_elements.is_empty() {
2065 page.elements.push(LayoutElement {
2066 x: node_x,
2067 y: content_top,
2068 width: outer_width,
2069 height: content_bottom - content_top,
2070 draw: draw_cmd.clone(),
2071 children: all_elements,
2072 node_type: Some(node_kind_name(&node.kind).to_string()),
2073 resolved_style: Some(style.clone()),
2074 source_location: node.source_location.clone(),
2075 href: None,
2076 bookmark: None,
2077 alt: None,
2078 is_header_row: false,
2079 col_span: 1,
2080 overflow: Overflow::default(),
2081 opacity: 1.0,
2082 });
2083 }
2084 }
2085
2086 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2088 if !all_elements.is_empty() {
2089 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
2090 let content_top = cursor.content_y + header_h;
2091 let rect_height =
2092 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2093 cursor.elements.push(LayoutElement {
2094 x: node_x,
2095 y: content_top,
2096 width: outer_width,
2097 height: rect_height,
2098 draw: draw_cmd,
2099 children: all_elements,
2100 node_type: Some(node_kind_name(&node.kind).to_string()),
2101 resolved_style: Some(style.clone()),
2102 source_location: node.source_location.clone(),
2103 href: None,
2104 bookmark: None,
2105 alt: None,
2106 is_header_row: false,
2107 col_span: 1,
2108 overflow: Overflow::default(),
2109 opacity: 1.0,
2110 });
2111 }
2112 }
2113
2114 cursor.y += padding.bottom + border.bottom + margin.bottom;
2115 }
2116
2117 #[allow(clippy::too_many_arguments)]
2118 fn layout_children(
2119 &self,
2120 children: &[Node],
2121 _parent_raw_style: &Style,
2122 cursor: &mut PageCursor,
2123 pages: &mut Vec<LayoutPage>,
2124 content_x: f64,
2125 available_width: f64,
2126 parent_style: Option<&ResolvedStyle>,
2127 font_context: &FontContext,
2128 ) {
2129 let parent_box_y = cursor.content_y + cursor.y;
2131 let parent_box_x = content_x;
2132
2133 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2139 let saved_cb = cursor.containing_block;
2140 if parent_positioned {
2141 let cb_height = parent_style
2142 .and_then(|ps| match ps.height {
2143 SizeConstraint::Fixed(h) => {
2144 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2145 }
2146 SizeConstraint::Auto => None,
2147 })
2148 .unwrap_or(saved_cb.3);
2149 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2150 }
2151
2152 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2154 .iter()
2155 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2156
2157 let direction = parent_style
2158 .map(|s| s.flex_direction)
2159 .unwrap_or(FlexDirection::Column);
2160
2161 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2162 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2163
2164 match direction {
2166 FlexDirection::Column | FlexDirection::ColumnReverse => {
2167 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2168 flow_children.into_iter().rev().collect()
2169 } else {
2170 flow_children
2171 };
2172
2173 let justify = parent_style
2174 .map(|s| s.justify_content)
2175 .unwrap_or(JustifyContent::FlexStart);
2176 let align = parent_style
2177 .map(|s| s.align_items)
2178 .unwrap_or(AlignItems::Stretch);
2179
2180 let start_y = cursor.y;
2181 let initial_pages = pages.len();
2182
2183 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2185
2186 for (i, child) in items.iter().enumerate() {
2187 if i > 0 {
2188 cursor.y += row_gap;
2189 }
2190 let child_start = cursor.elements.len();
2191
2192 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2195 let has_auto_h = child_margin.has_auto_horizontal();
2196
2197 let (child_x, layout_w) = if has_auto_h {
2204 let child_style = child.style.resolve(parent_style, available_width);
2205 let has_explicit_width =
2206 matches!(child_style.width, SizeConstraint::Fixed(_));
2207 let intrinsic = self
2208 .measure_intrinsic_width(child, &child_style, font_context)
2209 .min(available_width);
2210 let w = match child_style.width {
2211 SizeConstraint::Fixed(fw) => fw,
2212 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2217 (available_width - child_margin.horizontal())
2218 .min(child_style.max_width)
2219 }
2220 SizeConstraint::Auto => intrinsic,
2221 }
2222 .min(child_style.max_width)
2223 .max(child_style.min_width);
2224 let lw = if has_explicit_width {
2225 available_width
2226 } else {
2227 w
2228 };
2229 let fixed_h = child_margin.horizontal();
2230 let slack = (available_width - w - fixed_h).max(0.0);
2231 let auto_left = child_margin.left.is_auto();
2232 let auto_right = child_margin.right.is_auto();
2233 let ml = match (auto_left, auto_right) {
2234 (true, true) => slack / 2.0,
2235 (true, false) => slack,
2236 (false, true) => 0.0,
2237 (false, false) => 0.0,
2238 };
2239 (content_x + child_margin.left.resolve() + ml, lw)
2240 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2241 let child_style = child.style.resolve(parent_style, available_width);
2242 let has_explicit_width =
2243 matches!(child_style.width, SizeConstraint::Fixed(_));
2244 let intrinsic = self
2245 .measure_intrinsic_width(child, &child_style, font_context)
2246 .min(available_width);
2247 let w = match child_style.width {
2248 SizeConstraint::Fixed(fw) => fw,
2249 SizeConstraint::Auto => intrinsic,
2250 };
2251 let lw = if has_explicit_width {
2252 available_width
2253 } else {
2254 w
2255 };
2256 match align {
2257 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2258 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2259 _ => (content_x, available_width),
2260 }
2261 } else {
2262 (content_x, available_width)
2263 };
2264
2265 self.layout_node(
2266 child,
2267 cursor,
2268 pages,
2269 child_x,
2270 layout_w,
2271 parent_style,
2272 font_context,
2273 None,
2274 None,
2275 );
2276
2277 child_ranges.push((child_start, cursor.elements.len()));
2278 }
2279
2280 let container_inner_h: Option<f64> = parent_style
2283 .and_then(|ps| match ps.height {
2284 SizeConstraint::Fixed(h) => {
2285 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2286 }
2287 SizeConstraint::Auto => None,
2288 })
2289 .or_else(|| {
2290 if parent_style.is_none() {
2292 Some(cursor.content_height - start_y)
2293 } else {
2294 None
2295 }
2296 });
2297
2298 if let Some(inner_h) = container_inner_h {
2299 if pages.len() == initial_pages {
2300 let child_styles: Vec<ResolvedStyle> = items
2301 .iter()
2302 .map(|child| child.style.resolve(parent_style, available_width))
2303 .collect();
2304 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2305 if total_grow > 0.0 {
2306 let children_total = cursor.y - start_y;
2307 let slack = (inner_h - children_total).max(0.0);
2308 if slack > 0.0 {
2309 let mut cumulative_shift = 0.0_f64;
2310 for (i, cs) in child_styles.iter().enumerate() {
2311 let (start, end) = child_ranges[i];
2312 if cumulative_shift > 0.001 {
2313 for j in start..end {
2314 offset_element_y(
2315 &mut cursor.elements[j],
2316 cumulative_shift,
2317 );
2318 }
2319 }
2320 if cs.flex_grow > 0.0 {
2321 let extra = slack * (cs.flex_grow / total_grow);
2322 if start < end {
2324 let elem = &mut cursor.elements[end - 1];
2325 elem.height += extra;
2326 reapply_justify_content(elem);
2327 }
2328 cumulative_shift += extra;
2329 }
2330 }
2331 cursor.y += cumulative_shift;
2332 }
2333 }
2334 }
2335 }
2336
2337 if let Some(inner_h) = container_inner_h {
2344 if pages.len() == initial_pages {
2345 let auto_styles: Vec<ResolvedStyle> = items
2346 .iter()
2347 .map(|child| child.style.resolve(parent_style, available_width))
2348 .collect();
2349 let total_autos: usize = auto_styles
2350 .iter()
2351 .map(|s| {
2352 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2353 })
2354 .sum();
2355 if total_autos > 0 {
2356 let children_total = cursor.y - start_y;
2357 let total_slack = (inner_h - children_total).max(0.0);
2358 if total_slack > 0.0 {
2359 let per_auto = total_slack / total_autos as f64;
2360 let mut cumulative_shift = 0.0_f64;
2361 for (i, cs) in auto_styles.iter().enumerate() {
2362 let (start, end) = child_ranges[i];
2363 let mt_auto = cs.margin.top.is_auto();
2364 let mb_auto = cs.margin.bottom.is_auto();
2365 let this_child_shift =
2369 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2370 if this_child_shift > 0.001 {
2371 for j in start..end {
2372 offset_element_y(
2373 &mut cursor.elements[j],
2374 this_child_shift,
2375 );
2376 }
2377 }
2378 cumulative_shift =
2381 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2382 }
2383 cursor.y += cumulative_shift;
2384 }
2385 }
2386 }
2387 }
2388
2389 let needs_justify =
2391 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2392 if needs_justify {
2393 let justify_inner_h = container_inner_h.or_else(|| {
2395 parent_style.and_then(|ps| match ps.height {
2396 SizeConstraint::Fixed(h) => {
2397 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2398 }
2399 SizeConstraint::Auto => None,
2400 })
2401 });
2402 if let Some(inner_h) = justify_inner_h {
2403 let children_total = cursor.y - start_y;
2404 let slack = inner_h - children_total;
2405 if slack > 0.0 {
2406 let n = child_ranges.len();
2407 let offsets: Vec<f64> = match justify {
2408 JustifyContent::FlexEnd => vec![slack; n],
2409 JustifyContent::Center => vec![slack / 2.0; n],
2410 JustifyContent::SpaceBetween => {
2411 if n <= 1 {
2412 vec![0.0; n]
2413 } else {
2414 let per_gap = slack / (n - 1) as f64;
2415 (0..n).map(|i| i as f64 * per_gap).collect()
2416 }
2417 }
2418 JustifyContent::SpaceAround => {
2419 let space = slack / n as f64;
2420 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2421 }
2422 JustifyContent::SpaceEvenly => {
2423 let space = slack / (n + 1) as f64;
2424 (0..n).map(|i| (i + 1) as f64 * space).collect()
2425 }
2426 JustifyContent::FlexStart => vec![0.0; n],
2427 };
2428 for (i, &(start, end)) in child_ranges.iter().enumerate() {
2429 let dy = offsets[i];
2430 if dy.abs() > 0.001 {
2431 for j in start..end {
2432 offset_element_y(&mut cursor.elements[j], dy);
2433 }
2434 }
2435 }
2436 cursor.y += *offsets.last().unwrap_or(&0.0);
2437 }
2438 }
2439 }
2440 }
2441
2442 FlexDirection::Row | FlexDirection::RowReverse => {
2443 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
2444 self.layout_flex_row(
2445 &flow_owned,
2446 cursor,
2447 pages,
2448 content_x,
2449 available_width,
2450 parent_style,
2451 column_gap,
2452 row_gap,
2453 font_context,
2454 );
2455 }
2456 }
2457
2458 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
2464 let ph = parent_style
2465 .and_then(|ps| match ps.height {
2466 SizeConstraint::Fixed(h) => {
2467 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2468 }
2469 SizeConstraint::Auto => None,
2470 })
2471 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
2472 (parent_box_x, parent_box_y, available_width, ph)
2473 } else {
2474 cursor.containing_block
2475 };
2476
2477 for abs_child in &abs_children {
2479 let abs_style = abs_child.style.resolve(parent_style, cb_w);
2480
2481 let child_width = match abs_style.width {
2483 SizeConstraint::Fixed(w) => w,
2484 SizeConstraint::Auto => {
2485 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
2487 (cb_w - l - r).max(0.0)
2488 } else {
2489 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
2490 }
2491 }
2492 };
2493
2494 let child_height = match abs_style.height {
2495 SizeConstraint::Fixed(h) => h,
2496 SizeConstraint::Auto => {
2497 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
2498 }
2499 };
2500
2501 let abs_x = if let Some(l) = abs_style.left {
2503 cb_x + l
2504 } else if let Some(r) = abs_style.right {
2505 cb_x + cb_w - r - child_width
2506 } else {
2507 cb_x
2508 };
2509
2510 let abs_y = if let Some(t) = abs_style.top {
2511 cb_y + t
2512 } else if let Some(b) = abs_style.bottom {
2513 cb_y + cb_h - b - child_height
2514 } else {
2515 cb_y
2516 };
2517
2518 let mut abs_cursor = PageCursor::new(&cursor.config);
2520 abs_cursor.y = 0.0;
2521 abs_cursor.content_x = abs_x;
2522 abs_cursor.content_y = abs_y;
2523
2524 self.layout_node(
2525 abs_child,
2526 &mut abs_cursor,
2527 &mut Vec::new(),
2528 abs_x,
2529 child_width,
2530 parent_style,
2531 font_context,
2532 None,
2533 None,
2534 );
2535
2536 cursor.elements.extend(abs_cursor.elements);
2538 }
2539
2540 cursor.containing_block = saved_cb;
2542 }
2543
2544 #[allow(clippy::too_many_arguments)]
2545 fn layout_flex_row(
2546 &self,
2547 children: &[Node],
2548 cursor: &mut PageCursor,
2549 pages: &mut Vec<LayoutPage>,
2550 content_x: f64,
2551 available_width: f64,
2552 parent_style: Option<&ResolvedStyle>,
2553 column_gap: f64,
2554 row_gap: f64,
2555 font_context: &FontContext,
2556 ) {
2557 if children.is_empty() {
2558 return;
2559 }
2560
2561 let flex_wrap = parent_style
2562 .map(|s| s.flex_wrap)
2563 .unwrap_or(FlexWrap::NoWrap);
2564
2565 let items: Vec<FlexItem> = children
2568 .iter()
2569 .map(|child| {
2570 let style = child.style.resolve(parent_style, available_width);
2571 let base_width = match style.flex_basis {
2572 SizeConstraint::Fixed(w) => w,
2573 SizeConstraint::Auto => match style.width {
2574 SizeConstraint::Fixed(w) => w,
2575 SizeConstraint::Auto => {
2576 self.measure_intrinsic_width(child, &style, font_context)
2577 }
2578 },
2579 };
2580 let min_content_width = self.measure_min_content_width(child, &style, font_context);
2581 FlexItem {
2582 node: child,
2583 style,
2584 base_width,
2585 min_content_width,
2586 }
2587 })
2588 .collect();
2589
2590 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2592 let lines = match flex_wrap {
2593 FlexWrap::NoWrap => {
2594 vec![flex::WrapLine {
2595 start: 0,
2596 end: items.len(),
2597 }]
2598 }
2599 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
2600 FlexWrap::WrapReverse => {
2601 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
2602 l.reverse();
2603 l
2604 }
2605 };
2606
2607 if lines.is_empty() {
2608 return;
2609 }
2610
2611 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
2613
2614 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
2616
2617 let initial_pages_count = pages.len();
2618 let flex_start_y = cursor.y;
2619 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
2620
2621 for (line_idx, line) in lines.iter().enumerate() {
2622 let line_items = &items[line.start..line.end];
2623 let line_count = line.end - line.start;
2624 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
2625 let distributable = available_width - line_gap;
2626
2627 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
2629 let remaining = distributable - total_base;
2630
2631 if remaining > 0.0 {
2632 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
2633 if total_grow > 0.0 {
2634 for (j, item) in line_items.iter().enumerate() {
2635 final_widths[line.start + j] =
2636 item.base_width + remaining * (item.style.flex_grow / total_grow);
2637 }
2638 }
2639 } else if remaining < 0.0 {
2640 let total_shrink: f64 = line_items
2641 .iter()
2642 .map(|i| i.style.flex_shrink * i.base_width)
2643 .sum();
2644 if total_shrink > 0.0 {
2645 for (j, item) in line_items.iter().enumerate() {
2646 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
2647 let w = item.base_width + remaining * factor;
2648 let floor = item.style.min_width.max(item.min_content_width);
2649 final_widths[line.start + j] = w.max(floor);
2650 }
2651 }
2652 }
2653
2654 let line_height: f64 = line_items
2656 .iter()
2657 .enumerate()
2658 .map(|(j, item)| {
2659 let fw = final_widths[line.start + j];
2660 self.measure_node_height(item.node, fw, &item.style, font_context)
2661 + item.style.margin.vertical()
2662 })
2663 .fold(0.0f64, f64::max);
2664
2665 if line_height > cursor.remaining_height() && cursor.y > 0.0 {
2672 pages.push(cursor.finalize());
2673 *cursor = cursor.new_page();
2674 }
2675
2676 if line_idx > 0 {
2678 cursor.y += row_gap;
2679 }
2680
2681 let row_start_y = cursor.y;
2682
2683 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
2685 let slack = available_width - actual_total - line_gap;
2686
2687 let (start_offset, between_extra) = match justify {
2688 JustifyContent::FlexStart => (0.0, 0.0),
2689 JustifyContent::FlexEnd => (slack, 0.0),
2690 JustifyContent::Center => (slack / 2.0, 0.0),
2691 JustifyContent::SpaceBetween => {
2692 if line_count > 1 {
2693 (0.0, slack / (line_count as f64 - 1.0))
2694 } else {
2695 (0.0, 0.0)
2696 }
2697 }
2698 JustifyContent::SpaceAround => {
2699 let s = slack / line_count as f64;
2700 (s / 2.0, s)
2701 }
2702 JustifyContent::SpaceEvenly => {
2703 let s = slack / (line_count as f64 + 1.0);
2704 (s, s)
2705 }
2706 };
2707
2708 let line_elem_start = cursor.elements.len();
2709 let mut x = content_x + start_offset;
2710
2711 for (j, item) in line_items.iter().enumerate() {
2712 if j > 0 {
2713 x += column_gap + between_extra;
2714 }
2715
2716 let fw = final_widths[line.start + j];
2717
2718 let align = item
2719 .style
2720 .align_self
2721 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
2722
2723 let item_height =
2724 self.measure_node_height(item.node, fw, &item.style, font_context);
2725
2726 let has_auto_v = item.style.margin.has_auto_vertical();
2728 let y_offset = if has_auto_v {
2729 let fixed_v = item.style.margin.vertical();
2730 let slack = (line_height - item_height - fixed_v).max(0.0);
2731 let auto_top = item.style.margin.top.is_auto();
2732 let auto_bottom = item.style.margin.bottom.is_auto();
2733 match (auto_top, auto_bottom) {
2734 (true, true) => slack / 2.0,
2735 (true, false) => slack,
2736 (false, true) => 0.0,
2737 (false, false) => 0.0,
2738 }
2739 } else {
2740 match align {
2741 AlignItems::FlexStart => 0.0,
2742 AlignItems::FlexEnd => {
2743 line_height - item_height - item.style.margin.vertical()
2744 }
2745 AlignItems::Center => {
2746 (line_height - item_height - item.style.margin.vertical()) / 2.0
2747 }
2748 AlignItems::Stretch => 0.0,
2749 AlignItems::Baseline => 0.0,
2750 }
2751 };
2752
2753 let cross_h = if matches!(align, AlignItems::Stretch)
2757 && matches!(item.style.height, SizeConstraint::Auto)
2758 && !has_auto_v
2759 {
2760 let stretch_h = line_height - item.style.margin.vertical();
2761 if stretch_h > item_height {
2762 Some(stretch_h)
2763 } else {
2764 None
2765 }
2766 } else {
2767 None
2768 };
2769
2770 let saved_y = cursor.y;
2771 cursor.y = row_start_y + y_offset;
2772
2773 self.layout_node(
2774 item.node,
2775 cursor,
2776 pages,
2777 x,
2778 available_width,
2779 parent_style,
2780 font_context,
2781 cross_h,
2782 Some(fw),
2783 );
2784
2785 cursor.y = saved_y;
2786 x += fw;
2787 }
2788
2789 cursor.y = row_start_y + line_height;
2790 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
2791 }
2792
2793 if pages.len() == initial_pages_count && !line_infos.is_empty() {
2795 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
2796 if !matches!(align_content, AlignContent::FlexStart)
2797 && !matches!(flex_wrap, FlexWrap::NoWrap)
2798 {
2799 if let Some(parent) = parent_style {
2800 if let SizeConstraint::Fixed(container_h) = parent.height {
2801 let inner_h = container_h
2802 - parent.padding.vertical()
2803 - parent.border_width.vertical();
2804 let total_used = cursor.y - flex_start_y;
2805 let slack = inner_h - total_used;
2806 if slack > 0.0 {
2807 let n = line_infos.len();
2808 let offsets: Vec<f64> = match align_content {
2809 AlignContent::FlexEnd => vec![slack; n],
2810 AlignContent::Center => vec![slack / 2.0; n],
2811 AlignContent::SpaceBetween => {
2812 if n <= 1 {
2813 vec![0.0; n]
2814 } else {
2815 let per_gap = slack / (n - 1) as f64;
2816 (0..n).map(|i| i as f64 * per_gap).collect()
2817 }
2818 }
2819 AlignContent::SpaceAround => {
2820 let space = slack / n as f64;
2821 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2822 }
2823 AlignContent::SpaceEvenly => {
2824 let space = slack / (n + 1) as f64;
2825 (0..n).map(|i| (i + 1) as f64 * space).collect()
2826 }
2827 AlignContent::Stretch => {
2828 let extra = slack / n as f64;
2829 (0..n).map(|i| i as f64 * extra).collect()
2830 }
2831 AlignContent::FlexStart => vec![0.0; n],
2832 };
2833 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
2834 let dy = offsets[i];
2835 if dy.abs() > 0.001 {
2836 for j in start..end {
2837 offset_element_y(&mut cursor.elements[j], dy);
2838 }
2839 }
2840 }
2841 cursor.y += *offsets.last().unwrap_or(&0.0);
2842 }
2843 }
2844 }
2845 }
2846 }
2847 }
2848
2849 #[allow(clippy::too_many_arguments)]
2852 fn layout_list(
2853 &self,
2854 node: &Node,
2855 ordered: bool,
2856 marker_type: ListMarkerType,
2857 start: u32,
2858 style: &ResolvedStyle,
2859 cursor: &mut PageCursor,
2860 pages: &mut Vec<LayoutPage>,
2861 x: f64,
2862 available_width: f64,
2863 font_context: &FontContext,
2864 ) {
2865 let margin = &style.margin.to_edges();
2866 let padding = &style.padding;
2867
2868 cursor.y += margin.top;
2869
2870 let list_x = x + margin.left;
2871 let outer_width = available_width - margin.horizontal();
2872 let inner_width = outer_width - padding.horizontal();
2873
2874 let n_items = node
2877 .children
2878 .iter()
2879 .filter(|c| matches!(c.kind, NodeKind::ListItem))
2880 .count() as u32;
2881
2882 let marker_gutter =
2883 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
2884
2885 let list_inner_x = list_x + padding.left;
2886 let content_x = list_inner_x + marker_gutter;
2887 let content_width = (inner_width - marker_gutter).max(0.0);
2888
2889 let snapshot = cursor.elements.len();
2892 let list_start_y = cursor.content_y + cursor.y;
2893 cursor.y += padding.top;
2894
2895 let mut item_index: u32 = 0;
2896 for child in &node.children {
2897 if !matches!(child.kind, NodeKind::ListItem) {
2898 continue;
2899 }
2900 let marker_idx = start + item_index;
2901 self.layout_list_item(
2902 child,
2903 marker_idx,
2904 ordered,
2905 marker_type,
2906 marker_gutter,
2907 style,
2908 cursor,
2909 pages,
2910 list_inner_x,
2911 content_x,
2912 content_width,
2913 font_context,
2914 );
2915 item_index += 1;
2916 }
2917
2918 cursor.y += padding.bottom;
2919
2920 let item_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2922 let list_height = cursor.content_y + cursor.y - list_start_y;
2923 cursor.elements.push(LayoutElement {
2924 x: list_x,
2925 y: list_start_y,
2926 width: outer_width,
2927 height: list_height,
2928 draw: DrawCommand::None,
2929 children: item_elements,
2930 node_type: Some("List".to_string()),
2931 resolved_style: Some(style.clone()),
2932 source_location: node.source_location.clone(),
2933 href: None,
2934 bookmark: node.bookmark.clone(),
2935 alt: None,
2936 is_header_row: false,
2937 col_span: 1,
2938 overflow: style.overflow,
2939 opacity: style.opacity,
2940 });
2941
2942 cursor.y += margin.bottom;
2943 }
2944
2945 #[allow(clippy::too_many_arguments)]
2946 fn layout_list_item(
2947 &self,
2948 item: &Node,
2949 marker_idx: u32,
2950 ordered: bool,
2951 marker_type: ListMarkerType,
2952 marker_gutter: f64,
2953 parent_style: &ResolvedStyle,
2954 cursor: &mut PageCursor,
2955 pages: &mut Vec<LayoutPage>,
2956 list_inner_x: f64,
2957 content_x: f64,
2958 content_width: f64,
2959 font_context: &FontContext,
2960 ) {
2961 let item_style = item.style.resolve(Some(parent_style), content_width);
2962 let item_margin = item_style.margin.to_edges();
2963
2964 cursor.y += item_margin.top;
2965 let item_start_y = cursor.content_y + cursor.y;
2966 let item_snapshot = cursor.elements.len();
2967
2968 let marker_str = format_marker(marker_idx, ordered, marker_type);
2972 if !marker_str.is_empty() {
2973 let saved_y = cursor.y;
2974 self.layout_text(
2975 &marker_str,
2976 None,
2977 &[],
2978 &item_style,
2979 cursor,
2980 pages,
2981 list_inner_x,
2982 marker_gutter,
2983 font_context,
2984 None,
2985 None,
2986 Some("Lbl"),
2987 );
2988 cursor.y = saved_y;
2989 }
2990
2991 self.layout_children(
2997 &item.children,
2998 &item.style,
2999 cursor,
3000 pages,
3001 content_x,
3002 content_width,
3003 Some(&item_style),
3004 font_context,
3005 );
3006
3007 let item_children: Vec<LayoutElement> = cursor.elements.drain(item_snapshot..).collect();
3010 let item_height = cursor.content_y + cursor.y - item_start_y;
3011 let item_width = content_x + content_width - list_inner_x;
3012 cursor.elements.push(LayoutElement {
3013 x: list_inner_x,
3014 y: item_start_y,
3015 width: item_width,
3016 height: item_height,
3017 draw: DrawCommand::None,
3018 children: item_children,
3019 node_type: Some("ListItem".to_string()),
3020 resolved_style: Some(item_style.clone()),
3021 source_location: item.source_location.clone(),
3022 href: None,
3023 bookmark: item.bookmark.clone(),
3024 alt: None,
3025 is_header_row: false,
3026 col_span: 1,
3027 overflow: item_style.overflow,
3028 opacity: item_style.opacity,
3029 });
3030
3031 cursor.y += item_margin.bottom;
3032 }
3033
3034 #[allow(clippy::too_many_arguments)]
3035 fn layout_table(
3036 &self,
3037 node: &Node,
3038 style: &ResolvedStyle,
3039 column_defs: &[ColumnDef],
3040 cursor: &mut PageCursor,
3041 pages: &mut Vec<LayoutPage>,
3042 x: f64,
3043 available_width: f64,
3044 font_context: &FontContext,
3045 ) {
3046 let padding = &style.padding;
3047 let margin = &style.margin.to_edges();
3048 let border = &style.border_width;
3049
3050 let table_x = x + margin.left;
3051 let table_width = match style.width {
3052 SizeConstraint::Fixed(w) => w,
3053 SizeConstraint::Auto => available_width - margin.horizontal(),
3054 };
3055 let inner_width = table_width - padding.horizontal() - border.horizontal();
3056
3057 let col_widths = self.resolve_column_widths(column_defs, inner_width, &node.children);
3058
3059 let mut header_rows: Vec<&Node> = Vec::new();
3060 let mut body_rows: Vec<&Node> = Vec::new();
3061
3062 for child in &node.children {
3063 match &child.kind {
3064 NodeKind::TableRow { is_header: true } => header_rows.push(child),
3065 _ => body_rows.push(child),
3066 }
3067 }
3068
3069 if !style.breakable {
3075 let total_height: f64 = node
3076 .children
3077 .iter()
3078 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3079 .sum::<f64>()
3080 + padding.vertical()
3081 + border.vertical();
3082 let fresh_page_available = cursor.content_height
3083 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3084 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3085 if total_height > cursor.remaining_height()
3086 && total_height <= fresh_page_available
3087 && cursor.y > 0.0
3088 {
3089 pages.push(cursor.finalize());
3090 *cursor = cursor.new_page();
3091 }
3092 }
3093
3094 let initial_page_count = pages.len();
3101 let snapshot = cursor.elements.len();
3102 let rect_start_y = cursor.content_y + cursor.y + margin.top;
3103
3104 cursor.y += margin.top + padding.top + border.top;
3105
3106 let cell_x_start = table_x + padding.left + border.left;
3107
3108 if !header_rows.is_empty() {
3133 let total_header_h: f64 = header_rows
3134 .iter()
3135 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3136 .sum();
3137 let first_body_h = body_rows
3138 .first()
3139 .map(|r| self.measure_table_row_height(r, &col_widths, style, font_context))
3140 .unwrap_or(0.0);
3141
3142 let needed = total_header_h + first_body_h;
3143 let fresh_page_available = cursor.content_height
3144 - cursor.fixed_header.iter().map(|(_, h)| *h).sum::<f64>()
3145 - cursor.fixed_footer.iter().map(|(_, h)| *h).sum::<f64>();
3146
3147 if needed > cursor.remaining_height() && needed <= fresh_page_available {
3148 pages.push(cursor.finalize());
3149 *cursor = cursor.new_page();
3150 cursor.y += padding.top + border.top;
3151 }
3152 }
3153
3154 for header_row in &header_rows {
3155 self.layout_table_row(
3156 header_row,
3157 &col_widths,
3158 style,
3159 cursor,
3160 cell_x_start,
3161 font_context,
3162 pages,
3163 );
3164 }
3165
3166 for body_row in &body_rows {
3167 let row_height =
3168 self.measure_table_row_height(body_row, &col_widths, style, font_context);
3169
3170 if row_height > cursor.remaining_height() {
3171 pages.push(cursor.finalize());
3172 *cursor = cursor.new_page();
3173
3174 cursor.y += padding.top + border.top;
3175 for header_row in &header_rows {
3176 self.layout_table_row(
3177 header_row,
3178 &col_widths,
3179 style,
3180 cursor,
3181 cell_x_start,
3182 font_context,
3183 pages,
3184 );
3185 }
3186 }
3187
3188 self.layout_table_row(
3189 body_row,
3190 &col_widths,
3191 style,
3192 cursor,
3193 cell_x_start,
3194 font_context,
3195 pages,
3196 );
3197 }
3198
3199 let has_visual = style.background_color.is_some()
3203 || style.background.is_some()
3204 || style.border_width.top > 0.0
3205 || style.border_width.right > 0.0
3206 || style.border_width.bottom > 0.0
3207 || style.border_width.left > 0.0;
3208 let draw_cmd = if has_visual {
3209 DrawCommand::Rect {
3210 background: style.background_color,
3211 border_width: style.border_width,
3212 border_color: style.border_color,
3213 border_style: style.border_style,
3214 border_radius: style.border_radius,
3215 opacity: 1.0,
3216 box_shadow: style.box_shadow.map(Box::new),
3217 background_gradient: style.background.clone().map(Box::new),
3218 }
3219 } else {
3220 DrawCommand::None
3221 };
3222 let make_wrapper =
3223 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
3224 x: table_x,
3225 y,
3226 width: table_width,
3227 height,
3228 draw,
3229 children,
3230 node_type: Some(node_kind_name(&node.kind).to_string()),
3231 resolved_style: Some(style.clone()),
3232 source_location: node.source_location.clone(),
3233 href: node.href.clone(),
3234 bookmark: None,
3235 alt: None,
3236 is_header_row: false,
3237 col_span: 1,
3238 overflow: Overflow::default(),
3239 opacity: style.opacity,
3240 };
3241
3242 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
3243
3244 if pages.len() == initial_page_count {
3245 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3247 cursor.elements.push(make_wrapper(
3248 rect_start_y,
3249 table_bottom_y - rect_start_y,
3250 child_elements,
3251 draw_cmd,
3252 ));
3253 } else {
3254 let page = &mut pages[initial_page_count];
3259 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3260 let page_content_bottom =
3261 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
3262 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
3263 if !our_elements.is_empty() {
3264 page.elements.push(make_wrapper(
3265 rect_start_y,
3266 page_content_bottom - rect_start_y,
3267 our_elements,
3268 draw_cmd.clone(),
3269 ));
3270 }
3271
3272 for page in &mut pages[initial_page_count + 1..] {
3274 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
3275 let content_top = page.config.margin.top + header_h;
3276 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
3277 let content_bottom = page.config.margin.top
3278 + (page.height - page.config.margin.vertical())
3279 - footer_h;
3280 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
3281 if !all_elements.is_empty() {
3282 page.elements.push(make_wrapper(
3283 content_top,
3284 content_bottom - content_top,
3285 all_elements,
3286 draw_cmd.clone(),
3287 ));
3288 }
3289 }
3290
3291 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
3293 if !all_elements.is_empty() {
3294 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
3295 let content_top = cursor.content_y + header_h;
3296 cursor.elements.push(make_wrapper(
3297 content_top,
3298 table_bottom_y - content_top,
3299 all_elements,
3300 draw_cmd,
3301 ));
3302 }
3303 }
3304
3305 cursor.y += padding.bottom + border.bottom + margin.bottom;
3306 }
3307
3308 #[allow(clippy::too_many_arguments)]
3309 fn layout_table_row(
3310 &self,
3311 row: &Node,
3312 col_widths: &[f64],
3313 parent_style: &ResolvedStyle,
3314 cursor: &mut PageCursor,
3315 start_x: f64,
3316 font_context: &FontContext,
3317 pages: &mut Vec<LayoutPage>,
3318 ) {
3319 let row_style = row
3320 .style
3321 .resolve(Some(parent_style), col_widths.iter().sum());
3322
3323 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
3324 let row_bl = self.row_baseline(row, &row_style, col_widths);
3325 let row_y = cursor.content_y + cursor.y;
3326 let total_width: f64 = col_widths.iter().sum();
3327
3328 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
3329
3330 let row_snapshot = cursor.elements.len();
3332
3333 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
3334 let mut cell_x = start_x;
3335 let mut col_idx = 0usize;
3341 for cell in row.children.iter() {
3342 let span = match &cell.kind {
3343 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
3344 _ => 1,
3345 };
3346 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
3347 col_idx += span;
3348
3349 let cell_style = cell.style.resolve(Some(&row_style), col_width);
3350
3351 let cell_snapshot = cursor.elements.len();
3353
3354 let inner_width =
3355 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
3356
3357 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
3358 let saved_y = cursor.y;
3359 cursor.y += cell_style.padding.top + cell_style.border_width.top;
3360
3361 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
3365 let content_h: f64 = cell
3366 .children
3367 .iter()
3368 .map(|ch| {
3369 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
3370 self.measure_node_height(ch, inner_width, &ch_style, font_context)
3371 })
3372 .sum();
3373 let inner_row =
3374 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
3375 let slack = (inner_row - content_h).max(0.0);
3376 cursor.y += match cell_style.vertical_align {
3377 crate::style::VerticalAlign::Middle => slack / 2.0,
3378 crate::style::VerticalAlign::Bottom => slack,
3379 crate::style::VerticalAlign::Baseline => row_bl
3384 .map(|b| {
3385 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
3386 (b - d).max(0.0)
3387 })
3388 .unwrap_or(0.0),
3389 crate::style::VerticalAlign::Top => 0.0,
3390 };
3391 }
3392
3393 let cursor_before_cell = cursor.clone();
3395 let mut cell_pages: Vec<LayoutPage> = Vec::new();
3396 for child in &cell.children {
3397 self.layout_node(
3398 child,
3399 cursor,
3400 &mut cell_pages,
3401 content_x,
3402 inner_width,
3403 Some(&cell_style),
3404 font_context,
3405 None,
3406 None,
3407 );
3408 }
3409
3410 if !cell_pages.is_empty() {
3412 let post_break_elements = std::mem::take(&mut cursor.elements);
3413 if let Some(last_page) = cell_pages.last_mut() {
3414 last_page.elements.extend(post_break_elements);
3415 }
3416 if !is_header {
3422 all_overflow_pages.extend(cell_pages);
3423 }
3424 *cursor = cursor_before_cell;
3425 }
3426
3427 cursor.y = saved_y;
3428
3429 let cell_children: Vec<LayoutElement> =
3431 cursor.elements.drain(cell_snapshot..).collect();
3432
3433 cursor.elements.push(LayoutElement {
3435 x: cell_x,
3436 y: row_y,
3437 width: col_width,
3438 height: row_height,
3439 draw: if cell_style.background_color.is_some()
3440 || cell_style.border_width.horizontal() > 0.0
3441 || cell_style.border_width.vertical() > 0.0
3442 {
3443 DrawCommand::Rect {
3444 background: cell_style.background_color,
3445 border_width: cell_style.border_width,
3446 border_color: cell_style.border_color,
3447 border_style: cell_style.border_style,
3448 border_radius: cell_style.border_radius,
3449 opacity: 1.0,
3450 box_shadow: cell_style.box_shadow.map(Box::new),
3451 background_gradient: cell_style.background.clone().map(Box::new),
3452 }
3453 } else {
3454 DrawCommand::None
3455 },
3456 children: cell_children,
3457 node_type: Some("TableCell".to_string()),
3458 resolved_style: Some(cell_style.clone()),
3459 source_location: cell.source_location.clone(),
3460 href: None,
3461 bookmark: cell.bookmark.clone(),
3462 alt: None,
3463 is_header_row: is_header,
3464 col_span: span as u32,
3465 overflow: Overflow::default(),
3466 opacity: 1.0,
3467 });
3468
3469 cell_x += col_width;
3470 }
3471
3472 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
3474 cursor.elements.push(LayoutElement {
3475 x: start_x,
3476 y: row_y,
3477 width: total_width,
3478 height: row_height,
3479 draw: if let Some(bg) = row_style.background_color {
3480 DrawCommand::Rect {
3481 background: Some(bg),
3482 border_width: Edges::default(),
3483 border_color: EdgeValues::uniform(Color::BLACK),
3484 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
3485 border_radius: CornerValues::uniform(0.0),
3486 opacity: 1.0,
3487 box_shadow: row_style.box_shadow.map(Box::new),
3488 background_gradient: row_style.background.clone().map(Box::new),
3489 }
3490 } else {
3491 DrawCommand::None
3492 },
3493 children: row_children,
3494 node_type: Some("TableRow".to_string()),
3495 resolved_style: Some(row_style.clone()),
3496 source_location: row.source_location.clone(),
3497 href: None,
3498 bookmark: row.bookmark.clone(),
3499 alt: None,
3500 is_header_row: is_header,
3501 col_span: 1,
3502 overflow: row_style.overflow,
3503 opacity: row_style.opacity,
3504 });
3505
3506 pages.extend(all_overflow_pages);
3508
3509 cursor.y += row_height;
3510 }
3511
3512 #[allow(clippy::too_many_arguments)]
3513 #[allow(clippy::too_many_arguments)]
3514 fn layout_text(
3515 &self,
3516 content: &str,
3517 href: Option<&str>,
3518 runs: &[TextRun],
3519 style: &ResolvedStyle,
3520 cursor: &mut PageCursor,
3521 pages: &mut Vec<LayoutPage>,
3522 x: f64,
3523 available_width: f64,
3524 font_context: &FontContext,
3525 source_location: Option<&SourceLocation>,
3526 bookmark: Option<&str>,
3527 node_type_override: Option<&str>,
3531 ) {
3532 let margin = &style.margin.to_edges();
3533 let text_x = x + margin.left;
3534 let text_width = match style.width {
3541 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
3542 SizeConstraint::Auto => available_width - margin.horizontal(),
3543 };
3544
3545 cursor.y += margin.top;
3546
3547 if !runs.is_empty() {
3549 self.layout_text_runs(
3550 runs,
3551 href,
3552 style,
3553 cursor,
3554 pages,
3555 text_x,
3556 text_width,
3557 font_context,
3558 source_location,
3559 bookmark,
3560 node_type_override,
3561 );
3562 cursor.y += margin.bottom;
3563 return;
3564 }
3565
3566 let content = substitute_page_placeholders(content);
3567 let transformed = apply_text_transform(&content, style.text_transform);
3568 let justify = matches!(style.text_align, TextAlign::Justify);
3569 let lines = match style.line_breaking {
3570 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
3571 font_context,
3572 &transformed,
3573 text_width,
3574 style.font_size,
3575 &style.font_family,
3576 style.font_weight,
3577 style.font_style,
3578 style.letter_spacing,
3579 style.hyphens,
3580 style.lang.as_deref(),
3581 justify,
3582 ),
3583 LineBreaking::Greedy => self.text_layout.break_into_lines(
3584 font_context,
3585 &transformed,
3586 text_width,
3587 style.font_size,
3588 &style.font_family,
3589 style.font_weight,
3590 style.font_style,
3591 style.letter_spacing,
3592 style.hyphens,
3593 style.lang.as_deref(),
3594 ),
3595 };
3596
3597 let lines = match style.text_overflow {
3599 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
3600 font_context,
3601 lines,
3602 text_width,
3603 style.font_size,
3604 &style.font_family,
3605 style.font_weight,
3606 style.font_style,
3607 style.letter_spacing,
3608 ),
3609 TextOverflow::Clip => self.text_layout.truncate_clip(
3610 font_context,
3611 lines,
3612 text_width,
3613 style.font_size,
3614 &style.font_family,
3615 style.font_weight,
3616 style.font_style,
3617 style.letter_spacing,
3618 ),
3619 TextOverflow::Wrap => lines,
3620 };
3621
3622 let line_height = style.font_size * style.line_height;
3623
3624 let line_heights: Vec<f64> = vec![line_height; lines.len()];
3626 let decision = page_break::decide_break(
3627 cursor.remaining_height(),
3628 &line_heights,
3629 true,
3630 style.min_orphan_lines as usize,
3631 style.min_widow_lines as usize,
3632 );
3633
3634 let mut snapshot = cursor.elements.len();
3636 let mut container_start_y = cursor.content_y + cursor.y;
3637 let mut is_first_element = true;
3638
3639 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
3641 pages.push(cursor.finalize());
3642 *cursor = cursor.new_page();
3643 snapshot = cursor.elements.len();
3644 container_start_y = cursor.content_y + cursor.y;
3645 }
3646
3647 let forced_break_at = match decision {
3649 page_break::BreakDecision::Split {
3650 items_on_current_page,
3651 } => Some(items_on_current_page),
3652 _ => None,
3653 };
3654 let mut first_break_done = false;
3655
3656 for (line_idx, line) in lines.iter().enumerate() {
3657 let needs_break = if let Some(break_at) = forced_break_at {
3659 if !first_break_done && line_idx == break_at {
3660 true
3661 } else {
3662 line_height > cursor.remaining_height()
3663 }
3664 } else {
3665 line_height > cursor.remaining_height()
3666 };
3667
3668 if needs_break {
3669 first_break_done = true;
3670 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3672 if !line_elements.is_empty() {
3673 let container_height = cursor.content_y + cursor.y - container_start_y;
3674 cursor.elements.push(LayoutElement {
3675 x: text_x,
3676 y: container_start_y,
3677 width: text_width,
3678 height: container_height,
3679 draw: DrawCommand::None,
3680 children: line_elements,
3681 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3682 resolved_style: Some(style.clone()),
3683 source_location: source_location.cloned(),
3684 href: href.map(|s| s.to_string()),
3685 bookmark: if is_first_element {
3686 bookmark.map(|s| s.to_string())
3687 } else {
3688 None
3689 },
3690 alt: None,
3691 is_header_row: false,
3692 col_span: 1,
3693 overflow: Overflow::default(),
3694 opacity: 1.0,
3695 });
3696 is_first_element = false;
3697 }
3698
3699 pages.push(cursor.finalize());
3700 *cursor = cursor.new_page();
3701
3702 snapshot = cursor.elements.len();
3704 container_start_y = cursor.content_y + cursor.y;
3705 }
3706
3707 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
3708
3709 let rendered_width = if glyphs.is_empty() {
3713 line.width
3714 } else {
3715 let last = &glyphs[glyphs.len() - 1];
3716 (last.x_offset + last.x_advance).max(line.width * 0.5)
3717 };
3718
3719 let line_x = match style.text_align {
3720 TextAlign::Left => text_x,
3721 TextAlign::Right => text_x + text_width - rendered_width,
3722 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
3723 TextAlign::Justify => text_x,
3724 };
3725
3726 let is_last_line = line_idx == lines.len() - 1;
3734 let user_ws = style.word_spacing;
3735 let (justified_width, word_spacing) =
3736 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
3737 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
3738 let (natural_width, space_count) = if let Some(idx) = last_non_space {
3739 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
3740 let s = glyphs[..=idx]
3741 .iter()
3742 .filter(|g| g.char_value == ' ')
3743 .count();
3744 (w, s)
3745 } else {
3746 (0.0, 0)
3747 };
3748 let slack = text_width - natural_width;
3749 let ws = if space_count > 0 && slack.abs() > 0.01 {
3750 slack / space_count as f64
3751 } else {
3752 0.0
3753 };
3754 (text_width, user_ws + ws)
3755 } else {
3756 (rendered_width, user_ws)
3757 };
3758
3759 let text_line = TextLine {
3760 x: line_x,
3761 y: cursor.content_y + cursor.y + style.font_size,
3762 glyphs,
3763 width: justified_width,
3764 height: line_height,
3765 word_spacing,
3766 };
3767
3768 cursor.elements.push(LayoutElement {
3769 x: line_x,
3770 y: cursor.content_y + cursor.y,
3771 width: justified_width,
3772 height: line_height,
3773 draw: DrawCommand::Text {
3774 lines: vec![text_line],
3775 color: style.color,
3776 text_decoration: style.text_decoration,
3777 opacity: 1.0,
3778 },
3779 children: vec![],
3780 node_type: Some("TextLine".to_string()),
3781 resolved_style: Some(style.clone()),
3782 source_location: None,
3783 href: href.map(|s| s.to_string()),
3784 bookmark: None,
3785 alt: None,
3786 is_header_row: false,
3787 col_span: 1,
3788 overflow: Overflow::default(),
3789 opacity: 1.0,
3790 });
3791
3792 cursor.y += line_height;
3793 }
3794
3795 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3797 if !line_elements.is_empty() {
3798 let container_height = cursor.content_y + cursor.y - container_start_y;
3799 cursor.elements.push(LayoutElement {
3800 x: text_x,
3801 y: container_start_y,
3802 width: text_width,
3803 height: container_height,
3804 draw: DrawCommand::None,
3805 children: line_elements,
3806 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3807 resolved_style: Some(style.clone()),
3808 source_location: source_location.cloned(),
3809 href: href.map(|s| s.to_string()),
3810 bookmark: if is_first_element {
3811 bookmark.map(|s| s.to_string())
3812 } else {
3813 None
3814 },
3815 alt: None,
3816 is_header_row: false,
3817 col_span: 1,
3818 overflow: Overflow::default(),
3819 opacity: 1.0,
3820 });
3821 }
3822
3823 cursor.y += margin.bottom;
3824 }
3825
3826 #[allow(clippy::too_many_arguments)]
3828 #[allow(clippy::too_many_arguments)]
3829 fn layout_text_runs(
3830 &self,
3831 runs: &[TextRun],
3832 parent_href: Option<&str>,
3833 style: &ResolvedStyle,
3834 cursor: &mut PageCursor,
3835 pages: &mut Vec<LayoutPage>,
3836 text_x: f64,
3837 text_width: f64,
3838 font_context: &FontContext,
3839 source_location: Option<&SourceLocation>,
3840 bookmark: Option<&str>,
3841 node_type_override: Option<&str>,
3843 ) {
3844 let mut styled_chars: Vec<StyledChar> = Vec::new();
3846 for run in runs {
3847 let run_style = run.style.resolve(Some(style), text_width);
3848 let run_href = run.href.as_deref().or(parent_href);
3849 let transform = run_style.text_transform;
3850 let run_content = substitute_page_placeholders(&run.content);
3851 let mut prev_is_whitespace = true;
3852 for ch in run_content.chars() {
3853 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
3854 prev_is_whitespace = ch.is_whitespace();
3855 styled_chars.push(StyledChar {
3856 ch: transformed_ch,
3857 font_family: run_style.font_family.clone(),
3858 font_size: run_style.font_size,
3859 font_weight: run_style.font_weight,
3860 font_style: run_style.font_style,
3861 color: run_style.color,
3862 href: run_href.map(|s| s.to_string()),
3863 text_decoration: run_style.text_decoration,
3864 letter_spacing: run_style.letter_spacing,
3865 });
3866 }
3867 }
3868
3869 let justify = matches!(style.text_align, TextAlign::Justify);
3871 let broken_lines = match style.line_breaking {
3872 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
3873 font_context,
3874 &styled_chars,
3875 text_width,
3876 style.hyphens,
3877 style.lang.as_deref(),
3878 justify,
3879 ),
3880 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
3881 font_context,
3882 &styled_chars,
3883 text_width,
3884 style.hyphens,
3885 style.lang.as_deref(),
3886 ),
3887 };
3888
3889 let broken_lines = match style.text_overflow {
3891 TextOverflow::Ellipsis => {
3892 self.text_layout
3893 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
3894 }
3895 TextOverflow::Clip => {
3896 self.text_layout
3897 .truncate_runs_clip(font_context, broken_lines, text_width)
3898 }
3899 TextOverflow::Wrap => broken_lines,
3900 };
3901
3902 let line_height = style.font_size * style.line_height;
3903
3904 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
3906 let decision = page_break::decide_break(
3907 cursor.remaining_height(),
3908 &line_heights,
3909 true,
3910 style.min_orphan_lines as usize,
3911 style.min_widow_lines as usize,
3912 );
3913
3914 let mut snapshot = cursor.elements.len();
3915 let mut container_start_y = cursor.content_y + cursor.y;
3916 let mut is_first_element = true;
3917
3918 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
3919 pages.push(cursor.finalize());
3920 *cursor = cursor.new_page();
3921 snapshot = cursor.elements.len();
3922 container_start_y = cursor.content_y + cursor.y;
3923 }
3924
3925 let forced_break_at = match decision {
3926 page_break::BreakDecision::Split {
3927 items_on_current_page,
3928 } => Some(items_on_current_page),
3929 _ => None,
3930 };
3931 let mut first_break_done = false;
3932
3933 for (line_idx, run_line) in broken_lines.iter().enumerate() {
3934 let needs_break = if let Some(break_at) = forced_break_at {
3935 if !first_break_done && line_idx == break_at {
3936 true
3937 } else {
3938 line_height > cursor.remaining_height()
3939 }
3940 } else {
3941 line_height > cursor.remaining_height()
3942 };
3943
3944 if needs_break {
3945 first_break_done = true;
3946 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
3947 if !line_elements.is_empty() {
3948 let container_height = cursor.content_y + cursor.y - container_start_y;
3949 cursor.elements.push(LayoutElement {
3950 x: text_x,
3951 y: container_start_y,
3952 width: text_width,
3953 height: container_height,
3954 draw: DrawCommand::None,
3955 children: line_elements,
3956 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
3957 resolved_style: Some(style.clone()),
3958 source_location: source_location.cloned(),
3959 href: parent_href.map(|s| s.to_string()),
3960 bookmark: if is_first_element {
3961 bookmark.map(|s| s.to_string())
3962 } else {
3963 None
3964 },
3965 alt: None,
3966 is_header_row: false,
3967 col_span: 1,
3968 overflow: Overflow::default(),
3969 opacity: 1.0,
3970 });
3971 is_first_element = false;
3972 }
3973
3974 pages.push(cursor.finalize());
3975 *cursor = cursor.new_page();
3976
3977 snapshot = cursor.elements.len();
3978 container_start_y = cursor.content_y + cursor.y;
3979 }
3980
3981 let line_x = match style.text_align {
3982 TextAlign::Left => text_x,
3983 TextAlign::Right => text_x + text_width - run_line.width,
3984 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
3985 TextAlign::Justify => text_x,
3986 };
3987
3988 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
3989
3990 let is_last_line = line_idx == broken_lines.len() - 1;
3997 let user_ws = style.word_spacing;
3998 let (justified_width, word_spacing) =
3999 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4000 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4001 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4002 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4003 let s = glyphs[..=idx]
4004 .iter()
4005 .filter(|g| g.char_value == ' ')
4006 .count();
4007 (w, s)
4008 } else {
4009 (0.0, 0)
4010 };
4011 let slack = text_width - natural_width;
4012 let ws = if space_count > 0 && slack.abs() > 0.01 {
4013 slack / space_count as f64
4014 } else {
4015 0.0
4016 };
4017 (text_width, user_ws + ws)
4018 } else {
4019 (run_line.width, user_ws)
4020 };
4021
4022 let text_line = TextLine {
4023 x: line_x,
4024 y: cursor.content_y + cursor.y + style.font_size,
4025 glyphs,
4026 width: justified_width,
4027 height: line_height,
4028 word_spacing,
4029 };
4030
4031 let text_dec = run_line
4033 .chars
4034 .iter()
4035 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
4036 .map(|sc| sc.text_decoration)
4037 .unwrap_or(style.text_decoration);
4038
4039 cursor.elements.push(LayoutElement {
4040 x: line_x,
4041 y: cursor.content_y + cursor.y,
4042 width: justified_width,
4043 height: line_height,
4044 draw: DrawCommand::Text {
4045 lines: vec![text_line],
4046 color: style.color,
4047 text_decoration: text_dec,
4048 opacity: 1.0,
4049 },
4050 children: vec![],
4051 node_type: Some("TextLine".to_string()),
4052 resolved_style: Some(style.clone()),
4053 source_location: None,
4054 href: parent_href.map(|s| s.to_string()),
4055 bookmark: None,
4056 alt: None,
4057 is_header_row: false,
4058 col_span: 1,
4059 overflow: Overflow::default(),
4060 opacity: 1.0,
4061 });
4062
4063 cursor.y += line_height;
4064 }
4065
4066 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
4067 if !line_elements.is_empty() {
4068 let container_height = cursor.content_y + cursor.y - container_start_y;
4069 cursor.elements.push(LayoutElement {
4070 x: text_x,
4071 y: container_start_y,
4072 width: text_width,
4073 height: container_height,
4074 draw: DrawCommand::None,
4075 children: line_elements,
4076 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4077 resolved_style: Some(style.clone()),
4078 source_location: source_location.cloned(),
4079 href: parent_href.map(|s| s.to_string()),
4080 bookmark: if is_first_element {
4081 bookmark.map(|s| s.to_string())
4082 } else {
4083 None
4084 },
4085 alt: None,
4086 is_header_row: false,
4087 col_span: 1,
4088 overflow: Overflow::default(),
4089 opacity: 1.0,
4090 });
4091 }
4092 }
4093
4094 fn build_positioned_glyphs_single_style(
4098 &self,
4099 line: &BrokenLine,
4100 style: &ResolvedStyle,
4101 href: Option<&str>,
4102 font_context: &FontContext,
4103 ) -> Vec<PositionedGlyph> {
4104 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4105 let line_text: String = line.chars.iter().collect();
4106 let direction = style.direction;
4107 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4109
4110 let font_runs = crate::font::fallback::segment_by_font(
4113 &line.chars,
4114 &style.font_family,
4115 style.font_weight,
4116 italic,
4117 font_context.registry(),
4118 );
4119 let needs_per_char_fallback = font_runs.len() > 1
4120 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
4121
4122 if needs_per_char_fallback {
4124 let bidi_runs = if has_bidi {
4125 bidi::analyze_bidi(&line_text, direction)
4126 } else {
4127 vec![crate::text::bidi::BidiRun {
4128 char_start: 0,
4129 char_end: line.chars.len(),
4130 level: unicode_bidi::Level::ltr(),
4131 is_rtl: false,
4132 }]
4133 };
4134
4135 let mut all_glyphs = Vec::new();
4136 let mut bidi_levels = Vec::new();
4137 let mut x = 0.0_f64;
4138
4139 for bidi_run in &bidi_runs {
4141 for font_run in &font_runs {
4143 let start = font_run.start.max(bidi_run.char_start);
4145 let end = font_run.end.min(bidi_run.char_end);
4146 if start >= end {
4147 continue;
4148 }
4149
4150 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
4151 let sub_text: String = sub_chars.iter().collect();
4152 let resolved_family = &font_run.family;
4153
4154 if let Some(font_data) =
4155 font_context.font_data(resolved_family, style.font_weight, italic)
4156 {
4157 if let Some(shaped) = shaping::shape_text_with_direction(
4158 &sub_text,
4159 font_data,
4160 bidi_run.is_rtl,
4161 ) {
4162 let units_per_em = font_context.units_per_em(
4163 resolved_family,
4164 style.font_weight,
4165 italic,
4166 );
4167 let scale = style.font_size / units_per_em as f64;
4168
4169 for sg in &shaped {
4170 let cluster = sg.cluster as usize;
4171 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
4172
4173 let cluster_text = if shaped.len() < sub_chars.len() {
4174 let cluster_end =
4175 self.find_cluster_end(&shaped, sg, sub_chars.len());
4176 if cluster_end > cluster + 1 {
4177 Some(
4178 sub_chars[cluster..cluster_end]
4179 .iter()
4180 .collect::<String>(),
4181 )
4182 } else {
4183 None
4184 }
4185 } else {
4186 None
4187 };
4188
4189 let glyph_x = x + sg.x_offset as f64 * scale;
4190 let glyph_y = sg.y_offset as f64 * scale;
4191 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4192
4193 all_glyphs.push(PositionedGlyph {
4194 glyph_id: sg.glyph_id,
4195 x_offset: glyph_x,
4196 y_offset: glyph_y,
4197 x_advance: advance,
4198 font_size: style.font_size,
4199 font_family: resolved_family.clone(),
4200 font_weight: style.font_weight,
4201 font_style: style.font_style,
4202 char_value,
4203 color: Some(style.color),
4204 href: href.map(|s| s.to_string()),
4205 text_decoration: style.text_decoration,
4206 letter_spacing: style.letter_spacing,
4207 cluster_text,
4208 });
4209 bidi_levels.push(bidi_run.level);
4210 x += advance;
4211 }
4212 continue;
4213 }
4214 }
4215
4216 for i in start..end {
4218 let ch = line.chars[i];
4219 let glyph_x = x;
4220 let char_width = font_context.char_width(
4221 ch,
4222 resolved_family,
4223 style.font_weight,
4224 italic,
4225 style.font_size,
4226 );
4227 let advance = char_width + style.letter_spacing;
4228 all_glyphs.push(PositionedGlyph {
4229 glyph_id: ch as u16,
4230 x_offset: glyph_x,
4231 y_offset: 0.0,
4232 x_advance: advance,
4233 font_size: style.font_size,
4234 font_family: resolved_family.clone(),
4235 font_weight: style.font_weight,
4236 font_style: style.font_style,
4237 char_value: ch,
4238 color: Some(style.color),
4239 href: href.map(|s| s.to_string()),
4240 text_decoration: style.text_decoration,
4241 letter_spacing: style.letter_spacing,
4242 cluster_text: None,
4243 });
4244 bidi_levels.push(bidi_run.level);
4245 x += advance;
4246 }
4247 }
4248 }
4249
4250 if has_bidi && !all_glyphs.is_empty() {
4252 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4253 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
4254 }
4255 return all_glyphs;
4256 }
4257
4258 if let Some(font_data) =
4261 font_context.font_data(&style.font_family, style.font_weight, italic)
4262 {
4263 if has_bidi {
4264 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4266 let units_per_em =
4267 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4268 let scale = style.font_size / units_per_em as f64;
4269
4270 let mut all_glyphs = Vec::new();
4271 let mut bidi_levels = Vec::new();
4272 let mut x = 0.0_f64;
4273
4274 for run in &bidi_runs {
4275 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
4276 let run_text: String = run_chars.iter().collect();
4277
4278 if let Some(shaped) =
4279 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
4280 {
4281 for sg in &shaped {
4282 let cluster = sg.cluster as usize;
4283 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
4284
4285 let cluster_text = if shaped.len() < run_chars.len() {
4286 let cluster_end =
4287 self.find_cluster_end(&shaped, sg, run_chars.len());
4288 if cluster_end > cluster + 1 {
4289 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
4290 } else {
4291 None
4292 }
4293 } else {
4294 None
4295 };
4296
4297 let glyph_x = x + sg.x_offset as f64 * scale;
4298 let glyph_y = sg.y_offset as f64 * scale;
4299 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
4300
4301 all_glyphs.push(PositionedGlyph {
4302 glyph_id: sg.glyph_id,
4303 x_offset: glyph_x,
4304 y_offset: glyph_y,
4305 x_advance: advance,
4306 font_size: style.font_size,
4307 font_family: style.font_family.clone(),
4308 font_weight: style.font_weight,
4309 font_style: style.font_style,
4310 char_value,
4311 color: Some(style.color),
4312 href: href.map(|s| s.to_string()),
4313 text_decoration: style.text_decoration,
4314 letter_spacing: style.letter_spacing,
4315 cluster_text,
4316 });
4317 bidi_levels.push(run.level);
4318
4319 x += advance;
4320 }
4321 }
4322 }
4323
4324 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
4326 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4327 return glyphs;
4328 }
4329
4330 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
4332 let units_per_em =
4333 font_context.units_per_em(&style.font_family, style.font_weight, italic);
4334 let scale = style.font_size / units_per_em as f64;
4335
4336 return self.shaped_glyphs_to_positioned(
4337 &shaped,
4338 &line.chars,
4339 &line.char_positions,
4340 scale,
4341 style.font_size,
4342 &style.font_family,
4343 style.font_weight,
4344 style.font_style,
4345 Some(style.color),
4346 href,
4347 style.text_decoration,
4348 style.letter_spacing,
4349 );
4350 }
4351 }
4352
4353 let mut glyphs: Vec<PositionedGlyph> = line
4355 .chars
4356 .iter()
4357 .enumerate()
4358 .map(|(j, ch)| {
4359 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
4360 let char_width = font_context.char_width(
4361 *ch,
4362 &style.font_family,
4363 style.font_weight,
4364 italic,
4365 style.font_size,
4366 );
4367 PositionedGlyph {
4368 glyph_id: *ch as u16,
4369 x_offset: glyph_x,
4370 y_offset: 0.0,
4371 x_advance: char_width,
4372 font_size: style.font_size,
4373 font_family: style.font_family.clone(),
4374 font_weight: style.font_weight,
4375 font_style: style.font_style,
4376 char_value: *ch,
4377 color: Some(style.color),
4378 href: href.map(|s| s.to_string()),
4379 text_decoration: style.text_decoration,
4380 letter_spacing: style.letter_spacing,
4381 cluster_text: None,
4382 }
4383 })
4384 .collect();
4385
4386 if has_bidi && !glyphs.is_empty() {
4388 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
4389 let mut levels = Vec::with_capacity(glyphs.len());
4390 let mut char_idx = 0;
4391 for run in &bidi_runs {
4392 for _ in run.char_start..run.char_end {
4393 if char_idx < glyphs.len() {
4394 levels.push(run.level);
4395 char_idx += 1;
4396 }
4397 }
4398 }
4399 while levels.len() < glyphs.len() {
4401 levels.push(unicode_bidi::Level::ltr());
4402 }
4403 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
4404 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4405 }
4406
4407 glyphs
4408 }
4409
4410 fn build_positioned_glyphs_runs(
4415 &self,
4416 run_line: &RunBrokenLine,
4417 font_context: &FontContext,
4418 direction: Direction,
4419 ) -> Vec<PositionedGlyph> {
4420 let chars = &run_line.chars;
4421 if chars.is_empty() {
4422 return vec![];
4423 }
4424
4425 let resolved_families: Vec<String> = chars
4428 .iter()
4429 .map(|sc| {
4430 if !sc.font_family.contains(',') {
4431 sc.font_family.clone()
4432 } else {
4433 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4434 let (_, family) = font_context.registry().resolve_for_char(
4435 &sc.font_family,
4436 sc.ch,
4437 sc.font_weight,
4438 italic,
4439 );
4440 family
4441 }
4442 })
4443 .collect();
4444
4445 let line_text: String = chars.iter().map(|c| c.ch).collect();
4446 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
4447 let bidi_runs = if has_bidi {
4448 Some(bidi::analyze_bidi(&line_text, direction))
4449 } else {
4450 None
4451 };
4452
4453 let mut glyphs = Vec::new();
4454 let mut bidi_levels = Vec::new();
4455 let mut i = 0;
4456
4457 while i < chars.len() {
4458 let sc = &chars[i];
4459 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
4460 let resolved_family = &resolved_families[i];
4461
4462 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4464 runs.iter()
4465 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
4466 });
4467
4468 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
4470 {
4471 let run_start = i;
4473 let mut run_end = i + 1;
4474 while run_end < chars.len() {
4475 let next = &chars[run_end];
4476 let next_italic =
4477 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
4478 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
4479 runs.iter()
4480 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
4481 });
4482 if resolved_families[run_end] == *resolved_family
4484 && next.font_weight == sc.font_weight
4485 && next_italic == italic
4486 && (next.font_size - sc.font_size).abs() < 0.001
4487 && next_is_rtl == is_rtl
4488 {
4489 run_end += 1;
4490 } else {
4491 break;
4492 }
4493 }
4494
4495 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
4496 if let Some(shaped) =
4497 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
4498 {
4499 let units_per_em =
4500 font_context.units_per_em(resolved_family, sc.font_weight, italic);
4501 let scale = sc.font_size / units_per_em as f64;
4502
4503 let run_chars: Vec<char> =
4505 chars[run_start..run_end].iter().map(|c| c.ch).collect();
4506 let run_positions: Vec<f64> = (run_start..run_end)
4507 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
4508 .collect();
4509
4510 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
4512 &shaped,
4513 &chars[run_start..run_end],
4514 &run_chars,
4515 &run_positions,
4516 scale,
4517 );
4518 for g in &mut run_glyphs {
4520 g.font_family = resolved_family.clone();
4521 }
4522 let run_level = if is_rtl {
4524 unicode_bidi::Level::rtl()
4525 } else {
4526 unicode_bidi::Level::ltr()
4527 };
4528 for _ in &run_glyphs {
4529 bidi_levels.push(run_level);
4530 }
4531 glyphs.extend(run_glyphs);
4532 i = run_end;
4533 continue;
4534 }
4535 }
4536
4537 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
4539 let char_width = font_context.char_width(
4540 sc.ch,
4541 resolved_family,
4542 sc.font_weight,
4543 italic,
4544 sc.font_size,
4545 );
4546 glyphs.push(PositionedGlyph {
4547 glyph_id: sc.ch as u16,
4548 x_offset: glyph_x,
4549 y_offset: 0.0,
4550 x_advance: char_width,
4551 font_size: sc.font_size,
4552 font_family: resolved_family.clone(),
4553 font_weight: sc.font_weight,
4554 font_style: sc.font_style,
4555 char_value: sc.ch,
4556 color: Some(sc.color),
4557 href: sc.href.clone(),
4558 text_decoration: sc.text_decoration,
4559 letter_spacing: sc.letter_spacing,
4560 cluster_text: None,
4561 });
4562 bidi_levels.push(if is_rtl {
4563 unicode_bidi::Level::rtl()
4564 } else {
4565 unicode_bidi::Level::ltr()
4566 });
4567 i += 1;
4568 }
4569
4570 if has_bidi && !glyphs.is_empty() {
4572 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
4573 bidi::reposition_after_reorder(&mut glyphs, 0.0);
4574 }
4575
4576 glyphs
4577 }
4578
4579 #[allow(clippy::too_many_arguments)]
4581 fn shaped_glyphs_to_positioned(
4582 &self,
4583 shaped: &[shaping::ShapedGlyph],
4584 chars: &[char],
4585 _char_positions: &[f64],
4586 scale: f64,
4587 font_size: f64,
4588 font_family: &str,
4589 font_weight: u32,
4590 font_style: FontStyle,
4591 color: Option<Color>,
4592 href: Option<&str>,
4593 text_decoration: TextDecoration,
4594 letter_spacing: f64,
4595 ) -> Vec<PositionedGlyph> {
4596 let mut result = Vec::with_capacity(shaped.len());
4597 let mut x = 0.0_f64;
4598
4599 for sg in shaped {
4600 let cluster = sg.cluster as usize;
4601 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4602
4603 let cluster_text = if shaped.len() < chars.len() {
4605 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4608 if cluster_end > cluster + 1 {
4609 Some(chars[cluster..cluster_end].iter().collect::<String>())
4610 } else {
4611 None
4612 }
4613 } else {
4614 None
4615 };
4616
4617 let glyph_x = x + sg.x_offset as f64 * scale;
4619 let glyph_y = sg.y_offset as f64 * scale;
4620 let advance = sg.x_advance as f64 * scale + letter_spacing;
4621
4622 result.push(PositionedGlyph {
4623 glyph_id: sg.glyph_id,
4624 x_offset: glyph_x,
4625 y_offset: glyph_y,
4626 x_advance: advance,
4627 font_size,
4628 font_family: font_family.to_string(),
4629 font_weight,
4630 font_style,
4631 char_value,
4632 color,
4633 href: href.map(|s| s.to_string()),
4634 text_decoration,
4635 letter_spacing,
4636 cluster_text,
4637 });
4638
4639 x += advance;
4640 }
4641
4642 result
4643 }
4644
4645 fn shaped_glyphs_to_positioned_runs(
4647 &self,
4648 shaped: &[shaping::ShapedGlyph],
4649 styled_chars: &[StyledChar],
4650 chars: &[char],
4651 char_positions: &[f64],
4652 scale: f64,
4653 ) -> Vec<PositionedGlyph> {
4654 let mut result = Vec::with_capacity(shaped.len());
4655 let base_x = char_positions.first().copied().unwrap_or(0.0);
4657 let mut x = 0.0_f64;
4658
4659 for sg in shaped {
4660 let cluster = sg.cluster as usize;
4661 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
4662 let char_value = chars.get(cluster).copied().unwrap_or(' ');
4663
4664 let cluster_text = if shaped.len() < chars.len() {
4665 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
4666 if cluster_end > cluster + 1 {
4667 Some(chars[cluster..cluster_end].iter().collect::<String>())
4668 } else {
4669 None
4670 }
4671 } else {
4672 None
4673 };
4674
4675 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
4676 let glyph_y = sg.y_offset as f64 * scale;
4677 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
4678
4679 result.push(PositionedGlyph {
4680 glyph_id: sg.glyph_id,
4681 x_offset: glyph_x,
4682 y_offset: glyph_y,
4683 x_advance: advance,
4684 font_size: sc.font_size,
4685 font_family: sc.font_family.clone(),
4686 font_weight: sc.font_weight,
4687 font_style: sc.font_style,
4688 char_value,
4689 color: Some(sc.color),
4690 href: sc.href.clone(),
4691 text_decoration: sc.text_decoration,
4692 letter_spacing: sc.letter_spacing,
4693 cluster_text,
4694 });
4695
4696 x += advance;
4697 }
4698
4699 result
4700 }
4701
4702 fn find_cluster_end(
4704 &self,
4705 shaped: &[shaping::ShapedGlyph],
4706 current: &shaping::ShapedGlyph,
4707 num_chars: usize,
4708 ) -> usize {
4709 for sg in shaped {
4711 if sg.cluster > current.cluster {
4712 return sg.cluster as usize;
4713 }
4714 }
4715 num_chars
4717 }
4718
4719 #[allow(clippy::too_many_arguments)]
4720 fn layout_image(
4721 &self,
4722 node: &Node,
4723 style: &ResolvedStyle,
4724 cursor: &mut PageCursor,
4725 pages: &mut Vec<LayoutPage>,
4726 x: f64,
4727 available_width: f64,
4728 explicit_width: Option<f64>,
4729 explicit_height: Option<f64>,
4730 ) {
4731 let margin = &style.margin.to_edges();
4732
4733 let src = match &node.kind {
4735 NodeKind::Image { src, .. } => src.as_str(),
4736 _ => "",
4737 };
4738
4739 let loaded = if !src.is_empty() {
4740 crate::image_loader::load_image(src).ok()
4741 } else {
4742 None
4743 };
4744
4745 let (img_width, img_height) = if let Some(ref img) = loaded {
4747 let intrinsic_w = img.width_px as f64;
4748 let intrinsic_h = img.height_px as f64;
4749 let aspect = if intrinsic_w > 0.0 {
4750 intrinsic_h / intrinsic_w
4751 } else {
4752 0.75
4753 };
4754
4755 match (explicit_width, explicit_height) {
4756 (Some(w), Some(h)) => (w, h),
4757 (Some(w), None) => (w, w * aspect),
4758 (None, Some(h)) => (h / aspect, h),
4759 (None, None) => {
4760 let max_w = available_width - margin.horizontal();
4761 let w = intrinsic_w.min(max_w);
4762 (w, w * aspect)
4763 }
4764 }
4765 } else {
4766 let w = explicit_width.unwrap_or(available_width - margin.horizontal());
4768 let h = explicit_height.unwrap_or(w * 0.75);
4769 (w, h)
4770 };
4771
4772 let total_height = img_height + margin.vertical();
4773
4774 if total_height > cursor.remaining_height() {
4775 pages.push(cursor.finalize());
4776 *cursor = cursor.new_page();
4777 }
4778
4779 cursor.y += margin.top;
4780
4781 let draw = if let Some(image_data) = loaded {
4782 DrawCommand::Image { image_data }
4783 } else {
4784 DrawCommand::ImagePlaceholder
4785 };
4786
4787 cursor.elements.push(LayoutElement {
4788 x: x + margin.left,
4789 y: cursor.content_y + cursor.y,
4790 width: img_width,
4791 height: img_height,
4792 draw,
4793 children: vec![],
4794 node_type: Some(node_kind_name(&node.kind).to_string()),
4795 resolved_style: Some(style.clone()),
4796 source_location: node.source_location.clone(),
4797 href: node.href.clone(),
4798 bookmark: node.bookmark.clone(),
4799 alt: node.alt.clone(),
4800 is_header_row: false,
4801 col_span: 1,
4802 overflow: style.overflow,
4803 opacity: style.opacity,
4804 });
4805
4806 cursor.y += img_height + margin.bottom;
4807 }
4808
4809 #[allow(clippy::too_many_arguments)]
4811 fn layout_svg(
4812 &self,
4813 node: &Node,
4814 style: &ResolvedStyle,
4815 cursor: &mut PageCursor,
4816 pages: &mut Vec<LayoutPage>,
4817 x: f64,
4818 _available_width: f64,
4819 svg_width: f64,
4820 svg_height: f64,
4821 view_box: Option<&str>,
4822 content: &str,
4823 ) {
4824 let margin = &style.margin.to_edges();
4825 let total_height = svg_height + margin.vertical();
4826
4827 if total_height > cursor.remaining_height() {
4828 pages.push(cursor.finalize());
4829 *cursor = cursor.new_page();
4830 }
4831
4832 cursor.y += margin.top;
4833
4834 let vb = view_box
4835 .and_then(crate::svg::parse_view_box)
4836 .unwrap_or(crate::svg::ViewBox {
4837 min_x: 0.0,
4838 min_y: 0.0,
4839 width: svg_width,
4840 height: svg_height,
4841 });
4842
4843 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
4844
4845 cursor.elements.push(LayoutElement {
4846 x: x + margin.left,
4847 y: cursor.content_y + cursor.y,
4848 width: svg_width,
4849 height: svg_height,
4850 draw: DrawCommand::Svg {
4851 commands,
4852 width: svg_width,
4853 height: svg_height,
4854 viewbox_min_x: vb.min_x,
4855 viewbox_min_y: vb.min_y,
4856 viewbox_width: vb.width,
4857 viewbox_height: vb.height,
4858 clip: false,
4859 },
4860 children: vec![],
4861 node_type: Some("Svg".to_string()),
4862 resolved_style: Some(style.clone()),
4863 source_location: node.source_location.clone(),
4864 href: node.href.clone(),
4865 bookmark: node.bookmark.clone(),
4866 alt: node.alt.clone(),
4867 is_header_row: false,
4868 col_span: 1,
4869 overflow: style.overflow,
4870 opacity: style.opacity,
4871 });
4872
4873 cursor.y += svg_height + margin.bottom;
4874 }
4875
4876 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
4878 use crate::svg::SvgCommand;
4879
4880 let mut commands = Vec::new();
4881 let mut cur_x = 0.0_f64;
4882 let mut cur_y = 0.0_f64;
4883
4884 for op in operations {
4885 match op {
4886 CanvasOp::MoveTo { x, y } => {
4887 commands.push(SvgCommand::MoveTo(*x, *y));
4888 cur_x = *x;
4889 cur_y = *y;
4890 }
4891 CanvasOp::LineTo { x, y } => {
4892 commands.push(SvgCommand::LineTo(*x, *y));
4893 cur_x = *x;
4894 cur_y = *y;
4895 }
4896 CanvasOp::BezierCurveTo {
4897 cp1x,
4898 cp1y,
4899 cp2x,
4900 cp2y,
4901 x,
4902 y,
4903 } => {
4904 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
4905 cur_x = *x;
4906 cur_y = *y;
4907 }
4908 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
4909 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
4911 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
4912 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
4913 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
4914 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
4915 cur_x = *x;
4916 cur_y = *y;
4917 }
4918 CanvasOp::ClosePath => {
4919 commands.push(SvgCommand::ClosePath);
4920 }
4921 CanvasOp::Rect {
4922 x,
4923 y,
4924 width,
4925 height,
4926 } => {
4927 commands.push(SvgCommand::MoveTo(*x, *y));
4928 commands.push(SvgCommand::LineTo(*x + *width, *y));
4929 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
4930 commands.push(SvgCommand::LineTo(*x, *y + *height));
4931 commands.push(SvgCommand::ClosePath);
4932 cur_x = *x;
4933 cur_y = *y;
4934 }
4935 CanvasOp::Circle { cx, cy, r } => {
4936 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
4937 }
4938 CanvasOp::Ellipse { cx, cy, rx, ry } => {
4939 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
4940 }
4941 CanvasOp::Arc {
4942 cx,
4943 cy,
4944 r,
4945 start_angle,
4946 end_angle,
4947 counterclockwise,
4948 } => {
4949 let steps = 32;
4953 let mut sweep = end_angle - start_angle;
4954 if !counterclockwise && sweep < 0.0 {
4955 sweep += 2.0 * std::f64::consts::PI;
4956 }
4957 if *counterclockwise && sweep > 0.0 {
4958 sweep -= 2.0 * std::f64::consts::PI;
4959 }
4960 for i in 0..=steps {
4961 let t = *start_angle + sweep * (i as f64 / steps as f64);
4962 let px = cx + r * t.cos();
4963 let py = cy + r * t.sin();
4964 if i == 0 {
4965 commands.push(SvgCommand::MoveTo(px, py));
4966 } else {
4967 commands.push(SvgCommand::LineTo(px, py));
4968 }
4969 }
4970 }
4971 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
4972 CanvasOp::Fill => commands.push(SvgCommand::Fill),
4973 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
4974 CanvasOp::SetFillColor { r, g, b } => {
4975 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
4977 }
4978 CanvasOp::SetStrokeColor { r, g, b } => {
4979 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
4980 }
4981 CanvasOp::SetLineWidth { width } => {
4982 commands.push(SvgCommand::SetStrokeWidth(*width));
4983 }
4984 CanvasOp::SetLineCap { cap } => {
4985 commands.push(SvgCommand::SetLineCap(*cap));
4986 }
4987 CanvasOp::SetLineJoin { join } => {
4988 commands.push(SvgCommand::SetLineJoin(*join));
4989 }
4990 CanvasOp::Save => commands.push(SvgCommand::SaveState),
4991 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
4992 }
4993 }
4994
4995 commands
4996 }
4997
4998 #[allow(clippy::too_many_arguments)]
5000 fn layout_canvas(
5001 &self,
5002 node: &Node,
5003 style: &ResolvedStyle,
5004 cursor: &mut PageCursor,
5005 pages: &mut Vec<LayoutPage>,
5006 x: f64,
5007 _available_width: f64,
5008 canvas_width: f64,
5009 canvas_height: f64,
5010 operations: &[CanvasOp],
5011 ) {
5012 let margin = style.margin.to_edges();
5013 let total_height = canvas_height + margin.top + margin.bottom;
5014
5015 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
5017 pages.push(cursor.finalize());
5018 *cursor = cursor.new_page();
5019 }
5020
5021 cursor.y += margin.top;
5022
5023 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
5024
5025 cursor.elements.push(LayoutElement {
5026 x: x + margin.left,
5027 y: cursor.content_y + cursor.y,
5028 width: canvas_width,
5029 height: canvas_height,
5030 draw: DrawCommand::Svg {
5031 commands: svg_commands,
5032 width: canvas_width,
5033 height: canvas_height,
5034 viewbox_min_x: 0.0,
5037 viewbox_min_y: 0.0,
5038 viewbox_width: canvas_width,
5039 viewbox_height: canvas_height,
5040 clip: true,
5041 },
5042 children: vec![],
5043 node_type: Some("Canvas".to_string()),
5044 resolved_style: Some(style.clone()),
5045 source_location: node.source_location.clone(),
5046 href: node.href.clone(),
5047 bookmark: node.bookmark.clone(),
5048 alt: node.alt.clone(),
5049 is_header_row: false,
5050 col_span: 1,
5051 overflow: style.overflow,
5052 opacity: style.opacity,
5053 });
5054
5055 cursor.y += canvas_height + margin.bottom;
5056 }
5057
5058 #[allow(clippy::too_many_arguments)]
5060 #[allow(clippy::too_many_arguments)]
5062 fn layout_chart(
5063 &self,
5064 node: &Node,
5065 style: &ResolvedStyle,
5066 cursor: &mut PageCursor,
5067 pages: &mut Vec<LayoutPage>,
5068 x: f64,
5069 chart_width: f64,
5070 chart_height: f64,
5071 primitives: Vec<crate::chart::ChartPrimitive>,
5072 node_type_name: &str,
5073 ) {
5074 let margin = &style.margin.to_edges();
5075 let total_height = chart_height + margin.vertical();
5076
5077 if total_height > cursor.remaining_height() {
5078 pages.push(cursor.finalize());
5079 *cursor = cursor.new_page();
5080 }
5081
5082 cursor.y += margin.top;
5083
5084 let draw = DrawCommand::Chart { primitives };
5085
5086 cursor.elements.push(LayoutElement {
5087 x: x + margin.left,
5088 y: cursor.content_y + cursor.y,
5089 width: chart_width,
5090 height: chart_height,
5091 draw,
5092 children: vec![],
5093 node_type: Some(node_type_name.to_string()),
5094 resolved_style: Some(style.clone()),
5095 source_location: node.source_location.clone(),
5096 href: node.href.clone(),
5097 bookmark: node.bookmark.clone(),
5098 alt: node.alt.clone(),
5099 is_header_row: false,
5100 col_span: 1,
5101 overflow: style.overflow,
5102 opacity: style.opacity,
5103 });
5104
5105 cursor.y += chart_height + margin.bottom;
5106 }
5107
5108 #[allow(clippy::too_many_arguments)]
5110 fn layout_form_field(
5111 &self,
5112 node: &Node,
5113 style: &ResolvedStyle,
5114 cursor: &mut PageCursor,
5115 pages: &mut Vec<LayoutPage>,
5116 x: f64,
5117 field_width: f64,
5118 field_height: f64,
5119 draw: DrawCommand,
5120 node_type_name: &str,
5121 ) {
5122 let margin = &style.margin.to_edges();
5123 let total_height = field_height + margin.vertical();
5124
5125 if total_height > cursor.remaining_height() {
5126 pages.push(cursor.finalize());
5127 *cursor = cursor.new_page();
5128 }
5129
5130 cursor.y += margin.top;
5131
5132 cursor.elements.push(LayoutElement {
5133 x: x + margin.left,
5134 y: cursor.content_y + cursor.y,
5135 width: field_width,
5136 height: field_height,
5137 draw,
5138 children: vec![],
5139 node_type: Some(node_type_name.to_string()),
5140 resolved_style: Some(style.clone()),
5141 source_location: node.source_location.clone(),
5142 href: node.href.clone(),
5143 bookmark: node.bookmark.clone(),
5144 alt: node.alt.clone(),
5145 is_header_row: false,
5146 col_span: 1,
5147 overflow: style.overflow,
5148 opacity: style.opacity,
5149 });
5150
5151 cursor.y += field_height + margin.bottom;
5152 }
5153
5154 #[allow(clippy::too_many_arguments)]
5155 fn layout_barcode(
5156 &self,
5157 node: &Node,
5158 style: &ResolvedStyle,
5159 cursor: &mut PageCursor,
5160 pages: &mut Vec<LayoutPage>,
5161 x: f64,
5162 available_width: f64,
5163 data: &str,
5164 format: crate::barcode::BarcodeFormat,
5165 explicit_width: Option<f64>,
5166 bar_height: f64,
5167 ) {
5168 let margin = &style.margin.to_edges();
5169 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
5170 let total_height = bar_height + margin.vertical();
5171
5172 if total_height > cursor.remaining_height() {
5173 pages.push(cursor.finalize());
5174 *cursor = cursor.new_page();
5175 }
5176
5177 cursor.y += margin.top;
5178
5179 let draw = match crate::barcode::generate_barcode(data, format) {
5180 Ok(barcode_data) => {
5181 let bar_width = if barcode_data.bars.is_empty() {
5182 0.0
5183 } else {
5184 display_width / barcode_data.bars.len() as f64
5185 };
5186 DrawCommand::Barcode {
5187 bars: barcode_data.bars,
5188 bar_width,
5189 height: bar_height,
5190 color: style.color,
5191 }
5192 }
5193 Err(_) => DrawCommand::None,
5194 };
5195
5196 cursor.elements.push(LayoutElement {
5197 x: x + margin.left,
5198 y: cursor.content_y + cursor.y,
5199 width: display_width,
5200 height: bar_height,
5201 draw,
5202 children: vec![],
5203 node_type: Some("Barcode".to_string()),
5204 resolved_style: Some(style.clone()),
5205 source_location: node.source_location.clone(),
5206 href: node.href.clone(),
5207 bookmark: node.bookmark.clone(),
5208 alt: node.alt.clone(),
5209 is_header_row: false,
5210 col_span: 1,
5211 overflow: style.overflow,
5212 opacity: style.opacity,
5213 });
5214
5215 cursor.y += bar_height + margin.bottom;
5216 }
5217
5218 #[allow(clippy::too_many_arguments)]
5220 fn layout_qrcode(
5221 &self,
5222 node: &Node,
5223 style: &ResolvedStyle,
5224 cursor: &mut PageCursor,
5225 pages: &mut Vec<LayoutPage>,
5226 x: f64,
5227 available_width: f64,
5228 data: &str,
5229 explicit_size: Option<f64>,
5230 ) {
5231 let margin = &style.margin.to_edges();
5232 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
5233 let total_height = display_size + margin.vertical();
5234
5235 if total_height > cursor.remaining_height() {
5236 pages.push(cursor.finalize());
5237 *cursor = cursor.new_page();
5238 }
5239
5240 cursor.y += margin.top;
5241
5242 let draw = match crate::qrcode::generate_qr(data) {
5243 Ok(matrix) => {
5244 let module_size = display_size / matrix.size as f64;
5245 DrawCommand::QrCode {
5246 modules: matrix.modules,
5247 module_size,
5248 color: style.color,
5249 }
5250 }
5251 Err(_) => DrawCommand::None,
5252 };
5253
5254 cursor.elements.push(LayoutElement {
5255 x: x + margin.left,
5256 y: cursor.content_y + cursor.y,
5257 width: display_size,
5258 height: display_size,
5259 draw,
5260 children: vec![],
5261 node_type: Some("QrCode".to_string()),
5262 resolved_style: Some(style.clone()),
5263 source_location: node.source_location.clone(),
5264 href: node.href.clone(),
5265 bookmark: node.bookmark.clone(),
5266 alt: node.alt.clone(),
5267 is_header_row: false,
5268 col_span: 1,
5269 overflow: style.overflow,
5270 opacity: style.opacity,
5271 });
5272
5273 cursor.y += display_size + margin.bottom;
5274 }
5275
5276 fn measure_node_height(
5279 &self,
5280 node: &Node,
5281 available_width: f64,
5282 style: &ResolvedStyle,
5283 font_context: &FontContext,
5284 ) -> f64 {
5285 match &node.kind {
5286 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5291 let measure_width = match style.width {
5294 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
5295 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5296 };
5297 if !runs.is_empty() {
5298 let mut styled_chars: Vec<StyledChar> = Vec::new();
5300 for run in runs {
5301 let run_style = run.style.resolve(Some(style), measure_width);
5302 let run_content = substitute_page_placeholders(&run.content);
5303 for ch in run_content.chars() {
5304 styled_chars.push(StyledChar {
5305 ch,
5306 font_family: run_style.font_family.clone(),
5307 font_size: run_style.font_size,
5308 font_weight: run_style.font_weight,
5309 font_style: run_style.font_style,
5310 color: run_style.color,
5311 href: None,
5312 text_decoration: run_style.text_decoration,
5313 letter_spacing: run_style.letter_spacing,
5314 });
5315 }
5316 }
5317 let broken_lines = self.text_layout.break_runs_into_lines(
5318 font_context,
5319 &styled_chars,
5320 measure_width,
5321 style.hyphens,
5322 style.lang.as_deref(),
5323 );
5324 let line_height = style.font_size * style.line_height;
5325 (broken_lines.len() as f64) * line_height + style.padding.vertical()
5326 } else {
5327 let content = substitute_page_placeholders(content);
5328 let lines = self.text_layout.break_into_lines(
5329 font_context,
5330 &content,
5331 measure_width,
5332 style.font_size,
5333 &style.font_family,
5334 style.font_weight,
5335 style.font_style,
5336 style.letter_spacing,
5337 style.hyphens,
5338 style.lang.as_deref(),
5339 );
5340 let line_height = style.font_size * style.line_height;
5341 (lines.len() as f64) * line_height + style.padding.vertical()
5342 }
5343 }
5344 NodeKind::Image {
5345 src,
5346 width: explicit_w,
5347 height: explicit_h,
5348 } => {
5349 if let SizeConstraint::Fixed(h) = style.height {
5351 return h + style.padding.vertical();
5352 }
5353 if let Some(h) = explicit_h {
5355 return *h + style.padding.vertical();
5356 }
5357 let aspect = self
5359 .get_image_dimensions(src)
5360 .map(|(w, h)| if w > 0 { h as f64 / w as f64 } else { 0.75 })
5361 .unwrap_or(0.75);
5362 let w = if let SizeConstraint::Fixed(w) = style.width {
5363 w
5364 } else {
5365 explicit_w.unwrap_or(available_width - style.margin.horizontal())
5366 };
5367 w * aspect + style.padding.vertical()
5368 }
5369 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
5370 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
5371 NodeKind::QrCode { size, .. } => {
5372 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
5373 display_size + style.margin.vertical()
5374 }
5375 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
5376 NodeKind::BarChart { height, .. }
5377 | NodeKind::LineChart { height, .. }
5378 | NodeKind::PieChart { height, .. }
5379 | NodeKind::AreaChart { height, .. }
5380 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
5381 NodeKind::TextField { height, .. }
5382 | NodeKind::Checkbox { height, .. }
5383 | NodeKind::Dropdown { height, .. }
5384 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
5385 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
5387 if let SizeConstraint::Fixed(h) = style.height {
5394 return h;
5395 }
5396 let outer_width = match style.width {
5397 SizeConstraint::Fixed(w) => w,
5398 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5399 };
5400 let inner_width =
5401 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5402 let col_widths = self.resolve_column_widths(columns, inner_width, &node.children);
5403 let row_gap = style.row_gap;
5404 let mut total = 0.0;
5405 for (i, row) in node.children.iter().enumerate() {
5406 if i > 0 {
5407 total += row_gap;
5408 }
5409 total += self.measure_table_row_height(row, &col_widths, style, font_context);
5410 }
5411 total + style.padding.vertical() + style.border_width.vertical()
5412 }
5413 NodeKind::TableRow { .. } => {
5414 let n = node.children.len().max(1);
5419 let usable = (available_width - style.margin.horizontal()).max(0.0);
5420 let col_w = usable / n as f64;
5421 let col_widths = vec![col_w; n];
5422 self.measure_table_row_height(node, &col_widths, style, font_context)
5423 }
5424 _ => {
5425 if let SizeConstraint::Fixed(h) = style.height {
5427 return h;
5428 }
5429 let outer_width = match style.width {
5433 SizeConstraint::Fixed(w) => w,
5434 SizeConstraint::Auto => available_width - style.margin.horizontal(),
5435 }
5436 .min(style.max_width)
5437 .max(style.min_width);
5438 let inner_width =
5439 outer_width - style.padding.horizontal() - style.border_width.horizontal();
5440 let children_height =
5441 self.measure_children_height(&node.children, inner_width, style, font_context);
5442 (children_height + style.padding.vertical() + style.border_width.vertical())
5443 .max(style.min_height)
5444 }
5445 }
5446 }
5447
5448 fn measure_children_height(
5449 &self,
5450 children: &[Node],
5451 available_width: f64,
5452 parent_style: &ResolvedStyle,
5453 font_context: &FontContext,
5454 ) -> f64 {
5455 if matches!(parent_style.display, Display::Grid) {
5457 if let Some(template_cols) = &parent_style.grid_template_columns {
5458 let num_columns = template_cols.len();
5459 if num_columns > 0 && !children.is_empty() {
5460 let col_gap = parent_style.column_gap;
5461 let row_gap = parent_style.row_gap;
5462
5463 let content_sizes: Vec<f64> = template_cols
5464 .iter()
5465 .map(|track| {
5466 if matches!(track, GridTrackSize::Auto) {
5467 available_width / num_columns as f64
5468 } else {
5469 0.0
5470 }
5471 })
5472 .collect();
5473
5474 let col_widths = grid::resolve_tracks(
5475 template_cols,
5476 available_width,
5477 col_gap,
5478 &content_sizes,
5479 );
5480
5481 let placements: Vec<Option<&GridPlacement>> = children
5482 .iter()
5483 .map(|child| child.style.grid_placement.as_ref())
5484 .collect();
5485
5486 let item_placements = grid::place_items(&placements, num_columns);
5487 let num_rows = grid::compute_num_rows(&item_placements);
5488
5489 if num_rows == 0 {
5490 return 0.0;
5491 }
5492
5493 let mut row_heights = vec![0.0_f64; num_rows];
5494 for placement in &item_placements {
5495 let cell_width = grid::span_width(
5496 placement.col_start,
5497 placement.col_end,
5498 &col_widths,
5499 col_gap,
5500 );
5501 let child = &children[placement.child_index];
5502 let child_style = child.style.resolve(Some(parent_style), cell_width);
5503 let h =
5504 self.measure_node_height(child, cell_width, &child_style, font_context);
5505 let span = placement.row_end - placement.row_start;
5506 let per_row = h / span as f64;
5507 for rh in row_heights
5508 .iter_mut()
5509 .take(placement.row_end.min(num_rows))
5510 .skip(placement.row_start)
5511 {
5512 if per_row > *rh {
5513 *rh = per_row;
5514 }
5515 }
5516 }
5517
5518 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
5519 return row_heights.iter().sum::<f64>() + total_row_gap;
5520 }
5521 }
5522 }
5523
5524 let direction = parent_style.flex_direction;
5525 let row_gap = parent_style.row_gap;
5526 let column_gap = parent_style.column_gap;
5527
5528 match direction {
5529 FlexDirection::Row | FlexDirection::RowReverse => {
5530 let styles: Vec<ResolvedStyle> = children
5533 .iter()
5534 .map(|child| child.style.resolve(Some(parent_style), available_width))
5535 .collect();
5536
5537 let base_widths: Vec<f64> = children
5538 .iter()
5539 .zip(&styles)
5540 .map(|(child, style)| match style.flex_basis {
5541 SizeConstraint::Fixed(w) => w,
5542 SizeConstraint::Auto => match style.width {
5543 SizeConstraint::Fixed(w) => w,
5544 SizeConstraint::Auto => self
5545 .measure_intrinsic_width(child, style, font_context)
5546 .min(available_width),
5547 },
5548 })
5549 .collect();
5550
5551 let lines = match parent_style.flex_wrap {
5552 FlexWrap::NoWrap => {
5553 vec![flex::WrapLine {
5554 start: 0,
5555 end: children.len(),
5556 }]
5557 }
5558 FlexWrap::Wrap | FlexWrap::WrapReverse => {
5559 flex::partition_into_lines(&base_widths, column_gap, available_width)
5560 }
5561 };
5562
5563 let mut final_widths = base_widths.clone();
5565 for line in &lines {
5566 let line_count = line.end - line.start;
5567 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
5568 let distributable = available_width - line_gap;
5569 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
5570 let remaining = distributable - total_base;
5571
5572 if remaining > 0.0 {
5573 let total_grow: f64 = styles[line.start..line.end]
5574 .iter()
5575 .map(|s| s.flex_grow)
5576 .sum();
5577 if total_grow > 0.0 {
5578 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5579 final_widths[line.start + j] = base_widths[line.start + j]
5580 + remaining * (s.flex_grow / total_grow);
5581 }
5582 }
5583 } else if remaining < 0.0 {
5584 let total_shrink: f64 = styles[line.start..line.end]
5585 .iter()
5586 .enumerate()
5587 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
5588 .sum();
5589 if total_shrink > 0.0 {
5590 for (j, s) in styles[line.start..line.end].iter().enumerate() {
5591 let factor =
5592 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
5593 let w = base_widths[line.start + j] + remaining * factor;
5594 final_widths[line.start + j] = w.max(s.min_width);
5595 }
5596 }
5597 }
5598 }
5599
5600 let mut total = 0.0;
5601 for (i, line) in lines.iter().enumerate() {
5602 let line_height: f64 = children[line.start..line.end]
5603 .iter()
5604 .enumerate()
5605 .map(|(j, child)| {
5606 let fw = final_widths[line.start + j];
5607 let child_style = child.style.resolve(Some(parent_style), fw);
5608 self.measure_node_height(child, fw, &child_style, font_context)
5609 + child_style.margin.vertical()
5610 })
5611 .fold(0.0f64, f64::max);
5612 total += line_height;
5613 if i > 0 {
5614 total += row_gap;
5615 }
5616 }
5617 total
5618 }
5619 FlexDirection::Column | FlexDirection::ColumnReverse => {
5620 let mut total = 0.0;
5621 for (i, child) in children.iter().enumerate() {
5622 let child_style = child.style.resolve(Some(parent_style), available_width);
5623 let child_height = self.measure_node_height(
5624 child,
5625 available_width,
5626 &child_style,
5627 font_context,
5628 );
5629 total += child_height + child_style.margin.vertical();
5630 if i > 0 {
5631 total += row_gap;
5632 }
5633 }
5634 total
5635 }
5636 }
5637 }
5638
5639 fn measure_intrinsic_width(
5641 &self,
5642 node: &Node,
5643 style: &ResolvedStyle,
5644 font_context: &FontContext,
5645 ) -> f64 {
5646 match &node.kind {
5647 NodeKind::Svg { width, .. } => {
5648 *width + style.padding.horizontal() + style.margin.horizontal()
5649 }
5650 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5651 let text_width = if !runs.is_empty() {
5657 runs.iter()
5658 .map(|run| {
5659 let run_style = run.style.resolve(Some(style), 0.0);
5660 let run_content = substitute_page_placeholders(&run.content);
5661 let transformed =
5662 apply_text_transform(&run_content, run_style.text_transform);
5663 let italic = matches!(
5664 run_style.font_style,
5665 FontStyle::Italic | FontStyle::Oblique
5666 );
5667 transformed
5671 .split('\n')
5672 .map(|segment| {
5673 font_context.measure_string(
5674 segment,
5675 &run_style.font_family,
5676 run_style.font_weight,
5677 italic,
5678 run_style.font_size,
5679 run_style.letter_spacing,
5680 )
5681 })
5682 .fold(0.0f64, f64::max)
5683 })
5684 .sum()
5685 } else {
5686 let content = substitute_page_placeholders(content);
5687 let transformed = apply_text_transform(&content, style.text_transform);
5688 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5689 transformed
5690 .split('\n')
5691 .map(|segment| {
5692 font_context.measure_string(
5693 segment,
5694 &style.font_family,
5695 style.font_weight,
5696 italic,
5697 style.font_size,
5698 style.letter_spacing,
5699 )
5700 })
5701 .fold(0.0f64, f64::max)
5702 };
5703 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
5706 }
5707 NodeKind::Image {
5708 src, width, height, ..
5709 } => {
5710 let w = if let SizeConstraint::Fixed(w) = style.width {
5711 w
5712 } else if let Some(w) = width {
5713 *w
5714 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
5715 let pixel_w = iw as f64;
5716 let pixel_h = ih as f64;
5717 let aspect = if pixel_w > 0.0 {
5718 pixel_h / pixel_w
5719 } else {
5720 0.75
5721 };
5722 let constrained_h = match style.height {
5724 SizeConstraint::Fixed(h) => Some(h),
5725 SizeConstraint::Auto => *height,
5726 };
5727 if let Some(h) = constrained_h {
5728 h / aspect
5729 } else {
5730 pixel_w
5731 }
5732 } else {
5733 100.0
5734 };
5735 w + style.padding.horizontal() + style.margin.horizontal()
5736 }
5737 NodeKind::Barcode { width, .. } => {
5738 let w = width.unwrap_or(0.0);
5739 w + style.padding.horizontal() + style.margin.horizontal()
5740 }
5741 NodeKind::QrCode { size, .. } => {
5742 let display_size = size.unwrap_or(0.0);
5743 display_size + style.padding.horizontal() + style.margin.horizontal()
5744 }
5745 NodeKind::Canvas { width, .. } => {
5746 *width + style.padding.horizontal() + style.margin.horizontal()
5747 }
5748 NodeKind::BarChart { width, .. }
5749 | NodeKind::LineChart { width, .. }
5750 | NodeKind::PieChart { width, .. }
5751 | NodeKind::AreaChart { width, .. }
5752 | NodeKind::DotPlot { width, .. } => {
5753 *width + style.padding.horizontal() + style.margin.horizontal()
5754 }
5755 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
5756 *width + style.padding.horizontal() + style.margin.horizontal()
5757 }
5758 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
5759 *width + style.padding.horizontal() + style.margin.horizontal()
5760 }
5761 NodeKind::Watermark { .. } => 0.0, _ => {
5763 if node.children.is_empty() {
5765 style.padding.horizontal() + style.margin.horizontal()
5766 } else {
5767 let direction = style.flex_direction;
5768 let gap = style.gap;
5769 let mut total = 0.0f64;
5770 for (i, child) in node.children.iter().enumerate() {
5771 let child_style = child.style.resolve(Some(style), 0.0);
5772 let child_width =
5773 self.measure_intrinsic_width(child, &child_style, font_context);
5774 match direction {
5775 FlexDirection::Row | FlexDirection::RowReverse => {
5776 total += child_width;
5777 if i > 0 {
5778 total += gap;
5779 }
5780 }
5781 _ => {
5782 total = total.max(child_width);
5783 }
5784 }
5785 }
5786 total
5787 + style.padding.horizontal()
5788 + style.margin.horizontal()
5789 + style.border_width.horizontal()
5790 }
5791 }
5792 }
5793 }
5794
5795 pub fn measure_min_content_width(
5799 &self,
5800 node: &Node,
5801 style: &ResolvedStyle,
5802 font_context: &FontContext,
5803 ) -> f64 {
5804 match &node.kind {
5805 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
5806 let word_width = if !runs.is_empty() {
5807 runs.iter()
5809 .map(|run| {
5810 let run_style = run.style.resolve(Some(style), 0.0);
5811 let run_content = substitute_page_placeholders(&run.content);
5812 let transformed =
5813 apply_text_transform(&run_content, run_style.text_transform);
5814 self.text_layout.measure_widest_word(
5815 font_context,
5816 &transformed,
5817 run_style.font_size,
5818 &run_style.font_family,
5819 run_style.font_weight,
5820 run_style.font_style,
5821 run_style.letter_spacing,
5822 style.hyphens,
5823 style.lang.as_deref(),
5824 )
5825 })
5826 .fold(0.0f64, f64::max)
5827 } else {
5828 let content = substitute_page_placeholders(content);
5829 let transformed = apply_text_transform(&content, style.text_transform);
5830 self.text_layout.measure_widest_word(
5831 font_context,
5832 &transformed,
5833 style.font_size,
5834 &style.font_family,
5835 style.font_weight,
5836 style.font_style,
5837 style.letter_spacing,
5838 style.hyphens,
5839 style.lang.as_deref(),
5840 )
5841 };
5842 word_width + style.padding.horizontal() + style.margin.horizontal()
5843 }
5844 NodeKind::Image { width, .. } => {
5845 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
5846 }
5847 NodeKind::Svg { width, .. } => {
5848 *width + style.padding.horizontal() + style.margin.horizontal()
5849 }
5850 _ => {
5851 if node.children.is_empty() {
5852 style.padding.horizontal()
5853 + style.margin.horizontal()
5854 + style.border_width.horizontal()
5855 } else {
5856 let mut max_child_min = 0.0f64;
5857 for child in &node.children {
5858 let child_style = child.style.resolve(Some(style), 0.0);
5859 let child_min =
5860 self.measure_min_content_width(child, &child_style, font_context);
5861 max_child_min = max_child_min.max(child_min);
5862 }
5863 max_child_min
5864 + style.padding.horizontal()
5865 + style.margin.horizontal()
5866 + style.border_width.horizontal()
5867 }
5868 }
5869 }
5870 }
5871
5872 fn cell_first_line_font_size(&self, cell: &Node, cell_style: &ResolvedStyle, w: f64) -> f64 {
5878 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<f64> {
5879 for ch in &node.children {
5880 let s = ch.style.resolve(Some(parent), w);
5881 match &ch.kind {
5882 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s.font_size),
5883 _ => {
5884 if let Some(f) = first(ch, &s, w) {
5885 return Some(f);
5886 }
5887 }
5888 }
5889 }
5890 None
5891 }
5892 first(cell, cell_style, w).unwrap_or(cell_style.font_size)
5893 }
5894
5895 fn cell_baseline_distance(
5898 &self,
5899 cell: &Node,
5900 cell_style: &ResolvedStyle,
5901 inner_width: f64,
5902 ) -> f64 {
5903 cell_style.padding.top
5904 + cell_style.border_width.top
5905 + self.cell_first_line_font_size(cell, cell_style, inner_width)
5906 }
5907
5908 fn row_baseline(
5911 &self,
5912 row: &Node,
5913 row_style: &ResolvedStyle,
5914 col_widths: &[f64],
5915 ) -> Option<f64> {
5916 let mut b: Option<f64> = None;
5917 let mut col_idx = 0usize;
5918 for cell in row.children.iter() {
5919 let span = match &cell.kind {
5920 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
5921 _ => 1,
5922 };
5923 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
5924 col_idx += span;
5925 let cell_style = cell.style.resolve(Some(row_style), col_width);
5926 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
5927 let iw = col_width
5928 - cell_style.padding.horizontal()
5929 - cell_style.border_width.horizontal();
5930 let d = self.cell_baseline_distance(cell, &cell_style, iw);
5931 b = Some(b.map_or(d, |m: f64| m.max(d)));
5932 }
5933 }
5934 b
5935 }
5936
5937 fn measure_table_row_height(
5938 &self,
5939 row: &Node,
5940 col_widths: &[f64],
5941 parent_style: &ResolvedStyle,
5942 font_context: &FontContext,
5943 ) -> f64 {
5944 let row_style = row
5945 .style
5946 .resolve(Some(parent_style), col_widths.iter().sum());
5947 let mut max_height: f64 = 0.0;
5948 let row_bl = self.row_baseline(row, &row_style, col_widths);
5951
5952 let mut col_idx = 0usize;
5953 for cell in row.children.iter() {
5954 let span = match &cell.kind {
5955 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
5956 _ => 1,
5957 };
5958 let col_width: f64 = col_widths.iter().skip(col_idx).take(span).copied().sum();
5959 col_idx += span;
5960 let cell_style = cell.style.resolve(Some(&row_style), col_width);
5961 let inner_width =
5962 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
5963
5964 let mut cell_content_height = 0.0;
5965 for child in &cell.children {
5966 let child_style = child.style.resolve(Some(&cell_style), inner_width);
5967 cell_content_height +=
5968 self.measure_node_height(child, inner_width, &child_style, font_context);
5969 }
5970
5971 let mut total = cell_content_height
5972 + cell_style.padding.vertical()
5973 + cell_style.border_width.vertical();
5974 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
5978 if let Some(b) = row_bl {
5979 let d = self.cell_baseline_distance(cell, &cell_style, inner_width);
5980 total += (b - d).max(0.0);
5981 }
5982 }
5983 if let SizeConstraint::Fixed(h) = cell_style.height {
5990 total = total.max(h);
5991 }
5992 max_height = max_height.max(total);
5993 }
5994
5995 max_height.max(row_style.min_height)
5996 }
5997
5998 fn resolve_column_widths(
5999 &self,
6000 defs: &[ColumnDef],
6001 available_width: f64,
6002 children: &[Node],
6003 ) -> Vec<f64> {
6004 if defs.is_empty() {
6005 let num_cols = children.first().map(|row| row.children.len()).unwrap_or(1);
6006 return vec![available_width / num_cols as f64; num_cols];
6007 }
6008
6009 let mut widths = Vec::new();
6010 let mut remaining = available_width;
6011 let mut auto_count = 0;
6012
6013 for def in defs {
6014 match def.width {
6015 ColumnWidth::Fixed(w) => {
6016 widths.push(w);
6017 remaining -= w;
6018 }
6019 ColumnWidth::Fraction(f) => {
6020 let w = available_width * f;
6021 widths.push(w);
6022 remaining -= w;
6023 }
6024 ColumnWidth::Auto => {
6025 widths.push(0.0);
6026 auto_count += 1;
6027 }
6028 }
6029 }
6030
6031 if auto_count > 0 {
6032 let auto_width = remaining / auto_count as f64;
6033 for (i, def) in defs.iter().enumerate() {
6034 if matches!(def.width, ColumnWidth::Auto) {
6035 widths[i] = auto_width;
6036 }
6037 }
6038 }
6039
6040 widths
6041 }
6042
6043 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
6044 for (page_index, page) in pages.iter_mut().enumerate() {
6045 if !page.watermarks.is_empty() {
6047 let (page_w, page_h) = page.config.size.dimensions();
6048 let cx = page_w / 2.0;
6049 let cy = page_h / 2.0;
6050
6051 let mut watermark_elements = Vec::new();
6052 for wm_node in &page.watermarks {
6053 if let NodeKind::Watermark {
6054 text,
6055 font_size,
6056 angle,
6057 } = &wm_node.kind
6058 {
6059 let style = wm_node.style.resolve(None, page_w);
6060 let color = style.color;
6061 let opacity = style.opacity;
6062 let angle_rad = angle.to_radians();
6063
6064 let italic =
6066 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
6067
6068 let shaped = self.text_layout.shape_text(
6070 font_context,
6071 text,
6072 &style.font_family,
6073 style.font_weight,
6074 style.font_style,
6075 );
6076
6077 let mut glyphs = Vec::new();
6078 let mut x_pos = 0.0;
6079 let text_chars: Vec<char> = text.chars().collect();
6080
6081 if let Some(shaped_glyphs) = shaped {
6082 let units_per_em = font_context.units_per_em(
6084 &style.font_family,
6085 style.font_weight,
6086 italic,
6087 ) as f64;
6088
6089 for sg in &shaped_glyphs {
6090 let advance = sg.x_advance as f64 / units_per_em * *font_size;
6091 let cluster_idx = sg.cluster as usize;
6092 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
6093 glyphs.push(PositionedGlyph {
6094 glyph_id: sg.glyph_id,
6095 char_value: ch,
6096 x_offset: x_pos,
6097 y_offset: 0.0,
6098 x_advance: advance,
6099 font_size: *font_size,
6100 font_family: style.font_family.clone(),
6101 font_weight: style.font_weight,
6102 font_style: style.font_style,
6103 color: Some(color),
6104 href: None,
6105 text_decoration: TextDecoration::None,
6106 letter_spacing: style.letter_spacing,
6107 cluster_text: None,
6108 });
6109 x_pos += advance + style.letter_spacing;
6110 }
6111 } else {
6112 for &ch in &text_chars {
6114 let w = font_context.char_width(
6115 ch,
6116 &style.font_family,
6117 style.font_weight,
6118 italic,
6119 *font_size,
6120 );
6121 glyphs.push(PositionedGlyph {
6122 glyph_id: ch as u16,
6123 char_value: ch,
6124 x_offset: x_pos,
6125 y_offset: 0.0,
6126 x_advance: w,
6127 font_size: *font_size,
6128 font_family: style.font_family.clone(),
6129 font_weight: style.font_weight,
6130 font_style: style.font_style,
6131 color: Some(color),
6132 href: None,
6133 text_decoration: TextDecoration::None,
6134 letter_spacing: style.letter_spacing,
6135 cluster_text: None,
6136 });
6137 x_pos += w + style.letter_spacing;
6138 }
6139 }
6140
6141 let text_width = x_pos;
6142
6143 let line = TextLine {
6144 x: 0.0,
6145 y: 0.0,
6146 glyphs,
6147 width: text_width,
6148 height: *font_size,
6149 word_spacing: 0.0,
6150 };
6151
6152 watermark_elements.push(LayoutElement {
6153 x: cx,
6154 y: cy,
6155 width: text_width,
6156 height: *font_size,
6157 draw: DrawCommand::Watermark {
6158 lines: vec![line],
6159 color,
6160 opacity,
6161 angle_rad,
6162 font_family: style.font_family.clone(),
6163 },
6164 children: vec![],
6165 node_type: Some("Watermark".to_string()),
6166 resolved_style: None,
6167 source_location: None,
6168 href: None,
6169 bookmark: None,
6170 alt: None,
6171 is_header_row: false,
6172 col_span: 1,
6173 overflow: Overflow::default(),
6174 opacity: 1.0,
6175 });
6176 }
6177 }
6178
6179 watermark_elements.append(&mut page.elements);
6181 page.elements = watermark_elements;
6182 page.watermarks.clear();
6183 }
6184
6185 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
6186 continue;
6187 }
6188
6189 if !page.fixed_header.is_empty() {
6191 let mut hdr_cursor = PageCursor::new(&page.config);
6192 for (node, _h) in &page.fixed_header {
6193 if !PageCursor::fixed_page_filter(node).applies(page_index) {
6196 continue;
6197 }
6198 let cw = hdr_cursor.content_width;
6199 let cx = hdr_cursor.content_x;
6200 let style = node.style.resolve(None, cw);
6201 self.layout_view(
6202 node,
6203 &style,
6204 &mut hdr_cursor,
6205 &mut Vec::new(),
6206 cx,
6207 cw,
6208 font_context,
6209 );
6210 }
6211 let mut combined = hdr_cursor.elements;
6213 combined.append(&mut page.elements);
6214 page.elements = combined;
6215 }
6216
6217 if !page.fixed_footer.is_empty() {
6222 let mut ftr_cursor = PageCursor::new(&page.config);
6223 let total_ftr: f64 = page
6224 .fixed_footer
6225 .iter()
6226 .filter(|(n, _)| PageCursor::fixed_page_filter(n).applies(page_index))
6227 .map(|(_, h)| *h)
6228 .sum();
6229 let target_y = ftr_cursor.content_height - total_ftr;
6230 for (node, _h) in &page.fixed_footer {
6232 if !PageCursor::fixed_page_filter(node).applies(page_index) {
6233 continue;
6234 }
6235 let cw = ftr_cursor.content_width;
6236 let cx = ftr_cursor.content_x;
6237 let style = node.style.resolve(None, cw);
6238 self.layout_view(
6239 node,
6240 &style,
6241 &mut ftr_cursor,
6242 &mut Vec::new(),
6243 cx,
6244 cw,
6245 font_context,
6246 );
6247 }
6248 for el in &mut ftr_cursor.elements {
6252 offset_element_y(el, target_y);
6253 }
6254 page.elements.extend(ftr_cursor.elements);
6255 }
6256
6257 page.fixed_header.clear();
6259 page.fixed_footer.clear();
6260 }
6261 }
6262
6263 #[allow(clippy::too_many_arguments)]
6268 fn layout_grid_children(
6269 &self,
6270 children: &[Node],
6271 parent_style: &ResolvedStyle,
6272 cursor: &mut PageCursor,
6273 pages: &mut Vec<LayoutPage>,
6274 x: f64,
6275 available_width: f64,
6276 font_context: &FontContext,
6277 ) {
6278 let template_cols = match &parent_style.grid_template_columns {
6279 Some(cols) => cols,
6280 None => return, };
6282
6283 let num_columns = template_cols.len();
6284 if num_columns == 0 || children.is_empty() {
6285 return;
6286 }
6287
6288 let col_gap = parent_style.column_gap;
6289 let row_gap = parent_style.row_gap;
6290
6291 let content_sizes: Vec<f64> = template_cols
6294 .iter()
6295 .map(|track| {
6296 if matches!(track, GridTrackSize::Auto) {
6297 available_width / num_columns as f64
6300 } else {
6301 0.0
6302 }
6303 })
6304 .collect();
6305
6306 let col_widths =
6307 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
6308
6309 let placements: Vec<Option<&GridPlacement>> = children
6311 .iter()
6312 .map(|child| child.style.grid_placement.as_ref())
6313 .collect();
6314
6315 let item_placements = grid::place_items(&placements, num_columns);
6317 let num_rows = grid::compute_num_rows(&item_placements);
6318
6319 if num_rows == 0 {
6320 return;
6321 }
6322
6323 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
6325 for placement in &item_placements {
6326 let cell_width =
6327 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
6328 let child = &children[placement.child_index];
6329 let child_style = child.style.resolve(Some(parent_style), cell_width);
6330 item_heights[placement.child_index] =
6331 self.measure_node_height(child, cell_width, &child_style, font_context);
6332 }
6333
6334 let template_rows = parent_style.grid_template_rows.as_deref();
6336 let mut row_heights = vec![0.0_f64; num_rows];
6337 for placement in &item_placements {
6338 let h = item_heights[placement.child_index];
6339 let span = placement.row_end - placement.row_start;
6340 let per_row = h / span as f64;
6341 for rh in row_heights
6342 .iter_mut()
6343 .take(placement.row_end.min(num_rows))
6344 .skip(placement.row_start)
6345 {
6346 if per_row > *rh {
6347 *rh = per_row;
6348 }
6349 }
6350 }
6351
6352 if let Some(template) = template_rows {
6354 let auto_row = parent_style.grid_auto_rows.as_ref();
6355 for (r, rh) in row_heights.iter_mut().enumerate() {
6356 let track = template.get(r).or(auto_row);
6357 if let Some(track) = track {
6358 match track {
6359 GridTrackSize::Pt(pts) => *rh = *pts,
6360 GridTrackSize::Auto => {} _ => {} }
6363 }
6364 }
6365 }
6366
6367 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
6369 if row_height > cursor.remaining_height() {
6374 pages.push(cursor.finalize());
6375 *cursor = cursor.new_page();
6376 }
6377
6378 let row_start_y = cursor.y;
6379
6380 for placement in &item_placements {
6382 if placement.row_start != row {
6383 continue; }
6385
6386 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
6387 let cell_width =
6388 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
6389
6390 let child = &children[placement.child_index];
6391
6392 self.layout_node(
6393 child,
6394 cursor,
6395 pages,
6396 cell_x,
6397 cell_width,
6398 Some(parent_style),
6399 font_context,
6400 None,
6401 None,
6402 );
6403 cursor.y = row_start_y;
6405 }
6406
6407 cursor.y = row_start_y + row_height + row_gap;
6408 }
6409
6410 if num_rows > 0 {
6412 cursor.y -= row_gap;
6413 }
6414 }
6415}
6416
6417struct FlexItem<'a> {
6418 node: &'a Node,
6419 style: ResolvedStyle,
6420 base_width: f64,
6421 min_content_width: f64,
6422}
6423
6424#[cfg(test)]
6425mod tests {
6426 use super::*;
6427 use crate::font::FontContext;
6428
6429 fn make_text(content: &str, font_size: f64) -> Node {
6430 Node {
6431 kind: NodeKind::Text {
6432 content: content.to_string(),
6433 href: None,
6434 runs: vec![],
6435 },
6436 style: Style {
6437 font_size: Some(font_size),
6438 ..Default::default()
6439 },
6440 children: vec![],
6441 id: None,
6442 source_location: None,
6443 bookmark: None,
6444 href: None,
6445 alt: None,
6446 }
6447 }
6448
6449 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
6450 Node {
6451 kind: NodeKind::View,
6452 style,
6453 children,
6454 id: None,
6455 source_location: None,
6456 bookmark: None,
6457 href: None,
6458 alt: None,
6459 }
6460 }
6461
6462 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
6463 Node {
6464 kind: NodeKind::Text {
6465 content: String::new(),
6466 href: None,
6467 runs,
6468 },
6469 style: Style::default(),
6470 children: vec![],
6471 id: None,
6472 source_location: None,
6473 bookmark: None,
6474 href: None,
6475 alt: None,
6476 }
6477 }
6478
6479 #[test]
6480 fn intrinsic_width_measures_runs_not_just_content() {
6481 let engine = LayoutEngine::new();
6485 let font_context = FontContext::new();
6486
6487 let runs_node = make_runs_text(vec![
6488 crate::model::TextRun {
6489 content: "Hello ".to_string(),
6490 style: Style::default(),
6491 href: None,
6492 },
6493 crate::model::TextRun {
6494 content: "World".to_string(),
6495 style: Style {
6496 font_weight: Some(700),
6497 ..Default::default()
6498 },
6499 href: None,
6500 },
6501 ]);
6502 let plain_node = make_text("Hello World", 12.0);
6503
6504 let runs_style = runs_node.style.resolve(None, 0.0);
6505 let plain_style = plain_node.style.resolve(None, 0.0);
6506 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
6507 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
6508
6509 assert!(
6512 runs_w >= plain_w,
6513 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
6514 );
6515 assert!(
6516 runs_w < plain_w * 1.5,
6517 "runs width ({runs_w}) should be close to plain width ({plain_w})"
6518 );
6519 }
6520
6521 #[test]
6522 fn intrinsic_width_of_multiline_text_is_widest_line() {
6523 let engine = LayoutEngine::new();
6524 let font_context = FontContext::new();
6525
6526 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
6527 let widest = make_text("Springfield, IL 62704", 12.0);
6528
6529 let m_style = multiline.style.resolve(None, 0.0);
6530 let w_style = widest.style.resolve(None, 0.0);
6531 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
6532 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
6533
6534 assert!(
6535 (m_w - w_w).abs() < 0.01,
6536 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
6537 );
6538 }
6539
6540 #[test]
6541 fn intrinsic_width_of_heading_measures_its_text() {
6542 let engine = LayoutEngine::new();
6545 let font_context = FontContext::new();
6546
6547 let heading = Node {
6548 kind: NodeKind::Heading {
6549 level: 1,
6550 content: "Invoice #2024-001".to_string(),
6551 href: None,
6552 runs: vec![],
6553 },
6554 style: Style {
6555 font_size: Some(24.0),
6556 ..Default::default()
6557 },
6558 children: vec![],
6559 id: None,
6560 source_location: None,
6561 bookmark: None,
6562 href: None,
6563 alt: None,
6564 };
6565 let style = heading.style.resolve(None, 0.0);
6566 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
6567 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
6568 }
6569
6570 #[test]
6571 fn measure_node_height_of_wrapping_heading_matches_text() {
6572 let engine = LayoutEngine::new();
6579 let font_context = FontContext::new();
6580
6581 let content = "Annual Performance Review";
6582 let heading = Node {
6583 kind: NodeKind::Heading {
6584 level: 1,
6585 content: content.to_string(),
6586 href: None,
6587 runs: vec![],
6588 },
6589 style: Style {
6590 font_size: Some(32.0),
6591 ..Default::default()
6592 },
6593 children: vec![],
6594 id: None,
6595 source_location: None,
6596 bookmark: None,
6597 href: None,
6598 alt: None,
6599 };
6600 let text = make_text(content, 32.0);
6601
6602 let width = 200.0;
6604 let h_style = heading.style.resolve(None, width);
6605 let t_style = text.style.resolve(None, width);
6606 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
6607 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
6608
6609 assert!(
6610 h_height > 32.0,
6611 "a wrapping 32pt heading must measure more than one line, got {h_height}"
6612 );
6613 assert!(
6614 (h_height - t_height).abs() < 0.01,
6615 "heading height ({h_height}) must equal the same text's height ({t_height})"
6616 );
6617 }
6618
6619 #[test]
6620 fn intrinsic_width_flex_row_sums_children() {
6621 let engine = LayoutEngine::new();
6622 let font_context = FontContext::new();
6623
6624 let child1 = make_text("Hello", 14.0);
6625 let child2 = make_text("World", 14.0);
6626
6627 let child1_style = child1.style.resolve(None, 0.0);
6628 let child2_style = child2.style.resolve(None, 0.0);
6629 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
6630 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
6631
6632 let row = make_styled_view(
6633 Style {
6634 flex_direction: Some(FlexDirection::Row),
6635 ..Default::default()
6636 },
6637 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
6638 );
6639 let row_style = row.style.resolve(None, 0.0);
6640 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
6641
6642 assert!(
6643 (row_w - (child1_w + child2_w)).abs() < 0.01,
6644 "Row intrinsic width ({}) should equal sum of children ({} + {})",
6645 row_w,
6646 child1_w,
6647 child2_w
6648 );
6649 }
6650
6651 #[test]
6652 fn intrinsic_width_flex_column_takes_max() {
6653 let engine = LayoutEngine::new();
6654 let font_context = FontContext::new();
6655
6656 let short = make_text("Hi", 14.0);
6657 let long = make_text("Hello World", 14.0);
6658
6659 let short_style = short.style.resolve(None, 0.0);
6660 let long_style = long.style.resolve(None, 0.0);
6661 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
6662 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
6663
6664 let col = make_styled_view(
6665 Style {
6666 flex_direction: Some(FlexDirection::Column),
6667 ..Default::default()
6668 },
6669 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
6670 );
6671 let col_style = col.style.resolve(None, 0.0);
6672 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
6673
6674 assert!(
6675 (col_w - long_w).abs() < 0.01,
6676 "Column intrinsic width ({}) should equal max child ({}, short was {})",
6677 col_w,
6678 long_w,
6679 short_w
6680 );
6681 }
6682
6683 #[test]
6684 fn intrinsic_width_nested_containers() {
6685 let engine = LayoutEngine::new();
6686 let font_context = FontContext::new();
6687
6688 let inner = make_styled_view(
6689 Style {
6690 flex_direction: Some(FlexDirection::Row),
6691 ..Default::default()
6692 },
6693 vec![make_text("A", 12.0), make_text("B", 12.0)],
6694 );
6695 let inner_style = inner.style.resolve(None, 0.0);
6696 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
6697
6698 let outer = make_styled_view(
6699 Style::default(),
6700 vec![make_styled_view(
6701 Style {
6702 flex_direction: Some(FlexDirection::Row),
6703 ..Default::default()
6704 },
6705 vec![make_text("A", 12.0), make_text("B", 12.0)],
6706 )],
6707 );
6708 let outer_style = outer.style.resolve(None, 0.0);
6709 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
6710
6711 assert!(
6712 (outer_w - inner_w).abs() < 0.01,
6713 "Nested container ({}) should match inner container ({})",
6714 outer_w,
6715 inner_w
6716 );
6717 }
6718
6719 #[test]
6720 fn intrinsic_width_row_with_gap() {
6721 let engine = LayoutEngine::new();
6722 let font_context = FontContext::new();
6723
6724 let no_gap = make_styled_view(
6725 Style {
6726 flex_direction: Some(FlexDirection::Row),
6727 ..Default::default()
6728 },
6729 vec![make_text("A", 12.0), make_text("B", 12.0)],
6730 );
6731 let with_gap = make_styled_view(
6732 Style {
6733 flex_direction: Some(FlexDirection::Row),
6734 gap: Some(10.0),
6735 ..Default::default()
6736 },
6737 vec![make_text("A", 12.0), make_text("B", 12.0)],
6738 );
6739
6740 let no_gap_style = no_gap.style.resolve(None, 0.0);
6741 let with_gap_style = with_gap.style.resolve(None, 0.0);
6742 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
6743 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
6744
6745 assert!(
6746 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
6747 "Gap should add 10pt: with_gap={}, no_gap={}",
6748 with_gap_w,
6749 no_gap_w
6750 );
6751 }
6752
6753 #[test]
6754 fn intrinsic_width_empty_container() {
6755 let engine = LayoutEngine::new();
6756 let font_context = FontContext::new();
6757
6758 let padding = 8.0;
6759 let empty = make_styled_view(
6760 Style {
6761 padding: Some(Edges::uniform(padding)),
6762 ..Default::default()
6763 },
6764 vec![],
6765 );
6766 let style = empty.style.resolve(None, 0.0);
6767 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
6768
6769 assert!(
6770 (w - padding * 2.0).abs() < 0.01,
6771 "Empty container width ({}) should equal horizontal padding ({})",
6772 w,
6773 padding * 2.0
6774 );
6775 }
6776
6777 #[test]
6780 fn flex_shrink_respects_min_content_width() {
6781 let engine = LayoutEngine::new();
6785 let font_context = FontContext::new();
6786
6787 let sale_text = make_text("SALE", 12.0);
6788 let sale_style = sale_text.style.resolve(None, 0.0);
6789 let sale_word_width =
6790 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
6791 assert!(
6792 sale_word_width > 0.0,
6793 "SALE should have non-zero min-content width"
6794 );
6795
6796 let container = make_styled_view(
6799 Style {
6800 flex_direction: Some(FlexDirection::Row),
6801 width: Some(Dimension::Pt(100.0)),
6802 ..Default::default()
6803 },
6804 vec![
6805 make_styled_view(
6806 Style {
6807 width: Some(Dimension::Pt(80.0)),
6808 flex_shrink: Some(1.0),
6809 ..Default::default()
6810 },
6811 vec![],
6812 ),
6813 make_styled_view(
6814 Style {
6815 width: Some(Dimension::Pt(60.0)),
6816 flex_shrink: Some(1.0),
6817 ..Default::default()
6818 },
6819 vec![make_text("SALE", 12.0)],
6820 ),
6821 ],
6822 );
6823
6824 let doc = Document {
6825 children: vec![Node::page(
6826 PageConfig::default(),
6827 Style::default(),
6828 vec![container],
6829 )],
6830 metadata: Default::default(),
6831 default_page: PageConfig::default(),
6832 first_page: None,
6833 fonts: vec![],
6834 tagged: false,
6835 pdfa: None,
6836 default_style: None,
6837 embedded_data: None,
6838 flatten_forms: false,
6839 pdf_ua: false,
6840 certification: None,
6841 };
6842
6843 let pages = engine.layout(&doc, &font_context);
6844 assert!(!pages.is_empty());
6845
6846 let page = &pages[0];
6849 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
6851 assert!(
6852 container_el.is_some(),
6853 "Should find container with 2 children"
6854 );
6855 let sale_child = &container_el.unwrap().children[1];
6856 assert!(
6857 sale_child.width >= sale_word_width - 0.01,
6858 "SALE child width ({}) should be >= min-content width ({})",
6859 sale_child.width,
6860 sale_word_width
6861 );
6862 }
6863
6864 #[test]
6867 fn column_justify_content_center() {
6868 let engine = LayoutEngine::new();
6871 let font_context = FontContext::new();
6872
6873 let container = make_styled_view(
6874 Style {
6875 flex_direction: Some(FlexDirection::Column),
6876 height: Some(Dimension::Pt(200.0)),
6877 justify_content: Some(JustifyContent::Center),
6878 ..Default::default()
6879 },
6880 vec![make_text("Centered", 12.0)],
6881 );
6882
6883 let doc = Document {
6884 children: vec![Node::page(
6885 PageConfig::default(),
6886 Style::default(),
6887 vec![container],
6888 )],
6889 metadata: Default::default(),
6890 default_page: PageConfig::default(),
6891 first_page: None,
6892 fonts: vec![],
6893 tagged: false,
6894 pdfa: None,
6895 default_style: None,
6896 embedded_data: None,
6897 flatten_forms: false,
6898 pdf_ua: false,
6899 certification: None,
6900 };
6901
6902 let pages = engine.layout(&doc, &font_context);
6903 let page = &pages[0];
6904
6905 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
6908 assert!(
6909 container_el.is_some(),
6910 "Should find container with children"
6911 );
6912 let container_el = container_el.unwrap();
6913 let child = &container_el.children[0];
6914
6915 let child_offset = child.y - container_el.y;
6917 let expected_offset = (200.0 - child.height) / 2.0;
6918 assert!(
6919 (child_offset - expected_offset).abs() < 2.0,
6920 "Child offset ({}) should be near center ({})",
6921 child_offset,
6922 expected_offset
6923 );
6924 }
6925
6926 #[test]
6927 fn column_align_items_center() {
6928 let engine = LayoutEngine::new();
6931 let font_context = FontContext::new();
6932
6933 let container = make_styled_view(
6934 Style {
6935 flex_direction: Some(FlexDirection::Column),
6936 width: Some(Dimension::Pt(300.0)),
6937 align_items: Some(AlignItems::Center),
6938 ..Default::default()
6939 },
6940 vec![make_text("Hi", 12.0)],
6941 );
6942
6943 let doc = Document {
6944 children: vec![Node::page(
6945 PageConfig::default(),
6946 Style::default(),
6947 vec![container],
6948 )],
6949 metadata: Default::default(),
6950 default_page: PageConfig::default(),
6951 first_page: None,
6952 fonts: vec![],
6953 tagged: false,
6954 pdfa: None,
6955 default_style: None,
6956 embedded_data: None,
6957 flatten_forms: false,
6958 pdf_ua: false,
6959 certification: None,
6960 };
6961
6962 let pages = engine.layout(&doc, &font_context);
6963 let page = &pages[0];
6964
6965 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
6966 assert!(container_el.is_some());
6967 let container_el = container_el.unwrap();
6968 let child = &container_el.children[0];
6969
6970 let child_center = child.x + child.width / 2.0;
6972 let container_center = container_el.x + container_el.width / 2.0;
6973 assert!(
6974 (child_center - container_center).abs() < 2.0,
6975 "Child center ({}) should be near container center ({})",
6976 child_center,
6977 container_center
6978 );
6979 }
6980
6981 #[test]
6984 fn absolute_child_positioned_relative_to_parent() {
6985 let engine = LayoutEngine::new();
6989 let font_context = FontContext::new();
6990
6991 let parent = make_styled_view(
6992 Style {
6993 position: Some(crate::model::Position::Relative),
6994 margin: Some(MarginEdges::from_edges(Edges {
6995 top: 50.0,
6996 left: 50.0,
6997 ..Default::default()
6998 })),
6999 width: Some(Dimension::Pt(200.0)),
7000 height: Some(Dimension::Pt(200.0)),
7001 ..Default::default()
7002 },
7003 vec![make_styled_view(
7004 Style {
7005 position: Some(crate::model::Position::Absolute),
7006 top: Some(10.0),
7007 left: Some(10.0),
7008 width: Some(Dimension::Pt(50.0)),
7009 height: Some(Dimension::Pt(50.0)),
7010 ..Default::default()
7011 },
7012 vec![],
7013 )],
7014 );
7015
7016 let doc = Document {
7017 children: vec![Node::page(
7018 PageConfig::default(),
7019 Style::default(),
7020 vec![parent],
7021 )],
7022 metadata: Default::default(),
7023 default_page: PageConfig::default(),
7024 first_page: None,
7025 fonts: vec![],
7026 tagged: false,
7027 pdfa: None,
7028 default_style: None,
7029 embedded_data: None,
7030 flatten_forms: false,
7031 pdf_ua: false,
7032 certification: None,
7033 };
7034
7035 let pages = engine.layout(&doc, &font_context);
7036 let page = &pages[0];
7037
7038 let parent_el = page
7041 .elements
7042 .iter()
7043 .find(|e| e.width > 190.0 && e.width < 210.0);
7044 assert!(parent_el.is_some(), "Should find the 200x200 parent");
7045 let parent_el = parent_el.unwrap();
7046
7047 let abs_child = parent_el
7049 .children
7050 .iter()
7051 .find(|e| e.width > 45.0 && e.width < 55.0);
7052 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
7053 let abs_child = abs_child.unwrap();
7054
7055 let expected_x = parent_el.x + 10.0;
7056 let expected_y = parent_el.y + 10.0;
7057 assert!(
7058 (abs_child.x - expected_x).abs() < 1.0,
7059 "Absolute child x ({}) should be parent.x + 10 ({})",
7060 abs_child.x,
7061 expected_x
7062 );
7063 assert!(
7064 (abs_child.y - expected_y).abs() < 1.0,
7065 "Absolute child y ({}) should be parent.y + 10 ({})",
7066 abs_child.y,
7067 expected_y
7068 );
7069 }
7070
7071 #[test]
7072 fn absolute_escapes_unpositioned_parent_to_page() {
7073 let engine = LayoutEngine::new();
7078 let font_context = FontContext::new();
7079 let parent = make_styled_view(
7080 Style {
7081 margin: Some(MarginEdges::from_edges(Edges {
7082 top: 50.0,
7083 left: 50.0,
7084 ..Default::default()
7085 })),
7086 width: Some(Dimension::Pt(200.0)),
7087 height: Some(Dimension::Pt(200.0)),
7088 ..Default::default()
7089 },
7090 vec![make_styled_view(
7091 Style {
7092 position: Some(crate::model::Position::Absolute),
7093 top: Some(10.0),
7094 left: Some(10.0),
7095 width: Some(Dimension::Pt(50.0)),
7096 height: Some(Dimension::Pt(50.0)),
7097 ..Default::default()
7098 },
7099 vec![],
7100 )],
7101 );
7102 let doc = Document {
7103 children: vec![Node::page(
7104 PageConfig::default(),
7105 Style::default(),
7106 vec![parent],
7107 )],
7108 metadata: Default::default(),
7109 default_page: PageConfig::default(),
7110 first_page: None,
7111 fonts: vec![],
7112 tagged: false,
7113 pdfa: None,
7114 default_style: None,
7115 embedded_data: None,
7116 flatten_forms: false,
7117 pdf_ua: false,
7118 certification: None,
7119 };
7120 let pages = engine.layout(&doc, &font_context);
7121 let page = &pages[0];
7122 let parent_el = page
7123 .elements
7124 .iter()
7125 .find(|e| e.width > 190.0 && e.width < 210.0)
7126 .expect("parent");
7127 let abs_child = parent_el
7128 .children
7129 .iter()
7130 .find(|e| e.width > 45.0 && e.width < 55.0)
7131 .expect("abs child");
7132 let page_left = PageConfig::default().margin.left;
7133 let page_top = PageConfig::default().margin.top;
7134 assert!(
7135 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
7136 "absolute escapes to the page: x {} should be page_left + 10 ({})",
7137 abs_child.x,
7138 page_left + 10.0
7139 );
7140 assert!(
7141 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
7142 "absolute escapes to the page: y {} should be page_top + 10 ({})",
7143 abs_child.y,
7144 page_top + 10.0
7145 );
7146 assert!(
7147 abs_child.x < parent_el.x,
7148 "child must no longer be parent-relative (parent is 50pt further in)"
7149 );
7150 }
7151
7152 #[test]
7153 fn text_transform_none_passthrough() {
7154 assert_eq!(
7155 apply_text_transform("Hello World", TextTransform::None),
7156 "Hello World"
7157 );
7158 }
7159
7160 #[test]
7161 fn text_transform_uppercase() {
7162 assert_eq!(
7163 apply_text_transform("hello world", TextTransform::Uppercase),
7164 "HELLO WORLD"
7165 );
7166 }
7167
7168 #[test]
7169 fn text_transform_lowercase() {
7170 assert_eq!(
7171 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
7172 "hello world"
7173 );
7174 }
7175
7176 #[test]
7177 fn text_transform_capitalize() {
7178 assert_eq!(
7179 apply_text_transform("hello world", TextTransform::Capitalize),
7180 "Hello World"
7181 );
7182 assert_eq!(
7183 apply_text_transform(" hello world ", TextTransform::Capitalize),
7184 " Hello World "
7185 );
7186 assert_eq!(
7187 apply_text_transform("already Capitalized", TextTransform::Capitalize),
7188 "Already Capitalized"
7189 );
7190 }
7191
7192 #[test]
7193 fn text_transform_capitalize_empty() {
7194 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
7195 }
7196
7197 #[test]
7198 fn apply_char_transform_uppercase() {
7199 assert_eq!(
7200 apply_char_transform('a', TextTransform::Uppercase, false),
7201 'A'
7202 );
7203 assert_eq!(
7204 apply_char_transform('A', TextTransform::Uppercase, false),
7205 'A'
7206 );
7207 }
7208
7209 #[test]
7210 fn apply_char_transform_capitalize_word_start() {
7211 assert_eq!(
7212 apply_char_transform('h', TextTransform::Capitalize, true),
7213 'H'
7214 );
7215 assert_eq!(
7216 apply_char_transform('h', TextTransform::Capitalize, false),
7217 'h'
7218 );
7219 }
7220
7221 #[test]
7224 fn column_flex_grow_single_child_fills_container() {
7225 let engine = LayoutEngine::new();
7228 let font_context = FontContext::new();
7229
7230 let child = make_styled_view(
7231 Style {
7232 flex_grow: Some(1.0),
7233 ..Default::default()
7234 },
7235 vec![make_text("Short", 12.0)],
7236 );
7237
7238 let container = make_styled_view(
7239 Style {
7240 flex_direction: Some(FlexDirection::Column),
7241 height: Some(Dimension::Pt(300.0)),
7242 ..Default::default()
7243 },
7244 vec![child],
7245 );
7246
7247 let doc = Document {
7248 children: vec![Node::page(
7249 PageConfig::default(),
7250 Style::default(),
7251 vec![container],
7252 )],
7253 metadata: Default::default(),
7254 default_page: PageConfig::default(),
7255 first_page: None,
7256 fonts: vec![],
7257 tagged: false,
7258 pdfa: None,
7259 default_style: None,
7260 embedded_data: None,
7261 flatten_forms: false,
7262 pdf_ua: false,
7263 certification: None,
7264 };
7265
7266 let pages = engine.layout(&doc, &font_context);
7267 let page = &pages[0];
7268
7269 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
7270 assert!(container_el.is_some());
7271 let container_el = container_el.unwrap();
7272 assert!(
7273 (container_el.height - 300.0).abs() < 1.0,
7274 "Container should be 300pt, got {}",
7275 container_el.height
7276 );
7277
7278 let child_el = &container_el.children[0];
7279 assert!(
7280 (child_el.height - 300.0).abs() < 1.0,
7281 "flex-grow child should expand to 300pt, got {}",
7282 child_el.height
7283 );
7284 }
7285
7286 #[test]
7287 fn column_flex_grow_two_children_proportional() {
7288 let engine = LayoutEngine::new();
7291 let font_context = FontContext::new();
7292
7293 let child1 = make_styled_view(
7294 Style {
7295 flex_grow: Some(1.0),
7296 ..Default::default()
7297 },
7298 vec![make_text("A", 12.0)],
7299 );
7300 let child2 = make_styled_view(
7301 Style {
7302 flex_grow: Some(2.0),
7303 ..Default::default()
7304 },
7305 vec![make_text("B", 12.0)],
7306 );
7307
7308 let container = make_styled_view(
7309 Style {
7310 flex_direction: Some(FlexDirection::Column),
7311 height: Some(Dimension::Pt(300.0)),
7312 ..Default::default()
7313 },
7314 vec![child1, child2],
7315 );
7316
7317 let doc = Document {
7318 children: vec![Node::page(
7319 PageConfig::default(),
7320 Style::default(),
7321 vec![container],
7322 )],
7323 metadata: Default::default(),
7324 default_page: PageConfig::default(),
7325 first_page: None,
7326 fonts: vec![],
7327 tagged: false,
7328 pdfa: None,
7329 default_style: None,
7330 embedded_data: None,
7331 flatten_forms: false,
7332 pdf_ua: false,
7333 certification: None,
7334 };
7335
7336 let pages = engine.layout(&doc, &font_context);
7337 let page = &pages[0];
7338
7339 let container_el = page
7340 .elements
7341 .iter()
7342 .find(|e| e.children.len() == 2)
7343 .expect("Should find container with two children");
7344
7345 let c1 = &container_el.children[0];
7346 let c2 = &container_el.children[1];
7347
7348 let total = c1.height + c2.height;
7352 assert!(
7353 (total - 300.0).abs() < 2.0,
7354 "Children should sum to ~300pt, got {}",
7355 total
7356 );
7357
7358 let ratio = c2.height / c1.height;
7361 assert!(
7362 ratio > 1.3 && ratio < 2.5,
7363 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
7364 ratio
7365 );
7366 }
7367
7368 #[test]
7369 fn column_flex_grow_mixed_grow_and_fixed() {
7370 let engine = LayoutEngine::new();
7373 let font_context = FontContext::new();
7374
7375 let fixed_child = make_styled_view(
7376 Style {
7377 height: Some(Dimension::Pt(50.0)),
7378 ..Default::default()
7379 },
7380 vec![make_text("Fixed", 12.0)],
7381 );
7382 let grow_child = make_styled_view(
7383 Style {
7384 flex_grow: Some(1.0),
7385 ..Default::default()
7386 },
7387 vec![make_text("Grow", 12.0)],
7388 );
7389
7390 let container = make_styled_view(
7391 Style {
7392 flex_direction: Some(FlexDirection::Column),
7393 height: Some(Dimension::Pt(300.0)),
7394 ..Default::default()
7395 },
7396 vec![fixed_child, grow_child],
7397 );
7398
7399 let doc = Document {
7400 children: vec![Node::page(
7401 PageConfig::default(),
7402 Style::default(),
7403 vec![container],
7404 )],
7405 metadata: Default::default(),
7406 default_page: PageConfig::default(),
7407 first_page: None,
7408 fonts: vec![],
7409 tagged: false,
7410 pdfa: None,
7411 default_style: None,
7412 embedded_data: None,
7413 flatten_forms: false,
7414 pdf_ua: false,
7415 certification: None,
7416 };
7417
7418 let pages = engine.layout(&doc, &font_context);
7419 let page = &pages[0];
7420
7421 let container_el = page
7422 .elements
7423 .iter()
7424 .find(|e| e.children.len() == 2)
7425 .expect("Should find container with two children");
7426
7427 let fixed_el = &container_el.children[0];
7428 let grow_el = &container_el.children[1];
7429
7430 assert!(
7432 (fixed_el.height - 50.0).abs() < 1.0,
7433 "Fixed child should stay at 50pt, got {}",
7434 fixed_el.height
7435 );
7436
7437 assert!(
7439 (grow_el.height - 250.0).abs() < 2.0,
7440 "Grow child should expand to ~250pt, got {}",
7441 grow_el.height
7442 );
7443 }
7444
7445 #[test]
7446 fn column_flex_grow_page_level() {
7447 let engine = LayoutEngine::new();
7449 let font_context = FontContext::new();
7450
7451 let grow_child = make_styled_view(
7452 Style {
7453 flex_grow: Some(1.0),
7454 ..Default::default()
7455 },
7456 vec![make_text("Fill page", 12.0)],
7457 );
7458
7459 let doc = Document {
7460 children: vec![Node::page(
7461 PageConfig::default(),
7462 Style::default(),
7463 vec![grow_child],
7464 )],
7465 metadata: Default::default(),
7466 default_page: PageConfig::default(),
7467 first_page: None,
7468 fonts: vec![],
7469 tagged: false,
7470 pdfa: None,
7471 default_style: None,
7472 embedded_data: None,
7473 flatten_forms: false,
7474 pdf_ua: false,
7475 certification: None,
7476 };
7477
7478 let pages = engine.layout(&doc, &font_context);
7479 let page = &pages[0];
7480
7481 assert!(
7483 !page.elements.is_empty(),
7484 "Page should have at least one element"
7485 );
7486
7487 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
7488 let el = &page.elements[0];
7489 assert!(
7490 (el.height - content_height).abs() < 2.0,
7491 "Page-level flex-grow child should fill content height ({}), got {}",
7492 content_height,
7493 el.height
7494 );
7495 }
7496
7497 #[test]
7498 fn column_flex_grow_with_justify_content() {
7499 let engine = LayoutEngine::new();
7503 let font_context = FontContext::new();
7504
7505 let fixed_child = make_styled_view(
7506 Style {
7507 height: Some(Dimension::Pt(50.0)),
7508 ..Default::default()
7509 },
7510 vec![make_text("Top", 12.0)],
7511 );
7512 let grow_child = make_styled_view(
7513 Style {
7514 flex_grow: Some(1.0),
7515 ..Default::default()
7516 },
7517 vec![make_text("Fill", 12.0)],
7518 );
7519
7520 let container = make_styled_view(
7521 Style {
7522 flex_direction: Some(FlexDirection::Column),
7523 height: Some(Dimension::Pt(300.0)),
7524 justify_content: Some(JustifyContent::Center),
7525 ..Default::default()
7526 },
7527 vec![fixed_child, grow_child],
7528 );
7529
7530 let doc = Document {
7531 children: vec![Node::page(
7532 PageConfig::default(),
7533 Style::default(),
7534 vec![container],
7535 )],
7536 metadata: Default::default(),
7537 default_page: PageConfig::default(),
7538 first_page: None,
7539 fonts: vec![],
7540 tagged: false,
7541 pdfa: None,
7542 default_style: None,
7543 embedded_data: None,
7544 flatten_forms: false,
7545 pdf_ua: false,
7546 certification: None,
7547 };
7548
7549 let pages = engine.layout(&doc, &font_context);
7550 let page = &pages[0];
7551
7552 let container_el = page
7553 .elements
7554 .iter()
7555 .find(|e| e.children.len() == 2)
7556 .expect("Should find container");
7557
7558 let first_child = &container_el.children[0];
7561 assert!(
7562 (first_child.y - container_el.y).abs() < 1.0,
7563 "First child should be at top of container"
7564 );
7565
7566 let total = container_el.children[0].height + container_el.children[1].height;
7568 assert!(
7569 (total - 300.0).abs() < 2.0,
7570 "Children should fill container, got {}",
7571 total
7572 );
7573 }
7574
7575 #[test]
7576 fn column_flex_grow_child_justify_content_center() {
7577 let engine = LayoutEngine::new();
7580 let font_context = FontContext::new();
7581
7582 let inner_box = make_styled_view(
7584 Style {
7585 height: Some(Dimension::Pt(40.0)),
7586 ..Default::default()
7587 },
7588 vec![make_text("Centered", 12.0)],
7589 );
7590
7591 let grow_child = make_styled_view(
7593 Style {
7594 flex_grow: Some(1.0),
7595 flex_direction: Some(FlexDirection::Column),
7596 justify_content: Some(JustifyContent::Center),
7597 ..Default::default()
7598 },
7599 vec![inner_box],
7600 );
7601
7602 let container = make_styled_view(
7604 Style {
7605 flex_direction: Some(FlexDirection::Column),
7606 height: Some(Dimension::Pt(400.0)),
7607 ..Default::default()
7608 },
7609 vec![grow_child],
7610 );
7611
7612 let doc = Document {
7613 children: vec![Node::page(
7614 PageConfig::default(),
7615 Style::default(),
7616 vec![container],
7617 )],
7618 metadata: Default::default(),
7619 default_page: PageConfig::default(),
7620 first_page: None,
7621 fonts: vec![],
7622 tagged: false,
7623 pdfa: None,
7624 default_style: None,
7625 embedded_data: None,
7626 flatten_forms: false,
7627 pdf_ua: false,
7628 certification: None,
7629 };
7630
7631 let pages = engine.layout(&doc, &font_context);
7632 let page = &pages[0];
7633
7634 let container_el = page
7636 .elements
7637 .iter()
7638 .find(|e| e.height > 350.0 && e.children.len() == 1)
7639 .expect("Should find outer container");
7640
7641 let grow_el = &container_el.children[0];
7642 assert!(
7643 (grow_el.height - 400.0).abs() < 2.0,
7644 "Grow child should expand to 400, got {}",
7645 grow_el.height
7646 );
7647
7648 let inner_el = &grow_el.children[0];
7650 let expected_center = grow_el.y + grow_el.height / 2.0;
7651 let actual_center = inner_el.y + inner_el.height / 2.0;
7652 assert!(
7653 (actual_center - expected_center).abs() < 2.0,
7654 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
7655 expected_center,
7656 actual_center
7657 );
7658 }
7659
7660 #[test]
7661 fn column_flex_grow_child_justify_content_flex_end() {
7662 let engine = LayoutEngine::new();
7664 let font_context = FontContext::new();
7665
7666 let inner_box = make_styled_view(
7667 Style {
7668 height: Some(Dimension::Pt(30.0)),
7669 ..Default::default()
7670 },
7671 vec![make_text("Bottom", 12.0)],
7672 );
7673
7674 let grow_child = make_styled_view(
7675 Style {
7676 flex_grow: Some(1.0),
7677 flex_direction: Some(FlexDirection::Column),
7678 justify_content: Some(JustifyContent::FlexEnd),
7679 ..Default::default()
7680 },
7681 vec![inner_box],
7682 );
7683
7684 let container = make_styled_view(
7685 Style {
7686 flex_direction: Some(FlexDirection::Column),
7687 height: Some(Dimension::Pt(300.0)),
7688 ..Default::default()
7689 },
7690 vec![grow_child],
7691 );
7692
7693 let doc = Document {
7694 children: vec![Node::page(
7695 PageConfig::default(),
7696 Style::default(),
7697 vec![container],
7698 )],
7699 metadata: Default::default(),
7700 default_page: PageConfig::default(),
7701 first_page: None,
7702 fonts: vec![],
7703 tagged: false,
7704 pdfa: None,
7705 default_style: None,
7706 embedded_data: None,
7707 flatten_forms: false,
7708 pdf_ua: false,
7709 certification: None,
7710 };
7711
7712 let pages = engine.layout(&doc, &font_context);
7713 let page = &pages[0];
7714
7715 let container_el = page
7716 .elements
7717 .iter()
7718 .find(|e| e.height > 250.0 && e.children.len() == 1)
7719 .expect("Should find outer container");
7720
7721 let grow_el = &container_el.children[0];
7722 let inner_el = &grow_el.children[0];
7723
7724 let inner_bottom = inner_el.y + inner_el.height;
7726 let grow_bottom = grow_el.y + grow_el.height;
7727 assert!(
7728 (inner_bottom - grow_bottom).abs() < 2.0,
7729 "Inner box bottom ({}) should align with grow child bottom ({})",
7730 inner_bottom,
7731 grow_bottom
7732 );
7733 }
7734
7735 #[test]
7736 fn column_flex_grow_child_no_justify_unchanged() {
7737 let engine = LayoutEngine::new();
7739 let font_context = FontContext::new();
7740
7741 let inner_box = make_styled_view(
7742 Style {
7743 height: Some(Dimension::Pt(50.0)),
7744 ..Default::default()
7745 },
7746 vec![make_text("Top", 12.0)],
7747 );
7748
7749 let grow_child = make_styled_view(
7750 Style {
7751 flex_grow: Some(1.0),
7752 flex_direction: Some(FlexDirection::Column),
7753 ..Default::default()
7755 },
7756 vec![inner_box],
7757 );
7758
7759 let container = make_styled_view(
7760 Style {
7761 flex_direction: Some(FlexDirection::Column),
7762 height: Some(Dimension::Pt(300.0)),
7763 ..Default::default()
7764 },
7765 vec![grow_child],
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 fonts: vec![],
7778 tagged: false,
7779 pdfa: None,
7780 default_style: None,
7781 embedded_data: None,
7782 flatten_forms: false,
7783 pdf_ua: false,
7784 certification: None,
7785 };
7786
7787 let pages = engine.layout(&doc, &font_context);
7788 let page = &pages[0];
7789
7790 let container_el = page
7791 .elements
7792 .iter()
7793 .find(|e| e.height > 250.0 && e.children.len() == 1)
7794 .expect("Should find outer container");
7795
7796 let grow_el = &container_el.children[0];
7797 let inner_el = &grow_el.children[0];
7798
7799 assert!(
7801 (inner_el.y - grow_el.y).abs() < 2.0,
7802 "Inner box ({}) should be at top of grow child ({})",
7803 inner_el.y,
7804 grow_el.y
7805 );
7806 }
7807
7808 #[test]
7809 fn column_flex_grow_child_align_items_center() {
7810 let engine = LayoutEngine::new();
7812 let font_context = FontContext::new();
7813
7814 let text = make_text("Hello", 12.0);
7815
7816 let grow_child = make_styled_view(
7817 Style {
7818 flex_grow: Some(1.0),
7819 flex_direction: Some(FlexDirection::Column),
7820 align_items: Some(AlignItems::Center),
7821 ..Default::default()
7822 },
7823 vec![text],
7824 );
7825
7826 let container = make_styled_view(
7827 Style {
7828 flex_direction: Some(FlexDirection::Column),
7829 height: Some(Dimension::Pt(300.0)),
7830 ..Default::default()
7831 },
7832 vec![grow_child],
7833 );
7834
7835 let doc = Document {
7836 children: vec![Node::page(
7837 PageConfig::default(),
7838 Style::default(),
7839 vec![container],
7840 )],
7841 metadata: Default::default(),
7842 default_page: PageConfig::default(),
7843 first_page: None,
7844 fonts: vec![],
7845 tagged: false,
7846 pdfa: None,
7847 default_style: None,
7848 embedded_data: None,
7849 flatten_forms: false,
7850 pdf_ua: false,
7851 certification: None,
7852 };
7853
7854 let pages = engine.layout(&doc, &font_context);
7855 let page = &pages[0];
7856
7857 let container_el = page
7858 .elements
7859 .iter()
7860 .find(|e| e.height > 250.0 && e.children.len() == 1)
7861 .expect("Should find outer container");
7862
7863 let grow_el = &container_el.children[0];
7864 assert!(
7865 !grow_el.children.is_empty(),
7866 "Grow child should have text child"
7867 );
7868
7869 let text_el = &grow_el.children[0];
7870 let text_center = text_el.x + text_el.width / 2.0;
7871 let grow_center = grow_el.x + grow_el.width / 2.0;
7872 assert!(
7873 (text_center - grow_center).abs() < 2.0,
7874 "Text center ({}) should be near grow child center ({})",
7875 text_center,
7876 grow_center
7877 );
7878 }
7879
7880 #[test]
7881 fn image_intrinsic_width_respects_height_constraint() {
7882 let engine = LayoutEngine::new();
7886 let font_context = FontContext::new();
7887
7888 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
7890
7891 let image_node = Node {
7892 kind: NodeKind::Image {
7893 src: one_px_png.to_string(),
7894 width: None,
7895 height: Some(36.0),
7896 },
7897 style: Style::default(),
7898 children: vec![],
7899 id: None,
7900 source_location: None,
7901 bookmark: None,
7902 href: None,
7903 alt: None,
7904 };
7905
7906 let resolved = image_node.style.resolve(None, 0.0);
7907 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
7908
7909 assert!(
7911 (intrinsic - 36.0).abs() < 1.0,
7912 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
7913 intrinsic
7914 );
7915 }
7916}