Skip to main content

cranpose_ui/text/
measure.rs

1use crate::text_layout_result::TextLayoutResult;
2use cranpose_core::NodeId;
3use std::borrow::Cow;
4use std::cell::RefCell;
5use std::ops::Range;
6
7use super::layout_options::{TextLayoutOptions, TextOverflow};
8use super::paragraph::{Hyphens, LineBreak};
9use super::style::TextStyle;
10
11const ELLIPSIS: &str = "\u{2026}";
12const DEFAULT_FONT_SIZE_SP: f32 = 14.0;
13const WRAP_EPSILON: f32 = 0.5;
14const SCALE_DOWN_SEARCH_STEPS: usize = 14;
15const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
16const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
17const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
18
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct TextMetrics {
21    pub width: f32,
22    pub height: f32,
23    /// Height of a single line of text
24    pub line_height: f32,
25    /// Number of lines in the text
26    pub line_count: usize,
27}
28
29#[derive(Clone, Debug, PartialEq)]
30pub struct PreparedTextLayout {
31    pub text: crate::text::AnnotatedString,
32    pub visual_style: TextStyle,
33    pub metrics: TextMetrics,
34    pub did_overflow: bool,
35}
36
37pub trait TextMeasurer: 'static {
38    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
39
40    fn measure_for_node(
41        &self,
42        node_id: Option<NodeId>,
43        text: &crate::text::AnnotatedString,
44        style: &TextStyle,
45    ) -> TextMetrics {
46        let _ = node_id;
47        self.measure(text, style)
48    }
49
50    fn measure_subsequence(
51        &self,
52        text: &crate::text::AnnotatedString,
53        range: Range<usize>,
54        style: &TextStyle,
55    ) -> TextMetrics {
56        self.measure(&text.subsequence(range), style)
57    }
58
59    fn measure_subsequence_for_node(
60        &self,
61        node_id: Option<NodeId>,
62        text: &crate::text::AnnotatedString,
63        range: Range<usize>,
64        style: &TextStyle,
65    ) -> TextMetrics {
66        let _ = node_id;
67        self.measure_subsequence(text, range, style)
68    }
69
70    fn get_offset_for_position(
71        &self,
72        text: &crate::text::AnnotatedString,
73        style: &TextStyle,
74        x: f32,
75        y: f32,
76    ) -> usize;
77
78    fn get_cursor_x_for_offset(
79        &self,
80        text: &crate::text::AnnotatedString,
81        style: &TextStyle,
82        offset: usize,
83    ) -> f32;
84
85    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
86
87    /// Returns an alternate break boundary for `Hyphens::Auto` when a greedy break
88    /// split lands in the middle of a word.
89    ///
90    /// `segment_start_char` and `measured_break_char` are character-boundary indices
91    /// in `line` (not byte offsets). Return `None` to delegate to fallback behavior.
92    fn choose_auto_hyphen_break(
93        &self,
94        _line: &str,
95        _style: &TextStyle,
96        _segment_start_char: usize,
97        _measured_break_char: usize,
98    ) -> Option<usize> {
99        None
100    }
101
102    fn measure_with_options(
103        &self,
104        text: &crate::text::AnnotatedString,
105        style: &TextStyle,
106        options: TextLayoutOptions,
107        max_width: Option<f32>,
108    ) -> TextMetrics {
109        self.prepare_with_options(text, style, options, max_width)
110            .metrics
111    }
112
113    fn measure_with_options_for_node(
114        &self,
115        node_id: Option<NodeId>,
116        text: &crate::text::AnnotatedString,
117        style: &TextStyle,
118        options: TextLayoutOptions,
119        max_width: Option<f32>,
120    ) -> TextMetrics {
121        self.prepare_with_options_for_node(node_id, text, style, options, max_width)
122            .metrics
123    }
124
125    fn prepare_with_options(
126        &self,
127        text: &crate::text::AnnotatedString,
128        style: &TextStyle,
129        options: TextLayoutOptions,
130        max_width: Option<f32>,
131    ) -> PreparedTextLayout {
132        self.prepare_with_options_fallback(text, style, options, max_width)
133    }
134
135    fn prepare_with_options_for_node(
136        &self,
137        node_id: Option<NodeId>,
138        text: &crate::text::AnnotatedString,
139        style: &TextStyle,
140        options: TextLayoutOptions,
141        max_width: Option<f32>,
142    ) -> PreparedTextLayout {
143        prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
144    }
145
146    fn prepare_with_options_fallback(
147        &self,
148        text: &crate::text::AnnotatedString,
149        style: &TextStyle,
150        options: TextLayoutOptions,
151        max_width: Option<f32>,
152    ) -> PreparedTextLayout {
153        prepare_text_layout_fallback(self, text, style, options, max_width)
154    }
155}
156
157#[derive(Default)]
158struct MonospacedTextMeasurer;
159
160impl MonospacedTextMeasurer {
161    const DEFAULT_SIZE: f32 = 14.0;
162    const CHAR_WIDTH_RATIO: f32 = 0.6; // Width is 0.6 of Height
163
164    fn get_metrics(style: &TextStyle) -> (f32, f32) {
165        let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
166        let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
167        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
168        (
169            (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
170            line_height,
171        )
172    }
173}
174
175impl TextMeasurer for MonospacedTextMeasurer {
176    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
177        let (char_width, line_height) = Self::get_metrics(style);
178
179        let lines: Vec<&str> = text.text.split('\n').collect();
180        let line_count = lines.len().max(1);
181
182        let width = lines
183            .iter()
184            .map(|line| line.chars().count() as f32 * char_width)
185            .fold(0.0_f32, f32::max);
186
187        TextMetrics {
188            width,
189            height: line_count as f32 * line_height,
190            line_height,
191            line_count,
192        }
193    }
194
195    fn measure_subsequence(
196        &self,
197        text: &crate::text::AnnotatedString,
198        range: Range<usize>,
199        style: &TextStyle,
200    ) -> TextMetrics {
201        let (char_width, line_height) = Self::get_metrics(style);
202        let slice = &text.text[range];
203        let line_count = slice.split('\n').count().max(1);
204        let width = slice
205            .split('\n')
206            .map(|line| line.chars().count() as f32 * char_width)
207            .fold(0.0_f32, f32::max);
208
209        TextMetrics {
210            width,
211            height: line_count as f32 * line_height,
212            line_height,
213            line_count,
214        }
215    }
216
217    fn get_offset_for_position(
218        &self,
219        text: &crate::text::AnnotatedString,
220        style: &TextStyle,
221        x: f32,
222        y: f32,
223    ) -> usize {
224        let (char_width, line_height) = Self::get_metrics(style);
225
226        if text.text.is_empty() {
227            return 0;
228        }
229
230        let line_index = (y / line_height).floor().max(0.0) as usize;
231        let lines: Vec<&str> = text.text.split('\n').collect();
232        let target_line = line_index.min(lines.len().saturating_sub(1));
233
234        let mut line_start_byte = 0;
235        for line in lines.iter().take(target_line) {
236            line_start_byte += line.len() + 1;
237        }
238
239        let line_text = lines.get(target_line).unwrap_or(&"");
240        let char_index = (x / char_width).round() as usize;
241        let line_char_count = line_text.chars().count();
242        let clamped_index = char_index.min(line_char_count);
243
244        let offset_in_line = line_text
245            .char_indices()
246            .nth(clamped_index)
247            .map(|(i, _)| i)
248            .unwrap_or(line_text.len());
249
250        line_start_byte + offset_in_line
251    }
252
253    fn get_cursor_x_for_offset(
254        &self,
255        text: &crate::text::AnnotatedString,
256        style: &TextStyle,
257        offset: usize,
258    ) -> f32 {
259        let (char_width, _) = Self::get_metrics(style);
260
261        let clamped_offset = offset.min(text.text.len());
262        let char_count = text.text[..clamped_offset].chars().count();
263        char_count as f32 * char_width
264    }
265
266    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
267        let (char_width, line_height) = Self::get_metrics(style);
268        TextLayoutResult::monospaced(&text.text, char_width, line_height)
269    }
270}
271
272thread_local! {
273    static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
274}
275
276pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
277    TEXT_MEASURER.with(|m| {
278        *m.borrow_mut() = Box::new(measurer);
279    });
280}
281
282pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
283    TEXT_MEASURER.with(|m| m.borrow().measure(text, style))
284}
285
286pub fn measure_text_for_node(
287    node_id: Option<NodeId>,
288    text: &crate::text::AnnotatedString,
289    style: &TextStyle,
290) -> TextMetrics {
291    TEXT_MEASURER.with(|m| m.borrow().measure_for_node(node_id, text, style))
292}
293
294pub fn measure_text_with_options(
295    text: &crate::text::AnnotatedString,
296    style: &TextStyle,
297    options: TextLayoutOptions,
298    max_width: Option<f32>,
299) -> TextMetrics {
300    TEXT_MEASURER.with(|m| {
301        m.borrow()
302            .measure_with_options(text, style, options.normalized(), max_width)
303    })
304}
305
306pub fn measure_text_with_options_for_node(
307    node_id: Option<NodeId>,
308    text: &crate::text::AnnotatedString,
309    style: &TextStyle,
310    options: TextLayoutOptions,
311    max_width: Option<f32>,
312) -> TextMetrics {
313    TEXT_MEASURER.with(|m| {
314        m.borrow().measure_with_options_for_node(
315            node_id,
316            text,
317            style,
318            options.normalized(),
319            max_width,
320        )
321    })
322}
323
324pub fn prepare_text_layout(
325    text: &crate::text::AnnotatedString,
326    style: &TextStyle,
327    options: TextLayoutOptions,
328    max_width: Option<f32>,
329) -> PreparedTextLayout {
330    TEXT_MEASURER.with(|m| {
331        m.borrow()
332            .prepare_with_options(text, style, options.normalized(), max_width)
333    })
334}
335
336pub fn prepare_text_layout_for_node(
337    node_id: Option<NodeId>,
338    text: &crate::text::AnnotatedString,
339    style: &TextStyle,
340    options: TextLayoutOptions,
341    max_width: Option<f32>,
342) -> PreparedTextLayout {
343    TEXT_MEASURER.with(|m| {
344        m.borrow().prepare_with_options_for_node(
345            node_id,
346            text,
347            style,
348            options.normalized(),
349            max_width,
350        )
351    })
352}
353
354pub fn get_offset_for_position(
355    text: &crate::text::AnnotatedString,
356    style: &TextStyle,
357    x: f32,
358    y: f32,
359) -> usize {
360    TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, style, x, y))
361}
362
363pub fn get_cursor_x_for_offset(
364    text: &crate::text::AnnotatedString,
365    style: &TextStyle,
366    offset: usize,
367) -> f32 {
368    TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, style, offset))
369}
370
371pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
372    TEXT_MEASURER.with(|m| m.borrow().layout(text, style))
373}
374
375fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
376    measurer: &M,
377    text: &crate::text::AnnotatedString,
378    style: &TextStyle,
379    options: TextLayoutOptions,
380    max_width: Option<f32>,
381) -> PreparedTextLayout {
382    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
383}
384
385pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
386    measurer: &M,
387    node_id: Option<NodeId>,
388    text: &crate::text::AnnotatedString,
389    style: &TextStyle,
390    options: TextLayoutOptions,
391    max_width: Option<f32>,
392) -> PreparedTextLayout {
393    let opts = options.normalized();
394    let max_width = normalize_max_width(max_width);
395    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
396        return prepare_scale_down_text_layout(
397            measurer,
398            node_id,
399            text,
400            style,
401            opts,
402            max_width,
403            min_font_size_sp,
404        );
405    }
406
407    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
408        .then_some(max_width)
409        .flatten();
410    let line_break_mode = style
411        .paragraph_style
412        .line_break
413        .take_or_else(|| LineBreak::Simple);
414    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
415
416    let mut line_ranges = split_line_ranges(text.text.as_str());
417    if let Some(width_limit) = wrap_width {
418        let mut wrapped_ranges = Vec::with_capacity(line_ranges.len());
419        for line_range in line_ranges.drain(..) {
420            let wrapped_line_ranges = wrap_line_to_width(
421                measurer,
422                text,
423                line_range,
424                style,
425                width_limit,
426                line_break_mode,
427                hyphens_mode,
428            );
429            wrapped_ranges.extend(wrapped_line_ranges);
430        }
431        line_ranges = wrapped_ranges;
432    }
433
434    let mut did_overflow = false;
435    let mut visible_lines: Vec<DisplayLine> = line_ranges
436        .into_iter()
437        .map(DisplayLine::from_source_range)
438        .collect();
439
440    if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
441        did_overflow = true;
442        visible_lines.truncate(opts.max_lines);
443        if let Some(last_line) = visible_lines.last_mut() {
444            let overflowed = apply_line_overflow(
445                measurer,
446                last_line.display_text(text),
447                style,
448                max_width,
449                opts,
450                true,
451                true,
452            );
453            last_line.apply_display_text(text, overflowed);
454        }
455    }
456
457    if let Some(width_limit) = max_width {
458        let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
459        let visible_len = visible_lines.len();
460        for (line_index, line) in visible_lines.iter_mut().enumerate() {
461            let width = line.measure_width(measurer, node_id, text, style);
462            if width > width_limit + WRAP_EPSILON {
463                if opts.overflow == TextOverflow::Visible {
464                    continue;
465                }
466                did_overflow = true;
467                let overflowed = apply_line_overflow(
468                    measurer,
469                    line.display_text(text),
470                    style,
471                    Some(width_limit),
472                    opts,
473                    line_index + 1 == visible_len,
474                    single_line_ellipsis,
475                );
476                line.apply_display_text(text, overflowed);
477            }
478        }
479    }
480
481    let display_annotated = build_display_annotated(text, &visible_lines);
482    debug_assert_eq!(
483        display_annotated.text,
484        join_display_line_text(text, &visible_lines)
485    );
486    let line_height = measurer
487        .measure_for_node(node_id, text, style)
488        .line_height
489        .max(0.0);
490    let display_line_count = visible_lines.len().max(1);
491    let layout_line_count = display_line_count.max(opts.min_lines);
492
493    let measured_width = if visible_lines.is_empty() {
494        0.0
495    } else {
496        visible_lines
497            .iter()
498            .map(|line| line.measure_width(measurer, node_id, text, style))
499            .fold(0.0_f32, f32::max)
500    };
501    let width = if opts.overflow == TextOverflow::Visible {
502        measured_width
503    } else if let Some(width_limit) = max_width {
504        measured_width.min(width_limit)
505    } else {
506        measured_width
507    };
508
509    PreparedTextLayout {
510        text: display_annotated,
511        visual_style: style.clone(),
512        metrics: TextMetrics {
513            width,
514            height: layout_line_count as f32 * line_height,
515            line_height,
516            line_count: layout_line_count,
517        },
518        did_overflow,
519    }
520}
521
522fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
523    measurer: &M,
524    node_id: Option<NodeId>,
525    text: &crate::text::AnnotatedString,
526    style: &TextStyle,
527    options: TextLayoutOptions,
528    max_width: Option<f32>,
529    min_font_size_sp: f32,
530) -> PreparedTextLayout {
531    let clipped_options = TextLayoutOptions {
532        overflow: TextOverflow::Clip,
533        ..options
534    }
535    .normalized();
536
537    let full_size = prepare_scaled_text_layout(
538        measurer,
539        node_id,
540        text,
541        style,
542        clipped_options,
543        max_width,
544        1.0,
545    );
546    let Some(width_limit) = max_width else {
547        return full_size;
548    };
549    if !full_size.did_overflow {
550        return full_size;
551    }
552
553    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
554    if !base_font_size.is_finite() || base_font_size <= 0.0 {
555        return full_size;
556    }
557    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
558    if min_scale >= 1.0 {
559        return full_size;
560    }
561
562    let min_size = prepare_scaled_text_layout(
563        measurer,
564        node_id,
565        text,
566        style,
567        clipped_options,
568        Some(width_limit),
569        min_scale,
570    );
571    if min_size.did_overflow {
572        return min_size;
573    }
574
575    let mut low = min_scale;
576    let mut high = 1.0;
577    let mut best = min_size;
578    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
579        let mid = (low + high) * 0.5;
580        let candidate = prepare_scaled_text_layout(
581            measurer,
582            node_id,
583            text,
584            style,
585            clipped_options,
586            Some(width_limit),
587            mid,
588        );
589        if candidate.did_overflow {
590            high = mid;
591        } else {
592            low = mid;
593            best = candidate;
594        }
595    }
596
597    best
598}
599
600fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
601    measurer: &M,
602    node_id: Option<NodeId>,
603    text: &crate::text::AnnotatedString,
604    style: &TextStyle,
605    options: TextLayoutOptions,
606    max_width: Option<f32>,
607    font_scale: f32,
608) -> PreparedTextLayout {
609    let visual_style = scale_text_style_font_sizes(style, font_scale);
610    let visual_text = scale_annotated_font_sizes(text, font_scale);
611    prepare_text_layout_with_measurer_for_node(
612        measurer,
613        node_id,
614        visual_text.as_ref(),
615        &visual_style,
616        options,
617        max_width,
618    )
619}
620
621fn scale_annotated_font_sizes(
622    text: &crate::text::AnnotatedString,
623    factor: f32,
624) -> Cow<'_, crate::text::AnnotatedString> {
625    if is_identity_scale(factor) || !annotated_text_needs_scaling(text) {
626        return Cow::Borrowed(text);
627    }
628
629    let mut scaled = text.clone();
630    for span in &mut scaled.span_styles {
631        span.item = scale_span_style_font_sizes(&span.item, factor, None);
632    }
633    Cow::Owned(scaled)
634}
635
636fn scale_text_style_font_sizes(style: &TextStyle, factor: f32) -> TextStyle {
637    if is_identity_scale(factor) {
638        return style.clone();
639    }
640
641    let mut scaled = style.clone();
642    scaled.span_style =
643        scale_span_style_font_sizes(&style.span_style, factor, Some(DEFAULT_FONT_SIZE_SP));
644    scaled.paragraph_style.line_height =
645        scale_text_unit_sp(scaled.paragraph_style.line_height, factor);
646    if let Some(mut indent) = scaled.paragraph_style.text_indent {
647        indent.first_line = scale_text_unit_sp(indent.first_line, factor);
648        indent.rest_line = scale_text_unit_sp(indent.rest_line, factor);
649        scaled.paragraph_style.text_indent = Some(indent);
650    }
651    scaled
652}
653
654fn scale_span_style_font_sizes(
655    style: &crate::text::SpanStyle,
656    factor: f32,
657    default_font_size_sp: Option<f32>,
658) -> crate::text::SpanStyle {
659    let mut scaled = style.clone();
660    scaled.font_size = match (style.font_size, default_font_size_sp) {
661        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
662            crate::text::TextUnit::Sp(default_size * factor)
663        }
664        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, factor),
665        (unit, None) => scale_text_unit_sp(unit, factor),
666    };
667    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, factor);
668    if let Some(mut shadow) = scaled.shadow {
669        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
670        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
671        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
672        scaled.shadow = Some(shadow);
673    }
674    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
675        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
676            width: width * factor,
677        });
678    }
679    scaled
680}
681
682fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
683    text.span_styles
684        .iter()
685        .any(|span| span_style_needs_scaling(&span.item))
686}
687
688fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
689    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
690        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
691        || matches!(
692            style.draw_style,
693            Some(crate::text::TextDrawStyle::Stroke { .. })
694        )
695        || style.shadow.is_some()
696}
697
698fn scale_text_unit_sp(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
699    match unit {
700        crate::text::TextUnit::Sp(value) if value.is_finite() => {
701            crate::text::TextUnit::Sp(value * factor)
702        }
703        other => other,
704    }
705}
706
707fn scale_text_unit_sp_and_em(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
708    match unit {
709        crate::text::TextUnit::Sp(value) if value.is_finite() => {
710            crate::text::TextUnit::Sp(value * factor)
711        }
712        crate::text::TextUnit::Em(value) if value.is_finite() => {
713            crate::text::TextUnit::Em(value * factor)
714        }
715        other => other,
716    }
717}
718
719fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
720    if value.is_finite() {
721        value * factor
722    } else {
723        value
724    }
725}
726
727fn is_identity_scale(factor: f32) -> bool {
728    (factor - 1.0).abs() <= f32::EPSILON
729}
730
731#[derive(Clone, Debug)]
732enum DisplayLineText {
733    Source,
734    Remapped(crate::text::AnnotatedString),
735}
736
737#[derive(Clone, Debug)]
738struct DisplayLine {
739    source_range: Range<usize>,
740    text: DisplayLineText,
741}
742
743impl DisplayLine {
744    fn from_source_range(source_range: Range<usize>) -> Self {
745        Self {
746            source_range,
747            text: DisplayLineText::Source,
748        }
749    }
750
751    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
752        match &self.text {
753            DisplayLineText::Source => &source.text[self.source_range.clone()],
754            DisplayLineText::Remapped(annotated) => annotated.text.as_str(),
755        }
756    }
757
758    fn measure_width<M: TextMeasurer + ?Sized>(
759        &self,
760        measurer: &M,
761        node_id: Option<NodeId>,
762        source: &crate::text::AnnotatedString,
763        style: &TextStyle,
764    ) -> f32 {
765        match &self.text {
766            DisplayLineText::Source => {
767                measurer
768                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
769                    .width
770            }
771            DisplayLineText::Remapped(annotated) => {
772                measurer.measure_for_node(node_id, annotated, style).width
773            }
774        }
775    }
776
777    fn apply_display_text(&mut self, source: &crate::text::AnnotatedString, display_text: String) {
778        let source_text = &source.text[self.source_range.clone()];
779        self.text = if source_text == display_text {
780            DisplayLineText::Source
781        } else {
782            DisplayLineText::Remapped(remap_annotated_subsequence_for_display(
783                source,
784                self.source_range.clone(),
785                display_text.as_str(),
786            ))
787        };
788    }
789}
790
791fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
792    if text.is_empty() {
793        return single_line_range(0..0);
794    }
795
796    let mut ranges = Vec::new();
797    let mut start = 0usize;
798    for (idx, ch) in text.char_indices() {
799        if ch == '\n' {
800            ranges.push(start..idx);
801            start = idx + ch.len_utf8();
802        }
803    }
804    ranges.push(start..text.len());
805    ranges
806}
807
808fn build_display_annotated(
809    source: &crate::text::AnnotatedString,
810    lines: &[DisplayLine],
811) -> crate::text::AnnotatedString {
812    if lines.is_empty() {
813        return crate::text::AnnotatedString::from("");
814    }
815
816    let mut builder = crate::text::AnnotatedString::builder();
817    for (idx, line) in lines.iter().enumerate() {
818        builder = match &line.text {
819            DisplayLineText::Source => {
820                builder.append_annotated_subsequence(source, line.source_range.clone())
821            }
822            DisplayLineText::Remapped(annotated) => builder.append_annotated(annotated),
823        };
824        if idx + 1 < lines.len() {
825            builder = builder.append("\n");
826        }
827    }
828    builder.to_annotated_string()
829}
830
831fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
832    let mut text = String::new();
833    for (idx, line) in lines.iter().enumerate() {
834        text.push_str(line.display_text(source));
835        if idx + 1 < lines.len() {
836            text.push('\n');
837        }
838    }
839    text
840}
841
842fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
843    while end > start {
844        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
845            break;
846        };
847        if ch.is_whitespace() {
848            end = start + idx;
849        } else {
850            break;
851        }
852    }
853    end
854}
855
856fn remap_annotated_subsequence_for_display(
857    source: &crate::text::AnnotatedString,
858    source_range: Range<usize>,
859    display_text: &str,
860) -> crate::text::AnnotatedString {
861    let source_text = &source.text[source_range.clone()];
862    if source_text == display_text {
863        return source.subsequence(source_range);
864    }
865
866    let display_chars = map_display_chars_to_source(source_text, display_text);
867    crate::text::AnnotatedString {
868        text: display_text.to_string(),
869        span_styles: remap_subsequence_range_styles(
870            &source.span_styles,
871            source_range.clone(),
872            &display_chars,
873        ),
874        paragraph_styles: remap_subsequence_range_styles(
875            &source.paragraph_styles,
876            source_range.clone(),
877            &display_chars,
878        ),
879        string_annotations: remap_subsequence_range_styles(
880            &source.string_annotations,
881            source_range.clone(),
882            &display_chars,
883        ),
884        link_annotations: remap_subsequence_range_styles(
885            &source.link_annotations,
886            source_range,
887            &display_chars,
888        ),
889    }
890}
891
892#[derive(Clone, Copy)]
893struct DisplayCharMap {
894    display_start: usize,
895    display_end: usize,
896    source_start: Option<usize>,
897}
898
899fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
900    let source_chars: Vec<(usize, char)> = source.char_indices().collect();
901    let mut source_index = 0usize;
902    let mut maps = Vec::with_capacity(display.chars().count());
903
904    for (display_start, display_char) in display.char_indices() {
905        let display_end = display_start + display_char.len_utf8();
906        let mut source_start = None;
907        while source_index < source_chars.len() {
908            let (candidate_start, candidate_char) = source_chars[source_index];
909            source_index += 1;
910            if candidate_char == display_char {
911                source_start = Some(candidate_start);
912                break;
913            }
914        }
915        maps.push(DisplayCharMap {
916            display_start,
917            display_end,
918            source_start,
919        });
920    }
921
922    maps
923}
924
925fn remap_subsequence_range_styles<T: Clone>(
926    styles: &[crate::text::RangeStyle<T>],
927    source_range: Range<usize>,
928    display_chars: &[DisplayCharMap],
929) -> Vec<crate::text::RangeStyle<T>> {
930    let mut remapped = Vec::new();
931
932    for style in styles {
933        let overlap_start = style.range.start.max(source_range.start);
934        let overlap_end = style.range.end.min(source_range.end);
935        if overlap_start >= overlap_end {
936            continue;
937        }
938        let local_source_range =
939            (overlap_start - source_range.start)..(overlap_end - source_range.start);
940        let mut range_start = None;
941        let mut range_end = 0usize;
942
943        for map in display_chars {
944            let in_range = map.source_start.is_some_and(|source_start| {
945                source_start >= local_source_range.start && source_start < local_source_range.end
946            });
947
948            if in_range {
949                if range_start.is_none() {
950                    range_start = Some(map.display_start);
951                }
952                range_end = map.display_end;
953                continue;
954            }
955
956            if let Some(start) = range_start.take() {
957                if start < range_end {
958                    remapped.push(crate::text::RangeStyle {
959                        item: style.item.clone(),
960                        range: start..range_end,
961                    });
962                }
963            }
964        }
965
966        if let Some(start) = range_start.take() {
967            if start < range_end {
968                remapped.push(crate::text::RangeStyle {
969                    item: style.item.clone(),
970                    range: start..range_end,
971                });
972            }
973        }
974    }
975
976    remapped
977}
978
979fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
980    match max_width {
981        Some(width) if width.is_finite() && width > 0.0 => Some(width),
982        _ => None,
983    }
984}
985
986fn absolute_range(base: &Range<usize>, relative: Range<usize>) -> Range<usize> {
987    (base.start + relative.start)..(base.start + relative.end)
988}
989
990fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
991    std::iter::once(range).collect()
992}
993
994fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
995    measurer: &M,
996    text: &crate::text::AnnotatedString,
997    line_range: Range<usize>,
998    style: &TextStyle,
999    max_width: f32,
1000    line_break: LineBreak,
1001    hyphens: Hyphens,
1002) -> Vec<Range<usize>> {
1003    let line_text = &text.text[line_range.clone()];
1004    if line_text.is_empty() {
1005        return single_line_range(line_range.start..line_range.start);
1006    }
1007
1008    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1009        && line_text.chars().any(char::is_whitespace)
1010    {
1011        if let Some(balanced) = wrap_line_with_word_balance(
1012            measurer,
1013            text,
1014            line_range.clone(),
1015            style,
1016            max_width,
1017            line_break,
1018        ) {
1019            return balanced;
1020        }
1021    }
1022
1023    wrap_line_greedy(
1024        measurer, text, line_range, style, max_width, line_break, hyphens,
1025    )
1026}
1027
1028fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1029    measurer: &M,
1030    text: &crate::text::AnnotatedString,
1031    line_range: Range<usize>,
1032    style: &TextStyle,
1033    max_width: f32,
1034    line_break: LineBreak,
1035    hyphens: Hyphens,
1036) -> Vec<Range<usize>> {
1037    let line_text = &text.text[line_range.clone()];
1038    let boundaries = char_boundaries(line_text);
1039    let mut wrapped = Vec::new();
1040    let mut start_idx = 0usize;
1041
1042    while start_idx < boundaries.len() - 1 {
1043        let mut low = start_idx + 1;
1044        let mut high = boundaries.len() - 1;
1045        let mut best = start_idx + 1;
1046
1047        while low <= high {
1048            let mid = (low + high) / 2;
1049            let segment_range = absolute_range(&line_range, boundaries[start_idx]..boundaries[mid]);
1050            let width = measurer
1051                .measure_subsequence(text, segment_range, style)
1052                .width;
1053            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1054                best = mid;
1055                low = mid + 1;
1056            } else {
1057                if mid == 0 {
1058                    break;
1059                }
1060                high = mid - 1;
1061            }
1062        }
1063
1064        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1065        let mut effective_wrap_idx = wrap_idx;
1066        let can_hyphenate = hyphens == Hyphens::Auto
1067            && wrap_idx == best
1068            && best < boundaries.len() - 1
1069            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1070        if can_hyphenate {
1071            effective_wrap_idx = resolve_auto_hyphen_break(
1072                measurer,
1073                line_text,
1074                style,
1075                &boundaries,
1076                start_idx,
1077                wrap_idx,
1078            );
1079        }
1080
1081        let segment_start = boundaries[start_idx];
1082        let mut segment_end = boundaries[effective_wrap_idx];
1083        if wrap_idx != best {
1084            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1085        }
1086        wrapped.push(absolute_range(&line_range, segment_start..segment_end));
1087
1088        start_idx = if wrap_idx != best {
1089            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1090        } else {
1091            effective_wrap_idx
1092        };
1093    }
1094
1095    if wrapped.is_empty() {
1096        wrapped.push(line_range.start..line_range.start);
1097    }
1098
1099    wrapped
1100}
1101
1102fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1103    measurer: &M,
1104    text: &crate::text::AnnotatedString,
1105    line_range: Range<usize>,
1106    style: &TextStyle,
1107    max_width: f32,
1108    line_break: LineBreak,
1109) -> Option<Vec<Range<usize>>> {
1110    let line_text = &text.text[line_range.clone()];
1111    let boundaries = char_boundaries(line_text);
1112    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1113    if breakpoints.len() <= 2 {
1114        return None;
1115    }
1116
1117    let node_count = breakpoints.len();
1118    let mut best_cost = vec![f32::INFINITY; node_count];
1119    let mut next_index = vec![None; node_count];
1120    best_cost[node_count - 1] = 0.0;
1121
1122    for start in (0..node_count - 1).rev() {
1123        for end in start + 1..node_count {
1124            let start_byte = boundaries[breakpoints[start]];
1125            let end_byte = boundaries[breakpoints[end]];
1126            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1127            if trimmed_end <= start_byte {
1128                continue;
1129            }
1130            let segment_range = absolute_range(&line_range, start_byte..trimmed_end);
1131            let segment_width = measurer
1132                .measure_subsequence(text, segment_range, style)
1133                .width;
1134            if segment_width > max_width + WRAP_EPSILON {
1135                continue;
1136            }
1137            if !best_cost[end].is_finite() {
1138                continue;
1139            }
1140            let slack = (max_width - segment_width).max(0.0);
1141            let is_last = end == node_count - 1;
1142            let segment_cost = match line_break {
1143                LineBreak::Heading => slack * slack,
1144                LineBreak::Paragraph => {
1145                    if is_last {
1146                        slack * slack * 0.16
1147                    } else {
1148                        slack * slack
1149                    }
1150                }
1151                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1152            };
1153            let candidate = segment_cost + best_cost[end];
1154            if candidate < best_cost[start] {
1155                best_cost[start] = candidate;
1156                next_index[start] = Some(end);
1157            }
1158        }
1159    }
1160
1161    let mut wrapped = Vec::new();
1162    let mut current = 0usize;
1163    while current < node_count - 1 {
1164        let next = next_index[current]?;
1165        let start_byte = boundaries[breakpoints[current]];
1166        let end_byte = boundaries[breakpoints[next]];
1167        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1168        if trimmed_end <= start_byte {
1169            return None;
1170        }
1171        wrapped.push(absolute_range(&line_range, start_byte..trimmed_end));
1172        current = next;
1173    }
1174
1175    if wrapped.is_empty() {
1176        return None;
1177    }
1178
1179    Some(wrapped)
1180}
1181
1182fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1183    let mut points = vec![0usize];
1184    for idx in 1..boundaries.len() - 1 {
1185        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1186        let current = &line[boundaries[idx]..boundaries[idx + 1]];
1187        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1188            points.push(idx);
1189        }
1190    }
1191    let end = boundaries.len() - 1;
1192    if points.last().copied() != Some(end) {
1193        points.push(end);
1194    }
1195    points
1196}
1197
1198fn choose_wrap_break(
1199    line: &str,
1200    boundaries: &[usize],
1201    start_idx: usize,
1202    best: usize,
1203    _line_break: LineBreak,
1204) -> usize {
1205    if best >= boundaries.len() - 1 {
1206        return best;
1207    }
1208
1209    if best <= start_idx + 1 {
1210        return best;
1211    }
1212
1213    for idx in (start_idx + 1..best).rev() {
1214        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1215        if prev.chars().all(char::is_whitespace) {
1216            return idx;
1217        }
1218    }
1219    best
1220}
1221
1222fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1223    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1224        return false;
1225    }
1226    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1227    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1228    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1229}
1230
1231fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1232    measurer: &M,
1233    line: &str,
1234    style: &TextStyle,
1235    boundaries: &[usize],
1236    start_idx: usize,
1237    break_idx: usize,
1238) -> usize {
1239    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
1240        if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
1241            return candidate;
1242        }
1243    }
1244    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1245}
1246
1247fn is_valid_auto_hyphen_break(
1248    line: &str,
1249    boundaries: &[usize],
1250    start_idx: usize,
1251    break_idx: usize,
1252    candidate_idx: usize,
1253) -> bool {
1254    let end_idx = boundaries.len().saturating_sub(1);
1255    candidate_idx > start_idx
1256        && candidate_idx < end_idx
1257        && candidate_idx <= break_idx
1258        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1259        && is_break_inside_word(line, boundaries, candidate_idx)
1260}
1261
1262fn choose_auto_hyphen_break_fallback(
1263    boundaries: &[usize],
1264    start_idx: usize,
1265    break_idx: usize,
1266) -> usize {
1267    let end_idx = boundaries.len().saturating_sub(1);
1268    if break_idx >= end_idx {
1269        return break_idx;
1270    }
1271    let trailing_len = end_idx.saturating_sub(break_idx);
1272    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1273        return break_idx;
1274    }
1275
1276    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1277    let max_break = break_idx.saturating_sub(1);
1278    if min_break > max_break {
1279        return break_idx;
1280    }
1281
1282    let mut best_break = break_idx;
1283    let mut best_penalty = usize::MAX;
1284    for idx in min_break..=max_break {
1285        let candidate_trailing_len = end_idx.saturating_sub(idx);
1286        let candidate_prefix_len = idx.saturating_sub(start_idx);
1287        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1288            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1289        {
1290            continue;
1291        }
1292
1293        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1294        if penalty < best_penalty {
1295            best_penalty = penalty;
1296            best_break = idx;
1297            if penalty == 0 {
1298                break;
1299            }
1300        }
1301    }
1302    best_break
1303}
1304
1305fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1306    while idx < boundaries.len() - 1 {
1307        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1308        if !ch.chars().all(char::is_whitespace) {
1309            break;
1310        }
1311        idx += 1;
1312    }
1313    idx
1314}
1315
1316fn apply_line_overflow<M: TextMeasurer + ?Sized>(
1317    measurer: &M,
1318    line: &str,
1319    style: &TextStyle,
1320    max_width: Option<f32>,
1321    options: TextLayoutOptions,
1322    is_last_visible_line: bool,
1323    single_line_ellipsis: bool,
1324) -> String {
1325    if options.overflow == TextOverflow::Clip || !is_last_visible_line {
1326        return line.to_string();
1327    }
1328
1329    let Some(width_limit) = max_width else {
1330        return match options.overflow {
1331            TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
1332            TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
1333            TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
1334            TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
1335                line.to_string()
1336            }
1337        };
1338    };
1339
1340    match options.overflow {
1341        TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
1342        TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
1343        TextOverflow::StartEllipsis => {
1344            if single_line_ellipsis {
1345                fit_start_ellipsis(measurer, line, style, width_limit)
1346            } else {
1347                line.to_string()
1348            }
1349        }
1350        TextOverflow::MiddleEllipsis => {
1351            if single_line_ellipsis {
1352                fit_middle_ellipsis(measurer, line, style, width_limit)
1353            } else {
1354                line.to_string()
1355            }
1356        }
1357        TextOverflow::ScaleDown { .. } => line.to_string(),
1358    }
1359}
1360
1361fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
1362    measurer: &M,
1363    line: &str,
1364    style: &TextStyle,
1365    max_width: f32,
1366) -> String {
1367    if measurer
1368        .measure(&crate::text::AnnotatedString::from(line), style)
1369        .width
1370        <= max_width + WRAP_EPSILON
1371    {
1372        return line.to_string();
1373    }
1374
1375    let ellipsis_width = measurer
1376        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1377        .width;
1378    if ellipsis_width > max_width + WRAP_EPSILON {
1379        return String::new();
1380    }
1381
1382    let boundaries = char_boundaries(line);
1383    let mut low = 0usize;
1384    let mut high = boundaries.len() - 1;
1385    let mut best = 0usize;
1386
1387    while low <= high {
1388        let mid = (low + high) / 2;
1389        let prefix = &line[..boundaries[mid]];
1390        let candidate = format!("{prefix}{ELLIPSIS}");
1391        let width = measurer
1392            .measure(
1393                &crate::text::AnnotatedString::from(candidate.as_str()),
1394                style,
1395            )
1396            .width;
1397        if width <= max_width + WRAP_EPSILON {
1398            best = mid;
1399            low = mid + 1;
1400        } else if mid == 0 {
1401            break;
1402        } else {
1403            high = mid - 1;
1404        }
1405    }
1406
1407    format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
1408}
1409
1410fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
1411    measurer: &M,
1412    line: &str,
1413    style: &TextStyle,
1414    max_width: f32,
1415) -> String {
1416    if measurer
1417        .measure(&crate::text::AnnotatedString::from(line), style)
1418        .width
1419        <= max_width + WRAP_EPSILON
1420    {
1421        return line.to_string();
1422    }
1423
1424    let ellipsis_width = measurer
1425        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1426        .width;
1427    if ellipsis_width > max_width + WRAP_EPSILON {
1428        return String::new();
1429    }
1430
1431    let boundaries = char_boundaries(line);
1432    let mut low = 0usize;
1433    let mut high = boundaries.len() - 1;
1434    let mut best = boundaries.len() - 1;
1435
1436    while low <= high {
1437        let mid = (low + high) / 2;
1438        let suffix = &line[boundaries[mid]..];
1439        let candidate = format!("{ELLIPSIS}{suffix}");
1440        let width = measurer
1441            .measure(
1442                &crate::text::AnnotatedString::from(candidate.as_str()),
1443                style,
1444            )
1445            .width;
1446        if width <= max_width + WRAP_EPSILON {
1447            best = mid;
1448            if mid == 0 {
1449                break;
1450            }
1451            high = mid - 1;
1452        } else {
1453            low = mid + 1;
1454        }
1455    }
1456
1457    format!("{ELLIPSIS}{}", &line[boundaries[best]..])
1458}
1459
1460fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
1461    measurer: &M,
1462    line: &str,
1463    style: &TextStyle,
1464    max_width: f32,
1465) -> String {
1466    if measurer
1467        .measure(&crate::text::AnnotatedString::from(line), style)
1468        .width
1469        <= max_width + WRAP_EPSILON
1470    {
1471        return line.to_string();
1472    }
1473
1474    let ellipsis_width = measurer
1475        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1476        .width;
1477    if ellipsis_width > max_width + WRAP_EPSILON {
1478        return String::new();
1479    }
1480
1481    let boundaries = char_boundaries(line);
1482    let total_chars = boundaries.len().saturating_sub(1);
1483    for keep in (0..=total_chars).rev() {
1484        let keep_start = keep.div_ceil(2);
1485        let keep_end = keep / 2;
1486        let start = &line[..boundaries[keep_start]];
1487        let end_start = boundaries[total_chars.saturating_sub(keep_end)];
1488        let end = &line[end_start..];
1489        let candidate = format!("{start}{ELLIPSIS}{end}");
1490        if measurer
1491            .measure(
1492                &crate::text::AnnotatedString::from(candidate.as_str()),
1493                style,
1494            )
1495            .width
1496            <= max_width + WRAP_EPSILON
1497        {
1498            return candidate;
1499        }
1500    }
1501
1502    ELLIPSIS.to_string()
1503}
1504
1505fn char_boundaries(text: &str) -> Vec<usize> {
1506    let mut out = Vec::with_capacity(text.chars().count() + 1);
1507    out.push(0);
1508    for (idx, _) in text.char_indices() {
1509        if idx != 0 {
1510            out.push(idx);
1511        }
1512    }
1513    out.push(text.len());
1514    out
1515}
1516
1517#[cfg(test)]
1518mod tests {
1519    use super::*;
1520    use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
1521    use crate::text_layout_result::TextLayoutResult;
1522
1523    struct ContractBreakMeasurer {
1524        retreat: usize,
1525    }
1526
1527    impl TextMeasurer for ContractBreakMeasurer {
1528        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1529            MonospacedTextMeasurer.measure(
1530                &crate::text::AnnotatedString::from(text.text.as_str()),
1531                style,
1532            )
1533        }
1534
1535        fn get_offset_for_position(
1536            &self,
1537            text: &crate::text::AnnotatedString,
1538            style: &TextStyle,
1539            x: f32,
1540            y: f32,
1541        ) -> usize {
1542            MonospacedTextMeasurer.get_offset_for_position(
1543                &crate::text::AnnotatedString::from(text.text.as_str()),
1544                style,
1545                x,
1546                y,
1547            )
1548        }
1549
1550        fn get_cursor_x_for_offset(
1551            &self,
1552            text: &crate::text::AnnotatedString,
1553            style: &TextStyle,
1554            offset: usize,
1555        ) -> f32 {
1556            MonospacedTextMeasurer.get_cursor_x_for_offset(
1557                &crate::text::AnnotatedString::from(text.text.as_str()),
1558                style,
1559                offset,
1560            )
1561        }
1562
1563        fn layout(
1564            &self,
1565            text: &crate::text::AnnotatedString,
1566            style: &TextStyle,
1567        ) -> TextLayoutResult {
1568            MonospacedTextMeasurer.layout(
1569                &crate::text::AnnotatedString::from(text.text.as_str()),
1570                style,
1571            )
1572        }
1573
1574        fn choose_auto_hyphen_break(
1575            &self,
1576            _line: &str,
1577            _style: &TextStyle,
1578            _segment_start_char: usize,
1579            measured_break_char: usize,
1580        ) -> Option<usize> {
1581            measured_break_char.checked_sub(self.retreat)
1582        }
1583    }
1584
1585    fn style_with_line_break(line_break: LineBreak) -> TextStyle {
1586        TextStyle {
1587            span_style: crate::text::SpanStyle {
1588                font_size: TextUnit::Sp(10.0),
1589                ..Default::default()
1590            },
1591            paragraph_style: ParagraphStyle {
1592                line_break,
1593                ..Default::default()
1594            },
1595        }
1596    }
1597
1598    fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
1599        TextStyle {
1600            span_style: crate::text::SpanStyle {
1601                font_size: TextUnit::Sp(10.0),
1602                ..Default::default()
1603            },
1604            paragraph_style: ParagraphStyle {
1605                hyphens,
1606                ..Default::default()
1607            },
1608        }
1609    }
1610
1611    fn assert_f32_close(actual: f32, expected: f32) {
1612        assert!(
1613            (actual - expected).abs() <= 0.01,
1614            "actual={actual}, expected={expected}"
1615        );
1616    }
1617
1618    #[test]
1619    fn text_layout_options_wraps_and_limits_lines() {
1620        let style = TextStyle {
1621            span_style: crate::text::SpanStyle {
1622                font_size: TextUnit::Sp(10.0),
1623                ..Default::default()
1624            },
1625            ..Default::default()
1626        };
1627        let options = TextLayoutOptions {
1628            overflow: TextOverflow::Clip,
1629            soft_wrap: true,
1630            max_lines: 2,
1631            min_lines: 1,
1632        };
1633
1634        let prepared = prepare_text_layout(
1635            &crate::text::AnnotatedString::from("A B C D E F"),
1636            &style,
1637            options,
1638            Some(24.0), // roughly 4 chars in monospaced fallback
1639        );
1640
1641        assert!(prepared.did_overflow);
1642        assert!(prepared.metrics.line_count <= 2);
1643    }
1644
1645    #[test]
1646    fn text_layout_options_end_ellipsis_applies() {
1647        let style = TextStyle {
1648            span_style: crate::text::SpanStyle {
1649                font_size: TextUnit::Sp(10.0),
1650                ..Default::default()
1651            },
1652            ..Default::default()
1653        };
1654        let options = TextLayoutOptions {
1655            overflow: TextOverflow::Ellipsis,
1656            soft_wrap: false,
1657            max_lines: 1,
1658            min_lines: 1,
1659        };
1660
1661        let prepared = prepare_text_layout(
1662            &crate::text::AnnotatedString::from("Long long line"),
1663            &style,
1664            options,
1665            Some(20.0),
1666        );
1667        assert!(prepared.did_overflow);
1668        assert!(prepared.text.text.contains(ELLIPSIS));
1669    }
1670
1671    #[test]
1672    fn text_layout_options_visible_keeps_full_text() {
1673        let style = TextStyle {
1674            span_style: crate::text::SpanStyle {
1675                font_size: TextUnit::Sp(10.0),
1676                ..Default::default()
1677            },
1678            ..Default::default()
1679        };
1680        let options = TextLayoutOptions {
1681            overflow: TextOverflow::Visible,
1682            soft_wrap: false,
1683            max_lines: 1,
1684            min_lines: 1,
1685        };
1686
1687        let input = "This should remain unchanged";
1688        let prepared = prepare_text_layout(
1689            &crate::text::AnnotatedString::from(input),
1690            &style,
1691            options,
1692            Some(10.0),
1693        );
1694        assert_eq!(prepared.text.text, input);
1695    }
1696
1697    #[test]
1698    fn text_layout_options_respects_min_lines() {
1699        let style = TextStyle {
1700            span_style: crate::text::SpanStyle {
1701                font_size: TextUnit::Sp(10.0),
1702                ..Default::default()
1703            },
1704            ..Default::default()
1705        };
1706        let options = TextLayoutOptions {
1707            overflow: TextOverflow::Clip,
1708            soft_wrap: true,
1709            max_lines: 4,
1710            min_lines: 3,
1711        };
1712
1713        let prepared = prepare_text_layout(
1714            &crate::text::AnnotatedString::from("short"),
1715            &style,
1716            options,
1717            Some(100.0),
1718        );
1719        assert_eq!(prepared.metrics.line_count, 3);
1720    }
1721
1722    #[test]
1723    fn text_layout_options_middle_ellipsis_for_single_line() {
1724        let style = TextStyle {
1725            span_style: crate::text::SpanStyle {
1726                font_size: TextUnit::Sp(10.0),
1727                ..Default::default()
1728            },
1729            ..Default::default()
1730        };
1731        let options = TextLayoutOptions {
1732            overflow: TextOverflow::MiddleEllipsis,
1733            soft_wrap: false,
1734            max_lines: 1,
1735            min_lines: 1,
1736        };
1737
1738        let prepared = prepare_text_layout(
1739            &crate::text::AnnotatedString::from("abcdefghijk"),
1740            &style,
1741            options,
1742            Some(24.0),
1743        );
1744        assert!(prepared.text.text.contains(ELLIPSIS));
1745        assert!(prepared.did_overflow);
1746    }
1747
1748    #[test]
1749    fn text_layout_options_scale_down_fits_without_rewriting_text() {
1750        let style = TextStyle {
1751            span_style: crate::text::SpanStyle {
1752                font_size: TextUnit::Sp(20.0),
1753                ..Default::default()
1754            },
1755            ..Default::default()
1756        };
1757        let options = TextLayoutOptions {
1758            overflow: TextOverflow::ScaleDown {
1759                min_font_size_sp: 10.0,
1760            },
1761            soft_wrap: false,
1762            max_lines: 1,
1763            min_lines: 1,
1764        };
1765
1766        let prepared = prepare_text_layout(
1767            &crate::text::AnnotatedString::from("ABCDE"),
1768            &style,
1769            options,
1770            Some(36.0),
1771        );
1772
1773        assert_eq!(prepared.text.text, "ABCDE");
1774        assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
1775        assert!(!prepared.did_overflow);
1776        let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
1777        assert!(visual_font_size < 20.0);
1778        assert!(visual_font_size >= 10.0);
1779    }
1780
1781    #[test]
1782    fn text_layout_options_scale_down_scales_root_shadow() {
1783        let style = TextStyle {
1784            span_style: crate::text::SpanStyle {
1785                font_size: TextUnit::Sp(20.0),
1786                shadow: Some(crate::text::Shadow {
1787                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
1788                    offset: crate::modifier::Point::new(8.0, 4.0),
1789                    blur_radius: 6.0,
1790                }),
1791                ..Default::default()
1792            },
1793            ..Default::default()
1794        };
1795        let options = TextLayoutOptions {
1796            overflow: TextOverflow::ScaleDown {
1797                min_font_size_sp: 10.0,
1798            },
1799            soft_wrap: false,
1800            max_lines: 1,
1801            min_lines: 1,
1802        };
1803
1804        let prepared = prepare_text_layout(
1805            &crate::text::AnnotatedString::from("ABCDE"),
1806            &style,
1807            options,
1808            Some(36.0),
1809        );
1810
1811        let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
1812        let shadow = prepared
1813            .visual_style
1814            .span_style
1815            .shadow
1816            .expect("scaled style should retain shadow");
1817        assert_f32_close(shadow.offset.x, 8.0 * font_scale);
1818        assert_f32_close(shadow.offset.y, 4.0 * font_scale);
1819        assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
1820    }
1821
1822    #[test]
1823    fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
1824        let style = TextStyle {
1825            span_style: crate::text::SpanStyle {
1826                font_size: TextUnit::Sp(20.0),
1827                ..Default::default()
1828            },
1829            ..Default::default()
1830        };
1831        let options = TextLayoutOptions {
1832            overflow: TextOverflow::ScaleDown {
1833                min_font_size_sp: 10.0,
1834            },
1835            soft_wrap: false,
1836            max_lines: 1,
1837            min_lines: 1,
1838        };
1839
1840        let prepared = prepare_text_layout(
1841            &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
1842            &style,
1843            options,
1844            Some(12.0),
1845        );
1846
1847        assert_eq!(prepared.text.text, "ABCDEFGHIJ");
1848        assert!(prepared.did_overflow);
1849        assert_eq!(prepared.metrics.width, 12.0);
1850        assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
1851    }
1852
1853    #[test]
1854    fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
1855        let plain = crate::text::AnnotatedString::from("plain");
1856        assert!(matches!(
1857            scale_annotated_font_sizes(&plain, 0.5),
1858            std::borrow::Cow::Borrowed(_)
1859        ));
1860
1861        let colored = crate::text::annotated_string::Builder::new()
1862            .push_style(crate::text::SpanStyle {
1863                color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
1864                ..Default::default()
1865            })
1866            .append("colored")
1867            .pop()
1868            .to_annotated_string();
1869        assert!(matches!(
1870            scale_annotated_font_sizes(&colored, 0.5),
1871            std::borrow::Cow::Borrowed(_)
1872        ));
1873    }
1874
1875    #[test]
1876    fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
1877        let text = crate::text::annotated_string::Builder::new()
1878            .push_style(crate::text::SpanStyle {
1879                shadow: Some(crate::text::Shadow {
1880                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
1881                    offset: crate::modifier::Point::new(6.0, 2.0),
1882                    blur_radius: 4.0,
1883                }),
1884                ..Default::default()
1885            })
1886            .append("shadow")
1887            .pop()
1888            .to_annotated_string();
1889
1890        let scaled = scale_annotated_font_sizes(&text, 0.5);
1891        let std::borrow::Cow::Owned(scaled) = scaled else {
1892            panic!("shadowed span should be scaled into owned text");
1893        };
1894        let shadow = scaled.span_styles[0]
1895            .item
1896            .shadow
1897            .expect("scaled span should retain shadow");
1898        assert_f32_close(shadow.offset.x, 3.0);
1899        assert_f32_close(shadow.offset.y, 1.0);
1900        assert_f32_close(shadow.blur_radius, 2.0);
1901    }
1902
1903    #[test]
1904    fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
1905        let style = TextStyle {
1906            span_style: crate::text::SpanStyle {
1907                font_size: TextUnit::Sp(10.0),
1908                ..Default::default()
1909            },
1910            ..Default::default()
1911        };
1912        let options = TextLayoutOptions {
1913            overflow: TextOverflow::Clip,
1914            soft_wrap: true,
1915            max_lines: usize::MAX,
1916            min_lines: 1,
1917        };
1918
1919        let text = "if counter % 2 == 0";
1920        let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
1921        let prepared = prepare_text_layout(
1922            &crate::text::AnnotatedString::from(text),
1923            &style,
1924            options,
1925            Some(exact_width - 0.1),
1926        );
1927
1928        assert!(
1929            !prepared.text.text.contains('\n'),
1930            "unexpected line split: {:?}",
1931            prepared.text
1932        );
1933    }
1934
1935    #[test]
1936    fn line_break_mode_changes_wrap_strategy_contract() {
1937        let text = "This is an example text";
1938        let options = TextLayoutOptions {
1939            overflow: TextOverflow::Clip,
1940            soft_wrap: true,
1941            max_lines: usize::MAX,
1942            min_lines: 1,
1943        };
1944
1945        let simple = prepare_text_layout(
1946            &crate::text::AnnotatedString::from(text),
1947            &style_with_line_break(LineBreak::Simple),
1948            options,
1949            Some(120.0),
1950        );
1951        let heading = prepare_text_layout(
1952            &crate::text::AnnotatedString::from(text),
1953            &style_with_line_break(LineBreak::Heading),
1954            options,
1955            Some(120.0),
1956        );
1957        let paragraph = prepare_text_layout(
1958            &crate::text::AnnotatedString::from(text),
1959            &style_with_line_break(LineBreak::Paragraph),
1960            options,
1961            Some(50.0),
1962        );
1963
1964        assert_eq!(
1965            simple.text.text.lines().collect::<Vec<_>>(),
1966            vec!["This is an example", "text"]
1967        );
1968        assert_eq!(
1969            heading.text.text.lines().collect::<Vec<_>>(),
1970            vec!["This is an", "example text"]
1971        );
1972        assert_eq!(
1973            paragraph.text.text.lines().collect::<Vec<_>>(),
1974            vec!["This", "is an", "example", "text"]
1975        );
1976    }
1977
1978    #[test]
1979    fn hyphens_mode_changes_wrap_strategy_contract() {
1980        let text = "Transformation";
1981        let options = TextLayoutOptions {
1982            overflow: TextOverflow::Clip,
1983            soft_wrap: true,
1984            max_lines: usize::MAX,
1985            min_lines: 1,
1986        };
1987
1988        let auto = prepare_text_layout(
1989            &crate::text::AnnotatedString::from(text),
1990            &style_with_hyphens(Hyphens::Auto),
1991            options,
1992            Some(24.0),
1993        );
1994        let none = prepare_text_layout(
1995            &crate::text::AnnotatedString::from(text),
1996            &style_with_hyphens(Hyphens::None),
1997            options,
1998            Some(24.0),
1999        );
2000
2001        assert_eq!(
2002            auto.text.text.lines().collect::<Vec<_>>(),
2003            vec!["Tran", "sfor", "ma", "tion"]
2004        );
2005        assert_eq!(
2006            none.text.text.lines().collect::<Vec<_>>(),
2007            vec!["Tran", "sfor", "mati", "on"]
2008        );
2009        assert!(
2010            !auto.text.text.contains('-'),
2011            "automatic hyphenation should influence breaks without mutating source text content"
2012        );
2013    }
2014
2015    #[test]
2016    fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
2017        let text = "Transformation";
2018        let style = style_with_hyphens(Hyphens::Auto);
2019        let options = TextLayoutOptions {
2020            overflow: TextOverflow::Clip,
2021            soft_wrap: true,
2022            max_lines: usize::MAX,
2023            min_lines: 1,
2024        };
2025
2026        let prepared = prepare_text_layout_fallback(
2027            &ContractBreakMeasurer { retreat: 1 },
2028            &crate::text::AnnotatedString::from(text),
2029            &style,
2030            options,
2031            Some(24.0),
2032        );
2033
2034        assert_eq!(
2035            prepared.text.text.lines().collect::<Vec<_>>(),
2036            vec!["Tra", "nsf", "orm", "ati", "on"]
2037        );
2038    }
2039
2040    #[test]
2041    fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
2042        let text = "Transformation";
2043        let style = style_with_hyphens(Hyphens::Auto);
2044        let options = TextLayoutOptions {
2045            overflow: TextOverflow::Clip,
2046            soft_wrap: true,
2047            max_lines: usize::MAX,
2048            min_lines: 1,
2049        };
2050
2051        let prepared = prepare_text_layout_fallback(
2052            &ContractBreakMeasurer { retreat: 10 },
2053            &crate::text::AnnotatedString::from(text),
2054            &style,
2055            options,
2056            Some(24.0),
2057        );
2058
2059        assert_eq!(
2060            prepared.text.text.lines().collect::<Vec<_>>(),
2061            vec!["Tran", "sfor", "ma", "tion"]
2062        );
2063    }
2064
2065    #[test]
2066    fn transformed_text_keeps_span_ranges_within_display_bounds() {
2067        let style = TextStyle {
2068            span_style: crate::text::SpanStyle {
2069                font_size: TextUnit::Sp(10.0),
2070                ..Default::default()
2071            },
2072            ..Default::default()
2073        };
2074        let options = TextLayoutOptions {
2075            overflow: TextOverflow::Ellipsis,
2076            soft_wrap: false,
2077            max_lines: 1,
2078            min_lines: 1,
2079        };
2080        let annotated = crate::text::AnnotatedString::builder()
2081            .push_style(crate::text::SpanStyle {
2082                font_weight: Some(crate::text::FontWeight::BOLD),
2083                ..Default::default()
2084            })
2085            .append("Styled overflow text sample")
2086            .pop()
2087            .to_annotated_string();
2088
2089        let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
2090        assert!(prepared.did_overflow);
2091        for span in &prepared.text.span_styles {
2092            assert!(span.range.start < span.range.end);
2093            assert!(span.range.end <= prepared.text.text.len());
2094            assert!(prepared.text.text.is_char_boundary(span.range.start));
2095            assert!(prepared.text.text.is_char_boundary(span.range.end));
2096        }
2097    }
2098
2099    #[test]
2100    fn wrapped_text_splits_styles_around_inserted_newlines() {
2101        let style = TextStyle {
2102            span_style: crate::text::SpanStyle {
2103                font_size: TextUnit::Sp(10.0),
2104                ..Default::default()
2105            },
2106            ..Default::default()
2107        };
2108        let options = TextLayoutOptions {
2109            overflow: TextOverflow::Clip,
2110            soft_wrap: true,
2111            max_lines: usize::MAX,
2112            min_lines: 1,
2113        };
2114        let annotated = crate::text::AnnotatedString::builder()
2115            .push_style(crate::text::SpanStyle {
2116                text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
2117                ..Default::default()
2118            })
2119            .append("Wrapped style text example")
2120            .pop()
2121            .to_annotated_string();
2122
2123        let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
2124        assert!(prepared.text.text.contains('\n'));
2125        assert!(!prepared.text.span_styles.is_empty());
2126        for span in &prepared.text.span_styles {
2127            assert!(span.range.end <= prepared.text.text.len());
2128        }
2129    }
2130
2131    #[test]
2132    fn mixed_font_size_segments_wrap_without_truncation() {
2133        let style = TextStyle {
2134            span_style: crate::text::SpanStyle {
2135                font_size: TextUnit::Sp(14.0),
2136                ..Default::default()
2137            },
2138            ..Default::default()
2139        };
2140        let options = TextLayoutOptions {
2141            overflow: TextOverflow::Clip,
2142            soft_wrap: true,
2143            max_lines: usize::MAX,
2144            min_lines: 1,
2145        };
2146        let annotated = crate::text::AnnotatedString::builder()
2147            .append("You can also ")
2148            .push_style(crate::text::SpanStyle {
2149                font_size: TextUnit::Sp(22.0),
2150                ..Default::default()
2151            })
2152            .append("change font size")
2153            .pop()
2154            .append(" dynamically mid-sentence!")
2155            .to_annotated_string();
2156
2157        let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
2158        assert!(prepared.text.text.contains('\n'));
2159        assert!(prepared.text.text.contains("mid-sentence!"));
2160        assert!(!prepared.did_overflow);
2161    }
2162}