1pub mod audit;
35pub mod flex;
36pub mod grid;
37pub mod page_break;
38
39use std::cell::RefCell;
40use std::collections::HashMap;
41use std::sync::Arc;
42
43use serde::Serialize;
44
45use crate::font::FontContext;
46use crate::model::*;
47use crate::style::*;
48use crate::text::bidi;
49use crate::text::shaping;
50use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
51
52#[derive(Debug, Clone, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct BookmarkEntry {
56 pub title: String,
57 pub page_index: usize,
58 pub y: f64,
59}
60
61#[derive(Debug, Clone, Serialize)]
65#[serde(rename_all = "camelCase")]
66pub struct LayoutInfo {
67 pub pages: Vec<PageInfo>,
68}
69
70#[derive(Debug, Clone, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct PageInfo {
74 pub width: f64,
75 pub height: f64,
76 pub content_x: f64,
77 pub content_y: f64,
78 pub content_width: f64,
79 pub content_height: f64,
80 pub elements: Vec<ElementInfo>,
81}
82
83#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct ElementStyleInfo {
87 pub margin: Edges,
89 pub padding: Edges,
90 pub border_width: Edges,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub width: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub height: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_width: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub min_height: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_width: Option<f64>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub max_height: Option<f64>,
103 pub flex_direction: FlexDirection,
105 pub justify_content: JustifyContent,
106 pub align_items: AlignItems,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub align_self: Option<AlignItems>,
109 pub flex_wrap: FlexWrap,
110 pub align_content: AlignContent,
111 pub flex_grow: f64,
112 pub flex_shrink: f64,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub flex_basis: Option<String>,
115 pub gap: f64,
116 pub row_gap: f64,
117 pub column_gap: f64,
118 pub font_family: String,
120 pub font_size: f64,
121 pub font_weight: u32,
122 pub font_style: FontStyle,
123 pub line_height: f64,
124 pub text_align: TextAlign,
125 pub letter_spacing: f64,
126 pub word_spacing: f64,
127 pub text_decoration: TextDecoration,
128 pub text_transform: TextTransform,
129 pub color: Color,
131 pub background_color: Option<Color>,
132 pub border_color: EdgeValues<Color>,
133 pub border_style: EdgeValues<crate::style::BorderStyle>,
134 pub border_radius: CornerValues,
135 pub opacity: f64,
136 pub position: Position,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub top: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub right: Option<f64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub bottom: Option<f64>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub left: Option<f64>,
146 pub overflow: Overflow,
148 pub breakable: bool,
150 pub break_before: bool,
151 pub min_widow_lines: u32,
152 pub min_orphan_lines: u32,
153}
154
155fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
156 match sc {
157 SizeConstraint::Auto => None,
158 SizeConstraint::Fixed(v) => Some(format!("{v}")),
159 }
160}
161
162impl ElementStyleInfo {
163 fn from_resolved(style: &ResolvedStyle) -> Self {
164 ElementStyleInfo {
165 margin: style.margin.to_edges(),
166 padding: style.padding,
167 border_width: style.border_width,
168 width: size_constraint_to_str(&style.width),
169 height: size_constraint_to_str(&style.height),
170 min_width: if style.min_width > 0.0 {
171 Some(style.min_width)
172 } else {
173 None
174 },
175 min_height: if style.min_height > 0.0 {
176 Some(style.min_height)
177 } else {
178 None
179 },
180 max_width: if style.max_width.is_finite() {
181 Some(style.max_width)
182 } else {
183 None
184 },
185 max_height: if style.max_height.is_finite() {
186 Some(style.max_height)
187 } else {
188 None
189 },
190 flex_direction: style.flex_direction,
191 justify_content: style.justify_content,
192 align_items: style.align_items,
193 align_self: style.align_self,
194 flex_wrap: style.flex_wrap,
195 align_content: style.align_content,
196 flex_grow: style.flex_grow,
197 flex_shrink: style.flex_shrink,
198 flex_basis: size_constraint_to_str(&style.flex_basis),
199 gap: style.gap,
200 row_gap: style.row_gap,
201 column_gap: style.column_gap,
202 font_family: style.font_family.clone(),
203 font_size: style.font_size,
204 font_weight: style.font_weight,
205 font_style: style.font_style,
206 line_height: style.line_height,
207 text_align: style.text_align,
208 letter_spacing: style.letter_spacing,
209 word_spacing: style.word_spacing,
210 text_decoration: style.text_decoration,
211 text_transform: style.text_transform,
212 color: style.color,
213 background_color: style.background_color,
214 border_color: style.border_color,
215 border_style: style.border_style,
216 border_radius: style.border_radius,
217 opacity: style.opacity,
218 position: style.position,
219 top: style.top,
220 right: style.right,
221 bottom: style.bottom,
222 left: style.left,
223 overflow: style.overflow,
224 breakable: style.breakable,
225 break_before: style.break_before,
226 min_widow_lines: style.min_widow_lines,
227 min_orphan_lines: style.min_orphan_lines,
228 }
229 }
230}
231
232impl Default for ElementStyleInfo {
233 fn default() -> Self {
234 ElementStyleInfo {
235 margin: Edges::default(),
236 padding: Edges::default(),
237 border_width: Edges::default(),
238 width: None,
239 height: None,
240 min_width: None,
241 min_height: None,
242 max_width: None,
243 max_height: None,
244 flex_direction: FlexDirection::default(),
245 justify_content: JustifyContent::default(),
246 align_items: AlignItems::default(),
247 align_self: None,
248 flex_wrap: FlexWrap::default(),
249 align_content: AlignContent::default(),
250 flex_grow: 0.0,
251 flex_shrink: 1.0,
252 flex_basis: None,
253 gap: 0.0,
254 row_gap: 0.0,
255 column_gap: 0.0,
256 font_family: "Helvetica".to_string(),
257 font_size: 12.0,
258 font_weight: 400,
259 font_style: FontStyle::default(),
260 line_height: 1.4,
261 text_align: TextAlign::default(),
262 letter_spacing: 0.0,
263 word_spacing: 0.0,
264 text_decoration: TextDecoration::None,
265 text_transform: TextTransform::None,
266 color: Color::BLACK,
267 background_color: None,
268 border_color: EdgeValues::uniform(Color::BLACK),
269 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
270 border_radius: CornerValues::uniform(0.0),
271 opacity: 1.0,
272 position: Position::default(),
273 top: None,
274 right: None,
275 bottom: None,
276 left: None,
277 overflow: Overflow::default(),
278 breakable: false,
279 break_before: false,
280 min_widow_lines: 2,
281 min_orphan_lines: 2,
282 }
283 }
284}
285
286#[derive(Debug, Clone, Serialize)]
288#[serde(rename_all = "camelCase")]
289pub struct ElementInfo {
290 pub x: f64,
291 pub y: f64,
292 pub width: f64,
293 pub height: f64,
294 pub kind: String,
296 pub node_type: String,
298 pub style: ElementStyleInfo,
300 pub children: Vec<ElementInfo>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub source_location: Option<SourceLocation>,
305 #[serde(skip_serializing_if = "Option::is_none")]
307 pub text_content: Option<String>,
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub href: Option<String>,
311 #[serde(skip_serializing_if = "Option::is_none")]
313 pub bookmark: Option<String>,
314}
315
316impl LayoutInfo {
317 pub fn from_pages(pages: &[LayoutPage]) -> Self {
319 LayoutInfo {
320 pages: pages
321 .iter()
322 .map(|page| {
323 let (page_w, page_h) = page.config.size.dimensions();
324 let content_x = page.config.margin.left;
325 let content_y = page.config.margin.top;
326 let content_width = page_w - page.config.margin.horizontal();
327 let content_height = page_h - page.config.margin.vertical();
328
329 let elements = Self::build_element_tree(&page.elements);
330
331 PageInfo {
332 width: page_w,
333 height: page_h,
334 content_x,
335 content_y,
336 content_width,
337 content_height,
338 elements,
339 }
340 })
341 .collect(),
342 }
343 }
344
345 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
346 elems
347 .iter()
348 .map(|elem| {
349 let kind = match &elem.draw {
350 DrawCommand::None => "None",
351 DrawCommand::Rect { .. } => "Rect",
352 DrawCommand::Text { .. } => "Text",
353 DrawCommand::Image { .. } => "Image",
354 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
355 DrawCommand::Svg { .. } => "Svg",
356 DrawCommand::Barcode { .. } => "Barcode",
357 DrawCommand::QrCode { .. } => "QrCode",
358 DrawCommand::Chart { .. } => "Chart",
359 DrawCommand::Watermark { .. } => "Watermark",
360 DrawCommand::FormField { .. } => "FormField",
361 };
362 let text_content = match &elem.draw {
363 DrawCommand::Text { lines, .. } => {
364 let text: String = lines
365 .iter()
366 .flat_map(|line| {
367 line.glyphs.iter().flat_map(|g| {
368 g.cluster_text.as_deref().unwrap_or("").chars().chain(
370 if g.cluster_text.is_none() {
371 Some(g.char_value)
372 } else {
373 None
374 },
375 )
376 })
377 })
378 .collect();
379 if text.is_empty() {
380 None
381 } else {
382 Some(text)
383 }
384 }
385 _ => None,
386 };
387 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
388 let style = elem
389 .resolved_style
390 .as_ref()
391 .map(ElementStyleInfo::from_resolved)
392 .unwrap_or_default();
393 ElementInfo {
394 x: elem.x,
395 y: elem.y,
396 width: elem.width,
397 height: elem.height,
398 kind: kind.to_string(),
399 node_type,
400 style,
401 children: Self::build_element_tree(&elem.children),
402 source_location: elem.source_location.clone(),
403 text_content,
404 href: elem.href.clone(),
405 bookmark: elem.bookmark.clone(),
406 }
407 })
408 .collect()
409 }
410}
411
412pub(crate) type FixedEntry = (Node, f64, Option<ResolvedStyle>);
422
423pub(crate) type WatermarkEntry = (Node, Option<ResolvedStyle>);
426
427#[derive(Debug, Clone)]
429pub struct LayoutPage {
430 pub width: f64,
431 pub height: f64,
432 pub elements: Vec<LayoutElement>,
433 pub(crate) fixed_header: Vec<FixedEntry>,
435 pub(crate) fixed_footer: Vec<FixedEntry>,
437 pub(crate) watermarks: Vec<WatermarkEntry>,
439 pub(crate) config: PageConfig,
441 pub(crate) page_name: Option<String>,
443}
444
445#[derive(Debug, Clone)]
447pub struct LayoutElement {
448 pub x: f64,
450 pub y: f64,
451 pub width: f64,
453 pub height: f64,
454 pub draw: DrawCommand,
456 pub children: Vec<LayoutElement>,
458 pub node_type: Option<String>,
460 pub resolved_style: Option<ResolvedStyle>,
462 pub source_location: Option<SourceLocation>,
464 pub href: Option<String>,
466 pub bookmark: Option<String>,
468 pub alt: Option<String>,
470 pub is_header_row: bool,
472 pub list_numbering: Option<&'static str>,
477 pub actual_text: Option<String>,
482 pub col_span: u32,
485 pub overflow: Overflow,
487 pub opacity: f64,
492}
493
494fn fixed_applies_on(node: &Node, page_index: usize, page_name: Option<&str>) -> bool {
497 match &node.kind {
498 NodeKind::Fixed {
499 pages,
500 page_name: only,
501 exclude_page_names,
502 ..
503 } => {
504 pages.applies(page_index)
505 && match only {
506 Some(n) => page_name == Some(n.as_str()),
507 None => true,
508 }
509 && !exclude_page_names
510 .iter()
511 .any(|n| page_name == Some(n.as_str()))
512 }
513 _ => true,
514 }
515}
516
517fn node_kind_name(kind: &NodeKind) -> &'static str {
519 match kind {
520 NodeKind::View => "View",
521 NodeKind::Text { .. } => "Text",
522 NodeKind::Heading { level: 1, .. } => "H1",
523 NodeKind::Heading { level: 2, .. } => "H2",
524 NodeKind::Heading { level: 3, .. } => "H3",
525 NodeKind::Heading { level: 4, .. } => "H4",
526 NodeKind::Heading { level: 5, .. } => "H5",
527 NodeKind::Heading { .. } => "H6",
531 NodeKind::List { .. } => "List",
532 NodeKind::ListItem => "ListItem",
533 NodeKind::Image { .. } => "Image",
534 NodeKind::Table { .. } => "Table",
535 NodeKind::TableRow { .. } => "TableRow",
536 NodeKind::TableCell { .. } => "TableCell",
537 NodeKind::Fixed {
538 position: FixedPosition::Header,
539 ..
540 } => "FixedHeader",
541 NodeKind::Fixed {
542 position: FixedPosition::Footer,
543 ..
544 } => "FixedFooter",
545 NodeKind::Page { .. } => "Page",
546 NodeKind::PageBreak => "PageBreak",
547 NodeKind::PageName { .. } => "PageName",
548 NodeKind::Svg { .. } => "Svg",
549 NodeKind::Canvas { .. } => "Canvas",
550 NodeKind::Barcode { .. } => "Barcode",
551 NodeKind::QrCode { .. } => "QrCode",
552 NodeKind::BarChart { .. } => "BarChart",
553 NodeKind::LineChart { .. } => "LineChart",
554 NodeKind::PieChart { .. } => "PieChart",
555 NodeKind::AreaChart { .. } => "AreaChart",
556 NodeKind::DotPlot { .. } => "DotPlot",
557 NodeKind::Watermark { .. } => "Watermark",
558 NodeKind::TextField { .. } => "TextField",
559 NodeKind::Checkbox { .. } => "Checkbox",
560 NodeKind::Dropdown { .. } => "Dropdown",
561 NodeKind::RadioButton { .. } => "RadioButton",
562 }
563}
564
565fn bookmark_marker(node: &Node, x: f64, y: f64) -> Option<LayoutElement> {
584 node.bookmark.as_ref().map(|title| LayoutElement {
585 x,
586 y,
587 width: 0.0,
588 height: 0.0,
589 draw: DrawCommand::None,
590 children: vec![],
591 node_type: Some("Bookmark".to_string()),
592 resolved_style: None,
593 source_location: None,
594 href: None,
595 bookmark: Some(title.clone()),
596 alt: None,
597 is_header_row: false,
598 actual_text: None,
599 list_numbering: None,
600 col_span: 1,
601 overflow: Overflow::default(),
602 opacity: 1.0,
603 })
604}
605
606fn format_marker(idx: u32, ordered: bool, marker_type: ListMarkerType) -> String {
613 if !ordered {
614 return match marker_type {
618 ListMarkerType::None => String::new(),
619 _ => "•".to_string(),
620 };
621 }
622 let body = match marker_type {
623 ListMarkerType::LowerAlpha => to_alpha(idx, false),
624 ListMarkerType::UpperAlpha => to_alpha(idx, true),
625 ListMarkerType::LowerRoman => to_roman(idx, false),
626 ListMarkerType::UpperRoman => to_roman(idx, true),
627 ListMarkerType::None => return String::new(),
628 _ => idx.to_string(),
631 };
632 format!("{body}.")
633}
634
635fn to_alpha(mut n: u32, upper: bool) -> String {
638 if n == 0 {
639 return String::new();
640 }
641 let base = if upper { b'A' } else { b'a' };
642 let mut bytes: Vec<u8> = Vec::new();
643 while n > 0 {
644 let rem = ((n - 1) % 26) as u8;
645 bytes.push(base + rem);
646 n = (n - 1) / 26;
647 }
648 bytes.reverse();
649 String::from_utf8(bytes).unwrap_or_default()
650}
651
652fn to_roman(n: u32, upper: bool) -> String {
656 if n == 0 || n > 3999 {
657 return n.to_string();
658 }
659 const UPPER: &[(&str, u32)] = &[
660 ("M", 1000),
661 ("CM", 900),
662 ("D", 500),
663 ("CD", 400),
664 ("C", 100),
665 ("XC", 90),
666 ("L", 50),
667 ("XL", 40),
668 ("X", 10),
669 ("IX", 9),
670 ("V", 5),
671 ("IV", 4),
672 ("I", 1),
673 ];
674 const LOWER: &[(&str, u32)] = &[
675 ("m", 1000),
676 ("cm", 900),
677 ("d", 500),
678 ("cd", 400),
679 ("c", 100),
680 ("xc", 90),
681 ("l", 50),
682 ("xl", 40),
683 ("x", 10),
684 ("ix", 9),
685 ("v", 5),
686 ("iv", 4),
687 ("i", 1),
688 ];
689 let table = if upper { UPPER } else { LOWER };
690 let mut out = String::new();
691 let mut remaining = n;
692 for &(sym, val) in table {
693 while remaining >= val {
694 out.push_str(sym);
695 remaining -= val;
696 }
697 }
698 out
699}
700
701fn compute_marker_gutter_width(
708 ordered: bool,
709 marker_type: ListMarkerType,
710 start: u32,
711 n_items: u32,
712 style: &ResolvedStyle,
713) -> f64 {
714 if matches!(marker_type, ListMarkerType::None) {
715 return 0.0;
716 }
717 let approx_char_w = style.font_size * 0.6;
719 let gap = 6.0_f64;
720 if !ordered {
721 return approx_char_w + gap;
723 }
724 let last_idx = start + n_items.saturating_sub(1);
726 let widest = match marker_type {
727 ListMarkerType::Decimal => format_marker(last_idx, true, ListMarkerType::Decimal),
728 ListMarkerType::LowerAlpha => format_marker(last_idx, true, ListMarkerType::LowerAlpha),
729 ListMarkerType::UpperAlpha => format_marker(last_idx, true, ListMarkerType::UpperAlpha),
730 ListMarkerType::LowerRoman => format_marker(last_idx, true, ListMarkerType::LowerRoman),
731 ListMarkerType::UpperRoman => format_marker(last_idx, true, ListMarkerType::UpperRoman),
732 _ => format_marker(last_idx, true, ListMarkerType::Decimal),
733 };
734 widest.chars().count() as f64 * approx_char_w + gap
735}
736
737#[derive(Debug, Clone)]
739pub enum FormFieldType {
740 TextField {
741 value: Option<String>,
742 placeholder: Option<String>,
743 multiline: bool,
744 password: bool,
745 read_only: bool,
746 max_length: Option<u32>,
747 font_size: f64,
748 },
749 Checkbox {
750 checked: bool,
751 read_only: bool,
752 },
753 Dropdown {
754 options: Vec<String>,
755 value: Option<String>,
756 read_only: bool,
757 font_size: f64,
758 },
759 RadioButton {
760 value: String,
761 checked: bool,
762 read_only: bool,
763 },
764}
765
766#[derive(Debug, Clone)]
768pub enum DrawCommand {
769 None,
771 Rect {
773 background: Option<Color>,
774 border_width: Edges,
775 border_color: EdgeValues<Color>,
776 border_style: EdgeValues<crate::style::BorderStyle>,
777 border_radius: CornerValues,
778 opacity: f64,
779 box_shadow: Option<Box<crate::style::BoxShadow>>,
782 background_gradient: Option<Box<crate::style::Background>>,
786 },
787 Text {
789 lines: Vec<TextLine>,
790 color: Color,
791 text_decoration: TextDecoration,
792 opacity: f64,
793 },
794 Image {
796 image_data: crate::image_loader::LoadedImage,
797 },
798 ImagePlaceholder,
800 Svg {
802 commands: Vec<crate::svg::SvgCommand>,
803 width: f64,
805 height: f64,
807 viewbox_min_x: f64,
811 viewbox_min_y: f64,
812 viewbox_width: f64,
813 viewbox_height: f64,
814 clip: bool,
816 },
817 Barcode {
819 bars: Vec<u8>,
820 bar_width: f64,
821 height: f64,
822 color: Color,
823 },
824 QrCode {
826 modules: Vec<Vec<bool>>,
827 module_size: f64,
828 color: Color,
829 },
830 Chart {
832 primitives: Vec<crate::chart::ChartPrimitive>,
833 },
834 Watermark {
836 lines: Vec<TextLine>,
837 color: Color,
838 opacity: f64,
839 angle_rad: f64,
840 font_family: String,
842 },
843 FormField {
845 field_type: FormFieldType,
846 name: String,
847 },
848}
849
850#[derive(Debug, Clone)]
851pub struct TextLine {
852 pub x: f64,
853 pub y: f64,
854 pub glyphs: Vec<PositionedGlyph>,
855 pub width: f64,
856 pub height: f64,
857 pub word_spacing: f64,
859}
860
861#[derive(Debug, Clone)]
862pub struct PositionedGlyph {
863 pub glyph_id: u16,
866 pub x_offset: f64,
868 pub y_offset: f64,
870 pub x_advance: f64,
872 pub font_size: f64,
873 pub font_family: Arc<str>,
879 pub font_weight: u32,
880 pub font_style: FontStyle,
881 pub char_value: char,
883 pub color: Option<Color>,
885 pub href: Option<String>,
887 pub text_decoration: TextDecoration,
889 pub letter_spacing: f64,
891 pub cluster_text: Option<String>,
894 pub extraction_text: Option<String>,
896 pub ligature: bool,
907}
908
909fn shaped_glyph_x(
916 char_positions: &[f64],
917 cluster: usize,
918 pen: f64,
919 cluster_pen: f64,
920 sg: &shaping::ShapedGlyph,
921 scale: f64,
922) -> f64 {
923 let base = char_positions
924 .get(cluster)
925 .map_or(pen, |&p| p + (pen - cluster_pen));
926 base + sg.x_offset as f64 * scale
927}
928
929type ClusterText = (Option<String>, bool, Option<String>);
945
946fn cluster_texts(shaped: &[shaping::ShapedGlyph], chars: &[char]) -> Vec<ClusterText> {
947 let num_chars = chars.len();
948 let fewer_glyphs_than_chars = shaped.len() < num_chars;
949 let mut starts: Vec<u32> = shaped.iter().map(|g| g.cluster).collect();
950 starts.sort_unstable();
951 let mut ordinals = HashMap::<u32, usize>::new();
952 let mut extractions = HashMap::<u32, Vec<String>>::new();
953 shaped
954 .iter()
955 .map(|sg| {
956 let first = starts.partition_point(|&c| c < sg.cluster);
957 let past = starts.partition_point(|&c| c <= sg.cluster);
958 let glyphs_in_cluster = past - first;
959 let start = (sg.cluster as usize).min(num_chars);
960 let end = starts
961 .get(past)
962 .map_or(num_chars, |&c| c as usize)
963 .min(num_chars);
964 let ordinal = ordinals.entry(sg.cluster).or_default();
965 let extraction = if glyphs_in_cluster > 1 || end > start + 1 {
966 let texts = extractions.entry(sg.cluster).or_insert_with(|| {
967 let mut chunks: Vec<Vec<char>> = chars[start..end]
970 .split_inclusive(|&ch| ch != '\u{200D}' && ch != '\u{200C}')
971 .map(|chunk| chunk.to_vec())
972 .collect();
973 if chunks.len() > glyphs_in_cluster {
974 let mut grouped: Vec<Vec<char>> = Vec::new();
978 for chunk in chunks {
979 if chunk.first().is_some_and(|&ch| {
980 unicode_bidi::bidi_class(ch) == unicode_bidi::BidiClass::NSM
981 }) && !grouped.is_empty()
982 {
983 grouped.last_mut().unwrap().extend(chunk);
984 } else {
985 grouped.push(chunk);
986 }
987 }
988 chunks = grouped;
989 }
990 let rtl = chars.get(start).is_some_and(|&ch| {
991 matches!(
992 unicode_bidi::bidi_class(ch),
993 unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL
994 )
995 });
996 (0..glyphs_in_cluster)
997 .map(|i| {
998 let text: String = if i >= chunks.len() {
999 "\u{200B}".to_string()
1002 } else if i + 1 == glyphs_in_cluster {
1003 chunks[i..].iter().flat_map(|chunk| chunk.iter()).collect()
1004 } else {
1005 chunks[i].iter().collect()
1006 };
1007 if rtl {
1008 text.chars().rev().collect()
1009 } else {
1010 text
1011 }
1012 })
1013 .collect()
1014 });
1015 Some(texts[*ordinal].clone())
1016 } else {
1017 None
1018 };
1019 *ordinal += 1;
1020 let ligature = end > start + 1 && glyphs_in_cluster == 1;
1021 let cluster_text = if end > start + 1 && (ligature || fewer_glyphs_than_chars) {
1022 Some(chars[start..end].iter().collect())
1023 } else {
1024 None
1025 };
1026 (cluster_text, ligature, extraction)
1027 })
1028 .collect()
1029}
1030
1031fn offset_element_y(el: &mut LayoutElement, dy: f64) {
1034 el.y += dy;
1035 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
1036 for line in lines.iter_mut() {
1037 line.y += dy;
1038 }
1039 }
1040 for child in &mut el.children {
1041 offset_element_y(child, dy);
1042 }
1043}
1044
1045fn offset_element_x(el: &mut LayoutElement, dx: f64) {
1047 el.x += dx;
1048 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
1049 for line in lines.iter_mut() {
1050 line.x += dx;
1051 }
1052 }
1053 for child in &mut el.children {
1054 offset_element_x(child, dx);
1055 }
1056}
1057
1058fn reapply_justify_content(elem: &mut LayoutElement) {
1062 let style = match elem.resolved_style {
1063 Some(ref s) => s,
1064 None => return,
1065 };
1066 if matches!(style.justify_content, JustifyContent::FlexStart) {
1067 return;
1068 }
1069 if elem.children.is_empty() {
1070 return;
1071 }
1072
1073 let padding_top = style.padding.top + style.border_width.top;
1074 let padding_bottom = style.padding.bottom + style.border_width.bottom;
1075 let inner_h = elem.height - padding_top - padding_bottom;
1076 let content_top = elem.y + padding_top;
1077
1078 let last_child = &elem.children[elem.children.len() - 1];
1080 let children_bottom = last_child.y + last_child.height;
1081 let children_span = children_bottom - content_top;
1082 let slack = inner_h - children_span;
1083 if slack < 0.001 {
1084 return;
1085 }
1086
1087 let n = elem.children.len();
1088 let offsets: Vec<f64> = match style.justify_content {
1089 JustifyContent::FlexEnd => vec![slack; n],
1090 JustifyContent::Center => vec![slack / 2.0; n],
1091 JustifyContent::SpaceBetween => {
1092 if n <= 1 {
1093 vec![0.0; n]
1094 } else {
1095 let per_gap = slack / (n - 1) as f64;
1096 (0..n).map(|i| i as f64 * per_gap).collect()
1097 }
1098 }
1099 JustifyContent::SpaceAround => {
1100 let space = slack / n as f64;
1101 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
1102 }
1103 JustifyContent::SpaceEvenly => {
1104 let space = slack / (n + 1) as f64;
1105 (0..n).map(|i| (i + 1) as f64 * space).collect()
1106 }
1107 JustifyContent::FlexStart => unreachable!(),
1108 };
1109
1110 for (i, child) in elem.children.iter_mut().enumerate() {
1111 let dy = offsets[i];
1112 if dy.abs() > 0.001 {
1113 offset_element_y(child, dy);
1114 }
1115 }
1116}
1117
1118fn apply_text_transform(text: &str, transform: TextTransform) -> String {
1120 match transform {
1121 TextTransform::None => text.to_string(),
1122 TextTransform::Uppercase => text.to_uppercase(),
1123 TextTransform::Lowercase => text.to_lowercase(),
1124 TextTransform::Capitalize => {
1125 let mut result = String::with_capacity(text.len());
1126 let mut prev_is_whitespace = true;
1127 for ch in text.chars() {
1128 if prev_is_whitespace && ch.is_alphabetic() {
1129 for upper in ch.to_uppercase() {
1130 result.push(upper);
1131 }
1132 } else {
1133 result.push(ch);
1134 }
1135 prev_is_whitespace = ch.is_whitespace();
1136 }
1137 result
1138 }
1139 }
1140}
1141
1142pub const PAGE_NUMBER_SENTINEL: char = '\x02';
1146
1147pub const TOTAL_PAGES_SENTINEL: char = '\x03';
1149
1150fn substitute_page_placeholders(text: &str) -> String {
1155 if text.contains("{{pageNumber}}") || text.contains("{{totalPages}}") {
1156 text.replace("{{pageNumber}}", &PAGE_NUMBER_SENTINEL.to_string())
1157 .replace("{{totalPages}}", &TOTAL_PAGES_SENTINEL.to_string())
1158 } else {
1159 text.to_string()
1160 }
1161}
1162
1163fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
1166 match transform {
1167 TextTransform::None => ch,
1168 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
1169 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
1170 TextTransform::Capitalize => {
1171 if is_word_start && ch.is_alphabetic() {
1172 ch.to_uppercase().next().unwrap_or(ch)
1173 } else {
1174 ch
1175 }
1176 }
1177 }
1178}
1179
1180pub struct LayoutEngine {
1182 text_layout: TextLayout,
1183 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
1184 warnings: RefCell<Vec<String>>,
1196 measure_check: bool,
1210}
1211
1212#[derive(Debug, Clone)]
1214struct PageCursor {
1215 config: PageConfig,
1216 base_config: PageConfig,
1219 page_index: usize,
1224 content_width: f64,
1225 content_height: f64,
1226 y: f64,
1227 elements: Vec<LayoutElement>,
1228 fixed_header: Vec<FixedEntry>,
1229 fixed_footer: Vec<FixedEntry>,
1230 watermarks: Vec<WatermarkEntry>,
1232 content_x: f64,
1233 content_y: f64,
1234 continuation_top_offset: f64,
1236 containing_block: (f64, f64, f64, f64),
1241 left_config: Option<PageConfig>,
1249 right_config: Option<PageConfig>,
1250 display_config: Option<PageConfig>,
1253 flow_anchor_left: f64,
1258 page_name: Option<String>,
1262 base_display: Option<PageConfig>,
1266 first_display: Option<PageConfig>,
1270 doc_base: PageConfig,
1273 doc_left: Option<PageConfig>,
1274 doc_right: Option<PageConfig>,
1275 named_sets: std::collections::HashMap<String, crate::model::NamedPageSet>,
1277}
1278
1279impl PageCursor {
1280 fn new(config: &PageConfig) -> Self {
1281 let (page_w, page_h) = config.size.dimensions();
1282 let content_width = page_w - config.margin.horizontal();
1283 let content_height = page_h - config.margin.vertical();
1284
1285 Self {
1286 config: config.clone(),
1287 base_config: config.clone(),
1288 page_index: 0,
1289 content_width,
1290 content_height,
1291 y: 0.0,
1292 elements: Vec::new(),
1293 fixed_header: Vec::new(),
1294 fixed_footer: Vec::new(),
1295 watermarks: Vec::new(),
1296 content_x: config.margin.left,
1297 content_y: config.margin.top,
1298 continuation_top_offset: 0.0,
1299 containing_block: (
1300 config.margin.left,
1301 config.margin.top,
1302 content_width,
1303 content_height,
1304 ),
1305 left_config: None,
1306 right_config: None,
1307 display_config: None,
1308 flow_anchor_left: config.margin.left,
1309 page_name: None,
1310 base_display: None,
1311 first_display: None,
1312 doc_base: config.clone(),
1313 doc_left: None,
1314 doc_right: None,
1315 named_sets: std::collections::HashMap::new(),
1316 }
1317 }
1318
1319 fn display_for(&self, page_number: usize) -> Option<PageConfig> {
1326 if page_number == 1 {
1327 if let Some(first) = &self.first_display {
1328 return Some(first.clone());
1329 }
1330 }
1331 let parity = if page_number % 2 == 1 {
1332 self.right_config.clone()
1333 } else {
1334 self.left_config.clone()
1335 };
1336 parity.or_else(|| self.base_display.clone())
1337 }
1338
1339 fn fixed_applies(&self, node: &Node) -> bool {
1342 fixed_applies_on(node, self.page_index, self.page_name.as_deref())
1343 }
1344
1345 fn renamed_page(&self, name: Option<String>, in_place: bool) -> Self {
1352 let set = name
1353 .as_deref()
1354 .and_then(|n| self.named_sets.get(n))
1355 .cloned();
1356 let real = set
1357 .as_ref()
1358 .map(|s| s.base.clone())
1359 .unwrap_or_else(|| self.doc_base.clone());
1360 let mut cursor = PageCursor::new(&real);
1361 cursor.page_index = if in_place {
1362 self.page_index
1363 } else {
1364 self.page_index + 1
1365 };
1366 cursor.page_name = name;
1367 cursor.doc_base = self.doc_base.clone();
1368 cursor.doc_left = self.doc_left.clone();
1369 cursor.doc_right = self.doc_right.clone();
1370 cursor.named_sets = self.named_sets.clone();
1371 match &set {
1372 Some(s) => {
1373 cursor.left_config = s.display_left.clone();
1374 cursor.right_config = s.display_right.clone();
1375 cursor.base_display = s.display.clone();
1376 cursor.first_display = s.display_first.clone();
1377 }
1378 None => {
1379 cursor.left_config = self.doc_left.clone();
1383 cursor.right_config = self.doc_right.clone();
1384 }
1385 }
1386 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1387 cursor.flow_anchor_left = self.flow_anchor_left;
1388 cursor.fixed_header = self.fixed_header.clone();
1389 cursor.fixed_footer = self.fixed_footer.clone();
1390 cursor.watermarks = self.watermarks.clone();
1391 cursor.continuation_top_offset = self.continuation_top_offset;
1392
1393 let header_height: f64 = cursor
1394 .fixed_header
1395 .iter()
1396 .filter(|(n, _, _)| cursor.fixed_applies(n))
1397 .map(|(_, h, _)| *h)
1398 .sum();
1399 cursor.y = header_height + cursor.continuation_top_offset;
1400 cursor
1401 }
1402
1403 fn new_first(first: &PageConfig, base: &PageConfig) -> Self {
1406 let mut cursor = PageCursor::new(first);
1407 cursor.base_config = base.clone();
1408 cursor
1409 }
1410
1411 fn remaining_height(&self) -> f64 {
1412 let footer_height: f64 = self
1413 .fixed_footer
1414 .iter()
1415 .filter(|(n, _, _)| self.fixed_applies(n))
1416 .map(|(_, h, _)| *h)
1417 .sum();
1418 (self.content_height - self.y - footer_height).max(0.0)
1419 }
1420
1421 fn finalize(&self) -> LayoutPage {
1422 let (page_w, page_h) = self.config.size.dimensions();
1423
1424 let (elements, config) = match &self.display_config {
1430 Some(display) => {
1431 let dx = display.margin.left - self.flow_anchor_left;
1432 let mut elements = self.elements.clone();
1433 if dx != 0.0 {
1434 fn shift(els: &mut [LayoutElement], dx: f64) {
1435 for el in els {
1436 el.x += dx;
1437 shift(&mut el.children, dx);
1438 }
1439 }
1440 shift(&mut elements, dx);
1441 }
1442 (elements, display.clone())
1443 }
1444 None => (self.elements.clone(), self.config.clone()),
1445 };
1446
1447 LayoutPage {
1448 width: page_w,
1449 height: page_h,
1450 elements,
1451 fixed_header: self.fixed_header.clone(),
1452 fixed_footer: self.fixed_footer.clone(),
1453 watermarks: self.watermarks.clone(),
1454 config,
1455 page_name: self.page_name.clone(),
1456 }
1457 }
1458
1459 fn new_page(&self) -> Self {
1460 let mut cursor = PageCursor::new(&self.base_config);
1463 cursor.page_index = self.page_index + 1;
1464 cursor.page_name = self.page_name.clone();
1465 cursor.left_config = self.left_config.clone();
1466 cursor.right_config = self.right_config.clone();
1467 cursor.base_display = self.base_display.clone();
1468 cursor.first_display = self.first_display.clone();
1469 cursor.doc_base = self.doc_base.clone();
1470 cursor.doc_left = self.doc_left.clone();
1471 cursor.doc_right = self.doc_right.clone();
1472 cursor.named_sets = self.named_sets.clone();
1473 cursor.display_config = cursor.display_for(cursor.page_index + 1);
1474 cursor.flow_anchor_left = self.flow_anchor_left;
1475 cursor.fixed_header = self.fixed_header.clone();
1476 cursor.fixed_footer = self.fixed_footer.clone();
1477 cursor.watermarks = self.watermarks.clone();
1478 cursor.continuation_top_offset = self.continuation_top_offset;
1479
1480 let header_height: f64 = cursor
1481 .fixed_header
1482 .iter()
1483 .filter(|(n, _, _)| cursor.fixed_applies(n))
1484 .map(|(_, h, _)| *h)
1485 .sum();
1486 cursor.y = header_height + cursor.continuation_top_offset;
1487
1488 cursor
1489 }
1490}
1491
1492impl Default for LayoutEngine {
1493 fn default() -> Self {
1494 Self::new()
1495 }
1496}
1497
1498impl LayoutEngine {
1499 pub fn new() -> Self {
1500 Self {
1501 text_layout: TextLayout::new(),
1502 image_dim_cache: RefCell::new(HashMap::new()),
1503 warnings: RefCell::new(Vec::new()),
1504 measure_check: std::env::var("FORME_MEASURE_CHECK").is_ok_and(|v| v == "1"),
1505 }
1506 }
1507
1508 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
1510 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
1511 return Some(*dims);
1512 }
1513 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
1514 self.image_dim_cache
1515 .borrow_mut()
1516 .insert(src.to_string(), dims);
1517 Some(dims)
1518 } else {
1519 None
1520 }
1521 }
1522
1523 fn defect(&self, msg: String) {
1526 let mut w = self.warnings.borrow_mut();
1527 if !w.contains(&msg) {
1530 w.push(msg);
1531 }
1532 }
1533
1534 pub fn take_warnings(&self) -> Vec<String> {
1538 std::mem::take(&mut *self.warnings.borrow_mut())
1539 }
1540
1541 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
1543 self.warnings.borrow_mut().clear();
1544 let mut pages: Vec<LayoutPage> = Vec::new();
1545 let mut cursor = match &document.first_page {
1546 Some(first) => PageCursor::new_first(first, &document.default_page),
1547 None => PageCursor::new(&document.default_page),
1548 };
1549 cursor.left_config = document.left_page.clone();
1555 cursor.right_config = document.right_page.clone();
1556 cursor.doc_base = document.default_page.clone();
1558 cursor.doc_left = document.left_page.clone();
1559 cursor.doc_right = document.right_page.clone();
1560 cursor.named_sets = document.named_pages.clone();
1561 if document.first_page.is_none() {
1562 cursor.display_config = cursor.display_for(1);
1563 }
1564
1565 let base = document.default_style.clone().unwrap_or_default();
1567 let root_style = Style {
1568 lang: base.lang.clone().or(document.metadata.lang.clone()),
1569 ..base
1570 }
1571 .resolve(None, cursor.content_width);
1572
1573 for node in &document.children {
1574 match &node.kind {
1575 NodeKind::Page { config } => {
1576 if !cursor.elements.is_empty() || cursor.y > 0.0 {
1577 pages.push(cursor.finalize());
1578 }
1579 cursor = PageCursor::new(config);
1580 cursor.page_index = pages.len();
1581
1582 let mut page_root = node.style.resolve(Some(&root_style), cursor.content_width);
1589 page_root.height = SizeConstraint::Fixed(cursor.content_height);
1590
1591 let cx = cursor.content_x;
1592 let cw = cursor.content_width;
1593 self.layout_children(
1594 &node.children,
1595 &node.style,
1596 &mut cursor,
1597 &mut pages,
1598 cx,
1599 cw,
1600 Some(&page_root),
1601 font_context,
1602 );
1603 }
1604 NodeKind::PageBreak => {
1605 pages.push(cursor.finalize());
1606 cursor = cursor.new_page();
1607 }
1608 _ => {
1609 let cx = cursor.content_x;
1610 let cw = cursor.content_width;
1611 self.layout_node(
1612 node,
1613 &mut cursor,
1614 &mut pages,
1615 cx,
1616 cw,
1617 Some(&root_style),
1618 font_context,
1619 None,
1620 None,
1621 );
1622 }
1623 }
1624 }
1625
1626 if !cursor.elements.is_empty() {
1632 pages.push(cursor.finalize());
1633 }
1634
1635 fn has_visible_ink(els: &[LayoutElement]) -> bool {
1642 els.iter().any(|e| {
1643 let paints = match &e.draw {
1644 DrawCommand::None => false,
1645 DrawCommand::Rect {
1649 background,
1650 border_width,
1651 ..
1652 } => {
1653 background.is_some()
1654 || border_width.top > 0.0
1655 || border_width.right > 0.0
1656 || border_width.bottom > 0.0
1657 || border_width.left > 0.0
1658 }
1659 _ => true,
1660 };
1661 paints || has_visible_ink(&e.children)
1662 })
1663 }
1664 while pages.len() > 1 && !has_visible_ink(&pages.last().unwrap().elements) {
1665 pages.pop();
1666 }
1667
1668 self.inject_fixed_elements(&mut pages, font_context);
1669
1670 pages
1671 }
1672
1673 #[allow(clippy::too_many_arguments)]
1674 fn layout_node(
1675 &self,
1676 node: &Node,
1677 cursor: &mut PageCursor,
1678 pages: &mut Vec<LayoutPage>,
1679 x: f64,
1680 available_width: f64,
1681 parent_style: Option<&ResolvedStyle>,
1682 font_context: &FontContext,
1683 cross_axis_height: Option<f64>,
1684 forced_outer_width: Option<f64>,
1685 ) {
1686 let mut style = node.style.resolve(parent_style, available_width);
1687
1688 if let Some(h) = cross_axis_height {
1691 if matches!(style.height, SizeConstraint::Auto) {
1692 style.height = SizeConstraint::Fixed(h);
1693 }
1694 }
1695
1696 if let Some(w) = forced_outer_width {
1701 style.width = SizeConstraint::Fixed(w);
1702 }
1703
1704 if style.break_before && !cursor.elements.is_empty() {
1715 pages.push(cursor.finalize());
1716 *cursor = cursor.new_page();
1717 }
1718
1719 let elem_start = cursor.elements.len();
1722
1723 match &node.kind {
1724 NodeKind::PageBreak => {
1725 pages.push(cursor.finalize());
1726 *cursor = cursor.new_page();
1727 }
1728
1729 NodeKind::PageName { name } => {
1730 if cursor.page_name.as_deref() != name.as_deref() {
1736 let has_content = !cursor.elements.is_empty();
1743 if has_content {
1744 pages.push(cursor.finalize());
1745 }
1746 *cursor = cursor.renamed_page(name.clone(), !has_content);
1747 }
1748 }
1749
1750 NodeKind::Fixed {
1751 position, pages, ..
1752 } => {
1753 let height = self.measure_node_height(node, available_width, &style, font_context);
1754 match position {
1755 FixedPosition::Header => {
1756 cursor
1757 .fixed_header
1758 .push((node.clone(), height, parent_style.cloned()));
1759 let _ = pages;
1763 if cursor.fixed_applies(node) {
1764 cursor.y += height;
1765 }
1766 }
1767 FixedPosition::Footer => {
1768 cursor
1769 .fixed_footer
1770 .push((node.clone(), height, parent_style.cloned()));
1771 }
1772 }
1773 }
1774
1775 NodeKind::Watermark { .. } => {
1776 cursor
1778 .watermarks
1779 .push((node.clone(), parent_style.cloned()));
1780 }
1781
1782 NodeKind::TextField {
1783 name,
1784 value,
1785 placeholder,
1786 width: field_w,
1787 height: field_h,
1788 multiline,
1789 password,
1790 read_only,
1791 max_length,
1792 font_size,
1793 } => {
1794 self.layout_form_field(
1795 node,
1796 &style,
1797 cursor,
1798 pages,
1799 x,
1800 *field_w,
1801 *field_h,
1802 DrawCommand::FormField {
1803 field_type: FormFieldType::TextField {
1804 value: value.clone(),
1805 placeholder: placeholder.clone(),
1806 multiline: *multiline,
1807 password: *password,
1808 read_only: *read_only,
1809 max_length: *max_length,
1810 font_size: *font_size,
1811 },
1812 name: name.clone(),
1813 },
1814 "TextField",
1815 );
1816 }
1817
1818 NodeKind::Checkbox {
1819 name,
1820 checked,
1821 width: field_w,
1822 height: field_h,
1823 read_only,
1824 } => {
1825 self.layout_form_field(
1826 node,
1827 &style,
1828 cursor,
1829 pages,
1830 x,
1831 *field_w,
1832 *field_h,
1833 DrawCommand::FormField {
1834 field_type: FormFieldType::Checkbox {
1835 checked: *checked,
1836 read_only: *read_only,
1837 },
1838 name: name.clone(),
1839 },
1840 "Checkbox",
1841 );
1842 }
1843
1844 NodeKind::Dropdown {
1845 name,
1846 options,
1847 value,
1848 width: field_w,
1849 height: field_h,
1850 read_only,
1851 font_size,
1852 } => {
1853 self.layout_form_field(
1854 node,
1855 &style,
1856 cursor,
1857 pages,
1858 x,
1859 *field_w,
1860 *field_h,
1861 DrawCommand::FormField {
1862 field_type: FormFieldType::Dropdown {
1863 options: options.clone(),
1864 value: value.clone(),
1865 read_only: *read_only,
1866 font_size: *font_size,
1867 },
1868 name: name.clone(),
1869 },
1870 "Dropdown",
1871 );
1872 }
1873
1874 NodeKind::RadioButton {
1875 name,
1876 value,
1877 checked,
1878 width: field_w,
1879 height: field_h,
1880 read_only,
1881 } => {
1882 self.layout_form_field(
1883 node,
1884 &style,
1885 cursor,
1886 pages,
1887 x,
1888 *field_w,
1889 *field_h,
1890 DrawCommand::FormField {
1891 field_type: FormFieldType::RadioButton {
1892 value: value.clone(),
1893 checked: *checked,
1894 read_only: *read_only,
1895 },
1896 name: name.clone(),
1897 },
1898 "RadioButton",
1899 );
1900 }
1901
1902 NodeKind::Text {
1903 content,
1904 href,
1905 runs,
1906 } => {
1907 self.layout_text(
1908 content,
1909 href.as_deref(),
1910 runs,
1911 &style,
1912 cursor,
1913 pages,
1914 x,
1915 available_width,
1916 font_context,
1917 node.source_location.as_ref(),
1918 node.bookmark.as_deref(),
1919 None,
1920 );
1921 }
1922
1923 NodeKind::Heading {
1924 content,
1925 href,
1926 runs,
1927 ..
1928 } => {
1929 let heading_role = node_kind_name(&node.kind); self.layout_text(
1935 content,
1936 href.as_deref(),
1937 runs,
1938 &style,
1939 cursor,
1940 pages,
1941 x,
1942 available_width,
1943 font_context,
1944 node.source_location.as_ref(),
1945 node.bookmark.as_deref(),
1946 Some(heading_role),
1947 );
1948 }
1949
1950 NodeKind::List {
1951 ordered,
1952 marker_type,
1953 start,
1954 } => {
1955 self.layout_list(
1956 node,
1957 *ordered,
1958 *marker_type,
1959 *start,
1960 &style,
1961 cursor,
1962 pages,
1963 x,
1964 available_width,
1965 font_context,
1966 );
1967 }
1968
1969 NodeKind::ListItem => {
1970 self.layout_view(
1975 node,
1976 &style,
1977 cursor,
1978 pages,
1979 x,
1980 available_width,
1981 font_context,
1982 );
1983 }
1984
1985 NodeKind::Image { width, height, .. } => {
1986 self.layout_image(
1987 node,
1988 &style,
1989 cursor,
1990 pages,
1991 x,
1992 available_width,
1993 *width,
1994 *height,
1995 );
1996 }
1997
1998 NodeKind::Table { columns } => {
1999 self.layout_table(
2000 node,
2001 &style,
2002 columns,
2003 cursor,
2004 pages,
2005 x,
2006 available_width,
2007 font_context,
2008 );
2009 }
2010
2011 NodeKind::View | NodeKind::Page { .. } => {
2012 self.layout_view(
2013 node,
2014 &style,
2015 cursor,
2016 pages,
2017 x,
2018 available_width,
2019 font_context,
2020 );
2021 }
2022
2023 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
2024 self.layout_view(
2025 node,
2026 &style,
2027 cursor,
2028 pages,
2029 x,
2030 available_width,
2031 font_context,
2032 );
2033 }
2034
2035 NodeKind::Svg {
2036 width: svg_w,
2037 height: svg_h,
2038 view_box,
2039 content,
2040 } => {
2041 self.layout_svg(
2042 node,
2043 &style,
2044 cursor,
2045 pages,
2046 x,
2047 available_width,
2048 *svg_w,
2049 *svg_h,
2050 view_box.as_deref(),
2051 content,
2052 );
2053 }
2054
2055 NodeKind::Barcode {
2056 data,
2057 format,
2058 width: explicit_width,
2059 height: bar_height,
2060 } => {
2061 self.layout_barcode(
2062 node,
2063 &style,
2064 cursor,
2065 pages,
2066 x,
2067 available_width,
2068 data,
2069 *format,
2070 *explicit_width,
2071 *bar_height,
2072 );
2073 }
2074
2075 NodeKind::QrCode {
2076 data,
2077 size: explicit_size,
2078 } => {
2079 self.layout_qrcode(
2080 node,
2081 &style,
2082 cursor,
2083 pages,
2084 x,
2085 available_width,
2086 data,
2087 *explicit_size,
2088 );
2089 }
2090
2091 NodeKind::Canvas {
2092 width: canvas_w,
2093 height: canvas_h,
2094 operations,
2095 } => {
2096 self.layout_canvas(
2097 node,
2098 &style,
2099 cursor,
2100 pages,
2101 x,
2102 available_width,
2103 *canvas_w,
2104 *canvas_h,
2105 operations,
2106 );
2107 }
2108
2109 NodeKind::BarChart {
2110 data,
2111 width: chart_w,
2112 height: chart_h,
2113 color,
2114 show_labels,
2115 show_values,
2116 show_grid,
2117 title,
2118 } => {
2119 let config = crate::chart::bar::BarChartConfig {
2120 color: color.clone(),
2121 show_labels: *show_labels,
2122 show_values: *show_values,
2123 show_grid: *show_grid,
2124 title: title.clone(),
2125 };
2126 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
2127 self.layout_chart(
2128 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
2129 );
2130 }
2131
2132 NodeKind::LineChart {
2133 series,
2134 labels,
2135 width: chart_w,
2136 height: chart_h,
2137 show_points,
2138 show_grid,
2139 title,
2140 } => {
2141 let config = crate::chart::line::LineChartConfig {
2142 show_points: *show_points,
2143 show_grid: *show_grid,
2144 title: title.clone(),
2145 };
2146 let primitives =
2147 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
2148 self.layout_chart(
2149 node,
2150 &style,
2151 cursor,
2152 pages,
2153 x,
2154 *chart_w,
2155 *chart_h,
2156 primitives,
2157 "LineChart",
2158 );
2159 }
2160
2161 NodeKind::PieChart {
2162 data,
2163 width: chart_w,
2164 height: chart_h,
2165 donut,
2166 show_legend,
2167 title,
2168 } => {
2169 let config = crate::chart::pie::PieChartConfig {
2170 donut: *donut,
2171 show_legend: *show_legend,
2172 title: title.clone(),
2173 };
2174 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
2175 self.layout_chart(
2176 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
2177 );
2178 }
2179
2180 NodeKind::AreaChart {
2181 series,
2182 labels,
2183 width: chart_w,
2184 height: chart_h,
2185 show_grid,
2186 title,
2187 } => {
2188 let config = crate::chart::area::AreaChartConfig {
2189 show_grid: *show_grid,
2190 title: title.clone(),
2191 };
2192 let primitives =
2193 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
2194 self.layout_chart(
2195 node,
2196 &style,
2197 cursor,
2198 pages,
2199 x,
2200 *chart_w,
2201 *chart_h,
2202 primitives,
2203 "AreaChart",
2204 );
2205 }
2206
2207 NodeKind::DotPlot {
2208 groups,
2209 width: chart_w,
2210 height: chart_h,
2211 x_min,
2212 x_max,
2213 y_min,
2214 y_max,
2215 x_label,
2216 y_label,
2217 show_legend,
2218 dot_size,
2219 } => {
2220 let config = crate::chart::dot::DotPlotConfig {
2221 x_min: *x_min,
2222 x_max: *x_max,
2223 y_min: *y_min,
2224 y_max: *y_max,
2225 x_label: x_label.clone(),
2226 y_label: y_label.clone(),
2227 show_legend: *show_legend,
2228 dot_size: *dot_size,
2229 };
2230 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
2231 self.layout_chart(
2232 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
2233 );
2234 }
2235 }
2236
2237 if matches!(style.position, Position::Relative)
2245 && (style.top.is_some()
2246 || style.left.is_some()
2247 || style.right.is_some()
2248 || style.bottom.is_some())
2249 {
2250 let dx = style.left.unwrap_or(0.0) - style.right.unwrap_or(0.0);
2251 let dy = style.top.unwrap_or(0.0) - style.bottom.unwrap_or(0.0);
2252 for el in &mut cursor.elements[elem_start..] {
2253 if dx != 0.0 {
2254 offset_element_x(el, dx);
2255 }
2256 if dy != 0.0 {
2257 offset_element_y(el, dy);
2258 }
2259 }
2260 }
2261 }
2262
2263 #[allow(clippy::too_many_arguments)]
2264 fn layout_view(
2265 &self,
2266 node: &Node,
2267 style: &ResolvedStyle,
2268 cursor: &mut PageCursor,
2269 pages: &mut Vec<LayoutPage>,
2270 x: f64,
2271 available_width: f64,
2272 font_context: &FontContext,
2273 ) {
2274 let padding = &style.padding;
2275 let margin = &style.margin.to_edges();
2276 let border = &style.border_width;
2277
2278 let outer_width = match style.width {
2279 SizeConstraint::Fixed(w) => w,
2280 SizeConstraint::Auto => available_width - margin.horizontal(),
2281 }
2282 .min(style.max_width)
2284 .max(style.min_width);
2285 let inner_width = outer_width - padding.horizontal() - border.horizontal();
2286
2287 let children_height =
2288 self.measure_children_height(&node.children, inner_width, style, font_context);
2289 let total_height = match style.height {
2290 SizeConstraint::Fixed(h) => h,
2291 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
2292 }
2293 .max(style.min_height);
2294
2295 let node_x = x + margin.left;
2296
2297 let fits = total_height <= cursor.remaining_height() - margin.vertical();
2298
2299 if fits || !style.breakable {
2300 if !fits && !style.breakable {
2301 pages.push(cursor.finalize());
2302 *cursor = cursor.new_page();
2303 }
2304
2305 let rect_y = cursor.content_y + cursor.y + margin.top;
2307 let snapshot = cursor.elements.len();
2308
2309 if let Some(marker) = bookmark_marker(node, node_x, rect_y) {
2314 cursor.elements.push(marker);
2315 }
2316
2317 let saved_y = cursor.y;
2318 cursor.y += margin.top + padding.top + border.top;
2319
2320 let pages_before = pages.len();
2321 let children_x = node_x + padding.left + border.left;
2322 let is_grid =
2323 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2324 if is_grid {
2325 self.layout_grid_children(
2326 &node.children,
2327 style,
2328 cursor,
2329 pages,
2330 children_x,
2331 inner_width,
2332 font_context,
2333 );
2334 } else {
2335 self.layout_children(
2336 &node.children,
2337 &node.style,
2338 cursor,
2339 pages,
2340 children_x,
2341 inner_width,
2342 Some(style),
2343 font_context,
2344 );
2345 }
2346
2347 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2349
2350 if self.measure_check
2357 && matches!(style.height, SizeConstraint::Auto)
2358 && pages.len() == pages_before
2359 && !child_elements.is_empty()
2360 {
2361 let content_top = rect_y + padding.top + border.top;
2362 let extent = child_elements
2363 .iter()
2364 .map(|el| el.y + el.height)
2365 .fold(f64::NEG_INFINITY, f64::max)
2366 - content_top;
2367 if extent > 0.0 && children_height - extent > 2.0 {
2368 self.defect(format!(
2369 "measure-check: {} measured children at {children_height:.1}pt but they occupy {extent:.1}pt (phantom {:.1}pt)",
2370 node_kind_name(&node.kind),
2371 children_height - extent,
2372 ));
2373 }
2374 }
2375
2376 let rect_element = LayoutElement {
2377 x: node_x,
2378 y: rect_y,
2379 width: outer_width,
2380 height: total_height,
2381 draw: DrawCommand::Rect {
2382 background: style.background_color,
2383 border_width: style.border_width,
2384 border_color: style.border_color,
2385 border_style: style.border_style,
2386 border_radius: style.border_radius,
2387 opacity: 1.0,
2388 box_shadow: style.box_shadow.map(Box::new),
2389 background_gradient: style.background.clone().map(Box::new),
2390 },
2391 children: child_elements,
2392 node_type: Some(node_kind_name(&node.kind).to_string()),
2393 resolved_style: Some(style.clone()),
2394 source_location: node.source_location.clone(),
2395 href: node.href.clone(),
2396 bookmark: None,
2399 alt: None,
2400 is_header_row: false,
2401 actual_text: None,
2402 list_numbering: None,
2403 col_span: 1,
2404 overflow: style.overflow,
2405 opacity: style.opacity,
2406 };
2407 cursor.elements.push(rect_element);
2408
2409 cursor.y = saved_y + total_height + margin.vertical();
2410 } else {
2411 self.layout_breakable_view(
2412 node,
2413 style,
2414 cursor,
2415 pages,
2416 node_x,
2417 outer_width,
2418 inner_width,
2419 font_context,
2420 );
2421 }
2422 }
2423
2424 #[allow(clippy::too_many_arguments)]
2425 fn layout_breakable_view(
2426 &self,
2427 node: &Node,
2428 style: &ResolvedStyle,
2429 cursor: &mut PageCursor,
2430 pages: &mut Vec<LayoutPage>,
2431 node_x: f64,
2432 outer_width: f64,
2433 inner_width: f64,
2434 font_context: &FontContext,
2435 ) {
2436 let padding = &style.padding;
2437 let border = &style.border_width;
2438 let margin = &style.margin.to_edges();
2439
2440 let initial_page_count = pages.len();
2442 let snapshot = cursor.elements.len();
2443 let rect_start_y = cursor.content_y + cursor.y + margin.top;
2444
2445 if let Some(marker) = bookmark_marker(node, node_x, rect_start_y) {
2453 cursor.elements.push(marker);
2454 }
2455
2456 cursor.y += margin.top + padding.top + border.top;
2457 let prev_continuation_offset = cursor.continuation_top_offset;
2458 cursor.continuation_top_offset = padding.top + border.top;
2459
2460 let children_x = node_x + padding.left + border.left;
2461 let is_grid =
2462 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
2463 if is_grid {
2464 self.layout_grid_children(
2465 &node.children,
2466 style,
2467 cursor,
2468 pages,
2469 children_x,
2470 inner_width,
2471 font_context,
2472 );
2473 } else {
2474 self.layout_children(
2475 &node.children,
2476 &node.style,
2477 cursor,
2478 pages,
2479 children_x,
2480 inner_width,
2481 Some(style),
2482 font_context,
2483 );
2484 }
2485
2486 cursor.continuation_top_offset = prev_continuation_offset;
2487
2488 let mut spans_by_height = false;
2517 if pages.len() == initial_page_count {
2518 if let SizeConstraint::Fixed(h) = style.height {
2519 let consumed =
2520 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2521 let mut owed = h - consumed;
2522 while owed > 0.0 {
2523 let room = cursor.remaining_height();
2524 if owed <= room {
2525 cursor.y += owed;
2526 break;
2527 }
2528 owed -= room;
2529 cursor.y += room;
2530 pages.push(cursor.finalize());
2531 *cursor = cursor.new_page();
2532 spans_by_height = true;
2533 }
2534 }
2535 }
2536
2537 let has_visual = style.background_color.is_some()
2539 || style.border_width.top > 0.0
2540 || style.border_width.right > 0.0
2541 || style.border_width.bottom > 0.0
2542 || style.border_width.left > 0.0;
2543 let needs_wrapper = has_visual || style.flex_grow > 0.0;
2545
2546 if !needs_wrapper {
2547 cursor.y += padding.bottom + border.bottom + margin.bottom;
2549 return;
2550 }
2551
2552 let draw_cmd = DrawCommand::Rect {
2553 background: style.background_color,
2554 border_width: style.border_width,
2555 border_color: style.border_color,
2556 border_style: style.border_style,
2557 border_radius: style.border_radius,
2558 opacity: 1.0,
2559 box_shadow: style.box_shadow.map(Box::new),
2560 background_gradient: style.background.clone().map(Box::new),
2561 };
2562
2563 if pages.len() == initial_page_count {
2564 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
2566 let rect_height =
2567 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
2568 cursor.elements.push(LayoutElement {
2569 x: node_x,
2570 y: rect_start_y,
2571 width: outer_width,
2572 height: rect_height,
2573 draw: draw_cmd,
2574 children: child_elements,
2575 node_type: Some(node_kind_name(&node.kind).to_string()),
2576 resolved_style: Some(style.clone()),
2577 source_location: node.source_location.clone(),
2578 href: node.href.clone(),
2579 bookmark: None,
2584 alt: None,
2585 is_header_row: false,
2586 actual_text: None,
2587 list_numbering: None,
2588 col_span: 1,
2589 overflow: style.overflow,
2590 opacity: style.opacity,
2591 });
2592 } else {
2593 let page = &mut pages[initial_page_count];
2597 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
2598 let page_content_bottom =
2599 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
2600 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
2601 if !our_elements.is_empty() {
2602 let rect_height = page_content_bottom - rect_start_y;
2603 page.elements.push(LayoutElement {
2604 x: node_x,
2605 y: rect_start_y,
2606 width: outer_width,
2607 height: rect_height,
2608 draw: draw_cmd.clone(),
2609 children: our_elements,
2610 node_type: Some(node_kind_name(&node.kind).to_string()),
2611 resolved_style: Some(style.clone()),
2612 source_location: node.source_location.clone(),
2613 href: node.href.clone(),
2614 bookmark: None,
2616 alt: None,
2617 is_header_row: false,
2618 actual_text: None,
2619 list_numbering: None,
2620 col_span: 1,
2621 overflow: Overflow::default(),
2622 opacity: 1.0,
2623 });
2624 }
2625
2626 for page in &mut pages[initial_page_count + 1..] {
2628 let header_h: f64 = page.fixed_header.iter().map(|(_, h, _)| *h).sum();
2629 let content_top = page.config.margin.top + header_h;
2630 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
2631 let content_bottom = page.config.margin.top
2632 + (page.height - page.config.margin.vertical())
2633 - footer_h;
2634 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
2635 if !all_elements.is_empty() || spans_by_height {
2636 page.elements.push(LayoutElement {
2637 x: node_x,
2638 y: content_top,
2639 width: outer_width,
2640 height: content_bottom - content_top,
2641 draw: draw_cmd.clone(),
2642 children: all_elements,
2643 node_type: Some(node_kind_name(&node.kind).to_string()),
2644 resolved_style: Some(style.clone()),
2645 source_location: node.source_location.clone(),
2646 href: None,
2647 bookmark: None,
2648 alt: None,
2649 is_header_row: false,
2650 actual_text: None,
2651 list_numbering: None,
2652 col_span: 1,
2653 overflow: Overflow::default(),
2654 opacity: 1.0,
2655 });
2656 }
2657 }
2658
2659 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
2661 if !all_elements.is_empty() || spans_by_height {
2662 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h, _)| *h).sum();
2663 let content_top = cursor.content_y + header_h;
2664 let rect_height =
2665 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
2666 cursor.elements.push(LayoutElement {
2667 x: node_x,
2668 y: content_top,
2669 width: outer_width,
2670 height: rect_height,
2671 draw: draw_cmd,
2672 children: all_elements,
2673 node_type: Some(node_kind_name(&node.kind).to_string()),
2674 resolved_style: Some(style.clone()),
2675 source_location: node.source_location.clone(),
2676 href: None,
2677 bookmark: None,
2678 alt: None,
2679 is_header_row: false,
2680 actual_text: None,
2681 list_numbering: None,
2682 col_span: 1,
2683 overflow: Overflow::default(),
2684 opacity: 1.0,
2685 });
2686 }
2687 }
2688
2689 cursor.y += padding.bottom + border.bottom + margin.bottom;
2690 }
2691
2692 #[allow(clippy::too_many_arguments)]
2693 fn layout_children(
2694 &self,
2695 children: &[Node],
2696 _parent_raw_style: &Style,
2697 cursor: &mut PageCursor,
2698 pages: &mut Vec<LayoutPage>,
2699 content_x: f64,
2700 available_width: f64,
2701 parent_style: Option<&ResolvedStyle>,
2702 font_context: &FontContext,
2703 ) {
2704 let parent_box_y = cursor.content_y + cursor.y;
2706 let parent_box_x = content_x;
2707 let entry_page_index = pages.len();
2711
2712 let parent_positioned = parent_style.map(|s| s.positioned).unwrap_or(false);
2718 let saved_cb = cursor.containing_block;
2719 if parent_positioned {
2720 let cb_height = parent_style
2721 .and_then(|ps| match ps.height {
2722 SizeConstraint::Fixed(h) => {
2723 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2724 }
2725 SizeConstraint::Auto => None,
2726 })
2727 .unwrap_or(saved_cb.3);
2728 cursor.containing_block = (parent_box_x, parent_box_y, available_width, cb_height);
2729 }
2730
2731 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
2733 .iter()
2734 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
2735
2736 let direction = parent_style
2737 .map(|s| s.flex_direction)
2738 .unwrap_or(FlexDirection::Column);
2739
2740 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
2741 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
2742
2743 match direction {
2745 FlexDirection::Column | FlexDirection::ColumnReverse => {
2746 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
2747 flow_children.into_iter().rev().collect()
2748 } else {
2749 flow_children
2750 };
2751
2752 let justify = parent_style
2753 .map(|s| s.justify_content)
2754 .unwrap_or(JustifyContent::FlexStart);
2755 let align = parent_style
2756 .map(|s| s.align_items)
2757 .unwrap_or(AlignItems::Stretch);
2758
2759 let start_y = cursor.y;
2760 let initial_pages = pages.len();
2761
2762 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
2764
2765 for (i, child) in items.iter().enumerate() {
2766 if i > 0 {
2767 cursor.y += row_gap;
2768 }
2769 let child_start = cursor.elements.len();
2770
2771 let child_margin = &child.style.resolve(parent_style, available_width).margin;
2774 let has_auto_h = child_margin.has_auto_horizontal();
2775
2776 let (child_x, layout_w) = if has_auto_h {
2783 let child_style = child.style.resolve(parent_style, available_width);
2784 let has_explicit_width =
2785 matches!(child_style.width, SizeConstraint::Fixed(_));
2786 let intrinsic = self
2787 .measure_intrinsic_width(child, &child_style, font_context)
2788 .min(available_width);
2789 let w = match child_style.width {
2790 SizeConstraint::Fixed(fw) => fw,
2791 SizeConstraint::Auto if child_style.max_width.is_finite() => {
2796 (available_width - child_margin.horizontal())
2797 .min(child_style.max_width)
2798 }
2799 SizeConstraint::Auto => intrinsic,
2800 }
2801 .min(child_style.max_width)
2802 .max(child_style.min_width);
2803 let lw = if has_explicit_width {
2804 available_width
2805 } else {
2806 w
2807 };
2808 let fixed_h = child_margin.horizontal();
2809 let slack = (available_width - w - fixed_h).max(0.0);
2810 let auto_left = child_margin.left.is_auto();
2811 let auto_right = child_margin.right.is_auto();
2812 let ml = match (auto_left, auto_right) {
2813 (true, true) => slack / 2.0,
2814 (true, false) => slack,
2815 (false, true) => 0.0,
2816 (false, false) => 0.0,
2817 };
2818 (content_x + child_margin.left.resolve() + ml, lw)
2819 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
2820 let child_style = child.style.resolve(parent_style, available_width);
2821 let has_explicit_width =
2822 matches!(child_style.width, SizeConstraint::Fixed(_));
2823 let intrinsic = self
2824 .measure_intrinsic_width(child, &child_style, font_context)
2825 .min(available_width);
2826 let w = match child_style.width {
2835 SizeConstraint::Fixed(fw) => fw,
2836 SizeConstraint::Auto => intrinsic,
2837 }
2838 .min(child_style.max_width)
2839 .max(child_style.min_width);
2840 let lw = if has_explicit_width {
2841 available_width
2842 } else {
2843 w
2844 };
2845 match align {
2846 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
2847 AlignItems::FlexEnd => (content_x + available_width - w, lw),
2848 _ => (content_x, available_width),
2849 }
2850 } else {
2851 (content_x, available_width)
2852 };
2853
2854 self.layout_node(
2855 child,
2856 cursor,
2857 pages,
2858 child_x,
2859 layout_w,
2860 parent_style,
2861 font_context,
2862 None,
2863 None,
2864 );
2865
2866 child_ranges.push((child_start, cursor.elements.len()));
2867 }
2868
2869 let container_inner_h: Option<f64> = parent_style
2872 .and_then(|ps| match ps.height {
2873 SizeConstraint::Fixed(h) => {
2874 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2875 }
2876 SizeConstraint::Auto => None,
2877 })
2878 .or_else(|| {
2879 if parent_style.is_none() {
2881 Some(cursor.content_height - start_y)
2882 } else {
2883 None
2884 }
2885 });
2886
2887 if let Some(inner_h) = container_inner_h {
2888 if pages.len() == initial_pages {
2889 let child_styles: Vec<ResolvedStyle> = items
2890 .iter()
2891 .map(|child| child.style.resolve(parent_style, available_width))
2892 .collect();
2893 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
2894 if total_grow > 0.0 {
2895 let children_total = cursor.y - start_y;
2896 let slack = (inner_h - children_total).max(0.0);
2897 if slack > 0.0 {
2898 let mut cumulative_shift = 0.0_f64;
2899 for (i, cs) in child_styles.iter().enumerate() {
2900 let (start, end) = child_ranges[i];
2901 if cumulative_shift > 0.001 {
2902 for j in start..end {
2903 offset_element_y(
2904 &mut cursor.elements[j],
2905 cumulative_shift,
2906 );
2907 }
2908 }
2909 if cs.flex_grow > 0.0 {
2910 let extra = slack * (cs.flex_grow / total_grow);
2911 if start < end {
2913 let elem = &mut cursor.elements[end - 1];
2914 elem.height += extra;
2915 reapply_justify_content(elem);
2916 }
2917 cumulative_shift += extra;
2918 }
2919 }
2920 cursor.y += cumulative_shift;
2921 }
2922 }
2923 }
2924 }
2925
2926 if let Some(inner_h) = container_inner_h {
2933 if pages.len() == initial_pages {
2934 let auto_styles: Vec<ResolvedStyle> = items
2935 .iter()
2936 .map(|child| child.style.resolve(parent_style, available_width))
2937 .collect();
2938 let total_autos: usize = auto_styles
2939 .iter()
2940 .map(|s| {
2941 s.margin.top.is_auto() as usize + s.margin.bottom.is_auto() as usize
2942 })
2943 .sum();
2944 if total_autos > 0 {
2945 let children_total = cursor.y - start_y;
2946 let total_slack = (inner_h - children_total).max(0.0);
2947 if total_slack > 0.0 {
2948 let per_auto = total_slack / total_autos as f64;
2949 let mut cumulative_shift = 0.0_f64;
2950 for (i, cs) in auto_styles.iter().enumerate() {
2951 let (start, end) = child_ranges[i];
2952 let mt_auto = cs.margin.top.is_auto();
2953 let mb_auto = cs.margin.bottom.is_auto();
2954 let this_child_shift =
2958 cumulative_shift + if mt_auto { per_auto } else { 0.0 };
2959 if this_child_shift > 0.001 {
2960 for j in start..end {
2961 offset_element_y(
2962 &mut cursor.elements[j],
2963 this_child_shift,
2964 );
2965 }
2966 }
2967 cumulative_shift =
2970 this_child_shift + if mb_auto { per_auto } else { 0.0 };
2971 }
2972 cursor.y += cumulative_shift;
2973 }
2974 }
2975 }
2976 }
2977
2978 let needs_justify =
2980 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
2981 if needs_justify {
2982 let justify_inner_h = container_inner_h.or_else(|| {
2984 parent_style.and_then(|ps| match ps.height {
2985 SizeConstraint::Fixed(h) => {
2986 Some(h - ps.padding.vertical() - ps.border_width.vertical())
2987 }
2988 SizeConstraint::Auto => None,
2989 })
2990 });
2991 if let Some(inner_h) = justify_inner_h {
2992 let children_total = cursor.y - start_y;
2993 let slack = inner_h - children_total;
2994 if slack > 0.0 {
2995 let n = child_ranges.len();
2996 let offsets: Vec<f64> = match justify {
2997 JustifyContent::FlexEnd => vec![slack; n],
2998 JustifyContent::Center => vec![slack / 2.0; n],
2999 JustifyContent::SpaceBetween => {
3000 if n <= 1 {
3001 vec![0.0; n]
3002 } else {
3003 let per_gap = slack / (n - 1) as f64;
3004 (0..n).map(|i| i as f64 * per_gap).collect()
3005 }
3006 }
3007 JustifyContent::SpaceAround => {
3008 let space = slack / n as f64;
3009 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3010 }
3011 JustifyContent::SpaceEvenly => {
3012 let space = slack / (n + 1) as f64;
3013 (0..n).map(|i| (i + 1) as f64 * space).collect()
3014 }
3015 JustifyContent::FlexStart => vec![0.0; n],
3016 };
3017 for (i, &(start, end)) in child_ranges.iter().enumerate() {
3018 let dy = offsets[i];
3019 if dy.abs() > 0.001 {
3020 for j in start..end {
3021 offset_element_y(&mut cursor.elements[j], dy);
3022 }
3023 }
3024 }
3025 cursor.y += *offsets.last().unwrap_or(&0.0);
3026 }
3027 }
3028 }
3029 }
3030
3031 FlexDirection::Row | FlexDirection::RowReverse => {
3032 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
3033 self.layout_flex_row(
3034 &flow_owned,
3035 cursor,
3036 pages,
3037 content_x,
3038 available_width,
3039 parent_style,
3040 column_gap,
3041 row_gap,
3042 font_context,
3043 );
3044 }
3045 }
3046
3047 let parent_fragmented = parent_positioned && pages.len() > entry_page_index;
3061 let (cb_x, cb_y, cb_w, cb_h) = if parent_positioned {
3062 let ph = parent_style
3063 .and_then(|ps| match ps.height {
3064 SizeConstraint::Fixed(h) => {
3065 Some(h - ps.padding.vertical() - ps.border_width.vertical())
3066 }
3067 SizeConstraint::Auto => None,
3068 })
3069 .unwrap_or_else(|| {
3070 if parent_fragmented {
3071 let first = &pages[entry_page_index];
3072 (first.height - first.config.margin.bottom) - parent_box_y
3073 } else {
3074 cursor.content_y + cursor.y - parent_box_y
3075 }
3076 });
3077 (parent_box_x, parent_box_y, available_width, ph)
3078 } else {
3079 cursor.containing_block
3080 };
3081
3082 for abs_child in &abs_children {
3084 let abs_style = abs_child.style.resolve(parent_style, cb_w);
3085
3086 let child_width = match abs_style.width {
3088 SizeConstraint::Fixed(w) => w,
3089 SizeConstraint::Auto => {
3090 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
3092 (cb_w - l - r).max(0.0)
3093 } else {
3094 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
3095 }
3096 }
3097 };
3098
3099 let child_height = match abs_style.height {
3100 SizeConstraint::Fixed(h) => h,
3101 SizeConstraint::Auto => {
3102 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
3103 }
3104 };
3105
3106 let abs_margin = abs_style.margin.to_edges();
3114 let abs_x = if let Some(l) = abs_style.left {
3115 cb_x + l
3116 } else if let Some(r) = abs_style.right {
3117 cb_x + cb_w - r - child_width - abs_margin.horizontal()
3118 } else {
3119 cb_x
3120 };
3121
3122 let abs_y = if let Some(t) = abs_style.top {
3123 cb_y + t
3124 } else if let Some(b) = abs_style.bottom {
3125 cb_y + cb_h - b - child_height - abs_margin.vertical()
3126 } else {
3127 cb_y
3128 };
3129
3130 let mut abs_cursor = PageCursor::new(&cursor.config);
3132 abs_cursor.y = 0.0;
3133 abs_cursor.content_x = abs_x;
3134 abs_cursor.content_y = abs_y;
3135
3136 self.layout_node(
3137 abs_child,
3138 &mut abs_cursor,
3139 &mut Vec::new(),
3140 abs_x,
3141 child_width,
3142 parent_style,
3143 font_context,
3144 None,
3145 None,
3146 );
3147
3148 if parent_fragmented {
3152 pages[entry_page_index].elements.extend(abs_cursor.elements);
3153 } else {
3154 cursor.elements.extend(abs_cursor.elements);
3155 }
3156 }
3157
3158 cursor.containing_block = saved_cb;
3160 }
3161
3162 #[allow(clippy::too_many_arguments)]
3163 fn layout_flex_row(
3164 &self,
3165 children: &[Node],
3166 cursor: &mut PageCursor,
3167 pages: &mut Vec<LayoutPage>,
3168 content_x: f64,
3169 available_width: f64,
3170 parent_style: Option<&ResolvedStyle>,
3171 column_gap: f64,
3172 row_gap: f64,
3173 font_context: &FontContext,
3174 ) {
3175 if children.is_empty() {
3176 return;
3177 }
3178
3179 let flex_wrap = parent_style
3180 .map(|s| s.flex_wrap)
3181 .unwrap_or(FlexWrap::NoWrap);
3182
3183 let items: Vec<FlexItem> = children
3186 .iter()
3187 .map(|child| {
3188 let style = child.style.resolve(parent_style, available_width);
3189 let base_width = match style.flex_basis {
3190 SizeConstraint::Fixed(w) => w,
3191 SizeConstraint::Auto => match style.width {
3192 SizeConstraint::Fixed(w) => w,
3193 SizeConstraint::Auto => {
3194 self.measure_intrinsic_width(child, &style, font_context)
3195 }
3196 },
3197 };
3198 let min_content_width = self.measure_min_content_width(child, &style, font_context);
3199 FlexItem {
3200 node: child,
3201 style,
3202 base_width,
3203 min_content_width,
3204 }
3205 })
3206 .collect();
3207
3208 let outer_widths: Vec<f64> = items
3217 .iter()
3218 .map(|i| i.base_width + i.style.margin.horizontal())
3219 .collect();
3220 let lines = match flex_wrap {
3221 FlexWrap::NoWrap => {
3222 vec![flex::WrapLine {
3223 start: 0,
3224 end: items.len(),
3225 }]
3226 }
3227 FlexWrap::Wrap => {
3234 flex::partition_into_lines(&outer_widths, column_gap, available_width)
3235 }
3236 FlexWrap::WrapReverse => {
3237 let mut l = flex::partition_into_lines(&outer_widths, column_gap, available_width);
3238 l.reverse();
3239 l
3240 }
3241 };
3242
3243 if lines.is_empty() {
3244 return;
3245 }
3246
3247 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
3249
3250 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
3252
3253 let initial_pages_count = pages.len();
3254 let flex_start_y = cursor.y;
3255 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
3256
3257 for (line_idx, line) in lines.iter().enumerate() {
3258 let parallel = true;
3273 let line_items = &items[line.start..line.end];
3274 let line_count = line.end - line.start;
3275 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
3276 let distributable = available_width - line_gap;
3277
3278 let total_base: f64 = line_items
3292 .iter()
3293 .map(|i| i.base_width + i.style.margin.horizontal())
3294 .sum();
3295 let remaining = distributable - total_base;
3296
3297 if remaining > 0.0 {
3298 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
3299 if total_grow > 0.0 {
3300 for (j, item) in line_items.iter().enumerate() {
3301 final_widths[line.start + j] =
3302 item.base_width + remaining * (item.style.flex_grow / total_grow);
3303 }
3304 }
3305 } else if remaining < 0.0 {
3306 let total_shrink: f64 = line_items
3307 .iter()
3308 .map(|i| i.style.flex_shrink * i.base_width)
3309 .sum();
3310 if total_shrink > 0.0 {
3311 for (j, item) in line_items.iter().enumerate() {
3312 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
3313 let w = item.base_width + remaining * factor;
3314 let floor = item.style.min_width.max(item.min_content_width);
3315 final_widths[line.start + j] = w.max(floor);
3316 }
3317 }
3318 }
3319
3320 let mut line_height: f64 = line_items
3322 .iter()
3323 .enumerate()
3324 .map(|(j, item)| {
3325 let fw = final_widths[line.start + j];
3326 self.measure_node_height(item.node, fw, &item.style, font_context)
3327 + item.style.margin.vertical()
3328 })
3329 .fold(0.0f64, f64::max);
3330
3331 let parent_align = parent_style.map(|s| s.align_items).unwrap_or_default();
3337 let baseline_ds: Vec<Option<f64>> = line_items
3338 .iter()
3339 .enumerate()
3340 .map(|(j, item)| {
3341 let align = item.style.align_self.unwrap_or(parent_align);
3342 if matches!(align, AlignItems::Baseline) {
3343 let fw = final_widths[line.start + j];
3344 Some(self.flex_item_baseline_distance(
3345 item.node,
3346 &item.style,
3347 fw,
3348 font_context,
3349 ))
3350 } else {
3351 None
3352 }
3353 })
3354 .collect();
3355 let line_baseline = baseline_ds
3356 .iter()
3357 .flatten()
3358 .fold(None, |m: Option<f64>, &d| Some(m.map_or(d, |x| x.max(d))));
3359 if let Some(bl) = line_baseline {
3360 for (j, item) in line_items.iter().enumerate() {
3361 if let Some(d) = baseline_ds[j] {
3362 let fw = final_widths[line.start + j];
3363 let h = self.measure_node_height(item.node, fw, &item.style, font_context)
3364 + item.style.margin.vertical();
3365 line_height = line_height.max(bl - d + h);
3366 }
3367 }
3368 }
3369
3370 if let Some(ps) = parent_style {
3388 if matches!(ps.flex_wrap, FlexWrap::NoWrap) {
3389 if let SizeConstraint::Fixed(h) = ps.height {
3390 line_height = h - ps.padding.vertical() - ps.border_width.vertical();
3391 }
3392 }
3393 }
3394
3395 if !parallel && line_height > cursor.remaining_height() && cursor.y > 0.0 {
3417 pages.push(cursor.finalize());
3418 *cursor = cursor.new_page();
3419 }
3420
3421 if line_idx > 0 {
3423 cursor.y += row_gap;
3424 }
3425
3426 let row_start_y = cursor.y;
3427
3428 let actual_total: f64 = (line.start..line.end)
3437 .map(|i| final_widths[i] + items[i].style.margin.horizontal())
3438 .sum();
3439 let slack = available_width - actual_total - line_gap;
3440
3441 let (start_offset, between_extra) = match justify {
3442 JustifyContent::FlexStart => (0.0, 0.0),
3443 JustifyContent::FlexEnd => (slack, 0.0),
3444 JustifyContent::Center => (slack / 2.0, 0.0),
3445 JustifyContent::SpaceBetween => {
3446 if line_count > 1 {
3447 (0.0, slack / (line_count as f64 - 1.0))
3448 } else {
3449 (0.0, 0.0)
3450 }
3451 }
3452 JustifyContent::SpaceAround => {
3453 let s = slack / line_count as f64;
3454 (s / 2.0, s)
3455 }
3456 JustifyContent::SpaceEvenly => {
3457 let s = slack / (line_count as f64 + 1.0);
3458 (s, s)
3459 }
3460 };
3461
3462 let line_elem_start = cursor.elements.len();
3463 let mut x = content_x + start_offset;
3464
3465 let base_page_elements = if parallel {
3472 std::mem::take(&mut cursor.elements)
3473 } else {
3474 Vec::new()
3475 };
3476 let mut item_frags: Vec<ItemFragments> = Vec::with_capacity(line_items.len());
3477
3478 let line_start_pages = pages.len();
3489
3490 for (j, item) in line_items.iter().enumerate() {
3491 if j > 0 {
3492 x += column_gap + between_extra;
3493 }
3494
3495 let fw = final_widths[line.start + j];
3496
3497 let align = item
3498 .style
3499 .align_self
3500 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
3501
3502 let item_height =
3503 self.measure_node_height(item.node, fw, &item.style, font_context);
3504
3505 let has_auto_v = item.style.margin.has_auto_vertical();
3507 let y_offset = if has_auto_v {
3508 let fixed_v = item.style.margin.vertical();
3509 let slack = (line_height - item_height - fixed_v).max(0.0);
3510 let auto_top = item.style.margin.top.is_auto();
3511 let auto_bottom = item.style.margin.bottom.is_auto();
3512 match (auto_top, auto_bottom) {
3513 (true, true) => slack / 2.0,
3514 (true, false) => slack,
3515 (false, true) => 0.0,
3516 (false, false) => 0.0,
3517 }
3518 } else {
3519 match align {
3520 AlignItems::FlexStart => 0.0,
3521 AlignItems::FlexEnd => {
3522 line_height - item_height - item.style.margin.vertical()
3523 }
3524 AlignItems::Center => {
3525 (line_height - item_height - item.style.margin.vertical()) / 2.0
3526 }
3527 AlignItems::Stretch => 0.0,
3528 AlignItems::Baseline => match (line_baseline, baseline_ds[j]) {
3529 (Some(bl), Some(d)) => bl - d,
3530 _ => 0.0,
3531 },
3532 }
3533 };
3534
3535 let cross_h = if matches!(align, AlignItems::Stretch)
3539 && matches!(item.style.height, SizeConstraint::Auto)
3540 && !has_auto_v
3541 {
3542 let stretch_h = line_height - item.style.margin.vertical();
3543 if stretch_h > item_height {
3544 Some(stretch_h)
3545 } else {
3546 None
3547 }
3548 } else {
3549 None
3550 };
3551
3552 let saved_y = cursor.y;
3553 cursor.y = row_start_y + y_offset;
3554
3555 let mut item_cursor = cursor.clone();
3588 let mut item_pages: Vec<LayoutPage> = Vec::new();
3589
3590 self.layout_node(
3591 item.node,
3592 &mut item_cursor,
3593 &mut item_pages,
3594 x,
3595 available_width,
3596 parent_style,
3597 font_context,
3598 cross_h,
3599 Some(fw),
3600 );
3601
3602 if parallel {
3603 let final_y = item_cursor.y;
3606 let tail = std::mem::take(&mut item_cursor.elements);
3607 item_frags.push(ItemFragments {
3608 pages: item_pages,
3609 tail,
3610 cursor: item_cursor,
3611 final_y,
3612 stretches: matches!(align, AlignItems::Stretch)
3613 && matches!(item.style.height, SizeConstraint::Auto)
3614 && !has_auto_v,
3615 });
3616 } else {
3617 pages.extend(item_pages);
3619 *cursor = item_cursor;
3620 }
3621
3622 cursor.y = saved_y;
3623 x += fw + item.style.margin.horizontal();
3630 }
3631
3632 if !parallel {
3639 if line_items.len() > 1 && pages.len() > line_start_pages {
3644 let near = line_items
3645 .iter()
3646 .find_map(|it| first_text_snippet(it.node))
3647 .map(|t| format!(" (row beginning \"{t}\")"))
3648 .unwrap_or_default();
3649 self.defect(format!(
3650 "render defect: a wrapped flex row crossing a page boundary lays its children sequentially — a column taller than the page does not continue side by side{near}"
3651 ));
3652 }
3653 cursor.y = row_start_y + line_height;
3654 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3655 continue;
3656 }
3657
3658 let frag_count = item_frags
3659 .iter()
3660 .map(|f| f.pages.len() + 1)
3661 .max()
3662 .unwrap_or(1);
3663
3664 let mut empty_bands: Vec<(usize, LayoutElement)> = Vec::new();
3676 if frag_count > 1 {
3677 let page_bottom = cursor.content_y + cursor.content_height;
3678 let last = frag_count - 1;
3679 let row_end = cursor.content_y + end_y_of(&item_frags, last);
3680 for f in item_frags.iter_mut() {
3681 if !f.stretches {
3682 continue;
3683 }
3684 let own_last = f.pages.len();
3685 for (k, page) in f.pages.iter_mut().enumerate() {
3686 let bottom = if k == last { row_end } else { page_bottom };
3687 stretch_fragment(&mut page.elements, bottom);
3688 }
3689 let bottom = if own_last == last {
3690 row_end
3691 } else {
3692 page_bottom
3693 };
3694 stretch_fragment(&mut f.tail, bottom);
3695
3696 if own_last < last {
3709 let template = f
3710 .tail
3711 .iter()
3712 .chain(f.pages.iter().flat_map(|p| p.elements.iter()))
3713 .find(|el| paints_a_box(el))
3714 .cloned();
3715 if let Some(t) = template {
3716 for k in (own_last + 1)..=last {
3717 let bottom = if k == last { row_end } else { page_bottom };
3718 let mut band = t.clone();
3719 band.children.clear();
3720 band.y = cursor.content_y;
3721 band.height = (bottom - cursor.content_y).max(0.0);
3722 empty_bands.push((k, band));
3726 }
3727 }
3728 }
3729 }
3730 }
3731
3732 let mut merged: Vec<Vec<LayoutElement>> = vec![Vec::new(); frag_count];
3733 merged[0] = base_page_elements;
3735 for (k, band) in empty_bands {
3738 merged[k].push(band);
3739 }
3740 for f in item_frags.iter_mut() {
3741 for (k, page) in f.pages.iter_mut().enumerate() {
3742 merged[k].append(&mut page.elements);
3743 }
3744 let tail_index = f.pages.len();
3747 let mut tail = std::mem::take(&mut f.tail);
3748 merged[tail_index].append(&mut tail);
3749 }
3750
3751 if frag_count == 1 {
3752 cursor.elements = std::mem::take(&mut merged[0]);
3756 cursor.y = row_start_y + line_height;
3757 } else {
3758 let carrier_idx = item_frags
3769 .iter()
3770 .enumerate()
3771 .max_by_key(|(_, f)| f.pages.len())
3772 .map(|(i, _)| i)
3773 .expect("a flex line has at least one item");
3774 let mut carrier = item_frags.swap_remove(carrier_idx);
3775
3776 for (k, page) in carrier.pages.iter_mut().enumerate() {
3777 page.elements = std::mem::take(&mut merged[k]);
3778 }
3779 pages.append(&mut carrier.pages);
3780
3781 let last = frag_count - 1;
3784 let end_y = item_frags
3785 .iter()
3786 .chain(std::iter::once(&carrier))
3787 .filter(|f| f.pages.len() == last)
3788 .map(|f| f.final_y)
3789 .fold(f64::MIN, f64::max);
3790
3791 let mut merged_tail = std::mem::take(&mut merged[last]);
3792 *cursor = carrier.cursor;
3793 cursor.elements.clear();
3794 cursor.elements.append(&mut merged_tail);
3795 cursor.y = end_y;
3796 }
3797
3798 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
3799 }
3800
3801 if pages.len() == initial_pages_count && !line_infos.is_empty() {
3803 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
3804 if !matches!(align_content, AlignContent::FlexStart)
3805 && !matches!(flex_wrap, FlexWrap::NoWrap)
3806 {
3807 if let Some(parent) = parent_style {
3808 if let SizeConstraint::Fixed(container_h) = parent.height {
3809 let inner_h = container_h
3810 - parent.padding.vertical()
3811 - parent.border_width.vertical();
3812 let total_used = cursor.y - flex_start_y;
3813 let slack = inner_h - total_used;
3814 if slack > 0.0 {
3815 let n = line_infos.len();
3816 let offsets: Vec<f64> = match align_content {
3817 AlignContent::FlexEnd => vec![slack; n],
3818 AlignContent::Center => vec![slack / 2.0; n],
3819 AlignContent::SpaceBetween => {
3820 if n <= 1 {
3821 vec![0.0; n]
3822 } else {
3823 let per_gap = slack / (n - 1) as f64;
3824 (0..n).map(|i| i as f64 * per_gap).collect()
3825 }
3826 }
3827 AlignContent::SpaceAround => {
3828 let space = slack / n as f64;
3829 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
3830 }
3831 AlignContent::SpaceEvenly => {
3832 let space = slack / (n + 1) as f64;
3833 (0..n).map(|i| (i + 1) as f64 * space).collect()
3834 }
3835 AlignContent::Stretch => {
3836 let extra = slack / n as f64;
3837 (0..n).map(|i| i as f64 * extra).collect()
3838 }
3839 AlignContent::FlexStart => vec![0.0; n],
3840 };
3841 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
3842 let dy = offsets[i];
3843 if dy.abs() > 0.001 {
3844 for j in start..end {
3845 offset_element_y(&mut cursor.elements[j], dy);
3846 }
3847 }
3848 }
3849 cursor.y += *offsets.last().unwrap_or(&0.0);
3850 }
3851 }
3852 }
3853 }
3854 }
3855 }
3856
3857 #[allow(clippy::too_many_arguments)]
3860 fn layout_list(
3861 &self,
3862 node: &Node,
3863 ordered: bool,
3864 marker_type: ListMarkerType,
3865 start: u32,
3866 style: &ResolvedStyle,
3867 cursor: &mut PageCursor,
3868 pages: &mut Vec<LayoutPage>,
3869 x: f64,
3870 available_width: f64,
3871 font_context: &FontContext,
3872 ) {
3873 let margin = &style.margin.to_edges();
3874 let padding = &style.padding;
3875
3876 cursor.y += margin.top;
3877
3878 let list_x = x + margin.left;
3879 let outer_width = available_width - margin.horizontal();
3880 let inner_width = outer_width - padding.horizontal();
3881
3882 let n_items = node
3885 .children
3886 .iter()
3887 .filter(|c| matches!(c.kind, NodeKind::ListItem))
3888 .count() as u32;
3889
3890 let marker_gutter =
3891 compute_marker_gutter_width(ordered, marker_type, start, n_items, style);
3892
3893 let list_inner_x = list_x + padding.left;
3894 let content_x = list_inner_x + marker_gutter;
3895 let content_width = (inner_width - marker_gutter).max(0.0);
3896
3897 let snapshot = cursor.elements.len();
3900 let list_start_y = cursor.content_y + cursor.y;
3901 cursor.y += padding.top;
3902
3903 let mut item_index: u32 = 0;
3904 for child in &node.children {
3905 if !matches!(child.kind, NodeKind::ListItem) {
3906 continue;
3907 }
3908 let marker_idx = start + item_index;
3909 self.layout_list_item(
3910 child,
3911 marker_idx,
3912 ordered,
3913 marker_type,
3914 marker_gutter,
3915 style,
3916 cursor,
3917 pages,
3918 list_inner_x,
3919 content_x,
3920 content_width,
3921 font_context,
3922 );
3923 item_index += 1;
3924 }
3925
3926 cursor.y += padding.bottom;
3927
3928 let item_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
3930 let list_height = cursor.content_y + cursor.y - list_start_y;
3931 cursor.elements.push(LayoutElement {
3932 x: list_x,
3933 y: list_start_y,
3934 width: outer_width,
3935 height: list_height,
3936 draw: DrawCommand::None,
3937 children: item_elements,
3938 node_type: Some("List".to_string()),
3939 resolved_style: Some(style.clone()),
3940 source_location: node.source_location.clone(),
3941 href: None,
3942 bookmark: node.bookmark.clone(),
3943 alt: None,
3944 is_header_row: false,
3945 actual_text: None,
3949 list_numbering: match marker_type {
3950 ListMarkerType::None => None,
3951 ListMarkerType::Disc => Some("Disc"),
3952 ListMarkerType::Circle => Some("Circle"),
3953 ListMarkerType::Square => Some("Square"),
3954 ListMarkerType::Decimal => Some("Decimal"),
3955 ListMarkerType::LowerAlpha => Some("LowerAlpha"),
3956 ListMarkerType::UpperAlpha => Some("UpperAlpha"),
3957 ListMarkerType::LowerRoman => Some("LowerRoman"),
3958 ListMarkerType::UpperRoman => Some("UpperRoman"),
3959 },
3960 col_span: 1,
3961 overflow: style.overflow,
3962 opacity: style.opacity,
3963 });
3964
3965 cursor.y += margin.bottom;
3966 }
3967
3968 #[allow(clippy::too_many_arguments)]
3969 fn layout_list_item(
3970 &self,
3971 item: &Node,
3972 marker_idx: u32,
3973 ordered: bool,
3974 marker_type: ListMarkerType,
3975 marker_gutter: f64,
3976 parent_style: &ResolvedStyle,
3977 cursor: &mut PageCursor,
3978 pages: &mut Vec<LayoutPage>,
3979 list_inner_x: f64,
3980 content_x: f64,
3981 content_width: f64,
3982 font_context: &FontContext,
3983 ) {
3984 let item_style = item.style.resolve(Some(parent_style), content_width);
3985 let item_margin = item_style.margin.to_edges();
3986
3987 cursor.y += item_margin.top;
3988 let item_start_y = cursor.content_y + cursor.y;
3989 let item_snapshot = cursor.elements.len();
3990
3991 let marker_str = format_marker(marker_idx, ordered, marker_type);
3995 if !marker_str.is_empty() {
3996 let saved_y = cursor.y;
3997 self.layout_text(
3998 &marker_str,
3999 None,
4000 &[],
4001 &item_style,
4002 cursor,
4003 pages,
4004 list_inner_x,
4005 marker_gutter,
4006 font_context,
4007 None,
4008 None,
4009 Some("Lbl"),
4010 );
4011 cursor.y = saved_y;
4012 }
4013
4014 self.layout_children(
4020 &item.children,
4021 &item.style,
4022 cursor,
4023 pages,
4024 content_x,
4025 content_width,
4026 Some(&item_style),
4027 font_context,
4028 );
4029
4030 let item_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, item_snapshot);
4033 let item_height = cursor.content_y + cursor.y - item_start_y;
4034 let item_width = content_x + content_width - list_inner_x;
4035 cursor.elements.push(LayoutElement {
4036 x: list_inner_x,
4037 y: item_start_y,
4038 width: item_width,
4039 height: item_height,
4040 draw: DrawCommand::None,
4041 children: item_children,
4042 node_type: Some("ListItem".to_string()),
4043 resolved_style: Some(item_style.clone()),
4044 source_location: item.source_location.clone(),
4045 href: None,
4046 bookmark: item.bookmark.clone(),
4047 alt: None,
4048 is_header_row: false,
4049 actual_text: None,
4050 list_numbering: None,
4051 col_span: 1,
4052 overflow: item_style.overflow,
4053 opacity: item_style.opacity,
4054 });
4055
4056 cursor.y += item_margin.bottom;
4057 }
4058
4059 #[allow(clippy::too_many_arguments)]
4060 fn layout_table(
4061 &self,
4062 node: &Node,
4063 style: &ResolvedStyle,
4064 column_defs: &[ColumnDef],
4065 cursor: &mut PageCursor,
4066 pages: &mut Vec<LayoutPage>,
4067 x: f64,
4068 available_width: f64,
4069 font_context: &FontContext,
4070 ) {
4071 let padding = &style.padding;
4072 let margin = &style.margin.to_edges();
4073 let border = &style.border_width;
4074
4075 let table_x = x + margin.left;
4076 let table_width = match style.width {
4077 SizeConstraint::Fixed(w) => w,
4078 SizeConstraint::Auto => available_width - margin.horizontal(),
4079 };
4080 let inner_width = table_width - padding.horizontal() - border.horizontal();
4081
4082 let col_widths = self.resolve_column_widths(
4083 column_defs,
4084 inner_width,
4085 &node.children,
4086 style,
4087 font_context,
4088 );
4089
4090 let all_offsets = Self::table_column_offsets(&node.children);
4094 let mut header_rows: Vec<(&Node, &[usize])> = Vec::new();
4095 let mut body_rows: Vec<(&Node, &[usize])> = Vec::new();
4096
4097 for (child, offs) in node.children.iter().zip(&all_offsets) {
4098 match &child.kind {
4099 NodeKind::TableRow { is_header: true } => header_rows.push((child, offs)),
4100 _ => body_rows.push((child, offs)),
4101 }
4102 }
4103
4104 if !style.breakable {
4110 let total_height: f64 = node
4111 .children
4112 .iter()
4113 .zip(&all_offsets)
4114 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4115 .sum::<f64>()
4116 + padding.vertical()
4117 + border.vertical();
4118 let fresh_page_available = cursor.content_height
4119 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4120 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4121 if total_height > cursor.remaining_height()
4122 && total_height <= fresh_page_available
4123 && cursor.y > 0.0
4124 {
4125 pages.push(cursor.finalize());
4126 *cursor = cursor.new_page();
4127 }
4128 }
4129
4130 let initial_page_count = pages.len();
4137 let snapshot = cursor.elements.len();
4138 let rect_start_y = cursor.content_y + cursor.y + margin.top;
4139
4140 cursor.y += margin.top + padding.top + border.top;
4141
4142 let cell_x_start = table_x + padding.left + border.left;
4143
4144 if !header_rows.is_empty() {
4169 let total_header_h: f64 = header_rows
4170 .iter()
4171 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4172 .sum();
4173 let first_body_h = body_rows
4174 .first()
4175 .map(|(r, o)| self.measure_table_row_height(r, &col_widths, o, style, font_context))
4176 .unwrap_or(0.0);
4177
4178 let needed = total_header_h + first_body_h;
4179 let fresh_page_available = cursor.content_height
4180 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4181 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4182
4183 if needed > cursor.remaining_height() && needed <= fresh_page_available {
4184 pages.push(cursor.finalize());
4185 *cursor = cursor.new_page();
4186 cursor.y += padding.top + border.top;
4187 }
4188 }
4189
4190 for (header_row, offs) in &header_rows {
4191 self.layout_table_row(
4192 header_row,
4193 &col_widths,
4194 offs,
4195 style,
4196 cursor,
4197 cell_x_start,
4198 font_context,
4199 pages,
4200 );
4201 }
4202
4203 for (body_row, offs) in &body_rows {
4204 let row_height =
4205 self.measure_table_row_height(body_row, &col_widths, offs, style, font_context);
4206
4207 let fresh_page_available = cursor.content_height
4215 - cursor.fixed_header.iter().map(|(_, h, _)| *h).sum::<f64>()
4216 - cursor.fixed_footer.iter().map(|(_, h, _)| *h).sum::<f64>();
4217 if row_height > fresh_page_available {
4218 self.defect(format!(
4219 "render defect: table row needs {row_height:.0}pt but a page holds {fresh_page_available:.0}pt — rows are atomic, so it is placed whole and overflows",
4220 ));
4221 }
4222 if row_height > cursor.remaining_height()
4223 && cursor.remaining_height() < fresh_page_available - 0.5
4224 {
4225 pages.push(cursor.finalize());
4226 *cursor = cursor.new_page();
4227
4228 cursor.y += padding.top + border.top;
4229 for (header_row, h_offs) in &header_rows {
4230 self.layout_table_row(
4231 header_row,
4232 &col_widths,
4233 h_offs,
4234 style,
4235 cursor,
4236 cell_x_start,
4237 font_context,
4238 pages,
4239 );
4240 }
4241 }
4242
4243 self.layout_table_row(
4244 body_row,
4245 &col_widths,
4246 offs,
4247 style,
4248 cursor,
4249 cell_x_start,
4250 font_context,
4251 pages,
4252 );
4253 }
4254
4255 let has_visual = style.background_color.is_some()
4259 || style.background.is_some()
4260 || style.border_width.top > 0.0
4261 || style.border_width.right > 0.0
4262 || style.border_width.bottom > 0.0
4263 || style.border_width.left > 0.0;
4264 let draw_cmd = if has_visual {
4265 DrawCommand::Rect {
4266 background: style.background_color,
4267 border_width: style.border_width,
4268 border_color: style.border_color,
4269 border_style: style.border_style,
4270 border_radius: style.border_radius,
4271 opacity: 1.0,
4272 box_shadow: style.box_shadow.map(Box::new),
4273 background_gradient: style.background.clone().map(Box::new),
4274 }
4275 } else {
4276 DrawCommand::None
4277 };
4278 let make_wrapper =
4279 |y: f64, height: f64, children: Vec<LayoutElement>, draw| LayoutElement {
4280 x: table_x,
4281 y,
4282 width: table_width,
4283 height,
4284 draw,
4285 children,
4286 node_type: Some(node_kind_name(&node.kind).to_string()),
4287 resolved_style: Some(style.clone()),
4288 source_location: node.source_location.clone(),
4289 href: node.href.clone(),
4290 bookmark: None,
4291 alt: None,
4292 is_header_row: false,
4293 actual_text: None,
4294 list_numbering: None,
4295 col_span: 1,
4296 overflow: Overflow::default(),
4297 opacity: style.opacity,
4298 };
4299
4300 let table_bottom_y = cursor.content_y + cursor.y + padding.bottom + border.bottom;
4301
4302 if pages.len() == initial_page_count {
4303 let child_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4305 cursor.elements.push(make_wrapper(
4306 rect_start_y,
4307 table_bottom_y - rect_start_y,
4308 child_elements,
4309 draw_cmd,
4310 ));
4311 } else {
4312 let page = &mut pages[initial_page_count];
4331 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
4332 let page_content_bottom =
4333 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
4334 let our_elements: Vec<LayoutElement> = drain_since(&mut page.elements, snapshot);
4335 if !our_elements.is_empty() {
4336 page.elements.push(make_wrapper(
4337 rect_start_y,
4338 page_content_bottom - rect_start_y,
4339 our_elements,
4340 draw_cmd.clone(),
4341 ));
4342 }
4343
4344 for page in &mut pages[initial_page_count + 1..] {
4346 let header_h: f64 = page.fixed_header.iter().map(|(_, h, _)| *h).sum();
4347 let content_top = page.config.margin.top + header_h;
4348 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h, _)| *h).sum();
4349 let content_bottom = page.config.margin.top
4350 + (page.height - page.config.margin.vertical())
4351 - footer_h;
4352 let all_elements: Vec<LayoutElement> = std::mem::take(&mut page.elements);
4353 if !all_elements.is_empty() {
4354 page.elements.push(make_wrapper(
4355 content_top,
4356 content_bottom - content_top,
4357 all_elements,
4358 draw_cmd.clone(),
4359 ));
4360 }
4361 }
4362
4363 let all_elements: Vec<LayoutElement> = std::mem::take(&mut cursor.elements);
4365 if !all_elements.is_empty() {
4366 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h, _)| *h).sum();
4367 let content_top = cursor.content_y + header_h;
4368 cursor.elements.push(make_wrapper(
4369 content_top,
4370 table_bottom_y - content_top,
4371 all_elements,
4372 draw_cmd,
4373 ));
4374 }
4375 }
4376
4377 cursor.y += padding.bottom + border.bottom + margin.bottom;
4378 }
4379
4380 fn subtree_forces_break(node: &Node) -> bool {
4390 if matches!(node.kind, NodeKind::PageBreak | NodeKind::PageName { .. }) {
4391 return true;
4392 }
4393 if node.style.break_before == Some(true) {
4394 return true;
4395 }
4396 node.children.iter().any(Self::subtree_forces_break)
4397 }
4398
4399 fn occupancy_column_count(children: &[Node]) -> usize {
4405 let offsets = Self::table_column_offsets(children);
4406 children
4407 .iter()
4408 .zip(&offsets)
4409 .map(|(row, offs)| {
4410 row.children
4411 .iter()
4412 .zip(offs)
4413 .map(|(cell, &start)| start + Self::cell_col_span(cell))
4414 .max()
4415 .unwrap_or(0)
4416 })
4417 .max()
4418 .unwrap_or(1)
4419 .max(1)
4420 }
4421
4422 fn table_column_offsets(rows: &[Node]) -> Vec<Vec<usize>> {
4432 fn spans(cell: &Node) -> (usize, u32) {
4433 match &cell.kind {
4434 NodeKind::TableCell { col_span, row_span } => {
4435 ((*col_span).max(1) as usize, (*row_span).max(1))
4436 }
4437 _ => (1, 1),
4438 }
4439 }
4440 let mut pending: Vec<u32> = Vec::new();
4441 let mut out = Vec::with_capacity(rows.len());
4442 for row in rows {
4443 let mut offsets = Vec::with_capacity(row.children.len());
4444 let mut col = 0usize;
4445 for cell in &row.children {
4446 let (span, rspan) = spans(cell);
4447 while pending.get(col).copied().unwrap_or(0) > 0 {
4448 col += 1;
4449 }
4450 offsets.push(col);
4451 if rspan > 1 {
4452 if pending.len() < col + span {
4453 pending.resize(col + span, 0);
4454 }
4455 for slot in pending.iter_mut().take(col + span).skip(col) {
4456 *slot = (*slot).max(rspan);
4457 }
4458 }
4459 col += span;
4460 }
4461 out.push(offsets);
4462 for p in pending.iter_mut() {
4463 *p = p.saturating_sub(1);
4464 }
4465 }
4466 out
4467 }
4468
4469 #[allow(clippy::too_many_arguments)]
4470 fn layout_table_row(
4471 &self,
4472 row: &Node,
4473 col_widths: &[f64],
4474 col_offsets: &[usize],
4475 parent_style: &ResolvedStyle,
4476 cursor: &mut PageCursor,
4477 start_x: f64,
4478 font_context: &FontContext,
4479 pages: &mut Vec<LayoutPage>,
4480 ) {
4481 let row_style = row
4482 .style
4483 .resolve(Some(parent_style), col_widths.iter().sum());
4484
4485 let row_height =
4486 self.measure_table_row_height(row, col_widths, col_offsets, parent_style, font_context);
4487 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
4488 let row_y = cursor.content_y + cursor.y;
4489 let total_width: f64 = col_widths.iter().sum();
4490
4491 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
4492
4493 let row_snapshot = cursor.elements.len();
4495
4496 let row_may_break =
4508 row_height > cursor.remaining_height() || Self::subtree_forces_break(row);
4509
4510 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
4511 for (cell_i, cell) in row.children.iter().enumerate() {
4515 let span = match &cell.kind {
4516 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
4517 _ => 1,
4518 };
4519 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
4520 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
4521 let cell_x = start_x + col_widths.iter().take(start_col).copied().sum::<f64>();
4522
4523 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4524
4525 let cell_snapshot = cursor.elements.len();
4527
4528 let inner_width =
4529 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4530
4531 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
4532 let saved_y = cursor.y;
4533 cursor.y += cell_style.padding.top + cell_style.border_width.top;
4534
4535 if !matches!(cell_style.vertical_align, crate::style::VerticalAlign::Top) {
4539 let content_h: f64 = cell
4540 .children
4541 .iter()
4542 .map(|ch| {
4543 let ch_style = ch.style.resolve(Some(&cell_style), inner_width);
4544 self.measure_node_height(ch, inner_width, &ch_style, font_context)
4545 })
4546 .sum();
4547 let inner_row =
4548 row_height - cell_style.padding.vertical() - cell_style.border_width.vertical();
4549 let slack = (inner_row - content_h).max(0.0);
4550 cursor.y += match cell_style.vertical_align {
4551 crate::style::VerticalAlign::Middle => slack / 2.0,
4552 crate::style::VerticalAlign::Bottom => slack,
4553 crate::style::VerticalAlign::Baseline => row_bl
4558 .map(|b| {
4559 let d = self.cell_baseline_distance(
4560 cell,
4561 &cell_style,
4562 inner_width,
4563 font_context,
4564 );
4565 (b - d).max(0.0)
4566 })
4567 .unwrap_or(0.0),
4568 crate::style::VerticalAlign::Top => 0.0,
4569 };
4570 }
4571
4572 let cursor_before_cell = if row_may_break {
4577 Some(cursor.clone())
4578 } else {
4579 None
4580 };
4581 let mut cell_pages: Vec<LayoutPage> = Vec::new();
4582 for child in &cell.children {
4583 self.layout_node(
4584 child,
4585 cursor,
4586 &mut cell_pages,
4587 content_x,
4588 inner_width,
4589 Some(&cell_style),
4590 font_context,
4591 None,
4592 None,
4593 );
4594 }
4595
4596 if !cell_pages.is_empty() {
4598 let post_break_elements = std::mem::take(&mut cursor.elements);
4599 if let Some(last_page) = cell_pages.last_mut() {
4600 last_page.elements.extend(post_break_elements);
4601 }
4602 if !is_header {
4608 all_overflow_pages.extend(cell_pages);
4609 }
4610 *cursor = cursor_before_cell
4616 .expect("table cell broke but no rollback checkpoint was taken (row_may_break under-approximated)");
4617 }
4618
4619 cursor.y = saved_y;
4620
4621 let cell_children: Vec<LayoutElement> =
4623 drain_since(&mut cursor.elements, cell_snapshot);
4624
4625 cursor.elements.push(LayoutElement {
4627 x: cell_x,
4628 y: row_y,
4629 width: col_width,
4630 height: row_height,
4631 draw: if cell_style.background_color.is_some()
4632 || cell_style.border_width.horizontal() > 0.0
4633 || cell_style.border_width.vertical() > 0.0
4634 {
4635 DrawCommand::Rect {
4636 background: cell_style.background_color,
4637 border_width: cell_style.border_width,
4638 border_color: cell_style.border_color,
4639 border_style: cell_style.border_style,
4640 border_radius: cell_style.border_radius,
4641 opacity: 1.0,
4642 box_shadow: cell_style.box_shadow.map(Box::new),
4643 background_gradient: cell_style.background.clone().map(Box::new),
4644 }
4645 } else {
4646 DrawCommand::None
4647 },
4648 children: cell_children,
4649 node_type: Some("TableCell".to_string()),
4650 resolved_style: Some(cell_style.clone()),
4651 source_location: cell.source_location.clone(),
4652 href: None,
4653 bookmark: cell.bookmark.clone(),
4654 alt: None,
4655 is_header_row: is_header,
4656 actual_text: None,
4657 list_numbering: None,
4658 col_span: span as u32,
4659 overflow: Overflow::default(),
4660 opacity: 1.0,
4661 });
4662 }
4663
4664 let row_children: Vec<LayoutElement> = drain_since(&mut cursor.elements, row_snapshot);
4666 let row_element = LayoutElement {
4667 x: start_x,
4668 y: row_y,
4669 width: total_width,
4670 height: row_height,
4671 draw: if let Some(bg) = row_style.background_color {
4672 DrawCommand::Rect {
4673 background: Some(bg),
4674 border_width: Edges::default(),
4675 border_color: EdgeValues::uniform(Color::BLACK),
4676 border_style: EdgeValues::uniform(crate::style::BorderStyle::Solid),
4677 border_radius: CornerValues::uniform(0.0),
4678 opacity: 1.0,
4679 box_shadow: row_style.box_shadow.map(Box::new),
4680 background_gradient: row_style.background.clone().map(Box::new),
4681 }
4682 } else {
4683 DrawCommand::None
4684 },
4685 children: row_children,
4686 node_type: Some("TableRow".to_string()),
4687 resolved_style: Some(row_style.clone()),
4688 source_location: row.source_location.clone(),
4689 href: None,
4690 bookmark: row.bookmark.clone(),
4691 alt: None,
4692 is_header_row: is_header,
4693 actual_text: None,
4694 list_numbering: None,
4695 col_span: 1,
4696 overflow: row_style.overflow,
4697 opacity: row_style.opacity,
4698 };
4699
4700 if let Some(first_overflow) = all_overflow_pages.first_mut() {
4701 first_overflow.elements.push(row_element);
4711 } else {
4712 cursor.elements.push(row_element);
4713 cursor.y += row_height;
4714 }
4715
4716 pages.extend(all_overflow_pages);
4718 }
4719
4720 #[allow(clippy::too_many_arguments)]
4721 #[allow(clippy::too_many_arguments)]
4722 fn layout_text(
4723 &self,
4724 content: &str,
4725 href: Option<&str>,
4726 runs: &[TextRun],
4727 style: &ResolvedStyle,
4728 cursor: &mut PageCursor,
4729 pages: &mut Vec<LayoutPage>,
4730 x: f64,
4731 available_width: f64,
4732 font_context: &FontContext,
4733 source_location: Option<&SourceLocation>,
4734 bookmark: Option<&str>,
4735 node_type_override: Option<&str>,
4739 ) {
4740 if style.border_width.top > 0.0
4747 || style.border_width.right > 0.0
4748 || style.border_width.bottom > 0.0
4749 || style.border_width.left > 0.0
4750 {
4751 self.defect(
4752 "render defect: a border on a text node is not painted (wrap the text in a container element)".to_string(),
4753 );
4754 }
4755 if style.background_color.is_some() {
4756 self.defect(
4757 "render defect: a background on a text node is not painted (wrap the text in a container element)".to_string(),
4758 );
4759 }
4760 let margin = &style.margin.to_edges();
4761 let text_x = x + margin.left;
4762 let text_width = match style.width {
4769 SizeConstraint::Fixed(w) => (w - margin.horizontal()).max(0.0),
4770 SizeConstraint::Auto => available_width - margin.horizontal(),
4771 };
4772 let text_width = text_width.min(style.max_width).max(style.min_width);
4783
4784 cursor.y += margin.top;
4785
4786 if !runs.is_empty() {
4788 self.layout_text_runs(
4789 runs,
4790 href,
4791 style,
4792 cursor,
4793 pages,
4794 text_x,
4795 text_width,
4796 font_context,
4797 source_location,
4798 bookmark,
4799 node_type_override,
4800 );
4801 cursor.y += margin.bottom;
4802 return;
4803 }
4804
4805 let content = substitute_page_placeholders(content);
4806 let transformed = apply_text_transform(&content, style.text_transform);
4807 let justify = matches!(style.text_align, TextAlign::Justify);
4808 let lines = match style.line_breaking {
4809 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
4810 font_context,
4811 &transformed,
4812 text_width,
4813 style.font_size,
4814 &style.font_family,
4815 style.font_weight,
4816 style.font_style,
4817 style.letter_spacing,
4818 style.word_spacing,
4819 style.hyphens,
4820 style.lang.as_deref(),
4821 justify,
4822 ),
4823 LineBreaking::Greedy => self.text_layout.break_into_lines(
4824 font_context,
4825 &transformed,
4826 text_width,
4827 style.font_size,
4828 &style.font_family,
4829 style.font_weight,
4830 style.font_style,
4831 style.letter_spacing,
4832 style.word_spacing,
4833 style.hyphens,
4834 style.lang.as_deref(),
4835 ),
4836 };
4837
4838 let lines = match style.text_overflow {
4840 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
4841 font_context,
4842 lines,
4843 text_width,
4844 style.font_size,
4845 &style.font_family,
4846 style.font_weight,
4847 style.font_style,
4848 style.letter_spacing,
4849 style.word_spacing,
4850 ),
4851 TextOverflow::Clip => self.text_layout.truncate_clip(
4852 font_context,
4853 lines,
4854 text_width,
4855 style.font_size,
4856 &style.font_family,
4857 style.font_weight,
4858 style.font_style,
4859 style.letter_spacing,
4860 style.word_spacing,
4861 ),
4862 TextOverflow::Wrap => lines,
4863 };
4864
4865 let line_height = style.font_size * style.line_height;
4866
4867 let line_heights: Vec<f64> = vec![line_height; lines.len()];
4869 let decision = page_break::decide_break(
4870 cursor.remaining_height(),
4871 &line_heights,
4872 true,
4873 style.min_orphan_lines as usize,
4874 style.min_widow_lines as usize,
4875 );
4876
4877 let mut snapshot = cursor.elements.len();
4879 let mut container_start_y = cursor.content_y + cursor.y;
4880 let mut is_first_element = true;
4881
4882 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
4884 pages.push(cursor.finalize());
4885 *cursor = cursor.new_page();
4886 snapshot = cursor.elements.len();
4887 container_start_y = cursor.content_y + cursor.y;
4888 }
4889
4890 let forced_break_at = match decision {
4892 page_break::BreakDecision::Split {
4893 items_on_current_page,
4894 } => Some(items_on_current_page),
4895 _ => None,
4896 };
4897 let mut first_break_done = false;
4898
4899 for (line_idx, line) in lines.iter().enumerate() {
4900 let needs_break = if let Some(break_at) = forced_break_at {
4902 if !first_break_done && line_idx == break_at {
4903 true
4904 } else {
4905 line_height > cursor.remaining_height()
4906 }
4907 } else {
4908 line_height > cursor.remaining_height()
4909 };
4910
4911 if needs_break {
4912 first_break_done = true;
4913 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
4915 if !line_elements.is_empty() {
4916 let container_height = cursor.content_y + cursor.y - container_start_y;
4917 cursor.elements.push(LayoutElement {
4918 x: text_x,
4919 y: container_start_y,
4920 width: text_width,
4921 height: container_height,
4922 draw: DrawCommand::None,
4923 children: line_elements,
4924 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
4925 resolved_style: Some(style.clone()),
4926 source_location: source_location.cloned(),
4927 href: href.map(|s| s.to_string()),
4928 bookmark: if is_first_element {
4929 bookmark.map(|s| s.to_string())
4930 } else {
4931 None
4932 },
4933 alt: None,
4934 is_header_row: false,
4935 actual_text: None,
4936 list_numbering: None,
4937 col_span: 1,
4938 overflow: Overflow::default(),
4939 opacity: 1.0,
4940 });
4941 is_first_element = false;
4942 }
4943
4944 pages.push(cursor.finalize());
4945 *cursor = cursor.new_page();
4946
4947 snapshot = cursor.elements.len();
4949 container_start_y = cursor.content_y + cursor.y;
4950 }
4951
4952 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
4953
4954 let rendered_width = if glyphs.is_empty() {
4958 line.width
4959 } else {
4960 let last = &glyphs[glyphs.len() - 1];
4961 (last.x_offset + last.x_advance).max(line.width * 0.5)
4962 };
4963
4964 let line_x = match style.text_align {
4965 TextAlign::Left => text_x,
4966 TextAlign::Right => text_x + text_width - rendered_width,
4967 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4968 TextAlign::Justify => text_x,
4969 };
4970
4971 let is_last_line = line_idx == lines.len() - 1;
4979 let user_ws = style.word_spacing;
4980 let (justified_width, word_spacing) =
4981 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4982 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4983 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4984 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4985 let s = glyphs[..=idx]
4986 .iter()
4987 .filter(|g| g.char_value == ' ')
4988 .count();
4989 (w, s)
4990 } else {
4991 (0.0, 0)
4992 };
4993 let slack = text_width - natural_width;
4994 let ws = if space_count > 0 && slack.abs() > 0.01 {
4995 slack / space_count as f64
4996 } else {
4997 0.0
4998 };
4999 (text_width, user_ws + ws)
5000 } else {
5001 (rendered_width, user_ws)
5002 };
5003
5004 let text_line = TextLine {
5005 x: line_x,
5006 y: cursor.content_y
5012 + cursor.y
5013 + baseline_in_line(
5014 line_height,
5015 style.font_size,
5016 font_context.baseline_metrics(
5017 &style.font_family,
5018 style.font_weight,
5019 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5020 ),
5021 ),
5022 glyphs,
5023 width: justified_width,
5024 height: line_height,
5025 word_spacing,
5026 };
5027
5028 cursor.elements.push(LayoutElement {
5029 x: line_x,
5030 y: cursor.content_y + cursor.y,
5031 width: justified_width,
5032 height: line_height,
5033 draw: DrawCommand::Text {
5034 lines: vec![text_line],
5035 color: style.color,
5036 text_decoration: style.text_decoration,
5037 opacity: 1.0,
5038 },
5039 children: vec![],
5040 node_type: Some("TextLine".to_string()),
5041 resolved_style: Some(style.clone()),
5042 source_location: None,
5043 href: href.map(|s| s.to_string()),
5044 bookmark: None,
5045 alt: None,
5046 is_header_row: false,
5047 actual_text: None,
5048 list_numbering: None,
5049 col_span: 1,
5050 overflow: Overflow::default(),
5051 opacity: 1.0,
5052 });
5053
5054 cursor.y += line_height;
5055 }
5056
5057 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5059 if !line_elements.is_empty() {
5060 let container_height = cursor.content_y + cursor.y - container_start_y;
5061 cursor.elements.push(LayoutElement {
5062 x: text_x,
5063 y: container_start_y,
5064 width: text_width,
5065 height: container_height,
5066 draw: DrawCommand::None,
5067 children: line_elements,
5068 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5069 resolved_style: Some(style.clone()),
5070 source_location: source_location.cloned(),
5071 href: href.map(|s| s.to_string()),
5072 bookmark: if is_first_element {
5073 bookmark.map(|s| s.to_string())
5074 } else {
5075 None
5076 },
5077 alt: None,
5078 is_header_row: false,
5079 actual_text: None,
5080 list_numbering: None,
5081 col_span: 1,
5082 overflow: Overflow::default(),
5083 opacity: 1.0,
5084 });
5085 }
5086
5087 cursor.y += margin.bottom;
5088 }
5089
5090 #[allow(clippy::too_many_arguments)]
5092 #[allow(clippy::too_many_arguments)]
5093 fn layout_text_runs(
5094 &self,
5095 runs: &[TextRun],
5096 parent_href: Option<&str>,
5097 style: &ResolvedStyle,
5098 cursor: &mut PageCursor,
5099 pages: &mut Vec<LayoutPage>,
5100 text_x: f64,
5101 text_width: f64,
5102 font_context: &FontContext,
5103 source_location: Option<&SourceLocation>,
5104 bookmark: Option<&str>,
5105 node_type_override: Option<&str>,
5107 ) {
5108 let mut styled_chars: Vec<StyledChar> = Vec::new();
5110 for run in runs {
5111 let run_style = run.style.resolve(Some(style), text_width);
5112 let run_href = run.href.as_deref().or(parent_href);
5113 let transform = run_style.text_transform;
5114 let run_content = substitute_page_placeholders(&run.content);
5115 let mut prev_is_whitespace = true;
5116 for ch in run_content.chars() {
5117 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
5118 prev_is_whitespace = ch.is_whitespace();
5119 styled_chars.push(StyledChar {
5120 ch: transformed_ch,
5121 font_family: run_style.font_family.clone(),
5122 font_size: run_style.font_size,
5123 font_weight: run_style.font_weight,
5124 font_style: run_style.font_style,
5125 color: run_style.color,
5126 href: run_href.map(|s| s.to_string()),
5127 text_decoration: run_style.text_decoration,
5128 letter_spacing: run_style.letter_spacing,
5129 word_spacing: run_style.word_spacing,
5130 });
5131 }
5132 }
5133
5134 let justify = matches!(style.text_align, TextAlign::Justify);
5136 let broken_lines = match style.line_breaking {
5137 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
5138 font_context,
5139 &styled_chars,
5140 text_width,
5141 style.hyphens,
5142 style.lang.as_deref(),
5143 justify,
5144 ),
5145 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
5146 font_context,
5147 &styled_chars,
5148 text_width,
5149 style.hyphens,
5150 style.lang.as_deref(),
5151 ),
5152 };
5153
5154 let broken_lines = match style.text_overflow {
5156 TextOverflow::Ellipsis => {
5157 self.text_layout
5158 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
5159 }
5160 TextOverflow::Clip => {
5161 self.text_layout
5162 .truncate_runs_clip(font_context, broken_lines, text_width)
5163 }
5164 TextOverflow::Wrap => broken_lines,
5165 };
5166
5167 let line_height = style.font_size * style.line_height;
5168
5169 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
5171 let decision = page_break::decide_break(
5172 cursor.remaining_height(),
5173 &line_heights,
5174 true,
5175 style.min_orphan_lines as usize,
5176 style.min_widow_lines as usize,
5177 );
5178
5179 let mut snapshot = cursor.elements.len();
5180 let mut container_start_y = cursor.content_y + cursor.y;
5181 let mut is_first_element = true;
5182
5183 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
5184 pages.push(cursor.finalize());
5185 *cursor = cursor.new_page();
5186 snapshot = cursor.elements.len();
5187 container_start_y = cursor.content_y + cursor.y;
5188 }
5189
5190 let forced_break_at = match decision {
5191 page_break::BreakDecision::Split {
5192 items_on_current_page,
5193 } => Some(items_on_current_page),
5194 _ => None,
5195 };
5196 let mut first_break_done = false;
5197
5198 for (line_idx, run_line) in broken_lines.iter().enumerate() {
5199 let needs_break = if let Some(break_at) = forced_break_at {
5200 if !first_break_done && line_idx == break_at {
5201 true
5202 } else {
5203 line_height > cursor.remaining_height()
5204 }
5205 } else {
5206 line_height > cursor.remaining_height()
5207 };
5208
5209 if needs_break {
5210 first_break_done = true;
5211 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5212 if !line_elements.is_empty() {
5213 let container_height = cursor.content_y + cursor.y - container_start_y;
5214 cursor.elements.push(LayoutElement {
5215 x: text_x,
5216 y: container_start_y,
5217 width: text_width,
5218 height: container_height,
5219 draw: DrawCommand::None,
5220 children: line_elements,
5221 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5222 resolved_style: Some(style.clone()),
5223 source_location: source_location.cloned(),
5224 href: parent_href.map(|s| s.to_string()),
5225 bookmark: if is_first_element {
5226 bookmark.map(|s| s.to_string())
5227 } else {
5228 None
5229 },
5230 alt: None,
5231 is_header_row: false,
5232 actual_text: None,
5233 list_numbering: None,
5234 col_span: 1,
5235 overflow: Overflow::default(),
5236 opacity: 1.0,
5237 });
5238 is_first_element = false;
5239 }
5240
5241 pages.push(cursor.finalize());
5242 *cursor = cursor.new_page();
5243
5244 snapshot = cursor.elements.len();
5245 container_start_y = cursor.content_y + cursor.y;
5246 }
5247
5248 let line_x = match style.text_align {
5249 TextAlign::Left => text_x,
5250 TextAlign::Right => text_x + text_width - run_line.width,
5251 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
5252 TextAlign::Justify => text_x,
5253 };
5254
5255 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
5256
5257 let is_last_line = line_idx == broken_lines.len() - 1;
5264 let user_ws = style.word_spacing;
5265 let (justified_width, word_spacing) =
5266 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5267 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
5268 let (natural_width, space_count) = if let Some(idx) = last_non_space {
5269 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
5270 let s = glyphs[..=idx]
5271 .iter()
5272 .filter(|g| g.char_value == ' ')
5273 .count();
5274 (w, s)
5275 } else {
5276 (0.0, 0)
5277 };
5278 let slack = text_width - natural_width;
5279 let ws = if space_count > 0 && slack.abs() > 0.01 {
5280 slack / space_count as f64
5281 } else {
5282 0.0
5283 };
5284 (text_width, user_ws + ws)
5285 } else {
5286 (run_line.width, user_ws)
5287 };
5288
5289 let text_line = TextLine {
5290 x: line_x,
5291 y: cursor.content_y
5297 + cursor.y
5298 + baseline_in_line(
5299 line_height,
5300 style.font_size,
5301 font_context.baseline_metrics(
5302 &style.font_family,
5303 style.font_weight,
5304 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5305 ),
5306 ),
5307 glyphs,
5308 width: justified_width,
5309 height: line_height,
5310 word_spacing,
5311 };
5312
5313 let text_dec = run_line
5315 .chars
5316 .iter()
5317 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
5318 .map(|sc| sc.text_decoration)
5319 .unwrap_or(style.text_decoration);
5320
5321 cursor.elements.push(LayoutElement {
5322 x: line_x,
5323 y: cursor.content_y + cursor.y,
5324 width: justified_width,
5325 height: line_height,
5326 draw: DrawCommand::Text {
5327 lines: vec![text_line],
5328 color: style.color,
5329 text_decoration: text_dec,
5330 opacity: 1.0,
5331 },
5332 children: vec![],
5333 node_type: Some("TextLine".to_string()),
5334 resolved_style: Some(style.clone()),
5335 source_location: None,
5336 href: parent_href.map(|s| s.to_string()),
5337 bookmark: None,
5338 alt: None,
5339 is_header_row: false,
5340 actual_text: None,
5341 list_numbering: None,
5342 col_span: 1,
5343 overflow: Overflow::default(),
5344 opacity: 1.0,
5345 });
5346
5347 cursor.y += line_height;
5348 }
5349
5350 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5351 if !line_elements.is_empty() {
5352 let container_height = cursor.content_y + cursor.y - container_start_y;
5353 cursor.elements.push(LayoutElement {
5354 x: text_x,
5355 y: container_start_y,
5356 width: text_width,
5357 height: container_height,
5358 draw: DrawCommand::None,
5359 children: line_elements,
5360 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5361 resolved_style: Some(style.clone()),
5362 source_location: source_location.cloned(),
5363 href: parent_href.map(|s| s.to_string()),
5364 bookmark: if is_first_element {
5365 bookmark.map(|s| s.to_string())
5366 } else {
5367 None
5368 },
5369 alt: None,
5370 is_header_row: false,
5371 actual_text: None,
5372 list_numbering: None,
5373 col_span: 1,
5374 overflow: Overflow::default(),
5375 opacity: 1.0,
5376 });
5377 }
5378 }
5379
5380 fn build_positioned_glyphs_single_style(
5384 &self,
5385 line: &BrokenLine,
5386 style: &ResolvedStyle,
5387 href: Option<&str>,
5388 font_context: &FontContext,
5389 ) -> Vec<PositionedGlyph> {
5390 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5391 let line_text: String = line.chars.iter().collect();
5392 let direction = style.direction;
5393 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5395
5396 let font_runs = crate::font::fallback::segment_by_font(
5399 &line.chars,
5400 &style.font_family,
5401 style.font_weight,
5402 italic,
5403 font_context.registry(),
5404 );
5405 let needs_per_char_fallback = font_runs.len() > 1
5406 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
5407
5408 if needs_per_char_fallback {
5410 let bidi_runs = if has_bidi {
5411 bidi::analyze_bidi(&line_text, direction)
5412 } else {
5413 vec![crate::text::bidi::BidiRun {
5414 char_start: 0,
5415 char_end: line.chars.len(),
5416 level: unicode_bidi::Level::ltr(),
5417 is_rtl: false,
5418 }]
5419 };
5420
5421 let mut all_glyphs = Vec::new();
5422 let mut bidi_levels = Vec::new();
5423 let mut x = 0.0_f64;
5424
5425 for bidi_run in &bidi_runs {
5427 for font_run in &font_runs {
5429 let start = font_run.start.max(bidi_run.char_start);
5431 let end = font_run.end.min(bidi_run.char_end);
5432 if start >= end {
5433 continue;
5434 }
5435
5436 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
5437 let sub_text: String = sub_chars.iter().collect();
5438 let resolved_family = &font_run.family;
5439
5440 if let Some(font_data) =
5441 font_context.font_data(resolved_family, style.font_weight, italic)
5442 {
5443 if let Some(shaped) = shaping::shape_text_with_direction(
5444 &sub_text,
5445 font_data,
5446 bidi_run.is_rtl,
5447 ) {
5448 let units_per_em = font_context.units_per_em(
5449 resolved_family,
5450 style.font_weight,
5451 italic,
5452 );
5453 let scale = style.font_size / units_per_em as f64;
5454
5455 let clusters = cluster_texts(&shaped, &sub_chars);
5456 for (sg, (cluster_text, ligature, extraction_text)) in
5457 shaped.iter().zip(clusters)
5458 {
5459 let cluster = sg.cluster as usize;
5460 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
5461
5462 let glyph_x = x + sg.x_offset as f64 * scale;
5463 let glyph_y = sg.y_offset as f64 * scale;
5464 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5465
5466 all_glyphs.push(PositionedGlyph {
5467 glyph_id: sg.glyph_id,
5468 x_offset: glyph_x,
5469 y_offset: glyph_y,
5470 x_advance: advance,
5471 font_size: style.font_size,
5472 font_family: Arc::from(resolved_family.as_str()),
5473 font_weight: style.font_weight,
5474 font_style: style.font_style,
5475 char_value,
5476 color: Some(style.color),
5477 href: href.map(|s| s.to_string()),
5478 text_decoration: style.text_decoration,
5479 letter_spacing: style.letter_spacing,
5480 cluster_text,
5481 extraction_text,
5482
5483 ligature,
5484 });
5485 bidi_levels.push(bidi_run.level);
5486 x += advance;
5487 }
5488 continue;
5489 }
5490 }
5491
5492 for i in start..end {
5494 let ch = line.chars[i];
5495 let glyph_x = x;
5496 let char_width = font_context.char_width(
5497 ch,
5498 resolved_family,
5499 style.font_weight,
5500 italic,
5501 style.font_size,
5502 );
5503 let advance = char_width + style.letter_spacing;
5504 all_glyphs.push(PositionedGlyph {
5505 glyph_id: ch as u16,
5506 x_offset: glyph_x,
5507 y_offset: 0.0,
5508 x_advance: advance,
5509 font_size: style.font_size,
5510 font_family: Arc::from(resolved_family.as_str()),
5511 font_weight: style.font_weight,
5512 font_style: style.font_style,
5513 char_value: ch,
5514 color: Some(style.color),
5515 href: href.map(|s| s.to_string()),
5516 text_decoration: style.text_decoration,
5517 letter_spacing: style.letter_spacing,
5518 cluster_text: None,
5519 extraction_text: None,
5520 ligature: false,
5521 });
5522 bidi_levels.push(bidi_run.level);
5523 x += advance;
5524 }
5525 }
5526 }
5527
5528 if has_bidi && !all_glyphs.is_empty() {
5530 all_glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5531 }
5532 return all_glyphs;
5533 }
5534
5535 if let Some(font_data) =
5538 font_context.font_data(&style.font_family, style.font_weight, italic)
5539 {
5540 if has_bidi {
5541 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5543 let units_per_em =
5544 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5545 let scale = style.font_size / units_per_em as f64;
5546
5547 let mut all_glyphs = Vec::new();
5548 let mut bidi_levels = Vec::new();
5549 let mut x = 0.0_f64;
5550
5551 for run in &bidi_runs {
5552 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5553 let run_text: String = run_chars.iter().collect();
5554
5555 if let Some(shaped) =
5556 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5557 {
5558 let clusters = cluster_texts(&shaped, &run_chars);
5559 for (sg, (cluster_text, ligature, extraction_text)) in
5560 shaped.iter().zip(clusters)
5561 {
5562 let cluster = sg.cluster as usize;
5563 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5564
5565 let glyph_x = x + sg.x_offset as f64 * scale;
5566 let glyph_y = sg.y_offset as f64 * scale;
5567 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5568
5569 all_glyphs.push(PositionedGlyph {
5570 glyph_id: sg.glyph_id,
5571 x_offset: glyph_x,
5572 y_offset: glyph_y,
5573 x_advance: advance,
5574 font_size: style.font_size,
5575 font_family: Arc::from(style.font_family.as_str()),
5576 font_weight: style.font_weight,
5577 font_style: style.font_style,
5578 char_value,
5579 color: Some(style.color),
5580 href: href.map(|s| s.to_string()),
5581 text_decoration: style.text_decoration,
5582 letter_spacing: style.letter_spacing,
5583 cluster_text,
5584 extraction_text,
5585
5586 ligature,
5587 });
5588 bidi_levels.push(run.level);
5589
5590 x += advance;
5591 }
5592 }
5593 }
5594
5595 let glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5597 return glyphs;
5598 }
5599
5600 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5602 let units_per_em =
5603 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5604 let scale = style.font_size / units_per_em as f64;
5605
5606 return self.shaped_glyphs_to_positioned(
5607 &shaped,
5608 &line.chars,
5609 &line.char_positions,
5610 scale,
5611 style.font_size,
5612 &style.font_family,
5613 style.font_weight,
5614 style.font_style,
5615 Some(style.color),
5616 href,
5617 style.text_decoration,
5618 style.letter_spacing,
5619 );
5620 }
5621 }
5622
5623 let mut glyphs: Vec<PositionedGlyph> = line
5625 .chars
5626 .iter()
5627 .enumerate()
5628 .map(|(j, ch)| {
5629 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5630 let char_width = font_context.char_width(
5631 *ch,
5632 &style.font_family,
5633 style.font_weight,
5634 italic,
5635 style.font_size,
5636 );
5637 PositionedGlyph {
5638 glyph_id: *ch as u16,
5639 x_offset: glyph_x,
5640 y_offset: 0.0,
5641 x_advance: char_width,
5642 font_size: style.font_size,
5643 font_family: Arc::from(style.font_family.as_str()),
5644 font_weight: style.font_weight,
5645 font_style: style.font_style,
5646 char_value: *ch,
5647 color: Some(style.color),
5648 href: href.map(|s| s.to_string()),
5649 text_decoration: style.text_decoration,
5650 letter_spacing: style.letter_spacing,
5651 cluster_text: None,
5652 extraction_text: None,
5653 ligature: false,
5654 }
5655 })
5656 .collect();
5657
5658 if has_bidi && !glyphs.is_empty() {
5660 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5661 let mut levels = Vec::with_capacity(glyphs.len());
5662 let mut char_idx = 0;
5663 for run in &bidi_runs {
5664 for _ in run.char_start..run.char_end {
5665 if char_idx < glyphs.len() {
5666 levels.push(run.level);
5667 char_idx += 1;
5668 }
5669 }
5670 }
5671 while levels.len() < glyphs.len() {
5673 levels.push(unicode_bidi::Level::ltr());
5674 }
5675 glyphs = bidi::reorder_and_position(glyphs, &levels);
5676 }
5677
5678 glyphs
5679 }
5680
5681 fn build_positioned_glyphs_runs(
5686 &self,
5687 run_line: &RunBrokenLine,
5688 font_context: &FontContext,
5689 direction: Direction,
5690 ) -> Vec<PositionedGlyph> {
5691 let chars = &run_line.chars;
5692 if chars.is_empty() {
5693 return vec![];
5694 }
5695
5696 let resolved_families = crate::text::resolved_style_families(chars, font_context);
5698
5699 let line_text: String = chars.iter().map(|c| c.ch).collect();
5700 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5701 let mut char_levels = vec![unicode_bidi::Level::ltr(); chars.len()];
5706 if has_bidi {
5707 for run in bidi::analyze_bidi(&line_text, direction) {
5708 for level in &mut char_levels[run.char_start..run.char_end.min(chars.len())] {
5709 *level = run.level;
5710 }
5711 }
5712 }
5713
5714 let mut glyphs = Vec::new();
5715 let mut bidi_levels = Vec::new();
5716 let mut i = 0;
5717
5718 while i < chars.len() {
5719 let sc = &chars[i];
5720 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5721 let resolved_family = &resolved_families[i];
5722 let level = char_levels[i];
5723 let is_rtl = level.is_rtl();
5724
5725 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5727 {
5728 let run_start = i;
5730 let mut run_end = i + 1;
5731 while run_end < chars.len() {
5732 let next = &chars[run_end];
5733 let next_italic =
5734 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5735 if resolved_families[run_end] == *resolved_family
5737 && next.font_weight == sc.font_weight
5738 && next_italic == italic
5739 && (next.font_size - sc.font_size).abs() < 0.001
5740 && char_levels[run_end] == level
5741 {
5742 run_end += 1;
5743 } else {
5744 break;
5745 }
5746 }
5747
5748 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5749 if let Some(shaped) =
5750 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5751 {
5752 let units_per_em =
5753 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5754 let scale = sc.font_size / units_per_em as f64;
5755
5756 let run_chars: Vec<char> =
5758 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5759 let run_positions: Vec<f64> = (run_start..run_end)
5760 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5761 .collect();
5762
5763 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5765 &shaped,
5766 &chars[run_start..run_end],
5767 &run_chars,
5768 &run_positions,
5769 scale,
5770 );
5771 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5773 for g in &mut run_glyphs {
5774 g.font_family = resolved_family_arc.clone();
5775 }
5776 for _ in &run_glyphs {
5778 bidi_levels.push(level);
5779 }
5780 glyphs.extend(run_glyphs);
5781 i = run_end;
5782 continue;
5783 }
5784 }
5785
5786 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5788 let char_width = font_context.char_width(
5789 sc.ch,
5790 resolved_family,
5791 sc.font_weight,
5792 italic,
5793 sc.font_size,
5794 );
5795 glyphs.push(PositionedGlyph {
5796 glyph_id: sc.ch as u16,
5797 x_offset: glyph_x,
5798 y_offset: 0.0,
5799 x_advance: char_width,
5800 font_size: sc.font_size,
5801 font_family: Arc::from(resolved_family.as_str()),
5802 font_weight: sc.font_weight,
5803 font_style: sc.font_style,
5804 char_value: sc.ch,
5805 color: Some(sc.color),
5806 href: sc.href.clone(),
5807 text_decoration: sc.text_decoration,
5808 letter_spacing: sc.letter_spacing,
5809 cluster_text: None,
5810 extraction_text: None,
5811 ligature: false,
5812 });
5813 bidi_levels.push(level);
5814 i += 1;
5815 }
5816
5817 if has_bidi && !glyphs.is_empty() {
5819 glyphs = bidi::reorder_and_position(glyphs, &bidi_levels);
5820 }
5821
5822 glyphs
5823 }
5824
5825 #[allow(clippy::too_many_arguments)]
5827 fn shaped_glyphs_to_positioned(
5828 &self,
5829 shaped: &[shaping::ShapedGlyph],
5830 chars: &[char],
5831 char_positions: &[f64],
5832 scale: f64,
5833 font_size: f64,
5834 font_family: &str,
5835 font_weight: u32,
5836 font_style: FontStyle,
5837 color: Option<Color>,
5838 href: Option<&str>,
5839 text_decoration: TextDecoration,
5840 letter_spacing: f64,
5841 ) -> Vec<PositionedGlyph> {
5842 let mut result = Vec::with_capacity(shaped.len());
5843 let mut x = 0.0_f64;
5844 let mut cluster_pen = 0.0_f64;
5845 let mut prev_cluster: Option<usize> = None;
5846
5847 let clusters = cluster_texts(shaped, chars);
5848 for (sg, (cluster_text, ligature, extraction_text)) in shaped.iter().zip(clusters) {
5849 let cluster = sg.cluster as usize;
5850 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5851
5852 if prev_cluster != Some(cluster) {
5853 cluster_pen = x;
5854 prev_cluster = Some(cluster);
5855 }
5856 let glyph_x = shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale);
5857 let glyph_y = sg.y_offset as f64 * scale;
5858 let advance = sg.x_advance as f64 * scale + letter_spacing;
5859
5860 result.push(PositionedGlyph {
5861 glyph_id: sg.glyph_id,
5862 x_offset: glyph_x,
5863 y_offset: glyph_y,
5864 x_advance: advance,
5865 font_size,
5866 font_family: Arc::from(font_family),
5867 font_weight,
5868 font_style,
5869 char_value,
5870 color,
5871 href: href.map(|s| s.to_string()),
5872 text_decoration,
5873 letter_spacing,
5874 cluster_text,
5875 extraction_text,
5876
5877 ligature,
5878 });
5879
5880 x += advance;
5881 }
5882
5883 result
5884 }
5885
5886 fn shaped_glyphs_to_positioned_runs(
5888 &self,
5889 shaped: &[shaping::ShapedGlyph],
5890 styled_chars: &[StyledChar],
5891 chars: &[char],
5892 char_positions: &[f64],
5893 scale: f64,
5894 ) -> Vec<PositionedGlyph> {
5895 let mut result = Vec::with_capacity(shaped.len());
5896 let base_x = char_positions.first().copied().unwrap_or(0.0);
5899 let mut x = 0.0_f64;
5900 let mut cluster_pen = 0.0_f64;
5901 let mut prev_cluster: Option<usize> = None;
5902
5903 let clusters = cluster_texts(shaped, chars);
5904 for (sg, (cluster_text, ligature, extraction_text)) in shaped.iter().zip(clusters) {
5905 let cluster = sg.cluster as usize;
5906 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5907 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5908
5909 if prev_cluster != Some(cluster) {
5910 cluster_pen = x;
5911 prev_cluster = Some(cluster);
5912 }
5913 let glyph_x = if char_positions.get(cluster).is_some() {
5914 shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale)
5915 } else {
5916 base_x + x + sg.x_offset as f64 * scale
5917 };
5918 let glyph_y = sg.y_offset as f64 * scale;
5919 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5920
5921 result.push(PositionedGlyph {
5922 glyph_id: sg.glyph_id,
5923 x_offset: glyph_x,
5924 y_offset: glyph_y,
5925 x_advance: advance,
5926 font_size: sc.font_size,
5927 font_family: Arc::from(sc.font_family.as_str()),
5928 font_weight: sc.font_weight,
5929 font_style: sc.font_style,
5930 char_value,
5931 color: Some(sc.color),
5932 href: sc.href.clone(),
5933 text_decoration: sc.text_decoration,
5934 letter_spacing: sc.letter_spacing,
5935 cluster_text,
5936 extraction_text,
5937
5938 ligature,
5939 });
5940
5941 x += advance;
5942 }
5943
5944 result
5945 }
5946
5947 fn image_display_size(
5957 &self,
5958 src: &str,
5959 style: &ResolvedStyle,
5960 explicit_width: Option<f64>,
5961 explicit_height: Option<f64>,
5962 available_width: f64,
5963 ) -> (f64, f64) {
5964 let dims = if src.is_empty() {
5965 None
5966 } else {
5967 self.get_image_dimensions(src)
5968 };
5969 let aspect = dims
5970 .map(|(w, h)| {
5971 if w > 0 {
5972 f64::from(h) / f64::from(w)
5973 } else {
5974 0.75
5975 }
5976 })
5977 .unwrap_or(0.75);
5978
5979 let style_w = match style.width {
5980 SizeConstraint::Fixed(w) => Some(w),
5981 SizeConstraint::Auto => None,
5982 };
5983 let style_h = match style.height {
5984 SizeConstraint::Fixed(h) => Some(h),
5985 SizeConstraint::Auto => None,
5986 };
5987 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
5988
5989 let width_source = style_w.or(explicit_width);
5990 let height_source = style_h.or(explicit_height);
5991 match (width_source, height_source) {
5992 (Some(w), Some(h)) => (clamp(w), h),
5993 (Some(w), None) => {
5994 let w = clamp(w);
5995 (w, w * aspect)
5996 }
5997 (None, Some(h)) => (clamp(h / aspect), h),
5998 (None, None) => {
5999 let w = clamp(
6002 dims.map(|(w, _)| f64::from(w))
6003 .unwrap_or(available_width)
6004 .min(available_width),
6005 );
6006 (w, w * aspect)
6007 }
6008 }
6009 }
6010
6011 #[allow(clippy::too_many_arguments)]
6012 fn layout_image(
6013 &self,
6014 node: &Node,
6015 style: &ResolvedStyle,
6016 cursor: &mut PageCursor,
6017 pages: &mut Vec<LayoutPage>,
6018 x: f64,
6019 available_width: f64,
6020 explicit_width: Option<f64>,
6021 explicit_height: Option<f64>,
6022 ) {
6023 let margin = &style.margin.to_edges();
6024
6025 let src = match &node.kind {
6027 NodeKind::Image { src, .. } => src.as_str(),
6028 _ => "",
6029 };
6030
6031 let loaded = if !src.is_empty() {
6032 crate::image_loader::load_image(src).ok()
6033 } else {
6034 None
6035 };
6036
6037 let (img_width, img_height) = self.image_display_size(
6038 src,
6039 style,
6040 explicit_width,
6041 explicit_height,
6042 available_width - margin.horizontal(),
6043 );
6044
6045 let total_height = img_height + margin.vertical();
6046
6047 if total_height > cursor.remaining_height() {
6048 pages.push(cursor.finalize());
6049 *cursor = cursor.new_page();
6050 }
6051
6052 cursor.y += margin.top;
6053
6054 let draw = if let Some(image_data) = loaded {
6055 DrawCommand::Image { image_data }
6056 } else {
6057 DrawCommand::ImagePlaceholder
6058 };
6059
6060 cursor.elements.push(LayoutElement {
6061 x: x + margin.left,
6062 y: cursor.content_y + cursor.y,
6063 width: img_width,
6064 height: img_height,
6065 draw,
6066 children: vec![],
6067 node_type: Some(node_kind_name(&node.kind).to_string()),
6068 resolved_style: Some(style.clone()),
6069 source_location: node.source_location.clone(),
6070 href: node.href.clone(),
6071 bookmark: node.bookmark.clone(),
6072 alt: node.alt.clone(),
6073 is_header_row: false,
6074 actual_text: None,
6075 list_numbering: None,
6076 col_span: 1,
6077 overflow: style.overflow,
6078 opacity: style.opacity,
6079 });
6080
6081 cursor.y += img_height + margin.bottom;
6082 }
6083
6084 #[allow(clippy::too_many_arguments)]
6086 fn layout_svg(
6087 &self,
6088 node: &Node,
6089 style: &ResolvedStyle,
6090 cursor: &mut PageCursor,
6091 pages: &mut Vec<LayoutPage>,
6092 x: f64,
6093 _available_width: f64,
6094 svg_width: f64,
6095 svg_height: f64,
6096 view_box: Option<&str>,
6097 content: &str,
6098 ) {
6099 let margin = &style.margin.to_edges();
6100 let total_height = svg_height + margin.vertical();
6101
6102 if total_height > cursor.remaining_height() {
6103 pages.push(cursor.finalize());
6104 *cursor = cursor.new_page();
6105 }
6106
6107 cursor.y += margin.top;
6108
6109 let vb = view_box
6110 .and_then(crate::svg::parse_view_box)
6111 .unwrap_or(crate::svg::ViewBox {
6112 min_x: 0.0,
6113 min_y: 0.0,
6114 width: svg_width,
6115 height: svg_height,
6116 });
6117
6118 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
6119
6120 cursor.elements.push(LayoutElement {
6121 x: x + margin.left,
6122 y: cursor.content_y + cursor.y,
6123 width: svg_width,
6124 height: svg_height,
6125 draw: DrawCommand::Svg {
6126 commands,
6127 width: svg_width,
6128 height: svg_height,
6129 viewbox_min_x: vb.min_x,
6130 viewbox_min_y: vb.min_y,
6131 viewbox_width: vb.width,
6132 viewbox_height: vb.height,
6133 clip: false,
6134 },
6135 children: vec![],
6136 node_type: Some("Svg".to_string()),
6137 resolved_style: Some(style.clone()),
6138 source_location: node.source_location.clone(),
6139 href: node.href.clone(),
6140 bookmark: node.bookmark.clone(),
6141 alt: node.alt.clone(),
6142 is_header_row: false,
6143 actual_text: None,
6144 list_numbering: None,
6145 col_span: 1,
6146 overflow: style.overflow,
6147 opacity: style.opacity,
6148 });
6149
6150 cursor.y += svg_height + margin.bottom;
6151 }
6152
6153 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
6155 use crate::svg::SvgCommand;
6156
6157 let mut commands = Vec::new();
6158 let mut cur_x = 0.0_f64;
6159 let mut cur_y = 0.0_f64;
6160
6161 for op in operations {
6162 match op {
6163 CanvasOp::MoveTo { x, y } => {
6164 commands.push(SvgCommand::MoveTo(*x, *y));
6165 cur_x = *x;
6166 cur_y = *y;
6167 }
6168 CanvasOp::LineTo { x, y } => {
6169 commands.push(SvgCommand::LineTo(*x, *y));
6170 cur_x = *x;
6171 cur_y = *y;
6172 }
6173 CanvasOp::BezierCurveTo {
6174 cp1x,
6175 cp1y,
6176 cp2x,
6177 cp2y,
6178 x,
6179 y,
6180 } => {
6181 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
6182 cur_x = *x;
6183 cur_y = *y;
6184 }
6185 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
6186 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
6188 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
6189 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
6190 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
6191 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
6192 cur_x = *x;
6193 cur_y = *y;
6194 }
6195 CanvasOp::ClosePath => {
6196 commands.push(SvgCommand::ClosePath);
6197 }
6198 CanvasOp::Rect {
6199 x,
6200 y,
6201 width,
6202 height,
6203 } => {
6204 commands.push(SvgCommand::MoveTo(*x, *y));
6205 commands.push(SvgCommand::LineTo(*x + *width, *y));
6206 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
6207 commands.push(SvgCommand::LineTo(*x, *y + *height));
6208 commands.push(SvgCommand::ClosePath);
6209 cur_x = *x;
6210 cur_y = *y;
6211 }
6212 CanvasOp::Circle { cx, cy, r } => {
6213 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
6214 }
6215 CanvasOp::Ellipse { cx, cy, rx, ry } => {
6216 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
6217 }
6218 CanvasOp::Arc {
6219 cx,
6220 cy,
6221 r,
6222 start_angle,
6223 end_angle,
6224 counterclockwise,
6225 } => {
6226 let steps = 32;
6230 let mut sweep = end_angle - start_angle;
6231 if !counterclockwise && sweep < 0.0 {
6232 sweep += 2.0 * std::f64::consts::PI;
6233 }
6234 if *counterclockwise && sweep > 0.0 {
6235 sweep -= 2.0 * std::f64::consts::PI;
6236 }
6237 for i in 0..=steps {
6238 let t = *start_angle + sweep * (i as f64 / steps as f64);
6239 let px = cx + r * t.cos();
6240 let py = cy + r * t.sin();
6241 if i == 0 {
6242 commands.push(SvgCommand::MoveTo(px, py));
6243 } else {
6244 commands.push(SvgCommand::LineTo(px, py));
6245 }
6246 }
6247 }
6248 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
6249 CanvasOp::Fill => commands.push(SvgCommand::Fill),
6250 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
6251 CanvasOp::SetFillColor { r, g, b } => {
6252 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
6254 }
6255 CanvasOp::SetStrokeColor { r, g, b } => {
6256 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
6257 }
6258 CanvasOp::SetLineWidth { width } => {
6259 commands.push(SvgCommand::SetStrokeWidth(*width));
6260 }
6261 CanvasOp::SetLineCap { cap } => {
6262 commands.push(SvgCommand::SetLineCap(*cap));
6263 }
6264 CanvasOp::SetLineJoin { join } => {
6265 commands.push(SvgCommand::SetLineJoin(*join));
6266 }
6267 CanvasOp::Save => commands.push(SvgCommand::SaveState),
6268 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
6269 }
6270 }
6271
6272 commands
6273 }
6274
6275 #[allow(clippy::too_many_arguments)]
6277 fn layout_canvas(
6278 &self,
6279 node: &Node,
6280 style: &ResolvedStyle,
6281 cursor: &mut PageCursor,
6282 pages: &mut Vec<LayoutPage>,
6283 x: f64,
6284 _available_width: f64,
6285 canvas_width: f64,
6286 canvas_height: f64,
6287 operations: &[CanvasOp],
6288 ) {
6289 let margin = style.margin.to_edges();
6290 let total_height = canvas_height + margin.top + margin.bottom;
6291
6292 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
6294 pages.push(cursor.finalize());
6295 *cursor = cursor.new_page();
6296 }
6297
6298 cursor.y += margin.top;
6299
6300 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
6301
6302 cursor.elements.push(LayoutElement {
6303 x: x + margin.left,
6304 y: cursor.content_y + cursor.y,
6305 width: canvas_width,
6306 height: canvas_height,
6307 draw: DrawCommand::Svg {
6308 commands: svg_commands,
6309 width: canvas_width,
6310 height: canvas_height,
6311 viewbox_min_x: 0.0,
6314 viewbox_min_y: 0.0,
6315 viewbox_width: canvas_width,
6316 viewbox_height: canvas_height,
6317 clip: true,
6318 },
6319 children: vec![],
6320 node_type: Some("Canvas".to_string()),
6321 resolved_style: Some(style.clone()),
6322 source_location: node.source_location.clone(),
6323 href: node.href.clone(),
6324 bookmark: node.bookmark.clone(),
6325 alt: node.alt.clone(),
6326 is_header_row: false,
6327 actual_text: None,
6328 list_numbering: None,
6329 col_span: 1,
6330 overflow: style.overflow,
6331 opacity: style.opacity,
6332 });
6333
6334 cursor.y += canvas_height + margin.bottom;
6335 }
6336
6337 #[allow(clippy::too_many_arguments)]
6339 #[allow(clippy::too_many_arguments)]
6341 fn layout_chart(
6342 &self,
6343 node: &Node,
6344 style: &ResolvedStyle,
6345 cursor: &mut PageCursor,
6346 pages: &mut Vec<LayoutPage>,
6347 x: f64,
6348 chart_width: f64,
6349 chart_height: f64,
6350 primitives: Vec<crate::chart::ChartPrimitive>,
6351 node_type_name: &str,
6352 ) {
6353 let margin = &style.margin.to_edges();
6354 let total_height = chart_height + margin.vertical();
6355
6356 if total_height > cursor.remaining_height() {
6357 pages.push(cursor.finalize());
6358 *cursor = cursor.new_page();
6359 }
6360
6361 cursor.y += margin.top;
6362
6363 let draw = DrawCommand::Chart { primitives };
6364
6365 cursor.elements.push(LayoutElement {
6366 x: x + margin.left,
6367 y: cursor.content_y + cursor.y,
6368 width: chart_width,
6369 height: chart_height,
6370 draw,
6371 children: vec![],
6372 node_type: Some(node_type_name.to_string()),
6373 resolved_style: Some(style.clone()),
6374 source_location: node.source_location.clone(),
6375 href: node.href.clone(),
6376 bookmark: node.bookmark.clone(),
6377 alt: node.alt.clone(),
6378 is_header_row: false,
6379 actual_text: None,
6380 list_numbering: None,
6381 col_span: 1,
6382 overflow: style.overflow,
6383 opacity: style.opacity,
6384 });
6385
6386 cursor.y += chart_height + margin.bottom;
6387 }
6388
6389 #[allow(clippy::too_many_arguments)]
6391 fn layout_form_field(
6392 &self,
6393 node: &Node,
6394 style: &ResolvedStyle,
6395 cursor: &mut PageCursor,
6396 pages: &mut Vec<LayoutPage>,
6397 x: f64,
6398 field_width: f64,
6399 field_height: f64,
6400 draw: DrawCommand,
6401 node_type_name: &str,
6402 ) {
6403 let margin = &style.margin.to_edges();
6404 let total_height = field_height + margin.vertical();
6405
6406 if total_height > cursor.remaining_height() {
6407 pages.push(cursor.finalize());
6408 *cursor = cursor.new_page();
6409 }
6410
6411 cursor.y += margin.top;
6412
6413 cursor.elements.push(LayoutElement {
6414 x: x + margin.left,
6415 y: cursor.content_y + cursor.y,
6416 width: field_width,
6417 height: field_height,
6418 draw,
6419 children: vec![],
6420 node_type: Some(node_type_name.to_string()),
6421 resolved_style: Some(style.clone()),
6422 source_location: node.source_location.clone(),
6423 href: node.href.clone(),
6424 bookmark: node.bookmark.clone(),
6425 alt: node.alt.clone(),
6426 is_header_row: false,
6427 actual_text: None,
6428 list_numbering: None,
6429 col_span: 1,
6430 overflow: style.overflow,
6431 opacity: style.opacity,
6432 });
6433
6434 cursor.y += field_height + margin.bottom;
6435 }
6436
6437 #[allow(clippy::too_many_arguments)]
6438 fn layout_barcode(
6439 &self,
6440 node: &Node,
6441 style: &ResolvedStyle,
6442 cursor: &mut PageCursor,
6443 pages: &mut Vec<LayoutPage>,
6444 x: f64,
6445 available_width: f64,
6446 data: &str,
6447 format: crate::barcode::BarcodeFormat,
6448 explicit_width: Option<f64>,
6449 bar_height: f64,
6450 ) {
6451 let margin = &style.margin.to_edges();
6452 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
6453 let total_height = bar_height + margin.vertical();
6454
6455 if total_height > cursor.remaining_height() {
6456 pages.push(cursor.finalize());
6457 *cursor = cursor.new_page();
6458 }
6459
6460 cursor.y += margin.top;
6461
6462 let draw = match crate::barcode::generate_barcode(data, format) {
6463 Ok(barcode_data) => {
6464 let bar_width = if barcode_data.bars.is_empty() {
6465 0.0
6466 } else {
6467 display_width / barcode_data.bars.len() as f64
6468 };
6469 DrawCommand::Barcode {
6470 bars: barcode_data.bars,
6471 bar_width,
6472 height: bar_height,
6473 color: style.color,
6474 }
6475 }
6476 Err(_) => DrawCommand::None,
6477 };
6478
6479 cursor.elements.push(LayoutElement {
6480 x: x + margin.left,
6481 y: cursor.content_y + cursor.y,
6482 width: display_width,
6483 height: bar_height,
6484 draw,
6485 children: vec![],
6486 node_type: Some("Barcode".to_string()),
6487 resolved_style: Some(style.clone()),
6488 source_location: node.source_location.clone(),
6489 href: node.href.clone(),
6490 bookmark: node.bookmark.clone(),
6491 alt: node.alt.clone(),
6492 is_header_row: false,
6493 actual_text: Some(data.to_string()),
6494 list_numbering: None,
6495 col_span: 1,
6496 overflow: style.overflow,
6497 opacity: style.opacity,
6498 });
6499
6500 cursor.y += bar_height + margin.bottom;
6501 }
6502
6503 #[allow(clippy::too_many_arguments)]
6505 fn layout_qrcode(
6506 &self,
6507 node: &Node,
6508 style: &ResolvedStyle,
6509 cursor: &mut PageCursor,
6510 pages: &mut Vec<LayoutPage>,
6511 x: f64,
6512 available_width: f64,
6513 data: &str,
6514 explicit_size: Option<f64>,
6515 ) {
6516 let margin = &style.margin.to_edges();
6517 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6518 let total_height = display_size + margin.vertical();
6519
6520 if total_height > cursor.remaining_height() {
6521 pages.push(cursor.finalize());
6522 *cursor = cursor.new_page();
6523 }
6524
6525 cursor.y += margin.top;
6526
6527 let draw = match crate::qrcode::generate_qr(data) {
6528 Ok(matrix) => {
6529 let module_size = display_size / matrix.size as f64;
6530 DrawCommand::QrCode {
6531 modules: matrix.modules,
6532 module_size,
6533 color: style.color,
6534 }
6535 }
6536 Err(_) => DrawCommand::None,
6537 };
6538
6539 cursor.elements.push(LayoutElement {
6540 x: x + margin.left,
6541 y: cursor.content_y + cursor.y,
6542 width: display_size,
6543 height: display_size,
6544 draw,
6545 children: vec![],
6546 node_type: Some("QrCode".to_string()),
6547 resolved_style: Some(style.clone()),
6548 source_location: node.source_location.clone(),
6549 href: node.href.clone(),
6550 bookmark: node.bookmark.clone(),
6551 alt: node.alt.clone(),
6552 is_header_row: false,
6553 actual_text: Some(data.to_string()),
6554 list_numbering: None,
6555 col_span: 1,
6556 overflow: style.overflow,
6557 opacity: style.opacity,
6558 });
6559
6560 cursor.y += display_size + margin.bottom;
6561 }
6562
6563 fn measure_node_height(
6566 &self,
6567 node: &Node,
6568 available_width: f64,
6569 style: &ResolvedStyle,
6570 font_context: &FontContext,
6571 ) -> f64 {
6572 match &node.kind {
6573 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6578 let measure_width = match style.width {
6581 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6582 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6583 };
6584 if !runs.is_empty() {
6591 let mut styled_chars: Vec<StyledChar> = Vec::new();
6592 for run in runs {
6593 let run_style = run.style.resolve(Some(style), measure_width);
6594 let transform = run_style.text_transform;
6595 let run_content = substitute_page_placeholders(&run.content);
6596 let mut prev_is_whitespace = true;
6597 for ch in run_content.chars() {
6598 let transformed_ch =
6599 apply_char_transform(ch, transform, prev_is_whitespace);
6600 prev_is_whitespace = ch.is_whitespace();
6601 styled_chars.push(StyledChar {
6602 ch: transformed_ch,
6603 font_family: run_style.font_family.clone(),
6604 font_size: run_style.font_size,
6605 font_weight: run_style.font_weight,
6606 font_style: run_style.font_style,
6607 color: run_style.color,
6608 href: None,
6609 text_decoration: run_style.text_decoration,
6610 letter_spacing: run_style.letter_spacing,
6611 word_spacing: run_style.word_spacing,
6612 });
6613 }
6614 }
6615 let justify = matches!(style.text_align, TextAlign::Justify);
6616 let broken_lines = match style.line_breaking {
6617 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6618 font_context,
6619 &styled_chars,
6620 measure_width,
6621 style.hyphens,
6622 style.lang.as_deref(),
6623 justify,
6624 ),
6625 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6626 font_context,
6627 &styled_chars,
6628 measure_width,
6629 style.hyphens,
6630 style.lang.as_deref(),
6631 ),
6632 };
6633 let line_height = style.font_size * style.line_height;
6634 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6635 } else {
6636 let content = substitute_page_placeholders(content);
6637 let transformed = apply_text_transform(&content, style.text_transform);
6638 let justify = matches!(style.text_align, TextAlign::Justify);
6639 let lines = match style.line_breaking {
6640 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6641 font_context,
6642 &transformed,
6643 measure_width,
6644 style.font_size,
6645 &style.font_family,
6646 style.font_weight,
6647 style.font_style,
6648 style.letter_spacing,
6649 style.word_spacing,
6650 style.hyphens,
6651 style.lang.as_deref(),
6652 justify,
6653 ),
6654 LineBreaking::Greedy => self.text_layout.break_into_lines(
6655 font_context,
6656 &transformed,
6657 measure_width,
6658 style.font_size,
6659 &style.font_family,
6660 style.font_weight,
6661 style.font_style,
6662 style.letter_spacing,
6663 style.word_spacing,
6664 style.hyphens,
6665 style.lang.as_deref(),
6666 ),
6667 };
6668 let line_height = style.font_size * style.line_height;
6669 (lines.len() as f64) * line_height + style.padding.vertical()
6670 }
6671 }
6672 NodeKind::Image {
6673 src,
6674 width: explicit_w,
6675 height: explicit_h,
6676 } => {
6677 let (_, h) = self.image_display_size(
6679 src,
6680 style,
6681 *explicit_w,
6682 *explicit_h,
6683 available_width - style.margin.horizontal(),
6684 );
6685 h + style.padding.vertical()
6686 }
6687 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6688 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6689 NodeKind::QrCode { size, .. } => {
6690 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6691 display_size + style.margin.vertical()
6692 }
6693 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6694 NodeKind::BarChart { height, .. }
6695 | NodeKind::LineChart { height, .. }
6696 | NodeKind::PieChart { height, .. }
6697 | NodeKind::AreaChart { height, .. }
6698 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6699 NodeKind::TextField { height, .. }
6700 | NodeKind::Checkbox { height, .. }
6701 | NodeKind::Dropdown { height, .. }
6702 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6703 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6705 if let SizeConstraint::Fixed(h) = style.height {
6712 return h;
6713 }
6714 let outer_width = match style.width {
6715 SizeConstraint::Fixed(w) => w,
6716 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6717 };
6718 let inner_width =
6719 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6720 let col_widths = self.resolve_column_widths(
6721 columns,
6722 inner_width,
6723 &node.children,
6724 style,
6725 font_context,
6726 );
6727 let row_gap = style.row_gap;
6728 let offsets = Self::table_column_offsets(&node.children);
6729 let mut total = 0.0;
6730 for (i, row) in node.children.iter().enumerate() {
6731 if i > 0 {
6732 total += row_gap;
6733 }
6734 total += self.measure_table_row_height(
6735 row,
6736 &col_widths,
6737 &offsets[i],
6738 style,
6739 font_context,
6740 );
6741 }
6742 total + style.padding.vertical() + style.border_width.vertical()
6743 }
6744 NodeKind::TableRow { .. } => {
6745 let n = node.children.len().max(1);
6750 let usable = (available_width - style.margin.horizontal()).max(0.0);
6751 let col_w = usable / n as f64;
6752 let col_widths = vec![col_w; n];
6753 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6754 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6755 }
6756 _ => {
6757 if let SizeConstraint::Fixed(h) = style.height {
6759 return h;
6760 }
6761 let outer_width = match style.width {
6765 SizeConstraint::Fixed(w) => w,
6766 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6767 }
6768 .min(style.max_width)
6769 .max(style.min_width);
6770 let inner_width =
6771 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6772 let children_height =
6773 self.measure_children_height(&node.children, inner_width, style, font_context);
6774 (children_height + style.padding.vertical() + style.border_width.vertical())
6775 .max(style.min_height)
6776 }
6777 }
6778 }
6779
6780 fn measure_children_height(
6781 &self,
6782 children: &[Node],
6783 available_width: f64,
6784 parent_style: &ResolvedStyle,
6785 font_context: &FontContext,
6786 ) -> f64 {
6787 if children
6793 .iter()
6794 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6795 {
6796 let flow: Vec<Node> = children
6797 .iter()
6798 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6799 .cloned()
6800 .collect();
6801 return self.measure_children_height(
6802 &flow,
6803 available_width,
6804 parent_style,
6805 font_context,
6806 );
6807 }
6808 if matches!(parent_style.display, Display::Grid) {
6810 if let Some(template_cols) = &parent_style.grid_template_columns {
6811 let num_columns = template_cols.len();
6812 if num_columns > 0 && !children.is_empty() {
6813 let col_gap = parent_style.column_gap;
6814 let row_gap = parent_style.row_gap;
6815
6816 let content_sizes: Vec<f64> = template_cols
6817 .iter()
6818 .map(|track| {
6819 if matches!(track, GridTrackSize::Auto) {
6820 available_width / num_columns as f64
6821 } else {
6822 0.0
6823 }
6824 })
6825 .collect();
6826
6827 let col_widths = grid::resolve_tracks(
6828 template_cols,
6829 available_width,
6830 col_gap,
6831 &content_sizes,
6832 );
6833
6834 let placements: Vec<Option<&GridPlacement>> = children
6835 .iter()
6836 .map(|child| child.style.grid_placement.as_ref())
6837 .collect();
6838
6839 let item_placements = grid::place_items(&placements, num_columns);
6840 let num_rows = grid::compute_num_rows(&item_placements);
6841
6842 if num_rows == 0 {
6843 return 0.0;
6844 }
6845
6846 let mut row_heights = vec![0.0_f64; num_rows];
6847 for placement in &item_placements {
6848 let cell_width = grid::span_width(
6849 placement.col_start,
6850 placement.col_end,
6851 &col_widths,
6852 col_gap,
6853 );
6854 let child = &children[placement.child_index];
6855 let child_style = child.style.resolve(Some(parent_style), cell_width);
6856 let h =
6857 self.measure_node_height(child, cell_width, &child_style, font_context);
6858 let span = placement.row_end - placement.row_start;
6859 let per_row = h / span as f64;
6860 for rh in row_heights
6861 .iter_mut()
6862 .take(placement.row_end.min(num_rows))
6863 .skip(placement.row_start)
6864 {
6865 if per_row > *rh {
6866 *rh = per_row;
6867 }
6868 }
6869 }
6870
6871 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6872 return row_heights.iter().sum::<f64>() + total_row_gap;
6873 }
6874 }
6875 }
6876
6877 let direction = parent_style.flex_direction;
6878 let row_gap = parent_style.row_gap;
6879 let column_gap = parent_style.column_gap;
6880
6881 match direction {
6882 FlexDirection::Row | FlexDirection::RowReverse => {
6883 let styles: Vec<ResolvedStyle> = children
6886 .iter()
6887 .map(|child| child.style.resolve(Some(parent_style), available_width))
6888 .collect();
6889
6890 let base_widths: Vec<f64> = children
6891 .iter()
6892 .zip(&styles)
6893 .map(|(child, style)| match style.flex_basis {
6894 SizeConstraint::Fixed(w) => w,
6895 SizeConstraint::Auto => match style.width {
6896 SizeConstraint::Fixed(w) => w,
6897 SizeConstraint::Auto => self
6898 .measure_intrinsic_width(child, style, font_context)
6899 .min(available_width),
6900 },
6901 })
6902 .collect();
6903
6904 let lines = match parent_style.flex_wrap {
6905 FlexWrap::NoWrap => {
6906 vec![flex::WrapLine {
6907 start: 0,
6908 end: children.len(),
6909 }]
6910 }
6911 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6912 flex::partition_into_lines(&base_widths, column_gap, available_width)
6913 }
6914 };
6915
6916 let mut final_widths = base_widths.clone();
6918 for line in &lines {
6919 let line_count = line.end - line.start;
6920 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6921 let distributable = available_width - line_gap;
6922 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6923 let remaining = distributable - total_base;
6924
6925 if remaining > 0.0 {
6926 let total_grow: f64 = styles[line.start..line.end]
6927 .iter()
6928 .map(|s| s.flex_grow)
6929 .sum();
6930 if total_grow > 0.0 {
6931 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6932 final_widths[line.start + j] = base_widths[line.start + j]
6933 + remaining * (s.flex_grow / total_grow);
6934 }
6935 }
6936 } else if remaining < 0.0 {
6937 let total_shrink: f64 = styles[line.start..line.end]
6938 .iter()
6939 .enumerate()
6940 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6941 .sum();
6942 if total_shrink > 0.0 {
6943 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6944 let factor =
6945 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
6946 let w = base_widths[line.start + j] + remaining * factor;
6947 final_widths[line.start + j] = w.max(s.min_width);
6948 }
6949 }
6950 }
6951 }
6952
6953 let mut total = 0.0;
6954 for (i, line) in lines.iter().enumerate() {
6955 let line_height: f64 = children[line.start..line.end]
6956 .iter()
6957 .enumerate()
6958 .map(|(j, child)| {
6959 let fw = final_widths[line.start + j];
6960 let child_style =
6969 child.style.resolve(Some(parent_style), available_width);
6970 self.measure_node_height(child, fw, &child_style, font_context)
6971 + child_style.margin.vertical()
6972 })
6973 .fold(0.0f64, f64::max);
6974 total += line_height;
6975 if i > 0 {
6976 total += row_gap;
6977 }
6978 }
6979 total
6980 }
6981 FlexDirection::Column | FlexDirection::ColumnReverse => {
6982 let mut total = 0.0;
6983 for (i, child) in children.iter().enumerate() {
6984 let child_style = child.style.resolve(Some(parent_style), available_width);
6985 let child_height = self.measure_node_height(
6986 child,
6987 available_width,
6988 &child_style,
6989 font_context,
6990 );
6991 total += child_height + child_style.margin.vertical();
6992 if i > 0 {
6993 total += row_gap;
6994 }
6995 }
6996 total
6997 }
6998 }
6999 }
7000
7001 fn measure_intrinsic_width(
7003 &self,
7004 node: &Node,
7005 style: &ResolvedStyle,
7006 font_context: &FontContext,
7007 ) -> f64 {
7008 match &node.kind {
7009 NodeKind::Svg { width, .. } => {
7010 *width + style.padding.horizontal() + style.margin.horizontal()
7011 }
7012 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7013 let text_width = if !runs.is_empty() {
7019 runs.iter()
7020 .map(|run| {
7021 let run_style = run.style.resolve(Some(style), 0.0);
7022 let run_content = substitute_page_placeholders(&run.content);
7023 let transformed =
7024 apply_text_transform(&run_content, run_style.text_transform);
7025 let italic = matches!(
7026 run_style.font_style,
7027 FontStyle::Italic | FontStyle::Oblique
7028 );
7029 transformed
7033 .split('\n')
7034 .map(|segment| {
7035 font_context.measure_string(
7036 segment,
7037 &run_style.font_family,
7038 run_style.font_weight,
7039 italic,
7040 run_style.font_size,
7041 run_style.letter_spacing,
7042 )
7043 })
7044 .fold(0.0f64, f64::max)
7045 })
7046 .sum()
7047 } else {
7048 let content = substitute_page_placeholders(content);
7049 let transformed = apply_text_transform(&content, style.text_transform);
7050 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7051 transformed
7052 .split('\n')
7053 .map(|segment| {
7054 font_context.measure_string(
7055 segment,
7056 &style.font_family,
7057 style.font_weight,
7058 italic,
7059 style.font_size,
7060 style.letter_spacing,
7061 )
7062 })
7063 .fold(0.0f64, f64::max)
7064 };
7065 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
7068 }
7069 NodeKind::Image {
7070 src, width, height, ..
7071 } => {
7072 let w = if let SizeConstraint::Fixed(w) = style.width {
7073 w
7074 } else if let Some(w) = width {
7075 *w
7076 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
7077 let pixel_w = iw as f64;
7078 let pixel_h = ih as f64;
7079 let aspect = if pixel_w > 0.0 {
7080 pixel_h / pixel_w
7081 } else {
7082 0.75
7083 };
7084 let constrained_h = match style.height {
7086 SizeConstraint::Fixed(h) => Some(h),
7087 SizeConstraint::Auto => *height,
7088 };
7089 if let Some(h) = constrained_h {
7090 h / aspect
7091 } else {
7092 pixel_w
7093 }
7094 } else {
7095 100.0
7096 };
7097 w + style.padding.horizontal() + style.margin.horizontal()
7098 }
7099 NodeKind::Barcode { width, .. } => {
7100 let w = width.unwrap_or(0.0);
7101 w + style.padding.horizontal() + style.margin.horizontal()
7102 }
7103 NodeKind::QrCode { size, .. } => {
7104 let display_size = size.unwrap_or(0.0);
7105 display_size + style.padding.horizontal() + style.margin.horizontal()
7106 }
7107 NodeKind::Canvas { width, .. } => {
7108 *width + style.padding.horizontal() + style.margin.horizontal()
7109 }
7110 NodeKind::BarChart { width, .. }
7111 | NodeKind::LineChart { width, .. }
7112 | NodeKind::PieChart { width, .. }
7113 | NodeKind::AreaChart { width, .. }
7114 | NodeKind::DotPlot { width, .. } => {
7115 *width + style.padding.horizontal() + style.margin.horizontal()
7116 }
7117 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
7118 *width + style.padding.horizontal() + style.margin.horizontal()
7119 }
7120 NodeKind::Table { columns } => {
7121 let num_cols = node
7126 .children
7127 .iter()
7128 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7129 .max()
7130 .unwrap_or(1)
7131 .max(columns.len().max(1));
7132 let (_, col_max) =
7133 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7134 col_max.iter().sum::<f64>()
7135 + style.padding.horizontal()
7136 + style.margin.horizontal()
7137 + style.border_width.horizontal()
7138 }
7139 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
7140 *width + style.padding.horizontal() + style.margin.horizontal()
7141 }
7142 NodeKind::Watermark { .. } => 0.0, _ => {
7144 if let SizeConstraint::Fixed(w) = style.width {
7152 return w
7153 + style.padding.horizontal()
7154 + style.border_width.horizontal()
7155 + style.margin.horizontal();
7156 }
7157 if node.children.is_empty() {
7159 style.padding.horizontal() + style.margin.horizontal()
7160 } else {
7161 let direction = style.flex_direction;
7162 let gap = style.column_gap;
7174 let mut total = 0.0f64;
7175 for (i, child) in node.children.iter().enumerate() {
7176 let child_style = child.style.resolve(Some(style), 0.0);
7177 let child_width =
7178 self.measure_intrinsic_width(child, &child_style, font_context);
7179 match direction {
7180 FlexDirection::Row | FlexDirection::RowReverse => {
7181 total += child_width;
7182 if i > 0 {
7183 total += gap;
7184 }
7185 }
7186 _ => {
7187 total = total.max(child_width);
7188 }
7189 }
7190 }
7191 total
7192 + style.padding.horizontal()
7193 + style.margin.horizontal()
7194 + style.border_width.horizontal()
7195 }
7196 }
7197 }
7198 }
7199
7200 pub fn measure_min_content_width(
7204 &self,
7205 node: &Node,
7206 style: &ResolvedStyle,
7207 font_context: &FontContext,
7208 ) -> f64 {
7209 match &node.kind {
7210 NodeKind::Table { columns } => {
7211 let num_cols = node
7214 .children
7215 .iter()
7216 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7217 .max()
7218 .unwrap_or(1)
7219 .max(columns.len().max(1));
7220 let (col_min, _) =
7221 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7222 col_min.iter().sum::<f64>()
7223 + style.padding.horizontal()
7224 + style.margin.horizontal()
7225 + style.border_width.horizontal()
7226 }
7227 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7228 let word_width = if !runs.is_empty() {
7229 runs.iter()
7231 .map(|run| {
7232 let run_style = run.style.resolve(Some(style), 0.0);
7233 let run_content = substitute_page_placeholders(&run.content);
7234 let transformed =
7235 apply_text_transform(&run_content, run_style.text_transform);
7236 self.text_layout.measure_widest_word(
7237 font_context,
7238 &transformed,
7239 run_style.font_size,
7240 &run_style.font_family,
7241 run_style.font_weight,
7242 run_style.font_style,
7243 run_style.letter_spacing,
7244 run_style.word_spacing,
7245 style.hyphens,
7246 style.lang.as_deref(),
7247 )
7248 })
7249 .fold(0.0f64, f64::max)
7250 } else {
7251 let content = substitute_page_placeholders(content);
7252 let transformed = apply_text_transform(&content, style.text_transform);
7253 self.text_layout.measure_widest_word(
7254 font_context,
7255 &transformed,
7256 style.font_size,
7257 &style.font_family,
7258 style.font_weight,
7259 style.font_style,
7260 style.letter_spacing,
7261 style.word_spacing,
7262 style.hyphens,
7263 style.lang.as_deref(),
7264 )
7265 };
7266 word_width + style.padding.horizontal() + style.margin.horizontal()
7267 }
7268 NodeKind::Image { width, .. } => {
7269 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
7270 }
7271 NodeKind::Svg { width, .. } => {
7272 *width + style.padding.horizontal() + style.margin.horizontal()
7273 }
7274 _ => {
7275 if node.children.is_empty() {
7276 style.padding.horizontal()
7277 + style.margin.horizontal()
7278 + style.border_width.horizontal()
7279 } else {
7280 let mut max_child_min = 0.0f64;
7281 for child in &node.children {
7282 let child_style = child.style.resolve(Some(style), 0.0);
7283 let child_min =
7284 self.measure_min_content_width(child, &child_style, font_context);
7285 max_child_min = max_child_min.max(child_min);
7286 }
7287 max_child_min
7288 + style.padding.horizontal()
7289 + style.margin.horizontal()
7290 + style.border_width.horizontal()
7291 }
7292 }
7293 }
7294 }
7295
7296 fn flex_item_baseline_distance(
7303 &self,
7304 item: &Node,
7305 style: &ResolvedStyle,
7306 w: f64,
7307 font_context: &FontContext,
7308 ) -> f64 {
7309 let first_line = match &item.kind {
7310 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
7311 let metrics = font_context.baseline_metrics(
7312 &style.font_family,
7313 style.font_weight,
7314 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
7315 );
7316 baseline_in_line(
7317 style.font_size * style.line_height,
7318 style.font_size,
7319 metrics,
7320 )
7321 }
7322 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
7323 };
7324 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
7325 }
7326
7327 fn cell_first_baseline_in_line(
7334 &self,
7335 cell: &Node,
7336 cell_style: &ResolvedStyle,
7337 w: f64,
7338 font_context: &FontContext,
7339 ) -> f64 {
7340 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
7341 for ch in &node.children {
7342 let s = ch.style.resolve(Some(parent), w);
7343 match &ch.kind {
7344 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
7345 _ => {
7346 if let Some(f) = first(ch, &s, w) {
7347 return Some(f);
7348 }
7349 }
7350 }
7351 }
7352 None
7353 }
7354 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
7355 let metrics = font_context.baseline_metrics(
7356 &s.font_family,
7357 s.font_weight,
7358 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
7359 );
7360 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
7361 }
7362
7363 fn cell_baseline_distance(
7368 &self,
7369 cell: &Node,
7370 cell_style: &ResolvedStyle,
7371 inner_width: f64,
7372 font_context: &FontContext,
7373 ) -> f64 {
7374 cell_style.padding.top
7375 + cell_style.border_width.top
7376 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
7377 }
7378
7379 fn row_baseline(
7382 &self,
7383 row: &Node,
7384 row_style: &ResolvedStyle,
7385 col_widths: &[f64],
7386 col_offsets: &[usize],
7387 font_context: &FontContext,
7388 ) -> Option<f64> {
7389 let mut b: Option<f64> = None;
7390 for (cell_i, cell) in row.children.iter().enumerate() {
7391 let span = match &cell.kind {
7392 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7393 _ => 1,
7394 };
7395 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7396 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7397 let cell_style = cell.style.resolve(Some(row_style), col_width);
7398 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7399 let iw = col_width
7400 - cell_style.padding.horizontal()
7401 - cell_style.border_width.horizontal();
7402 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
7403 b = Some(b.map_or(d, |m: f64| m.max(d)));
7404 }
7405 }
7406 b
7407 }
7408
7409 fn measure_table_row_height(
7410 &self,
7411 row: &Node,
7412 col_widths: &[f64],
7413 col_offsets: &[usize],
7414 parent_style: &ResolvedStyle,
7415 font_context: &FontContext,
7416 ) -> f64 {
7417 let row_style = row
7418 .style
7419 .resolve(Some(parent_style), col_widths.iter().sum());
7420 let mut max_height: f64 = 0.0;
7421 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
7424
7425 for (cell_i, cell) in row.children.iter().enumerate() {
7426 let span = match &cell.kind {
7427 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7428 _ => 1,
7429 };
7430 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7431 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7432 let cell_style = cell.style.resolve(Some(&row_style), col_width);
7433 let inner_width =
7434 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
7435
7436 let mut cell_content_height = 0.0;
7437 for child in &cell.children {
7438 let child_style = child.style.resolve(Some(&cell_style), inner_width);
7439 cell_content_height +=
7440 self.measure_node_height(child, inner_width, &child_style, font_context);
7441 }
7442
7443 let mut total = cell_content_height
7444 + cell_style.padding.vertical()
7445 + cell_style.border_width.vertical();
7446 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7450 if let Some(b) = row_bl {
7451 let d =
7452 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
7453 total += (b - d).max(0.0);
7454 }
7455 }
7456 if let SizeConstraint::Fixed(h) = cell_style.height {
7463 total = total.max(h);
7464 }
7465 max_height = max_height.max(total);
7466 }
7467
7468 max_height.max(row_style.min_height)
7469 }
7470
7471 fn cell_col_span(cell: &Node) -> usize {
7473 match &cell.kind {
7474 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7475 _ => 1,
7476 }
7477 }
7478
7479 fn measure_column_content(
7484 &self,
7485 children: &[Node],
7486 num_cols: usize,
7487 available_width: f64,
7488 table_style: &ResolvedStyle,
7489 font_context: &FontContext,
7490 ) -> (Vec<f64>, Vec<f64>) {
7491 let mut col_min = vec![0.0f64; num_cols];
7492 let mut col_max = vec![0.0f64; num_cols];
7493 let offsets = Self::table_column_offsets(children);
7494 for (row_i, row_node) in children.iter().enumerate() {
7495 for (cell_i, cell) in row_node.children.iter().enumerate() {
7496 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7497 let span = Self::cell_col_span(cell);
7498 let cell_style = cell.style.resolve(Some(table_style), available_width);
7499 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7500 let mut cmin = 0.0f64;
7501 let mut cmax = 0.0f64;
7502 for child in &cell.children {
7503 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7504 cmin =
7505 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7506 cmax =
7507 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7508 }
7509 cmin += chrome;
7510 let mut cmax = cmax.max(cmin) + chrome;
7511 if let SizeConstraint::Fixed(w) = cell_style.width {
7512 cmax = w.max(cmin);
7513 }
7514 let per_min = cmin / span as f64;
7515 let per_max = cmax / span as f64;
7516 for k in col..(col + span).min(num_cols) {
7517 col_min[k] = col_min[k].max(per_min);
7518 col_max[k] = col_max[k].max(per_max);
7519 }
7520 }
7521 }
7522 (col_min, col_max)
7523 }
7524
7525 fn resolve_column_widths(
7535 &self,
7536 defs: &[ColumnDef],
7537 available_width: f64,
7538 children: &[Node],
7539 table_style: &ResolvedStyle,
7540 font_context: &FontContext,
7541 ) -> Vec<f64> {
7542 if defs.is_empty() {
7543 let num_cols = Self::occupancy_column_count(children);
7544
7545 let (col_min, col_max) = self.measure_column_content(
7546 children,
7547 num_cols,
7548 available_width,
7549 table_style,
7550 font_context,
7551 );
7552
7553 let sum_min: f64 = col_min.iter().sum();
7554 let sum_max: f64 = col_max.iter().sum();
7555 let w = available_width;
7556 return if sum_max <= w {
7557 if sum_max <= f64::EPSILON {
7561 vec![w / num_cols as f64; num_cols]
7562 } else {
7563 col_max
7564 .iter()
7565 .map(|m| m + (w - sum_max) * (m / sum_max))
7566 .collect()
7567 }
7568 } else if sum_min <= w {
7569 let denom = (sum_max - sum_min).max(f64::EPSILON);
7572 col_min
7573 .iter()
7574 .zip(&col_max)
7575 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7576 .collect()
7577 } else {
7578 self.defect(format!(
7581 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7582 sum_min, w
7583 ));
7584 let scale = w / sum_min.max(f64::EPSILON);
7585 col_min.iter().map(|m| m * scale).collect()
7586 };
7587 }
7588
7589 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7595 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7596 while defs_vec.len() < num_cols {
7597 defs_vec.push(ColumnDef {
7598 width: ColumnWidth::Auto,
7599 });
7600 }
7601 let defs = &defs_vec[..];
7602
7603 let mut widths = Vec::new();
7604 let mut remaining = available_width;
7605 let mut auto_count = 0;
7606
7607 for def in defs {
7608 match def.width {
7609 ColumnWidth::Fixed(w) => {
7610 widths.push(w);
7611 remaining -= w;
7612 }
7613 ColumnWidth::Fraction(f) => {
7614 let w = available_width * f;
7615 widths.push(w);
7616 remaining -= w;
7617 }
7618 ColumnWidth::Auto => {
7619 widths.push(0.0);
7620 auto_count += 1;
7621 }
7622 }
7623 }
7624
7625 const OVERFULL_EPS: f64 = 0.01;
7634 if remaining < -OVERFULL_EPS {
7635 self.defect(format!(
7638 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7639 available_width - remaining,
7640 available_width
7641 ));
7642 remaining = 0.0;
7643 }
7644 remaining = remaining.max(0.0);
7647
7648 if auto_count > 0 {
7649 let auto_width = remaining / auto_count as f64;
7650 for (i, def) in defs.iter().enumerate() {
7651 if matches!(def.width, ColumnWidth::Auto) {
7652 widths[i] = auto_width;
7653 }
7654 }
7655 }
7656
7657 let (col_min, _) = self.measure_column_content(
7664 children,
7665 widths.len(),
7666 available_width,
7667 table_style,
7668 font_context,
7669 );
7670 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7671 if needs_floor {
7672 let sum_min: f64 = col_min.iter().sum();
7673 if sum_min > available_width {
7674 self.defect(format!(
7675 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7676 sum_min, available_width
7677 ));
7678 let scale = available_width / sum_min.max(f64::EPSILON);
7679 return col_min.iter().map(|m| m * scale).collect();
7680 }
7681 let deficit: f64 = widths
7682 .iter()
7683 .zip(&col_min)
7684 .map(|(w, m)| (m - w).max(0.0))
7685 .sum();
7686 let surplus: f64 = widths
7687 .iter()
7688 .zip(&col_min)
7689 .map(|(w, m)| (w - m).max(0.0))
7690 .sum();
7691 let take = if surplus > 0.0 {
7692 deficit / surplus
7693 } else {
7694 0.0
7695 };
7696 widths = widths
7697 .iter()
7698 .zip(&col_min)
7699 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7700 .collect();
7701 }
7702
7703 widths
7704 }
7705
7706 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7707 for (page_index, page) in pages.iter_mut().enumerate() {
7708 if !page.watermarks.is_empty() {
7710 let (page_w, page_h) = page.config.size.dimensions();
7711 let cx = page_w / 2.0;
7712 let cy = page_h / 2.0;
7713
7714 let mut watermark_elements = Vec::new();
7715 for (wm_node, wm_parent) in &page.watermarks {
7716 if let NodeKind::Watermark {
7717 text,
7718 font_size,
7719 angle,
7720 } = &wm_node.kind
7721 {
7722 let style = wm_node.style.resolve(wm_parent.as_ref(), page_w);
7723 let color = style.color;
7724 let opacity = style.opacity;
7725 let angle_rad = angle.to_radians();
7726
7727 let italic =
7729 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7730
7731 let shaped = self.text_layout.shape_text(
7733 font_context,
7734 text,
7735 &style.font_family,
7736 style.font_weight,
7737 style.font_style,
7738 );
7739
7740 let mut glyphs = Vec::new();
7741 let mut x_pos = 0.0;
7742 let text_chars: Vec<char> = text.chars().collect();
7743
7744 if let Some(shaped_glyphs) = shaped {
7745 let units_per_em = font_context.units_per_em(
7747 &style.font_family,
7748 style.font_weight,
7749 italic,
7750 ) as f64;
7751
7752 let clusters = cluster_texts(&shaped_glyphs, &text_chars);
7753 for (sg, (cluster_text, ligature, extraction_text)) in
7754 shaped_glyphs.iter().zip(clusters)
7755 {
7756 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7757 let cluster_idx = sg.cluster as usize;
7758 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7759 glyphs.push(PositionedGlyph {
7760 glyph_id: sg.glyph_id,
7761 char_value: ch,
7762 x_offset: x_pos,
7763 y_offset: 0.0,
7764 x_advance: advance,
7765 font_size: *font_size,
7766 font_family: Arc::from(style.font_family.as_str()),
7767 font_weight: style.font_weight,
7768 font_style: style.font_style,
7769 color: Some(color),
7770 href: None,
7771 text_decoration: TextDecoration::None,
7772 letter_spacing: style.letter_spacing,
7773 cluster_text,
7774 extraction_text,
7775
7776 ligature,
7777 });
7778 x_pos += advance + style.letter_spacing;
7779 }
7780 } else {
7781 for &ch in &text_chars {
7783 let w = font_context.char_width(
7784 ch,
7785 &style.font_family,
7786 style.font_weight,
7787 italic,
7788 *font_size,
7789 );
7790 glyphs.push(PositionedGlyph {
7791 glyph_id: ch as u16,
7792 char_value: ch,
7793 x_offset: x_pos,
7794 y_offset: 0.0,
7795 x_advance: w,
7796 font_size: *font_size,
7797 font_family: Arc::from(style.font_family.as_str()),
7798 font_weight: style.font_weight,
7799 font_style: style.font_style,
7800 color: Some(color),
7801 href: None,
7802 text_decoration: TextDecoration::None,
7803 letter_spacing: style.letter_spacing,
7804 cluster_text: None,
7805 extraction_text: None,
7806 ligature: false,
7807 });
7808 x_pos += w + style.letter_spacing;
7809 }
7810 }
7811
7812 let text_width = x_pos;
7813
7814 let line = TextLine {
7815 x: 0.0,
7816 y: 0.0,
7817 glyphs,
7818 width: text_width,
7819 height: *font_size,
7820 word_spacing: 0.0,
7821 };
7822
7823 watermark_elements.push(LayoutElement {
7824 x: cx,
7825 y: cy,
7826 width: text_width,
7827 height: *font_size,
7828 draw: DrawCommand::Watermark {
7829 lines: vec![line],
7830 color,
7831 opacity,
7832 angle_rad,
7833 font_family: style.font_family.clone(),
7834 },
7835 children: vec![],
7836 node_type: Some("Watermark".to_string()),
7837 resolved_style: None,
7838 source_location: None,
7839 href: None,
7840 bookmark: None,
7841 alt: None,
7842 is_header_row: false,
7843 actual_text: None,
7844 list_numbering: None,
7845 col_span: 1,
7846 overflow: Overflow::default(),
7847 opacity: 1.0,
7848 });
7849 }
7850 }
7851
7852 watermark_elements.append(&mut page.elements);
7854 page.elements = watermark_elements;
7855 page.watermarks.clear();
7856 }
7857
7858 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7859 continue;
7860 }
7861
7862 if !page.fixed_header.is_empty() {
7864 let mut hdr_cursor = PageCursor::new(&page.config);
7865 for (node, _h, parent) in &page.fixed_header {
7866 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7869 continue;
7870 }
7871 let cw = hdr_cursor.content_width;
7872 let cx = hdr_cursor.content_x;
7873 let style = node.style.resolve(parent.as_ref(), cw);
7874 self.layout_view(
7875 node,
7876 &style,
7877 &mut hdr_cursor,
7878 &mut Vec::new(),
7879 cx,
7880 cw,
7881 font_context,
7882 );
7883 }
7884 let mut combined = hdr_cursor.elements;
7886 combined.append(&mut page.elements);
7887 page.elements = combined;
7888 }
7889
7890 if !page.fixed_footer.is_empty() {
7895 let mut ftr_cursor = PageCursor::new(&page.config);
7896 let total_ftr: f64 = page
7897 .fixed_footer
7898 .iter()
7899 .filter(|(n, _, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7900 .map(|(_, h, _)| *h)
7901 .sum();
7902 let target_y = ftr_cursor.content_height - total_ftr;
7903 for (node, _h, parent) in &page.fixed_footer {
7905 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7906 continue;
7907 }
7908 let cw = ftr_cursor.content_width;
7909 let cx = ftr_cursor.content_x;
7910 let style = node.style.resolve(parent.as_ref(), cw);
7911 self.layout_view(
7912 node,
7913 &style,
7914 &mut ftr_cursor,
7915 &mut Vec::new(),
7916 cx,
7917 cw,
7918 font_context,
7919 );
7920 }
7921 for el in &mut ftr_cursor.elements {
7925 offset_element_y(el, target_y);
7926 }
7927 page.elements.extend(ftr_cursor.elements);
7928 }
7929
7930 page.fixed_header.clear();
7932 page.fixed_footer.clear();
7933 }
7934 }
7935
7936 #[allow(clippy::too_many_arguments)]
7941 fn layout_grid_children(
7942 &self,
7943 children: &[Node],
7944 parent_style: &ResolvedStyle,
7945 cursor: &mut PageCursor,
7946 pages: &mut Vec<LayoutPage>,
7947 x: f64,
7948 available_width: f64,
7949 font_context: &FontContext,
7950 ) {
7951 let template_cols = match &parent_style.grid_template_columns {
7952 Some(cols) => cols,
7953 None => return, };
7955
7956 let num_columns = template_cols.len();
7957 if num_columns == 0 || children.is_empty() {
7958 return;
7959 }
7960
7961 let col_gap = parent_style.column_gap;
7962 let row_gap = parent_style.row_gap;
7963
7964 let content_sizes: Vec<f64> = template_cols
7967 .iter()
7968 .map(|track| {
7969 if matches!(track, GridTrackSize::Auto) {
7970 available_width / num_columns as f64
7973 } else {
7974 0.0
7975 }
7976 })
7977 .collect();
7978
7979 let col_widths =
7980 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
7981
7982 let placements: Vec<Option<&GridPlacement>> = children
7984 .iter()
7985 .map(|child| child.style.grid_placement.as_ref())
7986 .collect();
7987
7988 let item_placements = grid::place_items(&placements, num_columns);
7990 let num_rows = grid::compute_num_rows(&item_placements);
7991
7992 if num_rows == 0 {
7993 return;
7994 }
7995
7996 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
7998 for placement in &item_placements {
7999 let cell_width =
8000 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
8001 let child = &children[placement.child_index];
8002 let child_style = child.style.resolve(Some(parent_style), cell_width);
8003 item_heights[placement.child_index] =
8004 self.measure_node_height(child, cell_width, &child_style, font_context);
8005 }
8006
8007 let template_rows = parent_style.grid_template_rows.as_deref();
8009 let mut row_heights = vec![0.0_f64; num_rows];
8010 for placement in &item_placements {
8011 let h = item_heights[placement.child_index];
8012 let span = placement.row_end - placement.row_start;
8013 let per_row = h / span as f64;
8014 for rh in row_heights
8015 .iter_mut()
8016 .take(placement.row_end.min(num_rows))
8017 .skip(placement.row_start)
8018 {
8019 if per_row > *rh {
8020 *rh = per_row;
8021 }
8022 }
8023 }
8024
8025 if let Some(template) = template_rows {
8027 let auto_row = parent_style.grid_auto_rows.as_ref();
8028 for (r, rh) in row_heights.iter_mut().enumerate() {
8029 let track = template.get(r).or(auto_row);
8030 if let Some(track) = track {
8031 match track {
8032 GridTrackSize::Pt(pts) => *rh = *pts,
8033 GridTrackSize::Auto => {} _ => {} }
8036 }
8037 }
8038 }
8039
8040 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
8042 if row_height > cursor.remaining_height() {
8047 pages.push(cursor.finalize());
8048 *cursor = cursor.new_page();
8049 }
8050
8051 let row_start_y = cursor.y;
8052
8053 for placement in &item_placements {
8055 if placement.row_start != row {
8056 continue; }
8058
8059 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
8060 let cell_width =
8061 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
8062
8063 let child = &children[placement.child_index];
8064
8065 self.layout_node(
8066 child,
8067 cursor,
8068 pages,
8069 cell_x,
8070 cell_width,
8071 Some(parent_style),
8072 font_context,
8073 None,
8074 None,
8075 );
8076 cursor.y = row_start_y;
8078 }
8079
8080 cursor.y = row_start_y + row_height + row_gap;
8081 }
8082
8083 if num_rows > 0 {
8085 cursor.y -= row_gap;
8086 }
8087 }
8088}
8089
8090fn end_y_of(frags: &[ItemFragments], last: usize) -> f64 {
8094 frags
8095 .iter()
8096 .filter(|f| f.pages.len() == last)
8097 .map(|f| f.final_y)
8098 .fold(f64::MIN, f64::max)
8099}
8100
8101fn paints_a_box(el: &LayoutElement) -> bool {
8109 match &el.draw {
8110 DrawCommand::Rect {
8111 background,
8112 border_width,
8113 ..
8114 } => {
8115 background.is_some()
8116 || border_width.top > 0.0
8117 || border_width.right > 0.0
8118 || border_width.bottom > 0.0
8119 || border_width.left > 0.0
8120 }
8121 _ => false,
8122 }
8123}
8124
8125fn drain_since(elements: &mut Vec<LayoutElement>, snapshot: usize) -> Vec<LayoutElement> {
8142 let start = snapshot.min(elements.len());
8143 elements.drain(start..).collect()
8144}
8145
8146fn stretch_fragment(elements: &mut [LayoutElement], bottom: f64) {
8147 for el in elements.iter_mut() {
8148 let grown = bottom - el.y;
8149 if grown > el.height {
8150 el.height = grown;
8151 }
8152 }
8153}
8154
8155struct ItemFragments {
8161 pages: Vec<LayoutPage>,
8162 tail: Vec<LayoutElement>,
8163 cursor: PageCursor,
8164 final_y: f64,
8165 stretches: bool,
8170}
8171
8172struct FlexItem<'a> {
8173 node: &'a Node,
8174 style: ResolvedStyle,
8175 base_width: f64,
8176 min_content_width: f64,
8177}
8178
8179fn first_text_snippet(node: &Node) -> Option<String> {
8182 fn walk(n: &Node) -> Option<&str> {
8183 match &n.kind {
8184 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
8185 if !content.trim().is_empty() {
8186 return Some(content.trim());
8187 }
8188 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
8189 return Some(r.content.trim());
8190 }
8191 None
8192 }
8193 _ => n.children.iter().find_map(walk),
8194 }
8195 }
8196 walk(node).map(|t| {
8197 let mut s: String = t.chars().take(32).collect();
8198 if t.chars().count() > 32 {
8199 s.push('…');
8200 }
8201 s
8202 })
8203}
8204
8205fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
8215 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
8216}
8217
8218#[cfg(test)]
8219mod tests {
8220 use super::*;
8221 use crate::font::FontContext;
8222
8223 fn sg(glyph_id: u16, cluster: u32) -> shaping::ShapedGlyph {
8224 shaping::ShapedGlyph {
8225 glyph_id,
8226 cluster,
8227 x_advance: 500,
8228 y_advance: 0,
8229 x_offset: 0,
8230 y_offset: 0,
8231 }
8232 }
8233
8234 #[test]
8235 fn test_cluster_texts_ltr_ligature() {
8236 let chars: Vec<char> = "office".chars().collect();
8238 let shaped = [sg(1, 0), sg(2, 1), sg(3, 4), sg(4, 5)];
8239 let got = cluster_texts(&shaped, &chars);
8240 assert_eq!(
8241 got,
8242 vec![
8243 (None, false, None),
8244 (Some("ffi".to_string()), true, Some("ffi".to_string())),
8245 (None, false, None),
8246 (None, false, None)
8247 ]
8248 );
8249 }
8250
8251 #[test]
8256 fn test_cluster_texts_rtl_ligature_uses_next_larger_cluster() {
8257 let chars: Vec<char> = "abcd".chars().collect();
8258 let shaped = [sg(1, 3), sg(2, 1), sg(3, 0)];
8259 let got = cluster_texts(&shaped, &chars);
8260 assert_eq!(
8261 got,
8262 vec![
8263 (None, false, None),
8264 (Some("bc".to_string()), true, Some("bc".to_string())),
8265 (None, false, None)
8266 ]
8267 );
8268 }
8269
8270 #[test]
8274 fn test_cluster_texts_ligature_found_when_counts_balance() {
8275 let chars: Vec<char> = "abcd".chars().collect();
8277 let shaped = [sg(1, 0), sg(2, 2), sg(3, 3), sg(4, 3)];
8278 let got = cluster_texts(&shaped, &chars);
8279 assert_eq!(
8280 got[0],
8281 (Some("ab".to_string()), true, Some("ab".to_string()))
8282 );
8283 assert_eq!(got[1], (None, false, None));
8284 assert_eq!(got[2], (None, false, Some("d".to_string())));
8285 assert_eq!(got[3], (None, false, Some("\u{200B}".to_string())));
8286 }
8287
8288 #[test]
8292 fn test_cluster_texts_multi_glyph_cluster_is_not_a_ligature() {
8293 let chars: Vec<char> = "kixy".chars().collect();
8295 let shaped = [sg(1, 0), sg(2, 0), sg(3, 3)];
8296 let got = cluster_texts(&shaped, &chars);
8297 assert_eq!(
8298 got[0],
8299 (Some("kix".to_string()), false, Some("k".to_string()))
8300 );
8301 assert_eq!(
8302 got[1],
8303 (Some("kix".to_string()), false, Some("ix".to_string()))
8304 );
8305 assert_eq!(got[2], (None, false, None));
8306 }
8307
8308 fn make_text(content: &str, font_size: f64) -> Node {
8309 Node {
8310 kind: NodeKind::Text {
8311 content: content.to_string(),
8312 href: None,
8313 runs: vec![],
8314 },
8315 style: Style {
8316 font_size: Some(font_size),
8317 ..Default::default()
8318 },
8319 children: vec![],
8320 id: None,
8321 source_location: None,
8322 bookmark: None,
8323 href: None,
8324 alt: None,
8325 }
8326 }
8327
8328 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
8329 Node {
8330 kind: NodeKind::View,
8331 style,
8332 children,
8333 id: None,
8334 source_location: None,
8335 bookmark: None,
8336 href: None,
8337 alt: None,
8338 }
8339 }
8340
8341 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
8342 Node {
8343 kind: NodeKind::Text {
8344 content: String::new(),
8345 href: None,
8346 runs,
8347 },
8348 style: Style::default(),
8349 children: vec![],
8350 id: None,
8351 source_location: None,
8352 bookmark: None,
8353 href: None,
8354 alt: None,
8355 }
8356 }
8357
8358 #[test]
8359 fn intrinsic_width_measures_runs_not_just_content() {
8360 let engine = LayoutEngine::new();
8364 let font_context = FontContext::new();
8365
8366 let runs_node = make_runs_text(vec![
8367 crate::model::TextRun {
8368 content: "Hello ".to_string(),
8369 style: Style::default(),
8370 href: None,
8371 },
8372 crate::model::TextRun {
8373 content: "World".to_string(),
8374 style: Style {
8375 font_weight: Some(700),
8376 ..Default::default()
8377 },
8378 href: None,
8379 },
8380 ]);
8381 let plain_node = make_text("Hello World", 12.0);
8382
8383 let runs_style = runs_node.style.resolve(None, 0.0);
8384 let plain_style = plain_node.style.resolve(None, 0.0);
8385 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
8386 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
8387
8388 assert!(
8391 runs_w >= plain_w,
8392 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
8393 );
8394 assert!(
8395 runs_w < plain_w * 1.5,
8396 "runs width ({runs_w}) should be close to plain width ({plain_w})"
8397 );
8398 }
8399
8400 #[test]
8401 fn intrinsic_width_of_multiline_text_is_widest_line() {
8402 let engine = LayoutEngine::new();
8403 let font_context = FontContext::new();
8404
8405 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
8406 let widest = make_text("Springfield, IL 62704", 12.0);
8407
8408 let m_style = multiline.style.resolve(None, 0.0);
8409 let w_style = widest.style.resolve(None, 0.0);
8410 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
8411 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
8412
8413 assert!(
8414 (m_w - w_w).abs() < 0.01,
8415 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
8416 );
8417 }
8418
8419 #[test]
8420 fn intrinsic_width_of_heading_measures_its_text() {
8421 let engine = LayoutEngine::new();
8424 let font_context = FontContext::new();
8425
8426 let heading = Node {
8427 kind: NodeKind::Heading {
8428 level: 1,
8429 content: "Invoice #2024-001".to_string(),
8430 href: None,
8431 runs: vec![],
8432 },
8433 style: Style {
8434 font_size: Some(24.0),
8435 ..Default::default()
8436 },
8437 children: vec![],
8438 id: None,
8439 source_location: None,
8440 bookmark: None,
8441 href: None,
8442 alt: None,
8443 };
8444 let style = heading.style.resolve(None, 0.0);
8445 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
8446 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
8447 }
8448
8449 #[test]
8450 fn measure_node_height_of_wrapping_heading_matches_text() {
8451 let engine = LayoutEngine::new();
8458 let font_context = FontContext::new();
8459
8460 let content = "Annual Performance Review";
8461 let heading = Node {
8462 kind: NodeKind::Heading {
8463 level: 1,
8464 content: content.to_string(),
8465 href: None,
8466 runs: vec![],
8467 },
8468 style: Style {
8469 font_size: Some(32.0),
8470 ..Default::default()
8471 },
8472 children: vec![],
8473 id: None,
8474 source_location: None,
8475 bookmark: None,
8476 href: None,
8477 alt: None,
8478 };
8479 let text = make_text(content, 32.0);
8480
8481 let width = 200.0;
8483 let h_style = heading.style.resolve(None, width);
8484 let t_style = text.style.resolve(None, width);
8485 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
8486 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
8487
8488 assert!(
8489 h_height > 32.0,
8490 "a wrapping 32pt heading must measure more than one line, got {h_height}"
8491 );
8492 assert!(
8493 (h_height - t_height).abs() < 0.01,
8494 "heading height ({h_height}) must equal the same text's height ({t_height})"
8495 );
8496 }
8497
8498 #[test]
8499 fn intrinsic_width_flex_row_sums_children() {
8500 let engine = LayoutEngine::new();
8501 let font_context = FontContext::new();
8502
8503 let child1 = make_text("Hello", 14.0);
8504 let child2 = make_text("World", 14.0);
8505
8506 let child1_style = child1.style.resolve(None, 0.0);
8507 let child2_style = child2.style.resolve(None, 0.0);
8508 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
8509 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
8510
8511 let row = make_styled_view(
8512 Style {
8513 flex_direction: Some(FlexDirection::Row),
8514 ..Default::default()
8515 },
8516 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
8517 );
8518 let row_style = row.style.resolve(None, 0.0);
8519 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
8520
8521 assert!(
8522 (row_w - (child1_w + child2_w)).abs() < 0.01,
8523 "Row intrinsic width ({}) should equal sum of children ({} + {})",
8524 row_w,
8525 child1_w,
8526 child2_w
8527 );
8528 }
8529
8530 #[test]
8531 fn intrinsic_width_flex_column_takes_max() {
8532 let engine = LayoutEngine::new();
8533 let font_context = FontContext::new();
8534
8535 let short = make_text("Hi", 14.0);
8536 let long = make_text("Hello World", 14.0);
8537
8538 let short_style = short.style.resolve(None, 0.0);
8539 let long_style = long.style.resolve(None, 0.0);
8540 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
8541 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
8542
8543 let col = make_styled_view(
8544 Style {
8545 flex_direction: Some(FlexDirection::Column),
8546 ..Default::default()
8547 },
8548 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
8549 );
8550 let col_style = col.style.resolve(None, 0.0);
8551 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
8552
8553 assert!(
8554 (col_w - long_w).abs() < 0.01,
8555 "Column intrinsic width ({}) should equal max child ({}, short was {})",
8556 col_w,
8557 long_w,
8558 short_w
8559 );
8560 }
8561
8562 #[test]
8563 fn intrinsic_width_nested_containers() {
8564 let engine = LayoutEngine::new();
8565 let font_context = FontContext::new();
8566
8567 let inner = make_styled_view(
8568 Style {
8569 flex_direction: Some(FlexDirection::Row),
8570 ..Default::default()
8571 },
8572 vec![make_text("A", 12.0), make_text("B", 12.0)],
8573 );
8574 let inner_style = inner.style.resolve(None, 0.0);
8575 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
8576
8577 let outer = make_styled_view(
8578 Style::default(),
8579 vec![make_styled_view(
8580 Style {
8581 flex_direction: Some(FlexDirection::Row),
8582 ..Default::default()
8583 },
8584 vec![make_text("A", 12.0), make_text("B", 12.0)],
8585 )],
8586 );
8587 let outer_style = outer.style.resolve(None, 0.0);
8588 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
8589
8590 assert!(
8591 (outer_w - inner_w).abs() < 0.01,
8592 "Nested container ({}) should match inner container ({})",
8593 outer_w,
8594 inner_w
8595 );
8596 }
8597
8598 #[test]
8599 fn intrinsic_width_row_with_gap() {
8600 let engine = LayoutEngine::new();
8601 let font_context = FontContext::new();
8602
8603 let no_gap = make_styled_view(
8604 Style {
8605 flex_direction: Some(FlexDirection::Row),
8606 ..Default::default()
8607 },
8608 vec![make_text("A", 12.0), make_text("B", 12.0)],
8609 );
8610 let with_gap = make_styled_view(
8611 Style {
8612 flex_direction: Some(FlexDirection::Row),
8613 gap: Some(10.0),
8614 ..Default::default()
8615 },
8616 vec![make_text("A", 12.0), make_text("B", 12.0)],
8617 );
8618
8619 let no_gap_style = no_gap.style.resolve(None, 0.0);
8620 let with_gap_style = with_gap.style.resolve(None, 0.0);
8621 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
8622 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
8623
8624 assert!(
8625 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
8626 "Gap should add 10pt: with_gap={}, no_gap={}",
8627 with_gap_w,
8628 no_gap_w
8629 );
8630 }
8631
8632 #[test]
8633 fn intrinsic_width_empty_container() {
8634 let engine = LayoutEngine::new();
8635 let font_context = FontContext::new();
8636
8637 let padding = 8.0;
8638 let empty = make_styled_view(
8639 Style {
8640 padding: Some(Edges::uniform(padding)),
8641 ..Default::default()
8642 },
8643 vec![],
8644 );
8645 let style = empty.style.resolve(None, 0.0);
8646 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
8647
8648 assert!(
8649 (w - padding * 2.0).abs() < 0.01,
8650 "Empty container width ({}) should equal horizontal padding ({})",
8651 w,
8652 padding * 2.0
8653 );
8654 }
8655
8656 #[test]
8659 fn flex_shrink_respects_min_content_width() {
8660 let engine = LayoutEngine::new();
8664 let font_context = FontContext::new();
8665
8666 let sale_text = make_text("SALE", 12.0);
8667 let sale_style = sale_text.style.resolve(None, 0.0);
8668 let sale_word_width =
8669 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8670 assert!(
8671 sale_word_width > 0.0,
8672 "SALE should have non-zero min-content width"
8673 );
8674
8675 let container = make_styled_view(
8678 Style {
8679 flex_direction: Some(FlexDirection::Row),
8680 width: Some(Dimension::Pt(100.0)),
8681 ..Default::default()
8682 },
8683 vec![
8684 make_styled_view(
8685 Style {
8686 width: Some(Dimension::Pt(80.0)),
8687 flex_shrink: Some(1.0),
8688 ..Default::default()
8689 },
8690 vec![],
8691 ),
8692 make_styled_view(
8693 Style {
8694 width: Some(Dimension::Pt(60.0)),
8695 flex_shrink: Some(1.0),
8696 ..Default::default()
8697 },
8698 vec![make_text("SALE", 12.0)],
8699 ),
8700 ],
8701 );
8702
8703 let doc = Document {
8704 children: vec![Node::page(
8705 PageConfig::default(),
8706 Style::default(),
8707 vec![container],
8708 )],
8709 metadata: Default::default(),
8710 default_page: PageConfig::default(),
8711 first_page: None,
8712 left_page: None,
8713 right_page: None,
8714 named_pages: Default::default(),
8715 attachments: vec![],
8716 zugferd: None,
8717 fonts: vec![],
8718 tagged: false,
8719 pdfa: None,
8720 default_style: None,
8721 embedded_data: None,
8722 flatten_forms: false,
8723 pdf_ua: false,
8724 certification: None,
8725 pdf_version: Default::default(),
8726 pdf_ua2: false,
8727 };
8728
8729 let pages = engine.layout(&doc, &font_context);
8730 assert!(!pages.is_empty());
8731
8732 let page = &pages[0];
8735 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8737 assert!(
8738 container_el.is_some(),
8739 "Should find container with 2 children"
8740 );
8741 let sale_child = &container_el.unwrap().children[1];
8742 assert!(
8743 sale_child.width >= sale_word_width - 0.01,
8744 "SALE child width ({}) should be >= min-content width ({})",
8745 sale_child.width,
8746 sale_word_width
8747 );
8748 }
8749
8750 #[test]
8753 fn column_justify_content_center() {
8754 let engine = LayoutEngine::new();
8757 let font_context = FontContext::new();
8758
8759 let container = make_styled_view(
8760 Style {
8761 flex_direction: Some(FlexDirection::Column),
8762 height: Some(Dimension::Pt(200.0)),
8763 justify_content: Some(JustifyContent::Center),
8764 ..Default::default()
8765 },
8766 vec![make_text("Centered", 12.0)],
8767 );
8768
8769 let doc = Document {
8770 children: vec![Node::page(
8771 PageConfig::default(),
8772 Style::default(),
8773 vec![container],
8774 )],
8775 metadata: Default::default(),
8776 default_page: PageConfig::default(),
8777 first_page: None,
8778 left_page: None,
8779 right_page: None,
8780 named_pages: Default::default(),
8781 attachments: vec![],
8782 zugferd: None,
8783 fonts: vec![],
8784 tagged: false,
8785 pdfa: None,
8786 default_style: None,
8787 embedded_data: None,
8788 flatten_forms: false,
8789 pdf_ua: false,
8790 certification: None,
8791 pdf_version: Default::default(),
8792 pdf_ua2: false,
8793 };
8794
8795 let pages = engine.layout(&doc, &font_context);
8796 let page = &pages[0];
8797
8798 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8801 assert!(
8802 container_el.is_some(),
8803 "Should find container with children"
8804 );
8805 let container_el = container_el.unwrap();
8806 let child = &container_el.children[0];
8807
8808 let child_offset = child.y - container_el.y;
8810 let expected_offset = (200.0 - child.height) / 2.0;
8811 assert!(
8812 (child_offset - expected_offset).abs() < 2.0,
8813 "Child offset ({}) should be near center ({})",
8814 child_offset,
8815 expected_offset
8816 );
8817 }
8818
8819 #[test]
8820 fn column_align_items_center() {
8821 let engine = LayoutEngine::new();
8824 let font_context = FontContext::new();
8825
8826 let container = make_styled_view(
8827 Style {
8828 flex_direction: Some(FlexDirection::Column),
8829 width: Some(Dimension::Pt(300.0)),
8830 align_items: Some(AlignItems::Center),
8831 ..Default::default()
8832 },
8833 vec![make_text("Hi", 12.0)],
8834 );
8835
8836 let doc = Document {
8837 children: vec![Node::page(
8838 PageConfig::default(),
8839 Style::default(),
8840 vec![container],
8841 )],
8842 metadata: Default::default(),
8843 default_page: PageConfig::default(),
8844 first_page: None,
8845 left_page: None,
8846 right_page: None,
8847 named_pages: Default::default(),
8848 attachments: vec![],
8849 zugferd: None,
8850 fonts: vec![],
8851 tagged: false,
8852 pdfa: None,
8853 default_style: None,
8854 embedded_data: None,
8855 flatten_forms: false,
8856 pdf_ua: false,
8857 certification: None,
8858 pdf_version: Default::default(),
8859 pdf_ua2: false,
8860 };
8861
8862 let pages = engine.layout(&doc, &font_context);
8863 let page = &pages[0];
8864
8865 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8866 assert!(container_el.is_some());
8867 let container_el = container_el.unwrap();
8868 let child = &container_el.children[0];
8869
8870 let child_center = child.x + child.width / 2.0;
8872 let container_center = container_el.x + container_el.width / 2.0;
8873 assert!(
8874 (child_center - container_center).abs() < 2.0,
8875 "Child center ({}) should be near container center ({})",
8876 child_center,
8877 container_center
8878 );
8879 }
8880
8881 #[test]
8884 fn absolute_child_positioned_relative_to_parent() {
8885 let engine = LayoutEngine::new();
8889 let font_context = FontContext::new();
8890
8891 let parent = make_styled_view(
8892 Style {
8893 position: Some(crate::model::Position::Relative),
8894 margin: Some(MarginEdges::from_edges(Edges {
8895 top: 50.0,
8896 left: 50.0,
8897 ..Default::default()
8898 })),
8899 width: Some(Dimension::Pt(200.0)),
8900 height: Some(Dimension::Pt(200.0)),
8901 ..Default::default()
8902 },
8903 vec![make_styled_view(
8904 Style {
8905 position: Some(crate::model::Position::Absolute),
8906 top: Some(10.0),
8907 left: Some(10.0),
8908 width: Some(Dimension::Pt(50.0)),
8909 height: Some(Dimension::Pt(50.0)),
8910 ..Default::default()
8911 },
8912 vec![],
8913 )],
8914 );
8915
8916 let doc = Document {
8917 children: vec![Node::page(
8918 PageConfig::default(),
8919 Style::default(),
8920 vec![parent],
8921 )],
8922 metadata: Default::default(),
8923 default_page: PageConfig::default(),
8924 first_page: None,
8925 left_page: None,
8926 right_page: None,
8927 named_pages: Default::default(),
8928 attachments: vec![],
8929 zugferd: None,
8930 fonts: vec![],
8931 tagged: false,
8932 pdfa: None,
8933 default_style: None,
8934 embedded_data: None,
8935 flatten_forms: false,
8936 pdf_ua: false,
8937 certification: None,
8938 pdf_version: Default::default(),
8939 pdf_ua2: false,
8940 };
8941
8942 let pages = engine.layout(&doc, &font_context);
8943 let page = &pages[0];
8944
8945 let parent_el = page
8948 .elements
8949 .iter()
8950 .find(|e| e.width > 190.0 && e.width < 210.0);
8951 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8952 let parent_el = parent_el.unwrap();
8953
8954 let abs_child = parent_el
8956 .children
8957 .iter()
8958 .find(|e| e.width > 45.0 && e.width < 55.0);
8959 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8960 let abs_child = abs_child.unwrap();
8961
8962 let expected_x = parent_el.x + 10.0;
8963 let expected_y = parent_el.y + 10.0;
8964 assert!(
8965 (abs_child.x - expected_x).abs() < 1.0,
8966 "Absolute child x ({}) should be parent.x + 10 ({})",
8967 abs_child.x,
8968 expected_x
8969 );
8970 assert!(
8971 (abs_child.y - expected_y).abs() < 1.0,
8972 "Absolute child y ({}) should be parent.y + 10 ({})",
8973 abs_child.y,
8974 expected_y
8975 );
8976 }
8977
8978 #[test]
8979 fn absolute_escapes_unpositioned_parent_to_page() {
8980 let engine = LayoutEngine::new();
8985 let font_context = FontContext::new();
8986 let parent = make_styled_view(
8987 Style {
8988 margin: Some(MarginEdges::from_edges(Edges {
8989 top: 50.0,
8990 left: 50.0,
8991 ..Default::default()
8992 })),
8993 width: Some(Dimension::Pt(200.0)),
8994 height: Some(Dimension::Pt(200.0)),
8995 ..Default::default()
8996 },
8997 vec![make_styled_view(
8998 Style {
8999 position: Some(crate::model::Position::Absolute),
9000 top: Some(10.0),
9001 left: Some(10.0),
9002 width: Some(Dimension::Pt(50.0)),
9003 height: Some(Dimension::Pt(50.0)),
9004 ..Default::default()
9005 },
9006 vec![],
9007 )],
9008 );
9009 let doc = Document {
9010 children: vec![Node::page(
9011 PageConfig::default(),
9012 Style::default(),
9013 vec![parent],
9014 )],
9015 metadata: Default::default(),
9016 default_page: PageConfig::default(),
9017 first_page: None,
9018 left_page: None,
9019 right_page: None,
9020 named_pages: Default::default(),
9021 attachments: vec![],
9022 zugferd: None,
9023 fonts: vec![],
9024 tagged: false,
9025 pdfa: None,
9026 default_style: None,
9027 embedded_data: None,
9028 flatten_forms: false,
9029 pdf_ua: false,
9030 certification: None,
9031 pdf_version: Default::default(),
9032 pdf_ua2: false,
9033 };
9034 let pages = engine.layout(&doc, &font_context);
9035 let page = &pages[0];
9036 let parent_el = page
9037 .elements
9038 .iter()
9039 .find(|e| e.width > 190.0 && e.width < 210.0)
9040 .expect("parent");
9041 let abs_child = parent_el
9042 .children
9043 .iter()
9044 .find(|e| e.width > 45.0 && e.width < 55.0)
9045 .expect("abs child");
9046 let page_left = PageConfig::default().margin.left;
9047 let page_top = PageConfig::default().margin.top;
9048 assert!(
9049 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
9050 "absolute escapes to the page: x {} should be page_left + 10 ({})",
9051 abs_child.x,
9052 page_left + 10.0
9053 );
9054 assert!(
9055 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
9056 "absolute escapes to the page: y {} should be page_top + 10 ({})",
9057 abs_child.y,
9058 page_top + 10.0
9059 );
9060 assert!(
9061 abs_child.x < parent_el.x,
9062 "child must no longer be parent-relative (parent is 50pt further in)"
9063 );
9064 }
9065
9066 #[test]
9067 fn text_transform_none_passthrough() {
9068 assert_eq!(
9069 apply_text_transform("Hello World", TextTransform::None),
9070 "Hello World"
9071 );
9072 }
9073
9074 #[test]
9075 fn text_transform_uppercase() {
9076 assert_eq!(
9077 apply_text_transform("hello world", TextTransform::Uppercase),
9078 "HELLO WORLD"
9079 );
9080 }
9081
9082 #[test]
9083 fn text_transform_lowercase() {
9084 assert_eq!(
9085 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
9086 "hello world"
9087 );
9088 }
9089
9090 #[test]
9091 fn text_transform_capitalize() {
9092 assert_eq!(
9093 apply_text_transform("hello world", TextTransform::Capitalize),
9094 "Hello World"
9095 );
9096 assert_eq!(
9097 apply_text_transform(" hello world ", TextTransform::Capitalize),
9098 " Hello World "
9099 );
9100 assert_eq!(
9101 apply_text_transform("already Capitalized", TextTransform::Capitalize),
9102 "Already Capitalized"
9103 );
9104 }
9105
9106 #[test]
9107 fn text_transform_capitalize_empty() {
9108 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
9109 }
9110
9111 #[test]
9112 fn apply_char_transform_uppercase() {
9113 assert_eq!(
9114 apply_char_transform('a', TextTransform::Uppercase, false),
9115 'A'
9116 );
9117 assert_eq!(
9118 apply_char_transform('A', TextTransform::Uppercase, false),
9119 'A'
9120 );
9121 }
9122
9123 #[test]
9124 fn apply_char_transform_capitalize_word_start() {
9125 assert_eq!(
9126 apply_char_transform('h', TextTransform::Capitalize, true),
9127 'H'
9128 );
9129 assert_eq!(
9130 apply_char_transform('h', TextTransform::Capitalize, false),
9131 'h'
9132 );
9133 }
9134
9135 #[test]
9138 fn column_flex_grow_single_child_fills_container() {
9139 let engine = LayoutEngine::new();
9142 let font_context = FontContext::new();
9143
9144 let child = make_styled_view(
9145 Style {
9146 flex_grow: Some(1.0),
9147 ..Default::default()
9148 },
9149 vec![make_text("Short", 12.0)],
9150 );
9151
9152 let container = make_styled_view(
9153 Style {
9154 flex_direction: Some(FlexDirection::Column),
9155 height: Some(Dimension::Pt(300.0)),
9156 ..Default::default()
9157 },
9158 vec![child],
9159 );
9160
9161 let doc = Document {
9162 children: vec![Node::page(
9163 PageConfig::default(),
9164 Style::default(),
9165 vec![container],
9166 )],
9167 metadata: Default::default(),
9168 default_page: PageConfig::default(),
9169 first_page: None,
9170 left_page: None,
9171 right_page: None,
9172 named_pages: Default::default(),
9173 attachments: vec![],
9174 zugferd: None,
9175 fonts: vec![],
9176 tagged: false,
9177 pdfa: None,
9178 default_style: None,
9179 embedded_data: None,
9180 flatten_forms: false,
9181 pdf_ua: false,
9182 certification: None,
9183 pdf_version: Default::default(),
9184 pdf_ua2: false,
9185 };
9186
9187 let pages = engine.layout(&doc, &font_context);
9188 let page = &pages[0];
9189
9190 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
9191 assert!(container_el.is_some());
9192 let container_el = container_el.unwrap();
9193 assert!(
9194 (container_el.height - 300.0).abs() < 1.0,
9195 "Container should be 300pt, got {}",
9196 container_el.height
9197 );
9198
9199 let child_el = &container_el.children[0];
9200 assert!(
9201 (child_el.height - 300.0).abs() < 1.0,
9202 "flex-grow child should expand to 300pt, got {}",
9203 child_el.height
9204 );
9205 }
9206
9207 #[test]
9208 fn column_flex_grow_two_children_proportional() {
9209 let engine = LayoutEngine::new();
9212 let font_context = FontContext::new();
9213
9214 let child1 = make_styled_view(
9215 Style {
9216 flex_grow: Some(1.0),
9217 ..Default::default()
9218 },
9219 vec![make_text("A", 12.0)],
9220 );
9221 let child2 = make_styled_view(
9222 Style {
9223 flex_grow: Some(2.0),
9224 ..Default::default()
9225 },
9226 vec![make_text("B", 12.0)],
9227 );
9228
9229 let container = make_styled_view(
9230 Style {
9231 flex_direction: Some(FlexDirection::Column),
9232 height: Some(Dimension::Pt(300.0)),
9233 ..Default::default()
9234 },
9235 vec![child1, child2],
9236 );
9237
9238 let doc = Document {
9239 children: vec![Node::page(
9240 PageConfig::default(),
9241 Style::default(),
9242 vec![container],
9243 )],
9244 metadata: Default::default(),
9245 default_page: PageConfig::default(),
9246 first_page: None,
9247 left_page: None,
9248 right_page: None,
9249 named_pages: Default::default(),
9250 attachments: vec![],
9251 zugferd: None,
9252 fonts: vec![],
9253 tagged: false,
9254 pdfa: None,
9255 default_style: None,
9256 embedded_data: None,
9257 flatten_forms: false,
9258 pdf_ua: false,
9259 certification: None,
9260 pdf_version: Default::default(),
9261 pdf_ua2: false,
9262 };
9263
9264 let pages = engine.layout(&doc, &font_context);
9265 let page = &pages[0];
9266
9267 let container_el = page
9268 .elements
9269 .iter()
9270 .find(|e| e.children.len() == 2)
9271 .expect("Should find container with two children");
9272
9273 let c1 = &container_el.children[0];
9274 let c2 = &container_el.children[1];
9275
9276 let total = c1.height + c2.height;
9280 assert!(
9281 (total - 300.0).abs() < 2.0,
9282 "Children should sum to ~300pt, got {}",
9283 total
9284 );
9285
9286 let ratio = c2.height / c1.height;
9289 assert!(
9290 ratio > 1.3 && ratio < 2.5,
9291 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
9292 ratio
9293 );
9294 }
9295
9296 #[test]
9297 fn column_flex_grow_mixed_grow_and_fixed() {
9298 let engine = LayoutEngine::new();
9301 let font_context = FontContext::new();
9302
9303 let fixed_child = make_styled_view(
9304 Style {
9305 height: Some(Dimension::Pt(50.0)),
9306 ..Default::default()
9307 },
9308 vec![make_text("Fixed", 12.0)],
9309 );
9310 let grow_child = make_styled_view(
9311 Style {
9312 flex_grow: Some(1.0),
9313 ..Default::default()
9314 },
9315 vec![make_text("Grow", 12.0)],
9316 );
9317
9318 let container = make_styled_view(
9319 Style {
9320 flex_direction: Some(FlexDirection::Column),
9321 height: Some(Dimension::Pt(300.0)),
9322 ..Default::default()
9323 },
9324 vec![fixed_child, grow_child],
9325 );
9326
9327 let doc = Document {
9328 children: vec![Node::page(
9329 PageConfig::default(),
9330 Style::default(),
9331 vec![container],
9332 )],
9333 metadata: Default::default(),
9334 default_page: PageConfig::default(),
9335 first_page: None,
9336 left_page: None,
9337 right_page: None,
9338 named_pages: Default::default(),
9339 attachments: vec![],
9340 zugferd: None,
9341 fonts: vec![],
9342 tagged: false,
9343 pdfa: None,
9344 default_style: None,
9345 embedded_data: None,
9346 flatten_forms: false,
9347 pdf_ua: false,
9348 certification: None,
9349 pdf_version: Default::default(),
9350 pdf_ua2: false,
9351 };
9352
9353 let pages = engine.layout(&doc, &font_context);
9354 let page = &pages[0];
9355
9356 let container_el = page
9357 .elements
9358 .iter()
9359 .find(|e| e.children.len() == 2)
9360 .expect("Should find container with two children");
9361
9362 let fixed_el = &container_el.children[0];
9363 let grow_el = &container_el.children[1];
9364
9365 assert!(
9367 (fixed_el.height - 50.0).abs() < 1.0,
9368 "Fixed child should stay at 50pt, got {}",
9369 fixed_el.height
9370 );
9371
9372 assert!(
9374 (grow_el.height - 250.0).abs() < 2.0,
9375 "Grow child should expand to ~250pt, got {}",
9376 grow_el.height
9377 );
9378 }
9379
9380 #[test]
9381 fn column_flex_grow_page_level() {
9382 let engine = LayoutEngine::new();
9384 let font_context = FontContext::new();
9385
9386 let grow_child = make_styled_view(
9387 Style {
9388 flex_grow: Some(1.0),
9389 ..Default::default()
9390 },
9391 vec![make_text("Fill page", 12.0)],
9392 );
9393
9394 let doc = Document {
9395 children: vec![Node::page(
9396 PageConfig::default(),
9397 Style::default(),
9398 vec![grow_child],
9399 )],
9400 metadata: Default::default(),
9401 default_page: PageConfig::default(),
9402 first_page: None,
9403 left_page: None,
9404 right_page: None,
9405 named_pages: Default::default(),
9406 attachments: vec![],
9407 zugferd: None,
9408 fonts: vec![],
9409 tagged: false,
9410 pdfa: None,
9411 default_style: None,
9412 embedded_data: None,
9413 flatten_forms: false,
9414 pdf_ua: false,
9415 certification: None,
9416 pdf_version: Default::default(),
9417 pdf_ua2: false,
9418 };
9419
9420 let pages = engine.layout(&doc, &font_context);
9421 let page = &pages[0];
9422
9423 assert!(
9425 !page.elements.is_empty(),
9426 "Page should have at least one element"
9427 );
9428
9429 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
9430 let el = &page.elements[0];
9431 assert!(
9432 (el.height - content_height).abs() < 2.0,
9433 "Page-level flex-grow child should fill content height ({}), got {}",
9434 content_height,
9435 el.height
9436 );
9437 }
9438
9439 #[test]
9440 fn column_flex_grow_with_justify_content() {
9441 let engine = LayoutEngine::new();
9445 let font_context = FontContext::new();
9446
9447 let fixed_child = make_styled_view(
9448 Style {
9449 height: Some(Dimension::Pt(50.0)),
9450 ..Default::default()
9451 },
9452 vec![make_text("Top", 12.0)],
9453 );
9454 let grow_child = make_styled_view(
9455 Style {
9456 flex_grow: Some(1.0),
9457 ..Default::default()
9458 },
9459 vec![make_text("Fill", 12.0)],
9460 );
9461
9462 let container = make_styled_view(
9463 Style {
9464 flex_direction: Some(FlexDirection::Column),
9465 height: Some(Dimension::Pt(300.0)),
9466 justify_content: Some(JustifyContent::Center),
9467 ..Default::default()
9468 },
9469 vec![fixed_child, grow_child],
9470 );
9471
9472 let doc = Document {
9473 children: vec![Node::page(
9474 PageConfig::default(),
9475 Style::default(),
9476 vec![container],
9477 )],
9478 metadata: Default::default(),
9479 default_page: PageConfig::default(),
9480 first_page: None,
9481 left_page: None,
9482 right_page: None,
9483 named_pages: Default::default(),
9484 attachments: vec![],
9485 zugferd: None,
9486 fonts: vec![],
9487 tagged: false,
9488 pdfa: None,
9489 default_style: None,
9490 embedded_data: None,
9491 flatten_forms: false,
9492 pdf_ua: false,
9493 certification: None,
9494 pdf_version: Default::default(),
9495 pdf_ua2: false,
9496 };
9497
9498 let pages = engine.layout(&doc, &font_context);
9499 let page = &pages[0];
9500
9501 let container_el = page
9502 .elements
9503 .iter()
9504 .find(|e| e.children.len() == 2)
9505 .expect("Should find container");
9506
9507 let first_child = &container_el.children[0];
9510 assert!(
9511 (first_child.y - container_el.y).abs() < 1.0,
9512 "First child should be at top of container"
9513 );
9514
9515 let total = container_el.children[0].height + container_el.children[1].height;
9517 assert!(
9518 (total - 300.0).abs() < 2.0,
9519 "Children should fill container, got {}",
9520 total
9521 );
9522 }
9523
9524 #[test]
9525 fn column_flex_grow_child_justify_content_center() {
9526 let engine = LayoutEngine::new();
9529 let font_context = FontContext::new();
9530
9531 let inner_box = make_styled_view(
9533 Style {
9534 height: Some(Dimension::Pt(40.0)),
9535 ..Default::default()
9536 },
9537 vec![make_text("Centered", 12.0)],
9538 );
9539
9540 let grow_child = make_styled_view(
9542 Style {
9543 flex_grow: Some(1.0),
9544 flex_direction: Some(FlexDirection::Column),
9545 justify_content: Some(JustifyContent::Center),
9546 ..Default::default()
9547 },
9548 vec![inner_box],
9549 );
9550
9551 let container = make_styled_view(
9553 Style {
9554 flex_direction: Some(FlexDirection::Column),
9555 height: Some(Dimension::Pt(400.0)),
9556 ..Default::default()
9557 },
9558 vec![grow_child],
9559 );
9560
9561 let doc = Document {
9562 children: vec![Node::page(
9563 PageConfig::default(),
9564 Style::default(),
9565 vec![container],
9566 )],
9567 metadata: Default::default(),
9568 default_page: PageConfig::default(),
9569 first_page: None,
9570 left_page: None,
9571 right_page: None,
9572 named_pages: Default::default(),
9573 attachments: vec![],
9574 zugferd: None,
9575 fonts: vec![],
9576 tagged: false,
9577 pdfa: None,
9578 default_style: None,
9579 embedded_data: None,
9580 flatten_forms: false,
9581 pdf_ua: false,
9582 certification: None,
9583 pdf_version: Default::default(),
9584 pdf_ua2: false,
9585 };
9586
9587 let pages = engine.layout(&doc, &font_context);
9588 let page = &pages[0];
9589
9590 let container_el = page
9592 .elements
9593 .iter()
9594 .find(|e| e.height > 350.0 && e.children.len() == 1)
9595 .expect("Should find outer container");
9596
9597 let grow_el = &container_el.children[0];
9598 assert!(
9599 (grow_el.height - 400.0).abs() < 2.0,
9600 "Grow child should expand to 400, got {}",
9601 grow_el.height
9602 );
9603
9604 let inner_el = &grow_el.children[0];
9606 let expected_center = grow_el.y + grow_el.height / 2.0;
9607 let actual_center = inner_el.y + inner_el.height / 2.0;
9608 assert!(
9609 (actual_center - expected_center).abs() < 2.0,
9610 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
9611 expected_center,
9612 actual_center
9613 );
9614 }
9615
9616 #[test]
9617 fn column_flex_grow_child_justify_content_flex_end() {
9618 let engine = LayoutEngine::new();
9620 let font_context = FontContext::new();
9621
9622 let inner_box = make_styled_view(
9623 Style {
9624 height: Some(Dimension::Pt(30.0)),
9625 ..Default::default()
9626 },
9627 vec![make_text("Bottom", 12.0)],
9628 );
9629
9630 let grow_child = make_styled_view(
9631 Style {
9632 flex_grow: Some(1.0),
9633 flex_direction: Some(FlexDirection::Column),
9634 justify_content: Some(JustifyContent::FlexEnd),
9635 ..Default::default()
9636 },
9637 vec![inner_box],
9638 );
9639
9640 let container = make_styled_view(
9641 Style {
9642 flex_direction: Some(FlexDirection::Column),
9643 height: Some(Dimension::Pt(300.0)),
9644 ..Default::default()
9645 },
9646 vec![grow_child],
9647 );
9648
9649 let doc = Document {
9650 children: vec![Node::page(
9651 PageConfig::default(),
9652 Style::default(),
9653 vec![container],
9654 )],
9655 metadata: Default::default(),
9656 default_page: PageConfig::default(),
9657 first_page: None,
9658 left_page: None,
9659 right_page: None,
9660 named_pages: Default::default(),
9661 attachments: vec![],
9662 zugferd: None,
9663 fonts: vec![],
9664 tagged: false,
9665 pdfa: None,
9666 default_style: None,
9667 embedded_data: None,
9668 flatten_forms: false,
9669 pdf_ua: false,
9670 certification: None,
9671 pdf_version: Default::default(),
9672 pdf_ua2: false,
9673 };
9674
9675 let pages = engine.layout(&doc, &font_context);
9676 let page = &pages[0];
9677
9678 let container_el = page
9679 .elements
9680 .iter()
9681 .find(|e| e.height > 250.0 && e.children.len() == 1)
9682 .expect("Should find outer container");
9683
9684 let grow_el = &container_el.children[0];
9685 let inner_el = &grow_el.children[0];
9686
9687 let inner_bottom = inner_el.y + inner_el.height;
9689 let grow_bottom = grow_el.y + grow_el.height;
9690 assert!(
9691 (inner_bottom - grow_bottom).abs() < 2.0,
9692 "Inner box bottom ({}) should align with grow child bottom ({})",
9693 inner_bottom,
9694 grow_bottom
9695 );
9696 }
9697
9698 #[test]
9699 fn column_flex_grow_child_no_justify_unchanged() {
9700 let engine = LayoutEngine::new();
9702 let font_context = FontContext::new();
9703
9704 let inner_box = make_styled_view(
9705 Style {
9706 height: Some(Dimension::Pt(50.0)),
9707 ..Default::default()
9708 },
9709 vec![make_text("Top", 12.0)],
9710 );
9711
9712 let grow_child = make_styled_view(
9713 Style {
9714 flex_grow: Some(1.0),
9715 flex_direction: Some(FlexDirection::Column),
9716 ..Default::default()
9718 },
9719 vec![inner_box],
9720 );
9721
9722 let container = make_styled_view(
9723 Style {
9724 flex_direction: Some(FlexDirection::Column),
9725 height: Some(Dimension::Pt(300.0)),
9726 ..Default::default()
9727 },
9728 vec![grow_child],
9729 );
9730
9731 let doc = Document {
9732 children: vec![Node::page(
9733 PageConfig::default(),
9734 Style::default(),
9735 vec![container],
9736 )],
9737 metadata: Default::default(),
9738 default_page: PageConfig::default(),
9739 first_page: None,
9740 left_page: None,
9741 right_page: None,
9742 named_pages: Default::default(),
9743 attachments: vec![],
9744 zugferd: None,
9745 fonts: vec![],
9746 tagged: false,
9747 pdfa: None,
9748 default_style: None,
9749 embedded_data: None,
9750 flatten_forms: false,
9751 pdf_ua: false,
9752 certification: None,
9753 pdf_version: Default::default(),
9754 pdf_ua2: false,
9755 };
9756
9757 let pages = engine.layout(&doc, &font_context);
9758 let page = &pages[0];
9759
9760 let container_el = page
9761 .elements
9762 .iter()
9763 .find(|e| e.height > 250.0 && e.children.len() == 1)
9764 .expect("Should find outer container");
9765
9766 let grow_el = &container_el.children[0];
9767 let inner_el = &grow_el.children[0];
9768
9769 assert!(
9771 (inner_el.y - grow_el.y).abs() < 2.0,
9772 "Inner box ({}) should be at top of grow child ({})",
9773 inner_el.y,
9774 grow_el.y
9775 );
9776 }
9777
9778 #[test]
9779 fn column_flex_grow_child_align_items_center() {
9780 let engine = LayoutEngine::new();
9782 let font_context = FontContext::new();
9783
9784 let text = make_text("Hello", 12.0);
9785
9786 let grow_child = make_styled_view(
9787 Style {
9788 flex_grow: Some(1.0),
9789 flex_direction: Some(FlexDirection::Column),
9790 align_items: Some(AlignItems::Center),
9791 ..Default::default()
9792 },
9793 vec![text],
9794 );
9795
9796 let container = make_styled_view(
9797 Style {
9798 flex_direction: Some(FlexDirection::Column),
9799 height: Some(Dimension::Pt(300.0)),
9800 ..Default::default()
9801 },
9802 vec![grow_child],
9803 );
9804
9805 let doc = Document {
9806 children: vec![Node::page(
9807 PageConfig::default(),
9808 Style::default(),
9809 vec![container],
9810 )],
9811 metadata: Default::default(),
9812 default_page: PageConfig::default(),
9813 first_page: None,
9814 left_page: None,
9815 right_page: None,
9816 named_pages: Default::default(),
9817 attachments: vec![],
9818 zugferd: None,
9819 fonts: vec![],
9820 tagged: false,
9821 pdfa: None,
9822 default_style: None,
9823 embedded_data: None,
9824 flatten_forms: false,
9825 pdf_ua: false,
9826 certification: None,
9827 pdf_version: Default::default(),
9828 pdf_ua2: false,
9829 };
9830
9831 let pages = engine.layout(&doc, &font_context);
9832 let page = &pages[0];
9833
9834 let container_el = page
9835 .elements
9836 .iter()
9837 .find(|e| e.height > 250.0 && e.children.len() == 1)
9838 .expect("Should find outer container");
9839
9840 let grow_el = &container_el.children[0];
9841 assert!(
9842 !grow_el.children.is_empty(),
9843 "Grow child should have text child"
9844 );
9845
9846 let text_el = &grow_el.children[0];
9847 let text_center = text_el.x + text_el.width / 2.0;
9848 let grow_center = grow_el.x + grow_el.width / 2.0;
9849 assert!(
9850 (text_center - grow_center).abs() < 2.0,
9851 "Text center ({}) should be near grow child center ({})",
9852 text_center,
9853 grow_center
9854 );
9855 }
9856
9857 #[test]
9858 fn image_intrinsic_width_respects_height_constraint() {
9859 let engine = LayoutEngine::new();
9863 let font_context = FontContext::new();
9864
9865 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9867
9868 let image_node = Node {
9869 kind: NodeKind::Image {
9870 src: one_px_png.to_string(),
9871 width: None,
9872 height: Some(36.0),
9873 },
9874 style: Style::default(),
9875 children: vec![],
9876 id: None,
9877 source_location: None,
9878 bookmark: None,
9879 href: None,
9880 alt: None,
9881 };
9882
9883 let resolved = image_node.style.resolve(None, 0.0);
9884 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9885
9886 assert!(
9888 (intrinsic - 36.0).abs() < 1.0,
9889 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9890 intrinsic
9891 );
9892 }
9893}