Skip to main content

cranpose_ui/text/
measure.rs

1use crate::text_layout_result::TextLayoutResult;
2use std::cell::RefCell;
3
4use super::layout_options::{TextLayoutOptions, TextOverflow};
5use super::paragraph::{Hyphens, LineBreak};
6use super::style::TextStyle;
7
8const ELLIPSIS: &str = "\u{2026}";
9const WRAP_EPSILON: f32 = 0.5;
10const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
11const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
12const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub struct TextMetrics {
16    pub width: f32,
17    pub height: f32,
18    /// Height of a single line of text
19    pub line_height: f32,
20    /// Number of lines in the text
21    pub line_count: usize,
22}
23
24#[derive(Clone, Debug, PartialEq)]
25pub struct PreparedTextLayout {
26    pub text: crate::text::AnnotatedString,
27    pub metrics: TextMetrics,
28    pub did_overflow: bool,
29}
30
31pub trait TextMeasurer: 'static {
32    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
33
34    fn get_offset_for_position(
35        &self,
36        text: &crate::text::AnnotatedString,
37        style: &TextStyle,
38        x: f32,
39        y: f32,
40    ) -> usize;
41
42    fn get_cursor_x_for_offset(
43        &self,
44        text: &crate::text::AnnotatedString,
45        style: &TextStyle,
46        offset: usize,
47    ) -> f32;
48
49    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
50
51    /// Returns an alternate break boundary for `Hyphens::Auto` when a greedy break
52    /// split lands in the middle of a word.
53    ///
54    /// `segment_start_char` and `measured_break_char` are character-boundary indices
55    /// in `line` (not byte offsets). Return `None` to delegate to fallback behavior.
56    fn choose_auto_hyphen_break(
57        &self,
58        _line: &str,
59        _style: &TextStyle,
60        _segment_start_char: usize,
61        _measured_break_char: usize,
62    ) -> Option<usize> {
63        None
64    }
65
66    fn measure_with_options(
67        &self,
68        text: &crate::text::AnnotatedString,
69        style: &TextStyle,
70        options: TextLayoutOptions,
71        max_width: Option<f32>,
72    ) -> TextMetrics {
73        self.prepare_with_options(text, style, options, max_width)
74            .metrics
75    }
76
77    fn prepare_with_options(
78        &self,
79        text: &crate::text::AnnotatedString,
80        style: &TextStyle,
81        options: TextLayoutOptions,
82        max_width: Option<f32>,
83    ) -> PreparedTextLayout {
84        prepare_text_layout_fallback(self, text, style, options, max_width)
85    }
86}
87
88#[derive(Default)]
89struct MonospacedTextMeasurer;
90
91impl MonospacedTextMeasurer {
92    const DEFAULT_SIZE: f32 = 14.0;
93    const CHAR_WIDTH_RATIO: f32 = 0.6; // Width is 0.6 of Height
94
95    fn get_metrics(style: &TextStyle) -> (f32, f32) {
96        let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
97        let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
98        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
99        (
100            (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
101            line_height,
102        )
103    }
104}
105
106impl TextMeasurer for MonospacedTextMeasurer {
107    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
108        let (char_width, line_height) = Self::get_metrics(style);
109
110        let lines: Vec<&str> = text.text.split('\n').collect();
111        let line_count = lines.len().max(1);
112
113        let width = lines
114            .iter()
115            .map(|line| line.chars().count() as f32 * char_width)
116            .fold(0.0_f32, f32::max);
117
118        TextMetrics {
119            width,
120            height: line_count as f32 * line_height,
121            line_height,
122            line_count,
123        }
124    }
125
126    fn get_offset_for_position(
127        &self,
128        text: &crate::text::AnnotatedString,
129        style: &TextStyle,
130        x: f32,
131        y: f32,
132    ) -> usize {
133        let (char_width, line_height) = Self::get_metrics(style);
134
135        if text.text.is_empty() {
136            return 0;
137        }
138
139        let line_index = (y / line_height).floor().max(0.0) as usize;
140        let lines: Vec<&str> = text.text.split('\n').collect();
141        let target_line = line_index.min(lines.len().saturating_sub(1));
142
143        let mut line_start_byte = 0;
144        for line in lines.iter().take(target_line) {
145            line_start_byte += line.len() + 1;
146        }
147
148        let line_text = lines.get(target_line).unwrap_or(&"");
149        let char_index = (x / char_width).round() as usize;
150        let line_char_count = line_text.chars().count();
151        let clamped_index = char_index.min(line_char_count);
152
153        let offset_in_line = line_text
154            .char_indices()
155            .nth(clamped_index)
156            .map(|(i, _)| i)
157            .unwrap_or(line_text.len());
158
159        line_start_byte + offset_in_line
160    }
161
162    fn get_cursor_x_for_offset(
163        &self,
164        text: &crate::text::AnnotatedString,
165        style: &TextStyle,
166        offset: usize,
167    ) -> f32 {
168        let (char_width, _) = Self::get_metrics(style);
169
170        let clamped_offset = offset.min(text.text.len());
171        let char_count = text.text[..clamped_offset].chars().count();
172        char_count as f32 * char_width
173    }
174
175    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
176        let (char_width, line_height) = Self::get_metrics(style);
177        TextLayoutResult::monospaced(&text.text, char_width, line_height)
178    }
179}
180
181thread_local! {
182    static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
183}
184
185pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
186    TEXT_MEASURER.with(|m| {
187        *m.borrow_mut() = Box::new(measurer);
188    });
189}
190
191pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
192    TEXT_MEASURER.with(|m| m.borrow().measure(text, style))
193}
194
195pub fn measure_text_with_options(
196    text: &crate::text::AnnotatedString,
197    style: &TextStyle,
198    options: TextLayoutOptions,
199    max_width: Option<f32>,
200) -> TextMetrics {
201    TEXT_MEASURER.with(|m| {
202        m.borrow()
203            .measure_with_options(text, style, options.normalized(), max_width)
204    })
205}
206
207pub fn prepare_text_layout(
208    text: &crate::text::AnnotatedString,
209    style: &TextStyle,
210    options: TextLayoutOptions,
211    max_width: Option<f32>,
212) -> PreparedTextLayout {
213    TEXT_MEASURER.with(|m| {
214        m.borrow()
215            .prepare_with_options(text, style, options.normalized(), max_width)
216    })
217}
218
219pub fn get_offset_for_position(
220    text: &crate::text::AnnotatedString,
221    style: &TextStyle,
222    x: f32,
223    y: f32,
224) -> usize {
225    TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, style, x, y))
226}
227
228pub fn get_cursor_x_for_offset(
229    text: &crate::text::AnnotatedString,
230    style: &TextStyle,
231    offset: usize,
232) -> f32 {
233    TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, style, offset))
234}
235
236pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
237    TEXT_MEASURER.with(|m| m.borrow().layout(text, style))
238}
239
240fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
241    measurer: &M,
242    text: &crate::text::AnnotatedString,
243    style: &TextStyle,
244    options: TextLayoutOptions,
245    max_width: Option<f32>,
246) -> PreparedTextLayout {
247    let opts = options.normalized();
248    let max_width = normalize_max_width(max_width);
249    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
250        .then_some(max_width)
251        .flatten();
252    let line_break_mode = style
253        .paragraph_style
254        .line_break
255        .take_or_else(|| LineBreak::Simple);
256    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
257
258    let mut lines = split_text_lines(text.text.as_str());
259    let mut annotated_lines = split_annotated_lines(text);
260    if let Some(width_limit) = wrap_width {
261        let mut wrapped = Vec::with_capacity(lines.len());
262        let mut wrapped_annotated = Vec::with_capacity(lines.len());
263        for line in &annotated_lines {
264            let wrapped_lines = wrap_line_to_width(
265                measurer,
266                line,
267                style,
268                width_limit,
269                line_break_mode,
270                hyphens_mode,
271            );
272            for wrapped_line in wrapped_lines {
273                wrapped.push(wrapped_line.text.clone());
274                wrapped_annotated.push(wrapped_line);
275            }
276        }
277        lines = wrapped;
278        annotated_lines = wrapped_annotated;
279    }
280
281    let mut did_overflow = false;
282    let mut visible_lines = lines.clone();
283    let mut visible_annotated_lines = annotated_lines.clone();
284
285    if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
286        did_overflow = true;
287        visible_lines.truncate(opts.max_lines);
288        visible_annotated_lines.truncate(opts.max_lines);
289        if let Some(last_line) = visible_lines.last_mut() {
290            *last_line =
291                apply_line_overflow(measurer, last_line, style, max_width, opts, true, true);
292            if let Some(last_annotated_line) = visible_annotated_lines.last_mut() {
293                *last_annotated_line =
294                    remap_annotated_for_display(last_annotated_line, last_line.as_str());
295            }
296        }
297    }
298
299    if let Some(width_limit) = max_width {
300        let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
301        let visible_len = visible_lines.len();
302        for (line_index, line) in visible_lines.iter_mut().enumerate() {
303            let width = visible_annotated_lines
304                .get(line_index)
305                .map(|annotated_line| measurer.measure(annotated_line, style).width)
306                .unwrap_or_default();
307            if width > width_limit + WRAP_EPSILON {
308                if opts.overflow == TextOverflow::Visible {
309                    continue;
310                }
311                did_overflow = true;
312                *line = apply_line_overflow(
313                    measurer,
314                    line,
315                    style,
316                    Some(width_limit),
317                    opts,
318                    line_index + 1 == visible_len,
319                    single_line_ellipsis,
320                );
321                if let Some(annotated_line) = visible_annotated_lines.get_mut(line_index) {
322                    *annotated_line = remap_annotated_for_display(annotated_line, line.as_str());
323                }
324            }
325        }
326    }
327
328    let display_annotated = join_annotated_lines(&visible_annotated_lines);
329    debug_assert_eq!(display_annotated.text, visible_lines.join("\n"));
330    let line_height = measurer.measure(text, style).line_height.max(0.0);
331    let display_line_count = visible_lines.len().max(1);
332    let layout_line_count = display_line_count.max(opts.min_lines);
333
334    let measured_width = if visible_annotated_lines.is_empty() {
335        0.0
336    } else {
337        visible_annotated_lines
338            .iter()
339            .map(|line| measurer.measure(line, style).width)
340            .fold(0.0_f32, f32::max)
341    };
342    let width = if opts.overflow == TextOverflow::Visible {
343        measured_width
344    } else if let Some(width_limit) = max_width {
345        measured_width.min(width_limit)
346    } else {
347        measured_width
348    };
349
350    PreparedTextLayout {
351        text: display_annotated,
352        metrics: TextMetrics {
353            width,
354            height: layout_line_count as f32 * line_height,
355            line_height,
356            line_count: layout_line_count,
357        },
358        did_overflow,
359    }
360}
361
362fn split_text_lines(text: &str) -> Vec<String> {
363    if text.is_empty() {
364        return vec![String::new()];
365    }
366    text.split('\n').map(ToString::to_string).collect()
367}
368
369fn split_annotated_lines(text: &crate::text::AnnotatedString) -> Vec<crate::text::AnnotatedString> {
370    if text.text.is_empty() {
371        return vec![crate::text::AnnotatedString::from("")];
372    }
373
374    let mut out = Vec::new();
375    let mut start = 0usize;
376    for (idx, ch) in text.text.char_indices() {
377        if ch == '\n' {
378            out.push(text.subsequence(start..idx));
379            start = idx + ch.len_utf8();
380        }
381    }
382    out.push(text.subsequence(start..text.text.len()));
383    out
384}
385
386fn join_annotated_lines(lines: &[crate::text::AnnotatedString]) -> crate::text::AnnotatedString {
387    if lines.is_empty() {
388        return crate::text::AnnotatedString::from("");
389    }
390
391    let mut text = String::new();
392    let mut span_styles = Vec::new();
393    let mut paragraph_styles = Vec::new();
394    let mut offset = 0usize;
395
396    for (idx, line) in lines.iter().enumerate() {
397        text.push_str(line.text.as_str());
398        for span in &line.span_styles {
399            span_styles.push(crate::text::RangeStyle {
400                item: span.item.clone(),
401                range: (span.range.start + offset)..(span.range.end + offset),
402            });
403        }
404        for span in &line.paragraph_styles {
405            paragraph_styles.push(crate::text::RangeStyle {
406                item: span.item.clone(),
407                range: (span.range.start + offset)..(span.range.end + offset),
408            });
409        }
410
411        offset += line.text.len();
412        if idx + 1 < lines.len() {
413            text.push('\n');
414            offset += 1;
415        }
416    }
417
418    crate::text::AnnotatedString {
419        text,
420        span_styles,
421        paragraph_styles,
422    }
423}
424
425fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
426    while end > start {
427        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
428            break;
429        };
430        if ch.is_whitespace() {
431            end = start + idx;
432        } else {
433            break;
434        }
435    }
436    end
437}
438
439fn remap_annotated_for_display(
440    source: &crate::text::AnnotatedString,
441    display_text: &str,
442) -> crate::text::AnnotatedString {
443    if source.text == display_text {
444        return source.clone();
445    }
446
447    let display_chars = map_display_chars_to_source(source.text.as_str(), display_text);
448    crate::text::AnnotatedString {
449        text: display_text.to_string(),
450        span_styles: remap_range_styles(&source.span_styles, &display_chars),
451        paragraph_styles: remap_range_styles(&source.paragraph_styles, &display_chars),
452    }
453}
454
455#[derive(Clone, Copy)]
456struct DisplayCharMap {
457    display_start: usize,
458    display_end: usize,
459    source_start: Option<usize>,
460}
461
462fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
463    let source_chars: Vec<(usize, char)> = source.char_indices().collect();
464    let mut source_index = 0usize;
465    let mut maps = Vec::with_capacity(display.chars().count());
466
467    for (display_start, display_char) in display.char_indices() {
468        let display_end = display_start + display_char.len_utf8();
469        let mut source_start = None;
470        while source_index < source_chars.len() {
471            let (candidate_start, candidate_char) = source_chars[source_index];
472            source_index += 1;
473            if candidate_char == display_char {
474                source_start = Some(candidate_start);
475                break;
476            }
477        }
478        maps.push(DisplayCharMap {
479            display_start,
480            display_end,
481            source_start,
482        });
483    }
484
485    maps
486}
487
488fn remap_range_styles<T: Clone>(
489    styles: &[crate::text::RangeStyle<T>],
490    display_chars: &[DisplayCharMap],
491) -> Vec<crate::text::RangeStyle<T>> {
492    let mut remapped = Vec::new();
493
494    for style in styles {
495        let mut range_start = None;
496        let mut range_end = 0usize;
497
498        for map in display_chars {
499            let in_range = map.source_start.is_some_and(|source_start| {
500                source_start >= style.range.start && source_start < style.range.end
501            });
502
503            if in_range {
504                if range_start.is_none() {
505                    range_start = Some(map.display_start);
506                }
507                range_end = map.display_end;
508                continue;
509            }
510
511            if let Some(start) = range_start.take() {
512                if start < range_end {
513                    remapped.push(crate::text::RangeStyle {
514                        item: style.item.clone(),
515                        range: start..range_end,
516                    });
517                }
518            }
519        }
520
521        if let Some(start) = range_start.take() {
522            if start < range_end {
523                remapped.push(crate::text::RangeStyle {
524                    item: style.item.clone(),
525                    range: start..range_end,
526                });
527            }
528        }
529    }
530
531    remapped
532}
533
534fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
535    match max_width {
536        Some(width) if width.is_finite() && width > 0.0 => Some(width),
537        _ => None,
538    }
539}
540
541fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
542    measurer: &M,
543    line: &crate::text::AnnotatedString,
544    style: &TextStyle,
545    max_width: f32,
546    line_break: LineBreak,
547    hyphens: Hyphens,
548) -> Vec<crate::text::AnnotatedString> {
549    if line.text.is_empty() {
550        return vec![crate::text::AnnotatedString::from("")];
551    }
552
553    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
554        && line.text.chars().any(char::is_whitespace)
555    {
556        if let Some(balanced) =
557            wrap_line_with_word_balance(measurer, line, style, max_width, line_break)
558        {
559            return balanced;
560        }
561    }
562
563    wrap_line_greedy(measurer, line, style, max_width, line_break, hyphens)
564}
565
566fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
567    measurer: &M,
568    line: &crate::text::AnnotatedString,
569    style: &TextStyle,
570    max_width: f32,
571    line_break: LineBreak,
572    hyphens: Hyphens,
573) -> Vec<crate::text::AnnotatedString> {
574    let line_text = line.text.as_str();
575    let boundaries = char_boundaries(line_text);
576    let mut wrapped = Vec::new();
577    let mut start_idx = 0usize;
578
579    while start_idx < boundaries.len() - 1 {
580        let mut low = start_idx + 1;
581        let mut high = boundaries.len() - 1;
582        let mut best = start_idx + 1;
583
584        while low <= high {
585            let mid = (low + high) / 2;
586            let segment = line.subsequence(boundaries[start_idx]..boundaries[mid]);
587            let width = measurer.measure(&segment, style).width;
588            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
589                best = mid;
590                low = mid + 1;
591            } else {
592                if mid == 0 {
593                    break;
594                }
595                high = mid - 1;
596            }
597        }
598
599        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
600        let mut effective_wrap_idx = wrap_idx;
601        let can_hyphenate = hyphens == Hyphens::Auto
602            && wrap_idx == best
603            && best < boundaries.len() - 1
604            && is_break_inside_word(line_text, &boundaries, wrap_idx);
605        if can_hyphenate {
606            effective_wrap_idx = resolve_auto_hyphen_break(
607                measurer,
608                line_text,
609                style,
610                &boundaries,
611                start_idx,
612                wrap_idx,
613            );
614        }
615
616        let segment_start = boundaries[start_idx];
617        let mut segment_end = boundaries[effective_wrap_idx];
618        if wrap_idx != best {
619            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
620        }
621        wrapped.push(line.subsequence(segment_start..segment_end));
622
623        start_idx = if wrap_idx != best {
624            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
625        } else {
626            effective_wrap_idx
627        };
628    }
629
630    if wrapped.is_empty() {
631        wrapped.push(crate::text::AnnotatedString::from(""));
632    }
633
634    wrapped
635}
636
637fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
638    measurer: &M,
639    line: &crate::text::AnnotatedString,
640    style: &TextStyle,
641    max_width: f32,
642    line_break: LineBreak,
643) -> Option<Vec<crate::text::AnnotatedString>> {
644    let line_text = line.text.as_str();
645    let boundaries = char_boundaries(line_text);
646    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
647    if breakpoints.len() <= 2 {
648        return None;
649    }
650
651    let node_count = breakpoints.len();
652    let mut best_cost = vec![f32::INFINITY; node_count];
653    let mut next_index = vec![None; node_count];
654    best_cost[node_count - 1] = 0.0;
655
656    for start in (0..node_count - 1).rev() {
657        for end in start + 1..node_count {
658            let start_byte = boundaries[breakpoints[start]];
659            let end_byte = boundaries[breakpoints[end]];
660            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
661            if trimmed_end <= start_byte {
662                continue;
663            }
664            let segment = line.subsequence(start_byte..trimmed_end);
665            let segment_width = measurer.measure(&segment, style).width;
666            if segment_width > max_width + WRAP_EPSILON {
667                continue;
668            }
669            if !best_cost[end].is_finite() {
670                continue;
671            }
672            let slack = (max_width - segment_width).max(0.0);
673            let is_last = end == node_count - 1;
674            let segment_cost = match line_break {
675                LineBreak::Heading => slack * slack,
676                LineBreak::Paragraph => {
677                    if is_last {
678                        slack * slack * 0.16
679                    } else {
680                        slack * slack
681                    }
682                }
683                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
684            };
685            let candidate = segment_cost + best_cost[end];
686            if candidate < best_cost[start] {
687                best_cost[start] = candidate;
688                next_index[start] = Some(end);
689            }
690        }
691    }
692
693    let mut wrapped = Vec::new();
694    let mut current = 0usize;
695    while current < node_count - 1 {
696        let next = next_index[current]?;
697        let start_byte = boundaries[breakpoints[current]];
698        let end_byte = boundaries[breakpoints[next]];
699        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
700        if trimmed_end <= start_byte {
701            return None;
702        }
703        wrapped.push(line.subsequence(start_byte..trimmed_end));
704        current = next;
705    }
706
707    if wrapped.is_empty() {
708        return None;
709    }
710
711    Some(wrapped)
712}
713
714fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
715    let mut points = vec![0usize];
716    for idx in 1..boundaries.len() - 1 {
717        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
718        let current = &line[boundaries[idx]..boundaries[idx + 1]];
719        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
720            points.push(idx);
721        }
722    }
723    let end = boundaries.len() - 1;
724    if points.last().copied() != Some(end) {
725        points.push(end);
726    }
727    points
728}
729
730fn choose_wrap_break(
731    line: &str,
732    boundaries: &[usize],
733    start_idx: usize,
734    best: usize,
735    _line_break: LineBreak,
736) -> usize {
737    if best >= boundaries.len() - 1 {
738        return best;
739    }
740
741    if best <= start_idx + 1 {
742        return best;
743    }
744
745    for idx in (start_idx + 1..best).rev() {
746        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
747        if prev.chars().all(char::is_whitespace) {
748            return idx;
749        }
750    }
751    best
752}
753
754fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
755    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
756        return false;
757    }
758    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
759    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
760    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
761}
762
763fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
764    measurer: &M,
765    line: &str,
766    style: &TextStyle,
767    boundaries: &[usize],
768    start_idx: usize,
769    break_idx: usize,
770) -> usize {
771    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
772        if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
773            return candidate;
774        }
775    }
776    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
777}
778
779fn is_valid_auto_hyphen_break(
780    line: &str,
781    boundaries: &[usize],
782    start_idx: usize,
783    break_idx: usize,
784    candidate_idx: usize,
785) -> bool {
786    let end_idx = boundaries.len().saturating_sub(1);
787    candidate_idx > start_idx
788        && candidate_idx < end_idx
789        && candidate_idx <= break_idx
790        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
791        && is_break_inside_word(line, boundaries, candidate_idx)
792}
793
794fn choose_auto_hyphen_break_fallback(
795    boundaries: &[usize],
796    start_idx: usize,
797    break_idx: usize,
798) -> usize {
799    let end_idx = boundaries.len().saturating_sub(1);
800    if break_idx >= end_idx {
801        return break_idx;
802    }
803    let trailing_len = end_idx.saturating_sub(break_idx);
804    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
805        return break_idx;
806    }
807
808    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
809    let max_break = break_idx.saturating_sub(1);
810    if min_break > max_break {
811        return break_idx;
812    }
813
814    let mut best_break = break_idx;
815    let mut best_penalty = usize::MAX;
816    for idx in min_break..=max_break {
817        let candidate_trailing_len = end_idx.saturating_sub(idx);
818        let candidate_prefix_len = idx.saturating_sub(start_idx);
819        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
820            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
821        {
822            continue;
823        }
824
825        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
826        if penalty < best_penalty {
827            best_penalty = penalty;
828            best_break = idx;
829            if penalty == 0 {
830                break;
831            }
832        }
833    }
834    best_break
835}
836
837fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
838    while idx < boundaries.len() - 1 {
839        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
840        if !ch.chars().all(char::is_whitespace) {
841            break;
842        }
843        idx += 1;
844    }
845    idx
846}
847
848fn apply_line_overflow<M: TextMeasurer + ?Sized>(
849    measurer: &M,
850    line: &str,
851    style: &TextStyle,
852    max_width: Option<f32>,
853    options: TextLayoutOptions,
854    is_last_visible_line: bool,
855    single_line_ellipsis: bool,
856) -> String {
857    if options.overflow == TextOverflow::Clip || !is_last_visible_line {
858        return line.to_string();
859    }
860
861    let Some(width_limit) = max_width else {
862        return match options.overflow {
863            TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
864            TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
865            TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
866            TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
867        };
868    };
869
870    match options.overflow {
871        TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
872        TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
873        TextOverflow::StartEllipsis => {
874            if single_line_ellipsis {
875                fit_start_ellipsis(measurer, line, style, width_limit)
876            } else {
877                line.to_string()
878            }
879        }
880        TextOverflow::MiddleEllipsis => {
881            if single_line_ellipsis {
882                fit_middle_ellipsis(measurer, line, style, width_limit)
883            } else {
884                line.to_string()
885            }
886        }
887    }
888}
889
890fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
891    measurer: &M,
892    line: &str,
893    style: &TextStyle,
894    max_width: f32,
895) -> String {
896    if measurer
897        .measure(&crate::text::AnnotatedString::from(line), style)
898        .width
899        <= max_width + WRAP_EPSILON
900    {
901        return line.to_string();
902    }
903
904    let ellipsis_width = measurer
905        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
906        .width;
907    if ellipsis_width > max_width + WRAP_EPSILON {
908        return String::new();
909    }
910
911    let boundaries = char_boundaries(line);
912    let mut low = 0usize;
913    let mut high = boundaries.len() - 1;
914    let mut best = 0usize;
915
916    while low <= high {
917        let mid = (low + high) / 2;
918        let prefix = &line[..boundaries[mid]];
919        let candidate = format!("{prefix}{ELLIPSIS}");
920        let width = measurer
921            .measure(
922                &crate::text::AnnotatedString::from(candidate.as_str()),
923                style,
924            )
925            .width;
926        if width <= max_width + WRAP_EPSILON {
927            best = mid;
928            low = mid + 1;
929        } else if mid == 0 {
930            break;
931        } else {
932            high = mid - 1;
933        }
934    }
935
936    format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
937}
938
939fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
940    measurer: &M,
941    line: &str,
942    style: &TextStyle,
943    max_width: f32,
944) -> String {
945    if measurer
946        .measure(&crate::text::AnnotatedString::from(line), style)
947        .width
948        <= max_width + WRAP_EPSILON
949    {
950        return line.to_string();
951    }
952
953    let ellipsis_width = measurer
954        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
955        .width;
956    if ellipsis_width > max_width + WRAP_EPSILON {
957        return String::new();
958    }
959
960    let boundaries = char_boundaries(line);
961    let mut low = 0usize;
962    let mut high = boundaries.len() - 1;
963    let mut best = boundaries.len() - 1;
964
965    while low <= high {
966        let mid = (low + high) / 2;
967        let suffix = &line[boundaries[mid]..];
968        let candidate = format!("{ELLIPSIS}{suffix}");
969        let width = measurer
970            .measure(
971                &crate::text::AnnotatedString::from(candidate.as_str()),
972                style,
973            )
974            .width;
975        if width <= max_width + WRAP_EPSILON {
976            best = mid;
977            if mid == 0 {
978                break;
979            }
980            high = mid - 1;
981        } else {
982            low = mid + 1;
983        }
984    }
985
986    format!("{ELLIPSIS}{}", &line[boundaries[best]..])
987}
988
989fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
990    measurer: &M,
991    line: &str,
992    style: &TextStyle,
993    max_width: f32,
994) -> String {
995    if measurer
996        .measure(&crate::text::AnnotatedString::from(line), style)
997        .width
998        <= max_width + WRAP_EPSILON
999    {
1000        return line.to_string();
1001    }
1002
1003    let ellipsis_width = measurer
1004        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1005        .width;
1006    if ellipsis_width > max_width + WRAP_EPSILON {
1007        return String::new();
1008    }
1009
1010    let boundaries = char_boundaries(line);
1011    let total_chars = boundaries.len().saturating_sub(1);
1012    for keep in (0..=total_chars).rev() {
1013        let keep_start = keep.div_ceil(2);
1014        let keep_end = keep / 2;
1015        let start = &line[..boundaries[keep_start]];
1016        let end_start = boundaries[total_chars.saturating_sub(keep_end)];
1017        let end = &line[end_start..];
1018        let candidate = format!("{start}{ELLIPSIS}{end}");
1019        if measurer
1020            .measure(
1021                &crate::text::AnnotatedString::from(candidate.as_str()),
1022                style,
1023            )
1024            .width
1025            <= max_width + WRAP_EPSILON
1026        {
1027            return candidate;
1028        }
1029    }
1030
1031    ELLIPSIS.to_string()
1032}
1033
1034fn char_boundaries(text: &str) -> Vec<usize> {
1035    let mut out = Vec::with_capacity(text.chars().count() + 1);
1036    out.push(0);
1037    for (idx, _) in text.char_indices() {
1038        if idx != 0 {
1039            out.push(idx);
1040        }
1041    }
1042    out.push(text.len());
1043    out
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use super::*;
1049    use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
1050    use crate::text_layout_result::TextLayoutResult;
1051
1052    struct ContractBreakMeasurer {
1053        retreat: usize,
1054    }
1055
1056    impl TextMeasurer for ContractBreakMeasurer {
1057        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
1058            MonospacedTextMeasurer.measure(
1059                &crate::text::AnnotatedString::from(text.text.as_str()),
1060                style,
1061            )
1062        }
1063
1064        fn get_offset_for_position(
1065            &self,
1066            text: &crate::text::AnnotatedString,
1067            style: &TextStyle,
1068            x: f32,
1069            y: f32,
1070        ) -> usize {
1071            MonospacedTextMeasurer.get_offset_for_position(
1072                &crate::text::AnnotatedString::from(text.text.as_str()),
1073                style,
1074                x,
1075                y,
1076            )
1077        }
1078
1079        fn get_cursor_x_for_offset(
1080            &self,
1081            text: &crate::text::AnnotatedString,
1082            style: &TextStyle,
1083            offset: usize,
1084        ) -> f32 {
1085            MonospacedTextMeasurer.get_cursor_x_for_offset(
1086                &crate::text::AnnotatedString::from(text.text.as_str()),
1087                style,
1088                offset,
1089            )
1090        }
1091
1092        fn layout(
1093            &self,
1094            text: &crate::text::AnnotatedString,
1095            style: &TextStyle,
1096        ) -> TextLayoutResult {
1097            MonospacedTextMeasurer.layout(
1098                &crate::text::AnnotatedString::from(text.text.as_str()),
1099                style,
1100            )
1101        }
1102
1103        fn choose_auto_hyphen_break(
1104            &self,
1105            _line: &str,
1106            _style: &TextStyle,
1107            _segment_start_char: usize,
1108            measured_break_char: usize,
1109        ) -> Option<usize> {
1110            measured_break_char.checked_sub(self.retreat)
1111        }
1112    }
1113
1114    fn style_with_line_break(line_break: LineBreak) -> TextStyle {
1115        TextStyle {
1116            span_style: crate::text::SpanStyle {
1117                font_size: TextUnit::Sp(10.0),
1118                ..Default::default()
1119            },
1120            paragraph_style: ParagraphStyle {
1121                line_break,
1122                ..Default::default()
1123            },
1124        }
1125    }
1126
1127    fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
1128        TextStyle {
1129            span_style: crate::text::SpanStyle {
1130                font_size: TextUnit::Sp(10.0),
1131                ..Default::default()
1132            },
1133            paragraph_style: ParagraphStyle {
1134                hyphens,
1135                ..Default::default()
1136            },
1137        }
1138    }
1139
1140    #[test]
1141    fn text_layout_options_wraps_and_limits_lines() {
1142        let style = TextStyle {
1143            span_style: crate::text::SpanStyle {
1144                font_size: TextUnit::Sp(10.0),
1145                ..Default::default()
1146            },
1147            ..Default::default()
1148        };
1149        let options = TextLayoutOptions {
1150            overflow: TextOverflow::Clip,
1151            soft_wrap: true,
1152            max_lines: 2,
1153            min_lines: 1,
1154        };
1155
1156        let prepared = prepare_text_layout(
1157            &crate::text::AnnotatedString::from("A B C D E F"),
1158            &style,
1159            options,
1160            Some(24.0), // roughly 4 chars in monospaced fallback
1161        );
1162
1163        assert!(prepared.did_overflow);
1164        assert!(prepared.metrics.line_count <= 2);
1165    }
1166
1167    #[test]
1168    fn text_layout_options_end_ellipsis_applies() {
1169        let style = TextStyle {
1170            span_style: crate::text::SpanStyle {
1171                font_size: TextUnit::Sp(10.0),
1172                ..Default::default()
1173            },
1174            ..Default::default()
1175        };
1176        let options = TextLayoutOptions {
1177            overflow: TextOverflow::Ellipsis,
1178            soft_wrap: false,
1179            max_lines: 1,
1180            min_lines: 1,
1181        };
1182
1183        let prepared = prepare_text_layout(
1184            &crate::text::AnnotatedString::from("Long long line"),
1185            &style,
1186            options,
1187            Some(20.0),
1188        );
1189        assert!(prepared.did_overflow);
1190        assert!(prepared.text.text.contains(ELLIPSIS));
1191    }
1192
1193    #[test]
1194    fn text_layout_options_visible_keeps_full_text() {
1195        let style = TextStyle {
1196            span_style: crate::text::SpanStyle {
1197                font_size: TextUnit::Sp(10.0),
1198                ..Default::default()
1199            },
1200            ..Default::default()
1201        };
1202        let options = TextLayoutOptions {
1203            overflow: TextOverflow::Visible,
1204            soft_wrap: false,
1205            max_lines: 1,
1206            min_lines: 1,
1207        };
1208
1209        let input = "This should remain unchanged";
1210        let prepared = prepare_text_layout(
1211            &crate::text::AnnotatedString::from(input),
1212            &style,
1213            options,
1214            Some(10.0),
1215        );
1216        assert_eq!(prepared.text.text, input);
1217    }
1218
1219    #[test]
1220    fn text_layout_options_respects_min_lines() {
1221        let style = TextStyle {
1222            span_style: crate::text::SpanStyle {
1223                font_size: TextUnit::Sp(10.0),
1224                ..Default::default()
1225            },
1226            ..Default::default()
1227        };
1228        let options = TextLayoutOptions {
1229            overflow: TextOverflow::Clip,
1230            soft_wrap: true,
1231            max_lines: 4,
1232            min_lines: 3,
1233        };
1234
1235        let prepared = prepare_text_layout(
1236            &crate::text::AnnotatedString::from("short"),
1237            &style,
1238            options,
1239            Some(100.0),
1240        );
1241        assert_eq!(prepared.metrics.line_count, 3);
1242    }
1243
1244    #[test]
1245    fn text_layout_options_middle_ellipsis_for_single_line() {
1246        let style = TextStyle {
1247            span_style: crate::text::SpanStyle {
1248                font_size: TextUnit::Sp(10.0),
1249                ..Default::default()
1250            },
1251            ..Default::default()
1252        };
1253        let options = TextLayoutOptions {
1254            overflow: TextOverflow::MiddleEllipsis,
1255            soft_wrap: false,
1256            max_lines: 1,
1257            min_lines: 1,
1258        };
1259
1260        let prepared = prepare_text_layout(
1261            &crate::text::AnnotatedString::from("abcdefghijk"),
1262            &style,
1263            options,
1264            Some(24.0),
1265        );
1266        assert!(prepared.text.text.contains(ELLIPSIS));
1267        assert!(prepared.did_overflow);
1268    }
1269
1270    #[test]
1271    fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
1272        let style = TextStyle {
1273            span_style: crate::text::SpanStyle {
1274                font_size: TextUnit::Sp(10.0),
1275                ..Default::default()
1276            },
1277            ..Default::default()
1278        };
1279        let options = TextLayoutOptions {
1280            overflow: TextOverflow::Clip,
1281            soft_wrap: true,
1282            max_lines: usize::MAX,
1283            min_lines: 1,
1284        };
1285
1286        let text = "if counter % 2 == 0";
1287        let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
1288        let prepared = prepare_text_layout(
1289            &crate::text::AnnotatedString::from(text),
1290            &style,
1291            options,
1292            Some(exact_width - 0.1),
1293        );
1294
1295        assert!(
1296            !prepared.text.text.contains('\n'),
1297            "unexpected line split: {:?}",
1298            prepared.text
1299        );
1300    }
1301
1302    #[test]
1303    fn line_break_mode_changes_wrap_strategy_contract() {
1304        let text = "This is an example text";
1305        let options = TextLayoutOptions {
1306            overflow: TextOverflow::Clip,
1307            soft_wrap: true,
1308            max_lines: usize::MAX,
1309            min_lines: 1,
1310        };
1311
1312        let simple = prepare_text_layout(
1313            &crate::text::AnnotatedString::from(text),
1314            &style_with_line_break(LineBreak::Simple),
1315            options,
1316            Some(120.0),
1317        );
1318        let heading = prepare_text_layout(
1319            &crate::text::AnnotatedString::from(text),
1320            &style_with_line_break(LineBreak::Heading),
1321            options,
1322            Some(120.0),
1323        );
1324        let paragraph = prepare_text_layout(
1325            &crate::text::AnnotatedString::from(text),
1326            &style_with_line_break(LineBreak::Paragraph),
1327            options,
1328            Some(50.0),
1329        );
1330
1331        assert_eq!(
1332            simple.text.text.lines().collect::<Vec<_>>(),
1333            vec!["This is an example", "text"]
1334        );
1335        assert_eq!(
1336            heading.text.text.lines().collect::<Vec<_>>(),
1337            vec!["This is an", "example text"]
1338        );
1339        assert_eq!(
1340            paragraph.text.text.lines().collect::<Vec<_>>(),
1341            vec!["This", "is an", "example", "text"]
1342        );
1343    }
1344
1345    #[test]
1346    fn hyphens_mode_changes_wrap_strategy_contract() {
1347        let text = "Transformation";
1348        let options = TextLayoutOptions {
1349            overflow: TextOverflow::Clip,
1350            soft_wrap: true,
1351            max_lines: usize::MAX,
1352            min_lines: 1,
1353        };
1354
1355        let auto = prepare_text_layout(
1356            &crate::text::AnnotatedString::from(text),
1357            &style_with_hyphens(Hyphens::Auto),
1358            options,
1359            Some(24.0),
1360        );
1361        let none = prepare_text_layout(
1362            &crate::text::AnnotatedString::from(text),
1363            &style_with_hyphens(Hyphens::None),
1364            options,
1365            Some(24.0),
1366        );
1367
1368        assert_eq!(
1369            auto.text.text.lines().collect::<Vec<_>>(),
1370            vec!["Tran", "sfor", "ma", "tion"]
1371        );
1372        assert_eq!(
1373            none.text.text.lines().collect::<Vec<_>>(),
1374            vec!["Tran", "sfor", "mati", "on"]
1375        );
1376        assert!(
1377            !auto.text.text.contains('-'),
1378            "automatic hyphenation should influence breaks without mutating source text content"
1379        );
1380    }
1381
1382    #[test]
1383    fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
1384        let text = "Transformation";
1385        let style = style_with_hyphens(Hyphens::Auto);
1386        let options = TextLayoutOptions {
1387            overflow: TextOverflow::Clip,
1388            soft_wrap: true,
1389            max_lines: usize::MAX,
1390            min_lines: 1,
1391        };
1392
1393        let prepared = prepare_text_layout_fallback(
1394            &ContractBreakMeasurer { retreat: 1 },
1395            &crate::text::AnnotatedString::from(text),
1396            &style,
1397            options,
1398            Some(24.0),
1399        );
1400
1401        assert_eq!(
1402            prepared.text.text.lines().collect::<Vec<_>>(),
1403            vec!["Tra", "nsf", "orm", "ati", "on"]
1404        );
1405    }
1406
1407    #[test]
1408    fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
1409        let text = "Transformation";
1410        let style = style_with_hyphens(Hyphens::Auto);
1411        let options = TextLayoutOptions {
1412            overflow: TextOverflow::Clip,
1413            soft_wrap: true,
1414            max_lines: usize::MAX,
1415            min_lines: 1,
1416        };
1417
1418        let prepared = prepare_text_layout_fallback(
1419            &ContractBreakMeasurer { retreat: 10 },
1420            &crate::text::AnnotatedString::from(text),
1421            &style,
1422            options,
1423            Some(24.0),
1424        );
1425
1426        assert_eq!(
1427            prepared.text.text.lines().collect::<Vec<_>>(),
1428            vec!["Tran", "sfor", "ma", "tion"]
1429        );
1430    }
1431
1432    #[test]
1433    fn transformed_text_keeps_span_ranges_within_display_bounds() {
1434        let style = TextStyle {
1435            span_style: crate::text::SpanStyle {
1436                font_size: TextUnit::Sp(10.0),
1437                ..Default::default()
1438            },
1439            ..Default::default()
1440        };
1441        let options = TextLayoutOptions {
1442            overflow: TextOverflow::Ellipsis,
1443            soft_wrap: false,
1444            max_lines: 1,
1445            min_lines: 1,
1446        };
1447        let annotated = crate::text::AnnotatedString::builder()
1448            .push_style(crate::text::SpanStyle {
1449                font_weight: Some(crate::text::FontWeight::BOLD),
1450                ..Default::default()
1451            })
1452            .append("Styled overflow text sample")
1453            .pop()
1454            .to_annotated_string();
1455
1456        let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
1457        assert!(prepared.did_overflow);
1458        for span in &prepared.text.span_styles {
1459            assert!(span.range.start < span.range.end);
1460            assert!(span.range.end <= prepared.text.text.len());
1461            assert!(prepared.text.text.is_char_boundary(span.range.start));
1462            assert!(prepared.text.text.is_char_boundary(span.range.end));
1463        }
1464    }
1465
1466    #[test]
1467    fn wrapped_text_splits_styles_around_inserted_newlines() {
1468        let style = TextStyle {
1469            span_style: crate::text::SpanStyle {
1470                font_size: TextUnit::Sp(10.0),
1471                ..Default::default()
1472            },
1473            ..Default::default()
1474        };
1475        let options = TextLayoutOptions {
1476            overflow: TextOverflow::Clip,
1477            soft_wrap: true,
1478            max_lines: usize::MAX,
1479            min_lines: 1,
1480        };
1481        let annotated = crate::text::AnnotatedString::builder()
1482            .push_style(crate::text::SpanStyle {
1483                text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
1484                ..Default::default()
1485            })
1486            .append("Wrapped style text example")
1487            .pop()
1488            .to_annotated_string();
1489
1490        let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
1491        assert!(prepared.text.text.contains('\n'));
1492        assert!(!prepared.text.span_styles.is_empty());
1493        for span in &prepared.text.span_styles {
1494            assert!(span.range.end <= prepared.text.text.len());
1495        }
1496    }
1497
1498    #[test]
1499    fn mixed_font_size_segments_wrap_without_truncation() {
1500        let style = TextStyle {
1501            span_style: crate::text::SpanStyle {
1502                font_size: TextUnit::Sp(14.0),
1503                ..Default::default()
1504            },
1505            ..Default::default()
1506        };
1507        let options = TextLayoutOptions {
1508            overflow: TextOverflow::Clip,
1509            soft_wrap: true,
1510            max_lines: usize::MAX,
1511            min_lines: 1,
1512        };
1513        let annotated = crate::text::AnnotatedString::builder()
1514            .append("You can also ")
1515            .push_style(crate::text::SpanStyle {
1516                font_size: TextUnit::Sp(22.0),
1517                ..Default::default()
1518            })
1519            .append("change font size")
1520            .pop()
1521            .append(" dynamically mid-sentence!")
1522            .to_annotated_string();
1523
1524        let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
1525        assert!(prepared.text.text.contains('\n'));
1526        assert!(prepared.text.text.contains("mid-sentence!"));
1527        assert!(!prepared.did_overflow);
1528    }
1529}