1use crate::error::Result;
6use crate::font::FontManager;
7use crate::output::{Color, FieldKind, FontId, GroupElement, MediaId, SourceSpan};
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct TabStop {
12 pub pos_pt: f64,
13 pub align: TabAlign,
14 pub leader: Option<TabLeader>,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Align {
20 Start,
21 Center,
22 End,
23 Justify,
24 Distribute,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum TabAlign {
30 Left,
31 Center,
32 Right,
33 Decimal,
34 Bar,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum TabLeader {
40 None,
41 Dot,
42 Hyphen,
43 Underscore,
44 Heavy,
45 MiddleDot,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Underline {
51 Single,
52 Words,
53 Double,
54 Thick,
55 Dotted,
56 Dash,
57 DotDash,
58 DotDotDash,
59 Wave,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
64pub enum LineSpacing {
65 Single,
66 Multiple(f64),
68 Exact(f64),
69 AtLeast(f64),
70}
71
72#[derive(Debug, Clone)]
74#[non_exhaustive]
75pub enum InlineItem {
76 Text(TextSegment),
78 Tab,
80 LineBreak,
82 PageBreak,
84 ColumnBreak,
86 Image {
88 width: f64,
89 height: f64,
90 media_id: MediaId,
91 },
92 Group {
94 width: f64,
95 height: f64,
96 group: GroupElement,
97 },
98 Marker(TextSegment),
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum NoteStream {
110 Footnote,
112 Endnote,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct NoteRef {
119 pub stream: NoteStream,
120 pub id: i32,
121}
122
123#[derive(Debug, Clone)]
125pub struct TextSegment {
126 pub text: String,
127 pub source: Option<SourceSpan>,
129 pub font_id: FontId,
130 pub font_size: f64,
131 pub glyph_ids: Vec<u16>,
132 pub advances: Vec<f64>,
133 pub width: f64,
134 pub ascent: f64,
135 pub descent: f64,
136 pub line_gap: f64,
138 pub color: Color,
139 pub bold: bool,
140 pub italic: bool,
141 pub underline: Option<Underline>,
143 pub strike: bool,
145 pub dstrike: bool,
147 pub highlight: Option<Color>,
149 pub baseline_offset: f64,
151 pub hyperlink_url: Option<String>,
153 pub field_kind: Option<FieldKind>,
155 pub note: Option<NoteRef>,
157}
158
159#[derive(Debug, Clone)]
161#[non_exhaustive]
162pub enum LineItem {
163 Text(TextSegment),
164 Tab {
165 width: f64,
166 leader: Option<TextSegment>,
168 },
169 Image {
170 width: f64,
171 height: f64,
172 media_id: MediaId,
173 },
174 Group {
175 width: f64,
176 height: f64,
177 group: GroupElement,
178 },
179 Marker(TextSegment),
180}
181
182impl LineItem {
183 pub fn width(&self) -> f64 {
184 match self {
185 LineItem::Text(seg) => seg.width,
186 LineItem::Tab { width, .. } => *width,
187 LineItem::Image { width, .. } => *width,
188 LineItem::Group { width, .. } => *width,
189 LineItem::Marker(seg) => seg.width,
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct LayoutLine {
197 pub items: Vec<LineItem>,
198 pub width: f64,
200 pub ascent: f64,
202 pub descent: f64,
204 pub line_gap: f64,
206 pub height: f64,
208 pub indent_left: f64,
210 pub available_width: f64,
212 pub is_last: bool,
214}
215
216impl LayoutLine {
217 pub fn baseline_offset(&self) -> f64 {
219 let leading = self.height - self.ascent - self.descent;
220 self.ascent + if leading >= 0.0 { leading / 2.0 } else { 0.0 }
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct LineBreakParams {
227 pub available_width: f64,
229 pub ind_left: f64,
231 pub ind_right: f64,
233 pub ind_first_line: f64,
235 pub ind_hanging: f64,
237 pub tab_stops: Vec<TabStop>,
239 pub line_spacing: LineSpacing,
241 pub jc: Option<Align>,
243 pub wrap: bool,
245 pub line_prefix_widths: Vec<f64>,
251 pub line_suffix_widths: Vec<f64>,
253}
254
255impl Default for LineBreakParams {
256 fn default() -> Self {
257 LineBreakParams {
258 available_width: 468.0, line_prefix_widths: Vec::new(),
260 line_suffix_widths: Vec::new(),
261 ind_left: 0.0,
262 ind_right: 0.0,
263 ind_first_line: 0.0,
264 ind_hanging: 0.0,
265 tab_stops: Vec::new(),
266 line_spacing: LineSpacing::Single,
267 jc: None,
268 wrap: true,
269 }
270 }
271}
272
273pub fn break_into_lines(
275 items: &[InlineItem],
276 params: &LineBreakParams,
277 fm: &FontManager,
278) -> Result<Vec<LayoutLine>> {
279 if items.is_empty() {
280 return Ok(vec![LayoutLine {
282 items: Vec::new(),
283 width: 0.0,
284 ascent: 0.0,
285 descent: 0.0,
286 line_gap: 0.0,
287 height: compute_line_height(0.0, 0.0, 0.0, 0.0, params),
288 indent_left: line_indent_at(params, 0, true),
289 available_width: line_width_at(params, 0, true),
290 is_last: true,
291 }]);
292 }
293
294 let mut lines: Vec<LayoutLine> = Vec::new();
295 let mut current_items: Vec<LineItem> = Vec::new();
296 let mut current_width: f64 = 0.0;
297 let mut current_ascent: f64 = 0.0;
298 let mut current_descent: f64 = 0.0;
299 let mut current_natural_height: f64 = 0.0;
300 let mut current_font_size: f64 = 0.0;
301 let mut line_index = 0usize;
304 let mut is_first_line = true;
305
306 let first_line_width = line_width_at(params, 0, true);
307
308 let mut line_avail = first_line_width;
309
310 let mut font_ctx: Option<(FontId, f64)> = None;
312 for item in items {
314 if let InlineItem::Text(seg) | InlineItem::Marker(seg) = item {
315 font_ctx = Some((seg.font_id, seg.font_size));
316 break;
317 }
318 }
319
320 let segments = build_breakable_segments(items, fm)?;
322
323 for seg in &segments {
324 match seg {
325 BreakableSegment::Items(seg_items) => {
326 let seg_width: f64 = seg_items.iter().map(inline_item_width).sum();
327
328 if params.wrap
329 && !current_items.is_empty()
330 && current_width + seg_width > line_avail + 0.01
331 {
332 let indent = line_indent_at(params, line_index, is_first_line);
334 let line_gap =
335 effective_line_gap(current_ascent, current_descent, current_natural_height);
336 lines.push(LayoutLine {
337 items: std::mem::take(&mut current_items),
338 width: current_width,
339 ascent: current_ascent,
340 descent: current_descent,
341 line_gap,
342 height: compute_line_height(
343 current_ascent,
344 current_descent,
345 line_gap,
346 current_font_size,
347 params,
348 ),
349 indent_left: indent,
350 available_width: line_avail,
351 is_last: false,
352 });
353 current_width = 0.0;
354 current_ascent = 0.0;
355 current_descent = 0.0;
356 current_natural_height = 0.0;
357 current_font_size = 0.0;
358 is_first_line = false;
359 line_index += 1;
360 line_avail = line_width_at(params, line_index, false);
361 }
362
363 for item in seg_items {
365 let (w, a, d, natural_height, font_size) = item_metrics(item);
366 current_width += w;
367 if a > current_ascent {
368 current_ascent = a;
369 }
370 if d > current_descent {
371 current_descent = d;
372 }
373 current_natural_height = current_natural_height.max(natural_height);
374 current_font_size = current_font_size.max(font_size);
375 if let InlineItem::Text(seg) | InlineItem::Marker(seg) = item {
377 font_ctx = Some((seg.font_id, seg.font_size));
378 }
379 current_items.push(inline_to_line_item(
380 item,
381 current_width,
382 ¶ms.tab_stops,
383 fm,
384 font_ctx,
385 ));
386 }
387 }
388 BreakableSegment::ForcedBreak(break_type) => {
389 let indent = line_indent_at(params, line_index, is_first_line);
390 let line_gap =
391 effective_line_gap(current_ascent, current_descent, current_natural_height);
392 lines.push(LayoutLine {
393 items: std::mem::take(&mut current_items),
394 width: current_width,
395 ascent: current_ascent,
396 descent: current_descent,
397 line_gap,
398 height: compute_line_height(
399 current_ascent,
400 current_descent,
401 line_gap,
402 current_font_size,
403 params,
404 ),
405 indent_left: indent,
406 available_width: line_avail,
407 is_last: matches!(break_type, ForcedBreakType::Page | ForcedBreakType::Column),
408 });
409 current_width = 0.0;
410 current_ascent = 0.0;
411 current_descent = 0.0;
412 current_natural_height = 0.0;
413 current_font_size = 0.0;
414 is_first_line = false;
415 line_index += 1;
416 line_avail = line_width_at(params, line_index, false);
417 }
418 }
419 }
420
421 let indent = line_indent_at(params, line_index, is_first_line);
423 let line_gap = effective_line_gap(current_ascent, current_descent, current_natural_height);
424 lines.push(LayoutLine {
425 items: current_items,
426 width: current_width,
427 ascent: current_ascent,
428 descent: current_descent,
429 line_gap,
430 height: compute_line_height(
431 current_ascent,
432 current_descent,
433 line_gap,
434 current_font_size,
435 params,
436 ),
437 indent_left: indent,
438 available_width: line_avail,
439 is_last: true,
440 });
441
442 Ok(lines)
443}
444
445#[derive(Debug)]
448enum BreakableSegment {
449 Items(Vec<InlineItem>),
451 ForcedBreak(ForcedBreakType),
453}
454
455#[derive(Debug)]
456enum ForcedBreakType {
457 Line,
458 Page,
459 Column,
460}
461
462fn build_breakable_segments(
468 items: &[InlineItem],
469 fm: &FontManager,
470) -> Result<Vec<BreakableSegment>> {
471 let mut segments = Vec::new();
472 let mut current_group: Vec<InlineItem> = Vec::new();
473
474 for item in items {
475 match item {
476 InlineItem::LineBreak => {
477 if !current_group.is_empty() {
478 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
479 }
480 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Line));
481 }
482 InlineItem::PageBreak => {
483 if !current_group.is_empty() {
484 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
485 }
486 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Page));
487 }
488 InlineItem::ColumnBreak => {
489 if !current_group.is_empty() {
490 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
491 }
492 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Column));
493 }
494 InlineItem::Tab => {
495 if !current_group.is_empty() {
497 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
498 }
499 segments.push(BreakableSegment::Items(vec![item.clone()]));
500 }
501 InlineItem::Text(seg) => {
502 if seg.text.is_empty() {
503 if !current_group.is_empty() {
504 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
505 }
506 segments.push(BreakableSegment::Items(vec![item.clone()]));
507 continue;
508 }
509 let breaks = split_text_at_break_opportunities(seg);
511
512 for tb in &breaks {
513 let chunk = &seg.text[tb.start..tb.end];
514 if chunk.is_empty() {
515 continue;
516 }
517
518 if !current_group.is_empty() && chunk.starts_with(|c: char| c.is_whitespace()) {
520 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
521 }
522
523 let sub_item = split_text_subsegment(seg, tb.start, tb.end, fm)?;
525 current_group.push(sub_item);
526
527 if tb.is_break {
528 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
529 }
530 }
531
532 if !current_group.is_empty() {
534 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
535 }
536 }
537 InlineItem::Marker(_) | InlineItem::Image { .. } | InlineItem::Group { .. } => {
538 current_group.push(item.clone());
539 }
540 }
541 }
542
543 if !current_group.is_empty() {
544 segments.push(BreakableSegment::Items(current_group));
545 }
546
547 Ok(segments)
548}
549
550fn split_text_subsegment(
554 seg: &TextSegment,
555 byte_start: usize,
556 byte_end: usize,
557 fm: &FontManager,
558) -> Result<InlineItem> {
559 if byte_start == 0 && byte_end == seg.text.len() {
561 return Ok(InlineItem::Text(seg.clone()));
562 }
563
564 let sub_text = seg.text[byte_start..byte_end].to_string();
565 let mut shaped = fm.shape_text(seg.font_id, &sub_text, seg.font_size)?;
566 let original = fm.shape_text(seg.font_id, &seg.text, seg.font_size)?;
567 let spacing = if original.advances.len() == seg.advances.len() && !original.advances.is_empty()
568 {
569 (seg.width - original.width) / original.advances.len() as f64
570 } else {
571 0.0
572 };
573 for advance in &mut shaped.advances {
574 *advance += spacing;
575 }
576 shaped.width += spacing * shaped.advances.len() as f64;
577
578 let source = seg.source.map(|source| {
579 let start = seg.text[..byte_start].chars().count() as u32;
580 let end = seg.text[..byte_end].chars().count() as u32;
581 SourceSpan {
582 node: source.node,
583 char_start: source.char_start + start,
584 char_end: source.char_start + end,
585 }
586 });
587
588 Ok(InlineItem::Text(TextSegment {
589 text: sub_text,
590 source,
591 font_id: seg.font_id,
592 font_size: seg.font_size,
593 glyph_ids: shaped.glyph_ids,
594 advances: shaped.advances,
595 width: shaped.width,
596 ascent: seg.ascent,
597 descent: seg.descent,
598 line_gap: seg.line_gap,
599 color: seg.color,
600 bold: seg.bold,
601 italic: seg.italic,
602 underline: seg.underline,
603 strike: seg.strike,
604 dstrike: seg.dstrike,
605 highlight: seg.highlight,
606 baseline_offset: seg.baseline_offset,
607 hyperlink_url: seg.hyperlink_url.clone(),
608 field_kind: seg.field_kind,
609 note: seg.note,
610 }))
611}
612
613struct TextBreakInfo {
614 start: usize,
616 end: usize,
617 is_break: bool,
619}
620
621fn split_text_at_break_opportunities(seg: &TextSegment) -> Vec<TextBreakInfo> {
622 use unicode_linebreak::{BreakOpportunity, linebreaks};
623
624 let text = &seg.text;
625 if text.is_empty() {
626 return vec![];
627 }
628
629 let mut breaks = Vec::new();
630 let mut last_start = 0;
631
632 for (byte_pos, opportunity) in linebreaks(text) {
633 if byte_pos == 0 {
634 continue;
635 }
636
637 let is_break = matches!(
638 opportunity,
639 BreakOpportunity::Allowed | BreakOpportunity::Mandatory
640 );
641
642 breaks.push(TextBreakInfo {
643 start: last_start,
644 end: byte_pos,
645 is_break,
646 });
647 last_start = byte_pos;
648 }
649
650 if breaks.is_empty() {
652 breaks.push(TextBreakInfo {
653 start: 0,
654 end: text.len(),
655 is_break: true,
656 });
657 }
658
659 breaks
660}
661
662fn inline_item_width(item: &InlineItem) -> f64 {
663 match item {
664 InlineItem::Text(seg) => seg.width,
665 InlineItem::Tab => 36.0, InlineItem::Image { width, .. } => *width,
667 InlineItem::Group { width, .. } => *width,
668 InlineItem::Marker(seg) => seg.width,
669 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => 0.0,
670 }
671}
672
673fn item_metrics(item: &InlineItem) -> (f64, f64, f64, f64, f64) {
674 match item {
676 InlineItem::Text(seg) => (
677 seg.width,
678 seg.ascent,
679 seg.descent,
680 seg.ascent + seg.descent + seg.line_gap,
681 seg.font_size,
682 ),
683 InlineItem::Marker(seg) => (
684 seg.width,
685 seg.ascent,
686 seg.descent,
687 seg.ascent + seg.descent + seg.line_gap,
688 0.0,
689 ),
690 InlineItem::Tab => (36.0, 0.0, 0.0, 0.0, 0.0),
691 InlineItem::Image { width, height, .. } => (*width, *height, 0.0, *height, 0.0),
692 InlineItem::Group { width, height, .. } => (*width, *height, 0.0, *height, 0.0),
693 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => {
694 (0.0, 0.0, 0.0, 0.0, 0.0)
695 }
696 }
697}
698
699fn inline_to_line_item(
700 item: &InlineItem,
701 current_x: f64,
702 tab_stops: &[TabStop],
703 fm: &FontManager,
704 font_ctx: Option<(FontId, f64)>,
705) -> LineItem {
706 match item {
707 InlineItem::Text(seg) => LineItem::Text(seg.clone()),
708 InlineItem::Marker(seg) => LineItem::Marker(seg.clone()),
709 InlineItem::Tab => {
710 let (tab_width, leader_char) = resolve_tab_width(current_x, tab_stops);
711 let leader = leader_char.and_then(|ch| shape_leader(fm, font_ctx, ch, tab_width));
712 LineItem::Tab {
713 width: tab_width,
714 leader,
715 }
716 }
717 InlineItem::Image {
718 width,
719 height,
720 media_id,
721 } => LineItem::Image {
722 width: *width,
723 height: *height,
724 media_id: *media_id,
725 },
726 InlineItem::Group {
727 width,
728 height,
729 group,
730 } => LineItem::Group {
731 width: *width,
732 height: *height,
733 group: group.clone(),
734 },
735 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => LineItem::Tab {
736 width: 0.0,
737 leader: None,
738 },
739 }
740}
741
742fn shape_leader(
744 fm: &FontManager,
745 font_ctx: Option<(FontId, f64)>,
746 leader_char: char,
747 tab_width: f64,
748) -> Option<TextSegment> {
749 let (font_id, font_size) = font_ctx?;
750 if tab_width < 1.0 {
751 return None;
752 }
753
754 let single = String::from(leader_char);
756 let shaped = fm.shape_text(font_id, &single, font_size).ok()?;
757 if shaped.glyph_ids.is_empty() {
758 return None;
759 }
760 let char_advance = shaped.advances[0];
761 if char_advance < 0.5 {
762 return None;
763 }
764
765 let spacing = match leader_char {
767 '.' | '\u{00B7}' => char_advance * 0.5,
768 _ => char_advance * 0.15,
769 };
770 let step = char_advance + spacing;
771 let count = ((tab_width - spacing) / step).floor() as usize;
772 if count == 0 {
773 return None;
774 }
775
776 let leader_text: String = std::iter::repeat_n(leader_char, count).collect();
778 let mut glyph_ids = Vec::with_capacity(count);
779 let mut advances = Vec::with_capacity(count);
780 for i in 0..count {
781 glyph_ids.push(shaped.glyph_ids[0]);
782 if i + 1 < count {
783 advances.push(char_advance + spacing);
784 } else {
785 advances.push(char_advance);
786 }
787 }
788
789 let metrics = fm.metrics(font_id, font_size).ok()?;
790
791 Some(TextSegment {
792 text: leader_text,
793 source: None,
794 font_id,
795 font_size,
796 glyph_ids,
797 advances,
798 width: tab_width, ascent: metrics.ascent,
800 descent: metrics.descent,
801 line_gap: metrics.line_gap,
802 color: Color::BLACK,
803 bold: false,
804 italic: false,
805 underline: None,
806 strike: false,
807 dstrike: false,
808 highlight: None,
809 baseline_offset: 0.0,
810 hyperlink_url: None,
811 field_kind: None,
812 note: None,
813 })
814}
815
816fn resolve_tab_width(current_x: f64, tab_stops: &[TabStop]) -> (f64, Option<char>) {
818 for stop in tab_stops {
820 let stop_pos = stop.pos_pt;
821 if stop_pos > current_x {
822 let width = match stop.align {
823 TabAlign::Left => stop_pos - current_x,
824 TabAlign::Center => (stop_pos - current_x).max(0.0),
825 TabAlign::Right => (stop_pos - current_x).max(0.0),
826 _ => stop_pos - current_x,
827 };
828 let leader = stop.leader.and_then(|l| match l {
829 TabLeader::Dot => Some('.'),
830 TabLeader::Hyphen => Some('-'),
831 TabLeader::Underscore => Some('_'),
832 TabLeader::MiddleDot => Some('\u{00B7}'),
833 TabLeader::Heavy => Some('_'),
834 TabLeader::None => None,
835 });
836 return (width, leader);
837 }
838 }
839 let default_interval = 36.0;
841 let next_stop = ((current_x / default_interval).floor() + 1.0) * default_interval;
842 (next_stop - current_x, None)
843}
844
845fn compute_first_line_width(params: &LineBreakParams) -> f64 {
846 if params.ind_hanging > 0.0 {
847 params.available_width - params.ind_left - params.ind_right + params.ind_hanging
849 } else {
850 params.available_width - params.ind_left - params.ind_right - params.ind_first_line
851 }
852}
853
854fn compute_subsequent_line_width(params: &LineBreakParams) -> f64 {
855 params.available_width - params.ind_left - params.ind_right
856}
857
858fn line_prefix_width(params: &LineBreakParams, line_index: usize) -> f64 {
860 params
861 .line_prefix_widths
862 .get(line_index)
863 .copied()
864 .unwrap_or(0.0)
865}
866
867fn line_suffix_width(params: &LineBreakParams, line_index: usize) -> f64 {
869 params
870 .line_suffix_widths
871 .get(line_index)
872 .copied()
873 .unwrap_or(0.0)
874}
875
876fn line_width_at(params: &LineBreakParams, line_index: usize, is_first_line: bool) -> f64 {
878 let base = if is_first_line {
879 compute_first_line_width(params)
880 } else {
881 compute_subsequent_line_width(params)
882 };
883 (base - line_prefix_width(params, line_index) - line_suffix_width(params, line_index)).max(0.0)
884}
885
886fn line_indent_at(params: &LineBreakParams, line_index: usize, is_first_line: bool) -> f64 {
888 let base = if is_first_line {
889 first_line_indent(params)
890 } else {
891 subsequent_line_indent(params)
892 };
893 base + line_prefix_width(params, line_index)
894}
895
896fn first_line_indent(params: &LineBreakParams) -> f64 {
897 if params.ind_hanging > 0.0 {
898 params.ind_left - params.ind_hanging
899 } else {
900 params.ind_left + params.ind_first_line
901 }
902}
903
904fn subsequent_line_indent(params: &LineBreakParams) -> f64 {
905 params.ind_left
906}
907
908fn effective_line_gap(ascent: f64, descent: f64, natural_height: f64) -> f64 {
910 (natural_height - ascent - descent).max(0.0)
911}
912
913fn compute_line_height(
914 ascent: f64,
915 descent: f64,
916 line_gap: f64,
917 font_size: f64,
918 params: &LineBreakParams,
919) -> f64 {
920 let natural = ascent + descent + line_gap;
921 let natural = if natural < 1.0 { 12.0 } else { natural }; let font_size = if font_size < 1.0 { 12.0 } else { font_size };
923
924 match params.line_spacing {
925 LineSpacing::Single => natural,
926 LineSpacing::Multiple(factor) => font_size * factor,
927 LineSpacing::Exact(points) => points,
928 LineSpacing::AtLeast(points) => natural.max(points),
929 }
930}
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935
936 fn make_text_segment(text: &str, width: f64) -> TextSegment {
937 TextSegment {
938 text: text.to_string(),
939 source: None,
940 font_id: FontId(0),
941 font_size: 12.0,
942 glyph_ids: vec![],
943 advances: vec![],
944 width,
945 ascent: 10.0,
946 descent: 3.0,
947 line_gap: 0.0,
948 color: Color::BLACK,
949 bold: false,
950 italic: false,
951 underline: None,
952 strike: false,
953 dstrike: false,
954 highlight: None,
955 baseline_offset: 0.0,
956 hyperlink_url: None,
957 field_kind: None,
958 note: None,
959 }
960 }
961
962 fn deterministic_font_manager() -> FontManager {
963 FontManager::new_deterministic().expect("bundled fonts should load")
964 }
965
966 fn shaped_text_segment(fm: &mut FontManager, text: &str, spacing: f64) -> TextSegment {
967 let font_id = fm
968 .resolve_font(Some("Carlito"), false, false)
969 .expect("bundled Carlito should resolve");
970 let metrics = fm.metrics(font_id, 30.0).expect("Carlito metrics");
971 let mut shaped = fm.shape_text(font_id, text, 30.0).expect("shape text");
972 for advance in &mut shaped.advances {
973 *advance += spacing;
974 }
975 shaped.width += spacing * shaped.advances.len() as f64;
976 TextSegment {
977 text: text.to_owned(),
978 source: None,
979 font_id,
980 font_size: 30.0,
981 glyph_ids: shaped.glyph_ids,
982 advances: shaped.advances,
983 width: shaped.width,
984 ascent: metrics.ascent,
985 descent: metrics.descent,
986 line_gap: metrics.line_gap,
987 color: Color::BLACK,
988 bold: false,
989 italic: false,
990 underline: None,
991 strike: false,
992 dstrike: false,
993 highlight: None,
994 baseline_offset: 0.0,
995 hyperlink_url: None,
996 field_kind: None,
997 note: None,
998 }
999 }
1000
1001 #[test]
1002 fn empty_paragraph_gets_one_line() {
1003 let fm = deterministic_font_manager();
1004 let lines = break_into_lines(&[], &LineBreakParams::default(), &fm).unwrap();
1005 assert_eq!(lines.len(), 1);
1006 assert!(lines[0].is_last);
1007 assert!(lines[0].items.is_empty());
1008 }
1009
1010 #[test]
1011 fn single_word_fits_one_line() {
1012 let fm = deterministic_font_manager();
1013 let items = vec![InlineItem::Text(make_text_segment("Hello", 50.0))];
1014 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
1015 assert_eq!(lines.len(), 1);
1016 assert!(lines[0].is_last);
1017 }
1018
1019 #[test]
1020 fn words_wrap_to_multiple_lines() {
1021 let fm = deterministic_font_manager();
1022 let mut items = vec![
1024 InlineItem::Text(make_text_segment("Word1", 200.0)),
1025 InlineItem::Text(make_text_segment("Word2", 200.0)),
1026 ];
1027 items.push(InlineItem::Text(make_text_segment("Word3", 200.0)));
1028
1029 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
1030 assert!(lines.len() >= 2);
1031 }
1032
1033 #[test]
1034 fn ligature_runs_reshape_each_break_chunk_without_duplicate_glyphs() {
1035 let mut fm = deterministic_font_manager();
1036 let text = "by providing opportunities to crawl in cluttered spaces and handle 3-dimensional objects";
1037 let spacing = 0.4;
1038 let segment = shaped_text_segment(&mut fm, text, spacing);
1039 assert_ne!(segment.glyph_ids.len(), text.chars().count());
1040
1041 let lines = break_into_lines(
1042 &[InlineItem::Text(segment)],
1043 &LineBreakParams {
1044 available_width: 260.0,
1045 ..LineBreakParams::default()
1046 },
1047 &fm,
1048 )
1049 .expect("wrap ligature-bearing text");
1050 assert!(lines.len() > 1);
1051
1052 let mut rendered_text = String::new();
1053 for text_segment in lines.iter().flat_map(|line| {
1054 line.items.iter().filter_map(|item| match item {
1055 LineItem::Text(segment) => Some(segment),
1056 _ => None,
1057 })
1058 }) {
1059 rendered_text.push_str(&text_segment.text);
1060 let exact = fm
1061 .shape_text(
1062 text_segment.font_id,
1063 &text_segment.text,
1064 text_segment.font_size,
1065 )
1066 .expect("reshape emitted chunk");
1067 assert_eq!(text_segment.glyph_ids, exact.glyph_ids);
1068 assert_eq!(text_segment.advances.len(), exact.advances.len());
1069 for (actual, unspaced) in text_segment.advances.iter().zip(exact.advances) {
1070 assert!((actual - (unspaced + spacing)).abs() < 1.0e-10);
1071 }
1072 }
1073 assert_eq!(rendered_text, text);
1074 }
1075
1076 #[test]
1077 fn line_splitting_preserves_contiguous_unicode_source_ranges() {
1078 let mut fm = deterministic_font_manager();
1079 let node = crate::SourceNodeId::new(7).expect("a non-zero source id");
1080 let mut segment = shaped_text_segment(&mut fm, "ab 🚀界 cd", 0.0);
1081 segment.source = Some(crate::SourceSpan {
1082 node,
1083 char_start: 11,
1084 char_end: 19,
1085 });
1086
1087 let lines = break_into_lines(
1088 &[InlineItem::Text(segment)],
1089 &LineBreakParams {
1090 available_width: 55.0,
1091 ..LineBreakParams::default()
1092 },
1093 &fm,
1094 )
1095 .expect("split mixed Unicode text");
1096 let sourced = lines
1097 .iter()
1098 .flat_map(|line| &line.items)
1099 .filter_map(|item| match item {
1100 LineItem::Text(segment) => segment.source,
1101 _ => None,
1102 })
1103 .collect::<Vec<_>>();
1104
1105 assert!(sourced.len() > 1, "the fixture must cross a line boundary");
1106 assert_eq!(sourced.first().expect("first range").char_start, 11);
1107 assert_eq!(sourced.last().expect("last range").char_end, 19);
1108 for pair in sourced.windows(2) {
1109 assert_eq!(pair[0].node, node);
1110 assert_eq!(pair[0].char_end, pair[1].char_start);
1111 }
1112 }
1113
1114 #[test]
1115 fn forced_line_break() {
1116 let fm = deterministic_font_manager();
1117 let items = vec![
1118 InlineItem::Text(make_text_segment("Before", 50.0)),
1119 InlineItem::LineBreak,
1120 InlineItem::Text(make_text_segment("After", 50.0)),
1121 ];
1122 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
1123 assert!(lines.len() >= 2);
1124 }
1125
1126 #[test]
1127 fn line_height_exact() {
1128 let params = LineBreakParams {
1129 line_spacing: LineSpacing::Exact(24.0),
1130 ..Default::default()
1131 };
1132 let h = compute_line_height(10.0, 3.0, 5.0, 12.0, ¶ms);
1133 assert!((h - 24.0).abs() < 0.01);
1134 }
1135
1136 #[test]
1137 fn line_height_auto() {
1138 let params = LineBreakParams {
1139 line_spacing: LineSpacing::Multiple(2.0),
1140 ..Default::default()
1141 };
1142 let h = compute_line_height(10.0, 3.0, 2.0, 15.0, ¶ms);
1143 assert!((h - 30.0).abs() < 0.01); }
1145
1146 #[test]
1147 fn first_line_indent() {
1148 let params = LineBreakParams {
1149 ind_first_line: 36.0,
1150 ..Default::default()
1151 };
1152 let first_w = compute_first_line_width(¶ms);
1153 let subseq_w = compute_subsequent_line_width(¶ms);
1154 assert!(first_w < subseq_w);
1155 }
1156
1157 #[test]
1158 fn hanging_indent() {
1159 let params = LineBreakParams {
1160 ind_left: 36.0,
1161 ind_hanging: 36.0,
1162 ..Default::default()
1163 };
1164 let first_indent = super::first_line_indent(¶ms);
1165 let subseq_indent = super::subsequent_line_indent(¶ms);
1166 assert!(first_indent < subseq_indent);
1167 }
1168
1169 #[test]
1170 fn tab_stop_resolution() {
1171 let stops = vec![TabStop {
1172 pos_pt: 72.0,
1173 align: TabAlign::Left,
1174 leader: None,
1175 }];
1176 let (w, leader) = resolve_tab_width(36.0, &stops);
1177 assert!((w - 36.0).abs() < 0.01);
1178 assert!(leader.is_none());
1179 }
1180
1181 #[test]
1182 fn default_tab_stops() {
1183 let (w, _) = resolve_tab_width(10.0, &[]);
1184 assert!((w - 26.0).abs() < 0.01); }
1186
1187 #[test]
1188 fn tab_stop_with_dot_leader() {
1189 let stops = vec![TabStop {
1190 pos_pt: 400.0,
1191 align: TabAlign::Right,
1192 leader: Some(TabLeader::Dot),
1193 }];
1194 let (w, leader) = resolve_tab_width(100.0, &stops);
1195 assert!((w - 300.0).abs() < 0.01);
1196 assert_eq!(leader, Some('.'));
1197 }
1198
1199 #[test]
1200 fn the_eleven_line_tests_pass_with_owned_types() {
1201 assert!(LineBreakParams::default().wrap);
1202 empty_paragraph_gets_one_line();
1203 single_word_fits_one_line();
1204 words_wrap_to_multiple_lines();
1205 forced_line_break();
1206 line_height_exact();
1207 line_height_auto();
1208 first_line_indent();
1209 hanging_indent();
1210 tab_stop_resolution();
1211 default_tab_stops();
1212 tab_stop_with_dot_leader();
1213 }
1214
1215 #[test]
1216 fn line_spacing_variants_preserve_existing_height_rules() {
1217 let height = |line_spacing| {
1218 compute_line_height(
1219 10.0,
1220 3.0,
1221 2.0,
1222 11.0,
1223 &LineBreakParams {
1224 line_spacing,
1225 ..Default::default()
1226 },
1227 )
1228 };
1229
1230 assert!((height(LineSpacing::Single) - 15.0).abs() < 0.01);
1231 assert!((height(LineSpacing::Multiple(1.5)) - 16.5).abs() < 0.01);
1232 assert!((height(LineSpacing::Exact(8.25)) - 8.25).abs() < 0.01);
1233 assert!((height(LineSpacing::AtLeast(8.25)) - 15.0).abs() < 0.01);
1234 assert!((height(LineSpacing::AtLeast(18.5)) - 18.5).abs() < 0.01);
1235 }
1236
1237 #[test]
1238 fn mixed_font_line_uses_tallest_full_natural_advance() {
1239 let fm = deterministic_font_manager();
1240 let mut first = make_text_segment("first", 20.0);
1241 first.ascent = 10.0;
1242 first.descent = 2.0;
1243 first.line_gap = 4.0;
1244 let mut second = make_text_segment("second", 20.0);
1245 second.ascent = 8.0;
1246 second.descent = 5.0;
1247 second.line_gap = 1.0;
1248
1249 let lines = break_into_lines(
1250 &[InlineItem::Text(first), InlineItem::Text(second)],
1251 &LineBreakParams::default(),
1252 &fm,
1253 )
1254 .expect("lay out mixed-font line");
1255
1256 assert_eq!(lines.len(), 1);
1257 assert!((lines[0].ascent - 10.0).abs() < 0.01);
1258 assert!((lines[0].descent - 5.0).abs() < 0.01);
1259 assert!((lines[0].line_gap - 1.0).abs() < 0.01);
1260 assert!((lines[0].height - 16.0).abs() < 0.01);
1261 assert!((lines[0].baseline_offset() - 10.5).abs() < 0.01);
1262 }
1263
1264 #[test]
1265 fn multiple_spacing_uses_largest_text_point_size_on_each_line() {
1266 let fm = deterministic_font_manager();
1267 let mut first = make_text_segment("first", 20.0);
1268 first.font_size = 12.0;
1269 first.line_gap = 4.0;
1270 let mut second = make_text_segment("second", 20.0);
1271 second.font_size = 20.0;
1272 second.line_gap = 1.0;
1273
1274 let lines = break_into_lines(
1275 &[InlineItem::Text(first), InlineItem::Text(second)],
1276 &LineBreakParams {
1277 line_spacing: LineSpacing::Multiple(1.25),
1278 ..LineBreakParams::default()
1279 },
1280 &fm,
1281 )
1282 .expect("lay out percentage-spaced mixed-size line");
1283
1284 assert_eq!(lines.len(), 1);
1285 assert!((lines[0].height - 25.0).abs() < 0.01);
1286 }
1287
1288 #[test]
1289 fn positive_leading_is_split_and_below_natural_exact_spacing_is_not_clamped() {
1290 let positive = LayoutLine {
1291 items: Vec::new(),
1292 width: 0.0,
1293 ascent: 10.0,
1294 descent: 3.0,
1295 line_gap: 5.0,
1296 height: 18.0,
1297 indent_left: 0.0,
1298 available_width: 100.0,
1299 is_last: true,
1300 };
1301 let below_natural = LayoutLine {
1302 height: 8.0,
1303 ..positive.clone()
1304 };
1305
1306 assert!((positive.baseline_offset() - 12.5).abs() < 0.01);
1307 assert!((below_natural.height - 8.0).abs() < 0.01);
1308 assert!((below_natural.baseline_offset() - 10.0).abs() < 0.01);
1309 }
1310
1311 #[test]
1312 fn zero_gap_and_empty_segment_preserve_natural_height_rules() {
1313 let fm = deterministic_font_manager();
1314 let zero_gap = make_text_segment("zero", 20.0);
1315 let mut empty = make_text_segment("", 0.0);
1316 empty.line_gap = 4.0;
1317
1318 let zero_gap_line = break_into_lines(
1319 &[InlineItem::Text(zero_gap)],
1320 &LineBreakParams::default(),
1321 &fm,
1322 )
1323 .expect("lay out zero-gap line");
1324 let empty_line =
1325 break_into_lines(&[InlineItem::Text(empty)], &LineBreakParams::default(), &fm)
1326 .expect("lay out styled empty line");
1327
1328 assert!((zero_gap_line[0].height - 13.0).abs() < 0.01);
1329 assert!((empty_line[0].height - 17.0).abs() < 0.01);
1330 }
1331
1332 #[test]
1333 fn wrap_false_only_breaks_on_an_explicit_break() {
1334 let fm = deterministic_font_manager();
1335 let params = LineBreakParams {
1336 available_width: 100.0,
1337 wrap: false,
1338 ..Default::default()
1339 };
1340
1341 for forced_break in [
1342 InlineItem::LineBreak,
1343 InlineItem::PageBreak,
1344 InlineItem::ColumnBreak,
1345 ] {
1346 let items = vec![
1347 InlineItem::Text(make_text_segment("one", 80.0)),
1348 InlineItem::Text(make_text_segment("two", 80.0)),
1349 forced_break,
1350 InlineItem::Text(make_text_segment("three", 80.0)),
1351 InlineItem::Text(make_text_segment("four", 80.0)),
1352 ];
1353 let lines = break_into_lines(&items, ¶ms, &fm).unwrap();
1354
1355 assert_eq!(lines.len(), 2);
1356 assert!((lines[0].width - 160.0).abs() < 0.01);
1357 assert!((lines[1].width - 160.0).abs() < 0.01);
1358 }
1359 }
1360
1361 #[test]
1362 fn tab_stops_use_point_positions_and_owned_leaders() {
1363 let mut fm = deterministic_font_manager();
1364 let font_id = fm
1365 .resolve_font(Some("Carlito"), false, false)
1366 .expect("bundled Carlito should resolve");
1367 let stop = TabStop {
1368 pos_pt: 72.25,
1369 align: TabAlign::Decimal,
1370 leader: Some(TabLeader::Dot),
1371 };
1372
1373 let item = inline_to_line_item(&InlineItem::Tab, 12.0, &[stop], &fm, Some((font_id, 12.0)));
1374
1375 let LineItem::Tab {
1376 width,
1377 leader: Some(leader),
1378 } = item
1379 else {
1380 panic!("owned dot leader should shape into a tab line item");
1381 };
1382 assert!((width - 60.25).abs() < 0.01);
1383 assert!((leader.width - 60.25).abs() < 0.01);
1384 assert!(!leader.glyph_ids.is_empty());
1385 assert!(leader.text.chars().all(|ch| ch == '.'));
1386 }
1387
1388 #[test]
1389 fn staged_image_types_use_media_id_instead_of_embed_id() {
1390 let media_id = crate::MediaId::from_bytes(b"image");
1391 let item = inline_to_line_item(
1392 &InlineItem::Image {
1393 width: 10.0,
1394 height: 20.0,
1395 media_id,
1396 },
1397 0.0,
1398 &[],
1399 &deterministic_font_manager(),
1400 None,
1401 );
1402 let LineItem::Image {
1403 media_id: actual, ..
1404 } = item
1405 else {
1406 panic!("image should remain an image");
1407 };
1408 assert_eq!(actual, media_id);
1409 }
1410
1411 #[test]
1412 fn group_inline_item_breaks_and_positions_like_an_image() {
1413 use crate::{GroupElement, PositionedElement, Transform};
1414
1415 let group = GroupElement {
1416 transform: Transform::IDENTITY,
1417 clip: None,
1418 opacity: 1.0,
1419 effects: Vec::new(),
1420 children: vec![PositionedElement::FilledRect {
1421 rect: crate::Rect {
1422 x: 2.0,
1423 y: 3.0,
1424 width: 4.0,
1425 height: 5.0,
1426 },
1427 color: crate::Color::BLACK,
1428 }],
1429 };
1430 let items = vec![InlineItem::Group {
1431 width: 80.0,
1432 height: 40.0,
1433 group: group.clone(),
1434 }];
1435 let lines = break_into_lines(
1436 &items,
1437 &LineBreakParams {
1438 available_width: 80.0,
1439 ..Default::default()
1440 },
1441 &deterministic_font_manager(),
1442 )
1443 .expect("group line breaking");
1444
1445 assert_eq!(lines.len(), 1);
1446 assert_eq!(lines[0].width, 80.0);
1447 assert_eq!(lines[0].height, 40.0);
1448 let LineItem::Group {
1449 width,
1450 height,
1451 group: actual,
1452 } = &lines[0].items[0]
1453 else {
1454 panic!("inline group should remain a group line item");
1455 };
1456 assert_eq!((*width, *height), (80.0, 40.0));
1457 assert_eq!(actual, &group);
1458 }
1459}