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 mut glyphs =
4953 self.build_positioned_glyphs_single_style(line, style, href, font_context);
4954
4955 let rendered_width = if glyphs.is_empty() {
4959 line.width
4960 } else {
4961 let last = &glyphs[glyphs.len() - 1];
4962 (last.x_offset + last.x_advance).max(line.width * 0.5)
4963 };
4964
4965 let line_x = match style.text_align {
4966 TextAlign::Left => text_x,
4967 TextAlign::Right => text_x + text_width - rendered_width,
4968 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
4969 TextAlign::Justify => text_x,
4970 };
4971
4972 let is_last_line = line_idx == lines.len() - 1 || line.hard_break;
4982 let user_ws = style.word_spacing;
4983 let (justified_width, word_spacing) =
4984 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4985 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
4986 let (natural_width, space_count) = if let Some(idx) = last_non_space {
4987 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
4988 let s = glyphs[..=idx]
4989 .iter()
4990 .filter(|g| g.char_value == ' ')
4991 .count();
4992 (w, s)
4993 } else {
4994 (0.0, 0)
4995 };
4996 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
4997 Self::spread_justification(&mut glyphs, text_width);
4998 }
4999 let slack = text_width - natural_width;
5000 let ws = if space_count > 0 && slack.abs() > 0.01 {
5001 slack / space_count as f64
5002 } else {
5003 0.0
5004 };
5005 (text_width, user_ws + ws)
5006 } else {
5007 (rendered_width, user_ws)
5008 };
5009
5010 let text_line = TextLine {
5011 x: line_x,
5012 y: cursor.content_y
5018 + cursor.y
5019 + baseline_in_line(
5020 line_height,
5021 style.font_size,
5022 font_context.baseline_metrics(
5023 &style.font_family,
5024 style.font_weight,
5025 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5026 ),
5027 ),
5028 glyphs,
5029 width: justified_width,
5030 height: line_height,
5031 word_spacing,
5032 };
5033
5034 cursor.elements.push(LayoutElement {
5035 x: line_x,
5036 y: cursor.content_y + cursor.y,
5037 width: justified_width,
5038 height: line_height,
5039 draw: DrawCommand::Text {
5040 lines: vec![text_line],
5041 color: style.color,
5042 text_decoration: style.text_decoration,
5043 opacity: 1.0,
5044 },
5045 children: vec![],
5046 node_type: Some("TextLine".to_string()),
5047 resolved_style: Some(style.clone()),
5048 source_location: None,
5049 href: href.map(|s| s.to_string()),
5050 bookmark: None,
5051 alt: None,
5052 is_header_row: false,
5053 actual_text: None,
5054 list_numbering: None,
5055 col_span: 1,
5056 overflow: Overflow::default(),
5057 opacity: 1.0,
5058 });
5059
5060 cursor.y += line_height;
5061 }
5062
5063 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5065 if !line_elements.is_empty() {
5066 let container_height = cursor.content_y + cursor.y - container_start_y;
5067 cursor.elements.push(LayoutElement {
5068 x: text_x,
5069 y: container_start_y,
5070 width: text_width,
5071 height: container_height,
5072 draw: DrawCommand::None,
5073 children: line_elements,
5074 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5075 resolved_style: Some(style.clone()),
5076 source_location: source_location.cloned(),
5077 href: href.map(|s| s.to_string()),
5078 bookmark: if is_first_element {
5079 bookmark.map(|s| s.to_string())
5080 } else {
5081 None
5082 },
5083 alt: None,
5084 is_header_row: false,
5085 actual_text: None,
5086 list_numbering: None,
5087 col_span: 1,
5088 overflow: Overflow::default(),
5089 opacity: 1.0,
5090 });
5091 }
5092
5093 cursor.y += margin.bottom;
5094 }
5095
5096 fn spread_justification(glyphs: &mut [PositionedGlyph], width: f64) {
5108 let Some(last) = glyphs.iter().rposition(|g| g.char_value != ' ') else {
5109 return;
5110 };
5111 let spaces = glyphs[..last]
5112 .iter()
5113 .filter(|g| g.char_value == ' ')
5114 .count();
5115 let gap = width - (glyphs[last].x_offset + glyphs[last].x_advance);
5116 if spaces == 0 || gap.abs() <= 0.01 {
5117 return;
5118 }
5119 let share = gap / spaces as f64;
5120 let mut seen = 0usize;
5121 for g in glyphs.iter_mut() {
5122 g.x_offset += share * seen as f64;
5123 if g.char_value == ' ' {
5124 seen += 1;
5125 }
5126 }
5127 }
5128
5129 fn runs_to_styled_chars(
5133 runs: &[TextRun],
5134 parent_href: Option<&str>,
5135 style: &ResolvedStyle,
5136 width: f64,
5137 ) -> Vec<StyledChar> {
5138 let mut styled_chars: Vec<StyledChar> = Vec::new();
5139 for run in runs {
5140 let run_style = run.style.resolve(Some(style), width);
5141 let run_href = run.href.as_deref().or(parent_href);
5142 let transform = run_style.text_transform;
5143 let run_content = substitute_page_placeholders(&run.content);
5144 let mut prev_is_whitespace = true;
5145 for ch in run_content.chars() {
5146 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
5147 prev_is_whitespace = ch.is_whitespace();
5148 styled_chars.push(StyledChar {
5149 ch: transformed_ch,
5150 font_family: run_style.font_family.clone(),
5151 font_size: run_style.font_size,
5152 font_weight: run_style.font_weight,
5153 font_style: run_style.font_style,
5154 color: run_style.color,
5155 href: run_href.map(|s| s.to_string()),
5156 text_decoration: run_style.text_decoration,
5157 letter_spacing: run_style.letter_spacing,
5158 word_spacing: run_style.word_spacing,
5159 });
5160 }
5161 }
5162 styled_chars
5163 }
5164
5165 #[allow(clippy::too_many_arguments)]
5167 #[allow(clippy::too_many_arguments)]
5168 fn layout_text_runs(
5169 &self,
5170 runs: &[TextRun],
5171 parent_href: Option<&str>,
5172 style: &ResolvedStyle,
5173 cursor: &mut PageCursor,
5174 pages: &mut Vec<LayoutPage>,
5175 text_x: f64,
5176 text_width: f64,
5177 font_context: &FontContext,
5178 source_location: Option<&SourceLocation>,
5179 bookmark: Option<&str>,
5180 node_type_override: Option<&str>,
5182 ) {
5183 let styled_chars = Self::runs_to_styled_chars(runs, parent_href, style, text_width);
5184
5185 let justify = matches!(style.text_align, TextAlign::Justify);
5187 let broken_lines = match style.line_breaking {
5188 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
5189 font_context,
5190 &styled_chars,
5191 text_width,
5192 style.hyphens,
5193 style.lang.as_deref(),
5194 justify,
5195 ),
5196 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
5197 font_context,
5198 &styled_chars,
5199 text_width,
5200 style.hyphens,
5201 style.lang.as_deref(),
5202 ),
5203 };
5204
5205 let broken_lines = match style.text_overflow {
5207 TextOverflow::Ellipsis => {
5208 self.text_layout
5209 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
5210 }
5211 TextOverflow::Clip => {
5212 self.text_layout
5213 .truncate_runs_clip(font_context, broken_lines, text_width)
5214 }
5215 TextOverflow::Wrap => broken_lines,
5216 };
5217
5218 let line_height = style.font_size * style.line_height;
5219
5220 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
5222 let decision = page_break::decide_break(
5223 cursor.remaining_height(),
5224 &line_heights,
5225 true,
5226 style.min_orphan_lines as usize,
5227 style.min_widow_lines as usize,
5228 );
5229
5230 let mut snapshot = cursor.elements.len();
5231 let mut container_start_y = cursor.content_y + cursor.y;
5232 let mut is_first_element = true;
5233
5234 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
5235 pages.push(cursor.finalize());
5236 *cursor = cursor.new_page();
5237 snapshot = cursor.elements.len();
5238 container_start_y = cursor.content_y + cursor.y;
5239 }
5240
5241 let forced_break_at = match decision {
5242 page_break::BreakDecision::Split {
5243 items_on_current_page,
5244 } => Some(items_on_current_page),
5245 _ => None,
5246 };
5247 let mut first_break_done = false;
5248
5249 for (line_idx, run_line) in broken_lines.iter().enumerate() {
5250 let needs_break = if let Some(break_at) = forced_break_at {
5251 if !first_break_done && line_idx == break_at {
5252 true
5253 } else {
5254 line_height > cursor.remaining_height()
5255 }
5256 } else {
5257 line_height > cursor.remaining_height()
5258 };
5259
5260 if needs_break {
5261 first_break_done = true;
5262 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5263 if !line_elements.is_empty() {
5264 let container_height = cursor.content_y + cursor.y - container_start_y;
5265 cursor.elements.push(LayoutElement {
5266 x: text_x,
5267 y: container_start_y,
5268 width: text_width,
5269 height: container_height,
5270 draw: DrawCommand::None,
5271 children: line_elements,
5272 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5273 resolved_style: Some(style.clone()),
5274 source_location: source_location.cloned(),
5275 href: parent_href.map(|s| s.to_string()),
5276 bookmark: if is_first_element {
5277 bookmark.map(|s| s.to_string())
5278 } else {
5279 None
5280 },
5281 alt: None,
5282 is_header_row: false,
5283 actual_text: None,
5284 list_numbering: None,
5285 col_span: 1,
5286 overflow: Overflow::default(),
5287 opacity: 1.0,
5288 });
5289 is_first_element = false;
5290 }
5291
5292 pages.push(cursor.finalize());
5293 *cursor = cursor.new_page();
5294
5295 snapshot = cursor.elements.len();
5296 container_start_y = cursor.content_y + cursor.y;
5297 }
5298
5299 let line_x = match style.text_align {
5300 TextAlign::Left => text_x,
5301 TextAlign::Right => text_x + text_width - run_line.width,
5302 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
5303 TextAlign::Justify => text_x,
5304 };
5305
5306 let mut glyphs =
5307 self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
5308
5309 let is_last_line = line_idx == broken_lines.len() - 1 || run_line.hard_break;
5318 let user_ws = style.word_spacing;
5319 let (justified_width, word_spacing) =
5320 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5321 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
5322 let (natural_width, space_count) = if let Some(idx) = last_non_space {
5323 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
5324 let s = glyphs[..=idx]
5325 .iter()
5326 .filter(|g| g.char_value == ' ')
5327 .count();
5328 (w, s)
5329 } else {
5330 (0.0, 0)
5331 };
5332 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
5333 Self::spread_justification(&mut glyphs, text_width);
5334 }
5335 let slack = text_width - natural_width;
5336 let ws = if space_count > 0 && slack.abs() > 0.01 {
5337 slack / space_count as f64
5338 } else {
5339 0.0
5340 };
5341 (text_width, user_ws + ws)
5342 } else {
5343 (run_line.width, user_ws)
5344 };
5345
5346 let text_line = TextLine {
5347 x: line_x,
5348 y: cursor.content_y
5354 + cursor.y
5355 + baseline_in_line(
5356 line_height,
5357 style.font_size,
5358 font_context.baseline_metrics(
5359 &style.font_family,
5360 style.font_weight,
5361 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
5362 ),
5363 ),
5364 glyphs,
5365 width: justified_width,
5366 height: line_height,
5367 word_spacing,
5368 };
5369
5370 let text_dec = run_line
5372 .chars
5373 .iter()
5374 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
5375 .map(|sc| sc.text_decoration)
5376 .unwrap_or(style.text_decoration);
5377
5378 cursor.elements.push(LayoutElement {
5379 x: line_x,
5380 y: cursor.content_y + cursor.y,
5381 width: justified_width,
5382 height: line_height,
5383 draw: DrawCommand::Text {
5384 lines: vec![text_line],
5385 color: style.color,
5386 text_decoration: text_dec,
5387 opacity: 1.0,
5388 },
5389 children: vec![],
5390 node_type: Some("TextLine".to_string()),
5391 resolved_style: Some(style.clone()),
5392 source_location: None,
5393 href: parent_href.map(|s| s.to_string()),
5394 bookmark: None,
5395 alt: None,
5396 is_header_row: false,
5397 actual_text: None,
5398 list_numbering: None,
5399 col_span: 1,
5400 overflow: Overflow::default(),
5401 opacity: 1.0,
5402 });
5403
5404 cursor.y += line_height;
5405 }
5406
5407 let line_elements: Vec<LayoutElement> = drain_since(&mut cursor.elements, snapshot);
5408 if !line_elements.is_empty() {
5409 let container_height = cursor.content_y + cursor.y - container_start_y;
5410 cursor.elements.push(LayoutElement {
5411 x: text_x,
5412 y: container_start_y,
5413 width: text_width,
5414 height: container_height,
5415 draw: DrawCommand::None,
5416 children: line_elements,
5417 node_type: Some(node_type_override.unwrap_or("Text").to_string()),
5418 resolved_style: Some(style.clone()),
5419 source_location: source_location.cloned(),
5420 href: parent_href.map(|s| s.to_string()),
5421 bookmark: if is_first_element {
5422 bookmark.map(|s| s.to_string())
5423 } else {
5424 None
5425 },
5426 alt: None,
5427 is_header_row: false,
5428 actual_text: None,
5429 list_numbering: None,
5430 col_span: 1,
5431 overflow: Overflow::default(),
5432 opacity: 1.0,
5433 });
5434 }
5435 }
5436
5437 fn build_positioned_glyphs_single_style(
5441 &self,
5442 line: &BrokenLine,
5443 style: &ResolvedStyle,
5444 href: Option<&str>,
5445 font_context: &FontContext,
5446 ) -> Vec<PositionedGlyph> {
5447 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
5448 let line_text: String = line.chars.iter().collect();
5449 let direction = style.direction;
5450 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5452
5453 let font_runs = crate::font::fallback::segment_by_font(
5456 &line.chars,
5457 &style.font_family,
5458 style.font_weight,
5459 italic,
5460 font_context.registry(),
5461 );
5462 let needs_per_char_fallback = font_runs.len() > 1
5463 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
5464
5465 if needs_per_char_fallback {
5467 let bidi_runs = if has_bidi {
5468 bidi::analyze_bidi(&line_text, direction)
5469 } else {
5470 vec![crate::text::bidi::BidiRun {
5471 char_start: 0,
5472 char_end: line.chars.len(),
5473 level: unicode_bidi::Level::ltr(),
5474 is_rtl: false,
5475 }]
5476 };
5477
5478 let mut all_glyphs = Vec::new();
5479 let mut bidi_levels = Vec::new();
5480 let mut x = 0.0_f64;
5481
5482 for bidi_run in &bidi_runs {
5484 for font_run in &font_runs {
5486 let start = font_run.start.max(bidi_run.char_start);
5488 let end = font_run.end.min(bidi_run.char_end);
5489 if start >= end {
5490 continue;
5491 }
5492
5493 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
5494 let sub_text: String = sub_chars.iter().collect();
5495 let resolved_family = &font_run.family;
5496
5497 if let Some(font_data) =
5498 font_context.font_data(resolved_family, style.font_weight, italic)
5499 {
5500 if let Some(shaped) = shaping::shape_text_with_direction(
5501 &sub_text,
5502 font_data,
5503 bidi_run.is_rtl,
5504 ) {
5505 let units_per_em = font_context.units_per_em(
5506 resolved_family,
5507 style.font_weight,
5508 italic,
5509 );
5510 let scale = style.font_size / units_per_em as f64;
5511
5512 let clusters = cluster_texts(&shaped, &sub_chars);
5513 for (sg, (cluster_text, ligature, extraction_text)) in
5514 shaped.iter().zip(clusters)
5515 {
5516 let cluster = sg.cluster as usize;
5517 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
5518
5519 let glyph_x = x + sg.x_offset as f64 * scale;
5520 let glyph_y = sg.y_offset as f64 * scale;
5521 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5522
5523 all_glyphs.push(PositionedGlyph {
5524 glyph_id: sg.glyph_id,
5525 x_offset: glyph_x,
5526 y_offset: glyph_y,
5527 x_advance: advance,
5528 font_size: style.font_size,
5529 font_family: Arc::from(resolved_family.as_str()),
5530 font_weight: style.font_weight,
5531 font_style: style.font_style,
5532 char_value,
5533 color: Some(style.color),
5534 href: href.map(|s| s.to_string()),
5535 text_decoration: style.text_decoration,
5536 letter_spacing: style.letter_spacing,
5537 cluster_text,
5538 extraction_text,
5539
5540 ligature,
5541 });
5542 bidi_levels.push(bidi_run.level);
5543 x += advance;
5544 }
5545 continue;
5546 }
5547 }
5548
5549 for i in start..end {
5551 let ch = line.chars[i];
5552 let glyph_x = x;
5553 let char_width = font_context.char_width(
5554 ch,
5555 resolved_family,
5556 style.font_weight,
5557 italic,
5558 style.font_size,
5559 );
5560 let advance = char_width + style.letter_spacing;
5561 all_glyphs.push(PositionedGlyph {
5562 glyph_id: ch as u16,
5563 x_offset: glyph_x,
5564 y_offset: 0.0,
5565 x_advance: advance,
5566 font_size: style.font_size,
5567 font_family: Arc::from(resolved_family.as_str()),
5568 font_weight: style.font_weight,
5569 font_style: style.font_style,
5570 char_value: ch,
5571 color: Some(style.color),
5572 href: href.map(|s| s.to_string()),
5573 text_decoration: style.text_decoration,
5574 letter_spacing: style.letter_spacing,
5575 cluster_text: None,
5576 extraction_text: None,
5577 ligature: false,
5578 });
5579 bidi_levels.push(bidi_run.level);
5580 x += advance;
5581 }
5582 }
5583 }
5584
5585 if has_bidi && !all_glyphs.is_empty() {
5587 all_glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5588 }
5589 return all_glyphs;
5590 }
5591
5592 if let Some(font_data) =
5595 font_context.font_data(&style.font_family, style.font_weight, italic)
5596 {
5597 if has_bidi {
5598 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5600 let units_per_em =
5601 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5602 let scale = style.font_size / units_per_em as f64;
5603
5604 let mut all_glyphs = Vec::new();
5605 let mut bidi_levels = Vec::new();
5606 let mut x = 0.0_f64;
5607
5608 for run in &bidi_runs {
5609 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
5610 let run_text: String = run_chars.iter().collect();
5611
5612 if let Some(shaped) =
5613 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
5614 {
5615 let clusters = cluster_texts(&shaped, &run_chars);
5616 for (sg, (cluster_text, ligature, extraction_text)) in
5617 shaped.iter().zip(clusters)
5618 {
5619 let cluster = sg.cluster as usize;
5620 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
5621
5622 let glyph_x = x + sg.x_offset as f64 * scale;
5623 let glyph_y = sg.y_offset as f64 * scale;
5624 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
5625
5626 all_glyphs.push(PositionedGlyph {
5627 glyph_id: sg.glyph_id,
5628 x_offset: glyph_x,
5629 y_offset: glyph_y,
5630 x_advance: advance,
5631 font_size: style.font_size,
5632 font_family: Arc::from(style.font_family.as_str()),
5633 font_weight: style.font_weight,
5634 font_style: style.font_style,
5635 char_value,
5636 color: Some(style.color),
5637 href: href.map(|s| s.to_string()),
5638 text_decoration: style.text_decoration,
5639 letter_spacing: style.letter_spacing,
5640 cluster_text,
5641 extraction_text,
5642
5643 ligature,
5644 });
5645 bidi_levels.push(run.level);
5646
5647 x += advance;
5648 }
5649 }
5650 }
5651
5652 let glyphs = bidi::reorder_and_position(all_glyphs, &bidi_levels);
5654 return glyphs;
5655 }
5656
5657 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
5659 let units_per_em =
5660 font_context.units_per_em(&style.font_family, style.font_weight, italic);
5661 let scale = style.font_size / units_per_em as f64;
5662
5663 return self.shaped_glyphs_to_positioned(
5664 &shaped,
5665 &line.chars,
5666 &line.char_positions,
5667 scale,
5668 style.font_size,
5669 &style.font_family,
5670 style.font_weight,
5671 style.font_style,
5672 Some(style.color),
5673 href,
5674 style.text_decoration,
5675 style.letter_spacing,
5676 );
5677 }
5678 }
5679
5680 let mut glyphs: Vec<PositionedGlyph> = line
5682 .chars
5683 .iter()
5684 .enumerate()
5685 .map(|(j, ch)| {
5686 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
5687 let char_width = font_context.char_width(
5688 *ch,
5689 &style.font_family,
5690 style.font_weight,
5691 italic,
5692 style.font_size,
5693 );
5694 PositionedGlyph {
5695 glyph_id: *ch as u16,
5696 x_offset: glyph_x,
5697 y_offset: 0.0,
5698 x_advance: char_width,
5699 font_size: style.font_size,
5700 font_family: Arc::from(style.font_family.as_str()),
5701 font_weight: style.font_weight,
5702 font_style: style.font_style,
5703 char_value: *ch,
5704 color: Some(style.color),
5705 href: href.map(|s| s.to_string()),
5706 text_decoration: style.text_decoration,
5707 letter_spacing: style.letter_spacing,
5708 cluster_text: None,
5709 extraction_text: None,
5710 ligature: false,
5711 }
5712 })
5713 .collect();
5714
5715 if has_bidi && !glyphs.is_empty() {
5717 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
5718 let mut levels = Vec::with_capacity(glyphs.len());
5719 let mut char_idx = 0;
5720 for run in &bidi_runs {
5721 for _ in run.char_start..run.char_end {
5722 if char_idx < glyphs.len() {
5723 levels.push(run.level);
5724 char_idx += 1;
5725 }
5726 }
5727 }
5728 while levels.len() < glyphs.len() {
5730 levels.push(unicode_bidi::Level::ltr());
5731 }
5732 glyphs = bidi::reorder_and_position(glyphs, &levels);
5733 }
5734
5735 glyphs
5736 }
5737
5738 fn build_positioned_glyphs_runs(
5743 &self,
5744 run_line: &RunBrokenLine,
5745 font_context: &FontContext,
5746 direction: Direction,
5747 ) -> Vec<PositionedGlyph> {
5748 let chars = &run_line.chars;
5749 if chars.is_empty() {
5750 return vec![];
5751 }
5752
5753 let resolved_families = crate::text::resolved_style_families(chars, font_context);
5755
5756 let line_text: String = chars.iter().map(|c| c.ch).collect();
5757 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
5758 let mut char_levels = vec![unicode_bidi::Level::ltr(); chars.len()];
5763 if has_bidi {
5764 for run in bidi::analyze_bidi(&line_text, direction) {
5765 for level in &mut char_levels[run.char_start..run.char_end.min(chars.len())] {
5766 *level = run.level;
5767 }
5768 }
5769 }
5770
5771 let mut glyphs = Vec::new();
5772 let mut bidi_levels = Vec::new();
5773 let mut i = 0;
5774
5775 while i < chars.len() {
5776 let sc = &chars[i];
5777 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
5778 let resolved_family = &resolved_families[i];
5779 let level = char_levels[i];
5780 let is_rtl = level.is_rtl();
5781
5782 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
5784 {
5785 let run_start = i;
5787 let mut run_end = i + 1;
5788 while run_end < chars.len() {
5789 let next = &chars[run_end];
5790 let next_italic =
5791 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
5792 if resolved_families[run_end] == *resolved_family
5794 && next.font_weight == sc.font_weight
5795 && next_italic == italic
5796 && (next.font_size - sc.font_size).abs() < 0.001
5797 && char_levels[run_end] == level
5798 {
5799 run_end += 1;
5800 } else {
5801 break;
5802 }
5803 }
5804
5805 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
5806 if let Some(shaped) =
5807 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
5808 {
5809 let units_per_em =
5810 font_context.units_per_em(resolved_family, sc.font_weight, italic);
5811 let scale = sc.font_size / units_per_em as f64;
5812
5813 let run_chars: Vec<char> =
5815 chars[run_start..run_end].iter().map(|c| c.ch).collect();
5816 let run_positions: Vec<f64> = (run_start..run_end)
5817 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
5818 .collect();
5819
5820 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
5822 &shaped,
5823 &chars[run_start..run_end],
5824 &run_chars,
5825 &run_positions,
5826 scale,
5827 );
5828 let resolved_family_arc: Arc<str> = Arc::from(resolved_family.as_str());
5830 for g in &mut run_glyphs {
5831 g.font_family = resolved_family_arc.clone();
5832 }
5833 for _ in &run_glyphs {
5835 bidi_levels.push(level);
5836 }
5837 glyphs.extend(run_glyphs);
5838 i = run_end;
5839 continue;
5840 }
5841 }
5842
5843 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
5845 let char_width = font_context.char_width(
5846 sc.ch,
5847 resolved_family,
5848 sc.font_weight,
5849 italic,
5850 sc.font_size,
5851 );
5852 glyphs.push(PositionedGlyph {
5853 glyph_id: sc.ch as u16,
5854 x_offset: glyph_x,
5855 y_offset: 0.0,
5856 x_advance: char_width,
5857 font_size: sc.font_size,
5858 font_family: Arc::from(resolved_family.as_str()),
5859 font_weight: sc.font_weight,
5860 font_style: sc.font_style,
5861 char_value: sc.ch,
5862 color: Some(sc.color),
5863 href: sc.href.clone(),
5864 text_decoration: sc.text_decoration,
5865 letter_spacing: sc.letter_spacing,
5866 cluster_text: None,
5867 extraction_text: None,
5868 ligature: false,
5869 });
5870 bidi_levels.push(level);
5871 i += 1;
5872 }
5873
5874 if has_bidi && !glyphs.is_empty() {
5876 glyphs = bidi::reorder_and_position(glyphs, &bidi_levels);
5877 }
5878
5879 glyphs
5880 }
5881
5882 #[allow(clippy::too_many_arguments)]
5884 fn shaped_glyphs_to_positioned(
5885 &self,
5886 shaped: &[shaping::ShapedGlyph],
5887 chars: &[char],
5888 char_positions: &[f64],
5889 scale: f64,
5890 font_size: f64,
5891 font_family: &str,
5892 font_weight: u32,
5893 font_style: FontStyle,
5894 color: Option<Color>,
5895 href: Option<&str>,
5896 text_decoration: TextDecoration,
5897 letter_spacing: f64,
5898 ) -> Vec<PositionedGlyph> {
5899 let mut result = Vec::with_capacity(shaped.len());
5900 let mut x = 0.0_f64;
5901 let mut cluster_pen = 0.0_f64;
5902 let mut prev_cluster: Option<usize> = None;
5903
5904 let clusters = cluster_texts(shaped, chars);
5905 for (sg, (cluster_text, ligature, extraction_text)) in shaped.iter().zip(clusters) {
5906 let cluster = sg.cluster as usize;
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 = shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale);
5914 let glyph_y = sg.y_offset as f64 * scale;
5915 let advance = sg.x_advance as f64 * scale + letter_spacing;
5916
5917 result.push(PositionedGlyph {
5918 glyph_id: sg.glyph_id,
5919 x_offset: glyph_x,
5920 y_offset: glyph_y,
5921 x_advance: advance,
5922 font_size,
5923 font_family: Arc::from(font_family),
5924 font_weight,
5925 font_style,
5926 char_value,
5927 color,
5928 href: href.map(|s| s.to_string()),
5929 text_decoration,
5930 letter_spacing,
5931 cluster_text,
5932 extraction_text,
5933
5934 ligature,
5935 });
5936
5937 x += advance;
5938 }
5939
5940 result
5941 }
5942
5943 fn shaped_glyphs_to_positioned_runs(
5945 &self,
5946 shaped: &[shaping::ShapedGlyph],
5947 styled_chars: &[StyledChar],
5948 chars: &[char],
5949 char_positions: &[f64],
5950 scale: f64,
5951 ) -> Vec<PositionedGlyph> {
5952 let mut result = Vec::with_capacity(shaped.len());
5953 let base_x = char_positions.first().copied().unwrap_or(0.0);
5956 let mut x = 0.0_f64;
5957 let mut cluster_pen = 0.0_f64;
5958 let mut prev_cluster: Option<usize> = None;
5959
5960 let clusters = cluster_texts(shaped, chars);
5961 for (sg, (cluster_text, ligature, extraction_text)) in shaped.iter().zip(clusters) {
5962 let cluster = sg.cluster as usize;
5963 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
5964 let char_value = chars.get(cluster).copied().unwrap_or(' ');
5965
5966 if prev_cluster != Some(cluster) {
5967 cluster_pen = x;
5968 prev_cluster = Some(cluster);
5969 }
5970 let glyph_x = if char_positions.get(cluster).is_some() {
5971 shaped_glyph_x(char_positions, cluster, x, cluster_pen, sg, scale)
5972 } else {
5973 base_x + x + sg.x_offset as f64 * scale
5974 };
5975 let glyph_y = sg.y_offset as f64 * scale;
5976 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
5977
5978 result.push(PositionedGlyph {
5979 glyph_id: sg.glyph_id,
5980 x_offset: glyph_x,
5981 y_offset: glyph_y,
5982 x_advance: advance,
5983 font_size: sc.font_size,
5984 font_family: Arc::from(sc.font_family.as_str()),
5985 font_weight: sc.font_weight,
5986 font_style: sc.font_style,
5987 char_value,
5988 color: Some(sc.color),
5989 href: sc.href.clone(),
5990 text_decoration: sc.text_decoration,
5991 letter_spacing: sc.letter_spacing,
5992 cluster_text,
5993 extraction_text,
5994
5995 ligature,
5996 });
5997
5998 x += advance;
5999 }
6000
6001 result
6002 }
6003
6004 fn image_display_size(
6014 &self,
6015 src: &str,
6016 style: &ResolvedStyle,
6017 explicit_width: Option<f64>,
6018 explicit_height: Option<f64>,
6019 available_width: f64,
6020 ) -> (f64, f64) {
6021 let dims = if src.is_empty() {
6022 None
6023 } else {
6024 self.get_image_dimensions(src)
6025 };
6026 let aspect = dims
6027 .map(|(w, h)| {
6028 if w > 0 {
6029 f64::from(h) / f64::from(w)
6030 } else {
6031 0.75
6032 }
6033 })
6034 .unwrap_or(0.75);
6035
6036 let style_w = match style.width {
6037 SizeConstraint::Fixed(w) => Some(w),
6038 SizeConstraint::Auto => None,
6039 };
6040 let style_h = match style.height {
6041 SizeConstraint::Fixed(h) => Some(h),
6042 SizeConstraint::Auto => None,
6043 };
6044 let clamp = |w: f64| w.min(style.max_width).max(style.min_width);
6045
6046 let width_source = style_w.or(explicit_width);
6047 let height_source = style_h.or(explicit_height);
6048 match (width_source, height_source) {
6049 (Some(w), Some(h)) => (clamp(w), h),
6050 (Some(w), None) => {
6051 let w = clamp(w);
6052 (w, w * aspect)
6053 }
6054 (None, Some(h)) => (clamp(h / aspect), h),
6055 (None, None) => {
6056 let w = clamp(
6059 dims.map(|(w, _)| f64::from(w))
6060 .unwrap_or(available_width)
6061 .min(available_width),
6062 );
6063 (w, w * aspect)
6064 }
6065 }
6066 }
6067
6068 #[allow(clippy::too_many_arguments)]
6069 fn layout_image(
6070 &self,
6071 node: &Node,
6072 style: &ResolvedStyle,
6073 cursor: &mut PageCursor,
6074 pages: &mut Vec<LayoutPage>,
6075 x: f64,
6076 available_width: f64,
6077 explicit_width: Option<f64>,
6078 explicit_height: Option<f64>,
6079 ) {
6080 let margin = &style.margin.to_edges();
6081
6082 let src = match &node.kind {
6084 NodeKind::Image { src, .. } => src.as_str(),
6085 _ => "",
6086 };
6087
6088 let loaded = if !src.is_empty() {
6089 crate::image_loader::load_image(src).ok()
6090 } else {
6091 None
6092 };
6093
6094 let (img_width, img_height) = self.image_display_size(
6095 src,
6096 style,
6097 explicit_width,
6098 explicit_height,
6099 available_width - margin.horizontal(),
6100 );
6101
6102 let total_height = img_height + margin.vertical();
6103
6104 if total_height > cursor.remaining_height() {
6105 pages.push(cursor.finalize());
6106 *cursor = cursor.new_page();
6107 }
6108
6109 cursor.y += margin.top;
6110
6111 let draw = if let Some(image_data) = loaded {
6112 DrawCommand::Image { image_data }
6113 } else {
6114 DrawCommand::ImagePlaceholder
6115 };
6116
6117 cursor.elements.push(LayoutElement {
6118 x: x + margin.left,
6119 y: cursor.content_y + cursor.y,
6120 width: img_width,
6121 height: img_height,
6122 draw,
6123 children: vec![],
6124 node_type: Some(node_kind_name(&node.kind).to_string()),
6125 resolved_style: Some(style.clone()),
6126 source_location: node.source_location.clone(),
6127 href: node.href.clone(),
6128 bookmark: node.bookmark.clone(),
6129 alt: node.alt.clone(),
6130 is_header_row: false,
6131 actual_text: None,
6132 list_numbering: None,
6133 col_span: 1,
6134 overflow: style.overflow,
6135 opacity: style.opacity,
6136 });
6137
6138 cursor.y += img_height + margin.bottom;
6139 }
6140
6141 #[allow(clippy::too_many_arguments)]
6143 fn layout_svg(
6144 &self,
6145 node: &Node,
6146 style: &ResolvedStyle,
6147 cursor: &mut PageCursor,
6148 pages: &mut Vec<LayoutPage>,
6149 x: f64,
6150 _available_width: f64,
6151 svg_width: f64,
6152 svg_height: f64,
6153 view_box: Option<&str>,
6154 content: &str,
6155 ) {
6156 let margin = &style.margin.to_edges();
6157 let total_height = svg_height + margin.vertical();
6158
6159 if total_height > cursor.remaining_height() {
6160 pages.push(cursor.finalize());
6161 *cursor = cursor.new_page();
6162 }
6163
6164 cursor.y += margin.top;
6165
6166 let vb = view_box
6167 .and_then(crate::svg::parse_view_box)
6168 .unwrap_or(crate::svg::ViewBox {
6169 min_x: 0.0,
6170 min_y: 0.0,
6171 width: svg_width,
6172 height: svg_height,
6173 });
6174
6175 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
6176
6177 cursor.elements.push(LayoutElement {
6178 x: x + margin.left,
6179 y: cursor.content_y + cursor.y,
6180 width: svg_width,
6181 height: svg_height,
6182 draw: DrawCommand::Svg {
6183 commands,
6184 width: svg_width,
6185 height: svg_height,
6186 viewbox_min_x: vb.min_x,
6187 viewbox_min_y: vb.min_y,
6188 viewbox_width: vb.width,
6189 viewbox_height: vb.height,
6190 clip: false,
6191 },
6192 children: vec![],
6193 node_type: Some("Svg".to_string()),
6194 resolved_style: Some(style.clone()),
6195 source_location: node.source_location.clone(),
6196 href: node.href.clone(),
6197 bookmark: node.bookmark.clone(),
6198 alt: node.alt.clone(),
6199 is_header_row: false,
6200 actual_text: None,
6201 list_numbering: None,
6202 col_span: 1,
6203 overflow: style.overflow,
6204 opacity: style.opacity,
6205 });
6206
6207 cursor.y += svg_height + margin.bottom;
6208 }
6209
6210 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
6212 use crate::svg::SvgCommand;
6213
6214 let mut commands = Vec::new();
6215 let mut cur_x = 0.0_f64;
6216 let mut cur_y = 0.0_f64;
6217
6218 for op in operations {
6219 match op {
6220 CanvasOp::MoveTo { x, y } => {
6221 commands.push(SvgCommand::MoveTo(*x, *y));
6222 cur_x = *x;
6223 cur_y = *y;
6224 }
6225 CanvasOp::LineTo { x, y } => {
6226 commands.push(SvgCommand::LineTo(*x, *y));
6227 cur_x = *x;
6228 cur_y = *y;
6229 }
6230 CanvasOp::BezierCurveTo {
6231 cp1x,
6232 cp1y,
6233 cp2x,
6234 cp2y,
6235 x,
6236 y,
6237 } => {
6238 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
6239 cur_x = *x;
6240 cur_y = *y;
6241 }
6242 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
6243 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
6245 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
6246 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
6247 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
6248 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
6249 cur_x = *x;
6250 cur_y = *y;
6251 }
6252 CanvasOp::ClosePath => {
6253 commands.push(SvgCommand::ClosePath);
6254 }
6255 CanvasOp::Rect {
6256 x,
6257 y,
6258 width,
6259 height,
6260 } => {
6261 commands.push(SvgCommand::MoveTo(*x, *y));
6262 commands.push(SvgCommand::LineTo(*x + *width, *y));
6263 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
6264 commands.push(SvgCommand::LineTo(*x, *y + *height));
6265 commands.push(SvgCommand::ClosePath);
6266 cur_x = *x;
6267 cur_y = *y;
6268 }
6269 CanvasOp::Circle { cx, cy, r } => {
6270 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
6271 }
6272 CanvasOp::Ellipse { cx, cy, rx, ry } => {
6273 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
6274 }
6275 CanvasOp::Arc {
6276 cx,
6277 cy,
6278 r,
6279 start_angle,
6280 end_angle,
6281 counterclockwise,
6282 } => {
6283 let steps = 32;
6287 let mut sweep = end_angle - start_angle;
6288 if !counterclockwise && sweep < 0.0 {
6289 sweep += 2.0 * std::f64::consts::PI;
6290 }
6291 if *counterclockwise && sweep > 0.0 {
6292 sweep -= 2.0 * std::f64::consts::PI;
6293 }
6294 for i in 0..=steps {
6295 let t = *start_angle + sweep * (i as f64 / steps as f64);
6296 let px = cx + r * t.cos();
6297 let py = cy + r * t.sin();
6298 if i == 0 {
6299 commands.push(SvgCommand::MoveTo(px, py));
6300 } else {
6301 commands.push(SvgCommand::LineTo(px, py));
6302 }
6303 }
6304 }
6305 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
6306 CanvasOp::Fill => commands.push(SvgCommand::Fill),
6307 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
6308 CanvasOp::SetFillColor { r, g, b } => {
6309 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
6311 }
6312 CanvasOp::SetStrokeColor { r, g, b } => {
6313 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
6314 }
6315 CanvasOp::SetLineWidth { width } => {
6316 commands.push(SvgCommand::SetStrokeWidth(*width));
6317 }
6318 CanvasOp::SetLineCap { cap } => {
6319 commands.push(SvgCommand::SetLineCap(*cap));
6320 }
6321 CanvasOp::SetLineJoin { join } => {
6322 commands.push(SvgCommand::SetLineJoin(*join));
6323 }
6324 CanvasOp::Save => commands.push(SvgCommand::SaveState),
6325 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
6326 }
6327 }
6328
6329 commands
6330 }
6331
6332 #[allow(clippy::too_many_arguments)]
6334 fn layout_canvas(
6335 &self,
6336 node: &Node,
6337 style: &ResolvedStyle,
6338 cursor: &mut PageCursor,
6339 pages: &mut Vec<LayoutPage>,
6340 x: f64,
6341 _available_width: f64,
6342 canvas_width: f64,
6343 canvas_height: f64,
6344 operations: &[CanvasOp],
6345 ) {
6346 let margin = style.margin.to_edges();
6347 let total_height = canvas_height + margin.top + margin.bottom;
6348
6349 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
6351 pages.push(cursor.finalize());
6352 *cursor = cursor.new_page();
6353 }
6354
6355 cursor.y += margin.top;
6356
6357 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
6358
6359 cursor.elements.push(LayoutElement {
6360 x: x + margin.left,
6361 y: cursor.content_y + cursor.y,
6362 width: canvas_width,
6363 height: canvas_height,
6364 draw: DrawCommand::Svg {
6365 commands: svg_commands,
6366 width: canvas_width,
6367 height: canvas_height,
6368 viewbox_min_x: 0.0,
6371 viewbox_min_y: 0.0,
6372 viewbox_width: canvas_width,
6373 viewbox_height: canvas_height,
6374 clip: true,
6375 },
6376 children: vec![],
6377 node_type: Some("Canvas".to_string()),
6378 resolved_style: Some(style.clone()),
6379 source_location: node.source_location.clone(),
6380 href: node.href.clone(),
6381 bookmark: node.bookmark.clone(),
6382 alt: node.alt.clone(),
6383 is_header_row: false,
6384 actual_text: None,
6385 list_numbering: None,
6386 col_span: 1,
6387 overflow: style.overflow,
6388 opacity: style.opacity,
6389 });
6390
6391 cursor.y += canvas_height + margin.bottom;
6392 }
6393
6394 #[allow(clippy::too_many_arguments)]
6396 #[allow(clippy::too_many_arguments)]
6398 fn layout_chart(
6399 &self,
6400 node: &Node,
6401 style: &ResolvedStyle,
6402 cursor: &mut PageCursor,
6403 pages: &mut Vec<LayoutPage>,
6404 x: f64,
6405 chart_width: f64,
6406 chart_height: f64,
6407 primitives: Vec<crate::chart::ChartPrimitive>,
6408 node_type_name: &str,
6409 ) {
6410 let margin = &style.margin.to_edges();
6411 let total_height = chart_height + margin.vertical();
6412
6413 if total_height > cursor.remaining_height() {
6414 pages.push(cursor.finalize());
6415 *cursor = cursor.new_page();
6416 }
6417
6418 cursor.y += margin.top;
6419
6420 let draw = DrawCommand::Chart { primitives };
6421
6422 cursor.elements.push(LayoutElement {
6423 x: x + margin.left,
6424 y: cursor.content_y + cursor.y,
6425 width: chart_width,
6426 height: chart_height,
6427 draw,
6428 children: vec![],
6429 node_type: Some(node_type_name.to_string()),
6430 resolved_style: Some(style.clone()),
6431 source_location: node.source_location.clone(),
6432 href: node.href.clone(),
6433 bookmark: node.bookmark.clone(),
6434 alt: node.alt.clone(),
6435 is_header_row: false,
6436 actual_text: None,
6437 list_numbering: None,
6438 col_span: 1,
6439 overflow: style.overflow,
6440 opacity: style.opacity,
6441 });
6442
6443 cursor.y += chart_height + margin.bottom;
6444 }
6445
6446 #[allow(clippy::too_many_arguments)]
6448 fn layout_form_field(
6449 &self,
6450 node: &Node,
6451 style: &ResolvedStyle,
6452 cursor: &mut PageCursor,
6453 pages: &mut Vec<LayoutPage>,
6454 x: f64,
6455 field_width: f64,
6456 field_height: f64,
6457 draw: DrawCommand,
6458 node_type_name: &str,
6459 ) {
6460 let margin = &style.margin.to_edges();
6461 let total_height = field_height + margin.vertical();
6462
6463 if total_height > cursor.remaining_height() {
6464 pages.push(cursor.finalize());
6465 *cursor = cursor.new_page();
6466 }
6467
6468 cursor.y += margin.top;
6469
6470 cursor.elements.push(LayoutElement {
6471 x: x + margin.left,
6472 y: cursor.content_y + cursor.y,
6473 width: field_width,
6474 height: field_height,
6475 draw,
6476 children: vec![],
6477 node_type: Some(node_type_name.to_string()),
6478 resolved_style: Some(style.clone()),
6479 source_location: node.source_location.clone(),
6480 href: node.href.clone(),
6481 bookmark: node.bookmark.clone(),
6482 alt: node.alt.clone(),
6483 is_header_row: false,
6484 actual_text: None,
6485 list_numbering: None,
6486 col_span: 1,
6487 overflow: style.overflow,
6488 opacity: style.opacity,
6489 });
6490
6491 cursor.y += field_height + margin.bottom;
6492 }
6493
6494 #[allow(clippy::too_many_arguments)]
6495 fn layout_barcode(
6496 &self,
6497 node: &Node,
6498 style: &ResolvedStyle,
6499 cursor: &mut PageCursor,
6500 pages: &mut Vec<LayoutPage>,
6501 x: f64,
6502 available_width: f64,
6503 data: &str,
6504 format: crate::barcode::BarcodeFormat,
6505 explicit_width: Option<f64>,
6506 bar_height: f64,
6507 ) {
6508 let margin = &style.margin.to_edges();
6509 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
6510 let total_height = bar_height + margin.vertical();
6511
6512 if total_height > cursor.remaining_height() {
6513 pages.push(cursor.finalize());
6514 *cursor = cursor.new_page();
6515 }
6516
6517 cursor.y += margin.top;
6518
6519 let draw = match crate::barcode::generate_barcode(data, format) {
6520 Ok(barcode_data) => {
6521 let bar_width = if barcode_data.bars.is_empty() {
6522 0.0
6523 } else {
6524 display_width / barcode_data.bars.len() as f64
6525 };
6526 DrawCommand::Barcode {
6527 bars: barcode_data.bars,
6528 bar_width,
6529 height: bar_height,
6530 color: style.color,
6531 }
6532 }
6533 Err(_) => DrawCommand::None,
6534 };
6535
6536 cursor.elements.push(LayoutElement {
6537 x: x + margin.left,
6538 y: cursor.content_y + cursor.y,
6539 width: display_width,
6540 height: bar_height,
6541 draw,
6542 children: vec![],
6543 node_type: Some("Barcode".to_string()),
6544 resolved_style: Some(style.clone()),
6545 source_location: node.source_location.clone(),
6546 href: node.href.clone(),
6547 bookmark: node.bookmark.clone(),
6548 alt: node.alt.clone(),
6549 is_header_row: false,
6550 actual_text: Some(data.to_string()),
6551 list_numbering: None,
6552 col_span: 1,
6553 overflow: style.overflow,
6554 opacity: style.opacity,
6555 });
6556
6557 cursor.y += bar_height + margin.bottom;
6558 }
6559
6560 #[allow(clippy::too_many_arguments)]
6562 fn layout_qrcode(
6563 &self,
6564 node: &Node,
6565 style: &ResolvedStyle,
6566 cursor: &mut PageCursor,
6567 pages: &mut Vec<LayoutPage>,
6568 x: f64,
6569 available_width: f64,
6570 data: &str,
6571 explicit_size: Option<f64>,
6572 ) {
6573 let margin = &style.margin.to_edges();
6574 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
6575 let total_height = display_size + margin.vertical();
6576
6577 if total_height > cursor.remaining_height() {
6578 pages.push(cursor.finalize());
6579 *cursor = cursor.new_page();
6580 }
6581
6582 cursor.y += margin.top;
6583
6584 let draw = match crate::qrcode::generate_qr(data) {
6585 Ok(matrix) => {
6586 let module_size = display_size / matrix.size as f64;
6587 DrawCommand::QrCode {
6588 modules: matrix.modules,
6589 module_size,
6590 color: style.color,
6591 }
6592 }
6593 Err(_) => DrawCommand::None,
6594 };
6595
6596 cursor.elements.push(LayoutElement {
6597 x: x + margin.left,
6598 y: cursor.content_y + cursor.y,
6599 width: display_size,
6600 height: display_size,
6601 draw,
6602 children: vec![],
6603 node_type: Some("QrCode".to_string()),
6604 resolved_style: Some(style.clone()),
6605 source_location: node.source_location.clone(),
6606 href: node.href.clone(),
6607 bookmark: node.bookmark.clone(),
6608 alt: node.alt.clone(),
6609 is_header_row: false,
6610 actual_text: Some(data.to_string()),
6611 list_numbering: None,
6612 col_span: 1,
6613 overflow: style.overflow,
6614 opacity: style.opacity,
6615 });
6616
6617 cursor.y += display_size + margin.bottom;
6618 }
6619
6620 fn measure_node_height(
6623 &self,
6624 node: &Node,
6625 available_width: f64,
6626 style: &ResolvedStyle,
6627 font_context: &FontContext,
6628 ) -> f64 {
6629 match &node.kind {
6630 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
6635 let measure_width = match style.width {
6638 SizeConstraint::Fixed(w) => (w - style.margin.horizontal()).max(0.0),
6639 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6640 };
6641 if !runs.is_empty() {
6648 let mut styled_chars: Vec<StyledChar> = Vec::new();
6649 for run in runs {
6650 let run_style = run.style.resolve(Some(style), measure_width);
6651 let transform = run_style.text_transform;
6652 let run_content = substitute_page_placeholders(&run.content);
6653 let mut prev_is_whitespace = true;
6654 for ch in run_content.chars() {
6655 let transformed_ch =
6656 apply_char_transform(ch, transform, prev_is_whitespace);
6657 prev_is_whitespace = ch.is_whitespace();
6658 styled_chars.push(StyledChar {
6659 ch: transformed_ch,
6660 font_family: run_style.font_family.clone(),
6661 font_size: run_style.font_size,
6662 font_weight: run_style.font_weight,
6663 font_style: run_style.font_style,
6664 color: run_style.color,
6665 href: None,
6666 text_decoration: run_style.text_decoration,
6667 letter_spacing: run_style.letter_spacing,
6668 word_spacing: run_style.word_spacing,
6669 });
6670 }
6671 }
6672 let justify = matches!(style.text_align, TextAlign::Justify);
6673 let broken_lines = match style.line_breaking {
6674 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
6675 font_context,
6676 &styled_chars,
6677 measure_width,
6678 style.hyphens,
6679 style.lang.as_deref(),
6680 justify,
6681 ),
6682 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
6683 font_context,
6684 &styled_chars,
6685 measure_width,
6686 style.hyphens,
6687 style.lang.as_deref(),
6688 ),
6689 };
6690 let line_height = style.font_size * style.line_height;
6691 (broken_lines.len() as f64) * line_height + style.padding.vertical()
6692 } else {
6693 let content = substitute_page_placeholders(content);
6694 let transformed = apply_text_transform(&content, style.text_transform);
6695 let justify = matches!(style.text_align, TextAlign::Justify);
6696 let lines = match style.line_breaking {
6697 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
6698 font_context,
6699 &transformed,
6700 measure_width,
6701 style.font_size,
6702 &style.font_family,
6703 style.font_weight,
6704 style.font_style,
6705 style.letter_spacing,
6706 style.word_spacing,
6707 style.hyphens,
6708 style.lang.as_deref(),
6709 justify,
6710 ),
6711 LineBreaking::Greedy => self.text_layout.break_into_lines(
6712 font_context,
6713 &transformed,
6714 measure_width,
6715 style.font_size,
6716 &style.font_family,
6717 style.font_weight,
6718 style.font_style,
6719 style.letter_spacing,
6720 style.word_spacing,
6721 style.hyphens,
6722 style.lang.as_deref(),
6723 ),
6724 };
6725 let line_height = style.font_size * style.line_height;
6726 (lines.len() as f64) * line_height + style.padding.vertical()
6727 }
6728 }
6729 NodeKind::Image {
6730 src,
6731 width: explicit_w,
6732 height: explicit_h,
6733 } => {
6734 let (_, h) = self.image_display_size(
6736 src,
6737 style,
6738 *explicit_w,
6739 *explicit_h,
6740 available_width - style.margin.horizontal(),
6741 );
6742 h + style.padding.vertical()
6743 }
6744 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
6745 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
6746 NodeKind::QrCode { size, .. } => {
6747 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
6748 display_size + style.margin.vertical()
6749 }
6750 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
6751 NodeKind::BarChart { height, .. }
6752 | NodeKind::LineChart { height, .. }
6753 | NodeKind::PieChart { height, .. }
6754 | NodeKind::AreaChart { height, .. }
6755 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
6756 NodeKind::TextField { height, .. }
6757 | NodeKind::Checkbox { height, .. }
6758 | NodeKind::Dropdown { height, .. }
6759 | NodeKind::RadioButton { height, .. } => *height + style.margin.vertical(),
6760 NodeKind::Watermark { .. } => 0.0, NodeKind::Table { columns } => {
6762 if let SizeConstraint::Fixed(h) = style.height {
6769 return h;
6770 }
6771 let outer_width = match style.width {
6772 SizeConstraint::Fixed(w) => w,
6773 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6774 };
6775 let inner_width =
6776 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6777 let col_widths = self.resolve_column_widths(
6778 columns,
6779 inner_width,
6780 &node.children,
6781 style,
6782 font_context,
6783 );
6784 let row_gap = style.row_gap;
6785 let offsets = Self::table_column_offsets(&node.children);
6786 let mut total = 0.0;
6787 for (i, row) in node.children.iter().enumerate() {
6788 if i > 0 {
6789 total += row_gap;
6790 }
6791 total += self.measure_table_row_height(
6792 row,
6793 &col_widths,
6794 &offsets[i],
6795 style,
6796 font_context,
6797 );
6798 }
6799 total + style.padding.vertical() + style.border_width.vertical()
6800 }
6801 NodeKind::TableRow { .. } => {
6802 let n = node.children.len().max(1);
6807 let usable = (available_width - style.margin.horizontal()).max(0.0);
6808 let col_w = usable / n as f64;
6809 let col_widths = vec![col_w; n];
6810 let offsets = Self::table_column_offsets(std::slice::from_ref(node));
6811 self.measure_table_row_height(node, &col_widths, &offsets[0], style, font_context)
6812 }
6813 _ => {
6814 if let SizeConstraint::Fixed(h) = style.height {
6816 return h;
6817 }
6818 let outer_width = match style.width {
6822 SizeConstraint::Fixed(w) => w,
6823 SizeConstraint::Auto => available_width - style.margin.horizontal(),
6824 }
6825 .min(style.max_width)
6826 .max(style.min_width);
6827 let inner_width =
6828 outer_width - style.padding.horizontal() - style.border_width.horizontal();
6829 let children_height =
6830 self.measure_children_height(&node.children, inner_width, style, font_context);
6831 (children_height + style.padding.vertical() + style.border_width.vertical())
6832 .max(style.min_height)
6833 }
6834 }
6835 }
6836
6837 fn measure_children_height(
6838 &self,
6839 children: &[Node],
6840 available_width: f64,
6841 parent_style: &ResolvedStyle,
6842 font_context: &FontContext,
6843 ) -> f64 {
6844 if children
6850 .iter()
6851 .any(|c| matches!(c.style.position, Some(Position::Absolute)))
6852 {
6853 let flow: Vec<Node> = children
6854 .iter()
6855 .filter(|c| !matches!(c.style.position, Some(Position::Absolute)))
6856 .cloned()
6857 .collect();
6858 return self.measure_children_height(
6859 &flow,
6860 available_width,
6861 parent_style,
6862 font_context,
6863 );
6864 }
6865 if matches!(parent_style.display, Display::Grid) {
6867 if let Some(template_cols) = &parent_style.grid_template_columns {
6868 let num_columns = template_cols.len();
6869 if num_columns > 0 && !children.is_empty() {
6870 let col_gap = parent_style.column_gap;
6871 let row_gap = parent_style.row_gap;
6872
6873 let content_sizes: Vec<f64> = template_cols
6874 .iter()
6875 .map(|track| {
6876 if matches!(track, GridTrackSize::Auto) {
6877 available_width / num_columns as f64
6878 } else {
6879 0.0
6880 }
6881 })
6882 .collect();
6883
6884 let col_widths = grid::resolve_tracks(
6885 template_cols,
6886 available_width,
6887 col_gap,
6888 &content_sizes,
6889 );
6890
6891 let placements: Vec<Option<&GridPlacement>> = children
6892 .iter()
6893 .map(|child| child.style.grid_placement.as_ref())
6894 .collect();
6895
6896 let item_placements = grid::place_items(&placements, num_columns);
6897 let num_rows = grid::compute_num_rows(&item_placements);
6898
6899 if num_rows == 0 {
6900 return 0.0;
6901 }
6902
6903 let mut row_heights = vec![0.0_f64; num_rows];
6904 for placement in &item_placements {
6905 let cell_width = grid::span_width(
6906 placement.col_start,
6907 placement.col_end,
6908 &col_widths,
6909 col_gap,
6910 );
6911 let child = &children[placement.child_index];
6912 let child_style = child.style.resolve(Some(parent_style), cell_width);
6913 let h =
6914 self.measure_node_height(child, cell_width, &child_style, font_context);
6915 let span = placement.row_end - placement.row_start;
6916 let per_row = h / span as f64;
6917 for rh in row_heights
6918 .iter_mut()
6919 .take(placement.row_end.min(num_rows))
6920 .skip(placement.row_start)
6921 {
6922 if per_row > *rh {
6923 *rh = per_row;
6924 }
6925 }
6926 }
6927
6928 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
6929 return row_heights.iter().sum::<f64>() + total_row_gap;
6930 }
6931 }
6932 }
6933
6934 let direction = parent_style.flex_direction;
6935 let row_gap = parent_style.row_gap;
6936 let column_gap = parent_style.column_gap;
6937
6938 match direction {
6939 FlexDirection::Row | FlexDirection::RowReverse => {
6940 let styles: Vec<ResolvedStyle> = children
6943 .iter()
6944 .map(|child| child.style.resolve(Some(parent_style), available_width))
6945 .collect();
6946
6947 let base_widths: Vec<f64> = children
6948 .iter()
6949 .zip(&styles)
6950 .map(|(child, style)| match style.flex_basis {
6951 SizeConstraint::Fixed(w) => w,
6952 SizeConstraint::Auto => match style.width {
6953 SizeConstraint::Fixed(w) => w,
6954 SizeConstraint::Auto => self
6955 .measure_intrinsic_width(child, style, font_context)
6956 .min(available_width),
6957 },
6958 })
6959 .collect();
6960
6961 let lines = match parent_style.flex_wrap {
6962 FlexWrap::NoWrap => {
6963 vec![flex::WrapLine {
6964 start: 0,
6965 end: children.len(),
6966 }]
6967 }
6968 FlexWrap::Wrap | FlexWrap::WrapReverse => {
6969 flex::partition_into_lines(&base_widths, column_gap, available_width)
6970 }
6971 };
6972
6973 let mut final_widths = base_widths.clone();
6975 for line in &lines {
6976 let line_count = line.end - line.start;
6977 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
6978 let distributable = available_width - line_gap;
6979 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
6980 let remaining = distributable - total_base;
6981
6982 if remaining > 0.0 {
6983 let total_grow: f64 = styles[line.start..line.end]
6984 .iter()
6985 .map(|s| s.flex_grow)
6986 .sum();
6987 if total_grow > 0.0 {
6988 for (j, s) in styles[line.start..line.end].iter().enumerate() {
6989 final_widths[line.start + j] = base_widths[line.start + j]
6990 + remaining * (s.flex_grow / total_grow);
6991 }
6992 }
6993 } else if remaining < 0.0 {
6994 let total_shrink: f64 = styles[line.start..line.end]
6995 .iter()
6996 .enumerate()
6997 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
6998 .sum();
6999 if total_shrink > 0.0 {
7000 for (j, s) in styles[line.start..line.end].iter().enumerate() {
7001 let factor =
7002 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
7003 let w = base_widths[line.start + j] + remaining * factor;
7004 final_widths[line.start + j] = w.max(s.min_width);
7005 }
7006 }
7007 }
7008 }
7009
7010 let mut total = 0.0;
7011 for (i, line) in lines.iter().enumerate() {
7012 let line_height: f64 = children[line.start..line.end]
7013 .iter()
7014 .enumerate()
7015 .map(|(j, child)| {
7016 let fw = final_widths[line.start + j];
7017 let child_style =
7026 child.style.resolve(Some(parent_style), available_width);
7027 self.measure_node_height(child, fw, &child_style, font_context)
7028 + child_style.margin.vertical()
7029 })
7030 .fold(0.0f64, f64::max);
7031 total += line_height;
7032 if i > 0 {
7033 total += row_gap;
7034 }
7035 }
7036 total
7037 }
7038 FlexDirection::Column | FlexDirection::ColumnReverse => {
7039 let mut total = 0.0;
7040 for (i, child) in children.iter().enumerate() {
7041 let child_style = child.style.resolve(Some(parent_style), available_width);
7042 let child_height = self.measure_node_height(
7043 child,
7044 available_width,
7045 &child_style,
7046 font_context,
7047 );
7048 total += child_height + child_style.margin.vertical();
7049 if i > 0 {
7050 total += row_gap;
7051 }
7052 }
7053 total
7054 }
7055 }
7056 }
7057
7058 fn measure_intrinsic_width(
7060 &self,
7061 node: &Node,
7062 style: &ResolvedStyle,
7063 font_context: &FontContext,
7064 ) -> f64 {
7065 match &node.kind {
7066 NodeKind::Svg { width, .. } => {
7067 *width + style.padding.horizontal() + style.margin.horizontal()
7068 }
7069 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7070 let text_width = if !runs.is_empty() {
7078 let chars = Self::runs_to_styled_chars(runs, None, style, 0.0);
7081 chars
7082 .split(|c| c.ch == '\n')
7083 .map(|line| self.text_layout.measure_runs_width(font_context, line))
7084 .fold(0.0f64, f64::max)
7085 } else {
7086 let content = substitute_page_placeholders(content);
7087 let transformed = apply_text_transform(&content, style.text_transform);
7088 transformed
7089 .split('\n')
7090 .map(|segment| {
7091 self.text_layout.measure_width(
7092 font_context,
7093 segment,
7094 style.font_size,
7095 &style.font_family,
7096 style.font_weight,
7097 style.font_style,
7098 style.letter_spacing,
7099 style.word_spacing,
7100 )
7101 })
7102 .fold(0.0f64, f64::max)
7103 };
7104 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
7107 }
7108 NodeKind::Image {
7109 src, width, height, ..
7110 } => {
7111 let w = if let SizeConstraint::Fixed(w) = style.width {
7112 w
7113 } else if let Some(w) = width {
7114 *w
7115 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
7116 let pixel_w = iw as f64;
7117 let pixel_h = ih as f64;
7118 let aspect = if pixel_w > 0.0 {
7119 pixel_h / pixel_w
7120 } else {
7121 0.75
7122 };
7123 let constrained_h = match style.height {
7125 SizeConstraint::Fixed(h) => Some(h),
7126 SizeConstraint::Auto => *height,
7127 };
7128 if let Some(h) = constrained_h {
7129 h / aspect
7130 } else {
7131 pixel_w
7132 }
7133 } else {
7134 100.0
7135 };
7136 w + style.padding.horizontal() + style.margin.horizontal()
7137 }
7138 NodeKind::Barcode { width, .. } => {
7139 let w = width.unwrap_or(0.0);
7140 w + style.padding.horizontal() + style.margin.horizontal()
7141 }
7142 NodeKind::QrCode { size, .. } => {
7143 let display_size = size.unwrap_or(0.0);
7144 display_size + style.padding.horizontal() + style.margin.horizontal()
7145 }
7146 NodeKind::Canvas { width, .. } => {
7147 *width + style.padding.horizontal() + style.margin.horizontal()
7148 }
7149 NodeKind::BarChart { width, .. }
7150 | NodeKind::LineChart { width, .. }
7151 | NodeKind::PieChart { width, .. }
7152 | NodeKind::AreaChart { width, .. }
7153 | NodeKind::DotPlot { width, .. } => {
7154 *width + style.padding.horizontal() + style.margin.horizontal()
7155 }
7156 NodeKind::TextField { width, .. } | NodeKind::Dropdown { width, .. } => {
7157 *width + style.padding.horizontal() + style.margin.horizontal()
7158 }
7159 NodeKind::Table { columns } => {
7160 let num_cols = node
7165 .children
7166 .iter()
7167 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7168 .max()
7169 .unwrap_or(1)
7170 .max(columns.len().max(1));
7171 let (_, col_max) =
7172 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7173 col_max.iter().sum::<f64>()
7174 + style.padding.horizontal()
7175 + style.margin.horizontal()
7176 + style.border_width.horizontal()
7177 }
7178 NodeKind::Checkbox { width, .. } | NodeKind::RadioButton { width, .. } => {
7179 *width + style.padding.horizontal() + style.margin.horizontal()
7180 }
7181 NodeKind::Watermark { .. } => 0.0, _ => {
7183 if let SizeConstraint::Fixed(w) = style.width {
7191 return w
7192 + style.padding.horizontal()
7193 + style.border_width.horizontal()
7194 + style.margin.horizontal();
7195 }
7196 if node.children.is_empty() {
7198 style.padding.horizontal() + style.margin.horizontal()
7199 } else {
7200 let direction = style.flex_direction;
7201 let gap = style.column_gap;
7213 let mut total = 0.0f64;
7214 for (i, child) in node.children.iter().enumerate() {
7215 let child_style = child.style.resolve(Some(style), 0.0);
7216 let child_width =
7217 self.measure_intrinsic_width(child, &child_style, font_context);
7218 match direction {
7219 FlexDirection::Row | FlexDirection::RowReverse => {
7220 total += child_width;
7221 if i > 0 {
7222 total += gap;
7223 }
7224 }
7225 _ => {
7226 total = total.max(child_width);
7227 }
7228 }
7229 }
7230 total
7231 + style.padding.horizontal()
7232 + style.margin.horizontal()
7233 + style.border_width.horizontal()
7234 }
7235 }
7236 }
7237 }
7238
7239 pub fn measure_min_content_width(
7243 &self,
7244 node: &Node,
7245 style: &ResolvedStyle,
7246 font_context: &FontContext,
7247 ) -> f64 {
7248 match &node.kind {
7249 NodeKind::Table { columns } => {
7250 let num_cols = node
7253 .children
7254 .iter()
7255 .map(|row| row.children.iter().map(Self::cell_col_span).sum::<usize>())
7256 .max()
7257 .unwrap_or(1)
7258 .max(columns.len().max(1));
7259 let (col_min, _) =
7260 self.measure_column_content(&node.children, num_cols, 0.0, style, font_context);
7261 col_min.iter().sum::<f64>()
7262 + style.padding.horizontal()
7263 + style.margin.horizontal()
7264 + style.border_width.horizontal()
7265 }
7266 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
7267 let word_width = if !runs.is_empty() {
7268 runs.iter()
7270 .map(|run| {
7271 let run_style = run.style.resolve(Some(style), 0.0);
7272 let run_content = substitute_page_placeholders(&run.content);
7273 let transformed =
7274 apply_text_transform(&run_content, run_style.text_transform);
7275 self.text_layout.measure_widest_word(
7276 font_context,
7277 &transformed,
7278 run_style.font_size,
7279 &run_style.font_family,
7280 run_style.font_weight,
7281 run_style.font_style,
7282 run_style.letter_spacing,
7283 run_style.word_spacing,
7284 style.hyphens,
7285 style.lang.as_deref(),
7286 )
7287 })
7288 .fold(0.0f64, f64::max)
7289 } else {
7290 let content = substitute_page_placeholders(content);
7291 let transformed = apply_text_transform(&content, style.text_transform);
7292 self.text_layout.measure_widest_word(
7293 font_context,
7294 &transformed,
7295 style.font_size,
7296 &style.font_family,
7297 style.font_weight,
7298 style.font_style,
7299 style.letter_spacing,
7300 style.word_spacing,
7301 style.hyphens,
7302 style.lang.as_deref(),
7303 )
7304 };
7305 word_width + style.padding.horizontal() + style.margin.horizontal()
7306 }
7307 NodeKind::Image { width, .. } => {
7308 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
7309 }
7310 NodeKind::Svg { width, .. } => {
7311 *width + style.padding.horizontal() + style.margin.horizontal()
7312 }
7313 _ => {
7314 if node.children.is_empty() {
7315 style.padding.horizontal()
7316 + style.margin.horizontal()
7317 + style.border_width.horizontal()
7318 } else {
7319 let mut max_child_min = 0.0f64;
7320 for child in &node.children {
7321 let child_style = child.style.resolve(Some(style), 0.0);
7322 let child_min =
7323 self.measure_min_content_width(child, &child_style, font_context);
7324 max_child_min = max_child_min.max(child_min);
7325 }
7326 max_child_min
7327 + style.padding.horizontal()
7328 + style.margin.horizontal()
7329 + style.border_width.horizontal()
7330 }
7331 }
7332 }
7333 }
7334
7335 fn flex_item_baseline_distance(
7342 &self,
7343 item: &Node,
7344 style: &ResolvedStyle,
7345 w: f64,
7346 font_context: &FontContext,
7347 ) -> f64 {
7348 let first_line = match &item.kind {
7349 NodeKind::Text { .. } | NodeKind::Heading { .. } => {
7350 let metrics = font_context.baseline_metrics(
7351 &style.font_family,
7352 style.font_weight,
7353 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique),
7354 );
7355 baseline_in_line(
7356 style.font_size * style.line_height,
7357 style.font_size,
7358 metrics,
7359 )
7360 }
7361 _ => self.cell_first_baseline_in_line(item, style, w, font_context),
7362 };
7363 style.margin.to_edges().top + style.padding.top + style.border_width.top + first_line
7364 }
7365
7366 fn cell_first_baseline_in_line(
7373 &self,
7374 cell: &Node,
7375 cell_style: &ResolvedStyle,
7376 w: f64,
7377 font_context: &FontContext,
7378 ) -> f64 {
7379 fn first(node: &Node, parent: &ResolvedStyle, w: f64) -> Option<ResolvedStyle> {
7380 for ch in &node.children {
7381 let s = ch.style.resolve(Some(parent), w);
7382 match &ch.kind {
7383 NodeKind::Text { .. } | NodeKind::Heading { .. } => return Some(s),
7384 _ => {
7385 if let Some(f) = first(ch, &s, w) {
7386 return Some(f);
7387 }
7388 }
7389 }
7390 }
7391 None
7392 }
7393 let s = first(cell, cell_style, w).unwrap_or_else(|| cell_style.clone());
7394 let metrics = font_context.baseline_metrics(
7395 &s.font_family,
7396 s.font_weight,
7397 matches!(s.font_style, FontStyle::Italic | FontStyle::Oblique),
7398 );
7399 baseline_in_line(s.font_size * s.line_height, s.font_size, metrics)
7400 }
7401
7402 fn cell_baseline_distance(
7407 &self,
7408 cell: &Node,
7409 cell_style: &ResolvedStyle,
7410 inner_width: f64,
7411 font_context: &FontContext,
7412 ) -> f64 {
7413 cell_style.padding.top
7414 + cell_style.border_width.top
7415 + self.cell_first_baseline_in_line(cell, cell_style, inner_width, font_context)
7416 }
7417
7418 fn row_baseline(
7421 &self,
7422 row: &Node,
7423 row_style: &ResolvedStyle,
7424 col_widths: &[f64],
7425 col_offsets: &[usize],
7426 font_context: &FontContext,
7427 ) -> Option<f64> {
7428 let mut b: Option<f64> = None;
7429 for (cell_i, cell) in row.children.iter().enumerate() {
7430 let span = match &cell.kind {
7431 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7432 _ => 1,
7433 };
7434 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7435 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7436 let cell_style = cell.style.resolve(Some(row_style), col_width);
7437 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7438 let iw = col_width
7439 - cell_style.padding.horizontal()
7440 - cell_style.border_width.horizontal();
7441 let d = self.cell_baseline_distance(cell, &cell_style, iw, font_context);
7442 b = Some(b.map_or(d, |m: f64| m.max(d)));
7443 }
7444 }
7445 b
7446 }
7447
7448 fn measure_table_row_height(
7449 &self,
7450 row: &Node,
7451 col_widths: &[f64],
7452 col_offsets: &[usize],
7453 parent_style: &ResolvedStyle,
7454 font_context: &FontContext,
7455 ) -> f64 {
7456 let row_style = row
7457 .style
7458 .resolve(Some(parent_style), col_widths.iter().sum());
7459 let mut max_height: f64 = 0.0;
7460 let row_bl = self.row_baseline(row, &row_style, col_widths, col_offsets, font_context);
7463
7464 for (cell_i, cell) in row.children.iter().enumerate() {
7465 let span = match &cell.kind {
7466 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7467 _ => 1,
7468 };
7469 let start_col = col_offsets.get(cell_i).copied().unwrap_or(0);
7470 let col_width: f64 = col_widths.iter().skip(start_col).take(span).copied().sum();
7471 let cell_style = cell.style.resolve(Some(&row_style), col_width);
7472 let inner_width =
7473 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
7474
7475 let mut cell_content_height = 0.0;
7476 for child in &cell.children {
7477 let child_style = child.style.resolve(Some(&cell_style), inner_width);
7478 cell_content_height +=
7479 self.measure_node_height(child, inner_width, &child_style, font_context);
7480 }
7481
7482 let mut total = cell_content_height
7483 + cell_style.padding.vertical()
7484 + cell_style.border_width.vertical();
7485 if matches!(cell_style.vertical_align, VerticalAlign::Baseline) {
7489 if let Some(b) = row_bl {
7490 let d =
7491 self.cell_baseline_distance(cell, &cell_style, inner_width, font_context);
7492 total += (b - d).max(0.0);
7493 }
7494 }
7495 if let SizeConstraint::Fixed(h) = cell_style.height {
7502 total = total.max(h);
7503 }
7504 max_height = max_height.max(total);
7505 }
7506
7507 max_height.max(row_style.min_height)
7508 }
7509
7510 fn cell_col_span(cell: &Node) -> usize {
7512 match &cell.kind {
7513 NodeKind::TableCell { col_span, .. } => (*col_span).max(1) as usize,
7514 _ => 1,
7515 }
7516 }
7517
7518 fn measure_column_content(
7523 &self,
7524 children: &[Node],
7525 num_cols: usize,
7526 available_width: f64,
7527 table_style: &ResolvedStyle,
7528 font_context: &FontContext,
7529 ) -> (Vec<f64>, Vec<f64>) {
7530 let mut col_min = vec![0.0f64; num_cols];
7531 let mut col_max = vec![0.0f64; num_cols];
7532 let offsets = Self::table_column_offsets(children);
7533 for (row_i, row_node) in children.iter().enumerate() {
7534 for (cell_i, cell) in row_node.children.iter().enumerate() {
7535 let col = offsets[row_i].get(cell_i).copied().unwrap_or(0);
7536 let span = Self::cell_col_span(cell);
7537 let cell_style = cell.style.resolve(Some(table_style), available_width);
7538 let chrome = cell_style.padding.horizontal() + cell_style.border_width.horizontal();
7539 let mut cmin = 0.0f64;
7540 let mut cmax = 0.0f64;
7541 for child in &cell.children {
7542 let child_style = child.style.resolve(Some(&cell_style), 0.0);
7543 cmin =
7544 cmin.max(self.measure_min_content_width(child, &child_style, font_context));
7545 cmax =
7546 cmax.max(self.measure_intrinsic_width(child, &child_style, font_context));
7547 }
7548 cmin += chrome;
7549 let mut cmax = cmax.max(cmin) + chrome;
7550 if let SizeConstraint::Fixed(w) = cell_style.width {
7551 cmax = w.max(cmin);
7552 }
7553 let per_min = cmin / span as f64;
7554 let per_max = cmax / span as f64;
7555 for k in col..(col + span).min(num_cols) {
7556 col_min[k] = col_min[k].max(per_min);
7557 col_max[k] = col_max[k].max(per_max);
7558 }
7559 }
7560 }
7561 (col_min, col_max)
7562 }
7563
7564 fn resolve_column_widths(
7574 &self,
7575 defs: &[ColumnDef],
7576 available_width: f64,
7577 children: &[Node],
7578 table_style: &ResolvedStyle,
7579 font_context: &FontContext,
7580 ) -> Vec<f64> {
7581 if defs.is_empty() {
7582 let num_cols = Self::occupancy_column_count(children);
7583
7584 let (col_min, col_max) = self.measure_column_content(
7585 children,
7586 num_cols,
7587 available_width,
7588 table_style,
7589 font_context,
7590 );
7591
7592 let sum_min: f64 = col_min.iter().sum();
7593 let sum_max: f64 = col_max.iter().sum();
7594 let w = available_width;
7595 return if sum_max <= w {
7596 if sum_max <= f64::EPSILON {
7600 vec![w / num_cols as f64; num_cols]
7601 } else {
7602 col_max
7603 .iter()
7604 .map(|m| m + (w - sum_max) * (m / sum_max))
7605 .collect()
7606 }
7607 } else if sum_min <= w {
7608 let denom = (sum_max - sum_min).max(f64::EPSILON);
7611 col_min
7612 .iter()
7613 .zip(&col_max)
7614 .map(|(mn, mx)| mn + (w - sum_min) * ((mx - mn) / denom))
7615 .collect()
7616 } else {
7617 self.defect(format!(
7620 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7621 sum_min, w
7622 ));
7623 let scale = w / sum_min.max(f64::EPSILON);
7624 col_min.iter().map(|m| m * scale).collect()
7625 };
7626 }
7627
7628 let num_cols = Self::occupancy_column_count(children).max(defs.len());
7634 let mut defs_vec: Vec<ColumnDef> = defs.to_vec();
7635 while defs_vec.len() < num_cols {
7636 defs_vec.push(ColumnDef {
7637 width: ColumnWidth::Auto,
7638 });
7639 }
7640 let defs = &defs_vec[..];
7641
7642 let mut widths = Vec::new();
7643 let mut remaining = available_width;
7644 let mut auto_count = 0;
7645
7646 for def in defs {
7647 match def.width {
7648 ColumnWidth::Fixed(w) => {
7649 widths.push(w);
7650 remaining -= w;
7651 }
7652 ColumnWidth::Fraction(f) => {
7653 let w = available_width * f;
7654 widths.push(w);
7655 remaining -= w;
7656 }
7657 ColumnWidth::Auto => {
7658 widths.push(0.0);
7659 auto_count += 1;
7660 }
7661 }
7662 }
7663
7664 const OVERFULL_EPS: f64 = 0.01;
7673 if remaining < -OVERFULL_EPS {
7674 self.defect(format!(
7677 "render defect: table column widths total {:.0}pt but only {:.0}pt is available — remaining columns were clamped to their minimum",
7678 available_width - remaining,
7679 available_width
7680 ));
7681 remaining = 0.0;
7682 }
7683 remaining = remaining.max(0.0);
7686
7687 if auto_count > 0 {
7688 let auto_width = remaining / auto_count as f64;
7689 for (i, def) in defs.iter().enumerate() {
7690 if matches!(def.width, ColumnWidth::Auto) {
7691 widths[i] = auto_width;
7692 }
7693 }
7694 }
7695
7696 let (col_min, _) = self.measure_column_content(
7703 children,
7704 widths.len(),
7705 available_width,
7706 table_style,
7707 font_context,
7708 );
7709 let needs_floor = widths.iter().zip(&col_min).any(|(w, m)| *w + 0.01 < *m);
7710 if needs_floor {
7711 let sum_min: f64 = col_min.iter().sum();
7712 if sum_min > available_width {
7713 self.defect(format!(
7714 "render defect: table columns need {:.0}pt at min-content but only {:.0}pt is available — text will wrap tighter than intended",
7715 sum_min, available_width
7716 ));
7717 let scale = available_width / sum_min.max(f64::EPSILON);
7718 return col_min.iter().map(|m| m * scale).collect();
7719 }
7720 let deficit: f64 = widths
7721 .iter()
7722 .zip(&col_min)
7723 .map(|(w, m)| (m - w).max(0.0))
7724 .sum();
7725 let surplus: f64 = widths
7726 .iter()
7727 .zip(&col_min)
7728 .map(|(w, m)| (w - m).max(0.0))
7729 .sum();
7730 let take = if surplus > 0.0 {
7731 deficit / surplus
7732 } else {
7733 0.0
7734 };
7735 widths = widths
7736 .iter()
7737 .zip(&col_min)
7738 .map(|(w, m)| if *w < *m { *m } else { w - (w - m) * take })
7739 .collect();
7740 }
7741
7742 widths
7743 }
7744
7745 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
7746 for (page_index, page) in pages.iter_mut().enumerate() {
7747 if !page.watermarks.is_empty() {
7749 let (page_w, page_h) = page.config.size.dimensions();
7750 let cx = page_w / 2.0;
7751 let cy = page_h / 2.0;
7752
7753 let mut watermark_elements = Vec::new();
7754 for (wm_node, wm_parent) in &page.watermarks {
7755 if let NodeKind::Watermark {
7756 text,
7757 font_size,
7758 angle,
7759 } = &wm_node.kind
7760 {
7761 let style = wm_node.style.resolve(wm_parent.as_ref(), page_w);
7762 let color = style.color;
7763 let opacity = style.opacity;
7764 let angle_rad = angle.to_radians();
7765
7766 let italic =
7768 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
7769
7770 let shaped = self.text_layout.shape_text(
7772 font_context,
7773 text,
7774 &style.font_family,
7775 style.font_weight,
7776 style.font_style,
7777 );
7778
7779 let mut glyphs = Vec::new();
7780 let mut x_pos = 0.0;
7781 let text_chars: Vec<char> = text.chars().collect();
7782
7783 if let Some(shaped_glyphs) = shaped {
7784 let units_per_em = font_context.units_per_em(
7786 &style.font_family,
7787 style.font_weight,
7788 italic,
7789 ) as f64;
7790
7791 let clusters = cluster_texts(&shaped_glyphs, &text_chars);
7792 for (sg, (cluster_text, ligature, extraction_text)) in
7793 shaped_glyphs.iter().zip(clusters)
7794 {
7795 let advance = sg.x_advance as f64 / units_per_em * *font_size;
7796 let cluster_idx = sg.cluster as usize;
7797 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
7798 glyphs.push(PositionedGlyph {
7799 glyph_id: sg.glyph_id,
7800 char_value: ch,
7801 x_offset: x_pos,
7802 y_offset: 0.0,
7803 x_advance: advance,
7804 font_size: *font_size,
7805 font_family: Arc::from(style.font_family.as_str()),
7806 font_weight: style.font_weight,
7807 font_style: style.font_style,
7808 color: Some(color),
7809 href: None,
7810 text_decoration: TextDecoration::None,
7811 letter_spacing: style.letter_spacing,
7812 cluster_text,
7813 extraction_text,
7814
7815 ligature,
7816 });
7817 x_pos += advance + style.letter_spacing;
7818 }
7819 } else {
7820 for &ch in &text_chars {
7822 let w = font_context.char_width(
7823 ch,
7824 &style.font_family,
7825 style.font_weight,
7826 italic,
7827 *font_size,
7828 );
7829 glyphs.push(PositionedGlyph {
7830 glyph_id: ch as u16,
7831 char_value: ch,
7832 x_offset: x_pos,
7833 y_offset: 0.0,
7834 x_advance: w,
7835 font_size: *font_size,
7836 font_family: Arc::from(style.font_family.as_str()),
7837 font_weight: style.font_weight,
7838 font_style: style.font_style,
7839 color: Some(color),
7840 href: None,
7841 text_decoration: TextDecoration::None,
7842 letter_spacing: style.letter_spacing,
7843 cluster_text: None,
7844 extraction_text: None,
7845 ligature: false,
7846 });
7847 x_pos += w + style.letter_spacing;
7848 }
7849 }
7850
7851 let text_width = x_pos;
7852
7853 let line = TextLine {
7854 x: 0.0,
7855 y: 0.0,
7856 glyphs,
7857 width: text_width,
7858 height: *font_size,
7859 word_spacing: 0.0,
7860 };
7861
7862 watermark_elements.push(LayoutElement {
7863 x: cx,
7864 y: cy,
7865 width: text_width,
7866 height: *font_size,
7867 draw: DrawCommand::Watermark {
7868 lines: vec![line],
7869 color,
7870 opacity,
7871 angle_rad,
7872 font_family: style.font_family.clone(),
7873 },
7874 children: vec![],
7875 node_type: Some("Watermark".to_string()),
7876 resolved_style: None,
7877 source_location: None,
7878 href: None,
7879 bookmark: None,
7880 alt: None,
7881 is_header_row: false,
7882 actual_text: None,
7883 list_numbering: None,
7884 col_span: 1,
7885 overflow: Overflow::default(),
7886 opacity: 1.0,
7887 });
7888 }
7889 }
7890
7891 watermark_elements.append(&mut page.elements);
7893 page.elements = watermark_elements;
7894 page.watermarks.clear();
7895 }
7896
7897 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
7898 continue;
7899 }
7900
7901 if !page.fixed_header.is_empty() {
7903 let mut hdr_cursor = PageCursor::new(&page.config);
7904 for (node, _h, parent) in &page.fixed_header {
7905 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7908 continue;
7909 }
7910 let cw = hdr_cursor.content_width;
7911 let cx = hdr_cursor.content_x;
7912 let style = node.style.resolve(parent.as_ref(), cw);
7913 self.layout_view(
7914 node,
7915 &style,
7916 &mut hdr_cursor,
7917 &mut Vec::new(),
7918 cx,
7919 cw,
7920 font_context,
7921 );
7922 }
7923 let mut combined = hdr_cursor.elements;
7925 combined.append(&mut page.elements);
7926 page.elements = combined;
7927 }
7928
7929 if !page.fixed_footer.is_empty() {
7934 let mut ftr_cursor = PageCursor::new(&page.config);
7935 let total_ftr: f64 = page
7936 .fixed_footer
7937 .iter()
7938 .filter(|(n, _, _)| fixed_applies_on(n, page_index, page.page_name.as_deref()))
7939 .map(|(_, h, _)| *h)
7940 .sum();
7941 let target_y = ftr_cursor.content_height - total_ftr;
7942 for (node, _h, parent) in &page.fixed_footer {
7944 if !fixed_applies_on(node, page_index, page.page_name.as_deref()) {
7945 continue;
7946 }
7947 let cw = ftr_cursor.content_width;
7948 let cx = ftr_cursor.content_x;
7949 let style = node.style.resolve(parent.as_ref(), cw);
7950 self.layout_view(
7951 node,
7952 &style,
7953 &mut ftr_cursor,
7954 &mut Vec::new(),
7955 cx,
7956 cw,
7957 font_context,
7958 );
7959 }
7960 for el in &mut ftr_cursor.elements {
7964 offset_element_y(el, target_y);
7965 }
7966 page.elements.extend(ftr_cursor.elements);
7967 }
7968
7969 page.fixed_header.clear();
7971 page.fixed_footer.clear();
7972 }
7973 }
7974
7975 #[allow(clippy::too_many_arguments)]
7980 fn layout_grid_children(
7981 &self,
7982 children: &[Node],
7983 parent_style: &ResolvedStyle,
7984 cursor: &mut PageCursor,
7985 pages: &mut Vec<LayoutPage>,
7986 x: f64,
7987 available_width: f64,
7988 font_context: &FontContext,
7989 ) {
7990 let template_cols = match &parent_style.grid_template_columns {
7991 Some(cols) => cols,
7992 None => return, };
7994
7995 let num_columns = template_cols.len();
7996 if num_columns == 0 || children.is_empty() {
7997 return;
7998 }
7999
8000 let col_gap = parent_style.column_gap;
8001 let row_gap = parent_style.row_gap;
8002
8003 let content_sizes: Vec<f64> = template_cols
8006 .iter()
8007 .map(|track| {
8008 if matches!(track, GridTrackSize::Auto) {
8009 available_width / num_columns as f64
8012 } else {
8013 0.0
8014 }
8015 })
8016 .collect();
8017
8018 let col_widths =
8019 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
8020
8021 let placements: Vec<Option<&GridPlacement>> = children
8023 .iter()
8024 .map(|child| child.style.grid_placement.as_ref())
8025 .collect();
8026
8027 let item_placements = grid::place_items(&placements, num_columns);
8029 let num_rows = grid::compute_num_rows(&item_placements);
8030
8031 if num_rows == 0 {
8032 return;
8033 }
8034
8035 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
8037 for placement in &item_placements {
8038 let cell_width =
8039 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
8040 let child = &children[placement.child_index];
8041 let child_style = child.style.resolve(Some(parent_style), cell_width);
8042 item_heights[placement.child_index] =
8043 self.measure_node_height(child, cell_width, &child_style, font_context);
8044 }
8045
8046 let template_rows = parent_style.grid_template_rows.as_deref();
8048 let mut row_heights = vec![0.0_f64; num_rows];
8049 for placement in &item_placements {
8050 let h = item_heights[placement.child_index];
8051 let span = placement.row_end - placement.row_start;
8052 let per_row = h / span as f64;
8053 for rh in row_heights
8054 .iter_mut()
8055 .take(placement.row_end.min(num_rows))
8056 .skip(placement.row_start)
8057 {
8058 if per_row > *rh {
8059 *rh = per_row;
8060 }
8061 }
8062 }
8063
8064 if let Some(template) = template_rows {
8066 let auto_row = parent_style.grid_auto_rows.as_ref();
8067 for (r, rh) in row_heights.iter_mut().enumerate() {
8068 let track = template.get(r).or(auto_row);
8069 if let Some(track) = track {
8070 match track {
8071 GridTrackSize::Pt(pts) => *rh = *pts,
8072 GridTrackSize::Auto => {} _ => {} }
8075 }
8076 }
8077 }
8078
8079 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
8081 if row_height > cursor.remaining_height() {
8086 pages.push(cursor.finalize());
8087 *cursor = cursor.new_page();
8088 }
8089
8090 let row_start_y = cursor.y;
8091
8092 for placement in &item_placements {
8094 if placement.row_start != row {
8095 continue; }
8097
8098 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
8099 let cell_width =
8100 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
8101
8102 let child = &children[placement.child_index];
8103
8104 self.layout_node(
8105 child,
8106 cursor,
8107 pages,
8108 cell_x,
8109 cell_width,
8110 Some(parent_style),
8111 font_context,
8112 None,
8113 None,
8114 );
8115 cursor.y = row_start_y;
8117 }
8118
8119 cursor.y = row_start_y + row_height + row_gap;
8120 }
8121
8122 if num_rows > 0 {
8124 cursor.y -= row_gap;
8125 }
8126 }
8127}
8128
8129fn end_y_of(frags: &[ItemFragments], last: usize) -> f64 {
8133 frags
8134 .iter()
8135 .filter(|f| f.pages.len() == last)
8136 .map(|f| f.final_y)
8137 .fold(f64::MIN, f64::max)
8138}
8139
8140fn paints_a_box(el: &LayoutElement) -> bool {
8148 match &el.draw {
8149 DrawCommand::Rect {
8150 background,
8151 border_width,
8152 ..
8153 } => {
8154 background.is_some()
8155 || border_width.top > 0.0
8156 || border_width.right > 0.0
8157 || border_width.bottom > 0.0
8158 || border_width.left > 0.0
8159 }
8160 _ => false,
8161 }
8162}
8163
8164fn drain_since(elements: &mut Vec<LayoutElement>, snapshot: usize) -> Vec<LayoutElement> {
8181 let start = snapshot.min(elements.len());
8182 elements.drain(start..).collect()
8183}
8184
8185fn stretch_fragment(elements: &mut [LayoutElement], bottom: f64) {
8186 for el in elements.iter_mut() {
8187 let grown = bottom - el.y;
8188 if grown > el.height {
8189 el.height = grown;
8190 }
8191 }
8192}
8193
8194struct ItemFragments {
8200 pages: Vec<LayoutPage>,
8201 tail: Vec<LayoutElement>,
8202 cursor: PageCursor,
8203 final_y: f64,
8204 stretches: bool,
8209}
8210
8211struct FlexItem<'a> {
8212 node: &'a Node,
8213 style: ResolvedStyle,
8214 base_width: f64,
8215 min_content_width: f64,
8216}
8217
8218fn first_text_snippet(node: &Node) -> Option<String> {
8221 fn walk(n: &Node) -> Option<&str> {
8222 match &n.kind {
8223 NodeKind::Text { content, runs, .. } | NodeKind::Heading { content, runs, .. } => {
8224 if !content.trim().is_empty() {
8225 return Some(content.trim());
8226 }
8227 if let Some(r) = runs.iter().find(|r| !r.content.trim().is_empty()) {
8228 return Some(r.content.trim());
8229 }
8230 None
8231 }
8232 _ => n.children.iter().find_map(walk),
8233 }
8234 }
8235 walk(node).map(|t| {
8236 let mut s: String = t.chars().take(32).collect();
8237 if t.chars().count() > 32 {
8238 s.push('…');
8239 }
8240 s
8241 })
8242}
8243
8244fn baseline_in_line(line_height: f64, font_size: f64, (ascent, descent): (f64, f64)) -> f64 {
8254 (line_height - (ascent + descent) * font_size) / 2.0 + ascent * font_size
8255}
8256
8257#[cfg(test)]
8258mod tests {
8259 use super::*;
8260 use crate::font::FontContext;
8261
8262 fn sg(glyph_id: u16, cluster: u32) -> shaping::ShapedGlyph {
8263 shaping::ShapedGlyph {
8264 glyph_id,
8265 cluster,
8266 x_advance: 500,
8267 y_advance: 0,
8268 x_offset: 0,
8269 y_offset: 0,
8270 }
8271 }
8272
8273 #[test]
8274 fn test_cluster_texts_ltr_ligature() {
8275 let chars: Vec<char> = "office".chars().collect();
8277 let shaped = [sg(1, 0), sg(2, 1), sg(3, 4), sg(4, 5)];
8278 let got = cluster_texts(&shaped, &chars);
8279 assert_eq!(
8280 got,
8281 vec![
8282 (None, false, None),
8283 (Some("ffi".to_string()), true, Some("ffi".to_string())),
8284 (None, false, None),
8285 (None, false, None)
8286 ]
8287 );
8288 }
8289
8290 #[test]
8295 fn test_cluster_texts_rtl_ligature_uses_next_larger_cluster() {
8296 let chars: Vec<char> = "abcd".chars().collect();
8297 let shaped = [sg(1, 3), sg(2, 1), sg(3, 0)];
8298 let got = cluster_texts(&shaped, &chars);
8299 assert_eq!(
8300 got,
8301 vec![
8302 (None, false, None),
8303 (Some("bc".to_string()), true, Some("bc".to_string())),
8304 (None, false, None)
8305 ]
8306 );
8307 }
8308
8309 #[test]
8313 fn test_cluster_texts_ligature_found_when_counts_balance() {
8314 let chars: Vec<char> = "abcd".chars().collect();
8316 let shaped = [sg(1, 0), sg(2, 2), sg(3, 3), sg(4, 3)];
8317 let got = cluster_texts(&shaped, &chars);
8318 assert_eq!(
8319 got[0],
8320 (Some("ab".to_string()), true, Some("ab".to_string()))
8321 );
8322 assert_eq!(got[1], (None, false, None));
8323 assert_eq!(got[2], (None, false, Some("d".to_string())));
8324 assert_eq!(got[3], (None, false, Some("\u{200B}".to_string())));
8325 }
8326
8327 #[test]
8331 fn test_cluster_texts_multi_glyph_cluster_is_not_a_ligature() {
8332 let chars: Vec<char> = "kixy".chars().collect();
8334 let shaped = [sg(1, 0), sg(2, 0), sg(3, 3)];
8335 let got = cluster_texts(&shaped, &chars);
8336 assert_eq!(
8337 got[0],
8338 (Some("kix".to_string()), false, Some("k".to_string()))
8339 );
8340 assert_eq!(
8341 got[1],
8342 (Some("kix".to_string()), false, Some("ix".to_string()))
8343 );
8344 assert_eq!(got[2], (None, false, None));
8345 }
8346
8347 fn make_text(content: &str, font_size: f64) -> Node {
8348 Node {
8349 kind: NodeKind::Text {
8350 content: content.to_string(),
8351 href: None,
8352 runs: vec![],
8353 },
8354 style: Style {
8355 font_size: Some(font_size),
8356 ..Default::default()
8357 },
8358 children: vec![],
8359 id: None,
8360 source_location: None,
8361 bookmark: None,
8362 href: None,
8363 alt: None,
8364 }
8365 }
8366
8367 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
8368 Node {
8369 kind: NodeKind::View,
8370 style,
8371 children,
8372 id: None,
8373 source_location: None,
8374 bookmark: None,
8375 href: None,
8376 alt: None,
8377 }
8378 }
8379
8380 fn make_runs_text(runs: Vec<crate::model::TextRun>) -> Node {
8381 Node {
8382 kind: NodeKind::Text {
8383 content: String::new(),
8384 href: None,
8385 runs,
8386 },
8387 style: Style::default(),
8388 children: vec![],
8389 id: None,
8390 source_location: None,
8391 bookmark: None,
8392 href: None,
8393 alt: None,
8394 }
8395 }
8396
8397 #[test]
8398 fn intrinsic_width_measures_runs_not_just_content() {
8399 let engine = LayoutEngine::new();
8403 let font_context = FontContext::new();
8404
8405 let runs_node = make_runs_text(vec![
8406 crate::model::TextRun {
8407 content: "Hello ".to_string(),
8408 style: Style::default(),
8409 href: None,
8410 },
8411 crate::model::TextRun {
8412 content: "World".to_string(),
8413 style: Style {
8414 font_weight: Some(700),
8415 ..Default::default()
8416 },
8417 href: None,
8418 },
8419 ]);
8420 let plain_node = make_text("Hello World", 12.0);
8421
8422 let runs_style = runs_node.style.resolve(None, 0.0);
8423 let plain_style = plain_node.style.resolve(None, 0.0);
8424 let runs_w = engine.measure_intrinsic_width(&runs_node, &runs_style, &font_context);
8425 let plain_w = engine.measure_intrinsic_width(&plain_node, &plain_style, &font_context);
8426
8427 assert!(
8430 runs_w >= plain_w,
8431 "runs width ({runs_w}) must not undershoot plain width ({plain_w})"
8432 );
8433 assert!(
8434 runs_w < plain_w * 1.5,
8435 "runs width ({runs_w}) should be close to plain width ({plain_w})"
8436 );
8437 }
8438
8439 #[test]
8440 fn intrinsic_width_of_multiline_text_is_widest_line() {
8441 let engine = LayoutEngine::new();
8442 let font_context = FontContext::new();
8443
8444 let multiline = make_text("123 Main St\nSpringfield, IL 62704", 12.0);
8445 let widest = make_text("Springfield, IL 62704", 12.0);
8446
8447 let m_style = multiline.style.resolve(None, 0.0);
8448 let w_style = widest.style.resolve(None, 0.0);
8449 let m_w = engine.measure_intrinsic_width(&multiline, &m_style, &font_context);
8450 let w_w = engine.measure_intrinsic_width(&widest, &w_style, &font_context);
8451
8452 assert!(
8453 (m_w - w_w).abs() < 0.01,
8454 "multiline intrinsic width ({m_w}) must equal its widest line ({w_w})"
8455 );
8456 }
8457
8458 #[test]
8459 fn intrinsic_width_of_heading_measures_its_text() {
8460 let engine = LayoutEngine::new();
8463 let font_context = FontContext::new();
8464
8465 let heading = Node {
8466 kind: NodeKind::Heading {
8467 level: 1,
8468 content: "Invoice #2024-001".to_string(),
8469 href: None,
8470 runs: vec![],
8471 },
8472 style: Style {
8473 font_size: Some(24.0),
8474 ..Default::default()
8475 },
8476 children: vec![],
8477 id: None,
8478 source_location: None,
8479 bookmark: None,
8480 href: None,
8481 alt: None,
8482 };
8483 let style = heading.style.resolve(None, 0.0);
8484 let w = engine.measure_intrinsic_width(&heading, &style, &font_context);
8485 assert!(w > 100.0, "24pt heading text must measure wide, got {w}");
8486 }
8487
8488 #[test]
8489 fn measure_node_height_of_wrapping_heading_matches_text() {
8490 let engine = LayoutEngine::new();
8497 let font_context = FontContext::new();
8498
8499 let content = "Annual Performance Review";
8500 let heading = Node {
8501 kind: NodeKind::Heading {
8502 level: 1,
8503 content: content.to_string(),
8504 href: None,
8505 runs: vec![],
8506 },
8507 style: Style {
8508 font_size: Some(32.0),
8509 ..Default::default()
8510 },
8511 children: vec![],
8512 id: None,
8513 source_location: None,
8514 bookmark: None,
8515 href: None,
8516 alt: None,
8517 };
8518 let text = make_text(content, 32.0);
8519
8520 let width = 200.0;
8522 let h_style = heading.style.resolve(None, width);
8523 let t_style = text.style.resolve(None, width);
8524 let h_height = engine.measure_node_height(&heading, width, &h_style, &font_context);
8525 let t_height = engine.measure_node_height(&text, width, &t_style, &font_context);
8526
8527 assert!(
8528 h_height > 32.0,
8529 "a wrapping 32pt heading must measure more than one line, got {h_height}"
8530 );
8531 assert!(
8532 (h_height - t_height).abs() < 0.01,
8533 "heading height ({h_height}) must equal the same text's height ({t_height})"
8534 );
8535 }
8536
8537 #[test]
8538 fn intrinsic_width_flex_row_sums_children() {
8539 let engine = LayoutEngine::new();
8540 let font_context = FontContext::new();
8541
8542 let child1 = make_text("Hello", 14.0);
8543 let child2 = make_text("World", 14.0);
8544
8545 let child1_style = child1.style.resolve(None, 0.0);
8546 let child2_style = child2.style.resolve(None, 0.0);
8547 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
8548 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
8549
8550 let row = make_styled_view(
8551 Style {
8552 flex_direction: Some(FlexDirection::Row),
8553 ..Default::default()
8554 },
8555 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
8556 );
8557 let row_style = row.style.resolve(None, 0.0);
8558 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
8559
8560 assert!(
8561 (row_w - (child1_w + child2_w)).abs() < 0.01,
8562 "Row intrinsic width ({}) should equal sum of children ({} + {})",
8563 row_w,
8564 child1_w,
8565 child2_w
8566 );
8567 }
8568
8569 #[test]
8570 fn intrinsic_width_flex_column_takes_max() {
8571 let engine = LayoutEngine::new();
8572 let font_context = FontContext::new();
8573
8574 let short = make_text("Hi", 14.0);
8575 let long = make_text("Hello World", 14.0);
8576
8577 let short_style = short.style.resolve(None, 0.0);
8578 let long_style = long.style.resolve(None, 0.0);
8579 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
8580 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
8581
8582 let col = make_styled_view(
8583 Style {
8584 flex_direction: Some(FlexDirection::Column),
8585 ..Default::default()
8586 },
8587 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
8588 );
8589 let col_style = col.style.resolve(None, 0.0);
8590 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
8591
8592 assert!(
8593 (col_w - long_w).abs() < 0.01,
8594 "Column intrinsic width ({}) should equal max child ({}, short was {})",
8595 col_w,
8596 long_w,
8597 short_w
8598 );
8599 }
8600
8601 #[test]
8602 fn intrinsic_width_nested_containers() {
8603 let engine = LayoutEngine::new();
8604 let font_context = FontContext::new();
8605
8606 let inner = make_styled_view(
8607 Style {
8608 flex_direction: Some(FlexDirection::Row),
8609 ..Default::default()
8610 },
8611 vec![make_text("A", 12.0), make_text("B", 12.0)],
8612 );
8613 let inner_style = inner.style.resolve(None, 0.0);
8614 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
8615
8616 let outer = make_styled_view(
8617 Style::default(),
8618 vec![make_styled_view(
8619 Style {
8620 flex_direction: Some(FlexDirection::Row),
8621 ..Default::default()
8622 },
8623 vec![make_text("A", 12.0), make_text("B", 12.0)],
8624 )],
8625 );
8626 let outer_style = outer.style.resolve(None, 0.0);
8627 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
8628
8629 assert!(
8630 (outer_w - inner_w).abs() < 0.01,
8631 "Nested container ({}) should match inner container ({})",
8632 outer_w,
8633 inner_w
8634 );
8635 }
8636
8637 #[test]
8638 fn intrinsic_width_row_with_gap() {
8639 let engine = LayoutEngine::new();
8640 let font_context = FontContext::new();
8641
8642 let no_gap = make_styled_view(
8643 Style {
8644 flex_direction: Some(FlexDirection::Row),
8645 ..Default::default()
8646 },
8647 vec![make_text("A", 12.0), make_text("B", 12.0)],
8648 );
8649 let with_gap = make_styled_view(
8650 Style {
8651 flex_direction: Some(FlexDirection::Row),
8652 gap: Some(10.0),
8653 ..Default::default()
8654 },
8655 vec![make_text("A", 12.0), make_text("B", 12.0)],
8656 );
8657
8658 let no_gap_style = no_gap.style.resolve(None, 0.0);
8659 let with_gap_style = with_gap.style.resolve(None, 0.0);
8660 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
8661 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
8662
8663 assert!(
8664 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
8665 "Gap should add 10pt: with_gap={}, no_gap={}",
8666 with_gap_w,
8667 no_gap_w
8668 );
8669 }
8670
8671 #[test]
8672 fn intrinsic_width_empty_container() {
8673 let engine = LayoutEngine::new();
8674 let font_context = FontContext::new();
8675
8676 let padding = 8.0;
8677 let empty = make_styled_view(
8678 Style {
8679 padding: Some(Edges::uniform(padding)),
8680 ..Default::default()
8681 },
8682 vec![],
8683 );
8684 let style = empty.style.resolve(None, 0.0);
8685 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
8686
8687 assert!(
8688 (w - padding * 2.0).abs() < 0.01,
8689 "Empty container width ({}) should equal horizontal padding ({})",
8690 w,
8691 padding * 2.0
8692 );
8693 }
8694
8695 #[test]
8698 fn flex_shrink_respects_min_content_width() {
8699 let engine = LayoutEngine::new();
8703 let font_context = FontContext::new();
8704
8705 let sale_text = make_text("SALE", 12.0);
8706 let sale_style = sale_text.style.resolve(None, 0.0);
8707 let sale_word_width =
8708 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
8709 assert!(
8710 sale_word_width > 0.0,
8711 "SALE should have non-zero min-content width"
8712 );
8713
8714 let container = make_styled_view(
8717 Style {
8718 flex_direction: Some(FlexDirection::Row),
8719 width: Some(Dimension::Pt(100.0)),
8720 ..Default::default()
8721 },
8722 vec![
8723 make_styled_view(
8724 Style {
8725 width: Some(Dimension::Pt(80.0)),
8726 flex_shrink: Some(1.0),
8727 ..Default::default()
8728 },
8729 vec![],
8730 ),
8731 make_styled_view(
8732 Style {
8733 width: Some(Dimension::Pt(60.0)),
8734 flex_shrink: Some(1.0),
8735 ..Default::default()
8736 },
8737 vec![make_text("SALE", 12.0)],
8738 ),
8739 ],
8740 );
8741
8742 let doc = Document {
8743 children: vec![Node::page(
8744 PageConfig::default(),
8745 Style::default(),
8746 vec![container],
8747 )],
8748 metadata: Default::default(),
8749 default_page: PageConfig::default(),
8750 first_page: None,
8751 left_page: None,
8752 right_page: None,
8753 named_pages: Default::default(),
8754 attachments: vec![],
8755 zugferd: None,
8756 fonts: vec![],
8757 tagged: false,
8758 pdfa: None,
8759 default_style: None,
8760 embedded_data: None,
8761 flatten_forms: false,
8762 pdf_ua: false,
8763 certification: None,
8764 pdf_version: Default::default(),
8765 pdf_ua2: false,
8766 };
8767
8768 let pages = engine.layout(&doc, &font_context);
8769 assert!(!pages.is_empty());
8770
8771 let page = &pages[0];
8774 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
8776 assert!(
8777 container_el.is_some(),
8778 "Should find container with 2 children"
8779 );
8780 let sale_child = &container_el.unwrap().children[1];
8781 assert!(
8782 sale_child.width >= sale_word_width - 0.01,
8783 "SALE child width ({}) should be >= min-content width ({})",
8784 sale_child.width,
8785 sale_word_width
8786 );
8787 }
8788
8789 #[test]
8792 fn column_justify_content_center() {
8793 let engine = LayoutEngine::new();
8796 let font_context = FontContext::new();
8797
8798 let container = make_styled_view(
8799 Style {
8800 flex_direction: Some(FlexDirection::Column),
8801 height: Some(Dimension::Pt(200.0)),
8802 justify_content: Some(JustifyContent::Center),
8803 ..Default::default()
8804 },
8805 vec![make_text("Centered", 12.0)],
8806 );
8807
8808 let doc = Document {
8809 children: vec![Node::page(
8810 PageConfig::default(),
8811 Style::default(),
8812 vec![container],
8813 )],
8814 metadata: Default::default(),
8815 default_page: PageConfig::default(),
8816 first_page: None,
8817 left_page: None,
8818 right_page: None,
8819 named_pages: Default::default(),
8820 attachments: vec![],
8821 zugferd: None,
8822 fonts: vec![],
8823 tagged: false,
8824 pdfa: None,
8825 default_style: None,
8826 embedded_data: None,
8827 flatten_forms: false,
8828 pdf_ua: false,
8829 certification: None,
8830 pdf_version: Default::default(),
8831 pdf_ua2: false,
8832 };
8833
8834 let pages = engine.layout(&doc, &font_context);
8835 let page = &pages[0];
8836
8837 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8840 assert!(
8841 container_el.is_some(),
8842 "Should find container with children"
8843 );
8844 let container_el = container_el.unwrap();
8845 let child = &container_el.children[0];
8846
8847 let child_offset = child.y - container_el.y;
8849 let expected_offset = (200.0 - child.height) / 2.0;
8850 assert!(
8851 (child_offset - expected_offset).abs() < 2.0,
8852 "Child offset ({}) should be near center ({})",
8853 child_offset,
8854 expected_offset
8855 );
8856 }
8857
8858 #[test]
8859 fn column_align_items_center() {
8860 let engine = LayoutEngine::new();
8863 let font_context = FontContext::new();
8864
8865 let container = make_styled_view(
8866 Style {
8867 flex_direction: Some(FlexDirection::Column),
8868 width: Some(Dimension::Pt(300.0)),
8869 align_items: Some(AlignItems::Center),
8870 ..Default::default()
8871 },
8872 vec![make_text("Hi", 12.0)],
8873 );
8874
8875 let doc = Document {
8876 children: vec![Node::page(
8877 PageConfig::default(),
8878 Style::default(),
8879 vec![container],
8880 )],
8881 metadata: Default::default(),
8882 default_page: PageConfig::default(),
8883 first_page: None,
8884 left_page: None,
8885 right_page: None,
8886 named_pages: Default::default(),
8887 attachments: vec![],
8888 zugferd: None,
8889 fonts: vec![],
8890 tagged: false,
8891 pdfa: None,
8892 default_style: None,
8893 embedded_data: None,
8894 flatten_forms: false,
8895 pdf_ua: false,
8896 certification: None,
8897 pdf_version: Default::default(),
8898 pdf_ua2: false,
8899 };
8900
8901 let pages = engine.layout(&doc, &font_context);
8902 let page = &pages[0];
8903
8904 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
8905 assert!(container_el.is_some());
8906 let container_el = container_el.unwrap();
8907 let child = &container_el.children[0];
8908
8909 let child_center = child.x + child.width / 2.0;
8911 let container_center = container_el.x + container_el.width / 2.0;
8912 assert!(
8913 (child_center - container_center).abs() < 2.0,
8914 "Child center ({}) should be near container center ({})",
8915 child_center,
8916 container_center
8917 );
8918 }
8919
8920 #[test]
8923 fn absolute_child_positioned_relative_to_parent() {
8924 let engine = LayoutEngine::new();
8928 let font_context = FontContext::new();
8929
8930 let parent = make_styled_view(
8931 Style {
8932 position: Some(crate::model::Position::Relative),
8933 margin: Some(MarginEdges::from_edges(Edges {
8934 top: 50.0,
8935 left: 50.0,
8936 ..Default::default()
8937 })),
8938 width: Some(Dimension::Pt(200.0)),
8939 height: Some(Dimension::Pt(200.0)),
8940 ..Default::default()
8941 },
8942 vec![make_styled_view(
8943 Style {
8944 position: Some(crate::model::Position::Absolute),
8945 top: Some(10.0),
8946 left: Some(10.0),
8947 width: Some(Dimension::Pt(50.0)),
8948 height: Some(Dimension::Pt(50.0)),
8949 ..Default::default()
8950 },
8951 vec![],
8952 )],
8953 );
8954
8955 let doc = Document {
8956 children: vec![Node::page(
8957 PageConfig::default(),
8958 Style::default(),
8959 vec![parent],
8960 )],
8961 metadata: Default::default(),
8962 default_page: PageConfig::default(),
8963 first_page: None,
8964 left_page: None,
8965 right_page: None,
8966 named_pages: Default::default(),
8967 attachments: vec![],
8968 zugferd: None,
8969 fonts: vec![],
8970 tagged: false,
8971 pdfa: None,
8972 default_style: None,
8973 embedded_data: None,
8974 flatten_forms: false,
8975 pdf_ua: false,
8976 certification: None,
8977 pdf_version: Default::default(),
8978 pdf_ua2: false,
8979 };
8980
8981 let pages = engine.layout(&doc, &font_context);
8982 let page = &pages[0];
8983
8984 let parent_el = page
8987 .elements
8988 .iter()
8989 .find(|e| e.width > 190.0 && e.width < 210.0);
8990 assert!(parent_el.is_some(), "Should find the 200x200 parent");
8991 let parent_el = parent_el.unwrap();
8992
8993 let abs_child = parent_el
8995 .children
8996 .iter()
8997 .find(|e| e.width > 45.0 && e.width < 55.0);
8998 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
8999 let abs_child = abs_child.unwrap();
9000
9001 let expected_x = parent_el.x + 10.0;
9002 let expected_y = parent_el.y + 10.0;
9003 assert!(
9004 (abs_child.x - expected_x).abs() < 1.0,
9005 "Absolute child x ({}) should be parent.x + 10 ({})",
9006 abs_child.x,
9007 expected_x
9008 );
9009 assert!(
9010 (abs_child.y - expected_y).abs() < 1.0,
9011 "Absolute child y ({}) should be parent.y + 10 ({})",
9012 abs_child.y,
9013 expected_y
9014 );
9015 }
9016
9017 #[test]
9018 fn absolute_escapes_unpositioned_parent_to_page() {
9019 let engine = LayoutEngine::new();
9024 let font_context = FontContext::new();
9025 let parent = make_styled_view(
9026 Style {
9027 margin: Some(MarginEdges::from_edges(Edges {
9028 top: 50.0,
9029 left: 50.0,
9030 ..Default::default()
9031 })),
9032 width: Some(Dimension::Pt(200.0)),
9033 height: Some(Dimension::Pt(200.0)),
9034 ..Default::default()
9035 },
9036 vec![make_styled_view(
9037 Style {
9038 position: Some(crate::model::Position::Absolute),
9039 top: Some(10.0),
9040 left: Some(10.0),
9041 width: Some(Dimension::Pt(50.0)),
9042 height: Some(Dimension::Pt(50.0)),
9043 ..Default::default()
9044 },
9045 vec![],
9046 )],
9047 );
9048 let doc = Document {
9049 children: vec![Node::page(
9050 PageConfig::default(),
9051 Style::default(),
9052 vec![parent],
9053 )],
9054 metadata: Default::default(),
9055 default_page: PageConfig::default(),
9056 first_page: None,
9057 left_page: None,
9058 right_page: None,
9059 named_pages: Default::default(),
9060 attachments: vec![],
9061 zugferd: None,
9062 fonts: vec![],
9063 tagged: false,
9064 pdfa: None,
9065 default_style: None,
9066 embedded_data: None,
9067 flatten_forms: false,
9068 pdf_ua: false,
9069 certification: None,
9070 pdf_version: Default::default(),
9071 pdf_ua2: false,
9072 };
9073 let pages = engine.layout(&doc, &font_context);
9074 let page = &pages[0];
9075 let parent_el = page
9076 .elements
9077 .iter()
9078 .find(|e| e.width > 190.0 && e.width < 210.0)
9079 .expect("parent");
9080 let abs_child = parent_el
9081 .children
9082 .iter()
9083 .find(|e| e.width > 45.0 && e.width < 55.0)
9084 .expect("abs child");
9085 let page_left = PageConfig::default().margin.left;
9086 let page_top = PageConfig::default().margin.top;
9087 assert!(
9088 (abs_child.x - (page_left + 10.0)).abs() < 1.0,
9089 "absolute escapes to the page: x {} should be page_left + 10 ({})",
9090 abs_child.x,
9091 page_left + 10.0
9092 );
9093 assert!(
9094 (abs_child.y - (page_top + 10.0)).abs() < 1.0,
9095 "absolute escapes to the page: y {} should be page_top + 10 ({})",
9096 abs_child.y,
9097 page_top + 10.0
9098 );
9099 assert!(
9100 abs_child.x < parent_el.x,
9101 "child must no longer be parent-relative (parent is 50pt further in)"
9102 );
9103 }
9104
9105 #[test]
9106 fn text_transform_none_passthrough() {
9107 assert_eq!(
9108 apply_text_transform("Hello World", TextTransform::None),
9109 "Hello World"
9110 );
9111 }
9112
9113 #[test]
9114 fn text_transform_uppercase() {
9115 assert_eq!(
9116 apply_text_transform("hello world", TextTransform::Uppercase),
9117 "HELLO WORLD"
9118 );
9119 }
9120
9121 #[test]
9122 fn text_transform_lowercase() {
9123 assert_eq!(
9124 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
9125 "hello world"
9126 );
9127 }
9128
9129 #[test]
9130 fn text_transform_capitalize() {
9131 assert_eq!(
9132 apply_text_transform("hello world", TextTransform::Capitalize),
9133 "Hello World"
9134 );
9135 assert_eq!(
9136 apply_text_transform(" hello world ", TextTransform::Capitalize),
9137 " Hello World "
9138 );
9139 assert_eq!(
9140 apply_text_transform("already Capitalized", TextTransform::Capitalize),
9141 "Already Capitalized"
9142 );
9143 }
9144
9145 #[test]
9146 fn text_transform_capitalize_empty() {
9147 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
9148 }
9149
9150 #[test]
9151 fn apply_char_transform_uppercase() {
9152 assert_eq!(
9153 apply_char_transform('a', TextTransform::Uppercase, false),
9154 'A'
9155 );
9156 assert_eq!(
9157 apply_char_transform('A', TextTransform::Uppercase, false),
9158 'A'
9159 );
9160 }
9161
9162 #[test]
9163 fn apply_char_transform_capitalize_word_start() {
9164 assert_eq!(
9165 apply_char_transform('h', TextTransform::Capitalize, true),
9166 'H'
9167 );
9168 assert_eq!(
9169 apply_char_transform('h', TextTransform::Capitalize, false),
9170 'h'
9171 );
9172 }
9173
9174 #[test]
9177 fn column_flex_grow_single_child_fills_container() {
9178 let engine = LayoutEngine::new();
9181 let font_context = FontContext::new();
9182
9183 let child = make_styled_view(
9184 Style {
9185 flex_grow: Some(1.0),
9186 ..Default::default()
9187 },
9188 vec![make_text("Short", 12.0)],
9189 );
9190
9191 let container = make_styled_view(
9192 Style {
9193 flex_direction: Some(FlexDirection::Column),
9194 height: Some(Dimension::Pt(300.0)),
9195 ..Default::default()
9196 },
9197 vec![child],
9198 );
9199
9200 let doc = Document {
9201 children: vec![Node::page(
9202 PageConfig::default(),
9203 Style::default(),
9204 vec![container],
9205 )],
9206 metadata: Default::default(),
9207 default_page: PageConfig::default(),
9208 first_page: None,
9209 left_page: None,
9210 right_page: None,
9211 named_pages: Default::default(),
9212 attachments: vec![],
9213 zugferd: None,
9214 fonts: vec![],
9215 tagged: false,
9216 pdfa: None,
9217 default_style: None,
9218 embedded_data: None,
9219 flatten_forms: false,
9220 pdf_ua: false,
9221 certification: None,
9222 pdf_version: Default::default(),
9223 pdf_ua2: false,
9224 };
9225
9226 let pages = engine.layout(&doc, &font_context);
9227 let page = &pages[0];
9228
9229 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
9230 assert!(container_el.is_some());
9231 let container_el = container_el.unwrap();
9232 assert!(
9233 (container_el.height - 300.0).abs() < 1.0,
9234 "Container should be 300pt, got {}",
9235 container_el.height
9236 );
9237
9238 let child_el = &container_el.children[0];
9239 assert!(
9240 (child_el.height - 300.0).abs() < 1.0,
9241 "flex-grow child should expand to 300pt, got {}",
9242 child_el.height
9243 );
9244 }
9245
9246 #[test]
9247 fn column_flex_grow_two_children_proportional() {
9248 let engine = LayoutEngine::new();
9251 let font_context = FontContext::new();
9252
9253 let child1 = make_styled_view(
9254 Style {
9255 flex_grow: Some(1.0),
9256 ..Default::default()
9257 },
9258 vec![make_text("A", 12.0)],
9259 );
9260 let child2 = make_styled_view(
9261 Style {
9262 flex_grow: Some(2.0),
9263 ..Default::default()
9264 },
9265 vec![make_text("B", 12.0)],
9266 );
9267
9268 let container = make_styled_view(
9269 Style {
9270 flex_direction: Some(FlexDirection::Column),
9271 height: Some(Dimension::Pt(300.0)),
9272 ..Default::default()
9273 },
9274 vec![child1, child2],
9275 );
9276
9277 let doc = Document {
9278 children: vec![Node::page(
9279 PageConfig::default(),
9280 Style::default(),
9281 vec![container],
9282 )],
9283 metadata: Default::default(),
9284 default_page: PageConfig::default(),
9285 first_page: None,
9286 left_page: None,
9287 right_page: None,
9288 named_pages: Default::default(),
9289 attachments: vec![],
9290 zugferd: None,
9291 fonts: vec![],
9292 tagged: false,
9293 pdfa: None,
9294 default_style: None,
9295 embedded_data: None,
9296 flatten_forms: false,
9297 pdf_ua: false,
9298 certification: None,
9299 pdf_version: Default::default(),
9300 pdf_ua2: false,
9301 };
9302
9303 let pages = engine.layout(&doc, &font_context);
9304 let page = &pages[0];
9305
9306 let container_el = page
9307 .elements
9308 .iter()
9309 .find(|e| e.children.len() == 2)
9310 .expect("Should find container with two children");
9311
9312 let c1 = &container_el.children[0];
9313 let c2 = &container_el.children[1];
9314
9315 let total = c1.height + c2.height;
9319 assert!(
9320 (total - 300.0).abs() < 2.0,
9321 "Children should sum to ~300pt, got {}",
9322 total
9323 );
9324
9325 let ratio = c2.height / c1.height;
9328 assert!(
9329 ratio > 1.3 && ratio < 2.5,
9330 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
9331 ratio
9332 );
9333 }
9334
9335 #[test]
9336 fn column_flex_grow_mixed_grow_and_fixed() {
9337 let engine = LayoutEngine::new();
9340 let font_context = FontContext::new();
9341
9342 let fixed_child = make_styled_view(
9343 Style {
9344 height: Some(Dimension::Pt(50.0)),
9345 ..Default::default()
9346 },
9347 vec![make_text("Fixed", 12.0)],
9348 );
9349 let grow_child = make_styled_view(
9350 Style {
9351 flex_grow: Some(1.0),
9352 ..Default::default()
9353 },
9354 vec![make_text("Grow", 12.0)],
9355 );
9356
9357 let container = make_styled_view(
9358 Style {
9359 flex_direction: Some(FlexDirection::Column),
9360 height: Some(Dimension::Pt(300.0)),
9361 ..Default::default()
9362 },
9363 vec![fixed_child, grow_child],
9364 );
9365
9366 let doc = Document {
9367 children: vec![Node::page(
9368 PageConfig::default(),
9369 Style::default(),
9370 vec![container],
9371 )],
9372 metadata: Default::default(),
9373 default_page: PageConfig::default(),
9374 first_page: None,
9375 left_page: None,
9376 right_page: None,
9377 named_pages: Default::default(),
9378 attachments: vec![],
9379 zugferd: None,
9380 fonts: vec![],
9381 tagged: false,
9382 pdfa: None,
9383 default_style: None,
9384 embedded_data: None,
9385 flatten_forms: false,
9386 pdf_ua: false,
9387 certification: None,
9388 pdf_version: Default::default(),
9389 pdf_ua2: false,
9390 };
9391
9392 let pages = engine.layout(&doc, &font_context);
9393 let page = &pages[0];
9394
9395 let container_el = page
9396 .elements
9397 .iter()
9398 .find(|e| e.children.len() == 2)
9399 .expect("Should find container with two children");
9400
9401 let fixed_el = &container_el.children[0];
9402 let grow_el = &container_el.children[1];
9403
9404 assert!(
9406 (fixed_el.height - 50.0).abs() < 1.0,
9407 "Fixed child should stay at 50pt, got {}",
9408 fixed_el.height
9409 );
9410
9411 assert!(
9413 (grow_el.height - 250.0).abs() < 2.0,
9414 "Grow child should expand to ~250pt, got {}",
9415 grow_el.height
9416 );
9417 }
9418
9419 #[test]
9420 fn column_flex_grow_page_level() {
9421 let engine = LayoutEngine::new();
9423 let font_context = FontContext::new();
9424
9425 let grow_child = make_styled_view(
9426 Style {
9427 flex_grow: Some(1.0),
9428 ..Default::default()
9429 },
9430 vec![make_text("Fill page", 12.0)],
9431 );
9432
9433 let doc = Document {
9434 children: vec![Node::page(
9435 PageConfig::default(),
9436 Style::default(),
9437 vec![grow_child],
9438 )],
9439 metadata: Default::default(),
9440 default_page: PageConfig::default(),
9441 first_page: None,
9442 left_page: None,
9443 right_page: None,
9444 named_pages: Default::default(),
9445 attachments: vec![],
9446 zugferd: None,
9447 fonts: vec![],
9448 tagged: false,
9449 pdfa: None,
9450 default_style: None,
9451 embedded_data: None,
9452 flatten_forms: false,
9453 pdf_ua: false,
9454 certification: None,
9455 pdf_version: Default::default(),
9456 pdf_ua2: false,
9457 };
9458
9459 let pages = engine.layout(&doc, &font_context);
9460 let page = &pages[0];
9461
9462 assert!(
9464 !page.elements.is_empty(),
9465 "Page should have at least one element"
9466 );
9467
9468 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
9469 let el = &page.elements[0];
9470 assert!(
9471 (el.height - content_height).abs() < 2.0,
9472 "Page-level flex-grow child should fill content height ({}), got {}",
9473 content_height,
9474 el.height
9475 );
9476 }
9477
9478 #[test]
9479 fn column_flex_grow_with_justify_content() {
9480 let engine = LayoutEngine::new();
9484 let font_context = FontContext::new();
9485
9486 let fixed_child = make_styled_view(
9487 Style {
9488 height: Some(Dimension::Pt(50.0)),
9489 ..Default::default()
9490 },
9491 vec![make_text("Top", 12.0)],
9492 );
9493 let grow_child = make_styled_view(
9494 Style {
9495 flex_grow: Some(1.0),
9496 ..Default::default()
9497 },
9498 vec![make_text("Fill", 12.0)],
9499 );
9500
9501 let container = make_styled_view(
9502 Style {
9503 flex_direction: Some(FlexDirection::Column),
9504 height: Some(Dimension::Pt(300.0)),
9505 justify_content: Some(JustifyContent::Center),
9506 ..Default::default()
9507 },
9508 vec![fixed_child, grow_child],
9509 );
9510
9511 let doc = Document {
9512 children: vec![Node::page(
9513 PageConfig::default(),
9514 Style::default(),
9515 vec![container],
9516 )],
9517 metadata: Default::default(),
9518 default_page: PageConfig::default(),
9519 first_page: None,
9520 left_page: None,
9521 right_page: None,
9522 named_pages: Default::default(),
9523 attachments: vec![],
9524 zugferd: None,
9525 fonts: vec![],
9526 tagged: false,
9527 pdfa: None,
9528 default_style: None,
9529 embedded_data: None,
9530 flatten_forms: false,
9531 pdf_ua: false,
9532 certification: None,
9533 pdf_version: Default::default(),
9534 pdf_ua2: false,
9535 };
9536
9537 let pages = engine.layout(&doc, &font_context);
9538 let page = &pages[0];
9539
9540 let container_el = page
9541 .elements
9542 .iter()
9543 .find(|e| e.children.len() == 2)
9544 .expect("Should find container");
9545
9546 let first_child = &container_el.children[0];
9549 assert!(
9550 (first_child.y - container_el.y).abs() < 1.0,
9551 "First child should be at top of container"
9552 );
9553
9554 let total = container_el.children[0].height + container_el.children[1].height;
9556 assert!(
9557 (total - 300.0).abs() < 2.0,
9558 "Children should fill container, got {}",
9559 total
9560 );
9561 }
9562
9563 #[test]
9564 fn column_flex_grow_child_justify_content_center() {
9565 let engine = LayoutEngine::new();
9568 let font_context = FontContext::new();
9569
9570 let inner_box = make_styled_view(
9572 Style {
9573 height: Some(Dimension::Pt(40.0)),
9574 ..Default::default()
9575 },
9576 vec![make_text("Centered", 12.0)],
9577 );
9578
9579 let grow_child = make_styled_view(
9581 Style {
9582 flex_grow: Some(1.0),
9583 flex_direction: Some(FlexDirection::Column),
9584 justify_content: Some(JustifyContent::Center),
9585 ..Default::default()
9586 },
9587 vec![inner_box],
9588 );
9589
9590 let container = make_styled_view(
9592 Style {
9593 flex_direction: Some(FlexDirection::Column),
9594 height: Some(Dimension::Pt(400.0)),
9595 ..Default::default()
9596 },
9597 vec![grow_child],
9598 );
9599
9600 let doc = Document {
9601 children: vec![Node::page(
9602 PageConfig::default(),
9603 Style::default(),
9604 vec![container],
9605 )],
9606 metadata: Default::default(),
9607 default_page: PageConfig::default(),
9608 first_page: None,
9609 left_page: None,
9610 right_page: None,
9611 named_pages: Default::default(),
9612 attachments: vec![],
9613 zugferd: None,
9614 fonts: vec![],
9615 tagged: false,
9616 pdfa: None,
9617 default_style: None,
9618 embedded_data: None,
9619 flatten_forms: false,
9620 pdf_ua: false,
9621 certification: None,
9622 pdf_version: Default::default(),
9623 pdf_ua2: false,
9624 };
9625
9626 let pages = engine.layout(&doc, &font_context);
9627 let page = &pages[0];
9628
9629 let container_el = page
9631 .elements
9632 .iter()
9633 .find(|e| e.height > 350.0 && e.children.len() == 1)
9634 .expect("Should find outer container");
9635
9636 let grow_el = &container_el.children[0];
9637 assert!(
9638 (grow_el.height - 400.0).abs() < 2.0,
9639 "Grow child should expand to 400, got {}",
9640 grow_el.height
9641 );
9642
9643 let inner_el = &grow_el.children[0];
9645 let expected_center = grow_el.y + grow_el.height / 2.0;
9646 let actual_center = inner_el.y + inner_el.height / 2.0;
9647 assert!(
9648 (actual_center - expected_center).abs() < 2.0,
9649 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
9650 expected_center,
9651 actual_center
9652 );
9653 }
9654
9655 #[test]
9656 fn column_flex_grow_child_justify_content_flex_end() {
9657 let engine = LayoutEngine::new();
9659 let font_context = FontContext::new();
9660
9661 let inner_box = make_styled_view(
9662 Style {
9663 height: Some(Dimension::Pt(30.0)),
9664 ..Default::default()
9665 },
9666 vec![make_text("Bottom", 12.0)],
9667 );
9668
9669 let grow_child = make_styled_view(
9670 Style {
9671 flex_grow: Some(1.0),
9672 flex_direction: Some(FlexDirection::Column),
9673 justify_content: Some(JustifyContent::FlexEnd),
9674 ..Default::default()
9675 },
9676 vec![inner_box],
9677 );
9678
9679 let container = make_styled_view(
9680 Style {
9681 flex_direction: Some(FlexDirection::Column),
9682 height: Some(Dimension::Pt(300.0)),
9683 ..Default::default()
9684 },
9685 vec![grow_child],
9686 );
9687
9688 let doc = Document {
9689 children: vec![Node::page(
9690 PageConfig::default(),
9691 Style::default(),
9692 vec![container],
9693 )],
9694 metadata: Default::default(),
9695 default_page: PageConfig::default(),
9696 first_page: None,
9697 left_page: None,
9698 right_page: None,
9699 named_pages: Default::default(),
9700 attachments: vec![],
9701 zugferd: None,
9702 fonts: vec![],
9703 tagged: false,
9704 pdfa: None,
9705 default_style: None,
9706 embedded_data: None,
9707 flatten_forms: false,
9708 pdf_ua: false,
9709 certification: None,
9710 pdf_version: Default::default(),
9711 pdf_ua2: false,
9712 };
9713
9714 let pages = engine.layout(&doc, &font_context);
9715 let page = &pages[0];
9716
9717 let container_el = page
9718 .elements
9719 .iter()
9720 .find(|e| e.height > 250.0 && e.children.len() == 1)
9721 .expect("Should find outer container");
9722
9723 let grow_el = &container_el.children[0];
9724 let inner_el = &grow_el.children[0];
9725
9726 let inner_bottom = inner_el.y + inner_el.height;
9728 let grow_bottom = grow_el.y + grow_el.height;
9729 assert!(
9730 (inner_bottom - grow_bottom).abs() < 2.0,
9731 "Inner box bottom ({}) should align with grow child bottom ({})",
9732 inner_bottom,
9733 grow_bottom
9734 );
9735 }
9736
9737 #[test]
9738 fn column_flex_grow_child_no_justify_unchanged() {
9739 let engine = LayoutEngine::new();
9741 let font_context = FontContext::new();
9742
9743 let inner_box = make_styled_view(
9744 Style {
9745 height: Some(Dimension::Pt(50.0)),
9746 ..Default::default()
9747 },
9748 vec![make_text("Top", 12.0)],
9749 );
9750
9751 let grow_child = make_styled_view(
9752 Style {
9753 flex_grow: Some(1.0),
9754 flex_direction: Some(FlexDirection::Column),
9755 ..Default::default()
9757 },
9758 vec![inner_box],
9759 );
9760
9761 let container = make_styled_view(
9762 Style {
9763 flex_direction: Some(FlexDirection::Column),
9764 height: Some(Dimension::Pt(300.0)),
9765 ..Default::default()
9766 },
9767 vec![grow_child],
9768 );
9769
9770 let doc = Document {
9771 children: vec![Node::page(
9772 PageConfig::default(),
9773 Style::default(),
9774 vec![container],
9775 )],
9776 metadata: Default::default(),
9777 default_page: PageConfig::default(),
9778 first_page: None,
9779 left_page: None,
9780 right_page: None,
9781 named_pages: Default::default(),
9782 attachments: vec![],
9783 zugferd: None,
9784 fonts: vec![],
9785 tagged: false,
9786 pdfa: None,
9787 default_style: None,
9788 embedded_data: None,
9789 flatten_forms: false,
9790 pdf_ua: false,
9791 certification: None,
9792 pdf_version: Default::default(),
9793 pdf_ua2: false,
9794 };
9795
9796 let pages = engine.layout(&doc, &font_context);
9797 let page = &pages[0];
9798
9799 let container_el = page
9800 .elements
9801 .iter()
9802 .find(|e| e.height > 250.0 && e.children.len() == 1)
9803 .expect("Should find outer container");
9804
9805 let grow_el = &container_el.children[0];
9806 let inner_el = &grow_el.children[0];
9807
9808 assert!(
9810 (inner_el.y - grow_el.y).abs() < 2.0,
9811 "Inner box ({}) should be at top of grow child ({})",
9812 inner_el.y,
9813 grow_el.y
9814 );
9815 }
9816
9817 #[test]
9818 fn column_flex_grow_child_align_items_center() {
9819 let engine = LayoutEngine::new();
9821 let font_context = FontContext::new();
9822
9823 let text = make_text("Hello", 12.0);
9824
9825 let grow_child = make_styled_view(
9826 Style {
9827 flex_grow: Some(1.0),
9828 flex_direction: Some(FlexDirection::Column),
9829 align_items: Some(AlignItems::Center),
9830 ..Default::default()
9831 },
9832 vec![text],
9833 );
9834
9835 let container = make_styled_view(
9836 Style {
9837 flex_direction: Some(FlexDirection::Column),
9838 height: Some(Dimension::Pt(300.0)),
9839 ..Default::default()
9840 },
9841 vec![grow_child],
9842 );
9843
9844 let doc = Document {
9845 children: vec![Node::page(
9846 PageConfig::default(),
9847 Style::default(),
9848 vec![container],
9849 )],
9850 metadata: Default::default(),
9851 default_page: PageConfig::default(),
9852 first_page: None,
9853 left_page: None,
9854 right_page: None,
9855 named_pages: Default::default(),
9856 attachments: vec![],
9857 zugferd: None,
9858 fonts: vec![],
9859 tagged: false,
9860 pdfa: None,
9861 default_style: None,
9862 embedded_data: None,
9863 flatten_forms: false,
9864 pdf_ua: false,
9865 certification: None,
9866 pdf_version: Default::default(),
9867 pdf_ua2: false,
9868 };
9869
9870 let pages = engine.layout(&doc, &font_context);
9871 let page = &pages[0];
9872
9873 let container_el = page
9874 .elements
9875 .iter()
9876 .find(|e| e.height > 250.0 && e.children.len() == 1)
9877 .expect("Should find outer container");
9878
9879 let grow_el = &container_el.children[0];
9880 assert!(
9881 !grow_el.children.is_empty(),
9882 "Grow child should have text child"
9883 );
9884
9885 let text_el = &grow_el.children[0];
9886 let text_center = text_el.x + text_el.width / 2.0;
9887 let grow_center = grow_el.x + grow_el.width / 2.0;
9888 assert!(
9889 (text_center - grow_center).abs() < 2.0,
9890 "Text center ({}) should be near grow child center ({})",
9891 text_center,
9892 grow_center
9893 );
9894 }
9895
9896 #[test]
9897 fn image_intrinsic_width_respects_height_constraint() {
9898 let engine = LayoutEngine::new();
9902 let font_context = FontContext::new();
9903
9904 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
9906
9907 let image_node = Node {
9908 kind: NodeKind::Image {
9909 src: one_px_png.to_string(),
9910 width: None,
9911 height: Some(36.0),
9912 },
9913 style: Style::default(),
9914 children: vec![],
9915 id: None,
9916 source_location: None,
9917 bookmark: None,
9918 href: None,
9919 alt: None,
9920 };
9921
9922 let resolved = image_node.style.resolve(None, 0.0);
9923 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
9924
9925 assert!(
9927 (intrinsic - 36.0).abs() < 1.0,
9928 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
9929 intrinsic
9930 );
9931 }
9932}