Skip to main content

ferroui_pretext/
pretext_layout_line_break.rs

1//! Prepared line walking and incremental wrapping.
2
3use crate::pretext_layout_line_text::materialize_line;
4use crate::pretext_layout_measurement::{get_engine_profile, EngineProfile};
5use crate::{
6    LayoutCursor, LayoutLine, LayoutLineRange, LayoutLinesResult, LayoutResult, LineStats,
7    PreparedText, PreparedTextWithSegments, PretextLayout, SegmentBreakKind,
8};
9
10#[derive(Clone, Copy, Debug)]
11pub(crate) struct InternalLine {
12    pub start: LayoutCursor,
13    pub visible_end: LayoutCursor,
14    pub end: LayoutCursor,
15    pub width: f64,
16    pub append_hyphen: bool,
17}
18
19#[derive(Clone, Copy)]
20struct PendingBreak {
21    end_segment_index: usize,
22    fit_width: f64,
23    paint_width: f64,
24    kind: SegmentBreakKind,
25}
26
27/// The state shared by the batched and incremental walkers. All state is local
28/// to a walk; a cursor can resume a prepared paragraph at a different width.
29struct LineState {
30    start: LayoutCursor,
31    end: LayoutCursor,
32    width: f64,
33    has_content: bool,
34    pending: Option<PendingBreak>,
35}
36
37impl LineState {
38    fn new(start: LayoutCursor) -> Self {
39        Self {
40            start,
41            end: start,
42            width: 0.0,
43            has_content: false,
44            pending: None,
45        }
46    }
47
48    fn start_segment(&mut self, segment_index: usize, width: f64, reset_start: bool) {
49        if reset_start {
50            self.start = cursor(segment_index, 0);
51        }
52        self.end = cursor(segment_index + 1, 0);
53        self.width = width;
54        self.has_content = true;
55    }
56
57    fn start_grapheme(
58        &mut self,
59        segment_index: usize,
60        grapheme_index: usize,
61        width: f64,
62        reset_start: bool,
63    ) {
64        if reset_start {
65            self.start = cursor(segment_index, grapheme_index);
66        }
67        self.end = cursor(segment_index, grapheme_index + 1);
68        self.width = width;
69        self.has_content = true;
70    }
71
72    fn append_whole(&mut self, segment_index: usize, width: f64, reset_start: bool) {
73        if self.has_content {
74            self.width += width;
75            self.end = cursor(segment_index + 1, 0);
76        } else {
77            self.start_segment(segment_index, width, reset_start);
78        }
79    }
80
81    fn update_pending(
82        &mut self,
83        prepared: &PreparedText,
84        segment_index: usize,
85        width: f64,
86        simple: bool,
87    ) {
88        let kind = prepared.kinds[segment_index];
89        if !can_break_after(kind) {
90            return;
91        }
92        let (fit, paint) = if simple {
93            (0.0, 0.0)
94        } else if kind == SegmentBreakKind::Tab {
95            (0.0, width)
96        } else {
97            (
98                prepared.line_end_fit_advances[segment_index],
99                prepared.line_end_paint_advances[segment_index],
100            )
101        };
102        self.pending = Some(PendingBreak {
103            end_segment_index: segment_index + 1,
104            fit_width: self.width - width + fit,
105            paint_width: self.width - width + paint,
106            kind,
107        });
108    }
109
110    fn finish(
111        &self,
112        end: Option<LayoutCursor>,
113        width: Option<f64>,
114        append_hyphen: bool,
115    ) -> Option<InternalLine> {
116        self.has_content.then(|| {
117            let end = end.unwrap_or(self.end);
118            InternalLine {
119                start: self.start,
120                visible_end: end,
121                end,
122                width: width.unwrap_or(self.width),
123                append_hyphen,
124            }
125        })
126    }
127
128    fn emit(
129        &mut self,
130        end: Option<LayoutCursor>,
131        width: Option<f64>,
132        append_hyphen: bool,
133        on_line: &mut impl FnMut(InternalLine),
134        count: &mut usize,
135    ) {
136        if let Some(line) = self.finish(end, width, append_hyphen) {
137            *count += 1;
138            on_line(line);
139        }
140        self.width = 0.0;
141        self.has_content = false;
142        self.pending = None;
143    }
144
145    fn is_after_pending(&self, pending: PendingBreak) -> bool {
146        self.end.segment_index > pending.end_segment_index
147            || (self.end.segment_index == pending.end_segment_index && self.end.grapheme_index > 0)
148    }
149}
150
151const fn cursor(segment_index: usize, grapheme_index: usize) -> LayoutCursor {
152    LayoutCursor {
153        segment_index,
154        grapheme_index,
155    }
156}
157
158impl PretextLayout {
159    /// Counts lines without materializing text or allocating line objects.
160    pub fn layout(prepared: &PreparedText, max_width: f64, line_height: f64) -> LayoutResult {
161        let line_count = count_prepared_lines(prepared, max_width);
162        LayoutResult {
163            line_count,
164            height: line_count as f64 * line_height,
165        }
166    }
167
168    /// Materializes the text and geometry of each line.
169    pub fn layout_with_lines(
170        prepared: &PreparedTextWithSegments,
171        max_width: f64,
172        line_height: f64,
173    ) -> LayoutLinesResult {
174        let mut lines = Vec::new();
175        let line_count = walk_prepared_lines(prepared, max_width, |line| {
176            lines.push(materialize_line(prepared, line))
177        });
178        LayoutLinesResult {
179            line_count,
180            height: line_count as f64 * line_height,
181            lines,
182        }
183    }
184
185    /// Resumes at a segment/grapheme cursor. Every returned line advances the
186    /// cursor, including when an individual grapheme exceeds the width.
187    pub fn layout_next_line(
188        prepared: &PreparedTextWithSegments,
189        start: LayoutCursor,
190        max_width: f64,
191    ) -> Option<LayoutLine> {
192        try_step_line(prepared, start, max_width).map(|line| materialize_line(prepared, line))
193    }
194
195    /// Computes the next line without allocating text.
196    pub fn layout_next_line_range(
197        prepared: &PreparedTextWithSegments,
198        start: LayoutCursor,
199        max_width: f64,
200    ) -> Option<LayoutLineRange> {
201        try_step_line(prepared, start, max_width).map(|line| LayoutLineRange {
202            width: line.width,
203            start: line.start,
204            end: line.end,
205        })
206    }
207
208    /// Materializes a previously computed range. The cursor carries enough
209    /// information to recover discretionary hyphens without hidden walk state.
210    pub fn materialize_line_range(
211        prepared: &PreparedTextWithSegments,
212        line: &LayoutLineRange,
213    ) -> LayoutLine {
214        materialize_line(
215            prepared,
216            InternalLine {
217                start: line.start,
218                visible_end: line.end,
219                end: line.end,
220                width: line.width,
221                append_hyphen: false,
222            },
223        )
224    }
225
226    pub fn walk_line_ranges(
227        prepared: &PreparedTextWithSegments,
228        max_width: f64,
229        mut on_line: impl FnMut(LayoutLineRange),
230    ) -> usize {
231        let mut start = cursor(0, 0);
232        let mut count = 0;
233        while let Some(line) = Self::layout_next_line_range(prepared, start, max_width) {
234            start = line.end;
235            count += 1;
236            on_line(line);
237        }
238        count
239    }
240
241    pub fn measure_line_stats(prepared: &PreparedTextWithSegments, max_width: f64) -> LineStats {
242        let mut max_line_width = 0.0_f64;
243        let line_count = walk_prepared_lines(prepared, max_width, |line| {
244            max_line_width = max_line_width.max(line.width)
245        });
246        LineStats {
247            line_count,
248            max_line_width,
249        }
250    }
251
252    pub fn measure_natural_width(prepared: &PreparedTextWithSegments) -> f64 {
253        Self::measure_line_stats(prepared, f64::INFINITY).max_line_width
254    }
255}
256
257pub(crate) fn count_prepared_lines(prepared: &PreparedText, max_width: f64) -> usize {
258    walk_prepared_lines(prepared, max_width, |_| {})
259}
260
261/// A generic callback lets the count-only caller compile away output entirely.
262pub(crate) fn walk_prepared_lines(
263    prepared: &PreparedText,
264    max_width: f64,
265    mut on_line: impl FnMut(InternalLine),
266) -> usize {
267    if prepared.simple_line_walk_fast_path {
268        walk_prepared_lines_simple(prepared, max_width, &mut on_line)
269    } else {
270        walk_prepared_lines_general(prepared, max_width, &mut on_line)
271    }
272}
273
274fn append_breakable_walk(
275    state: &mut LineState,
276    prepared: &PreparedText,
277    start: LayoutCursor,
278    max_width: f64,
279    profile: EngineProfile,
280    on_line: &mut impl FnMut(InternalLine),
281    count: &mut usize,
282) {
283    let segment_index = start.segment_index;
284    let start_grapheme_index = start.grapheme_index;
285    let Some(widths) = prepared.breakable_widths[segment_index].as_deref() else {
286        return;
287    };
288    let prefixes = prepared.breakable_prefix_widths[segment_index].as_deref();
289    for grapheme_index in start_grapheme_index..widths.len() {
290        let width = get_breakable_advance(
291            widths,
292            prefixes,
293            grapheme_index,
294            profile.prefer_prefix_widths_for_breakable_runs,
295        );
296        if !state.has_content {
297            state.start_grapheme(segment_index, grapheme_index, width, true);
298        } else if state.width + width > max_width + profile.line_fit_epsilon {
299            state.emit(None, None, false, on_line, count);
300            state.start_grapheme(segment_index, grapheme_index, width, true);
301        } else {
302            state.width += width;
303            state.end = cursor(segment_index, grapheme_index + 1);
304        }
305    }
306    if state.has_content && state.end == cursor(segment_index, widths.len()) {
307        state.end = cursor(segment_index + 1, 0);
308    }
309}
310
311fn walk_prepared_lines_simple(
312    prepared: &PreparedText,
313    max_width: f64,
314    on_line: &mut impl FnMut(InternalLine),
315) -> usize {
316    let profile = get_engine_profile();
317    let epsilon = profile.line_fit_epsilon;
318    let mut count = 0;
319    let mut state = LineState::new(cursor(0, 0));
320    let mut index = 0;
321    while index < prepared.widths.len() {
322        if !state.has_content {
323            while index < prepared.widths.len() && is_skippable_at_line_start(prepared.kinds[index])
324            {
325                index += 1;
326            }
327            if index == prepared.widths.len() {
328                break;
329            }
330        }
331        let width = prepared.widths[index];
332        let kind = prepared.kinds[index];
333        if !state.has_content {
334            if width > max_width + epsilon && prepared.breakable_widths[index].is_some() {
335                append_breakable_walk(
336                    &mut state,
337                    prepared,
338                    cursor(index, 0),
339                    max_width,
340                    profile,
341                    on_line,
342                    &mut count,
343                );
344            } else {
345                state.start_segment(index, width, true);
346            }
347            state.update_pending(prepared, index, width, true);
348            index += 1;
349            continue;
350        }
351        if state.width + width > max_width + epsilon {
352            if can_break_after(kind) {
353                state.append_whole(index, width, true);
354                state.emit(
355                    Some(cursor(index + 1, 0)),
356                    Some(state.width - width),
357                    false,
358                    on_line,
359                    &mut count,
360                );
361                index += 1;
362                continue;
363            }
364            if let Some(pending) = state.pending {
365                if state.is_after_pending(pending) {
366                    state.emit(None, None, false, on_line, &mut count);
367                } else {
368                    state.emit(
369                        Some(cursor(pending.end_segment_index, 0)),
370                        Some(pending.paint_width),
371                        false,
372                        on_line,
373                        &mut count,
374                    );
375                }
376                continue;
377            }
378            state.emit(None, None, false, on_line, &mut count);
379            if width > max_width + epsilon && prepared.breakable_widths[index].is_some() {
380                append_breakable_walk(
381                    &mut state,
382                    prepared,
383                    cursor(index, 0),
384                    max_width,
385                    profile,
386                    on_line,
387                    &mut count,
388                );
389                index += 1;
390            }
391            continue;
392        }
393        state.append_whole(index, width, true);
394        state.update_pending(prepared, index, width, true);
395        index += 1;
396    }
397    state.emit(None, None, false, on_line, &mut count);
398    count
399}
400
401fn continue_soft_hyphen_walk(
402    state: &mut LineState,
403    prepared: &PreparedText,
404    index: usize,
405    max_width: f64,
406    profile: EngineProfile,
407    on_line: &mut impl FnMut(InternalLine),
408    count: &mut usize,
409) -> bool {
410    if !matches!(
411        state.pending,
412        Some(PendingBreak {
413            kind: SegmentBreakKind::SoftHyphen,
414            ..
415        })
416    ) {
417        return false;
418    }
419    let Some(widths) = prepared.breakable_widths[index].as_deref() else {
420        return false;
421    };
422    let prefixes = prepared.breakable_prefix_widths[index]
423        .as_deref()
424        .filter(|_| profile.prefer_prefix_widths_for_breakable_runs);
425    let (fit_count, fitted_width) = fit_soft_hyphen_break(
426        prefixes.unwrap_or(widths),
427        state.width,
428        max_width,
429        profile.line_fit_epsilon,
430        prepared.discretionary_hyphen_width,
431        prefixes.is_some(),
432    );
433    if fit_count == 0 {
434        return false;
435    }
436    state.width = fitted_width;
437    state.end = cursor(index, fit_count);
438    state.pending = None;
439    if fit_count == widths.len() {
440        state.end = cursor(index + 1, 0);
441    } else {
442        state.emit(
443            Some(cursor(index, fit_count)),
444            Some(fitted_width + prepared.discretionary_hyphen_width),
445            true,
446            on_line,
447            count,
448        );
449        append_breakable_walk(
450            state,
451            prepared,
452            cursor(index, fit_count),
453            max_width,
454            profile,
455            on_line,
456            count,
457        );
458    }
459    true
460}
461
462fn walk_prepared_lines_general(
463    prepared: &PreparedText,
464    max_width: f64,
465    on_line: &mut impl FnMut(InternalLine),
466) -> usize {
467    let profile = get_engine_profile();
468    let epsilon = profile.line_fit_epsilon;
469    let mut count = 0;
470    for chunk in &prepared.chunks {
471        if chunk.start_segment_index == chunk.end_segment_index {
472            let end = cursor(chunk.consumed_end_segment_index, 0);
473            on_line(InternalLine {
474                start: cursor(chunk.start_segment_index, 0),
475                visible_end: end,
476                end,
477                width: 0.0,
478                append_hyphen: false,
479            });
480            count += 1;
481            continue;
482        }
483        let mut state = LineState::new(cursor(chunk.start_segment_index, 0));
484        let mut index = chunk.start_segment_index;
485        while index < chunk.end_segment_index {
486            let kind = prepared.kinds[index];
487            let mut width = segment_advance(prepared, index, state.width);
488            if kind == SegmentBreakKind::SoftHyphen {
489                if state.has_content {
490                    state.end = cursor(index + 1, 0);
491                    let hyphen_width = state.width + prepared.discretionary_hyphen_width;
492                    state.pending = Some(PendingBreak {
493                        end_segment_index: index + 1,
494                        fit_width: hyphen_width,
495                        paint_width: hyphen_width,
496                        kind,
497                    });
498                }
499                index += 1;
500                continue;
501            }
502            if !state.has_content {
503                while index < chunk.end_segment_index
504                    && is_skippable_at_line_start(prepared.kinds[index])
505                {
506                    index += 1;
507                }
508                if index == chunk.end_segment_index {
509                    break;
510                }
511                width = segment_advance(prepared, index, state.width);
512                if width > max_width + epsilon && prepared.breakable_widths[index].is_some() {
513                    append_breakable_walk(
514                        &mut state,
515                        prepared,
516                        cursor(index, 0),
517                        max_width,
518                        profile,
519                        on_line,
520                        &mut count,
521                    );
522                } else {
523                    state.start_segment(index, width, true);
524                }
525                state.update_pending(prepared, index, width, false);
526                index += 1;
527                continue;
528            }
529            if state.width + width > max_width + epsilon {
530                let current_fit = state.width
531                    + if kind == SegmentBreakKind::Tab {
532                        0.0
533                    } else {
534                        prepared.line_end_fit_advances[index]
535                    };
536                let current_paint = state.width
537                    + if kind == SegmentBreakKind::Tab {
538                        width
539                    } else {
540                        prepared.line_end_paint_advances[index]
541                    };
542                if let Some(pending) = state.pending.filter(|p| {
543                    p.kind == SegmentBreakKind::SoftHyphen
544                        && profile.prefer_early_soft_hyphen_break
545                        && p.fit_width <= max_width + epsilon
546                }) {
547                    state.emit(
548                        Some(cursor(pending.end_segment_index, 0)),
549                        Some(pending.paint_width),
550                        true,
551                        on_line,
552                        &mut count,
553                    );
554                    continue;
555                }
556                if continue_soft_hyphen_walk(
557                    &mut state, prepared, index, max_width, profile, on_line, &mut count,
558                ) {
559                    index += 1;
560                    continue;
561                }
562                if can_break_after(kind) && current_fit <= max_width + epsilon {
563                    state.append_whole(index, width, true);
564                    state.emit(
565                        Some(cursor(index + 1, 0)),
566                        Some(current_paint),
567                        false,
568                        on_line,
569                        &mut count,
570                    );
571                    index += 1;
572                    continue;
573                }
574                if let Some(pending) = state.pending.filter(|p| p.fit_width <= max_width + epsilon)
575                {
576                    if state.is_after_pending(pending) {
577                        state.emit(None, None, false, on_line, &mut count);
578                    } else {
579                        state.emit(
580                            Some(cursor(pending.end_segment_index, 0)),
581                            Some(pending.paint_width),
582                            pending.kind == SegmentBreakKind::SoftHyphen,
583                            on_line,
584                            &mut count,
585                        );
586                    }
587                    continue;
588                }
589                state.emit(None, None, false, on_line, &mut count);
590                if width > max_width + epsilon && prepared.breakable_widths[index].is_some() {
591                    append_breakable_walk(
592                        &mut state,
593                        prepared,
594                        cursor(index, 0),
595                        max_width,
596                        profile,
597                        on_line,
598                        &mut count,
599                    );
600                    index += 1;
601                }
602                continue;
603            }
604            state.append_whole(index, width, true);
605            state.update_pending(prepared, index, width, false);
606            index += 1;
607        }
608        let paint = state
609            .pending
610            .filter(|p| p.end_segment_index == chunk.consumed_end_segment_index)
611            .map_or(state.width, |p| p.paint_width);
612        state.emit(
613            Some(cursor(chunk.consumed_end_segment_index, 0)),
614            Some(paint),
615            false,
616            on_line,
617            &mut count,
618        );
619    }
620    count
621}
622
623pub(crate) fn try_step_line(
624    prepared: &PreparedText,
625    start: LayoutCursor,
626    max_width: f64,
627) -> Option<InternalLine> {
628    let normalized = normalize_line_start(prepared, start)?;
629    if prepared.simple_line_walk_fast_path {
630        try_step_line_simple(prepared, normalized, max_width)
631    } else {
632        try_step_line_general(prepared, normalized, max_width)
633    }
634}
635
636fn append_breakable_step(
637    state: &mut LineState,
638    prepared: &PreparedText,
639    index: usize,
640    first: usize,
641    max_width: f64,
642    profile: EngineProfile,
643) -> Option<InternalLine> {
644    let widths = prepared.breakable_widths[index].as_deref()?;
645    let prefixes = prepared.breakable_prefix_widths[index].as_deref();
646    for grapheme_index in first..widths.len() {
647        let width = get_breakable_advance(
648            widths,
649            prefixes,
650            grapheme_index,
651            profile.prefer_prefix_widths_for_breakable_runs,
652        );
653        if !state.has_content {
654            state.start_grapheme(index, grapheme_index, width, false);
655        } else if state.width + width > max_width + profile.line_fit_epsilon {
656            return state.finish(None, None, false);
657        } else {
658            state.width += width;
659            state.end = cursor(index, grapheme_index + 1);
660        }
661    }
662    if state.has_content && state.end == cursor(index, widths.len()) {
663        state.end = cursor(index + 1, 0);
664    }
665    None
666}
667
668fn try_step_line_simple(
669    prepared: &PreparedText,
670    start: LayoutCursor,
671    max_width: f64,
672) -> Option<InternalLine> {
673    let profile = get_engine_profile();
674    let epsilon = profile.line_fit_epsilon;
675    let mut state = LineState::new(start);
676    for index in start.segment_index..prepared.widths.len() {
677        let width = prepared.widths[index];
678        let kind = prepared.kinds[index];
679        let first = if index == start.segment_index {
680            start.grapheme_index
681        } else {
682            0
683        };
684        if !state.has_content {
685            if first > 0
686                || (width > max_width + epsilon && prepared.breakable_widths[index].is_some())
687            {
688                if let Some(line) =
689                    append_breakable_step(&mut state, prepared, index, first, max_width, profile)
690                {
691                    return Some(line);
692                }
693            } else {
694                state.start_segment(index, width, false);
695            }
696            state.update_pending(prepared, index, width, true);
697            continue;
698        }
699        if state.width + width > max_width + epsilon {
700            if can_break_after(kind) {
701                state.append_whole(index, width, false);
702                return state.finish(Some(cursor(index + 1, 0)), Some(state.width - width), false);
703            }
704            if let Some(pending) = state.pending {
705                return if state.is_after_pending(pending) {
706                    state.finish(None, None, false)
707                } else {
708                    state.finish(
709                        Some(cursor(pending.end_segment_index, 0)),
710                        Some(pending.paint_width),
711                        false,
712                    )
713                };
714            }
715            return state.finish(None, None, false);
716        }
717        state.append_whole(index, width, false);
718        state.update_pending(prepared, index, width, true);
719    }
720    state.finish(None, None, false)
721}
722
723fn maybe_finish_at_soft_hyphen(
724    state: &mut LineState,
725    prepared: &PreparedText,
726    index: usize,
727    max_width: f64,
728    profile: EngineProfile,
729) -> Option<InternalLine> {
730    let pending = state
731        .pending
732        .filter(|p| p.kind == SegmentBreakKind::SoftHyphen)?;
733    if let Some(widths) = prepared.breakable_widths[index].as_deref() {
734        let prefixes = prepared.breakable_prefix_widths[index]
735            .as_deref()
736            .filter(|_| profile.prefer_prefix_widths_for_breakable_runs);
737        let (fit_count, fitted_width) = fit_soft_hyphen_break(
738            prefixes.unwrap_or(widths),
739            state.width,
740            max_width,
741            profile.line_fit_epsilon,
742            prepared.discretionary_hyphen_width,
743            prefixes.is_some(),
744        );
745        if fit_count == widths.len() {
746            state.width = fitted_width;
747            state.end = cursor(index + 1, 0);
748            state.pending = None;
749            return None;
750        }
751        if fit_count > 0 {
752            return state.finish(
753                Some(cursor(index, fit_count)),
754                Some(fitted_width + prepared.discretionary_hyphen_width),
755                true,
756            );
757        }
758    }
759    if pending.fit_width <= max_width + profile.line_fit_epsilon {
760        return state.finish(
761            Some(cursor(pending.end_segment_index, 0)),
762            Some(pending.paint_width),
763            true,
764        );
765    }
766    None
767}
768
769fn try_step_line_general(
770    prepared: &PreparedText,
771    start: LayoutCursor,
772    max_width: f64,
773) -> Option<InternalLine> {
774    let chunk = &prepared.chunks[find_chunk_index_for_start(prepared, start.segment_index)?];
775    if chunk.start_segment_index == chunk.end_segment_index {
776        let end = cursor(chunk.consumed_end_segment_index, 0);
777        return Some(InternalLine {
778            start: cursor(chunk.start_segment_index, 0),
779            visible_end: end,
780            end,
781            width: 0.0,
782            append_hyphen: false,
783        });
784    }
785    let profile = get_engine_profile();
786    let epsilon = profile.line_fit_epsilon;
787    let mut state = LineState::new(start);
788    for index in start.segment_index..chunk.end_segment_index {
789        let kind = prepared.kinds[index];
790        let first = if index == start.segment_index {
791            start.grapheme_index
792        } else {
793            0
794        };
795        let width = segment_advance(prepared, index, state.width);
796        if kind == SegmentBreakKind::SoftHyphen && first == 0 {
797            if state.has_content {
798                state.end = cursor(index + 1, 0);
799                let hyphen_width = state.width + prepared.discretionary_hyphen_width;
800                state.pending = Some(PendingBreak {
801                    end_segment_index: index + 1,
802                    fit_width: hyphen_width,
803                    paint_width: hyphen_width,
804                    kind,
805                });
806            }
807            continue;
808        }
809        if !state.has_content {
810            if first > 0
811                || (width > max_width + epsilon && prepared.breakable_widths[index].is_some())
812            {
813                if let Some(line) =
814                    append_breakable_step(&mut state, prepared, index, first, max_width, profile)
815                {
816                    return Some(line);
817                }
818            } else {
819                state.start_segment(index, width, false);
820            }
821            state.update_pending(prepared, index, width, false);
822            continue;
823        }
824        if state.width + width > max_width + epsilon {
825            let current_fit = state.width
826                + if kind == SegmentBreakKind::Tab {
827                    0.0
828                } else {
829                    prepared.line_end_fit_advances[index]
830                };
831            let current_paint = state.width
832                + if kind == SegmentBreakKind::Tab {
833                    width
834                } else {
835                    prepared.line_end_paint_advances[index]
836                };
837            if let Some(pending) = state.pending.filter(|p| {
838                p.kind == SegmentBreakKind::SoftHyphen
839                    && profile.prefer_early_soft_hyphen_break
840                    && p.fit_width <= max_width + epsilon
841            }) {
842                return state.finish(
843                    Some(cursor(pending.end_segment_index, 0)),
844                    Some(pending.paint_width),
845                    true,
846                );
847            }
848            if let Some(line) =
849                maybe_finish_at_soft_hyphen(&mut state, prepared, index, max_width, profile)
850            {
851                return Some(line);
852            }
853            if can_break_after(kind) && current_fit <= max_width + epsilon {
854                state.append_whole(index, width, false);
855                return state.finish(Some(cursor(index + 1, 0)), Some(current_paint), false);
856            }
857            if let Some(pending) = state.pending.filter(|p| p.fit_width <= max_width + epsilon) {
858                return if state.is_after_pending(pending) {
859                    state.finish(None, None, false)
860                } else {
861                    state.finish(
862                        Some(cursor(pending.end_segment_index, 0)),
863                        Some(pending.paint_width),
864                        pending.kind == SegmentBreakKind::SoftHyphen,
865                    )
866                };
867            }
868            return state.finish(None, None, false);
869        }
870        state.append_whole(index, width, false);
871        state.update_pending(prepared, index, width, false);
872    }
873    let paint = state
874        .pending
875        .filter(|p| {
876            p.end_segment_index == chunk.consumed_end_segment_index && state.end.grapheme_index == 0
877        })
878        .map_or(state.width, |p| p.paint_width);
879    state.finish(
880        Some(cursor(chunk.consumed_end_segment_index, 0)),
881        Some(paint),
882        false,
883    )
884}
885
886fn find_chunk_index_for_start(prepared: &PreparedText, segment_index: usize) -> Option<usize> {
887    // Chunks are sorted and disjoint. Binary search preserves source behavior
888    // while avoiding a scan from paragraph start for every streamed line.
889    let index = prepared
890        .chunks
891        .partition_point(|chunk| chunk.consumed_end_segment_index <= segment_index);
892    (index < prepared.chunks.len()).then_some(index)
893}
894
895fn normalize_line_start(prepared: &PreparedText, start: LayoutCursor) -> Option<LayoutCursor> {
896    if start.segment_index >= prepared.widths.len() {
897        return None;
898    }
899    if start.grapheme_index > 0 {
900        let widths = prepared.breakable_widths[start.segment_index].as_ref()?;
901        return (start.grapheme_index < widths.len()).then_some(start);
902    }
903    let chunk = &prepared.chunks[find_chunk_index_for_start(prepared, start.segment_index)?];
904    let mut index = start.segment_index.max(chunk.start_segment_index);
905    if chunk.start_segment_index == chunk.end_segment_index && index == chunk.start_segment_index {
906        return Some(cursor(index, 0));
907    }
908    while index < chunk.end_segment_index {
909        if !is_skippable_at_line_start(prepared.kinds[index]) {
910            return Some(cursor(index, 0));
911        }
912        index += 1;
913    }
914    (chunk.consumed_end_segment_index < prepared.widths.len())
915        .then_some(cursor(chunk.consumed_end_segment_index, 0))
916}
917
918pub(crate) fn is_skippable_at_line_start(kind: SegmentBreakKind) -> bool {
919    matches!(
920        kind,
921        SegmentBreakKind::Space | SegmentBreakKind::ZeroWidthBreak | SegmentBreakKind::SoftHyphen
922    )
923}
924
925fn get_breakable_advance(
926    widths: &[f64],
927    prefixes: Option<&[f64]>,
928    index: usize,
929    prefer_prefixes: bool,
930) -> f64 {
931    if let Some(prefixes) = prefixes.filter(|_| prefer_prefixes) {
932        prefixes[index] - if index > 0 { prefixes[index - 1] } else { 0.0 }
933    } else {
934        widths[index]
935    }
936}
937
938fn fit_soft_hyphen_break(
939    widths: &[f64],
940    initial_width: f64,
941    max_width: f64,
942    epsilon: f64,
943    hyphen_width: f64,
944    cumulative: bool,
945) -> (usize, f64) {
946    let mut fit_count = 0;
947    let mut fitted_width = initial_width;
948    while fit_count < widths.len() {
949        let next_width = if cumulative {
950            initial_width + widths[fit_count]
951        } else {
952            fitted_width + widths[fit_count]
953        };
954        let next_line_width = if fit_count + 1 < widths.len() {
955            next_width + hyphen_width
956        } else {
957            next_width
958        };
959        if next_line_width > max_width + epsilon {
960            break;
961        }
962        fitted_width = next_width;
963        fit_count += 1;
964    }
965    (fit_count, fitted_width)
966}
967
968pub(crate) fn can_break_after(kind: SegmentBreakKind) -> bool {
969    matches!(
970        kind,
971        SegmentBreakKind::Space
972            | SegmentBreakKind::PreservedSpace
973            | SegmentBreakKind::Tab
974            | SegmentBreakKind::ZeroWidthBreak
975            | SegmentBreakKind::SoftHyphen
976    )
977}
978
979fn segment_advance(prepared: &PreparedText, index: usize, line_width: f64) -> f64 {
980    if prepared.kinds[index] == SegmentBreakKind::Tab {
981        get_tab_advance(line_width, prepared.tab_stop_advance)
982    } else {
983        prepared.widths[index]
984    }
985}
986
987fn get_tab_advance(line_width: f64, stop: f64) -> f64 {
988    if stop <= 0.0 {
989        return 0.0;
990    }
991    let remainder = line_width % stop;
992    if remainder.abs() <= 1e-6 {
993        stop
994    } else {
995        stop - remainder
996    }
997}