1pub mod flex;
35pub mod grid;
36pub mod page_break;
37
38use std::cell::RefCell;
39use std::collections::HashMap;
40
41use serde::Serialize;
42
43use crate::font::FontContext;
44use crate::model::*;
45use crate::style::*;
46use crate::text::bidi;
47use crate::text::shaping;
48use crate::text::{BrokenLine, RunBrokenLine, StyledChar, TextLayout};
49
50#[derive(Debug, Clone, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct BookmarkEntry {
54 pub title: String,
55 pub page_index: usize,
56 pub y: f64,
57}
58
59#[derive(Debug, Clone, Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct LayoutInfo {
65 pub pages: Vec<PageInfo>,
66}
67
68#[derive(Debug, Clone, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct PageInfo {
72 pub width: f64,
73 pub height: f64,
74 pub content_x: f64,
75 pub content_y: f64,
76 pub content_width: f64,
77 pub content_height: f64,
78 pub elements: Vec<ElementInfo>,
79}
80
81#[derive(Debug, Clone, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct ElementStyleInfo {
85 pub margin: Edges,
87 pub padding: Edges,
88 pub border_width: Edges,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub width: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub height: Option<String>,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub min_width: Option<f64>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub min_height: Option<f64>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub max_width: Option<f64>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub max_height: Option<f64>,
101 pub flex_direction: FlexDirection,
103 pub justify_content: JustifyContent,
104 pub align_items: AlignItems,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub align_self: Option<AlignItems>,
107 pub flex_wrap: FlexWrap,
108 pub align_content: AlignContent,
109 pub flex_grow: f64,
110 pub flex_shrink: f64,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub flex_basis: Option<String>,
113 pub gap: f64,
114 pub row_gap: f64,
115 pub column_gap: f64,
116 pub font_family: String,
118 pub font_size: f64,
119 pub font_weight: u32,
120 pub font_style: FontStyle,
121 pub line_height: f64,
122 pub text_align: TextAlign,
123 pub letter_spacing: f64,
124 pub text_decoration: TextDecoration,
125 pub text_transform: TextTransform,
126 pub color: Color,
128 pub background_color: Option<Color>,
129 pub border_color: EdgeValues<Color>,
130 pub border_radius: CornerValues,
131 pub opacity: f64,
132 pub position: Position,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub top: Option<f64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub right: Option<f64>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub bottom: Option<f64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub left: Option<f64>,
142 pub overflow: Overflow,
144 pub breakable: bool,
146 pub break_before: bool,
147 pub min_widow_lines: u32,
148 pub min_orphan_lines: u32,
149}
150
151fn size_constraint_to_str(sc: &SizeConstraint) -> Option<String> {
152 match sc {
153 SizeConstraint::Auto => None,
154 SizeConstraint::Fixed(v) => Some(format!("{v}")),
155 }
156}
157
158impl ElementStyleInfo {
159 fn from_resolved(style: &ResolvedStyle) -> Self {
160 ElementStyleInfo {
161 margin: style.margin.to_edges(),
162 padding: style.padding,
163 border_width: style.border_width,
164 width: size_constraint_to_str(&style.width),
165 height: size_constraint_to_str(&style.height),
166 min_width: if style.min_width > 0.0 {
167 Some(style.min_width)
168 } else {
169 None
170 },
171 min_height: if style.min_height > 0.0 {
172 Some(style.min_height)
173 } else {
174 None
175 },
176 max_width: if style.max_width.is_finite() {
177 Some(style.max_width)
178 } else {
179 None
180 },
181 max_height: if style.max_height.is_finite() {
182 Some(style.max_height)
183 } else {
184 None
185 },
186 flex_direction: style.flex_direction,
187 justify_content: style.justify_content,
188 align_items: style.align_items,
189 align_self: style.align_self,
190 flex_wrap: style.flex_wrap,
191 align_content: style.align_content,
192 flex_grow: style.flex_grow,
193 flex_shrink: style.flex_shrink,
194 flex_basis: size_constraint_to_str(&style.flex_basis),
195 gap: style.gap,
196 row_gap: style.row_gap,
197 column_gap: style.column_gap,
198 font_family: style.font_family.clone(),
199 font_size: style.font_size,
200 font_weight: style.font_weight,
201 font_style: style.font_style,
202 line_height: style.line_height,
203 text_align: style.text_align,
204 letter_spacing: style.letter_spacing,
205 text_decoration: style.text_decoration,
206 text_transform: style.text_transform,
207 color: style.color,
208 background_color: style.background_color,
209 border_color: style.border_color,
210 border_radius: style.border_radius,
211 opacity: style.opacity,
212 position: style.position,
213 top: style.top,
214 right: style.right,
215 bottom: style.bottom,
216 left: style.left,
217 overflow: style.overflow,
218 breakable: style.breakable,
219 break_before: style.break_before,
220 min_widow_lines: style.min_widow_lines,
221 min_orphan_lines: style.min_orphan_lines,
222 }
223 }
224}
225
226impl Default for ElementStyleInfo {
227 fn default() -> Self {
228 ElementStyleInfo {
229 margin: Edges::default(),
230 padding: Edges::default(),
231 border_width: Edges::default(),
232 width: None,
233 height: None,
234 min_width: None,
235 min_height: None,
236 max_width: None,
237 max_height: None,
238 flex_direction: FlexDirection::default(),
239 justify_content: JustifyContent::default(),
240 align_items: AlignItems::default(),
241 align_self: None,
242 flex_wrap: FlexWrap::default(),
243 align_content: AlignContent::default(),
244 flex_grow: 0.0,
245 flex_shrink: 1.0,
246 flex_basis: None,
247 gap: 0.0,
248 row_gap: 0.0,
249 column_gap: 0.0,
250 font_family: "Helvetica".to_string(),
251 font_size: 12.0,
252 font_weight: 400,
253 font_style: FontStyle::default(),
254 line_height: 1.4,
255 text_align: TextAlign::default(),
256 letter_spacing: 0.0,
257 text_decoration: TextDecoration::None,
258 text_transform: TextTransform::None,
259 color: Color::BLACK,
260 background_color: None,
261 border_color: EdgeValues::uniform(Color::BLACK),
262 border_radius: CornerValues::uniform(0.0),
263 opacity: 1.0,
264 position: Position::default(),
265 top: None,
266 right: None,
267 bottom: None,
268 left: None,
269 overflow: Overflow::default(),
270 breakable: false,
271 break_before: false,
272 min_widow_lines: 2,
273 min_orphan_lines: 2,
274 }
275 }
276}
277
278#[derive(Debug, Clone, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct ElementInfo {
282 pub x: f64,
283 pub y: f64,
284 pub width: f64,
285 pub height: f64,
286 pub kind: String,
288 pub node_type: String,
290 pub style: ElementStyleInfo,
292 pub children: Vec<ElementInfo>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub source_location: Option<SourceLocation>,
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub text_content: Option<String>,
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub href: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub bookmark: Option<String>,
306}
307
308impl LayoutInfo {
309 pub fn from_pages(pages: &[LayoutPage]) -> Self {
311 LayoutInfo {
312 pages: pages
313 .iter()
314 .map(|page| {
315 let (page_w, page_h) = page.config.size.dimensions();
316 let content_x = page.config.margin.left;
317 let content_y = page.config.margin.top;
318 let content_width = page_w - page.config.margin.horizontal();
319 let content_height = page_h - page.config.margin.vertical();
320
321 let elements = Self::build_element_tree(&page.elements);
322
323 PageInfo {
324 width: page_w,
325 height: page_h,
326 content_x,
327 content_y,
328 content_width,
329 content_height,
330 elements,
331 }
332 })
333 .collect(),
334 }
335 }
336
337 fn build_element_tree(elems: &[LayoutElement]) -> Vec<ElementInfo> {
338 elems
339 .iter()
340 .map(|elem| {
341 let kind = match &elem.draw {
342 DrawCommand::None => "None",
343 DrawCommand::Rect { .. } => "Rect",
344 DrawCommand::Text { .. } => "Text",
345 DrawCommand::Image { .. } => "Image",
346 DrawCommand::ImagePlaceholder => "ImagePlaceholder",
347 DrawCommand::Svg { .. } => "Svg",
348 DrawCommand::Barcode { .. } => "Barcode",
349 DrawCommand::QrCode { .. } => "QrCode",
350 DrawCommand::Chart { .. } => "Chart",
351 DrawCommand::Watermark { .. } => "Watermark",
352 };
353 let text_content = match &elem.draw {
354 DrawCommand::Text { lines, .. } => {
355 let text: String = lines
356 .iter()
357 .flat_map(|line| {
358 line.glyphs.iter().flat_map(|g| {
359 g.cluster_text.as_deref().unwrap_or("").chars().chain(
361 if g.cluster_text.is_none() {
362 Some(g.char_value)
363 } else {
364 None
365 },
366 )
367 })
368 })
369 .collect();
370 if text.is_empty() {
371 None
372 } else {
373 Some(text)
374 }
375 }
376 _ => None,
377 };
378 let node_type = elem.node_type.clone().unwrap_or_else(|| kind.to_string());
379 let style = elem
380 .resolved_style
381 .as_ref()
382 .map(ElementStyleInfo::from_resolved)
383 .unwrap_or_default();
384 ElementInfo {
385 x: elem.x,
386 y: elem.y,
387 width: elem.width,
388 height: elem.height,
389 kind: kind.to_string(),
390 node_type,
391 style,
392 children: Self::build_element_tree(&elem.children),
393 source_location: elem.source_location.clone(),
394 text_content,
395 href: elem.href.clone(),
396 bookmark: elem.bookmark.clone(),
397 }
398 })
399 .collect()
400 }
401}
402
403#[derive(Debug, Clone)]
405pub struct LayoutPage {
406 pub width: f64,
407 pub height: f64,
408 pub elements: Vec<LayoutElement>,
409 pub(crate) fixed_header: Vec<(Node, f64)>,
411 pub(crate) fixed_footer: Vec<(Node, f64)>,
413 pub(crate) watermarks: Vec<Node>,
415 pub(crate) config: PageConfig,
417}
418
419#[derive(Debug, Clone)]
421pub struct LayoutElement {
422 pub x: f64,
424 pub y: f64,
425 pub width: f64,
427 pub height: f64,
428 pub draw: DrawCommand,
430 pub children: Vec<LayoutElement>,
432 pub node_type: Option<String>,
434 pub resolved_style: Option<ResolvedStyle>,
436 pub source_location: Option<SourceLocation>,
438 pub href: Option<String>,
440 pub bookmark: Option<String>,
442 pub alt: Option<String>,
444 pub is_header_row: bool,
446 pub overflow: Overflow,
448}
449
450fn node_kind_name(kind: &NodeKind) -> &'static str {
452 match kind {
453 NodeKind::View => "View",
454 NodeKind::Text { .. } => "Text",
455 NodeKind::Image { .. } => "Image",
456 NodeKind::Table { .. } => "Table",
457 NodeKind::TableRow { .. } => "TableRow",
458 NodeKind::TableCell { .. } => "TableCell",
459 NodeKind::Fixed {
460 position: FixedPosition::Header,
461 } => "FixedHeader",
462 NodeKind::Fixed {
463 position: FixedPosition::Footer,
464 } => "FixedFooter",
465 NodeKind::Page { .. } => "Page",
466 NodeKind::PageBreak => "PageBreak",
467 NodeKind::Svg { .. } => "Svg",
468 NodeKind::Canvas { .. } => "Canvas",
469 NodeKind::Barcode { .. } => "Barcode",
470 NodeKind::QrCode { .. } => "QrCode",
471 NodeKind::BarChart { .. } => "BarChart",
472 NodeKind::LineChart { .. } => "LineChart",
473 NodeKind::PieChart { .. } => "PieChart",
474 NodeKind::AreaChart { .. } => "AreaChart",
475 NodeKind::DotPlot { .. } => "DotPlot",
476 NodeKind::Watermark { .. } => "Watermark",
477 }
478}
479
480#[derive(Debug, Clone)]
482pub enum DrawCommand {
483 None,
485 Rect {
487 background: Option<Color>,
488 border_width: Edges,
489 border_color: EdgeValues<Color>,
490 border_radius: CornerValues,
491 opacity: f64,
492 },
493 Text {
495 lines: Vec<TextLine>,
496 color: Color,
497 text_decoration: TextDecoration,
498 opacity: f64,
499 },
500 Image {
502 image_data: crate::image_loader::LoadedImage,
503 },
504 ImagePlaceholder,
506 Svg {
508 commands: Vec<crate::svg::SvgCommand>,
509 width: f64,
510 height: f64,
511 clip: bool,
513 },
514 Barcode {
516 bars: Vec<u8>,
517 bar_width: f64,
518 height: f64,
519 color: Color,
520 },
521 QrCode {
523 modules: Vec<Vec<bool>>,
524 module_size: f64,
525 color: Color,
526 },
527 Chart {
529 primitives: Vec<crate::chart::ChartPrimitive>,
530 },
531 Watermark {
533 lines: Vec<TextLine>,
534 color: Color,
535 opacity: f64,
536 angle_rad: f64,
537 font_family: String,
539 },
540}
541
542#[derive(Debug, Clone)]
543pub struct TextLine {
544 pub x: f64,
545 pub y: f64,
546 pub glyphs: Vec<PositionedGlyph>,
547 pub width: f64,
548 pub height: f64,
549 pub word_spacing: f64,
551}
552
553#[derive(Debug, Clone)]
554pub struct PositionedGlyph {
555 pub glyph_id: u16,
558 pub x_offset: f64,
560 pub y_offset: f64,
562 pub x_advance: f64,
564 pub font_size: f64,
565 pub font_family: String,
566 pub font_weight: u32,
567 pub font_style: FontStyle,
568 pub char_value: char,
570 pub color: Option<Color>,
572 pub href: Option<String>,
574 pub text_decoration: TextDecoration,
576 pub letter_spacing: f64,
578 pub cluster_text: Option<String>,
581}
582
583fn offset_element_y(el: &mut LayoutElement, dy: f64) {
586 el.y += dy;
587 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
588 for line in lines.iter_mut() {
589 line.y += dy;
590 }
591 }
592 for child in &mut el.children {
593 offset_element_y(child, dy);
594 }
595}
596
597#[allow(dead_code)]
599fn offset_element_x(el: &mut LayoutElement, dx: f64) {
600 el.x += dx;
601 if let DrawCommand::Text { ref mut lines, .. } = el.draw {
602 for line in lines.iter_mut() {
603 line.x += dx;
604 }
605 }
606 for child in &mut el.children {
607 offset_element_x(child, dx);
608 }
609}
610
611fn reapply_justify_content(elem: &mut LayoutElement) {
615 let style = match elem.resolved_style {
616 Some(ref s) => s,
617 None => return,
618 };
619 if matches!(style.justify_content, JustifyContent::FlexStart) {
620 return;
621 }
622 if elem.children.is_empty() {
623 return;
624 }
625
626 let padding_top = style.padding.top + style.border_width.top;
627 let padding_bottom = style.padding.bottom + style.border_width.bottom;
628 let inner_h = elem.height - padding_top - padding_bottom;
629 let content_top = elem.y + padding_top;
630
631 let last_child = &elem.children[elem.children.len() - 1];
633 let children_bottom = last_child.y + last_child.height;
634 let children_span = children_bottom - content_top;
635 let slack = inner_h - children_span;
636 if slack < 0.001 {
637 return;
638 }
639
640 let n = elem.children.len();
641 let offsets: Vec<f64> = match style.justify_content {
642 JustifyContent::FlexEnd => vec![slack; n],
643 JustifyContent::Center => vec![slack / 2.0; n],
644 JustifyContent::SpaceBetween => {
645 if n <= 1 {
646 vec![0.0; n]
647 } else {
648 let per_gap = slack / (n - 1) as f64;
649 (0..n).map(|i| i as f64 * per_gap).collect()
650 }
651 }
652 JustifyContent::SpaceAround => {
653 let space = slack / n as f64;
654 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
655 }
656 JustifyContent::SpaceEvenly => {
657 let space = slack / (n + 1) as f64;
658 (0..n).map(|i| (i + 1) as f64 * space).collect()
659 }
660 JustifyContent::FlexStart => unreachable!(),
661 };
662
663 for (i, child) in elem.children.iter_mut().enumerate() {
664 let dy = offsets[i];
665 if dy.abs() > 0.001 {
666 offset_element_y(child, dy);
667 }
668 }
669}
670
671fn apply_text_transform(text: &str, transform: TextTransform) -> String {
673 match transform {
674 TextTransform::None => text.to_string(),
675 TextTransform::Uppercase => text.to_uppercase(),
676 TextTransform::Lowercase => text.to_lowercase(),
677 TextTransform::Capitalize => {
678 let mut result = String::with_capacity(text.len());
679 let mut prev_is_whitespace = true;
680 for ch in text.chars() {
681 if prev_is_whitespace && ch.is_alphabetic() {
682 for upper in ch.to_uppercase() {
683 result.push(upper);
684 }
685 } else {
686 result.push(ch);
687 }
688 prev_is_whitespace = ch.is_whitespace();
689 }
690 result
691 }
692 }
693}
694
695fn apply_char_transform(ch: char, transform: TextTransform, is_word_start: bool) -> char {
698 match transform {
699 TextTransform::None => ch,
700 TextTransform::Uppercase => ch.to_uppercase().next().unwrap_or(ch),
701 TextTransform::Lowercase => ch.to_lowercase().next().unwrap_or(ch),
702 TextTransform::Capitalize => {
703 if is_word_start && ch.is_alphabetic() {
704 ch.to_uppercase().next().unwrap_or(ch)
705 } else {
706 ch
707 }
708 }
709 }
710}
711
712pub struct LayoutEngine {
714 text_layout: TextLayout,
715 image_dim_cache: RefCell<HashMap<String, (u32, u32)>>,
716}
717
718#[derive(Debug, Clone)]
720struct PageCursor {
721 config: PageConfig,
722 content_width: f64,
723 content_height: f64,
724 y: f64,
725 elements: Vec<LayoutElement>,
726 fixed_header: Vec<(Node, f64)>,
727 fixed_footer: Vec<(Node, f64)>,
728 watermarks: Vec<Node>,
730 content_x: f64,
731 content_y: f64,
732 continuation_top_offset: f64,
734}
735
736impl PageCursor {
737 fn new(config: &PageConfig) -> Self {
738 let (page_w, page_h) = config.size.dimensions();
739 let content_width = page_w - config.margin.horizontal();
740 let content_height = page_h - config.margin.vertical();
741
742 Self {
743 config: config.clone(),
744 content_width,
745 content_height,
746 y: 0.0,
747 elements: Vec::new(),
748 fixed_header: Vec::new(),
749 fixed_footer: Vec::new(),
750 watermarks: Vec::new(),
751 content_x: config.margin.left,
752 content_y: config.margin.top,
753 continuation_top_offset: 0.0,
754 }
755 }
756
757 fn remaining_height(&self) -> f64 {
758 let footer_height: f64 = self.fixed_footer.iter().map(|(_, h)| *h).sum();
759 (self.content_height - self.y - footer_height).max(0.0)
760 }
761
762 fn finalize(&self) -> LayoutPage {
763 let (page_w, page_h) = self.config.size.dimensions();
764 LayoutPage {
765 width: page_w,
766 height: page_h,
767 elements: self.elements.clone(),
768 fixed_header: self.fixed_header.clone(),
769 fixed_footer: self.fixed_footer.clone(),
770 watermarks: self.watermarks.clone(),
771 config: self.config.clone(),
772 }
773 }
774
775 fn new_page(&self) -> Self {
776 let mut cursor = PageCursor::new(&self.config);
777 cursor.fixed_header = self.fixed_header.clone();
778 cursor.fixed_footer = self.fixed_footer.clone();
779 cursor.watermarks = self.watermarks.clone();
780 cursor.continuation_top_offset = self.continuation_top_offset;
781
782 let header_height: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
783 cursor.y = header_height + cursor.continuation_top_offset;
784
785 cursor
786 }
787}
788
789impl Default for LayoutEngine {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795impl LayoutEngine {
796 pub fn new() -> Self {
797 Self {
798 text_layout: TextLayout::new(),
799 image_dim_cache: RefCell::new(HashMap::new()),
800 }
801 }
802
803 fn get_image_dimensions(&self, src: &str) -> Option<(u32, u32)> {
805 if let Some(dims) = self.image_dim_cache.borrow().get(src) {
806 return Some(*dims);
807 }
808 if let Ok(dims) = crate::image_loader::load_image_dimensions(src) {
809 self.image_dim_cache
810 .borrow_mut()
811 .insert(src.to_string(), dims);
812 Some(dims)
813 } else {
814 None
815 }
816 }
817
818 pub fn layout(&self, document: &Document, font_context: &FontContext) -> Vec<LayoutPage> {
820 let mut pages: Vec<LayoutPage> = Vec::new();
821 let mut cursor = PageCursor::new(&document.default_page);
822
823 let base = document.default_style.clone().unwrap_or_default();
825 let root_style = Style {
826 lang: base.lang.clone().or(document.metadata.lang.clone()),
827 ..base
828 }
829 .resolve(None, cursor.content_width);
830
831 for node in &document.children {
832 match &node.kind {
833 NodeKind::Page { config } => {
834 if !cursor.elements.is_empty() || cursor.y > 0.0 {
835 pages.push(cursor.finalize());
836 }
837 cursor = PageCursor::new(config);
838
839 let mut page_root = root_style.clone();
844 page_root.height = SizeConstraint::Fixed(cursor.content_height);
845
846 let cx = cursor.content_x;
847 let cw = cursor.content_width;
848 self.layout_children(
849 &node.children,
850 &node.style,
851 &mut cursor,
852 &mut pages,
853 cx,
854 cw,
855 Some(&page_root),
856 font_context,
857 );
858 }
859 NodeKind::PageBreak => {
860 pages.push(cursor.finalize());
861 cursor = cursor.new_page();
862 }
863 _ => {
864 let cx = cursor.content_x;
865 let cw = cursor.content_width;
866 self.layout_node(
867 node,
868 &mut cursor,
869 &mut pages,
870 cx,
871 cw,
872 Some(&root_style),
873 font_context,
874 None,
875 );
876 }
877 }
878 }
879
880 if !cursor.elements.is_empty() || cursor.y > 0.0 {
881 pages.push(cursor.finalize());
882 }
883
884 self.inject_fixed_elements(&mut pages, font_context);
885
886 pages
887 }
888
889 #[allow(clippy::too_many_arguments)]
890 fn layout_node(
891 &self,
892 node: &Node,
893 cursor: &mut PageCursor,
894 pages: &mut Vec<LayoutPage>,
895 x: f64,
896 available_width: f64,
897 parent_style: Option<&ResolvedStyle>,
898 font_context: &FontContext,
899 cross_axis_height: Option<f64>,
900 ) {
901 let mut style = node.style.resolve(parent_style, available_width);
902
903 if let Some(h) = cross_axis_height {
906 if matches!(style.height, SizeConstraint::Auto) {
907 style.height = SizeConstraint::Fixed(h);
908 }
909 }
910
911 if style.break_before {
912 pages.push(cursor.finalize());
913 *cursor = cursor.new_page();
914 }
915
916 match &node.kind {
917 NodeKind::PageBreak => {
918 pages.push(cursor.finalize());
919 *cursor = cursor.new_page();
920 }
921
922 NodeKind::Fixed { position } => {
923 let height = self.measure_node_height(node, available_width, &style, font_context);
924 match position {
925 FixedPosition::Header => {
926 cursor.fixed_header.push((node.clone(), height));
927 cursor.y += height;
928 }
929 FixedPosition::Footer => {
930 cursor.fixed_footer.push((node.clone(), height));
931 }
932 }
933 }
934
935 NodeKind::Watermark { .. } => {
936 cursor.watermarks.push(node.clone());
938 }
939
940 NodeKind::Text {
941 content,
942 href,
943 runs,
944 } => {
945 self.layout_text(
946 content,
947 href.as_deref(),
948 runs,
949 &style,
950 cursor,
951 pages,
952 x,
953 available_width,
954 font_context,
955 node.source_location.as_ref(),
956 node.bookmark.as_deref(),
957 );
958 }
959
960 NodeKind::Image { width, height, .. } => {
961 self.layout_image(
962 node,
963 &style,
964 cursor,
965 pages,
966 x,
967 available_width,
968 *width,
969 *height,
970 );
971 }
972
973 NodeKind::Table { columns } => {
974 self.layout_table(
975 node,
976 &style,
977 columns,
978 cursor,
979 pages,
980 x,
981 available_width,
982 font_context,
983 );
984 }
985
986 NodeKind::View | NodeKind::Page { .. } => {
987 self.layout_view(
988 node,
989 &style,
990 cursor,
991 pages,
992 x,
993 available_width,
994 font_context,
995 );
996 }
997
998 NodeKind::TableRow { .. } | NodeKind::TableCell { .. } => {
999 self.layout_view(
1000 node,
1001 &style,
1002 cursor,
1003 pages,
1004 x,
1005 available_width,
1006 font_context,
1007 );
1008 }
1009
1010 NodeKind::Svg {
1011 width: svg_w,
1012 height: svg_h,
1013 view_box,
1014 content,
1015 } => {
1016 self.layout_svg(
1017 node,
1018 &style,
1019 cursor,
1020 pages,
1021 x,
1022 available_width,
1023 *svg_w,
1024 *svg_h,
1025 view_box.as_deref(),
1026 content,
1027 );
1028 }
1029
1030 NodeKind::Barcode {
1031 data,
1032 format,
1033 width: explicit_width,
1034 height: bar_height,
1035 } => {
1036 self.layout_barcode(
1037 node,
1038 &style,
1039 cursor,
1040 pages,
1041 x,
1042 available_width,
1043 data,
1044 *format,
1045 *explicit_width,
1046 *bar_height,
1047 );
1048 }
1049
1050 NodeKind::QrCode {
1051 data,
1052 size: explicit_size,
1053 } => {
1054 self.layout_qrcode(
1055 node,
1056 &style,
1057 cursor,
1058 pages,
1059 x,
1060 available_width,
1061 data,
1062 *explicit_size,
1063 );
1064 }
1065
1066 NodeKind::Canvas {
1067 width: canvas_w,
1068 height: canvas_h,
1069 operations,
1070 } => {
1071 self.layout_canvas(
1072 node,
1073 &style,
1074 cursor,
1075 pages,
1076 x,
1077 available_width,
1078 *canvas_w,
1079 *canvas_h,
1080 operations,
1081 );
1082 }
1083
1084 NodeKind::BarChart {
1085 data,
1086 width: chart_w,
1087 height: chart_h,
1088 color,
1089 show_labels,
1090 show_values,
1091 show_grid,
1092 title,
1093 } => {
1094 let config = crate::chart::bar::BarChartConfig {
1095 color: color.clone(),
1096 show_labels: *show_labels,
1097 show_values: *show_values,
1098 show_grid: *show_grid,
1099 title: title.clone(),
1100 };
1101 let primitives = crate::chart::bar::build(*chart_w, *chart_h, data, &config);
1102 self.layout_chart(
1103 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "BarChart",
1104 );
1105 }
1106
1107 NodeKind::LineChart {
1108 series,
1109 labels,
1110 width: chart_w,
1111 height: chart_h,
1112 show_points,
1113 show_grid,
1114 title,
1115 } => {
1116 let config = crate::chart::line::LineChartConfig {
1117 show_points: *show_points,
1118 show_grid: *show_grid,
1119 title: title.clone(),
1120 };
1121 let primitives =
1122 crate::chart::line::build(*chart_w, *chart_h, series, labels, &config);
1123 self.layout_chart(
1124 node,
1125 &style,
1126 cursor,
1127 pages,
1128 x,
1129 *chart_w,
1130 *chart_h,
1131 primitives,
1132 "LineChart",
1133 );
1134 }
1135
1136 NodeKind::PieChart {
1137 data,
1138 width: chart_w,
1139 height: chart_h,
1140 donut,
1141 show_legend,
1142 title,
1143 } => {
1144 let config = crate::chart::pie::PieChartConfig {
1145 donut: *donut,
1146 show_legend: *show_legend,
1147 title: title.clone(),
1148 };
1149 let primitives = crate::chart::pie::build(*chart_w, *chart_h, data, &config);
1150 self.layout_chart(
1151 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "PieChart",
1152 );
1153 }
1154
1155 NodeKind::AreaChart {
1156 series,
1157 labels,
1158 width: chart_w,
1159 height: chart_h,
1160 show_grid,
1161 title,
1162 } => {
1163 let config = crate::chart::area::AreaChartConfig {
1164 show_grid: *show_grid,
1165 title: title.clone(),
1166 };
1167 let primitives =
1168 crate::chart::area::build(*chart_w, *chart_h, series, labels, &config);
1169 self.layout_chart(
1170 node,
1171 &style,
1172 cursor,
1173 pages,
1174 x,
1175 *chart_w,
1176 *chart_h,
1177 primitives,
1178 "AreaChart",
1179 );
1180 }
1181
1182 NodeKind::DotPlot {
1183 groups,
1184 width: chart_w,
1185 height: chart_h,
1186 x_min,
1187 x_max,
1188 y_min,
1189 y_max,
1190 x_label,
1191 y_label,
1192 show_legend,
1193 dot_size,
1194 } => {
1195 let config = crate::chart::dot::DotPlotConfig {
1196 x_min: *x_min,
1197 x_max: *x_max,
1198 y_min: *y_min,
1199 y_max: *y_max,
1200 x_label: x_label.clone(),
1201 y_label: y_label.clone(),
1202 show_legend: *show_legend,
1203 dot_size: *dot_size,
1204 };
1205 let primitives = crate::chart::dot::build(*chart_w, *chart_h, groups, &config);
1206 self.layout_chart(
1207 node, &style, cursor, pages, x, *chart_w, *chart_h, primitives, "DotPlot",
1208 );
1209 }
1210 }
1211 }
1212
1213 #[allow(clippy::too_many_arguments)]
1214 fn layout_view(
1215 &self,
1216 node: &Node,
1217 style: &ResolvedStyle,
1218 cursor: &mut PageCursor,
1219 pages: &mut Vec<LayoutPage>,
1220 x: f64,
1221 available_width: f64,
1222 font_context: &FontContext,
1223 ) {
1224 let padding = &style.padding;
1225 let margin = &style.margin.to_edges();
1226 let border = &style.border_width;
1227
1228 let outer_width = match style.width {
1229 SizeConstraint::Fixed(w) => w,
1230 SizeConstraint::Auto => available_width - margin.horizontal(),
1231 };
1232 let inner_width = outer_width - padding.horizontal() - border.horizontal();
1233
1234 let children_height =
1235 self.measure_children_height(&node.children, inner_width, style, font_context);
1236 let total_height = match style.height {
1237 SizeConstraint::Fixed(h) => h,
1238 SizeConstraint::Auto => children_height + padding.vertical() + border.vertical(),
1239 };
1240
1241 let node_x = x + margin.left;
1242
1243 let fits = total_height <= cursor.remaining_height() - margin.vertical();
1244
1245 if fits || !style.breakable {
1246 if !fits && !style.breakable {
1247 pages.push(cursor.finalize());
1248 *cursor = cursor.new_page();
1249 }
1250
1251 let rect_y = cursor.content_y + cursor.y + margin.top;
1253 let snapshot = cursor.elements.len();
1254
1255 let saved_y = cursor.y;
1256 cursor.y += margin.top + padding.top + border.top;
1257
1258 let children_x = node_x + padding.left + border.left;
1259 let is_grid =
1260 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1261 if is_grid {
1262 self.layout_grid_children(
1263 &node.children,
1264 style,
1265 cursor,
1266 pages,
1267 children_x,
1268 inner_width,
1269 font_context,
1270 );
1271 } else {
1272 self.layout_children(
1273 &node.children,
1274 &node.style,
1275 cursor,
1276 pages,
1277 children_x,
1278 inner_width,
1279 Some(style),
1280 font_context,
1281 );
1282 }
1283
1284 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1286
1287 let rect_element = LayoutElement {
1288 x: node_x,
1289 y: rect_y,
1290 width: outer_width,
1291 height: total_height,
1292 draw: DrawCommand::Rect {
1293 background: style.background_color,
1294 border_width: style.border_width,
1295 border_color: style.border_color,
1296 border_radius: style.border_radius,
1297 opacity: style.opacity,
1298 },
1299 children: child_elements,
1300 node_type: Some(node_kind_name(&node.kind).to_string()),
1301 resolved_style: Some(style.clone()),
1302 source_location: node.source_location.clone(),
1303 href: node.href.clone(),
1304 bookmark: node.bookmark.clone(),
1305 alt: None,
1306 is_header_row: false,
1307 overflow: style.overflow,
1308 };
1309 cursor.elements.push(rect_element);
1310
1311 cursor.y = saved_y + total_height + margin.vertical();
1312 } else {
1313 self.layout_breakable_view(
1314 node,
1315 style,
1316 cursor,
1317 pages,
1318 node_x,
1319 outer_width,
1320 inner_width,
1321 font_context,
1322 );
1323 }
1324 }
1325
1326 #[allow(clippy::too_many_arguments)]
1327 fn layout_breakable_view(
1328 &self,
1329 node: &Node,
1330 style: &ResolvedStyle,
1331 cursor: &mut PageCursor,
1332 pages: &mut Vec<LayoutPage>,
1333 node_x: f64,
1334 outer_width: f64,
1335 inner_width: f64,
1336 font_context: &FontContext,
1337 ) {
1338 let padding = &style.padding;
1339 let border = &style.border_width;
1340 let margin = &style.margin.to_edges();
1341
1342 let initial_page_count = pages.len();
1344 let snapshot = cursor.elements.len();
1345 let rect_start_y = cursor.content_y + cursor.y + margin.top;
1346
1347 cursor.y += margin.top + padding.top + border.top;
1348 let prev_continuation_offset = cursor.continuation_top_offset;
1349 cursor.continuation_top_offset = padding.top + border.top;
1350
1351 if node.bookmark.is_some() {
1353 cursor.elements.push(LayoutElement {
1354 x: node_x,
1355 y: cursor.content_y + cursor.y,
1356 width: 0.0,
1357 height: 0.0,
1358 draw: DrawCommand::None,
1359 children: vec![],
1360 node_type: None,
1361 resolved_style: None,
1362 source_location: None,
1363 href: None,
1364 bookmark: node.bookmark.clone(),
1365 alt: None,
1366 is_header_row: false,
1367 overflow: Overflow::default(),
1368 });
1369 }
1370
1371 let children_x = node_x + padding.left + border.left;
1372 let is_grid =
1373 matches!(style.display, Display::Grid) && style.grid_template_columns.is_some();
1374 if is_grid {
1375 self.layout_grid_children(
1376 &node.children,
1377 style,
1378 cursor,
1379 pages,
1380 children_x,
1381 inner_width,
1382 font_context,
1383 );
1384 } else {
1385 self.layout_children(
1386 &node.children,
1387 &node.style,
1388 cursor,
1389 pages,
1390 children_x,
1391 inner_width,
1392 Some(style),
1393 font_context,
1394 );
1395 }
1396
1397 cursor.continuation_top_offset = prev_continuation_offset;
1398
1399 let has_visual = style.background_color.is_some()
1401 || style.border_width.top > 0.0
1402 || style.border_width.right > 0.0
1403 || style.border_width.bottom > 0.0
1404 || style.border_width.left > 0.0;
1405 let needs_wrapper = has_visual || style.flex_grow > 0.0;
1407
1408 if !needs_wrapper {
1409 cursor.y += padding.bottom + border.bottom + margin.bottom;
1411 return;
1412 }
1413
1414 let draw_cmd = DrawCommand::Rect {
1415 background: style.background_color,
1416 border_width: style.border_width,
1417 border_color: style.border_color,
1418 border_radius: style.border_radius,
1419 opacity: style.opacity,
1420 };
1421
1422 if pages.len() == initial_page_count {
1423 let child_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
1425 let rect_height =
1426 cursor.content_y + cursor.y + padding.bottom + border.bottom - rect_start_y;
1427 cursor.elements.push(LayoutElement {
1428 x: node_x,
1429 y: rect_start_y,
1430 width: outer_width,
1431 height: rect_height,
1432 draw: draw_cmd,
1433 children: child_elements,
1434 node_type: Some(node_kind_name(&node.kind).to_string()),
1435 resolved_style: Some(style.clone()),
1436 source_location: node.source_location.clone(),
1437 href: node.href.clone(),
1438 bookmark: node.bookmark.clone(),
1439 alt: None,
1440 is_header_row: false,
1441 overflow: style.overflow,
1442 });
1443 } else {
1444 let page = &mut pages[initial_page_count];
1448 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1449 let page_content_bottom =
1450 page.config.margin.top + (page.height - page.config.margin.vertical()) - footer_h;
1451 let our_elements: Vec<LayoutElement> = page.elements.drain(snapshot..).collect();
1452 if !our_elements.is_empty() {
1453 let rect_height = page_content_bottom - rect_start_y;
1454 page.elements.push(LayoutElement {
1455 x: node_x,
1456 y: rect_start_y,
1457 width: outer_width,
1458 height: rect_height,
1459 draw: draw_cmd.clone(),
1460 children: our_elements,
1461 node_type: Some(node_kind_name(&node.kind).to_string()),
1462 resolved_style: Some(style.clone()),
1463 source_location: node.source_location.clone(),
1464 href: node.href.clone(),
1465 bookmark: node.bookmark.clone(),
1466 alt: None,
1467 is_header_row: false,
1468 overflow: Overflow::default(),
1469 });
1470 }
1471
1472 for page in &mut pages[initial_page_count + 1..] {
1474 let header_h: f64 = page.fixed_header.iter().map(|(_, h)| *h).sum();
1475 let content_top = page.config.margin.top + header_h;
1476 let footer_h: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
1477 let content_bottom = page.config.margin.top
1478 + (page.height - page.config.margin.vertical())
1479 - footer_h;
1480 let all_elements: Vec<LayoutElement> = page.elements.drain(..).collect();
1481 if !all_elements.is_empty() {
1482 page.elements.push(LayoutElement {
1483 x: node_x,
1484 y: content_top,
1485 width: outer_width,
1486 height: content_bottom - content_top,
1487 draw: draw_cmd.clone(),
1488 children: all_elements,
1489 node_type: Some(node_kind_name(&node.kind).to_string()),
1490 resolved_style: Some(style.clone()),
1491 source_location: node.source_location.clone(),
1492 href: None,
1493 bookmark: None,
1494 alt: None,
1495 is_header_row: false,
1496 overflow: Overflow::default(),
1497 });
1498 }
1499 }
1500
1501 let all_elements: Vec<LayoutElement> = cursor.elements.drain(..).collect();
1503 if !all_elements.is_empty() {
1504 let header_h: f64 = cursor.fixed_header.iter().map(|(_, h)| *h).sum();
1505 let content_top = cursor.content_y + header_h;
1506 let rect_height =
1507 cursor.content_y + cursor.y + padding.bottom + border.bottom - content_top;
1508 cursor.elements.push(LayoutElement {
1509 x: node_x,
1510 y: content_top,
1511 width: outer_width,
1512 height: rect_height,
1513 draw: draw_cmd,
1514 children: all_elements,
1515 node_type: Some(node_kind_name(&node.kind).to_string()),
1516 resolved_style: Some(style.clone()),
1517 source_location: node.source_location.clone(),
1518 href: None,
1519 bookmark: None,
1520 alt: None,
1521 is_header_row: false,
1522 overflow: Overflow::default(),
1523 });
1524 }
1525 }
1526
1527 cursor.y += padding.bottom + border.bottom + margin.bottom;
1528 }
1529
1530 #[allow(clippy::too_many_arguments)]
1531 fn layout_children(
1532 &self,
1533 children: &[Node],
1534 _parent_raw_style: &Style,
1535 cursor: &mut PageCursor,
1536 pages: &mut Vec<LayoutPage>,
1537 content_x: f64,
1538 available_width: f64,
1539 parent_style: Option<&ResolvedStyle>,
1540 font_context: &FontContext,
1541 ) {
1542 let parent_box_y = cursor.content_y + cursor.y;
1544 let parent_box_x = content_x;
1545
1546 let (flow_children, abs_children): (Vec<&Node>, Vec<&Node>) = children
1548 .iter()
1549 .partition(|child| !matches!(child.style.position, Some(Position::Absolute)));
1550
1551 let direction = parent_style
1552 .map(|s| s.flex_direction)
1553 .unwrap_or(FlexDirection::Column);
1554
1555 let row_gap = parent_style.map(|s| s.row_gap).unwrap_or(0.0);
1556 let column_gap = parent_style.map(|s| s.column_gap).unwrap_or(0.0);
1557
1558 match direction {
1560 FlexDirection::Column | FlexDirection::ColumnReverse => {
1561 let items: Vec<&Node> = if matches!(direction, FlexDirection::ColumnReverse) {
1562 flow_children.into_iter().rev().collect()
1563 } else {
1564 flow_children
1565 };
1566
1567 let justify = parent_style
1568 .map(|s| s.justify_content)
1569 .unwrap_or(JustifyContent::FlexStart);
1570 let align = parent_style
1571 .map(|s| s.align_items)
1572 .unwrap_or(AlignItems::Stretch);
1573
1574 let start_y = cursor.y;
1575 let initial_pages = pages.len();
1576
1577 let mut child_ranges: Vec<(usize, usize)> = Vec::new();
1579
1580 for (i, child) in items.iter().enumerate() {
1581 if i > 0 {
1582 cursor.y += row_gap;
1583 }
1584 let child_start = cursor.elements.len();
1585
1586 let child_margin = &child.style.resolve(parent_style, available_width).margin;
1589 let has_auto_h = child_margin.has_auto_horizontal();
1590
1591 let (child_x, layout_w) = if has_auto_h {
1598 let child_style = child.style.resolve(parent_style, available_width);
1599 let has_explicit_width =
1600 matches!(child_style.width, SizeConstraint::Fixed(_));
1601 let intrinsic = self
1602 .measure_intrinsic_width(child, &child_style, font_context)
1603 .min(available_width);
1604 let w = match child_style.width {
1605 SizeConstraint::Fixed(fw) => fw,
1606 SizeConstraint::Auto => intrinsic,
1607 };
1608 let lw = if has_explicit_width {
1609 available_width
1610 } else {
1611 w
1612 };
1613 let fixed_h = child_margin.horizontal();
1614 let slack = (available_width - w - fixed_h).max(0.0);
1615 let auto_left = child_margin.left.is_auto();
1616 let auto_right = child_margin.right.is_auto();
1617 let ml = match (auto_left, auto_right) {
1618 (true, true) => slack / 2.0,
1619 (true, false) => slack,
1620 (false, true) => 0.0,
1621 (false, false) => 0.0,
1622 };
1623 (content_x + child_margin.left.resolve() + ml, lw)
1624 } else if !matches!(align, AlignItems::Stretch | AlignItems::FlexStart) {
1625 let child_style = child.style.resolve(parent_style, available_width);
1626 let has_explicit_width =
1627 matches!(child_style.width, SizeConstraint::Fixed(_));
1628 let intrinsic = self
1629 .measure_intrinsic_width(child, &child_style, font_context)
1630 .min(available_width);
1631 let w = match child_style.width {
1632 SizeConstraint::Fixed(fw) => fw,
1633 SizeConstraint::Auto => intrinsic,
1634 };
1635 let lw = if has_explicit_width {
1636 available_width
1637 } else {
1638 w
1639 };
1640 match align {
1641 AlignItems::Center => (content_x + (available_width - w) / 2.0, lw),
1642 AlignItems::FlexEnd => (content_x + available_width - w, lw),
1643 _ => (content_x, available_width),
1644 }
1645 } else {
1646 (content_x, available_width)
1647 };
1648
1649 self.layout_node(
1650 child,
1651 cursor,
1652 pages,
1653 child_x,
1654 layout_w,
1655 parent_style,
1656 font_context,
1657 None,
1658 );
1659
1660 child_ranges.push((child_start, cursor.elements.len()));
1661 }
1662
1663 let container_inner_h: Option<f64> = parent_style
1666 .and_then(|ps| match ps.height {
1667 SizeConstraint::Fixed(h) => {
1668 Some(h - ps.padding.vertical() - ps.border_width.vertical())
1669 }
1670 SizeConstraint::Auto => None,
1671 })
1672 .or_else(|| {
1673 if parent_style.is_none() {
1675 Some(cursor.content_height - start_y)
1676 } else {
1677 None
1678 }
1679 });
1680
1681 if let Some(inner_h) = container_inner_h {
1682 if pages.len() == initial_pages {
1683 let child_styles: Vec<ResolvedStyle> = items
1684 .iter()
1685 .map(|child| child.style.resolve(parent_style, available_width))
1686 .collect();
1687 let total_grow: f64 = child_styles.iter().map(|s| s.flex_grow).sum();
1688 if total_grow > 0.0 {
1689 let children_total = cursor.y - start_y;
1690 let slack = (inner_h - children_total).max(0.0);
1691 if slack > 0.0 {
1692 let mut cumulative_shift = 0.0_f64;
1693 for (i, cs) in child_styles.iter().enumerate() {
1694 let (start, end) = child_ranges[i];
1695 if cumulative_shift > 0.001 {
1696 for j in start..end {
1697 offset_element_y(
1698 &mut cursor.elements[j],
1699 cumulative_shift,
1700 );
1701 }
1702 }
1703 if cs.flex_grow > 0.0 {
1704 let extra = slack * (cs.flex_grow / total_grow);
1705 if start < end {
1707 let elem = &mut cursor.elements[end - 1];
1708 elem.height += extra;
1709 reapply_justify_content(elem);
1710 }
1711 cumulative_shift += extra;
1712 }
1713 }
1714 cursor.y += cumulative_shift;
1715 }
1716 }
1717 }
1718 }
1719
1720 let needs_justify =
1722 !matches!(justify, JustifyContent::FlexStart) && pages.len() == initial_pages;
1723 if needs_justify {
1724 let justify_inner_h = container_inner_h.or_else(|| {
1726 parent_style.and_then(|ps| match ps.height {
1727 SizeConstraint::Fixed(h) => {
1728 Some(h - ps.padding.vertical() - ps.border_width.vertical())
1729 }
1730 SizeConstraint::Auto => None,
1731 })
1732 });
1733 if let Some(inner_h) = justify_inner_h {
1734 let children_total = cursor.y - start_y;
1735 let slack = inner_h - children_total;
1736 if slack > 0.0 {
1737 let n = child_ranges.len();
1738 let offsets: Vec<f64> = match justify {
1739 JustifyContent::FlexEnd => vec![slack; n],
1740 JustifyContent::Center => vec![slack / 2.0; n],
1741 JustifyContent::SpaceBetween => {
1742 if n <= 1 {
1743 vec![0.0; n]
1744 } else {
1745 let per_gap = slack / (n - 1) as f64;
1746 (0..n).map(|i| i as f64 * per_gap).collect()
1747 }
1748 }
1749 JustifyContent::SpaceAround => {
1750 let space = slack / n as f64;
1751 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
1752 }
1753 JustifyContent::SpaceEvenly => {
1754 let space = slack / (n + 1) as f64;
1755 (0..n).map(|i| (i + 1) as f64 * space).collect()
1756 }
1757 JustifyContent::FlexStart => vec![0.0; n],
1758 };
1759 for (i, &(start, end)) in child_ranges.iter().enumerate() {
1760 let dy = offsets[i];
1761 if dy.abs() > 0.001 {
1762 for j in start..end {
1763 offset_element_y(&mut cursor.elements[j], dy);
1764 }
1765 }
1766 }
1767 cursor.y += *offsets.last().unwrap_or(&0.0);
1768 }
1769 }
1770 }
1771 }
1772
1773 FlexDirection::Row | FlexDirection::RowReverse => {
1774 let flow_owned: Vec<Node> = flow_children.into_iter().cloned().collect();
1775 self.layout_flex_row(
1776 &flow_owned,
1777 cursor,
1778 pages,
1779 content_x,
1780 available_width,
1781 parent_style,
1782 column_gap,
1783 row_gap,
1784 font_context,
1785 );
1786 }
1787 }
1788
1789 for abs_child in &abs_children {
1791 let abs_style = abs_child.style.resolve(parent_style, available_width);
1792
1793 let child_width = match abs_style.width {
1795 SizeConstraint::Fixed(w) => w,
1796 SizeConstraint::Auto => {
1797 if let (Some(l), Some(r)) = (abs_style.left, abs_style.right) {
1799 (available_width - l - r).max(0.0)
1800 } else {
1801 self.measure_intrinsic_width(abs_child, &abs_style, font_context)
1802 }
1803 }
1804 };
1805
1806 let child_height = match abs_style.height {
1807 SizeConstraint::Fixed(h) => h,
1808 SizeConstraint::Auto => {
1809 self.measure_node_height(abs_child, child_width, &abs_style, font_context)
1810 }
1811 };
1812
1813 let abs_x = if let Some(l) = abs_style.left {
1815 parent_box_x + l
1816 } else if let Some(r) = abs_style.right {
1817 parent_box_x + available_width - r - child_width
1818 } else {
1819 parent_box_x
1820 };
1821
1822 let parent_inner_height = parent_style
1824 .and_then(|ps| match ps.height {
1825 SizeConstraint::Fixed(h) => {
1826 Some(h - ps.padding.vertical() - ps.border_width.vertical())
1827 }
1828 SizeConstraint::Auto => None,
1829 })
1830 .unwrap_or(cursor.content_y + cursor.y - parent_box_y);
1831
1832 let abs_y = if let Some(t) = abs_style.top {
1833 parent_box_y + t
1834 } else if let Some(b) = abs_style.bottom {
1835 parent_box_y + parent_inner_height - b - child_height
1836 } else {
1837 parent_box_y
1838 };
1839
1840 let mut abs_cursor = PageCursor::new(&cursor.config);
1842 abs_cursor.y = 0.0;
1843 abs_cursor.content_x = abs_x;
1844 abs_cursor.content_y = abs_y;
1845
1846 self.layout_node(
1847 abs_child,
1848 &mut abs_cursor,
1849 &mut Vec::new(),
1850 abs_x,
1851 child_width,
1852 parent_style,
1853 font_context,
1854 None,
1855 );
1856
1857 cursor.elements.extend(abs_cursor.elements);
1859 }
1860 }
1861
1862 #[allow(clippy::too_many_arguments)]
1863 fn layout_flex_row(
1864 &self,
1865 children: &[Node],
1866 cursor: &mut PageCursor,
1867 pages: &mut Vec<LayoutPage>,
1868 content_x: f64,
1869 available_width: f64,
1870 parent_style: Option<&ResolvedStyle>,
1871 column_gap: f64,
1872 row_gap: f64,
1873 font_context: &FontContext,
1874 ) {
1875 if children.is_empty() {
1876 return;
1877 }
1878
1879 let flex_wrap = parent_style
1880 .map(|s| s.flex_wrap)
1881 .unwrap_or(FlexWrap::NoWrap);
1882
1883 let items: Vec<FlexItem> = children
1886 .iter()
1887 .map(|child| {
1888 let style = child.style.resolve(parent_style, available_width);
1889 let base_width = match style.flex_basis {
1890 SizeConstraint::Fixed(w) => w,
1891 SizeConstraint::Auto => match style.width {
1892 SizeConstraint::Fixed(w) => w,
1893 SizeConstraint::Auto => {
1894 self.measure_intrinsic_width(child, &style, font_context)
1895 }
1896 },
1897 };
1898 let min_content_width = self.measure_min_content_width(child, &style, font_context);
1899 FlexItem {
1900 node: child,
1901 style,
1902 base_width,
1903 min_content_width,
1904 }
1905 })
1906 .collect();
1907
1908 let base_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
1910 let lines = match flex_wrap {
1911 FlexWrap::NoWrap => {
1912 vec![flex::WrapLine {
1913 start: 0,
1914 end: items.len(),
1915 }]
1916 }
1917 FlexWrap::Wrap => flex::partition_into_lines(&base_widths, column_gap, available_width),
1918 FlexWrap::WrapReverse => {
1919 let mut l = flex::partition_into_lines(&base_widths, column_gap, available_width);
1920 l.reverse();
1921 l
1922 }
1923 };
1924
1925 if lines.is_empty() {
1926 return;
1927 }
1928
1929 let justify = parent_style.map(|s| s.justify_content).unwrap_or_default();
1931
1932 let mut final_widths: Vec<f64> = items.iter().map(|i| i.base_width).collect();
1934
1935 let initial_pages_count = pages.len();
1936 let flex_start_y = cursor.y;
1937 let mut line_infos: Vec<(usize, usize, f64)> = Vec::new();
1938
1939 for (line_idx, line) in lines.iter().enumerate() {
1940 let line_items = &items[line.start..line.end];
1941 let line_count = line.end - line.start;
1942 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
1943 let distributable = available_width - line_gap;
1944
1945 let total_base: f64 = line_items.iter().map(|i| i.base_width).sum();
1947 let remaining = distributable - total_base;
1948
1949 if remaining > 0.0 {
1950 let total_grow: f64 = line_items.iter().map(|i| i.style.flex_grow).sum();
1951 if total_grow > 0.0 {
1952 for (j, item) in line_items.iter().enumerate() {
1953 final_widths[line.start + j] =
1954 item.base_width + remaining * (item.style.flex_grow / total_grow);
1955 }
1956 }
1957 } else if remaining < 0.0 {
1958 let total_shrink: f64 = line_items
1959 .iter()
1960 .map(|i| i.style.flex_shrink * i.base_width)
1961 .sum();
1962 if total_shrink > 0.0 {
1963 for (j, item) in line_items.iter().enumerate() {
1964 let factor = (item.style.flex_shrink * item.base_width) / total_shrink;
1965 let w = item.base_width + remaining * factor;
1966 let floor = item.style.min_width.max(item.min_content_width);
1967 final_widths[line.start + j] = w.max(floor);
1968 }
1969 }
1970 }
1971
1972 let line_height: f64 = line_items
1974 .iter()
1975 .enumerate()
1976 .map(|(j, item)| {
1977 let fw = final_widths[line.start + j];
1978 self.measure_node_height(item.node, fw, &item.style, font_context)
1979 + item.style.margin.vertical()
1980 })
1981 .fold(0.0f64, f64::max);
1982
1983 if line_height > cursor.remaining_height() {
1985 pages.push(cursor.finalize());
1986 *cursor = cursor.new_page();
1987 }
1988
1989 if line_idx > 0 {
1991 cursor.y += row_gap;
1992 }
1993
1994 let row_start_y = cursor.y;
1995
1996 let actual_total: f64 = (line.start..line.end).map(|i| final_widths[i]).sum();
1998 let slack = available_width - actual_total - line_gap;
1999
2000 let (start_offset, between_extra) = match justify {
2001 JustifyContent::FlexStart => (0.0, 0.0),
2002 JustifyContent::FlexEnd => (slack, 0.0),
2003 JustifyContent::Center => (slack / 2.0, 0.0),
2004 JustifyContent::SpaceBetween => {
2005 if line_count > 1 {
2006 (0.0, slack / (line_count as f64 - 1.0))
2007 } else {
2008 (0.0, 0.0)
2009 }
2010 }
2011 JustifyContent::SpaceAround => {
2012 let s = slack / line_count as f64;
2013 (s / 2.0, s)
2014 }
2015 JustifyContent::SpaceEvenly => {
2016 let s = slack / (line_count as f64 + 1.0);
2017 (s, s)
2018 }
2019 };
2020
2021 let line_elem_start = cursor.elements.len();
2022 let mut x = content_x + start_offset;
2023
2024 for (j, item) in line_items.iter().enumerate() {
2025 if j > 0 {
2026 x += column_gap + between_extra;
2027 }
2028
2029 let fw = final_widths[line.start + j];
2030
2031 let align = item
2032 .style
2033 .align_self
2034 .unwrap_or(parent_style.map(|s| s.align_items).unwrap_or_default());
2035
2036 let item_height =
2037 self.measure_node_height(item.node, fw, &item.style, font_context);
2038
2039 let has_auto_v = item.style.margin.has_auto_vertical();
2041 let y_offset = if has_auto_v {
2042 let fixed_v = item.style.margin.vertical();
2043 let slack = (line_height - item_height - fixed_v).max(0.0);
2044 let auto_top = item.style.margin.top.is_auto();
2045 let auto_bottom = item.style.margin.bottom.is_auto();
2046 match (auto_top, auto_bottom) {
2047 (true, true) => slack / 2.0,
2048 (true, false) => slack,
2049 (false, true) => 0.0,
2050 (false, false) => 0.0,
2051 }
2052 } else {
2053 match align {
2054 AlignItems::FlexStart => 0.0,
2055 AlignItems::FlexEnd => {
2056 line_height - item_height - item.style.margin.vertical()
2057 }
2058 AlignItems::Center => {
2059 (line_height - item_height - item.style.margin.vertical()) / 2.0
2060 }
2061 AlignItems::Stretch => 0.0,
2062 AlignItems::Baseline => 0.0,
2063 }
2064 };
2065
2066 let cross_h = if matches!(align, AlignItems::Stretch)
2070 && matches!(item.style.height, SizeConstraint::Auto)
2071 && !has_auto_v
2072 {
2073 let stretch_h = line_height - item.style.margin.vertical();
2074 if stretch_h > item_height {
2075 Some(stretch_h)
2076 } else {
2077 None
2078 }
2079 } else {
2080 None
2081 };
2082
2083 let saved_y = cursor.y;
2084 cursor.y = row_start_y + y_offset;
2085
2086 self.layout_node(
2087 item.node,
2088 cursor,
2089 pages,
2090 x,
2091 fw,
2092 parent_style,
2093 font_context,
2094 cross_h,
2095 );
2096
2097 cursor.y = saved_y;
2098 x += fw;
2099 }
2100
2101 cursor.y = row_start_y + line_height;
2102 line_infos.push((line_elem_start, cursor.elements.len(), line_height));
2103 }
2104
2105 if pages.len() == initial_pages_count && !line_infos.is_empty() {
2107 let align_content = parent_style.map(|s| s.align_content).unwrap_or_default();
2108 if !matches!(align_content, AlignContent::FlexStart)
2109 && !matches!(flex_wrap, FlexWrap::NoWrap)
2110 {
2111 if let Some(parent) = parent_style {
2112 if let SizeConstraint::Fixed(container_h) = parent.height {
2113 let inner_h = container_h
2114 - parent.padding.vertical()
2115 - parent.border_width.vertical();
2116 let total_used = cursor.y - flex_start_y;
2117 let slack = inner_h - total_used;
2118 if slack > 0.0 {
2119 let n = line_infos.len();
2120 let offsets: Vec<f64> = match align_content {
2121 AlignContent::FlexEnd => vec![slack; n],
2122 AlignContent::Center => vec![slack / 2.0; n],
2123 AlignContent::SpaceBetween => {
2124 if n <= 1 {
2125 vec![0.0; n]
2126 } else {
2127 let per_gap = slack / (n - 1) as f64;
2128 (0..n).map(|i| i as f64 * per_gap).collect()
2129 }
2130 }
2131 AlignContent::SpaceAround => {
2132 let space = slack / n as f64;
2133 (0..n).map(|i| space / 2.0 + i as f64 * space).collect()
2134 }
2135 AlignContent::SpaceEvenly => {
2136 let space = slack / (n + 1) as f64;
2137 (0..n).map(|i| (i + 1) as f64 * space).collect()
2138 }
2139 AlignContent::Stretch => {
2140 let extra = slack / n as f64;
2141 (0..n).map(|i| i as f64 * extra).collect()
2142 }
2143 AlignContent::FlexStart => vec![0.0; n],
2144 };
2145 for (i, &(start, end, _)) in line_infos.iter().enumerate() {
2146 let dy = offsets[i];
2147 if dy.abs() > 0.001 {
2148 for j in start..end {
2149 offset_element_y(&mut cursor.elements[j], dy);
2150 }
2151 }
2152 }
2153 cursor.y += *offsets.last().unwrap_or(&0.0);
2154 }
2155 }
2156 }
2157 }
2158 }
2159 }
2160
2161 #[allow(clippy::too_many_arguments)]
2162 fn layout_table(
2163 &self,
2164 node: &Node,
2165 style: &ResolvedStyle,
2166 column_defs: &[ColumnDef],
2167 cursor: &mut PageCursor,
2168 pages: &mut Vec<LayoutPage>,
2169 x: f64,
2170 available_width: f64,
2171 font_context: &FontContext,
2172 ) {
2173 let padding = &style.padding;
2174 let margin = &style.margin.to_edges();
2175 let border = &style.border_width;
2176
2177 let table_x = x + margin.left;
2178 let table_width = match style.width {
2179 SizeConstraint::Fixed(w) => w,
2180 SizeConstraint::Auto => available_width - margin.horizontal(),
2181 };
2182 let inner_width = table_width - padding.horizontal() - border.horizontal();
2183
2184 let col_widths = self.resolve_column_widths(column_defs, inner_width, &node.children);
2185
2186 let mut header_rows: Vec<&Node> = Vec::new();
2187 let mut body_rows: Vec<&Node> = Vec::new();
2188
2189 for child in &node.children {
2190 match &child.kind {
2191 NodeKind::TableRow { is_header: true } => header_rows.push(child),
2192 _ => body_rows.push(child),
2193 }
2194 }
2195
2196 cursor.y += margin.top + padding.top + border.top;
2197
2198 let cell_x_start = table_x + padding.left + border.left;
2199 for header_row in &header_rows {
2200 self.layout_table_row(
2201 header_row,
2202 &col_widths,
2203 style,
2204 cursor,
2205 cell_x_start,
2206 font_context,
2207 pages,
2208 );
2209 }
2210
2211 for body_row in &body_rows {
2212 let row_height =
2213 self.measure_table_row_height(body_row, &col_widths, style, font_context);
2214
2215 if row_height > cursor.remaining_height() {
2216 pages.push(cursor.finalize());
2217 *cursor = cursor.new_page();
2218
2219 cursor.y += padding.top + border.top;
2220 for header_row in &header_rows {
2221 self.layout_table_row(
2222 header_row,
2223 &col_widths,
2224 style,
2225 cursor,
2226 cell_x_start,
2227 font_context,
2228 pages,
2229 );
2230 }
2231 }
2232
2233 self.layout_table_row(
2234 body_row,
2235 &col_widths,
2236 style,
2237 cursor,
2238 cell_x_start,
2239 font_context,
2240 pages,
2241 );
2242 }
2243
2244 cursor.y += padding.bottom + border.bottom + margin.bottom;
2245 }
2246
2247 #[allow(clippy::too_many_arguments)]
2248 fn layout_table_row(
2249 &self,
2250 row: &Node,
2251 col_widths: &[f64],
2252 parent_style: &ResolvedStyle,
2253 cursor: &mut PageCursor,
2254 start_x: f64,
2255 font_context: &FontContext,
2256 pages: &mut Vec<LayoutPage>,
2257 ) {
2258 let row_style = row
2259 .style
2260 .resolve(Some(parent_style), col_widths.iter().sum());
2261
2262 let row_height = self.measure_table_row_height(row, col_widths, parent_style, font_context);
2263 let row_y = cursor.content_y + cursor.y;
2264 let total_width: f64 = col_widths.iter().sum();
2265
2266 let is_header = matches!(row.kind, NodeKind::TableRow { is_header: true });
2267
2268 let row_snapshot = cursor.elements.len();
2270
2271 let mut all_overflow_pages: Vec<LayoutPage> = Vec::new();
2272 let mut cell_x = start_x;
2273 for (i, cell) in row.children.iter().enumerate() {
2274 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
2275
2276 let cell_style = cell.style.resolve(Some(&row_style), col_width);
2277
2278 let cell_snapshot = cursor.elements.len();
2280
2281 let inner_width =
2282 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
2283
2284 let content_x = cell_x + cell_style.padding.left + cell_style.border_width.left;
2285 let saved_y = cursor.y;
2286 cursor.y += cell_style.padding.top + cell_style.border_width.top;
2287
2288 let cursor_before_cell = cursor.clone();
2290 let mut cell_pages: Vec<LayoutPage> = Vec::new();
2291 for child in &cell.children {
2292 self.layout_node(
2293 child,
2294 cursor,
2295 &mut cell_pages,
2296 content_x,
2297 inner_width,
2298 Some(&cell_style),
2299 font_context,
2300 None,
2301 );
2302 }
2303
2304 if !cell_pages.is_empty() {
2306 let post_break_elements = std::mem::take(&mut cursor.elements);
2307 if let Some(last_page) = cell_pages.last_mut() {
2308 last_page.elements.extend(post_break_elements);
2309 }
2310 all_overflow_pages.extend(cell_pages);
2311 *cursor = cursor_before_cell;
2312 }
2313
2314 cursor.y = saved_y;
2315
2316 let cell_children: Vec<LayoutElement> =
2318 cursor.elements.drain(cell_snapshot..).collect();
2319
2320 cursor.elements.push(LayoutElement {
2322 x: cell_x,
2323 y: row_y,
2324 width: col_width,
2325 height: row_height,
2326 draw: if cell_style.background_color.is_some()
2327 || cell_style.border_width.horizontal() > 0.0
2328 || cell_style.border_width.vertical() > 0.0
2329 {
2330 DrawCommand::Rect {
2331 background: cell_style.background_color,
2332 border_width: cell_style.border_width,
2333 border_color: cell_style.border_color,
2334 border_radius: cell_style.border_radius,
2335 opacity: cell_style.opacity,
2336 }
2337 } else {
2338 DrawCommand::None
2339 },
2340 children: cell_children,
2341 node_type: Some("TableCell".to_string()),
2342 resolved_style: Some(cell_style.clone()),
2343 source_location: cell.source_location.clone(),
2344 href: None,
2345 bookmark: cell.bookmark.clone(),
2346 alt: None,
2347 is_header_row: is_header,
2348 overflow: Overflow::default(),
2349 });
2350
2351 cell_x += col_width;
2352 }
2353
2354 let row_children: Vec<LayoutElement> = cursor.elements.drain(row_snapshot..).collect();
2356 cursor.elements.push(LayoutElement {
2357 x: start_x,
2358 y: row_y,
2359 width: total_width,
2360 height: row_height,
2361 draw: if let Some(bg) = row_style.background_color {
2362 DrawCommand::Rect {
2363 background: Some(bg),
2364 border_width: Edges::default(),
2365 border_color: EdgeValues::uniform(Color::BLACK),
2366 border_radius: CornerValues::uniform(0.0),
2367 opacity: row_style.opacity,
2368 }
2369 } else {
2370 DrawCommand::None
2371 },
2372 children: row_children,
2373 node_type: Some("TableRow".to_string()),
2374 resolved_style: Some(row_style.clone()),
2375 source_location: row.source_location.clone(),
2376 href: None,
2377 bookmark: row.bookmark.clone(),
2378 alt: None,
2379 is_header_row: is_header,
2380 overflow: row_style.overflow,
2381 });
2382
2383 pages.extend(all_overflow_pages);
2385
2386 cursor.y += row_height;
2387 }
2388
2389 #[allow(clippy::too_many_arguments)]
2390 fn layout_text(
2391 &self,
2392 content: &str,
2393 href: Option<&str>,
2394 runs: &[TextRun],
2395 style: &ResolvedStyle,
2396 cursor: &mut PageCursor,
2397 pages: &mut Vec<LayoutPage>,
2398 x: f64,
2399 available_width: f64,
2400 font_context: &FontContext,
2401 source_location: Option<&SourceLocation>,
2402 bookmark: Option<&str>,
2403 ) {
2404 let margin = &style.margin.to_edges();
2405 let text_x = x + margin.left;
2406 let text_width = available_width - margin.horizontal();
2407
2408 cursor.y += margin.top;
2409
2410 if !runs.is_empty() {
2412 self.layout_text_runs(
2413 runs,
2414 href,
2415 style,
2416 cursor,
2417 pages,
2418 text_x,
2419 text_width,
2420 font_context,
2421 source_location,
2422 bookmark,
2423 );
2424 cursor.y += margin.bottom;
2425 return;
2426 }
2427
2428 let transformed = apply_text_transform(content, style.text_transform);
2429 let justify = matches!(style.text_align, TextAlign::Justify);
2430 let lines = match style.line_breaking {
2431 LineBreaking::Optimal => self.text_layout.break_into_lines_optimal(
2432 font_context,
2433 &transformed,
2434 text_width,
2435 style.font_size,
2436 &style.font_family,
2437 style.font_weight,
2438 style.font_style,
2439 style.letter_spacing,
2440 style.hyphens,
2441 style.lang.as_deref(),
2442 justify,
2443 ),
2444 LineBreaking::Greedy => self.text_layout.break_into_lines(
2445 font_context,
2446 &transformed,
2447 text_width,
2448 style.font_size,
2449 &style.font_family,
2450 style.font_weight,
2451 style.font_style,
2452 style.letter_spacing,
2453 style.hyphens,
2454 style.lang.as_deref(),
2455 ),
2456 };
2457
2458 let lines = match style.text_overflow {
2460 TextOverflow::Ellipsis => self.text_layout.truncate_with_ellipsis(
2461 font_context,
2462 lines,
2463 text_width,
2464 style.font_size,
2465 &style.font_family,
2466 style.font_weight,
2467 style.font_style,
2468 style.letter_spacing,
2469 ),
2470 TextOverflow::Clip => self.text_layout.truncate_clip(
2471 font_context,
2472 lines,
2473 text_width,
2474 style.font_size,
2475 &style.font_family,
2476 style.font_weight,
2477 style.font_style,
2478 style.letter_spacing,
2479 ),
2480 TextOverflow::Wrap => lines,
2481 };
2482
2483 let line_height = style.font_size * style.line_height;
2484
2485 let line_heights: Vec<f64> = vec![line_height; lines.len()];
2487 let decision = page_break::decide_break(
2488 cursor.remaining_height(),
2489 &line_heights,
2490 true,
2491 style.min_orphan_lines as usize,
2492 style.min_widow_lines as usize,
2493 );
2494
2495 let mut snapshot = cursor.elements.len();
2497 let mut container_start_y = cursor.content_y + cursor.y;
2498 let mut is_first_element = true;
2499
2500 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
2502 pages.push(cursor.finalize());
2503 *cursor = cursor.new_page();
2504 snapshot = cursor.elements.len();
2505 container_start_y = cursor.content_y + cursor.y;
2506 }
2507
2508 let forced_break_at = match decision {
2510 page_break::BreakDecision::Split {
2511 items_on_current_page,
2512 } => Some(items_on_current_page),
2513 _ => None,
2514 };
2515 let mut first_break_done = false;
2516
2517 for (line_idx, line) in lines.iter().enumerate() {
2518 let needs_break = if let Some(break_at) = forced_break_at {
2520 if !first_break_done && line_idx == break_at {
2521 true
2522 } else {
2523 line_height > cursor.remaining_height()
2524 }
2525 } else {
2526 line_height > cursor.remaining_height()
2527 };
2528
2529 if needs_break {
2530 first_break_done = true;
2531 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2533 if !line_elements.is_empty() {
2534 let container_height = cursor.content_y + cursor.y - container_start_y;
2535 cursor.elements.push(LayoutElement {
2536 x: text_x,
2537 y: container_start_y,
2538 width: text_width,
2539 height: container_height,
2540 draw: DrawCommand::None,
2541 children: line_elements,
2542 node_type: Some("Text".to_string()),
2543 resolved_style: Some(style.clone()),
2544 source_location: source_location.cloned(),
2545 href: href.map(|s| s.to_string()),
2546 bookmark: if is_first_element {
2547 bookmark.map(|s| s.to_string())
2548 } else {
2549 None
2550 },
2551 alt: None,
2552 is_header_row: false,
2553 overflow: Overflow::default(),
2554 });
2555 is_first_element = false;
2556 }
2557
2558 pages.push(cursor.finalize());
2559 *cursor = cursor.new_page();
2560
2561 snapshot = cursor.elements.len();
2563 container_start_y = cursor.content_y + cursor.y;
2564 }
2565
2566 let glyphs = self.build_positioned_glyphs_single_style(line, style, href, font_context);
2567
2568 let rendered_width = if glyphs.is_empty() {
2572 line.width
2573 } else {
2574 let last = &glyphs[glyphs.len() - 1];
2575 (last.x_offset + last.x_advance).max(line.width * 0.5)
2576 };
2577
2578 let line_x = match style.text_align {
2579 TextAlign::Left => text_x,
2580 TextAlign::Right => text_x + text_width - rendered_width,
2581 TextAlign::Center => text_x + (text_width - rendered_width) / 2.0,
2582 TextAlign::Justify => text_x,
2583 };
2584
2585 let is_last_line = line_idx == lines.len() - 1;
2590 let (justified_width, word_spacing) =
2591 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
2592 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
2593 let (natural_width, space_count) = if let Some(idx) = last_non_space {
2594 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
2595 let s = glyphs[..=idx]
2596 .iter()
2597 .filter(|g| g.char_value == ' ')
2598 .count();
2599 (w, s)
2600 } else {
2601 (0.0, 0)
2602 };
2603 let slack = text_width - natural_width;
2604 let ws = if space_count > 0 && slack.abs() > 0.01 {
2605 slack / space_count as f64
2606 } else {
2607 0.0
2608 };
2609 (text_width, ws)
2610 } else {
2611 (rendered_width, 0.0)
2612 };
2613
2614 let text_line = TextLine {
2615 x: line_x,
2616 y: cursor.content_y + cursor.y + style.font_size,
2617 glyphs,
2618 width: justified_width,
2619 height: line_height,
2620 word_spacing,
2621 };
2622
2623 cursor.elements.push(LayoutElement {
2624 x: line_x,
2625 y: cursor.content_y + cursor.y,
2626 width: justified_width,
2627 height: line_height,
2628 draw: DrawCommand::Text {
2629 lines: vec![text_line],
2630 color: style.color,
2631 text_decoration: style.text_decoration,
2632 opacity: style.opacity,
2633 },
2634 children: vec![],
2635 node_type: Some("TextLine".to_string()),
2636 resolved_style: Some(style.clone()),
2637 source_location: None,
2638 href: href.map(|s| s.to_string()),
2639 bookmark: None,
2640 alt: None,
2641 is_header_row: false,
2642 overflow: Overflow::default(),
2643 });
2644
2645 cursor.y += line_height;
2646 }
2647
2648 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2650 if !line_elements.is_empty() {
2651 let container_height = cursor.content_y + cursor.y - container_start_y;
2652 cursor.elements.push(LayoutElement {
2653 x: text_x,
2654 y: container_start_y,
2655 width: text_width,
2656 height: container_height,
2657 draw: DrawCommand::None,
2658 children: line_elements,
2659 node_type: Some("Text".to_string()),
2660 resolved_style: Some(style.clone()),
2661 source_location: source_location.cloned(),
2662 href: href.map(|s| s.to_string()),
2663 bookmark: if is_first_element {
2664 bookmark.map(|s| s.to_string())
2665 } else {
2666 None
2667 },
2668 alt: None,
2669 is_header_row: false,
2670 overflow: Overflow::default(),
2671 });
2672 }
2673
2674 cursor.y += margin.bottom;
2675 }
2676
2677 #[allow(clippy::too_many_arguments)]
2679 fn layout_text_runs(
2680 &self,
2681 runs: &[TextRun],
2682 parent_href: Option<&str>,
2683 style: &ResolvedStyle,
2684 cursor: &mut PageCursor,
2685 pages: &mut Vec<LayoutPage>,
2686 text_x: f64,
2687 text_width: f64,
2688 font_context: &FontContext,
2689 source_location: Option<&SourceLocation>,
2690 bookmark: Option<&str>,
2691 ) {
2692 let mut styled_chars: Vec<StyledChar> = Vec::new();
2694 for run in runs {
2695 let run_style = run.style.resolve(Some(style), text_width);
2696 let run_href = run.href.as_deref().or(parent_href);
2697 let transform = run_style.text_transform;
2698 let mut prev_is_whitespace = true;
2699 for ch in run.content.chars() {
2700 let transformed_ch = apply_char_transform(ch, transform, prev_is_whitespace);
2701 prev_is_whitespace = ch.is_whitespace();
2702 styled_chars.push(StyledChar {
2703 ch: transformed_ch,
2704 font_family: run_style.font_family.clone(),
2705 font_size: run_style.font_size,
2706 font_weight: run_style.font_weight,
2707 font_style: run_style.font_style,
2708 color: run_style.color,
2709 href: run_href.map(|s| s.to_string()),
2710 text_decoration: run_style.text_decoration,
2711 letter_spacing: run_style.letter_spacing,
2712 });
2713 }
2714 }
2715
2716 let justify = matches!(style.text_align, TextAlign::Justify);
2718 let broken_lines = match style.line_breaking {
2719 LineBreaking::Optimal => self.text_layout.break_runs_into_lines_optimal(
2720 font_context,
2721 &styled_chars,
2722 text_width,
2723 style.hyphens,
2724 style.lang.as_deref(),
2725 justify,
2726 ),
2727 LineBreaking::Greedy => self.text_layout.break_runs_into_lines(
2728 font_context,
2729 &styled_chars,
2730 text_width,
2731 style.hyphens,
2732 style.lang.as_deref(),
2733 ),
2734 };
2735
2736 let broken_lines = match style.text_overflow {
2738 TextOverflow::Ellipsis => {
2739 self.text_layout
2740 .truncate_runs_with_ellipsis(font_context, broken_lines, text_width)
2741 }
2742 TextOverflow::Clip => {
2743 self.text_layout
2744 .truncate_runs_clip(font_context, broken_lines, text_width)
2745 }
2746 TextOverflow::Wrap => broken_lines,
2747 };
2748
2749 let line_height = style.font_size * style.line_height;
2750
2751 let line_heights: Vec<f64> = vec![line_height; broken_lines.len()];
2753 let decision = page_break::decide_break(
2754 cursor.remaining_height(),
2755 &line_heights,
2756 true,
2757 style.min_orphan_lines as usize,
2758 style.min_widow_lines as usize,
2759 );
2760
2761 let mut snapshot = cursor.elements.len();
2762 let mut container_start_y = cursor.content_y + cursor.y;
2763 let mut is_first_element = true;
2764
2765 if matches!(decision, page_break::BreakDecision::MoveToNextPage) {
2766 pages.push(cursor.finalize());
2767 *cursor = cursor.new_page();
2768 snapshot = cursor.elements.len();
2769 container_start_y = cursor.content_y + cursor.y;
2770 }
2771
2772 let forced_break_at = match decision {
2773 page_break::BreakDecision::Split {
2774 items_on_current_page,
2775 } => Some(items_on_current_page),
2776 _ => None,
2777 };
2778 let mut first_break_done = false;
2779
2780 for (line_idx, run_line) in broken_lines.iter().enumerate() {
2781 let needs_break = if let Some(break_at) = forced_break_at {
2782 if !first_break_done && line_idx == break_at {
2783 true
2784 } else {
2785 line_height > cursor.remaining_height()
2786 }
2787 } else {
2788 line_height > cursor.remaining_height()
2789 };
2790
2791 if needs_break {
2792 first_break_done = true;
2793 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2794 if !line_elements.is_empty() {
2795 let container_height = cursor.content_y + cursor.y - container_start_y;
2796 cursor.elements.push(LayoutElement {
2797 x: text_x,
2798 y: container_start_y,
2799 width: text_width,
2800 height: container_height,
2801 draw: DrawCommand::None,
2802 children: line_elements,
2803 node_type: Some("Text".to_string()),
2804 resolved_style: Some(style.clone()),
2805 source_location: source_location.cloned(),
2806 href: parent_href.map(|s| s.to_string()),
2807 bookmark: if is_first_element {
2808 bookmark.map(|s| s.to_string())
2809 } else {
2810 None
2811 },
2812 alt: None,
2813 is_header_row: false,
2814 overflow: Overflow::default(),
2815 });
2816 is_first_element = false;
2817 }
2818
2819 pages.push(cursor.finalize());
2820 *cursor = cursor.new_page();
2821
2822 snapshot = cursor.elements.len();
2823 container_start_y = cursor.content_y + cursor.y;
2824 }
2825
2826 let line_x = match style.text_align {
2827 TextAlign::Left => text_x,
2828 TextAlign::Right => text_x + text_width - run_line.width,
2829 TextAlign::Center => text_x + (text_width - run_line.width) / 2.0,
2830 TextAlign::Justify => text_x,
2831 };
2832
2833 let glyphs = self.build_positioned_glyphs_runs(run_line, font_context, style.direction);
2834
2835 let is_last_line = line_idx == broken_lines.len() - 1;
2839 let (justified_width, word_spacing) =
2840 if matches!(style.text_align, TextAlign::Justify) && !is_last_line {
2841 let last_non_space = glyphs.iter().rposition(|g| g.char_value != ' ');
2842 let (natural_width, space_count) = if let Some(idx) = last_non_space {
2843 let w: f64 = glyphs[..=idx].iter().map(|g| g.x_advance).sum();
2844 let s = glyphs[..=idx]
2845 .iter()
2846 .filter(|g| g.char_value == ' ')
2847 .count();
2848 (w, s)
2849 } else {
2850 (0.0, 0)
2851 };
2852 let slack = text_width - natural_width;
2853 let ws = if space_count > 0 && slack.abs() > 0.01 {
2854 slack / space_count as f64
2855 } else {
2856 0.0
2857 };
2858 (text_width, ws)
2859 } else {
2860 (run_line.width, 0.0)
2861 };
2862
2863 let text_line = TextLine {
2864 x: line_x,
2865 y: cursor.content_y + cursor.y + style.font_size,
2866 glyphs,
2867 width: justified_width,
2868 height: line_height,
2869 word_spacing,
2870 };
2871
2872 let text_dec = run_line
2874 .chars
2875 .iter()
2876 .find(|sc| !matches!(sc.text_decoration, TextDecoration::None))
2877 .map(|sc| sc.text_decoration)
2878 .unwrap_or(style.text_decoration);
2879
2880 cursor.elements.push(LayoutElement {
2881 x: line_x,
2882 y: cursor.content_y + cursor.y,
2883 width: justified_width,
2884 height: line_height,
2885 draw: DrawCommand::Text {
2886 lines: vec![text_line],
2887 color: style.color,
2888 text_decoration: text_dec,
2889 opacity: style.opacity,
2890 },
2891 children: vec![],
2892 node_type: Some("TextLine".to_string()),
2893 resolved_style: Some(style.clone()),
2894 source_location: None,
2895 href: parent_href.map(|s| s.to_string()),
2896 bookmark: None,
2897 alt: None,
2898 is_header_row: false,
2899 overflow: Overflow::default(),
2900 });
2901
2902 cursor.y += line_height;
2903 }
2904
2905 let line_elements: Vec<LayoutElement> = cursor.elements.drain(snapshot..).collect();
2906 if !line_elements.is_empty() {
2907 let container_height = cursor.content_y + cursor.y - container_start_y;
2908 cursor.elements.push(LayoutElement {
2909 x: text_x,
2910 y: container_start_y,
2911 width: text_width,
2912 height: container_height,
2913 draw: DrawCommand::None,
2914 children: line_elements,
2915 node_type: Some("Text".to_string()),
2916 resolved_style: Some(style.clone()),
2917 source_location: source_location.cloned(),
2918 href: parent_href.map(|s| s.to_string()),
2919 bookmark: if is_first_element {
2920 bookmark.map(|s| s.to_string())
2921 } else {
2922 None
2923 },
2924 alt: None,
2925 is_header_row: false,
2926 overflow: Overflow::default(),
2927 });
2928 }
2929 }
2930
2931 fn build_positioned_glyphs_single_style(
2935 &self,
2936 line: &BrokenLine,
2937 style: &ResolvedStyle,
2938 href: Option<&str>,
2939 font_context: &FontContext,
2940 ) -> Vec<PositionedGlyph> {
2941 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
2942 let line_text: String = line.chars.iter().collect();
2943 let direction = style.direction;
2944 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
2946
2947 let font_runs = crate::font::fallback::segment_by_font(
2950 &line.chars,
2951 &style.font_family,
2952 style.font_weight,
2953 italic,
2954 font_context.registry(),
2955 );
2956 let needs_per_char_fallback = font_runs.len() > 1
2957 || (font_runs.len() == 1 && font_runs[0].family != style.font_family);
2958
2959 if needs_per_char_fallback {
2961 let bidi_runs = if has_bidi {
2962 bidi::analyze_bidi(&line_text, direction)
2963 } else {
2964 vec![crate::text::bidi::BidiRun {
2965 char_start: 0,
2966 char_end: line.chars.len(),
2967 level: unicode_bidi::Level::ltr(),
2968 is_rtl: false,
2969 }]
2970 };
2971
2972 let mut all_glyphs = Vec::new();
2973 let mut bidi_levels = Vec::new();
2974 let mut x = 0.0_f64;
2975
2976 for bidi_run in &bidi_runs {
2978 for font_run in &font_runs {
2980 let start = font_run.start.max(bidi_run.char_start);
2982 let end = font_run.end.min(bidi_run.char_end);
2983 if start >= end {
2984 continue;
2985 }
2986
2987 let sub_chars: Vec<char> = line.chars[start..end].to_vec();
2988 let sub_text: String = sub_chars.iter().collect();
2989 let resolved_family = &font_run.family;
2990
2991 if let Some(font_data) =
2992 font_context.font_data(resolved_family, style.font_weight, italic)
2993 {
2994 if let Some(shaped) = shaping::shape_text_with_direction(
2995 &sub_text,
2996 font_data,
2997 bidi_run.is_rtl,
2998 ) {
2999 let units_per_em = font_context.units_per_em(
3000 resolved_family,
3001 style.font_weight,
3002 italic,
3003 );
3004 let scale = style.font_size / units_per_em as f64;
3005
3006 for sg in &shaped {
3007 let cluster = sg.cluster as usize;
3008 let char_value = sub_chars.get(cluster).copied().unwrap_or(' ');
3009
3010 let cluster_text = if shaped.len() < sub_chars.len() {
3011 let cluster_end =
3012 self.find_cluster_end(&shaped, sg, sub_chars.len());
3013 if cluster_end > cluster + 1 {
3014 Some(
3015 sub_chars[cluster..cluster_end]
3016 .iter()
3017 .collect::<String>(),
3018 )
3019 } else {
3020 None
3021 }
3022 } else {
3023 None
3024 };
3025
3026 let glyph_x = x + sg.x_offset as f64 * scale;
3027 let glyph_y = sg.y_offset as f64 * scale;
3028 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3029
3030 all_glyphs.push(PositionedGlyph {
3031 glyph_id: sg.glyph_id,
3032 x_offset: glyph_x,
3033 y_offset: glyph_y,
3034 x_advance: advance,
3035 font_size: style.font_size,
3036 font_family: resolved_family.clone(),
3037 font_weight: style.font_weight,
3038 font_style: style.font_style,
3039 char_value,
3040 color: Some(style.color),
3041 href: href.map(|s| s.to_string()),
3042 text_decoration: style.text_decoration,
3043 letter_spacing: style.letter_spacing,
3044 cluster_text,
3045 });
3046 bidi_levels.push(bidi_run.level);
3047 x += advance;
3048 }
3049 continue;
3050 }
3051 }
3052
3053 for i in start..end {
3055 let ch = line.chars[i];
3056 let glyph_x = x;
3057 let char_width = font_context.char_width(
3058 ch,
3059 resolved_family,
3060 style.font_weight,
3061 italic,
3062 style.font_size,
3063 );
3064 let advance = char_width + style.letter_spacing;
3065 all_glyphs.push(PositionedGlyph {
3066 glyph_id: ch as u16,
3067 x_offset: glyph_x,
3068 y_offset: 0.0,
3069 x_advance: advance,
3070 font_size: style.font_size,
3071 font_family: resolved_family.clone(),
3072 font_weight: style.font_weight,
3073 font_style: style.font_style,
3074 char_value: ch,
3075 color: Some(style.color),
3076 href: href.map(|s| s.to_string()),
3077 text_decoration: style.text_decoration,
3078 letter_spacing: style.letter_spacing,
3079 cluster_text: None,
3080 });
3081 bidi_levels.push(bidi_run.level);
3082 x += advance;
3083 }
3084 }
3085 }
3086
3087 if has_bidi && !all_glyphs.is_empty() {
3089 all_glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3090 bidi::reposition_after_reorder(&mut all_glyphs, 0.0);
3091 }
3092 return all_glyphs;
3093 }
3094
3095 if let Some(font_data) =
3098 font_context.font_data(&style.font_family, style.font_weight, italic)
3099 {
3100 if has_bidi {
3101 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3103 let units_per_em =
3104 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3105 let scale = style.font_size / units_per_em as f64;
3106
3107 let mut all_glyphs = Vec::new();
3108 let mut bidi_levels = Vec::new();
3109 let mut x = 0.0_f64;
3110
3111 for run in &bidi_runs {
3112 let run_chars: Vec<char> = line.chars[run.char_start..run.char_end].to_vec();
3113 let run_text: String = run_chars.iter().collect();
3114
3115 if let Some(shaped) =
3116 shaping::shape_text_with_direction(&run_text, font_data, run.is_rtl)
3117 {
3118 for sg in &shaped {
3119 let cluster = sg.cluster as usize;
3120 let char_value = run_chars.get(cluster).copied().unwrap_or(' ');
3121
3122 let cluster_text = if shaped.len() < run_chars.len() {
3123 let cluster_end =
3124 self.find_cluster_end(&shaped, sg, run_chars.len());
3125 if cluster_end > cluster + 1 {
3126 Some(run_chars[cluster..cluster_end].iter().collect::<String>())
3127 } else {
3128 None
3129 }
3130 } else {
3131 None
3132 };
3133
3134 let glyph_x = x + sg.x_offset as f64 * scale;
3135 let glyph_y = sg.y_offset as f64 * scale;
3136 let advance = sg.x_advance as f64 * scale + style.letter_spacing;
3137
3138 all_glyphs.push(PositionedGlyph {
3139 glyph_id: sg.glyph_id,
3140 x_offset: glyph_x,
3141 y_offset: glyph_y,
3142 x_advance: advance,
3143 font_size: style.font_size,
3144 font_family: style.font_family.clone(),
3145 font_weight: style.font_weight,
3146 font_style: style.font_style,
3147 char_value,
3148 color: Some(style.color),
3149 href: href.map(|s| s.to_string()),
3150 text_decoration: style.text_decoration,
3151 letter_spacing: style.letter_spacing,
3152 cluster_text,
3153 });
3154 bidi_levels.push(run.level);
3155
3156 x += advance;
3157 }
3158 }
3159 }
3160
3161 let mut glyphs = bidi::reorder_line_glyphs(all_glyphs, &bidi_levels);
3163 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3164 return glyphs;
3165 }
3166
3167 if let Some(shaped) = shaping::shape_text(&line_text, font_data) {
3169 let units_per_em =
3170 font_context.units_per_em(&style.font_family, style.font_weight, italic);
3171 let scale = style.font_size / units_per_em as f64;
3172
3173 return self.shaped_glyphs_to_positioned(
3174 &shaped,
3175 &line.chars,
3176 &line.char_positions,
3177 scale,
3178 style.font_size,
3179 &style.font_family,
3180 style.font_weight,
3181 style.font_style,
3182 Some(style.color),
3183 href,
3184 style.text_decoration,
3185 style.letter_spacing,
3186 );
3187 }
3188 }
3189
3190 let mut glyphs: Vec<PositionedGlyph> = line
3192 .chars
3193 .iter()
3194 .enumerate()
3195 .map(|(j, ch)| {
3196 let glyph_x = line.char_positions.get(j).copied().unwrap_or(0.0);
3197 let char_width = font_context.char_width(
3198 *ch,
3199 &style.font_family,
3200 style.font_weight,
3201 italic,
3202 style.font_size,
3203 );
3204 PositionedGlyph {
3205 glyph_id: *ch as u16,
3206 x_offset: glyph_x,
3207 y_offset: 0.0,
3208 x_advance: char_width,
3209 font_size: style.font_size,
3210 font_family: style.font_family.clone(),
3211 font_weight: style.font_weight,
3212 font_style: style.font_style,
3213 char_value: *ch,
3214 color: Some(style.color),
3215 href: href.map(|s| s.to_string()),
3216 text_decoration: style.text_decoration,
3217 letter_spacing: style.letter_spacing,
3218 cluster_text: None,
3219 }
3220 })
3221 .collect();
3222
3223 if has_bidi && !glyphs.is_empty() {
3225 let bidi_runs = bidi::analyze_bidi(&line_text, direction);
3226 let mut levels = Vec::with_capacity(glyphs.len());
3227 let mut char_idx = 0;
3228 for run in &bidi_runs {
3229 for _ in run.char_start..run.char_end {
3230 if char_idx < glyphs.len() {
3231 levels.push(run.level);
3232 char_idx += 1;
3233 }
3234 }
3235 }
3236 while levels.len() < glyphs.len() {
3238 levels.push(unicode_bidi::Level::ltr());
3239 }
3240 glyphs = bidi::reorder_line_glyphs(glyphs, &levels);
3241 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3242 }
3243
3244 glyphs
3245 }
3246
3247 fn build_positioned_glyphs_runs(
3252 &self,
3253 run_line: &RunBrokenLine,
3254 font_context: &FontContext,
3255 direction: Direction,
3256 ) -> Vec<PositionedGlyph> {
3257 let chars = &run_line.chars;
3258 if chars.is_empty() {
3259 return vec![];
3260 }
3261
3262 let resolved_families: Vec<String> = chars
3265 .iter()
3266 .map(|sc| {
3267 if !sc.font_family.contains(',') {
3268 sc.font_family.clone()
3269 } else {
3270 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
3271 let (_, family) = font_context.registry().resolve_for_char(
3272 &sc.font_family,
3273 sc.ch,
3274 sc.font_weight,
3275 italic,
3276 );
3277 family
3278 }
3279 })
3280 .collect();
3281
3282 let line_text: String = chars.iter().map(|c| c.ch).collect();
3283 let has_bidi = !bidi::is_pure_ltr(&line_text, direction);
3284 let bidi_runs = if has_bidi {
3285 Some(bidi::analyze_bidi(&line_text, direction))
3286 } else {
3287 None
3288 };
3289
3290 let mut glyphs = Vec::new();
3291 let mut bidi_levels = Vec::new();
3292 let mut i = 0;
3293
3294 while i < chars.len() {
3295 let sc = &chars[i];
3296 let italic = matches!(sc.font_style, FontStyle::Italic | FontStyle::Oblique);
3297 let resolved_family = &resolved_families[i];
3298
3299 let is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
3301 runs.iter()
3302 .any(|r| i >= r.char_start && i < r.char_end && r.is_rtl)
3303 });
3304
3305 if let Some(font_data) = font_context.font_data(resolved_family, sc.font_weight, italic)
3307 {
3308 let run_start = i;
3310 let mut run_end = i + 1;
3311 while run_end < chars.len() {
3312 let next = &chars[run_end];
3313 let next_italic =
3314 matches!(next.font_style, FontStyle::Italic | FontStyle::Oblique);
3315 let next_is_rtl = bidi_runs.as_ref().is_some_and(|runs| {
3316 runs.iter()
3317 .any(|r| run_end >= r.char_start && run_end < r.char_end && r.is_rtl)
3318 });
3319 if resolved_families[run_end] == *resolved_family
3321 && next.font_weight == sc.font_weight
3322 && next_italic == italic
3323 && (next.font_size - sc.font_size).abs() < 0.001
3324 && next_is_rtl == is_rtl
3325 {
3326 run_end += 1;
3327 } else {
3328 break;
3329 }
3330 }
3331
3332 let run_text: String = chars[run_start..run_end].iter().map(|c| c.ch).collect();
3333 if let Some(shaped) =
3334 shaping::shape_text_with_direction(&run_text, font_data, is_rtl)
3335 {
3336 let units_per_em =
3337 font_context.units_per_em(resolved_family, sc.font_weight, italic);
3338 let scale = sc.font_size / units_per_em as f64;
3339
3340 let run_chars: Vec<char> =
3342 chars[run_start..run_end].iter().map(|c| c.ch).collect();
3343 let run_positions: Vec<f64> = (run_start..run_end)
3344 .map(|j| run_line.char_positions.get(j).copied().unwrap_or(0.0))
3345 .collect();
3346
3347 let mut run_glyphs = self.shaped_glyphs_to_positioned_runs(
3349 &shaped,
3350 &chars[run_start..run_end],
3351 &run_chars,
3352 &run_positions,
3353 scale,
3354 );
3355 for g in &mut run_glyphs {
3357 g.font_family = resolved_family.clone();
3358 }
3359 let run_level = if is_rtl {
3361 unicode_bidi::Level::rtl()
3362 } else {
3363 unicode_bidi::Level::ltr()
3364 };
3365 for _ in &run_glyphs {
3366 bidi_levels.push(run_level);
3367 }
3368 glyphs.extend(run_glyphs);
3369 i = run_end;
3370 continue;
3371 }
3372 }
3373
3374 let glyph_x = run_line.char_positions.get(i).copied().unwrap_or(0.0);
3376 let char_width = font_context.char_width(
3377 sc.ch,
3378 resolved_family,
3379 sc.font_weight,
3380 italic,
3381 sc.font_size,
3382 );
3383 glyphs.push(PositionedGlyph {
3384 glyph_id: sc.ch as u16,
3385 x_offset: glyph_x,
3386 y_offset: 0.0,
3387 x_advance: char_width,
3388 font_size: sc.font_size,
3389 font_family: resolved_family.clone(),
3390 font_weight: sc.font_weight,
3391 font_style: sc.font_style,
3392 char_value: sc.ch,
3393 color: Some(sc.color),
3394 href: sc.href.clone(),
3395 text_decoration: sc.text_decoration,
3396 letter_spacing: sc.letter_spacing,
3397 cluster_text: None,
3398 });
3399 bidi_levels.push(if is_rtl {
3400 unicode_bidi::Level::rtl()
3401 } else {
3402 unicode_bidi::Level::ltr()
3403 });
3404 i += 1;
3405 }
3406
3407 if has_bidi && !glyphs.is_empty() {
3409 glyphs = bidi::reorder_line_glyphs(glyphs, &bidi_levels);
3410 bidi::reposition_after_reorder(&mut glyphs, 0.0);
3411 }
3412
3413 glyphs
3414 }
3415
3416 #[allow(clippy::too_many_arguments)]
3418 fn shaped_glyphs_to_positioned(
3419 &self,
3420 shaped: &[shaping::ShapedGlyph],
3421 chars: &[char],
3422 _char_positions: &[f64],
3423 scale: f64,
3424 font_size: f64,
3425 font_family: &str,
3426 font_weight: u32,
3427 font_style: FontStyle,
3428 color: Option<Color>,
3429 href: Option<&str>,
3430 text_decoration: TextDecoration,
3431 letter_spacing: f64,
3432 ) -> Vec<PositionedGlyph> {
3433 let mut result = Vec::with_capacity(shaped.len());
3434 let mut x = 0.0_f64;
3435
3436 for sg in shaped {
3437 let cluster = sg.cluster as usize;
3438 let char_value = chars.get(cluster).copied().unwrap_or(' ');
3439
3440 let cluster_text = if shaped.len() < chars.len() {
3442 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
3445 if cluster_end > cluster + 1 {
3446 Some(chars[cluster..cluster_end].iter().collect::<String>())
3447 } else {
3448 None
3449 }
3450 } else {
3451 None
3452 };
3453
3454 let glyph_x = x + sg.x_offset as f64 * scale;
3456 let glyph_y = sg.y_offset as f64 * scale;
3457 let advance = sg.x_advance as f64 * scale + letter_spacing;
3458
3459 result.push(PositionedGlyph {
3460 glyph_id: sg.glyph_id,
3461 x_offset: glyph_x,
3462 y_offset: glyph_y,
3463 x_advance: advance,
3464 font_size,
3465 font_family: font_family.to_string(),
3466 font_weight,
3467 font_style,
3468 char_value,
3469 color,
3470 href: href.map(|s| s.to_string()),
3471 text_decoration,
3472 letter_spacing,
3473 cluster_text,
3474 });
3475
3476 x += advance;
3477 }
3478
3479 result
3480 }
3481
3482 fn shaped_glyphs_to_positioned_runs(
3484 &self,
3485 shaped: &[shaping::ShapedGlyph],
3486 styled_chars: &[StyledChar],
3487 chars: &[char],
3488 char_positions: &[f64],
3489 scale: f64,
3490 ) -> Vec<PositionedGlyph> {
3491 let mut result = Vec::with_capacity(shaped.len());
3492 let base_x = char_positions.first().copied().unwrap_or(0.0);
3494 let mut x = 0.0_f64;
3495
3496 for sg in shaped {
3497 let cluster = sg.cluster as usize;
3498 let sc = styled_chars.get(cluster).unwrap_or(&styled_chars[0]);
3499 let char_value = chars.get(cluster).copied().unwrap_or(' ');
3500
3501 let cluster_text = if shaped.len() < chars.len() {
3502 let cluster_end = self.find_cluster_end(shaped, sg, chars.len());
3503 if cluster_end > cluster + 1 {
3504 Some(chars[cluster..cluster_end].iter().collect::<String>())
3505 } else {
3506 None
3507 }
3508 } else {
3509 None
3510 };
3511
3512 let glyph_x = base_x + x + sg.x_offset as f64 * scale;
3513 let glyph_y = sg.y_offset as f64 * scale;
3514 let advance = sg.x_advance as f64 * scale + sc.letter_spacing;
3515
3516 result.push(PositionedGlyph {
3517 glyph_id: sg.glyph_id,
3518 x_offset: glyph_x,
3519 y_offset: glyph_y,
3520 x_advance: advance,
3521 font_size: sc.font_size,
3522 font_family: sc.font_family.clone(),
3523 font_weight: sc.font_weight,
3524 font_style: sc.font_style,
3525 char_value,
3526 color: Some(sc.color),
3527 href: sc.href.clone(),
3528 text_decoration: sc.text_decoration,
3529 letter_spacing: sc.letter_spacing,
3530 cluster_text,
3531 });
3532
3533 x += advance;
3534 }
3535
3536 result
3537 }
3538
3539 fn find_cluster_end(
3541 &self,
3542 shaped: &[shaping::ShapedGlyph],
3543 current: &shaping::ShapedGlyph,
3544 num_chars: usize,
3545 ) -> usize {
3546 for sg in shaped {
3548 if sg.cluster > current.cluster {
3549 return sg.cluster as usize;
3550 }
3551 }
3552 num_chars
3554 }
3555
3556 #[allow(clippy::too_many_arguments)]
3557 fn layout_image(
3558 &self,
3559 node: &Node,
3560 style: &ResolvedStyle,
3561 cursor: &mut PageCursor,
3562 pages: &mut Vec<LayoutPage>,
3563 x: f64,
3564 available_width: f64,
3565 explicit_width: Option<f64>,
3566 explicit_height: Option<f64>,
3567 ) {
3568 let margin = &style.margin.to_edges();
3569
3570 let src = match &node.kind {
3572 NodeKind::Image { src, .. } => src.as_str(),
3573 _ => "",
3574 };
3575
3576 let loaded = if !src.is_empty() {
3577 crate::image_loader::load_image(src).ok()
3578 } else {
3579 None
3580 };
3581
3582 let (img_width, img_height) = if let Some(ref img) = loaded {
3584 let intrinsic_w = img.width_px as f64;
3585 let intrinsic_h = img.height_px as f64;
3586 let aspect = if intrinsic_w > 0.0 {
3587 intrinsic_h / intrinsic_w
3588 } else {
3589 0.75
3590 };
3591
3592 match (explicit_width, explicit_height) {
3593 (Some(w), Some(h)) => (w, h),
3594 (Some(w), None) => (w, w * aspect),
3595 (None, Some(h)) => (h / aspect, h),
3596 (None, None) => {
3597 let max_w = available_width - margin.horizontal();
3598 let w = intrinsic_w.min(max_w);
3599 (w, w * aspect)
3600 }
3601 }
3602 } else {
3603 let w = explicit_width.unwrap_or(available_width - margin.horizontal());
3605 let h = explicit_height.unwrap_or(w * 0.75);
3606 (w, h)
3607 };
3608
3609 let total_height = img_height + margin.vertical();
3610
3611 if total_height > cursor.remaining_height() {
3612 pages.push(cursor.finalize());
3613 *cursor = cursor.new_page();
3614 }
3615
3616 cursor.y += margin.top;
3617
3618 let draw = if let Some(image_data) = loaded {
3619 DrawCommand::Image { image_data }
3620 } else {
3621 DrawCommand::ImagePlaceholder
3622 };
3623
3624 cursor.elements.push(LayoutElement {
3625 x: x + margin.left,
3626 y: cursor.content_y + cursor.y,
3627 width: img_width,
3628 height: img_height,
3629 draw,
3630 children: vec![],
3631 node_type: Some(node_kind_name(&node.kind).to_string()),
3632 resolved_style: Some(style.clone()),
3633 source_location: node.source_location.clone(),
3634 href: node.href.clone(),
3635 bookmark: node.bookmark.clone(),
3636 alt: node.alt.clone(),
3637 is_header_row: false,
3638 overflow: style.overflow,
3639 });
3640
3641 cursor.y += img_height + margin.bottom;
3642 }
3643
3644 #[allow(clippy::too_many_arguments)]
3646 fn layout_svg(
3647 &self,
3648 node: &Node,
3649 style: &ResolvedStyle,
3650 cursor: &mut PageCursor,
3651 pages: &mut Vec<LayoutPage>,
3652 x: f64,
3653 _available_width: f64,
3654 svg_width: f64,
3655 svg_height: f64,
3656 view_box: Option<&str>,
3657 content: &str,
3658 ) {
3659 let margin = &style.margin.to_edges();
3660 let total_height = svg_height + margin.vertical();
3661
3662 if total_height > cursor.remaining_height() {
3663 pages.push(cursor.finalize());
3664 *cursor = cursor.new_page();
3665 }
3666
3667 cursor.y += margin.top;
3668
3669 let vb = view_box
3670 .and_then(crate::svg::parse_view_box)
3671 .unwrap_or(crate::svg::ViewBox {
3672 min_x: 0.0,
3673 min_y: 0.0,
3674 width: svg_width,
3675 height: svg_height,
3676 });
3677
3678 let commands = crate::svg::parse_svg(content, vb, svg_width, svg_height);
3679
3680 cursor.elements.push(LayoutElement {
3681 x: x + margin.left,
3682 y: cursor.content_y + cursor.y,
3683 width: svg_width,
3684 height: svg_height,
3685 draw: DrawCommand::Svg {
3686 commands,
3687 width: svg_width,
3688 height: svg_height,
3689 clip: false,
3690 },
3691 children: vec![],
3692 node_type: Some("Svg".to_string()),
3693 resolved_style: Some(style.clone()),
3694 source_location: node.source_location.clone(),
3695 href: node.href.clone(),
3696 bookmark: node.bookmark.clone(),
3697 alt: node.alt.clone(),
3698 is_header_row: false,
3699 overflow: style.overflow,
3700 });
3701
3702 cursor.y += svg_height + margin.bottom;
3703 }
3704
3705 fn canvas_ops_to_svg_commands(operations: &[CanvasOp]) -> Vec<crate::svg::SvgCommand> {
3707 use crate::svg::SvgCommand;
3708
3709 let mut commands = Vec::new();
3710 let mut cur_x = 0.0_f64;
3711 let mut cur_y = 0.0_f64;
3712
3713 for op in operations {
3714 match op {
3715 CanvasOp::MoveTo { x, y } => {
3716 commands.push(SvgCommand::MoveTo(*x, *y));
3717 cur_x = *x;
3718 cur_y = *y;
3719 }
3720 CanvasOp::LineTo { x, y } => {
3721 commands.push(SvgCommand::LineTo(*x, *y));
3722 cur_x = *x;
3723 cur_y = *y;
3724 }
3725 CanvasOp::BezierCurveTo {
3726 cp1x,
3727 cp1y,
3728 cp2x,
3729 cp2y,
3730 x,
3731 y,
3732 } => {
3733 commands.push(SvgCommand::CurveTo(*cp1x, *cp1y, *cp2x, *cp2y, *x, *y));
3734 cur_x = *x;
3735 cur_y = *y;
3736 }
3737 CanvasOp::QuadraticCurveTo { cpx, cpy, x, y } => {
3738 let cp1x = cur_x + 2.0 / 3.0 * (*cpx - cur_x);
3740 let cp1y = cur_y + 2.0 / 3.0 * (*cpy - cur_y);
3741 let cp2x = *x + 2.0 / 3.0 * (*cpx - *x);
3742 let cp2y = *y + 2.0 / 3.0 * (*cpy - *y);
3743 commands.push(SvgCommand::CurveTo(cp1x, cp1y, cp2x, cp2y, *x, *y));
3744 cur_x = *x;
3745 cur_y = *y;
3746 }
3747 CanvasOp::ClosePath => {
3748 commands.push(SvgCommand::ClosePath);
3749 }
3750 CanvasOp::Rect {
3751 x,
3752 y,
3753 width,
3754 height,
3755 } => {
3756 commands.push(SvgCommand::MoveTo(*x, *y));
3757 commands.push(SvgCommand::LineTo(*x + *width, *y));
3758 commands.push(SvgCommand::LineTo(*x + *width, *y + *height));
3759 commands.push(SvgCommand::LineTo(*x, *y + *height));
3760 commands.push(SvgCommand::ClosePath);
3761 cur_x = *x;
3762 cur_y = *y;
3763 }
3764 CanvasOp::Circle { cx, cy, r } => {
3765 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *r, *r));
3766 }
3767 CanvasOp::Ellipse { cx, cy, rx, ry } => {
3768 commands.extend(crate::svg::ellipse_commands(*cx, *cy, *rx, *ry));
3769 }
3770 CanvasOp::Arc {
3771 cx,
3772 cy,
3773 r,
3774 start_angle,
3775 end_angle,
3776 counterclockwise,
3777 } => {
3778 let steps = 32;
3782 let mut sweep = end_angle - start_angle;
3783 if !counterclockwise && sweep < 0.0 {
3784 sweep += 2.0 * std::f64::consts::PI;
3785 }
3786 if *counterclockwise && sweep > 0.0 {
3787 sweep -= 2.0 * std::f64::consts::PI;
3788 }
3789 for i in 0..=steps {
3790 let t = *start_angle + sweep * (i as f64 / steps as f64);
3791 let px = cx + r * t.cos();
3792 let py = cy + r * t.sin();
3793 if i == 0 {
3794 commands.push(SvgCommand::MoveTo(px, py));
3795 } else {
3796 commands.push(SvgCommand::LineTo(px, py));
3797 }
3798 }
3799 }
3800 CanvasOp::Stroke => commands.push(SvgCommand::Stroke),
3801 CanvasOp::Fill => commands.push(SvgCommand::Fill),
3802 CanvasOp::FillAndStroke => commands.push(SvgCommand::FillAndStroke),
3803 CanvasOp::SetFillColor { r, g, b } => {
3804 commands.push(SvgCommand::SetFill(r / 255.0, g / 255.0, b / 255.0));
3806 }
3807 CanvasOp::SetStrokeColor { r, g, b } => {
3808 commands.push(SvgCommand::SetStroke(r / 255.0, g / 255.0, b / 255.0));
3809 }
3810 CanvasOp::SetLineWidth { width } => {
3811 commands.push(SvgCommand::SetStrokeWidth(*width));
3812 }
3813 CanvasOp::SetLineCap { cap } => {
3814 commands.push(SvgCommand::SetLineCap(*cap));
3815 }
3816 CanvasOp::SetLineJoin { join } => {
3817 commands.push(SvgCommand::SetLineJoin(*join));
3818 }
3819 CanvasOp::Save => commands.push(SvgCommand::SaveState),
3820 CanvasOp::Restore => commands.push(SvgCommand::RestoreState),
3821 }
3822 }
3823
3824 commands
3825 }
3826
3827 #[allow(clippy::too_many_arguments)]
3829 fn layout_canvas(
3830 &self,
3831 node: &Node,
3832 style: &ResolvedStyle,
3833 cursor: &mut PageCursor,
3834 pages: &mut Vec<LayoutPage>,
3835 x: f64,
3836 _available_width: f64,
3837 canvas_width: f64,
3838 canvas_height: f64,
3839 operations: &[CanvasOp],
3840 ) {
3841 let margin = style.margin.to_edges();
3842 let total_height = canvas_height + margin.top + margin.bottom;
3843
3844 if cursor.remaining_height() < total_height && cursor.y > 0.0 {
3846 pages.push(cursor.finalize());
3847 *cursor = cursor.new_page();
3848 }
3849
3850 cursor.y += margin.top;
3851
3852 let svg_commands = Self::canvas_ops_to_svg_commands(operations);
3853
3854 cursor.elements.push(LayoutElement {
3855 x: x + margin.left,
3856 y: cursor.content_y + cursor.y,
3857 width: canvas_width,
3858 height: canvas_height,
3859 draw: DrawCommand::Svg {
3860 commands: svg_commands,
3861 width: canvas_width,
3862 height: canvas_height,
3863 clip: true,
3864 },
3865 children: vec![],
3866 node_type: Some("Canvas".to_string()),
3867 resolved_style: Some(style.clone()),
3868 source_location: node.source_location.clone(),
3869 href: node.href.clone(),
3870 bookmark: node.bookmark.clone(),
3871 alt: node.alt.clone(),
3872 is_header_row: false,
3873 overflow: style.overflow,
3874 });
3875
3876 cursor.y += canvas_height + margin.bottom;
3877 }
3878
3879 #[allow(clippy::too_many_arguments)]
3881 #[allow(clippy::too_many_arguments)]
3883 fn layout_chart(
3884 &self,
3885 node: &Node,
3886 style: &ResolvedStyle,
3887 cursor: &mut PageCursor,
3888 pages: &mut Vec<LayoutPage>,
3889 x: f64,
3890 chart_width: f64,
3891 chart_height: f64,
3892 primitives: Vec<crate::chart::ChartPrimitive>,
3893 node_type_name: &str,
3894 ) {
3895 let margin = &style.margin.to_edges();
3896 let total_height = chart_height + margin.vertical();
3897
3898 if total_height > cursor.remaining_height() {
3899 pages.push(cursor.finalize());
3900 *cursor = cursor.new_page();
3901 }
3902
3903 cursor.y += margin.top;
3904
3905 let draw = DrawCommand::Chart { primitives };
3906
3907 cursor.elements.push(LayoutElement {
3908 x: x + margin.left,
3909 y: cursor.content_y + cursor.y,
3910 width: chart_width,
3911 height: chart_height,
3912 draw,
3913 children: vec![],
3914 node_type: Some(node_type_name.to_string()),
3915 resolved_style: Some(style.clone()),
3916 source_location: node.source_location.clone(),
3917 href: node.href.clone(),
3918 bookmark: node.bookmark.clone(),
3919 alt: node.alt.clone(),
3920 is_header_row: false,
3921 overflow: style.overflow,
3922 });
3923
3924 cursor.y += chart_height + margin.bottom;
3925 }
3926
3927 #[allow(clippy::too_many_arguments)]
3928 fn layout_barcode(
3929 &self,
3930 node: &Node,
3931 style: &ResolvedStyle,
3932 cursor: &mut PageCursor,
3933 pages: &mut Vec<LayoutPage>,
3934 x: f64,
3935 available_width: f64,
3936 data: &str,
3937 format: crate::barcode::BarcodeFormat,
3938 explicit_width: Option<f64>,
3939 bar_height: f64,
3940 ) {
3941 let margin = &style.margin.to_edges();
3942 let display_width = explicit_width.unwrap_or(available_width - margin.horizontal());
3943 let total_height = bar_height + margin.vertical();
3944
3945 if total_height > cursor.remaining_height() {
3946 pages.push(cursor.finalize());
3947 *cursor = cursor.new_page();
3948 }
3949
3950 cursor.y += margin.top;
3951
3952 let draw = match crate::barcode::generate_barcode(data, format) {
3953 Ok(barcode_data) => {
3954 let bar_width = if barcode_data.bars.is_empty() {
3955 0.0
3956 } else {
3957 display_width / barcode_data.bars.len() as f64
3958 };
3959 DrawCommand::Barcode {
3960 bars: barcode_data.bars,
3961 bar_width,
3962 height: bar_height,
3963 color: style.color,
3964 }
3965 }
3966 Err(_) => DrawCommand::None,
3967 };
3968
3969 cursor.elements.push(LayoutElement {
3970 x: x + margin.left,
3971 y: cursor.content_y + cursor.y,
3972 width: display_width,
3973 height: bar_height,
3974 draw,
3975 children: vec![],
3976 node_type: Some("Barcode".to_string()),
3977 resolved_style: Some(style.clone()),
3978 source_location: node.source_location.clone(),
3979 href: node.href.clone(),
3980 bookmark: node.bookmark.clone(),
3981 alt: node.alt.clone(),
3982 is_header_row: false,
3983 overflow: style.overflow,
3984 });
3985
3986 cursor.y += bar_height + margin.bottom;
3987 }
3988
3989 #[allow(clippy::too_many_arguments)]
3991 fn layout_qrcode(
3992 &self,
3993 node: &Node,
3994 style: &ResolvedStyle,
3995 cursor: &mut PageCursor,
3996 pages: &mut Vec<LayoutPage>,
3997 x: f64,
3998 available_width: f64,
3999 data: &str,
4000 explicit_size: Option<f64>,
4001 ) {
4002 let margin = &style.margin.to_edges();
4003 let display_size = explicit_size.unwrap_or(available_width - margin.horizontal());
4004 let total_height = display_size + margin.vertical();
4005
4006 if total_height > cursor.remaining_height() {
4007 pages.push(cursor.finalize());
4008 *cursor = cursor.new_page();
4009 }
4010
4011 cursor.y += margin.top;
4012
4013 let draw = match crate::qrcode::generate_qr(data) {
4014 Ok(matrix) => {
4015 let module_size = display_size / matrix.size as f64;
4016 DrawCommand::QrCode {
4017 modules: matrix.modules,
4018 module_size,
4019 color: style.color,
4020 }
4021 }
4022 Err(_) => DrawCommand::None,
4023 };
4024
4025 cursor.elements.push(LayoutElement {
4026 x: x + margin.left,
4027 y: cursor.content_y + cursor.y,
4028 width: display_size,
4029 height: display_size,
4030 draw,
4031 children: vec![],
4032 node_type: Some("QrCode".to_string()),
4033 resolved_style: Some(style.clone()),
4034 source_location: node.source_location.clone(),
4035 href: node.href.clone(),
4036 bookmark: node.bookmark.clone(),
4037 alt: node.alt.clone(),
4038 is_header_row: false,
4039 overflow: style.overflow,
4040 });
4041
4042 cursor.y += display_size + margin.bottom;
4043 }
4044
4045 fn measure_node_height(
4048 &self,
4049 node: &Node,
4050 available_width: f64,
4051 style: &ResolvedStyle,
4052 font_context: &FontContext,
4053 ) -> f64 {
4054 match &node.kind {
4055 NodeKind::Text { content, runs, .. } => {
4056 let measure_width = available_width - style.margin.horizontal();
4057 if !runs.is_empty() {
4058 let mut styled_chars: Vec<StyledChar> = Vec::new();
4060 for run in runs {
4061 let run_style = run.style.resolve(Some(style), measure_width);
4062 for ch in run.content.chars() {
4063 styled_chars.push(StyledChar {
4064 ch,
4065 font_family: run_style.font_family.clone(),
4066 font_size: run_style.font_size,
4067 font_weight: run_style.font_weight,
4068 font_style: run_style.font_style,
4069 color: run_style.color,
4070 href: None,
4071 text_decoration: run_style.text_decoration,
4072 letter_spacing: run_style.letter_spacing,
4073 });
4074 }
4075 }
4076 let broken_lines = self.text_layout.break_runs_into_lines(
4077 font_context,
4078 &styled_chars,
4079 measure_width,
4080 style.hyphens,
4081 style.lang.as_deref(),
4082 );
4083 let line_height = style.font_size * style.line_height;
4084 (broken_lines.len() as f64) * line_height + style.padding.vertical()
4085 } else {
4086 let lines = self.text_layout.break_into_lines(
4087 font_context,
4088 content,
4089 measure_width,
4090 style.font_size,
4091 &style.font_family,
4092 style.font_weight,
4093 style.font_style,
4094 style.letter_spacing,
4095 style.hyphens,
4096 style.lang.as_deref(),
4097 );
4098 let line_height = style.font_size * style.line_height;
4099 (lines.len() as f64) * line_height + style.padding.vertical()
4100 }
4101 }
4102 NodeKind::Image {
4103 src,
4104 width: explicit_w,
4105 height: explicit_h,
4106 } => {
4107 if let SizeConstraint::Fixed(h) = style.height {
4109 return h + style.padding.vertical();
4110 }
4111 if let Some(h) = explicit_h {
4113 return *h + style.padding.vertical();
4114 }
4115 let aspect = self
4117 .get_image_dimensions(src)
4118 .map(|(w, h)| if w > 0 { h as f64 / w as f64 } else { 0.75 })
4119 .unwrap_or(0.75);
4120 let w = if let SizeConstraint::Fixed(w) = style.width {
4121 w
4122 } else {
4123 explicit_w.unwrap_or(available_width - style.margin.horizontal())
4124 };
4125 w * aspect + style.padding.vertical()
4126 }
4127 NodeKind::Svg { height, .. } => *height + style.margin.vertical(),
4128 NodeKind::Barcode { height, .. } => *height + style.margin.vertical(),
4129 NodeKind::QrCode { size, .. } => {
4130 let display_size = size.unwrap_or(available_width - style.margin.horizontal());
4131 display_size + style.margin.vertical()
4132 }
4133 NodeKind::Canvas { height, .. } => *height + style.margin.vertical(),
4134 NodeKind::BarChart { height, .. }
4135 | NodeKind::LineChart { height, .. }
4136 | NodeKind::PieChart { height, .. }
4137 | NodeKind::AreaChart { height, .. }
4138 | NodeKind::DotPlot { height, .. } => *height + style.margin.vertical(),
4139 NodeKind::Watermark { .. } => 0.0, _ => {
4141 if let SizeConstraint::Fixed(h) = style.height {
4143 return h;
4144 }
4145 let outer_width = match style.width {
4147 SizeConstraint::Fixed(w) => w,
4148 SizeConstraint::Auto => available_width - style.margin.horizontal(),
4149 };
4150 let inner_width =
4151 outer_width - style.padding.horizontal() - style.border_width.horizontal();
4152 let children_height =
4153 self.measure_children_height(&node.children, inner_width, style, font_context);
4154 children_height + style.padding.vertical() + style.border_width.vertical()
4155 }
4156 }
4157 }
4158
4159 fn measure_children_height(
4160 &self,
4161 children: &[Node],
4162 available_width: f64,
4163 parent_style: &ResolvedStyle,
4164 font_context: &FontContext,
4165 ) -> f64 {
4166 if matches!(parent_style.display, Display::Grid) {
4168 if let Some(template_cols) = &parent_style.grid_template_columns {
4169 let num_columns = template_cols.len();
4170 if num_columns > 0 && !children.is_empty() {
4171 let col_gap = parent_style.column_gap;
4172 let row_gap = parent_style.row_gap;
4173
4174 let content_sizes: Vec<f64> = template_cols
4175 .iter()
4176 .map(|track| {
4177 if matches!(track, GridTrackSize::Auto) {
4178 available_width / num_columns as f64
4179 } else {
4180 0.0
4181 }
4182 })
4183 .collect();
4184
4185 let col_widths = grid::resolve_tracks(
4186 template_cols,
4187 available_width,
4188 col_gap,
4189 &content_sizes,
4190 );
4191
4192 let placements: Vec<Option<&GridPlacement>> = children
4193 .iter()
4194 .map(|child| child.style.grid_placement.as_ref())
4195 .collect();
4196
4197 let item_placements = grid::place_items(&placements, num_columns);
4198 let num_rows = grid::compute_num_rows(&item_placements);
4199
4200 if num_rows == 0 {
4201 return 0.0;
4202 }
4203
4204 let mut row_heights = vec![0.0_f64; num_rows];
4205 for placement in &item_placements {
4206 let cell_width = grid::span_width(
4207 placement.col_start,
4208 placement.col_end,
4209 &col_widths,
4210 col_gap,
4211 );
4212 let child = &children[placement.child_index];
4213 let child_style = child.style.resolve(Some(parent_style), cell_width);
4214 let h =
4215 self.measure_node_height(child, cell_width, &child_style, font_context);
4216 let span = placement.row_end - placement.row_start;
4217 let per_row = h / span as f64;
4218 for rh in row_heights
4219 .iter_mut()
4220 .take(placement.row_end.min(num_rows))
4221 .skip(placement.row_start)
4222 {
4223 if per_row > *rh {
4224 *rh = per_row;
4225 }
4226 }
4227 }
4228
4229 let total_row_gap = row_gap * (num_rows as f64 - 1.0).max(0.0);
4230 return row_heights.iter().sum::<f64>() + total_row_gap;
4231 }
4232 }
4233 }
4234
4235 let direction = parent_style.flex_direction;
4236 let row_gap = parent_style.row_gap;
4237 let column_gap = parent_style.column_gap;
4238
4239 match direction {
4240 FlexDirection::Row | FlexDirection::RowReverse => {
4241 let styles: Vec<ResolvedStyle> = children
4244 .iter()
4245 .map(|child| child.style.resolve(Some(parent_style), available_width))
4246 .collect();
4247
4248 let base_widths: Vec<f64> = children
4249 .iter()
4250 .zip(&styles)
4251 .map(|(child, style)| match style.flex_basis {
4252 SizeConstraint::Fixed(w) => w,
4253 SizeConstraint::Auto => match style.width {
4254 SizeConstraint::Fixed(w) => w,
4255 SizeConstraint::Auto => self
4256 .measure_intrinsic_width(child, style, font_context)
4257 .min(available_width),
4258 },
4259 })
4260 .collect();
4261
4262 let lines = match parent_style.flex_wrap {
4263 FlexWrap::NoWrap => {
4264 vec![flex::WrapLine {
4265 start: 0,
4266 end: children.len(),
4267 }]
4268 }
4269 FlexWrap::Wrap | FlexWrap::WrapReverse => {
4270 flex::partition_into_lines(&base_widths, column_gap, available_width)
4271 }
4272 };
4273
4274 let mut final_widths = base_widths.clone();
4276 for line in &lines {
4277 let line_count = line.end - line.start;
4278 let line_gap = column_gap * (line_count as f64 - 1.0).max(0.0);
4279 let distributable = available_width - line_gap;
4280 let total_base: f64 = base_widths[line.start..line.end].iter().sum();
4281 let remaining = distributable - total_base;
4282
4283 if remaining > 0.0 {
4284 let total_grow: f64 = styles[line.start..line.end]
4285 .iter()
4286 .map(|s| s.flex_grow)
4287 .sum();
4288 if total_grow > 0.0 {
4289 for (j, s) in styles[line.start..line.end].iter().enumerate() {
4290 final_widths[line.start + j] = base_widths[line.start + j]
4291 + remaining * (s.flex_grow / total_grow);
4292 }
4293 }
4294 } else if remaining < 0.0 {
4295 let total_shrink: f64 = styles[line.start..line.end]
4296 .iter()
4297 .enumerate()
4298 .map(|(j, s)| s.flex_shrink * base_widths[line.start + j])
4299 .sum();
4300 if total_shrink > 0.0 {
4301 for (j, s) in styles[line.start..line.end].iter().enumerate() {
4302 let factor =
4303 (s.flex_shrink * base_widths[line.start + j]) / total_shrink;
4304 let w = base_widths[line.start + j] + remaining * factor;
4305 final_widths[line.start + j] = w.max(s.min_width);
4306 }
4307 }
4308 }
4309 }
4310
4311 let mut total = 0.0;
4312 for (i, line) in lines.iter().enumerate() {
4313 let line_height: f64 = children[line.start..line.end]
4314 .iter()
4315 .enumerate()
4316 .map(|(j, child)| {
4317 let fw = final_widths[line.start + j];
4318 let child_style = child.style.resolve(Some(parent_style), fw);
4319 self.measure_node_height(child, fw, &child_style, font_context)
4320 + child_style.margin.vertical()
4321 })
4322 .fold(0.0f64, f64::max);
4323 total += line_height;
4324 if i > 0 {
4325 total += row_gap;
4326 }
4327 }
4328 total
4329 }
4330 FlexDirection::Column | FlexDirection::ColumnReverse => {
4331 let mut total = 0.0;
4332 for (i, child) in children.iter().enumerate() {
4333 let child_style = child.style.resolve(Some(parent_style), available_width);
4334 let child_height = self.measure_node_height(
4335 child,
4336 available_width,
4337 &child_style,
4338 font_context,
4339 );
4340 total += child_height + child_style.margin.vertical();
4341 if i > 0 {
4342 total += row_gap;
4343 }
4344 }
4345 total
4346 }
4347 }
4348 }
4349
4350 fn measure_intrinsic_width(
4352 &self,
4353 node: &Node,
4354 style: &ResolvedStyle,
4355 font_context: &FontContext,
4356 ) -> f64 {
4357 match &node.kind {
4358 NodeKind::Svg { width, .. } => {
4359 *width + style.padding.horizontal() + style.margin.horizontal()
4360 }
4361 NodeKind::Text { content, .. } => {
4362 let transformed = apply_text_transform(content, style.text_transform);
4363 let italic = matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4364 let text_width = font_context.measure_string(
4365 &transformed,
4366 &style.font_family,
4367 style.font_weight,
4368 italic,
4369 style.font_size,
4370 style.letter_spacing,
4371 );
4372 text_width + 0.01 + style.padding.horizontal() + style.margin.horizontal()
4375 }
4376 NodeKind::Image {
4377 src, width, height, ..
4378 } => {
4379 let w = if let SizeConstraint::Fixed(w) = style.width {
4380 w
4381 } else if let Some(w) = width {
4382 *w
4383 } else if let Some((iw, ih)) = self.get_image_dimensions(src) {
4384 let pixel_w = iw as f64;
4385 let pixel_h = ih as f64;
4386 let aspect = if pixel_w > 0.0 {
4387 pixel_h / pixel_w
4388 } else {
4389 0.75
4390 };
4391 let constrained_h = match style.height {
4393 SizeConstraint::Fixed(h) => Some(h),
4394 SizeConstraint::Auto => *height,
4395 };
4396 if let Some(h) = constrained_h {
4397 h / aspect
4398 } else {
4399 pixel_w
4400 }
4401 } else {
4402 100.0
4403 };
4404 w + style.padding.horizontal() + style.margin.horizontal()
4405 }
4406 NodeKind::Barcode { width, .. } => {
4407 let w = width.unwrap_or(0.0);
4408 w + style.padding.horizontal() + style.margin.horizontal()
4409 }
4410 NodeKind::QrCode { size, .. } => {
4411 let display_size = size.unwrap_or(0.0);
4412 display_size + style.padding.horizontal() + style.margin.horizontal()
4413 }
4414 NodeKind::Canvas { width, .. } => {
4415 *width + style.padding.horizontal() + style.margin.horizontal()
4416 }
4417 NodeKind::BarChart { width, .. }
4418 | NodeKind::LineChart { width, .. }
4419 | NodeKind::PieChart { width, .. }
4420 | NodeKind::AreaChart { width, .. }
4421 | NodeKind::DotPlot { width, .. } => {
4422 *width + style.padding.horizontal() + style.margin.horizontal()
4423 }
4424 NodeKind::Watermark { .. } => 0.0, _ => {
4426 if node.children.is_empty() {
4428 style.padding.horizontal() + style.margin.horizontal()
4429 } else {
4430 let direction = style.flex_direction;
4431 let gap = style.gap;
4432 let mut total = 0.0f64;
4433 for (i, child) in node.children.iter().enumerate() {
4434 let child_style = child.style.resolve(Some(style), 0.0);
4435 let child_width =
4436 self.measure_intrinsic_width(child, &child_style, font_context);
4437 match direction {
4438 FlexDirection::Row | FlexDirection::RowReverse => {
4439 total += child_width;
4440 if i > 0 {
4441 total += gap;
4442 }
4443 }
4444 _ => {
4445 total = total.max(child_width);
4446 }
4447 }
4448 }
4449 total
4450 + style.padding.horizontal()
4451 + style.margin.horizontal()
4452 + style.border_width.horizontal()
4453 }
4454 }
4455 }
4456 }
4457
4458 pub fn measure_min_content_width(
4462 &self,
4463 node: &Node,
4464 style: &ResolvedStyle,
4465 font_context: &FontContext,
4466 ) -> f64 {
4467 match &node.kind {
4468 NodeKind::Text { content, runs, .. } => {
4469 let word_width = if !runs.is_empty() {
4470 runs.iter()
4472 .map(|run| {
4473 let run_style = run.style.resolve(Some(style), 0.0);
4474 let transformed =
4475 apply_text_transform(&run.content, run_style.text_transform);
4476 self.text_layout.measure_widest_word(
4477 font_context,
4478 &transformed,
4479 run_style.font_size,
4480 &run_style.font_family,
4481 run_style.font_weight,
4482 run_style.font_style,
4483 run_style.letter_spacing,
4484 style.hyphens,
4485 style.lang.as_deref(),
4486 )
4487 })
4488 .fold(0.0f64, f64::max)
4489 } else {
4490 let transformed = apply_text_transform(content, style.text_transform);
4491 self.text_layout.measure_widest_word(
4492 font_context,
4493 &transformed,
4494 style.font_size,
4495 &style.font_family,
4496 style.font_weight,
4497 style.font_style,
4498 style.letter_spacing,
4499 style.hyphens,
4500 style.lang.as_deref(),
4501 )
4502 };
4503 word_width + style.padding.horizontal() + style.margin.horizontal()
4504 }
4505 NodeKind::Image { width, .. } => {
4506 width.unwrap_or(0.0) + style.padding.horizontal() + style.margin.horizontal()
4507 }
4508 NodeKind::Svg { width, .. } => {
4509 *width + style.padding.horizontal() + style.margin.horizontal()
4510 }
4511 _ => {
4512 if node.children.is_empty() {
4513 style.padding.horizontal()
4514 + style.margin.horizontal()
4515 + style.border_width.horizontal()
4516 } else {
4517 let mut max_child_min = 0.0f64;
4518 for child in &node.children {
4519 let child_style = child.style.resolve(Some(style), 0.0);
4520 let child_min =
4521 self.measure_min_content_width(child, &child_style, font_context);
4522 max_child_min = max_child_min.max(child_min);
4523 }
4524 max_child_min
4525 + style.padding.horizontal()
4526 + style.margin.horizontal()
4527 + style.border_width.horizontal()
4528 }
4529 }
4530 }
4531 }
4532
4533 fn measure_table_row_height(
4534 &self,
4535 row: &Node,
4536 col_widths: &[f64],
4537 parent_style: &ResolvedStyle,
4538 font_context: &FontContext,
4539 ) -> f64 {
4540 let row_style = row
4541 .style
4542 .resolve(Some(parent_style), col_widths.iter().sum());
4543 let mut max_height: f64 = 0.0;
4544
4545 for (i, cell) in row.children.iter().enumerate() {
4546 let col_width = col_widths.get(i).copied().unwrap_or(0.0);
4547 let cell_style = cell.style.resolve(Some(&row_style), col_width);
4548 let inner_width =
4549 col_width - cell_style.padding.horizontal() - cell_style.border_width.horizontal();
4550
4551 let mut cell_content_height = 0.0;
4552 for child in &cell.children {
4553 let child_style = child.style.resolve(Some(&cell_style), inner_width);
4554 cell_content_height +=
4555 self.measure_node_height(child, inner_width, &child_style, font_context);
4556 }
4557
4558 let total = cell_content_height
4559 + cell_style.padding.vertical()
4560 + cell_style.border_width.vertical();
4561 max_height = max_height.max(total);
4562 }
4563
4564 max_height.max(row_style.min_height)
4565 }
4566
4567 fn resolve_column_widths(
4568 &self,
4569 defs: &[ColumnDef],
4570 available_width: f64,
4571 children: &[Node],
4572 ) -> Vec<f64> {
4573 if defs.is_empty() {
4574 let num_cols = children.first().map(|row| row.children.len()).unwrap_or(1);
4575 return vec![available_width / num_cols as f64; num_cols];
4576 }
4577
4578 let mut widths = Vec::new();
4579 let mut remaining = available_width;
4580 let mut auto_count = 0;
4581
4582 for def in defs {
4583 match def.width {
4584 ColumnWidth::Fixed(w) => {
4585 widths.push(w);
4586 remaining -= w;
4587 }
4588 ColumnWidth::Fraction(f) => {
4589 let w = available_width * f;
4590 widths.push(w);
4591 remaining -= w;
4592 }
4593 ColumnWidth::Auto => {
4594 widths.push(0.0);
4595 auto_count += 1;
4596 }
4597 }
4598 }
4599
4600 if auto_count > 0 {
4601 let auto_width = remaining / auto_count as f64;
4602 for (i, def) in defs.iter().enumerate() {
4603 if matches!(def.width, ColumnWidth::Auto) {
4604 widths[i] = auto_width;
4605 }
4606 }
4607 }
4608
4609 widths
4610 }
4611
4612 fn inject_fixed_elements(&self, pages: &mut [LayoutPage], font_context: &FontContext) {
4613 for page in pages.iter_mut() {
4614 if !page.watermarks.is_empty() {
4616 let (page_w, page_h) = page.config.size.dimensions();
4617 let cx = page_w / 2.0;
4618 let cy = page_h / 2.0;
4619
4620 let mut watermark_elements = Vec::new();
4621 for wm_node in &page.watermarks {
4622 if let NodeKind::Watermark {
4623 text,
4624 font_size,
4625 angle,
4626 } = &wm_node.kind
4627 {
4628 let style = wm_node.style.resolve(None, page_w);
4629 let color = style.color;
4630 let opacity = style.opacity;
4631 let angle_rad = angle.to_radians();
4632
4633 let italic =
4635 matches!(style.font_style, FontStyle::Italic | FontStyle::Oblique);
4636
4637 let shaped = self.text_layout.shape_text(
4639 font_context,
4640 text,
4641 &style.font_family,
4642 style.font_weight,
4643 style.font_style,
4644 );
4645
4646 let mut glyphs = Vec::new();
4647 let mut x_pos = 0.0;
4648 let text_chars: Vec<char> = text.chars().collect();
4649
4650 if let Some(shaped_glyphs) = shaped {
4651 let units_per_em = font_context.units_per_em(
4653 &style.font_family,
4654 style.font_weight,
4655 italic,
4656 ) as f64;
4657
4658 for sg in &shaped_glyphs {
4659 let advance = sg.x_advance as f64 / units_per_em * *font_size;
4660 let cluster_idx = sg.cluster as usize;
4661 let ch = text_chars.get(cluster_idx).copied().unwrap_or(' ');
4662 glyphs.push(PositionedGlyph {
4663 glyph_id: sg.glyph_id,
4664 char_value: ch,
4665 x_offset: x_pos,
4666 y_offset: 0.0,
4667 x_advance: advance,
4668 font_size: *font_size,
4669 font_family: style.font_family.clone(),
4670 font_weight: style.font_weight,
4671 font_style: style.font_style,
4672 color: Some(color),
4673 href: None,
4674 text_decoration: TextDecoration::None,
4675 letter_spacing: style.letter_spacing,
4676 cluster_text: None,
4677 });
4678 x_pos += advance + style.letter_spacing;
4679 }
4680 } else {
4681 for &ch in &text_chars {
4683 let w = font_context.char_width(
4684 ch,
4685 &style.font_family,
4686 style.font_weight,
4687 italic,
4688 *font_size,
4689 );
4690 glyphs.push(PositionedGlyph {
4691 glyph_id: ch as u16,
4692 char_value: ch,
4693 x_offset: x_pos,
4694 y_offset: 0.0,
4695 x_advance: w,
4696 font_size: *font_size,
4697 font_family: style.font_family.clone(),
4698 font_weight: style.font_weight,
4699 font_style: style.font_style,
4700 color: Some(color),
4701 href: None,
4702 text_decoration: TextDecoration::None,
4703 letter_spacing: style.letter_spacing,
4704 cluster_text: None,
4705 });
4706 x_pos += w + style.letter_spacing;
4707 }
4708 }
4709
4710 let text_width = x_pos;
4711
4712 let line = TextLine {
4713 x: 0.0,
4714 y: 0.0,
4715 glyphs,
4716 width: text_width,
4717 height: *font_size,
4718 word_spacing: 0.0,
4719 };
4720
4721 watermark_elements.push(LayoutElement {
4722 x: cx,
4723 y: cy,
4724 width: text_width,
4725 height: *font_size,
4726 draw: DrawCommand::Watermark {
4727 lines: vec![line],
4728 color,
4729 opacity,
4730 angle_rad,
4731 font_family: style.font_family.clone(),
4732 },
4733 children: vec![],
4734 node_type: Some("Watermark".to_string()),
4735 resolved_style: None,
4736 source_location: None,
4737 href: None,
4738 bookmark: None,
4739 alt: None,
4740 is_header_row: false,
4741 overflow: Overflow::default(),
4742 });
4743 }
4744 }
4745
4746 watermark_elements.append(&mut page.elements);
4748 page.elements = watermark_elements;
4749 page.watermarks.clear();
4750 }
4751
4752 if page.fixed_header.is_empty() && page.fixed_footer.is_empty() {
4753 continue;
4754 }
4755
4756 if !page.fixed_header.is_empty() {
4758 let mut hdr_cursor = PageCursor::new(&page.config);
4759 for (node, _h) in &page.fixed_header {
4760 let cw = hdr_cursor.content_width;
4761 let cx = hdr_cursor.content_x;
4762 let style = node.style.resolve(None, cw);
4763 self.layout_view(
4764 node,
4765 &style,
4766 &mut hdr_cursor,
4767 &mut Vec::new(),
4768 cx,
4769 cw,
4770 font_context,
4771 );
4772 }
4773 let mut combined = hdr_cursor.elements;
4775 combined.append(&mut page.elements);
4776 page.elements = combined;
4777 }
4778
4779 if !page.fixed_footer.is_empty() {
4784 let mut ftr_cursor = PageCursor::new(&page.config);
4785 let total_ftr: f64 = page.fixed_footer.iter().map(|(_, h)| *h).sum();
4786 let target_y = ftr_cursor.content_height - total_ftr;
4787 for (node, _h) in &page.fixed_footer {
4789 let cw = ftr_cursor.content_width;
4790 let cx = ftr_cursor.content_x;
4791 let style = node.style.resolve(None, cw);
4792 self.layout_view(
4793 node,
4794 &style,
4795 &mut ftr_cursor,
4796 &mut Vec::new(),
4797 cx,
4798 cw,
4799 font_context,
4800 );
4801 }
4802 for el in &mut ftr_cursor.elements {
4806 offset_element_y(el, target_y);
4807 }
4808 page.elements.extend(ftr_cursor.elements);
4809 }
4810
4811 page.fixed_header.clear();
4813 page.fixed_footer.clear();
4814 }
4815 }
4816
4817 #[allow(clippy::too_many_arguments)]
4822 fn layout_grid_children(
4823 &self,
4824 children: &[Node],
4825 parent_style: &ResolvedStyle,
4826 cursor: &mut PageCursor,
4827 pages: &mut Vec<LayoutPage>,
4828 x: f64,
4829 available_width: f64,
4830 font_context: &FontContext,
4831 ) {
4832 let template_cols = match &parent_style.grid_template_columns {
4833 Some(cols) => cols,
4834 None => return, };
4836
4837 let num_columns = template_cols.len();
4838 if num_columns == 0 || children.is_empty() {
4839 return;
4840 }
4841
4842 let col_gap = parent_style.column_gap;
4843 let row_gap = parent_style.row_gap;
4844
4845 let content_sizes: Vec<f64> = template_cols
4848 .iter()
4849 .map(|track| {
4850 if matches!(track, GridTrackSize::Auto) {
4851 available_width / num_columns as f64
4854 } else {
4855 0.0
4856 }
4857 })
4858 .collect();
4859
4860 let col_widths =
4861 grid::resolve_tracks(template_cols, available_width, col_gap, &content_sizes);
4862
4863 let placements: Vec<Option<&GridPlacement>> = children
4865 .iter()
4866 .map(|child| child.style.grid_placement.as_ref())
4867 .collect();
4868
4869 let item_placements = grid::place_items(&placements, num_columns);
4871 let num_rows = grid::compute_num_rows(&item_placements);
4872
4873 if num_rows == 0 {
4874 return;
4875 }
4876
4877 let mut item_heights: Vec<f64> = vec![0.0; children.len()];
4879 for placement in &item_placements {
4880 let cell_width =
4881 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
4882 let child = &children[placement.child_index];
4883 let child_style = child.style.resolve(Some(parent_style), cell_width);
4884 item_heights[placement.child_index] =
4885 self.measure_node_height(child, cell_width, &child_style, font_context);
4886 }
4887
4888 let template_rows = parent_style.grid_template_rows.as_deref();
4890 let mut row_heights = vec![0.0_f64; num_rows];
4891 for placement in &item_placements {
4892 let h = item_heights[placement.child_index];
4893 let span = placement.row_end - placement.row_start;
4894 let per_row = h / span as f64;
4895 for rh in row_heights
4896 .iter_mut()
4897 .take(placement.row_end.min(num_rows))
4898 .skip(placement.row_start)
4899 {
4900 if per_row > *rh {
4901 *rh = per_row;
4902 }
4903 }
4904 }
4905
4906 if let Some(template) = template_rows {
4908 let auto_row = parent_style.grid_auto_rows.as_ref();
4909 for (r, rh) in row_heights.iter_mut().enumerate() {
4910 let track = template.get(r).or(auto_row);
4911 if let Some(track) = track {
4912 match track {
4913 GridTrackSize::Pt(pts) => *rh = *pts,
4914 GridTrackSize::Auto => {} _ => {} }
4917 }
4918 }
4919 }
4920
4921 for (row, &row_height) in row_heights.iter().enumerate().take(num_rows) {
4923 if row_height > cursor.remaining_height() && row > 0 {
4925 pages.push(cursor.finalize());
4926 *cursor = cursor.new_page();
4927 }
4928
4929 for placement in &item_placements {
4931 if placement.row_start != row {
4932 continue; }
4934
4935 let cell_x = x + grid::column_x_offset(placement.col_start, &col_widths, col_gap);
4936 let cell_width =
4937 grid::span_width(placement.col_start, placement.col_end, &col_widths, col_gap);
4938
4939 let child = &children[placement.child_index];
4940
4941 let saved_y = cursor.y;
4943 self.layout_node(
4944 child,
4945 cursor,
4946 pages,
4947 cell_x,
4948 cell_width,
4949 Some(parent_style),
4950 font_context,
4951 None,
4952 );
4953 cursor.y = saved_y;
4955 }
4956
4957 cursor.y += row_height + row_gap;
4958 }
4959
4960 if num_rows > 0 {
4962 cursor.y -= row_gap;
4963 }
4964 }
4965}
4966
4967struct FlexItem<'a> {
4968 node: &'a Node,
4969 style: ResolvedStyle,
4970 base_width: f64,
4971 min_content_width: f64,
4972}
4973
4974#[cfg(test)]
4975mod tests {
4976 use super::*;
4977 use crate::font::FontContext;
4978
4979 fn make_text(content: &str, font_size: f64) -> Node {
4980 Node {
4981 kind: NodeKind::Text {
4982 content: content.to_string(),
4983 href: None,
4984 runs: vec![],
4985 },
4986 style: Style {
4987 font_size: Some(font_size),
4988 ..Default::default()
4989 },
4990 children: vec![],
4991 id: None,
4992 source_location: None,
4993 bookmark: None,
4994 href: None,
4995 alt: None,
4996 }
4997 }
4998
4999 fn make_styled_view(style: Style, children: Vec<Node>) -> Node {
5000 Node {
5001 kind: NodeKind::View,
5002 style,
5003 children,
5004 id: None,
5005 source_location: None,
5006 bookmark: None,
5007 href: None,
5008 alt: None,
5009 }
5010 }
5011
5012 #[test]
5013 fn intrinsic_width_flex_row_sums_children() {
5014 let engine = LayoutEngine::new();
5015 let font_context = FontContext::new();
5016
5017 let child1 = make_text("Hello", 14.0);
5018 let child2 = make_text("World", 14.0);
5019
5020 let child1_style = child1.style.resolve(None, 0.0);
5021 let child2_style = child2.style.resolve(None, 0.0);
5022 let child1_w = engine.measure_intrinsic_width(&child1, &child1_style, &font_context);
5023 let child2_w = engine.measure_intrinsic_width(&child2, &child2_style, &font_context);
5024
5025 let row = make_styled_view(
5026 Style {
5027 flex_direction: Some(FlexDirection::Row),
5028 ..Default::default()
5029 },
5030 vec![make_text("Hello", 14.0), make_text("World", 14.0)],
5031 );
5032 let row_style = row.style.resolve(None, 0.0);
5033 let row_w = engine.measure_intrinsic_width(&row, &row_style, &font_context);
5034
5035 assert!(
5036 (row_w - (child1_w + child2_w)).abs() < 0.01,
5037 "Row intrinsic width ({}) should equal sum of children ({} + {})",
5038 row_w,
5039 child1_w,
5040 child2_w
5041 );
5042 }
5043
5044 #[test]
5045 fn intrinsic_width_flex_column_takes_max() {
5046 let engine = LayoutEngine::new();
5047 let font_context = FontContext::new();
5048
5049 let short = make_text("Hi", 14.0);
5050 let long = make_text("Hello World", 14.0);
5051
5052 let short_style = short.style.resolve(None, 0.0);
5053 let long_style = long.style.resolve(None, 0.0);
5054 let short_w = engine.measure_intrinsic_width(&short, &short_style, &font_context);
5055 let long_w = engine.measure_intrinsic_width(&long, &long_style, &font_context);
5056
5057 let col = make_styled_view(
5058 Style {
5059 flex_direction: Some(FlexDirection::Column),
5060 ..Default::default()
5061 },
5062 vec![make_text("Hi", 14.0), make_text("Hello World", 14.0)],
5063 );
5064 let col_style = col.style.resolve(None, 0.0);
5065 let col_w = engine.measure_intrinsic_width(&col, &col_style, &font_context);
5066
5067 assert!(
5068 (col_w - long_w).abs() < 0.01,
5069 "Column intrinsic width ({}) should equal max child ({}, short was {})",
5070 col_w,
5071 long_w,
5072 short_w
5073 );
5074 }
5075
5076 #[test]
5077 fn intrinsic_width_nested_containers() {
5078 let engine = LayoutEngine::new();
5079 let font_context = FontContext::new();
5080
5081 let inner = make_styled_view(
5082 Style {
5083 flex_direction: Some(FlexDirection::Row),
5084 ..Default::default()
5085 },
5086 vec![make_text("A", 12.0), make_text("B", 12.0)],
5087 );
5088 let inner_style = inner.style.resolve(None, 0.0);
5089 let inner_w = engine.measure_intrinsic_width(&inner, &inner_style, &font_context);
5090
5091 let outer = make_styled_view(
5092 Style::default(),
5093 vec![make_styled_view(
5094 Style {
5095 flex_direction: Some(FlexDirection::Row),
5096 ..Default::default()
5097 },
5098 vec![make_text("A", 12.0), make_text("B", 12.0)],
5099 )],
5100 );
5101 let outer_style = outer.style.resolve(None, 0.0);
5102 let outer_w = engine.measure_intrinsic_width(&outer, &outer_style, &font_context);
5103
5104 assert!(
5105 (outer_w - inner_w).abs() < 0.01,
5106 "Nested container ({}) should match inner container ({})",
5107 outer_w,
5108 inner_w
5109 );
5110 }
5111
5112 #[test]
5113 fn intrinsic_width_row_with_gap() {
5114 let engine = LayoutEngine::new();
5115 let font_context = FontContext::new();
5116
5117 let no_gap = make_styled_view(
5118 Style {
5119 flex_direction: Some(FlexDirection::Row),
5120 ..Default::default()
5121 },
5122 vec![make_text("A", 12.0), make_text("B", 12.0)],
5123 );
5124 let with_gap = make_styled_view(
5125 Style {
5126 flex_direction: Some(FlexDirection::Row),
5127 gap: Some(10.0),
5128 ..Default::default()
5129 },
5130 vec![make_text("A", 12.0), make_text("B", 12.0)],
5131 );
5132
5133 let no_gap_style = no_gap.style.resolve(None, 0.0);
5134 let with_gap_style = with_gap.style.resolve(None, 0.0);
5135 let no_gap_w = engine.measure_intrinsic_width(&no_gap, &no_gap_style, &font_context);
5136 let with_gap_w = engine.measure_intrinsic_width(&with_gap, &with_gap_style, &font_context);
5137
5138 assert!(
5139 (with_gap_w - no_gap_w - 10.0).abs() < 0.01,
5140 "Gap should add 10pt: with_gap={}, no_gap={}",
5141 with_gap_w,
5142 no_gap_w
5143 );
5144 }
5145
5146 #[test]
5147 fn intrinsic_width_empty_container() {
5148 let engine = LayoutEngine::new();
5149 let font_context = FontContext::new();
5150
5151 let padding = 8.0;
5152 let empty = make_styled_view(
5153 Style {
5154 padding: Some(Edges::uniform(padding)),
5155 ..Default::default()
5156 },
5157 vec![],
5158 );
5159 let style = empty.style.resolve(None, 0.0);
5160 let w = engine.measure_intrinsic_width(&empty, &style, &font_context);
5161
5162 assert!(
5163 (w - padding * 2.0).abs() < 0.01,
5164 "Empty container width ({}) should equal horizontal padding ({})",
5165 w,
5166 padding * 2.0
5167 );
5168 }
5169
5170 #[test]
5173 fn flex_shrink_respects_min_content_width() {
5174 let engine = LayoutEngine::new();
5178 let font_context = FontContext::new();
5179
5180 let sale_text = make_text("SALE", 12.0);
5181 let sale_style = sale_text.style.resolve(None, 0.0);
5182 let sale_word_width =
5183 engine.measure_min_content_width(&sale_text, &sale_style, &font_context);
5184 assert!(
5185 sale_word_width > 0.0,
5186 "SALE should have non-zero min-content width"
5187 );
5188
5189 let container = make_styled_view(
5192 Style {
5193 flex_direction: Some(FlexDirection::Row),
5194 width: Some(Dimension::Pt(100.0)),
5195 ..Default::default()
5196 },
5197 vec![
5198 make_styled_view(
5199 Style {
5200 width: Some(Dimension::Pt(80.0)),
5201 flex_shrink: Some(1.0),
5202 ..Default::default()
5203 },
5204 vec![],
5205 ),
5206 make_styled_view(
5207 Style {
5208 width: Some(Dimension::Pt(60.0)),
5209 flex_shrink: Some(1.0),
5210 ..Default::default()
5211 },
5212 vec![make_text("SALE", 12.0)],
5213 ),
5214 ],
5215 );
5216
5217 let doc = Document {
5218 children: vec![Node::page(
5219 PageConfig::default(),
5220 Style::default(),
5221 vec![container],
5222 )],
5223 metadata: Default::default(),
5224 default_page: PageConfig::default(),
5225 fonts: vec![],
5226 tagged: false,
5227 pdfa: None,
5228 default_style: None,
5229 embedded_data: None,
5230 };
5231
5232 let pages = engine.layout(&doc, &font_context);
5233 assert!(!pages.is_empty());
5234
5235 let page = &pages[0];
5238 let container_el = page.elements.iter().find(|e| e.children.len() == 2);
5240 assert!(
5241 container_el.is_some(),
5242 "Should find container with 2 children"
5243 );
5244 let sale_child = &container_el.unwrap().children[1];
5245 assert!(
5246 sale_child.width >= sale_word_width - 0.01,
5247 "SALE child width ({}) should be >= min-content width ({})",
5248 sale_child.width,
5249 sale_word_width
5250 );
5251 }
5252
5253 #[test]
5256 fn column_justify_content_center() {
5257 let engine = LayoutEngine::new();
5260 let font_context = FontContext::new();
5261
5262 let container = make_styled_view(
5263 Style {
5264 flex_direction: Some(FlexDirection::Column),
5265 height: Some(Dimension::Pt(200.0)),
5266 justify_content: Some(JustifyContent::Center),
5267 ..Default::default()
5268 },
5269 vec![make_text("Centered", 12.0)],
5270 );
5271
5272 let doc = Document {
5273 children: vec![Node::page(
5274 PageConfig::default(),
5275 Style::default(),
5276 vec![container],
5277 )],
5278 metadata: Default::default(),
5279 default_page: PageConfig::default(),
5280 fonts: vec![],
5281 tagged: false,
5282 pdfa: None,
5283 default_style: None,
5284 embedded_data: None,
5285 };
5286
5287 let pages = engine.layout(&doc, &font_context);
5288 let page = &pages[0];
5289
5290 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5293 assert!(
5294 container_el.is_some(),
5295 "Should find container with children"
5296 );
5297 let container_el = container_el.unwrap();
5298 let child = &container_el.children[0];
5299
5300 let child_offset = child.y - container_el.y;
5302 let expected_offset = (200.0 - child.height) / 2.0;
5303 assert!(
5304 (child_offset - expected_offset).abs() < 2.0,
5305 "Child offset ({}) should be near center ({})",
5306 child_offset,
5307 expected_offset
5308 );
5309 }
5310
5311 #[test]
5312 fn column_align_items_center() {
5313 let engine = LayoutEngine::new();
5316 let font_context = FontContext::new();
5317
5318 let container = make_styled_view(
5319 Style {
5320 flex_direction: Some(FlexDirection::Column),
5321 width: Some(Dimension::Pt(300.0)),
5322 align_items: Some(AlignItems::Center),
5323 ..Default::default()
5324 },
5325 vec![make_text("Hi", 12.0)],
5326 );
5327
5328 let doc = Document {
5329 children: vec![Node::page(
5330 PageConfig::default(),
5331 Style::default(),
5332 vec![container],
5333 )],
5334 metadata: Default::default(),
5335 default_page: PageConfig::default(),
5336 fonts: vec![],
5337 tagged: false,
5338 pdfa: None,
5339 default_style: None,
5340 embedded_data: None,
5341 };
5342
5343 let pages = engine.layout(&doc, &font_context);
5344 let page = &pages[0];
5345
5346 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5347 assert!(container_el.is_some());
5348 let container_el = container_el.unwrap();
5349 let child = &container_el.children[0];
5350
5351 let child_center = child.x + child.width / 2.0;
5353 let container_center = container_el.x + container_el.width / 2.0;
5354 assert!(
5355 (child_center - container_center).abs() < 2.0,
5356 "Child center ({}) should be near container center ({})",
5357 child_center,
5358 container_center
5359 );
5360 }
5361
5362 #[test]
5365 fn absolute_child_positioned_relative_to_parent() {
5366 let engine = LayoutEngine::new();
5369 let font_context = FontContext::new();
5370
5371 let parent = make_styled_view(
5372 Style {
5373 margin: Some(MarginEdges::from_edges(Edges {
5374 top: 50.0,
5375 left: 50.0,
5376 ..Default::default()
5377 })),
5378 width: Some(Dimension::Pt(200.0)),
5379 height: Some(Dimension::Pt(200.0)),
5380 ..Default::default()
5381 },
5382 vec![make_styled_view(
5383 Style {
5384 position: Some(crate::model::Position::Absolute),
5385 top: Some(10.0),
5386 left: Some(10.0),
5387 width: Some(Dimension::Pt(50.0)),
5388 height: Some(Dimension::Pt(50.0)),
5389 ..Default::default()
5390 },
5391 vec![],
5392 )],
5393 );
5394
5395 let doc = Document {
5396 children: vec![Node::page(
5397 PageConfig::default(),
5398 Style::default(),
5399 vec![parent],
5400 )],
5401 metadata: Default::default(),
5402 default_page: PageConfig::default(),
5403 fonts: vec![],
5404 tagged: false,
5405 pdfa: None,
5406 default_style: None,
5407 embedded_data: None,
5408 };
5409
5410 let pages = engine.layout(&doc, &font_context);
5411 let page = &pages[0];
5412
5413 let parent_el = page
5416 .elements
5417 .iter()
5418 .find(|e| e.width > 190.0 && e.width < 210.0);
5419 assert!(parent_el.is_some(), "Should find the 200x200 parent");
5420 let parent_el = parent_el.unwrap();
5421
5422 let abs_child = parent_el
5424 .children
5425 .iter()
5426 .find(|e| e.width > 45.0 && e.width < 55.0);
5427 assert!(abs_child.is_some(), "Should find 50x50 absolute child");
5428 let abs_child = abs_child.unwrap();
5429
5430 let expected_x = parent_el.x + 10.0;
5431 let expected_y = parent_el.y + 10.0;
5432 assert!(
5433 (abs_child.x - expected_x).abs() < 1.0,
5434 "Absolute child x ({}) should be parent.x + 10 ({})",
5435 abs_child.x,
5436 expected_x
5437 );
5438 assert!(
5439 (abs_child.y - expected_y).abs() < 1.0,
5440 "Absolute child y ({}) should be parent.y + 10 ({})",
5441 abs_child.y,
5442 expected_y
5443 );
5444 }
5445
5446 #[test]
5447 fn text_transform_none_passthrough() {
5448 assert_eq!(
5449 apply_text_transform("Hello World", TextTransform::None),
5450 "Hello World"
5451 );
5452 }
5453
5454 #[test]
5455 fn text_transform_uppercase() {
5456 assert_eq!(
5457 apply_text_transform("hello world", TextTransform::Uppercase),
5458 "HELLO WORLD"
5459 );
5460 }
5461
5462 #[test]
5463 fn text_transform_lowercase() {
5464 assert_eq!(
5465 apply_text_transform("HELLO WORLD", TextTransform::Lowercase),
5466 "hello world"
5467 );
5468 }
5469
5470 #[test]
5471 fn text_transform_capitalize() {
5472 assert_eq!(
5473 apply_text_transform("hello world", TextTransform::Capitalize),
5474 "Hello World"
5475 );
5476 assert_eq!(
5477 apply_text_transform(" hello world ", TextTransform::Capitalize),
5478 " Hello World "
5479 );
5480 assert_eq!(
5481 apply_text_transform("already Capitalized", TextTransform::Capitalize),
5482 "Already Capitalized"
5483 );
5484 }
5485
5486 #[test]
5487 fn text_transform_capitalize_empty() {
5488 assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
5489 }
5490
5491 #[test]
5492 fn apply_char_transform_uppercase() {
5493 assert_eq!(
5494 apply_char_transform('a', TextTransform::Uppercase, false),
5495 'A'
5496 );
5497 assert_eq!(
5498 apply_char_transform('A', TextTransform::Uppercase, false),
5499 'A'
5500 );
5501 }
5502
5503 #[test]
5504 fn apply_char_transform_capitalize_word_start() {
5505 assert_eq!(
5506 apply_char_transform('h', TextTransform::Capitalize, true),
5507 'H'
5508 );
5509 assert_eq!(
5510 apply_char_transform('h', TextTransform::Capitalize, false),
5511 'h'
5512 );
5513 }
5514
5515 #[test]
5518 fn column_flex_grow_single_child_fills_container() {
5519 let engine = LayoutEngine::new();
5522 let font_context = FontContext::new();
5523
5524 let child = make_styled_view(
5525 Style {
5526 flex_grow: Some(1.0),
5527 ..Default::default()
5528 },
5529 vec![make_text("Short", 12.0)],
5530 );
5531
5532 let container = make_styled_view(
5533 Style {
5534 flex_direction: Some(FlexDirection::Column),
5535 height: Some(Dimension::Pt(300.0)),
5536 ..Default::default()
5537 },
5538 vec![child],
5539 );
5540
5541 let doc = Document {
5542 children: vec![Node::page(
5543 PageConfig::default(),
5544 Style::default(),
5545 vec![container],
5546 )],
5547 metadata: Default::default(),
5548 default_page: PageConfig::default(),
5549 fonts: vec![],
5550 tagged: false,
5551 pdfa: None,
5552 default_style: None,
5553 embedded_data: None,
5554 };
5555
5556 let pages = engine.layout(&doc, &font_context);
5557 let page = &pages[0];
5558
5559 let container_el = page.elements.iter().find(|e| !e.children.is_empty());
5560 assert!(container_el.is_some());
5561 let container_el = container_el.unwrap();
5562 assert!(
5563 (container_el.height - 300.0).abs() < 1.0,
5564 "Container should be 300pt, got {}",
5565 container_el.height
5566 );
5567
5568 let child_el = &container_el.children[0];
5569 assert!(
5570 (child_el.height - 300.0).abs() < 1.0,
5571 "flex-grow child should expand to 300pt, got {}",
5572 child_el.height
5573 );
5574 }
5575
5576 #[test]
5577 fn column_flex_grow_two_children_proportional() {
5578 let engine = LayoutEngine::new();
5581 let font_context = FontContext::new();
5582
5583 let child1 = make_styled_view(
5584 Style {
5585 flex_grow: Some(1.0),
5586 ..Default::default()
5587 },
5588 vec![make_text("A", 12.0)],
5589 );
5590 let child2 = make_styled_view(
5591 Style {
5592 flex_grow: Some(2.0),
5593 ..Default::default()
5594 },
5595 vec![make_text("B", 12.0)],
5596 );
5597
5598 let container = make_styled_view(
5599 Style {
5600 flex_direction: Some(FlexDirection::Column),
5601 height: Some(Dimension::Pt(300.0)),
5602 ..Default::default()
5603 },
5604 vec![child1, child2],
5605 );
5606
5607 let doc = Document {
5608 children: vec![Node::page(
5609 PageConfig::default(),
5610 Style::default(),
5611 vec![container],
5612 )],
5613 metadata: Default::default(),
5614 default_page: PageConfig::default(),
5615 fonts: vec![],
5616 tagged: false,
5617 pdfa: None,
5618 default_style: None,
5619 embedded_data: None,
5620 };
5621
5622 let pages = engine.layout(&doc, &font_context);
5623 let page = &pages[0];
5624
5625 let container_el = page
5626 .elements
5627 .iter()
5628 .find(|e| e.children.len() == 2)
5629 .expect("Should find container with two children");
5630
5631 let c1 = &container_el.children[0];
5632 let c2 = &container_el.children[1];
5633
5634 let total = c1.height + c2.height;
5638 assert!(
5639 (total - 300.0).abs() < 2.0,
5640 "Children should sum to ~300pt, got {}",
5641 total
5642 );
5643
5644 let ratio = c2.height / c1.height;
5647 assert!(
5648 ratio > 1.3 && ratio < 2.5,
5649 "child2/child1 ratio should be between 1.3 and 2.5, got {}",
5650 ratio
5651 );
5652 }
5653
5654 #[test]
5655 fn column_flex_grow_mixed_grow_and_fixed() {
5656 let engine = LayoutEngine::new();
5659 let font_context = FontContext::new();
5660
5661 let fixed_child = make_styled_view(
5662 Style {
5663 height: Some(Dimension::Pt(50.0)),
5664 ..Default::default()
5665 },
5666 vec![make_text("Fixed", 12.0)],
5667 );
5668 let grow_child = make_styled_view(
5669 Style {
5670 flex_grow: Some(1.0),
5671 ..Default::default()
5672 },
5673 vec![make_text("Grow", 12.0)],
5674 );
5675
5676 let container = make_styled_view(
5677 Style {
5678 flex_direction: Some(FlexDirection::Column),
5679 height: Some(Dimension::Pt(300.0)),
5680 ..Default::default()
5681 },
5682 vec![fixed_child, grow_child],
5683 );
5684
5685 let doc = Document {
5686 children: vec![Node::page(
5687 PageConfig::default(),
5688 Style::default(),
5689 vec![container],
5690 )],
5691 metadata: Default::default(),
5692 default_page: PageConfig::default(),
5693 fonts: vec![],
5694 tagged: false,
5695 pdfa: None,
5696 default_style: None,
5697 embedded_data: None,
5698 };
5699
5700 let pages = engine.layout(&doc, &font_context);
5701 let page = &pages[0];
5702
5703 let container_el = page
5704 .elements
5705 .iter()
5706 .find(|e| e.children.len() == 2)
5707 .expect("Should find container with two children");
5708
5709 let fixed_el = &container_el.children[0];
5710 let grow_el = &container_el.children[1];
5711
5712 assert!(
5714 (fixed_el.height - 50.0).abs() < 1.0,
5715 "Fixed child should stay at 50pt, got {}",
5716 fixed_el.height
5717 );
5718
5719 assert!(
5721 (grow_el.height - 250.0).abs() < 2.0,
5722 "Grow child should expand to ~250pt, got {}",
5723 grow_el.height
5724 );
5725 }
5726
5727 #[test]
5728 fn column_flex_grow_page_level() {
5729 let engine = LayoutEngine::new();
5731 let font_context = FontContext::new();
5732
5733 let grow_child = make_styled_view(
5734 Style {
5735 flex_grow: Some(1.0),
5736 ..Default::default()
5737 },
5738 vec![make_text("Fill page", 12.0)],
5739 );
5740
5741 let doc = Document {
5742 children: vec![Node::page(
5743 PageConfig::default(),
5744 Style::default(),
5745 vec![grow_child],
5746 )],
5747 metadata: Default::default(),
5748 default_page: PageConfig::default(),
5749 fonts: vec![],
5750 tagged: false,
5751 pdfa: None,
5752 default_style: None,
5753 embedded_data: None,
5754 };
5755
5756 let pages = engine.layout(&doc, &font_context);
5757 let page = &pages[0];
5758
5759 assert!(
5761 !page.elements.is_empty(),
5762 "Page should have at least one element"
5763 );
5764
5765 let content_height = page.height - page.config.margin.top - page.config.margin.bottom;
5766 let el = &page.elements[0];
5767 assert!(
5768 (el.height - content_height).abs() < 2.0,
5769 "Page-level flex-grow child should fill content height ({}), got {}",
5770 content_height,
5771 el.height
5772 );
5773 }
5774
5775 #[test]
5776 fn column_flex_grow_with_justify_content() {
5777 let engine = LayoutEngine::new();
5781 let font_context = FontContext::new();
5782
5783 let fixed_child = make_styled_view(
5784 Style {
5785 height: Some(Dimension::Pt(50.0)),
5786 ..Default::default()
5787 },
5788 vec![make_text("Top", 12.0)],
5789 );
5790 let grow_child = make_styled_view(
5791 Style {
5792 flex_grow: Some(1.0),
5793 ..Default::default()
5794 },
5795 vec![make_text("Fill", 12.0)],
5796 );
5797
5798 let container = make_styled_view(
5799 Style {
5800 flex_direction: Some(FlexDirection::Column),
5801 height: Some(Dimension::Pt(300.0)),
5802 justify_content: Some(JustifyContent::Center),
5803 ..Default::default()
5804 },
5805 vec![fixed_child, grow_child],
5806 );
5807
5808 let doc = Document {
5809 children: vec![Node::page(
5810 PageConfig::default(),
5811 Style::default(),
5812 vec![container],
5813 )],
5814 metadata: Default::default(),
5815 default_page: PageConfig::default(),
5816 fonts: vec![],
5817 tagged: false,
5818 pdfa: None,
5819 default_style: None,
5820 embedded_data: None,
5821 };
5822
5823 let pages = engine.layout(&doc, &font_context);
5824 let page = &pages[0];
5825
5826 let container_el = page
5827 .elements
5828 .iter()
5829 .find(|e| e.children.len() == 2)
5830 .expect("Should find container");
5831
5832 let first_child = &container_el.children[0];
5835 assert!(
5836 (first_child.y - container_el.y).abs() < 1.0,
5837 "First child should be at top of container"
5838 );
5839
5840 let total = container_el.children[0].height + container_el.children[1].height;
5842 assert!(
5843 (total - 300.0).abs() < 2.0,
5844 "Children should fill container, got {}",
5845 total
5846 );
5847 }
5848
5849 #[test]
5850 fn column_flex_grow_child_justify_content_center() {
5851 let engine = LayoutEngine::new();
5854 let font_context = FontContext::new();
5855
5856 let inner_box = make_styled_view(
5858 Style {
5859 height: Some(Dimension::Pt(40.0)),
5860 ..Default::default()
5861 },
5862 vec![make_text("Centered", 12.0)],
5863 );
5864
5865 let grow_child = make_styled_view(
5867 Style {
5868 flex_grow: Some(1.0),
5869 flex_direction: Some(FlexDirection::Column),
5870 justify_content: Some(JustifyContent::Center),
5871 ..Default::default()
5872 },
5873 vec![inner_box],
5874 );
5875
5876 let container = make_styled_view(
5878 Style {
5879 flex_direction: Some(FlexDirection::Column),
5880 height: Some(Dimension::Pt(400.0)),
5881 ..Default::default()
5882 },
5883 vec![grow_child],
5884 );
5885
5886 let doc = Document {
5887 children: vec![Node::page(
5888 PageConfig::default(),
5889 Style::default(),
5890 vec![container],
5891 )],
5892 metadata: Default::default(),
5893 default_page: PageConfig::default(),
5894 fonts: vec![],
5895 tagged: false,
5896 pdfa: None,
5897 default_style: None,
5898 embedded_data: None,
5899 };
5900
5901 let pages = engine.layout(&doc, &font_context);
5902 let page = &pages[0];
5903
5904 let container_el = page
5906 .elements
5907 .iter()
5908 .find(|e| e.height > 350.0 && e.children.len() == 1)
5909 .expect("Should find outer container");
5910
5911 let grow_el = &container_el.children[0];
5912 assert!(
5913 (grow_el.height - 400.0).abs() < 2.0,
5914 "Grow child should expand to 400, got {}",
5915 grow_el.height
5916 );
5917
5918 let inner_el = &grow_el.children[0];
5920 let expected_center = grow_el.y + grow_el.height / 2.0;
5921 let actual_center = inner_el.y + inner_el.height / 2.0;
5922 assert!(
5923 (actual_center - expected_center).abs() < 2.0,
5924 "Inner box should be vertically centered. Expected center ~{}, got ~{}",
5925 expected_center,
5926 actual_center
5927 );
5928 }
5929
5930 #[test]
5931 fn column_flex_grow_child_justify_content_flex_end() {
5932 let engine = LayoutEngine::new();
5934 let font_context = FontContext::new();
5935
5936 let inner_box = make_styled_view(
5937 Style {
5938 height: Some(Dimension::Pt(30.0)),
5939 ..Default::default()
5940 },
5941 vec![make_text("Bottom", 12.0)],
5942 );
5943
5944 let grow_child = make_styled_view(
5945 Style {
5946 flex_grow: Some(1.0),
5947 flex_direction: Some(FlexDirection::Column),
5948 justify_content: Some(JustifyContent::FlexEnd),
5949 ..Default::default()
5950 },
5951 vec![inner_box],
5952 );
5953
5954 let container = make_styled_view(
5955 Style {
5956 flex_direction: Some(FlexDirection::Column),
5957 height: Some(Dimension::Pt(300.0)),
5958 ..Default::default()
5959 },
5960 vec![grow_child],
5961 );
5962
5963 let doc = Document {
5964 children: vec![Node::page(
5965 PageConfig::default(),
5966 Style::default(),
5967 vec![container],
5968 )],
5969 metadata: Default::default(),
5970 default_page: PageConfig::default(),
5971 fonts: vec![],
5972 tagged: false,
5973 pdfa: None,
5974 default_style: None,
5975 embedded_data: None,
5976 };
5977
5978 let pages = engine.layout(&doc, &font_context);
5979 let page = &pages[0];
5980
5981 let container_el = page
5982 .elements
5983 .iter()
5984 .find(|e| e.height > 250.0 && e.children.len() == 1)
5985 .expect("Should find outer container");
5986
5987 let grow_el = &container_el.children[0];
5988 let inner_el = &grow_el.children[0];
5989
5990 let inner_bottom = inner_el.y + inner_el.height;
5992 let grow_bottom = grow_el.y + grow_el.height;
5993 assert!(
5994 (inner_bottom - grow_bottom).abs() < 2.0,
5995 "Inner box bottom ({}) should align with grow child bottom ({})",
5996 inner_bottom,
5997 grow_bottom
5998 );
5999 }
6000
6001 #[test]
6002 fn column_flex_grow_child_no_justify_unchanged() {
6003 let engine = LayoutEngine::new();
6005 let font_context = FontContext::new();
6006
6007 let inner_box = make_styled_view(
6008 Style {
6009 height: Some(Dimension::Pt(50.0)),
6010 ..Default::default()
6011 },
6012 vec![make_text("Top", 12.0)],
6013 );
6014
6015 let grow_child = make_styled_view(
6016 Style {
6017 flex_grow: Some(1.0),
6018 flex_direction: Some(FlexDirection::Column),
6019 ..Default::default()
6021 },
6022 vec![inner_box],
6023 );
6024
6025 let container = make_styled_view(
6026 Style {
6027 flex_direction: Some(FlexDirection::Column),
6028 height: Some(Dimension::Pt(300.0)),
6029 ..Default::default()
6030 },
6031 vec![grow_child],
6032 );
6033
6034 let doc = Document {
6035 children: vec![Node::page(
6036 PageConfig::default(),
6037 Style::default(),
6038 vec![container],
6039 )],
6040 metadata: Default::default(),
6041 default_page: PageConfig::default(),
6042 fonts: vec![],
6043 tagged: false,
6044 pdfa: None,
6045 default_style: None,
6046 embedded_data: None,
6047 };
6048
6049 let pages = engine.layout(&doc, &font_context);
6050 let page = &pages[0];
6051
6052 let container_el = page
6053 .elements
6054 .iter()
6055 .find(|e| e.height > 250.0 && e.children.len() == 1)
6056 .expect("Should find outer container");
6057
6058 let grow_el = &container_el.children[0];
6059 let inner_el = &grow_el.children[0];
6060
6061 assert!(
6063 (inner_el.y - grow_el.y).abs() < 2.0,
6064 "Inner box ({}) should be at top of grow child ({})",
6065 inner_el.y,
6066 grow_el.y
6067 );
6068 }
6069
6070 #[test]
6071 fn column_flex_grow_child_align_items_center() {
6072 let engine = LayoutEngine::new();
6074 let font_context = FontContext::new();
6075
6076 let text = make_text("Hello", 12.0);
6077
6078 let grow_child = make_styled_view(
6079 Style {
6080 flex_grow: Some(1.0),
6081 flex_direction: Some(FlexDirection::Column),
6082 align_items: Some(AlignItems::Center),
6083 ..Default::default()
6084 },
6085 vec![text],
6086 );
6087
6088 let container = make_styled_view(
6089 Style {
6090 flex_direction: Some(FlexDirection::Column),
6091 height: Some(Dimension::Pt(300.0)),
6092 ..Default::default()
6093 },
6094 vec![grow_child],
6095 );
6096
6097 let doc = Document {
6098 children: vec![Node::page(
6099 PageConfig::default(),
6100 Style::default(),
6101 vec![container],
6102 )],
6103 metadata: Default::default(),
6104 default_page: PageConfig::default(),
6105 fonts: vec![],
6106 tagged: false,
6107 pdfa: None,
6108 default_style: None,
6109 embedded_data: None,
6110 };
6111
6112 let pages = engine.layout(&doc, &font_context);
6113 let page = &pages[0];
6114
6115 let container_el = page
6116 .elements
6117 .iter()
6118 .find(|e| e.height > 250.0 && e.children.len() == 1)
6119 .expect("Should find outer container");
6120
6121 let grow_el = &container_el.children[0];
6122 assert!(
6123 !grow_el.children.is_empty(),
6124 "Grow child should have text child"
6125 );
6126
6127 let text_el = &grow_el.children[0];
6128 let text_center = text_el.x + text_el.width / 2.0;
6129 let grow_center = grow_el.x + grow_el.width / 2.0;
6130 assert!(
6131 (text_center - grow_center).abs() < 2.0,
6132 "Text center ({}) should be near grow child center ({})",
6133 text_center,
6134 grow_center
6135 );
6136 }
6137
6138 #[test]
6139 fn image_intrinsic_width_respects_height_constraint() {
6140 let engine = LayoutEngine::new();
6144 let font_context = FontContext::new();
6145
6146 let one_px_png = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
6148
6149 let image_node = Node {
6150 kind: NodeKind::Image {
6151 src: one_px_png.to_string(),
6152 width: None,
6153 height: Some(36.0),
6154 },
6155 style: Style::default(),
6156 children: vec![],
6157 id: None,
6158 source_location: None,
6159 bookmark: None,
6160 href: None,
6161 alt: None,
6162 };
6163
6164 let resolved = image_node.style.resolve(None, 0.0);
6165 let intrinsic = engine.measure_intrinsic_width(&image_node, &resolved, &font_context);
6166
6167 assert!(
6169 (intrinsic - 36.0).abs() < 1.0,
6170 "Intrinsic width should be ~36 for 1:1 aspect image with height 36, got {}",
6171 intrinsic
6172 );
6173 }
6174}