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