1use std::collections::HashSet;
2use std::ops::Range;
3use std::sync::OnceLock;
4
5use icu_segmenter::LineSegmenter;
6use icu_segmenter::options::LineBreakOptions;
7
8use crate::layout::line::{LayoutLine, PositionedRun, RunDecorations};
9use crate::shaping::run::{ShapedGlyph, ShapedRun};
10use crate::shaping::shaper::{FontMetricsPx, TextDirection};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum RunOrder {
21 AlreadyVisual,
23 Logical(TextDirection),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum BreakOpportunity {
36 Allowed,
37 Mandatory,
38}
39
40fn line_segmenter() -> icu_segmenter::LineSegmenterBorrowed<'static> {
45 static CELL: OnceLock<icu_segmenter::LineSegmenterBorrowed<'static>> = OnceLock::new();
46 *CELL.get_or_init(|| LineSegmenter::new_auto(LineBreakOptions::default()))
47}
48
49fn is_mandatory_break_at(text: &str, byte_offset: usize) -> bool {
55 if byte_offset == 0 {
56 return false;
57 }
58 let preceding = &text[..byte_offset];
59 matches!(
60 preceding.chars().next_back(),
61 Some('\n' | '\r' | '\u{0085}' | '\u{000B}' | '\u{000C}' | '\u{2028}' | '\u{2029}')
62 )
63}
64
65fn enumerate_breaks(text: &str) -> Vec<(usize, BreakOpportunity)> {
69 line_segmenter()
70 .segment_str(text)
71 .map(|byte_offset| {
72 let kind = if is_mandatory_break_at(text, byte_offset) {
73 BreakOpportunity::Mandatory
74 } else {
75 BreakOpportunity::Allowed
76 };
77 (byte_offset, kind)
78 })
79 .collect()
80}
81
82fn hyphenation_breaks(text: &str, lang_code: [u8; 2]) -> Vec<usize> {
93 use hypher::hyphenate;
94
95 let mut offsets = Vec::new();
96
97 for (idx, ch) in text.char_indices() {
99 if ch == '\u{00AD}' {
100 offsets.push(idx + ch.len_utf8());
101 }
102 }
103
104 if let Some(lang) = hypher::Lang::from_iso(lang_code) {
106 let mut word_start: Option<usize> = None;
107 let flush = |start: usize, end: usize, offsets: &mut Vec<usize>| {
108 let word = &text[start..end];
109 if word.chars().count() < 5 {
112 return;
113 }
114 let mut pos = start;
115 let mut syllables = hyphenate(word, lang).peekable();
116 while let Some(syl) = syllables.next() {
117 pos += syl.len();
118 if syllables.peek().is_some() {
120 offsets.push(pos);
121 }
122 }
123 };
124 for (idx, ch) in text.char_indices() {
125 if ch.is_alphabetic() {
126 word_start.get_or_insert(idx);
127 } else if let Some(start) = word_start.take() {
128 flush(start, idx, &mut offsets);
129 }
130 }
131 if let Some(start) = word_start.take() {
132 flush(start, text.len(), &mut offsets);
133 }
134 }
135
136 offsets.sort_unstable();
137 offsets.dedup();
138 offsets
139}
140
141fn map_offsets_to_glyph_indices(flat: &[FlatGlyph], offsets: &[usize]) -> HashSet<usize> {
144 let mut set = HashSet::new();
145 let mut cursor = 0usize;
146 for &byte_offset in offsets {
147 while cursor < flat.len() && (flat[cursor].cluster as usize) < byte_offset {
148 cursor += 1;
149 }
150 set.insert(cursor.min(flat.len()));
151 }
152 set
153}
154
155fn append_hyphen(line: &mut LayoutLine, hyphen: &ShapedGlyph) {
160 if let Some(run) = line.runs.last_mut() {
161 let mut g = hyphen.clone();
162 g.cluster = run
163 .shaped_run
164 .glyphs
165 .last()
166 .map(|gl| gl.cluster)
167 .unwrap_or(0);
168 run.shaped_run.glyphs.push(g);
169 run.shaped_run.advance_width += hyphen.x_advance;
170 line.width += hyphen.x_advance;
171 }
172}
173
174fn byte_offset_to_char_offset(text: &str, byte_offset: usize) -> usize {
182 let mut off = byte_offset.min(text.len());
183 while off > 0 && !text.is_char_boundary(off) {
184 off -= 1;
185 }
186 text[..off].chars().count()
187}
188
189pub struct Hyphenator {
193 pub glyph: ShapedGlyph,
195 pub language: [u8; 2],
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
207pub enum Alignment {
208 #[default]
210 Start,
211 End,
213 Left,
214 Right,
215 Center,
216 Justify,
217}
218
219impl Alignment {
220 pub fn resolve_for(self, base: TextDirection) -> Alignment {
225 let rtl = base == TextDirection::RightToLeft;
226 match self {
227 Alignment::Start if rtl => Alignment::Right,
228 Alignment::Start => Alignment::Left,
229 Alignment::End if rtl => Alignment::Left,
230 Alignment::End => Alignment::Right,
231 absolute => absolute,
232 }
233 }
234}
235
236fn reorder_line_visually(line: &mut LayoutLine) {
249 if line.runs.len() < 2 {
250 return;
251 }
252 if line.runs.iter().all(|r| r.shaped_run.bidi_level % 2 == 0) {
258 return;
259 }
260
261 let levels: Vec<u8> = line.runs.iter().map(|r| r.shaped_run.bidi_level).collect();
262
263 let order = crate::shaping::shaper::visual_order(&levels);
264 if order.iter().copied().eq(0..order.len()) {
265 return; }
267
268 let origin = line.runs.iter().map(|r| r.x).fold(f32::INFINITY, f32::min);
271
272 let mut reordered: Vec<crate::layout::line::PositionedRun> = Vec::with_capacity(order.len());
273 let mut taken: Vec<Option<crate::layout::line::PositionedRun>> =
274 line.runs.drain(..).map(Some).collect();
275 for logical_idx in order {
276 if let Some(run) = taken[logical_idx].take() {
277 reordered.push(run);
278 }
279 }
280
281 let mut x = origin;
282 for run in &mut reordered {
283 run.x = x;
284 x += run.shaped_run.advance_width;
285 }
286 line.runs = reordered;
287}
288
289#[allow(clippy::too_many_arguments)]
299pub fn break_into_lines(
300 runs: Vec<ShapedRun>,
301 text: &str,
302 available_width: f32,
303 alignment: Alignment,
304 first_line_indent: f32,
305 metrics: &FontMetricsPx,
306 hyphenator: Option<Hyphenator>,
307 run_order: RunOrder,
308) -> Vec<LayoutLine> {
309 if runs.is_empty() || text.is_empty() {
310 return vec![make_empty_line(metrics, 0..0)];
312 }
313
314 let flat = flatten_runs(&runs);
316 if flat.is_empty() {
317 return vec![make_empty_line(metrics, 0..0)];
318 }
319
320 let breaks: Vec<(usize, BreakOpportunity)> = enumerate_breaks(text);
323
324 let (break_points, mandatory_breaks) = map_breaks_to_glyph_indices(&flat, &breaks);
326
327 let hyphen_points = if let Some(h) = &hyphenator {
330 map_offsets_to_glyph_indices(&flat, &hyphenation_breaks(text, h.language))
331 } else {
332 HashSet::new()
333 };
334 let hyphen_adv = hyphenator
335 .as_ref()
336 .map(|h| h.glyph.x_advance)
337 .unwrap_or(0.0);
338
339 let mut lines = Vec::new();
341 let mut line_start_glyph = 0usize;
342 let mut line_width = 0.0f32;
343 let mut last_break: Option<(usize, bool)> = None;
346 let mut effective_width = available_width - first_line_indent;
348
349 for i in 0..flat.len() {
350 let glyph_advance = flat[i].x_advance;
351 line_width += glyph_advance;
352
353 let is_mandatory = mandatory_breaks.contains(&(i + 1));
355
356 let exceeds_width = line_width > effective_width && line_start_glyph < i;
357
358 if is_mandatory || exceeds_width {
359 let (break_at, needs_hyphen) = if is_mandatory {
360 (i + 1, false)
361 } else if let Some((bp, hy)) = last_break {
362 if bp > line_start_glyph {
363 (bp, hy)
364 } else {
365 (i + 1, false) }
367 } else {
368 (i + 1, false) };
370
371 let indent = if lines.is_empty() {
372 first_line_indent
373 } else {
374 0.0
375 };
376 let mut line = build_line(
377 &runs,
378 &flat,
379 line_start_glyph,
380 break_at,
381 metrics,
382 indent,
383 text,
384 );
385 if needs_hyphen && let Some(h) = &hyphenator {
386 append_hyphen(&mut line, &h.glyph);
387 }
388 lines.push(line);
389
390 line_start_glyph = break_at;
391 effective_width = available_width;
393 line_width = 0.0;
395 for j in break_at..=i {
396 if j < flat.len() {
397 line_width += flat[j].x_advance;
398 }
399 }
400 last_break = None;
401 }
402
403 let at = i + 1;
409 if hyphen_points.contains(&at) && line_width + hyphen_adv <= effective_width {
410 last_break = Some((at, true));
411 } else if break_points.contains(&at) {
412 last_break = Some((at, false));
413 }
414 }
415
416 if line_start_glyph < flat.len() {
418 let line = build_line(
419 &runs,
420 &flat,
421 line_start_glyph,
422 flat.len(),
423 metrics,
424 if lines.is_empty() {
425 first_line_indent
426 } else {
427 0.0
428 },
429 text,
430 );
431 lines.push(line);
432 }
433
434 let base_direction = match run_order {
440 RunOrder::Logical(base) => {
441 for line in &mut lines {
442 reorder_line_visually(line);
443 }
444 base
445 }
446 RunOrder::AlreadyVisual => TextDirection::LeftToRight,
448 };
449
450 let alignment = alignment.resolve_for(base_direction);
454 let rtl_paragraph = base_direction == TextDirection::RightToLeft;
455 let effective_width = available_width;
456 let last_idx = lines.len().saturating_sub(1);
457 for (i, line) in lines.iter_mut().enumerate() {
458 let indent = if i == 0 { first_line_indent } else { 0.0 };
459 if rtl_paragraph && indent != 0.0 {
464 for run in &mut line.runs {
465 run.x -= indent;
466 }
467 }
468 let line_avail = effective_width - indent;
469 match alignment {
470 Alignment::Start | Alignment::End | Alignment::Left => {
474 if rtl_paragraph && indent != 0.0 {
481 for run in &mut line.runs {
482 run.x += indent;
483 }
484 }
485 }
486 Alignment::Right => {
487 let shift = (line_avail - line.width).max(0.0);
488 for run in &mut line.runs {
489 run.x += shift;
490 }
491 }
492 Alignment::Center => {
493 let shift = ((line_avail - line.width) / 2.0).max(0.0);
494 for run in &mut line.runs {
495 run.x += shift;
496 }
497 }
498 Alignment::Justify => {
499 if i < last_idx && line.width > 0.0 {
501 justify_line(line, line_avail, text);
502 }
503 }
504 }
505 }
506
507 if lines.is_empty() {
508 lines.push(make_empty_line(metrics, 0..0));
509 }
510
511 for line in &mut lines {
515 for run in &mut line.runs {
516 for glyph in &mut run.shaped_run.glyphs {
517 glyph.cluster = byte_offset_to_char_offset(text, glyph.cluster as usize) as u32;
518 }
519 }
520 }
521
522 lines
523}
524
525struct FlatGlyph {
527 x_advance: f32,
528 cluster: u32,
529 run_index: usize,
530 glyph_index_in_run: usize,
531}
532
533fn flatten_runs(runs: &[ShapedRun]) -> Vec<FlatGlyph> {
534 let mut flat = Vec::new();
535 for (run_idx, run) in runs.iter().enumerate() {
536 let cluster_offset = run.text_range.start as u32;
540 for (glyph_idx, glyph) in run.glyphs.iter().enumerate() {
541 flat.push(FlatGlyph {
542 x_advance: glyph.x_advance,
543 cluster: glyph.cluster + cluster_offset,
544 run_index: run_idx,
545 glyph_index_in_run: glyph_idx,
546 });
547 }
548 }
549 flat
550}
551
552fn map_breaks_to_glyph_indices(
558 flat: &[FlatGlyph],
559 breaks: &[(usize, BreakOpportunity)],
560) -> (HashSet<usize>, HashSet<usize>) {
561 let mut break_points = HashSet::new();
562 let mut mandatory_breaks = HashSet::new();
563 let mut glyph_cursor = 0usize;
564
565 for &(byte_offset, opportunity) in breaks {
566 while glyph_cursor < flat.len() && (flat[glyph_cursor].cluster as usize) < byte_offset {
568 glyph_cursor += 1;
569 }
570 let glyph_idx = if glyph_cursor < flat.len() {
571 glyph_cursor
572 } else {
573 flat.len()
574 };
575 break_points.insert(glyph_idx);
576 if opportunity == BreakOpportunity::Mandatory {
577 mandatory_breaks.insert(glyph_idx);
578 }
579 }
580
581 (break_points, mandatory_breaks)
582}
583
584fn build_line(
586 runs: &[ShapedRun],
587 flat: &[FlatGlyph],
588 start: usize,
589 end: usize,
590 metrics: &FontMetricsPx,
591 indent: f32,
592 text: &str,
593) -> LayoutLine {
594 let mut positioned_runs = Vec::new();
596 let mut x = indent;
597 let mut current_run_idx: Option<usize> = None;
598 let mut run_glyph_start = 0usize;
599
600 for i in start..end {
601 let fg = &flat[i];
602 if current_run_idx != Some(fg.run_index) {
603 if let Some(prev_run_idx) = current_run_idx {
605 let prev_end = if i > start {
607 flat[i - 1].glyph_index_in_run + 1
608 } else {
609 run_glyph_start
610 };
611 let sub_run = extract_sub_run(runs, prev_run_idx, run_glyph_start, prev_end);
612 if let Some((pr, advance)) = sub_run {
613 positioned_runs.push(PositionedRun {
614 decorations: RunDecorations {
615 underline_style: pr.underline_style,
616 overline: pr.overline,
617 strikeout: pr.strikeout,
618 is_link: pr.is_link,
619 foreground_color: pr.foreground_color,
620 underline_color: pr.underline_color,
621 background_color: pr.background_color,
622 anchor_href: pr.anchor_href.clone(),
623 tooltip: pr.tooltip.clone(),
624 vertical_alignment: pr.vertical_alignment,
625 },
626 shaped_run: pr,
627 x,
628 });
629 x += advance;
630 }
631 }
632 current_run_idx = Some(fg.run_index);
633 run_glyph_start = fg.glyph_index_in_run;
634 }
635 }
636
637 if let Some(run_idx) = current_run_idx {
639 let end_in_run = if end < flat.len() && flat[end].run_index == run_idx {
640 flat[end].glyph_index_in_run
641 } else if end > start {
642 flat[end - 1].glyph_index_in_run + 1
643 } else {
644 run_glyph_start
645 };
646 let sub_run = extract_sub_run(runs, run_idx, run_glyph_start, end_in_run);
647 if let Some((pr, advance)) = sub_run {
648 positioned_runs.push(PositionedRun {
649 decorations: RunDecorations {
650 underline_style: pr.underline_style,
651 overline: pr.overline,
652 strikeout: pr.strikeout,
653 is_link: pr.is_link,
654 foreground_color: pr.foreground_color,
655 underline_color: pr.underline_color,
656 background_color: pr.background_color,
657 anchor_href: pr.anchor_href.clone(),
658 tooltip: pr.tooltip.clone(),
659 vertical_alignment: pr.vertical_alignment,
660 },
661 shaped_run: pr,
662 x,
663 });
664 x += advance;
665 }
666 }
667
668 let width = x - indent;
669
670 let byte_start = flat[start..end.min(flat.len())]
679 .iter()
680 .map(|g| g.cluster as usize)
681 .min()
682 .unwrap_or(0);
683 let byte_end = if end >= flat.len() {
684 text.len()
688 } else {
689 let line_max = flat[start..end]
693 .iter()
694 .map(|g| g.cluster as usize)
695 .max()
696 .unwrap_or(0);
697 (flat[end].cluster as usize).max(line_max)
698 };
699 let char_start = byte_offset_to_char_offset(text, byte_start);
700 let char_end = byte_offset_to_char_offset(text, byte_end);
701
702 let mut ascent = metrics.ascent;
704 for run in &positioned_runs {
705 if run.shaped_run.image_name.is_some() && run.shaped_run.image_height > ascent {
706 ascent = run.shaped_run.image_height;
707 }
708 }
709 let line_height = ascent + metrics.descent + metrics.leading;
710
711 LayoutLine {
712 runs: positioned_runs,
713 y: 0.0, ascent,
715 descent: metrics.descent,
716 leading: metrics.leading,
717 width,
718 char_range: char_start..char_end,
719 line_height,
720 }
721}
722
723fn extract_sub_run(
726 runs: &[ShapedRun],
727 run_index: usize,
728 glyph_start: usize,
729 glyph_end: usize,
730) -> Option<(ShapedRun, f32)> {
731 let run = &runs[run_index];
732 let end = glyph_end.min(run.glyphs.len());
733 if glyph_start >= end {
734 return None;
735 }
736 let cluster_offset = run.text_range.start as u32;
737 let mut sub_glyphs = run.glyphs[glyph_start..end].to_vec();
738 for g in &mut sub_glyphs {
740 g.cluster += cluster_offset;
741 }
742 let advance: f32 = sub_glyphs.iter().map(|g| g.x_advance).sum();
743
744 let sub_run = ShapedRun {
745 font_face_id: run.font_face_id,
746 size_px: run.size_px,
747 weight: run.weight,
748 glyphs: sub_glyphs,
749 advance_width: advance,
750 text_range: run.text_range.clone(),
751 direction: run.direction,
752 bidi_level: run.bidi_level,
753 underline_style: run.underline_style,
754 overline: run.overline,
755 strikeout: run.strikeout,
756 is_link: run.is_link,
757 foreground_color: run.foreground_color,
758 underline_color: run.underline_color,
759 background_color: run.background_color,
760 anchor_href: run.anchor_href.clone(),
761 tooltip: run.tooltip.clone(),
762 vertical_alignment: run.vertical_alignment,
763 image_name: run.image_name.clone(),
764 image_height: run.image_height,
765 };
766 Some((sub_run, advance))
767}
768
769fn make_empty_line(metrics: &FontMetricsPx, char_range: Range<usize>) -> LayoutLine {
770 LayoutLine {
771 runs: Vec::new(),
772 y: 0.0,
773 ascent: metrics.ascent,
774 descent: metrics.descent,
775 leading: metrics.leading,
776 width: 0.0,
777 char_range,
778 line_height: metrics.ascent + metrics.descent + metrics.leading,
779 }
780}
781
782fn justify_line(line: &mut LayoutLine, target_width: f32, text: &str) {
787 let extra = target_width - line.width;
788 if extra <= 0.0 {
789 return;
790 }
791
792 let mut space_count = 0usize;
794 for run in &line.runs {
795 for glyph in &run.shaped_run.glyphs {
796 let byte_offset = glyph.cluster as usize;
797 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
798 && ch == ' '
799 {
800 space_count += 1;
801 }
802 }
803 }
804
805 if space_count == 0 {
806 return;
807 }
808
809 let extra_per_space = extra / space_count as f32;
810
811 for run in &mut line.runs {
813 for glyph in &mut run.shaped_run.glyphs {
814 let byte_offset = glyph.cluster as usize;
815 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
816 && ch == ' '
817 {
818 glyph.x_advance += extra_per_space;
819 }
820 }
821 run.shaped_run.advance_width = run.shaped_run.glyphs.iter().map(|g| g.x_advance).sum();
823 }
824
825 let first_x = line.runs.first().map(|r| r.x).unwrap_or(0.0);
827 let mut x = first_x;
828 for run in &mut line.runs {
829 run.x = x;
830 x += run.shaped_run.advance_width;
831 }
832
833 line.width = target_width;
834}