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