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;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum BreakOpportunity {
20 Allowed,
21 Mandatory,
22}
23
24fn line_segmenter() -> icu_segmenter::LineSegmenterBorrowed<'static> {
29 static CELL: OnceLock<icu_segmenter::LineSegmenterBorrowed<'static>> = OnceLock::new();
30 *CELL.get_or_init(|| LineSegmenter::new_auto(LineBreakOptions::default()))
31}
32
33fn is_mandatory_break_at(text: &str, byte_offset: usize) -> bool {
39 if byte_offset == 0 {
40 return false;
41 }
42 let preceding = &text[..byte_offset];
43 matches!(
44 preceding.chars().next_back(),
45 Some('\n' | '\r' | '\u{0085}' | '\u{000B}' | '\u{000C}' | '\u{2028}' | '\u{2029}')
46 )
47}
48
49fn enumerate_breaks(text: &str) -> Vec<(usize, BreakOpportunity)> {
53 line_segmenter()
54 .segment_str(text)
55 .map(|byte_offset| {
56 let kind = if is_mandatory_break_at(text, byte_offset) {
57 BreakOpportunity::Mandatory
58 } else {
59 BreakOpportunity::Allowed
60 };
61 (byte_offset, kind)
62 })
63 .collect()
64}
65
66fn hyphenation_breaks(text: &str, lang_code: [u8; 2]) -> Vec<usize> {
77 use hypher::hyphenate;
78
79 let mut offsets = Vec::new();
80
81 for (idx, ch) in text.char_indices() {
83 if ch == '\u{00AD}' {
84 offsets.push(idx + ch.len_utf8());
85 }
86 }
87
88 if let Some(lang) = hypher::Lang::from_iso(lang_code) {
90 let mut word_start: Option<usize> = None;
91 let flush = |start: usize, end: usize, offsets: &mut Vec<usize>| {
92 let word = &text[start..end];
93 if word.chars().count() < 5 {
96 return;
97 }
98 let mut pos = start;
99 let mut syllables = hyphenate(word, lang).peekable();
100 while let Some(syl) = syllables.next() {
101 pos += syl.len();
102 if syllables.peek().is_some() {
104 offsets.push(pos);
105 }
106 }
107 };
108 for (idx, ch) in text.char_indices() {
109 if ch.is_alphabetic() {
110 word_start.get_or_insert(idx);
111 } else if let Some(start) = word_start.take() {
112 flush(start, idx, &mut offsets);
113 }
114 }
115 if let Some(start) = word_start.take() {
116 flush(start, text.len(), &mut offsets);
117 }
118 }
119
120 offsets.sort_unstable();
121 offsets.dedup();
122 offsets
123}
124
125fn map_offsets_to_glyph_indices(flat: &[FlatGlyph], offsets: &[usize]) -> HashSet<usize> {
128 let mut set = HashSet::new();
129 let mut cursor = 0usize;
130 for &byte_offset in offsets {
131 while cursor < flat.len() && (flat[cursor].cluster as usize) < byte_offset {
132 cursor += 1;
133 }
134 set.insert(cursor.min(flat.len()));
135 }
136 set
137}
138
139fn append_hyphen(line: &mut LayoutLine, hyphen: &ShapedGlyph) {
144 if let Some(run) = line.runs.last_mut() {
145 let mut g = hyphen.clone();
146 g.cluster = run
147 .shaped_run
148 .glyphs
149 .last()
150 .map(|gl| gl.cluster)
151 .unwrap_or(0);
152 run.shaped_run.glyphs.push(g);
153 run.shaped_run.advance_width += hyphen.x_advance;
154 line.width += hyphen.x_advance;
155 }
156}
157
158fn byte_offset_to_char_offset(text: &str, byte_offset: usize) -> usize {
166 let mut off = byte_offset.min(text.len());
167 while off > 0 && !text.is_char_boundary(off) {
168 off -= 1;
169 }
170 text[..off].chars().count()
171}
172
173pub struct Hyphenator {
177 pub glyph: ShapedGlyph,
179 pub language: [u8; 2],
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum Alignment {
186 #[default]
187 Left,
188 Right,
189 Center,
190 Justify,
191}
192
193pub fn break_into_lines(
203 runs: Vec<ShapedRun>,
204 text: &str,
205 available_width: f32,
206 alignment: Alignment,
207 first_line_indent: f32,
208 metrics: &FontMetricsPx,
209 hyphenator: Option<Hyphenator>,
210) -> Vec<LayoutLine> {
211 if runs.is_empty() || text.is_empty() {
212 return vec![make_empty_line(metrics, 0..0)];
214 }
215
216 let flat = flatten_runs(&runs);
218 if flat.is_empty() {
219 return vec![make_empty_line(metrics, 0..0)];
220 }
221
222 let breaks: Vec<(usize, BreakOpportunity)> = enumerate_breaks(text);
225
226 let (break_points, mandatory_breaks) = map_breaks_to_glyph_indices(&flat, &breaks);
228
229 let hyphen_points = if let Some(h) = &hyphenator {
232 map_offsets_to_glyph_indices(&flat, &hyphenation_breaks(text, h.language))
233 } else {
234 HashSet::new()
235 };
236 let hyphen_adv = hyphenator
237 .as_ref()
238 .map(|h| h.glyph.x_advance)
239 .unwrap_or(0.0);
240
241 let mut lines = Vec::new();
243 let mut line_start_glyph = 0usize;
244 let mut line_width = 0.0f32;
245 let mut last_break: Option<(usize, bool)> = None;
248 let mut effective_width = available_width - first_line_indent;
250
251 for i in 0..flat.len() {
252 let glyph_advance = flat[i].x_advance;
253 line_width += glyph_advance;
254
255 let is_mandatory = mandatory_breaks.contains(&(i + 1));
257
258 let exceeds_width = line_width > effective_width && line_start_glyph < i;
259
260 if is_mandatory || exceeds_width {
261 let (break_at, needs_hyphen) = if is_mandatory {
262 (i + 1, false)
263 } else if let Some((bp, hy)) = last_break {
264 if bp > line_start_glyph {
265 (bp, hy)
266 } else {
267 (i + 1, false) }
269 } else {
270 (i + 1, false) };
272
273 let indent = if lines.is_empty() {
274 first_line_indent
275 } else {
276 0.0
277 };
278 let mut line = build_line(
279 &runs,
280 &flat,
281 line_start_glyph,
282 break_at,
283 metrics,
284 indent,
285 text,
286 );
287 if needs_hyphen && let Some(h) = &hyphenator {
288 append_hyphen(&mut line, &h.glyph);
289 }
290 lines.push(line);
291
292 line_start_glyph = break_at;
293 effective_width = available_width;
295 line_width = 0.0;
297 for j in break_at..=i {
298 if j < flat.len() {
299 line_width += flat[j].x_advance;
300 }
301 }
302 last_break = None;
303 }
304
305 let at = i + 1;
311 if hyphen_points.contains(&at) && line_width + hyphen_adv <= effective_width {
312 last_break = Some((at, true));
313 } else if break_points.contains(&at) {
314 last_break = Some((at, false));
315 }
316 }
317
318 if line_start_glyph < flat.len() {
320 let line = build_line(
321 &runs,
322 &flat,
323 line_start_glyph,
324 flat.len(),
325 metrics,
326 if lines.is_empty() {
327 first_line_indent
328 } else {
329 0.0
330 },
331 text,
332 );
333 lines.push(line);
334 }
335
336 let effective_width = available_width;
338 let last_idx = lines.len().saturating_sub(1);
339 for (i, line) in lines.iter_mut().enumerate() {
340 let indent = if i == 0 { first_line_indent } else { 0.0 };
341 let line_avail = effective_width - indent;
342 match alignment {
343 Alignment::Left => {} Alignment::Right => {
345 let shift = (line_avail - line.width).max(0.0);
346 for run in &mut line.runs {
347 run.x += shift;
348 }
349 }
350 Alignment::Center => {
351 let shift = ((line_avail - line.width) / 2.0).max(0.0);
352 for run in &mut line.runs {
353 run.x += shift;
354 }
355 }
356 Alignment::Justify => {
357 if i < last_idx && line.width > 0.0 {
359 justify_line(line, line_avail, text);
360 }
361 }
362 }
363 }
364
365 if lines.is_empty() {
366 lines.push(make_empty_line(metrics, 0..0));
367 }
368
369 for line in &mut lines {
373 for run in &mut line.runs {
374 for glyph in &mut run.shaped_run.glyphs {
375 glyph.cluster = byte_offset_to_char_offset(text, glyph.cluster as usize) as u32;
376 }
377 }
378 }
379
380 lines
381}
382
383struct FlatGlyph {
385 x_advance: f32,
386 cluster: u32,
387 run_index: usize,
388 glyph_index_in_run: usize,
389}
390
391fn flatten_runs(runs: &[ShapedRun]) -> Vec<FlatGlyph> {
392 let mut flat = Vec::new();
393 for (run_idx, run) in runs.iter().enumerate() {
394 let cluster_offset = run.text_range.start as u32;
398 for (glyph_idx, glyph) in run.glyphs.iter().enumerate() {
399 flat.push(FlatGlyph {
400 x_advance: glyph.x_advance,
401 cluster: glyph.cluster + cluster_offset,
402 run_index: run_idx,
403 glyph_index_in_run: glyph_idx,
404 });
405 }
406 }
407 flat
408}
409
410fn map_breaks_to_glyph_indices(
416 flat: &[FlatGlyph],
417 breaks: &[(usize, BreakOpportunity)],
418) -> (HashSet<usize>, HashSet<usize>) {
419 let mut break_points = HashSet::new();
420 let mut mandatory_breaks = HashSet::new();
421 let mut glyph_cursor = 0usize;
422
423 for &(byte_offset, opportunity) in breaks {
424 while glyph_cursor < flat.len() && (flat[glyph_cursor].cluster as usize) < byte_offset {
426 glyph_cursor += 1;
427 }
428 let glyph_idx = if glyph_cursor < flat.len() {
429 glyph_cursor
430 } else {
431 flat.len()
432 };
433 break_points.insert(glyph_idx);
434 if opportunity == BreakOpportunity::Mandatory {
435 mandatory_breaks.insert(glyph_idx);
436 }
437 }
438
439 (break_points, mandatory_breaks)
440}
441
442fn build_line(
444 runs: &[ShapedRun],
445 flat: &[FlatGlyph],
446 start: usize,
447 end: usize,
448 metrics: &FontMetricsPx,
449 indent: f32,
450 text: &str,
451) -> LayoutLine {
452 let mut positioned_runs = Vec::new();
454 let mut x = indent;
455 let mut current_run_idx: Option<usize> = None;
456 let mut run_glyph_start = 0usize;
457
458 for i in start..end {
459 let fg = &flat[i];
460 if current_run_idx != Some(fg.run_index) {
461 if let Some(prev_run_idx) = current_run_idx {
463 let prev_end = if i > start {
465 flat[i - 1].glyph_index_in_run + 1
466 } else {
467 run_glyph_start
468 };
469 let sub_run = extract_sub_run(runs, prev_run_idx, run_glyph_start, prev_end);
470 if let Some((pr, advance)) = sub_run {
471 positioned_runs.push(PositionedRun {
472 decorations: RunDecorations {
473 underline_style: pr.underline_style,
474 overline: pr.overline,
475 strikeout: pr.strikeout,
476 is_link: pr.is_link,
477 foreground_color: pr.foreground_color,
478 underline_color: pr.underline_color,
479 background_color: pr.background_color,
480 anchor_href: pr.anchor_href.clone(),
481 tooltip: pr.tooltip.clone(),
482 vertical_alignment: pr.vertical_alignment,
483 },
484 shaped_run: pr,
485 x,
486 });
487 x += advance;
488 }
489 }
490 current_run_idx = Some(fg.run_index);
491 run_glyph_start = fg.glyph_index_in_run;
492 }
493 }
494
495 if let Some(run_idx) = current_run_idx {
497 let end_in_run = if end < flat.len() && flat[end].run_index == run_idx {
498 flat[end].glyph_index_in_run
499 } else if end > start {
500 flat[end - 1].glyph_index_in_run + 1
501 } else {
502 run_glyph_start
503 };
504 let sub_run = extract_sub_run(runs, run_idx, run_glyph_start, end_in_run);
505 if let Some((pr, advance)) = sub_run {
506 positioned_runs.push(PositionedRun {
507 decorations: RunDecorations {
508 underline_style: pr.underline_style,
509 overline: pr.overline,
510 strikeout: pr.strikeout,
511 is_link: pr.is_link,
512 foreground_color: pr.foreground_color,
513 underline_color: pr.underline_color,
514 background_color: pr.background_color,
515 anchor_href: pr.anchor_href.clone(),
516 tooltip: pr.tooltip.clone(),
517 vertical_alignment: pr.vertical_alignment,
518 },
519 shaped_run: pr,
520 x,
521 });
522 x += advance;
523 }
524 }
525
526 let width = x - indent;
527
528 let byte_start = flat[start..end.min(flat.len())]
537 .iter()
538 .map(|g| g.cluster as usize)
539 .min()
540 .unwrap_or(0);
541 let byte_end = if end >= flat.len() {
542 text.len()
546 } else {
547 let line_max = flat[start..end]
551 .iter()
552 .map(|g| g.cluster as usize)
553 .max()
554 .unwrap_or(0);
555 (flat[end].cluster as usize).max(line_max)
556 };
557 let char_start = byte_offset_to_char_offset(text, byte_start);
558 let char_end = byte_offset_to_char_offset(text, byte_end);
559
560 let mut ascent = metrics.ascent;
562 for run in &positioned_runs {
563 if run.shaped_run.image_name.is_some() && run.shaped_run.image_height > ascent {
564 ascent = run.shaped_run.image_height;
565 }
566 }
567 let line_height = ascent + metrics.descent + metrics.leading;
568
569 LayoutLine {
570 runs: positioned_runs,
571 y: 0.0, ascent,
573 descent: metrics.descent,
574 leading: metrics.leading,
575 width,
576 char_range: char_start..char_end,
577 line_height,
578 }
579}
580
581fn extract_sub_run(
584 runs: &[ShapedRun],
585 run_index: usize,
586 glyph_start: usize,
587 glyph_end: usize,
588) -> Option<(ShapedRun, f32)> {
589 let run = &runs[run_index];
590 let end = glyph_end.min(run.glyphs.len());
591 if glyph_start >= end {
592 return None;
593 }
594 let cluster_offset = run.text_range.start as u32;
595 let mut sub_glyphs = run.glyphs[glyph_start..end].to_vec();
596 for g in &mut sub_glyphs {
598 g.cluster += cluster_offset;
599 }
600 let advance: f32 = sub_glyphs.iter().map(|g| g.x_advance).sum();
601
602 let sub_run = ShapedRun {
603 font_face_id: run.font_face_id,
604 size_px: run.size_px,
605 weight: run.weight,
606 glyphs: sub_glyphs,
607 advance_width: advance,
608 text_range: run.text_range.clone(),
609 direction: run.direction,
610 underline_style: run.underline_style,
611 overline: run.overline,
612 strikeout: run.strikeout,
613 is_link: run.is_link,
614 foreground_color: run.foreground_color,
615 underline_color: run.underline_color,
616 background_color: run.background_color,
617 anchor_href: run.anchor_href.clone(),
618 tooltip: run.tooltip.clone(),
619 vertical_alignment: run.vertical_alignment,
620 image_name: run.image_name.clone(),
621 image_height: run.image_height,
622 };
623 Some((sub_run, advance))
624}
625
626fn make_empty_line(metrics: &FontMetricsPx, char_range: Range<usize>) -> LayoutLine {
627 LayoutLine {
628 runs: Vec::new(),
629 y: 0.0,
630 ascent: metrics.ascent,
631 descent: metrics.descent,
632 leading: metrics.leading,
633 width: 0.0,
634 char_range,
635 line_height: metrics.ascent + metrics.descent + metrics.leading,
636 }
637}
638
639fn justify_line(line: &mut LayoutLine, target_width: f32, text: &str) {
644 let extra = target_width - line.width;
645 if extra <= 0.0 {
646 return;
647 }
648
649 let mut space_count = 0usize;
651 for run in &line.runs {
652 for glyph in &run.shaped_run.glyphs {
653 let byte_offset = glyph.cluster as usize;
654 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
655 && ch == ' '
656 {
657 space_count += 1;
658 }
659 }
660 }
661
662 if space_count == 0 {
663 return;
664 }
665
666 let extra_per_space = extra / space_count as f32;
667
668 for run in &mut line.runs {
670 for glyph in &mut run.shaped_run.glyphs {
671 let byte_offset = glyph.cluster as usize;
672 if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
673 && ch == ' '
674 {
675 glyph.x_advance += extra_per_space;
676 }
677 }
678 run.shaped_run.advance_width = run.shaped_run.glyphs.iter().map(|g| g.x_advance).sum();
680 }
681
682 let first_x = line.runs.first().map(|r| r.x).unwrap_or(0.0);
684 let mut x = first_x;
685 for run in &mut line.runs {
686 run.x = x;
687 x += run.shaped_run.advance_width;
688 }
689
690 line.width = target_width;
691}