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::{Cell, RefCell};
5use std::collections::{hash_map::Entry, HashMap, VecDeque};
6use std::hash::Hash;
7use std::ops::Range;
8use std::rc::Rc;
9use std::time::Instant;
10
11use super::layout_options::{TextLayoutOptions, TextOverflow};
12use super::paragraph::{Hyphens, LineBreak};
13use super::style::TextStyle;
14
15const ELLIPSIS: &str = "\u{2026}";
16const DEFAULT_FONT_SIZE_SP: f32 = 14.0;
17const WRAP_EPSILON: f32 = 0.5;
18const SCALE_DOWN_SEARCH_STEPS: usize = 14;
19const AUTO_HYPHEN_MIN_SEGMENT_CHARS: usize = 2;
20const AUTO_HYPHEN_MIN_TRAILING_CHARS: usize = 3;
21const AUTO_HYPHEN_PREFERRED_TRAILING_CHARS: usize = 4;
22const TEXT_SERVICE_CACHE_CAPACITY: usize = 8192;
23const TEXT_LAYOUT_TELEMETRY_ENV: &str = "CRANPOSE_TEXT_LAYOUT_TELEMETRY";
24
25fn text_layout_telemetry_enabled() -> bool {
26    std::env::var_os(TEXT_LAYOUT_TELEMETRY_ENV).is_some()
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct TextMetrics {
31    pub width: f32,
32    pub height: f32,
33    /// Height of a single line of text
34    pub line_height: f32,
35    /// Number of lines in the text
36    pub line_count: usize,
37}
38
39#[derive(Clone, Debug, PartialEq)]
40pub struct PreparedTextLayout {
41    pub text: crate::text::AnnotatedString,
42    pub visual_style: TextStyle,
43    pub metrics: TextMetrics,
44    pub did_overflow: bool,
45}
46
47#[derive(Clone, Debug, PartialEq)]
48pub struct TextLinePrefixWidths {
49    prefix_widths: Vec<f32>,
50    separator_before: Vec<f32>,
51    non_empty_overhang: f32,
52}
53
54impl TextLinePrefixWidths {
55    pub fn from_parts(
56        prefix_widths: Vec<f32>,
57        separator_before: Vec<f32>,
58        non_empty_overhang: f32,
59    ) -> Option<Self> {
60        if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
61            return None;
62        }
63        if prefix_widths
64            .iter()
65            .chain(separator_before.iter())
66            .any(|value| !value.is_finite())
67        {
68            return None;
69        }
70        let non_empty_overhang = non_empty_overhang.max(0.0);
71        if !non_empty_overhang.is_finite() {
72            return None;
73        }
74        Some(Self {
75            prefix_widths,
76            separator_before,
77            non_empty_overhang,
78        })
79    }
80
81    pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
82        if !char_width.is_finite() || !letter_spacing.is_finite() {
83            return None;
84        }
85        let char_width = char_width.max(0.0);
86        let letter_spacing = letter_spacing.max(0.0);
87        let mut prefix_widths = Vec::with_capacity(char_count + 1);
88        let mut separator_before = Vec::with_capacity(char_count);
89        let mut width = 0.0f32;
90        prefix_widths.push(width);
91        for index in 0..char_count {
92            let separator = if index == 0 { 0.0 } else { letter_spacing };
93            separator_before.push(separator);
94            width += separator + char_width;
95            prefix_widths.push(width);
96        }
97        Self::from_parts(prefix_widths, separator_before, 0.0)
98    }
99
100    pub fn char_count(&self) -> usize {
101        self.separator_before.len()
102    }
103
104    pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
105        if start > end || end > self.char_count() {
106            return None;
107        }
108        if start == end {
109            return Some(0.0);
110        }
111        let separator = self.separator_before.get(start).copied().unwrap_or(0.0);
112        Some(
113            (self.prefix_widths[end] - self.prefix_widths[start] - separator).max(0.0)
114                + self.non_empty_overhang,
115        )
116    }
117}
118
119pub trait TextMeasurer: 'static {
120    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
121
122    fn measure_for_node(
123        &self,
124        node_id: Option<NodeId>,
125        text: &crate::text::AnnotatedString,
126        style: &TextStyle,
127    ) -> TextMetrics {
128        let _ = node_id;
129        self.measure(text, style)
130    }
131
132    fn measure_subsequence(
133        &self,
134        text: &crate::text::AnnotatedString,
135        range: Range<usize>,
136        style: &TextStyle,
137    ) -> TextMetrics {
138        self.measure(&text.subsequence(range), style)
139    }
140
141    fn measure_subsequence_for_node(
142        &self,
143        node_id: Option<NodeId>,
144        text: &crate::text::AnnotatedString,
145        range: Range<usize>,
146        style: &TextStyle,
147    ) -> TextMetrics {
148        let _ = node_id;
149        self.measure_subsequence(text, range, style)
150    }
151
152    fn measure_line_prefix_widths(
153        &self,
154        text: &crate::text::AnnotatedString,
155        line_range: Range<usize>,
156        style: &TextStyle,
157    ) -> Option<TextLinePrefixWidths> {
158        let _ = text;
159        let _ = line_range;
160        let _ = style;
161        None
162    }
163
164    fn measure_line_width(
165        &self,
166        text: &crate::text::AnnotatedString,
167        line_range: Range<usize>,
168        style: &TextStyle,
169    ) -> Option<f32> {
170        let _ = text;
171        let _ = line_range;
172        let _ = style;
173        None
174    }
175
176    fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
177        self.measure(text, style).line_height
178    }
179
180    fn line_height_for_node(
181        &self,
182        node_id: Option<NodeId>,
183        text: &crate::text::AnnotatedString,
184        style: &TextStyle,
185    ) -> f32 {
186        let _ = node_id;
187        self.line_height(text, style)
188    }
189
190    fn get_offset_for_position(
191        &self,
192        text: &crate::text::AnnotatedString,
193        style: &TextStyle,
194        x: f32,
195        y: f32,
196    ) -> usize;
197
198    fn get_cursor_x_for_offset(
199        &self,
200        text: &crate::text::AnnotatedString,
201        style: &TextStyle,
202        offset: usize,
203    ) -> f32;
204
205    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
206
207    /// Returns an alternate break boundary for `Hyphens::Auto` when a greedy break
208    /// split lands in the middle of a word.
209    ///
210    /// `segment_start_char` and `measured_break_char` are character-boundary indices
211    /// in `line` (not byte offsets). Return `None` to delegate to fallback behavior.
212    fn choose_auto_hyphen_break(
213        &self,
214        _line: &str,
215        _style: &TextStyle,
216        _segment_start_char: usize,
217        _measured_break_char: usize,
218    ) -> Option<usize> {
219        None
220    }
221
222    fn measure_with_options(
223        &self,
224        text: &crate::text::AnnotatedString,
225        style: &TextStyle,
226        options: TextLayoutOptions,
227        max_width: Option<f32>,
228    ) -> TextMetrics {
229        self.prepare_with_options(text, style, options, max_width)
230            .metrics
231    }
232
233    fn measure_with_options_for_node(
234        &self,
235        node_id: Option<NodeId>,
236        text: &crate::text::AnnotatedString,
237        style: &TextStyle,
238        options: TextLayoutOptions,
239        max_width: Option<f32>,
240    ) -> TextMetrics {
241        self.prepare_with_options_for_node(node_id, text, style, options, max_width)
242            .metrics
243    }
244
245    fn prepare_with_options(
246        &self,
247        text: &crate::text::AnnotatedString,
248        style: &TextStyle,
249        options: TextLayoutOptions,
250        max_width: Option<f32>,
251    ) -> PreparedTextLayout {
252        self.prepare_with_options_fallback(text, style, options, max_width)
253    }
254
255    fn prepare_with_options_for_node(
256        &self,
257        node_id: Option<NodeId>,
258        text: &crate::text::AnnotatedString,
259        style: &TextStyle,
260        options: TextLayoutOptions,
261        max_width: Option<f32>,
262    ) -> PreparedTextLayout {
263        prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
264    }
265
266    fn prepare_with_options_fallback(
267        &self,
268        text: &crate::text::AnnotatedString,
269        style: &TextStyle,
270        options: TextLayoutOptions,
271        max_width: Option<f32>,
272    ) -> PreparedTextLayout {
273        prepare_text_layout_fallback(self, text, style, options, max_width)
274    }
275}
276
277#[derive(Default)]
278struct MonospacedTextMeasurer;
279
280impl MonospacedTextMeasurer {
281    const DEFAULT_SIZE: f32 = 14.0;
282    const CHAR_WIDTH_RATIO: f32 = 0.6; // Width is 0.6 of Height
283
284    fn get_metrics(style: &TextStyle) -> (f32, f32) {
285        let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
286        let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
287        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
288        (
289            (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
290            line_height,
291        )
292    }
293}
294
295impl TextMeasurer for MonospacedTextMeasurer {
296    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
297        let (char_width, line_height) = Self::get_metrics(style);
298
299        let lines: Vec<&str> = text.text.split('\n').collect();
300        let line_count = lines.len().max(1);
301
302        let width = lines
303            .iter()
304            .map(|line| line.chars().count() as f32 * char_width)
305            .fold(0.0_f32, f32::max);
306
307        TextMetrics {
308            width,
309            height: line_count as f32 * line_height,
310            line_height,
311            line_count,
312        }
313    }
314
315    fn measure_subsequence(
316        &self,
317        text: &crate::text::AnnotatedString,
318        range: Range<usize>,
319        style: &TextStyle,
320    ) -> TextMetrics {
321        let (char_width, line_height) = Self::get_metrics(style);
322        let slice = &text.text[range];
323        let line_count = slice.split('\n').count().max(1);
324        let width = slice
325            .split('\n')
326            .map(|line| line.chars().count() as f32 * char_width)
327            .fold(0.0_f32, f32::max);
328
329        TextMetrics {
330            width,
331            height: line_count as f32 * line_height,
332            line_height,
333            line_count,
334        }
335    }
336
337    fn measure_line_prefix_widths(
338        &self,
339        text: &crate::text::AnnotatedString,
340        line_range: Range<usize>,
341        style: &TextStyle,
342    ) -> Option<TextLinePrefixWidths> {
343        let (char_width, _) = Self::get_metrics(style);
344        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
345        TextLinePrefixWidths::monospaced(
346            text.text[line_range].chars().count(),
347            char_width,
348            letter_spacing,
349        )
350    }
351
352    fn measure_line_width(
353        &self,
354        text: &crate::text::AnnotatedString,
355        line_range: Range<usize>,
356        style: &TextStyle,
357    ) -> Option<f32> {
358        Some(self.measure_subsequence(text, line_range, style).width)
359    }
360
361    fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
362        let (_, line_height) = Self::get_metrics(style);
363        line_height
364    }
365
366    fn get_offset_for_position(
367        &self,
368        text: &crate::text::AnnotatedString,
369        style: &TextStyle,
370        x: f32,
371        y: f32,
372    ) -> usize {
373        let (char_width, line_height) = Self::get_metrics(style);
374
375        if text.text.is_empty() {
376            return 0;
377        }
378
379        let line_index = (y / line_height).floor().max(0.0) as usize;
380        let lines: Vec<&str> = text.text.split('\n').collect();
381        let target_line = line_index.min(lines.len().saturating_sub(1));
382
383        let mut line_start_byte = 0;
384        for line in lines.iter().take(target_line) {
385            line_start_byte += line.len() + 1;
386        }
387
388        let line_text = lines.get(target_line).unwrap_or(&"");
389        let char_index = (x / char_width).round() as usize;
390        let line_char_count = line_text.chars().count();
391        let clamped_index = char_index.min(line_char_count);
392
393        let offset_in_line = line_text
394            .char_indices()
395            .nth(clamped_index)
396            .map(|(i, _)| i)
397            .unwrap_or(line_text.len());
398
399        line_start_byte + offset_in_line
400    }
401
402    fn get_cursor_x_for_offset(
403        &self,
404        text: &crate::text::AnnotatedString,
405        style: &TextStyle,
406        offset: usize,
407    ) -> f32 {
408        let (char_width, _) = Self::get_metrics(style);
409
410        let clamped_offset = offset.min(text.text.len());
411        let char_count = text.text[..clamped_offset].chars().count();
412        char_count as f32 * char_width
413    }
414
415    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
416        let (char_width, line_height) = Self::get_metrics(style);
417        TextLayoutResult::monospaced(&text.text, char_width, line_height)
418    }
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
422struct TextBaseCacheKey {
423    text_hash: u64,
424    style_hash: u64,
425}
426
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
428struct TextOptionsCacheKey {
429    base: TextBaseCacheKey,
430    options: TextLayoutOptions,
431    max_width_bits: Option<u32>,
432}
433
434struct BoundedTextCache<K, V> {
435    capacity: usize,
436    entries: HashMap<K, V>,
437    order: VecDeque<K>,
438}
439
440impl<K, V> BoundedTextCache<K, V>
441where
442    K: Clone + Eq + Hash,
443    V: Clone,
444{
445    fn new(capacity: usize) -> Self {
446        Self {
447            capacity,
448            entries: HashMap::new(),
449            order: VecDeque::new(),
450        }
451    }
452
453    fn clear(&mut self) {
454        self.entries.clear();
455        self.order.clear();
456    }
457
458    fn get(&self, key: &K) -> Option<V> {
459        self.entries.get(key).cloned()
460    }
461
462    fn insert(&mut self, key: K, value: V) {
463        match self.entries.entry(key.clone()) {
464            Entry::Occupied(mut entry) => {
465                entry.insert(value);
466                return;
467            }
468            Entry::Vacant(_) => {}
469        }
470        if self.entries.len() == self.capacity {
471            while let Some(evicted) = self.order.pop_front() {
472                if self.entries.remove(&evicted).is_some() {
473                    break;
474                }
475            }
476        }
477        self.order.push_back(key.clone());
478        self.entries.insert(key, value);
479    }
480}
481
482pub(crate) struct TextService {
483    generation: Cell<u64>,
484    measurer: RefCell<Rc<dyn TextMeasurer>>,
485    metrics_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextMetrics>>,
486    options_metrics_cache: RefCell<BoundedTextCache<TextOptionsCacheKey, TextMetrics>>,
487    prepared_cache: RefCell<BoundedTextCache<TextOptionsCacheKey, PreparedTextLayout>>,
488    layout_cache: RefCell<BoundedTextCache<TextBaseCacheKey, TextLayoutResult>>,
489}
490
491impl TextService {
492    pub(crate) fn new() -> Self {
493        Self::from_measurer(Rc::new(MonospacedTextMeasurer))
494    }
495
496    pub(crate) fn from_measurer(measurer: Rc<dyn TextMeasurer>) -> Self {
497        Self {
498            generation: Cell::new(1),
499            measurer: RefCell::new(measurer),
500            metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
501            options_metrics_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
502            prepared_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
503            layout_cache: RefCell::new(BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY)),
504        }
505    }
506
507    pub(crate) fn set_measurer(&self, measurer: Rc<dyn TextMeasurer>) {
508        *self.measurer.borrow_mut() = measurer;
509        self.clear_caches();
510    }
511
512    pub(crate) fn generation(&self) -> u64 {
513        self.generation.get()
514    }
515
516    pub(crate) fn current_measurer(&self) -> Rc<dyn TextMeasurer> {
517        Rc::clone(&self.measurer.borrow())
518    }
519
520    pub(crate) fn with_measurer<R>(&self, f: impl FnOnce(&dyn TextMeasurer) -> R) -> R {
521        let measurer = self.current_measurer();
522        f(&*measurer)
523    }
524
525    pub(crate) fn measure(
526        &self,
527        node_id: Option<NodeId>,
528        text: &crate::text::AnnotatedString,
529        style: &TextStyle,
530    ) -> TextMetrics {
531        let key = text_base_cache_key(text, style);
532        if let Some(metrics) = self.metrics_cache.borrow().get(&key) {
533            return metrics;
534        }
535        let metrics = self.with_measurer(|m| m.measure_for_node(node_id, text, style));
536        self.metrics_cache.borrow_mut().insert(key, metrics);
537        metrics
538    }
539
540    pub(crate) fn measure_with_options(
541        &self,
542        node_id: Option<NodeId>,
543        text: &crate::text::AnnotatedString,
544        style: &TextStyle,
545        options: TextLayoutOptions,
546        max_width: Option<f32>,
547    ) -> TextMetrics {
548        let key = text_options_cache_key(text, style, options.normalized(), max_width);
549        if let Some(metrics) = self.options_metrics_cache.borrow().get(&key) {
550            return metrics;
551        }
552        let metrics = self.with_measurer(|m| {
553            m.measure_with_options_for_node(node_id, text, style, options.normalized(), max_width)
554        });
555        self.options_metrics_cache.borrow_mut().insert(key, metrics);
556        metrics
557    }
558
559    pub(crate) fn prepare_with_options(
560        &self,
561        node_id: Option<NodeId>,
562        text: &crate::text::AnnotatedString,
563        style: &TextStyle,
564        options: TextLayoutOptions,
565        max_width: Option<f32>,
566    ) -> PreparedTextLayout {
567        let key = text_options_cache_key(text, style, options.normalized(), max_width);
568        if let Some(prepared) = self.prepared_cache.borrow().get(&key) {
569            return prepared;
570        }
571        let prepared = self.with_measurer(|m| {
572            m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
573        });
574        self.prepared_cache
575            .borrow_mut()
576            .insert(key, prepared.clone());
577        self.options_metrics_cache
578            .borrow_mut()
579            .insert(key, prepared.metrics);
580        prepared
581    }
582
583    pub(crate) fn layout(
584        &self,
585        text: &crate::text::AnnotatedString,
586        style: &TextStyle,
587    ) -> TextLayoutResult {
588        let key = text_base_cache_key(text, style);
589        if let Some(layout) = self.layout_cache.borrow().get(&key) {
590            return layout;
591        }
592        let layout = self.with_measurer(|m| m.layout(text, style));
593        self.layout_cache.borrow_mut().insert(key, layout.clone());
594        layout
595    }
596
597    fn clear_caches(&self) {
598        self.generation
599            .set(self.generation.get().wrapping_add(1).max(1));
600        self.metrics_cache.borrow_mut().clear();
601        self.options_metrics_cache.borrow_mut().clear();
602        self.prepared_cache.borrow_mut().clear();
603        self.layout_cache.borrow_mut().clear();
604    }
605}
606
607fn text_base_cache_key(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextBaseCacheKey {
608    TextBaseCacheKey {
609        text_hash: text.render_hash(),
610        style_hash: style.measurement_hash(),
611    }
612}
613
614fn text_options_cache_key(
615    text: &crate::text::AnnotatedString,
616    style: &TextStyle,
617    options: TextLayoutOptions,
618    max_width: Option<f32>,
619) -> TextOptionsCacheKey {
620    TextOptionsCacheKey {
621        base: text_base_cache_key(text, style),
622        options: options.normalized(),
623        max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
624    }
625}
626
627pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
628    crate::render_state::set_current_text_measurer(Rc::new(measurer));
629}
630
631pub(crate) fn current_text_generation() -> u64 {
632    crate::render_state::with_text_service(TextService::generation)
633}
634
635pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
636    crate::render_state::with_text_service(|service| service.measure(None, text, style))
637}
638
639pub fn measure_text_for_node(
640    node_id: Option<NodeId>,
641    text: &crate::text::AnnotatedString,
642    style: &TextStyle,
643) -> TextMetrics {
644    crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
645}
646
647pub fn measure_text_with_options(
648    text: &crate::text::AnnotatedString,
649    style: &TextStyle,
650    options: TextLayoutOptions,
651    max_width: Option<f32>,
652) -> TextMetrics {
653    crate::render_state::with_text_service(|service| {
654        service.measure_with_options(None, text, style, options.normalized(), max_width)
655    })
656}
657
658pub fn measure_text_with_options_for_node(
659    node_id: Option<NodeId>,
660    text: &crate::text::AnnotatedString,
661    style: &TextStyle,
662    options: TextLayoutOptions,
663    max_width: Option<f32>,
664) -> TextMetrics {
665    crate::render_state::with_text_service(|service| {
666        service.measure_with_options(node_id, text, style, options.normalized(), max_width)
667    })
668}
669
670pub fn prepare_text_layout(
671    text: &crate::text::AnnotatedString,
672    style: &TextStyle,
673    options: TextLayoutOptions,
674    max_width: Option<f32>,
675) -> PreparedTextLayout {
676    crate::render_state::with_text_service(|service| {
677        service.prepare_with_options(None, text, style, options.normalized(), max_width)
678    })
679}
680
681pub fn prepare_text_layout_for_node(
682    node_id: Option<NodeId>,
683    text: &crate::text::AnnotatedString,
684    style: &TextStyle,
685    options: TextLayoutOptions,
686    max_width: Option<f32>,
687) -> PreparedTextLayout {
688    crate::render_state::with_text_service(|service| {
689        service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
690    })
691}
692
693pub fn get_offset_for_position(
694    text: &crate::text::AnnotatedString,
695    style: &TextStyle,
696    x: f32,
697    y: f32,
698) -> usize {
699    crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
700}
701
702pub fn get_cursor_x_for_offset(
703    text: &crate::text::AnnotatedString,
704    style: &TextStyle,
705    offset: usize,
706) -> f32 {
707    crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
708}
709
710pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
711    crate::render_state::with_text_service(|service| service.layout(text, style))
712}
713
714fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
715    measurer: &M,
716    text: &crate::text::AnnotatedString,
717    style: &TextStyle,
718    options: TextLayoutOptions,
719    max_width: Option<f32>,
720) -> PreparedTextLayout {
721    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
722}
723
724pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
725    measurer: &M,
726    node_id: Option<NodeId>,
727    text: &crate::text::AnnotatedString,
728    style: &TextStyle,
729    options: TextLayoutOptions,
730    max_width: Option<f32>,
731) -> PreparedTextLayout {
732    let telemetry = text_layout_telemetry_enabled();
733    let total_start = telemetry.then(Instant::now);
734    let opts = options.normalized();
735    let max_width = normalize_max_width(max_width);
736    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
737        return prepare_scale_down_text_layout(
738            measurer,
739            node_id,
740            text,
741            style,
742            opts,
743            max_width,
744            min_font_size_sp,
745        );
746    }
747
748    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
749        .then_some(max_width)
750        .flatten();
751    let line_break_mode = style
752        .paragraph_style
753        .line_break
754        .take_or_else(|| LineBreak::Simple);
755    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
756
757    let wrap_start = telemetry.then(Instant::now);
758    let line_ranges = split_line_ranges(text.text.as_str());
759    let source_line_count = line_ranges.len();
760    let mut visible_lines: Vec<DisplayLine>;
761    if let Some(width_limit) = wrap_width {
762        visible_lines = Vec::with_capacity(line_ranges.len());
763        for line_range in line_ranges {
764            let wrapped_lines = wrap_line_to_width(
765                measurer,
766                text,
767                line_range,
768                style,
769                width_limit,
770                line_break_mode,
771                hyphens_mode,
772            );
773            visible_lines.extend(wrapped_lines);
774        }
775    } else {
776        visible_lines = line_ranges
777            .into_iter()
778            .map(DisplayLine::from_source_range)
779            .collect();
780    }
781    let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
782
783    let overflow_start = telemetry.then(Instant::now);
784    let mut did_overflow = false;
785    if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
786        did_overflow = true;
787        visible_lines.truncate(opts.max_lines);
788        if let Some(last_line) = visible_lines.last_mut() {
789            let overflowed = apply_line_overflow(
790                measurer,
791                last_line.display_text(text),
792                style,
793                max_width,
794                opts,
795                true,
796                true,
797            );
798            last_line.apply_display_text(text, overflowed);
799        }
800    }
801
802    if let Some(width_limit) = max_width {
803        let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
804        let visible_len = visible_lines.len();
805        for (line_index, line) in visible_lines.iter_mut().enumerate() {
806            let width = line.measure_width(measurer, node_id, text, style);
807            if width > width_limit + WRAP_EPSILON {
808                if opts.overflow == TextOverflow::Visible {
809                    continue;
810                }
811                did_overflow = true;
812                let overflowed = apply_line_overflow(
813                    measurer,
814                    line.display_text(text),
815                    style,
816                    Some(width_limit),
817                    opts,
818                    line_index + 1 == visible_len,
819                    single_line_ellipsis,
820                );
821                line.apply_display_text(text, overflowed);
822            }
823        }
824    }
825    let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
826
827    let build_start = telemetry.then(Instant::now);
828    let display_annotated = build_display_annotated(text, &visible_lines);
829    debug_assert_eq!(
830        display_annotated.text,
831        join_display_line_text(text, &visible_lines)
832    );
833    let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
834
835    let metrics_start = telemetry.then(Instant::now);
836    let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
837    let display_line_count = visible_lines.len().max(1);
838    let layout_line_count = display_line_count.max(opts.min_lines);
839
840    let measured_width = if visible_lines.is_empty() {
841        0.0
842    } else {
843        visible_lines
844            .iter()
845            .map(|line| line.measure_width(measurer, node_id, text, style))
846            .fold(0.0_f32, f32::max)
847    };
848    let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
849    let width = if opts.overflow == TextOverflow::Visible {
850        measured_width
851    } else if let Some(width_limit) = max_width {
852        measured_width.min(width_limit)
853    } else {
854        measured_width
855    };
856
857    let prepared = PreparedTextLayout {
858        text: display_annotated,
859        visual_style: style.clone(),
860        metrics: TextMetrics {
861            width,
862            height: layout_line_count as f32 * line_height,
863            line_height,
864            line_count: layout_line_count,
865        },
866        did_overflow,
867    };
868
869    if let Some(start) = total_start {
870        eprintln!(
871            "[text-layout-telemetry] bytes={} spans={} source_lines={} display_lines={} wrap={} max_width={:?} wrap_ms={:.2} overflow_ms={:.2} build_ms={:.2} metrics_ms={:.2} total_ms={:.2}",
872            text.text.len(),
873            text.span_styles.len(),
874            source_line_count,
875            display_line_count,
876            wrap_width.is_some(),
877            max_width,
878            wrap_ms.unwrap_or(0.0),
879            overflow_ms.unwrap_or(0.0),
880            build_ms.unwrap_or(0.0),
881            metrics_ms.unwrap_or(0.0),
882            start.elapsed().as_secs_f64() * 1000.0,
883        );
884    }
885
886    prepared
887}
888
889fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
890    measurer: &M,
891    node_id: Option<NodeId>,
892    text: &crate::text::AnnotatedString,
893    style: &TextStyle,
894    options: TextLayoutOptions,
895    max_width: Option<f32>,
896    min_font_size_sp: f32,
897) -> PreparedTextLayout {
898    let clipped_options = TextLayoutOptions {
899        overflow: TextOverflow::Clip,
900        ..options
901    }
902    .normalized();
903
904    let full_size = prepare_scaled_text_layout(
905        measurer,
906        node_id,
907        text,
908        style,
909        clipped_options,
910        max_width,
911        1.0,
912    );
913    let Some(width_limit) = max_width else {
914        return full_size;
915    };
916    if !full_size.did_overflow {
917        return full_size;
918    }
919
920    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
921    if !base_font_size.is_finite() || base_font_size <= 0.0 {
922        return full_size;
923    }
924    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
925    if min_scale >= 1.0 {
926        return full_size;
927    }
928
929    let min_size = prepare_scaled_text_layout(
930        measurer,
931        node_id,
932        text,
933        style,
934        clipped_options,
935        Some(width_limit),
936        min_scale,
937    );
938    if min_size.did_overflow {
939        return min_size;
940    }
941
942    let mut low = min_scale;
943    let mut high = 1.0;
944    let mut best = min_size;
945    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
946        let mid = (low + high) * 0.5;
947        let candidate = prepare_scaled_text_layout(
948            measurer,
949            node_id,
950            text,
951            style,
952            clipped_options,
953            Some(width_limit),
954            mid,
955        );
956        if candidate.did_overflow {
957            high = mid;
958        } else {
959            low = mid;
960            best = candidate;
961        }
962    }
963
964    best
965}
966
967fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
968    measurer: &M,
969    node_id: Option<NodeId>,
970    text: &crate::text::AnnotatedString,
971    style: &TextStyle,
972    options: TextLayoutOptions,
973    max_width: Option<f32>,
974    font_scale: f32,
975) -> PreparedTextLayout {
976    let visual_style = scale_text_style_font_sizes(style, font_scale);
977    let visual_text = scale_annotated_font_sizes(text, font_scale);
978    prepare_text_layout_with_measurer_for_node(
979        measurer,
980        node_id,
981        visual_text.as_ref(),
982        &visual_style,
983        options,
984        max_width,
985    )
986}
987
988fn scale_annotated_font_sizes(
989    text: &crate::text::AnnotatedString,
990    factor: f32,
991) -> Cow<'_, crate::text::AnnotatedString> {
992    if is_identity_scale(factor) || !annotated_text_needs_scaling(text) {
993        return Cow::Borrowed(text);
994    }
995
996    let mut scaled = text.clone();
997    for span in &mut scaled.span_styles {
998        span.item = scale_span_style_font_sizes(&span.item, factor, None);
999    }
1000    Cow::Owned(scaled)
1001}
1002
1003fn scale_text_style_font_sizes(style: &TextStyle, factor: f32) -> TextStyle {
1004    if is_identity_scale(factor) {
1005        return style.clone();
1006    }
1007
1008    let mut scaled = style.clone();
1009    scaled.span_style =
1010        scale_span_style_font_sizes(&style.span_style, factor, Some(DEFAULT_FONT_SIZE_SP));
1011    scaled.paragraph_style.line_height =
1012        scale_text_unit_sp(scaled.paragraph_style.line_height, factor);
1013    if let Some(mut indent) = scaled.paragraph_style.text_indent {
1014        indent.first_line = scale_text_unit_sp(indent.first_line, factor);
1015        indent.rest_line = scale_text_unit_sp(indent.rest_line, factor);
1016        scaled.paragraph_style.text_indent = Some(indent);
1017    }
1018    scaled
1019}
1020
1021fn scale_span_style_font_sizes(
1022    style: &crate::text::SpanStyle,
1023    factor: f32,
1024    default_font_size_sp: Option<f32>,
1025) -> crate::text::SpanStyle {
1026    let mut scaled = style.clone();
1027    scaled.font_size = match (style.font_size, default_font_size_sp) {
1028        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1029            crate::text::TextUnit::Sp(default_size * factor)
1030        }
1031        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, factor),
1032        (unit, None) => scale_text_unit_sp(unit, factor),
1033    };
1034    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, factor);
1035    if let Some(mut shadow) = scaled.shadow {
1036        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1037        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1038        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1039        scaled.shadow = Some(shadow);
1040    }
1041    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1042        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1043            width: width * factor,
1044        });
1045    }
1046    scaled
1047}
1048
1049fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1050    text.span_styles
1051        .iter()
1052        .any(|span| span_style_needs_scaling(&span.item))
1053}
1054
1055fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1056    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1057        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1058        || matches!(
1059            style.draw_style,
1060            Some(crate::text::TextDrawStyle::Stroke { .. })
1061        )
1062        || style.shadow.is_some()
1063}
1064
1065fn scale_text_unit_sp(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
1066    match unit {
1067        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1068            crate::text::TextUnit::Sp(value * factor)
1069        }
1070        other => other,
1071    }
1072}
1073
1074fn scale_text_unit_sp_and_em(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
1075    match unit {
1076        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1077            crate::text::TextUnit::Sp(value * factor)
1078        }
1079        crate::text::TextUnit::Em(value) if value.is_finite() => {
1080            crate::text::TextUnit::Em(value * factor)
1081        }
1082        other => other,
1083    }
1084}
1085
1086fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1087    if value.is_finite() {
1088        value * factor
1089    } else {
1090        value
1091    }
1092}
1093
1094fn is_identity_scale(factor: f32) -> bool {
1095    (factor - 1.0).abs() <= f32::EPSILON
1096}
1097
1098#[derive(Clone, Debug)]
1099enum DisplayLineText {
1100    Source,
1101    Remapped(crate::text::AnnotatedString),
1102}
1103
1104#[derive(Clone, Debug)]
1105struct DisplayLine {
1106    source_range: Range<usize>,
1107    text: DisplayLineText,
1108    measured_width: Option<f32>,
1109}
1110
1111impl DisplayLine {
1112    fn from_source_range(source_range: Range<usize>) -> Self {
1113        Self {
1114            source_range,
1115            text: DisplayLineText::Source,
1116            measured_width: None,
1117        }
1118    }
1119
1120    fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1121        Self {
1122            source_range,
1123            text: DisplayLineText::Source,
1124            measured_width: measured_width
1125                .is_finite()
1126                .then_some(measured_width.max(0.0)),
1127        }
1128    }
1129
1130    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1131        match &self.text {
1132            DisplayLineText::Source => &source.text[self.source_range.clone()],
1133            DisplayLineText::Remapped(annotated) => annotated.text.as_str(),
1134        }
1135    }
1136
1137    fn measure_width<M: TextMeasurer + ?Sized>(
1138        &self,
1139        measurer: &M,
1140        node_id: Option<NodeId>,
1141        source: &crate::text::AnnotatedString,
1142        style: &TextStyle,
1143    ) -> f32 {
1144        match &self.text {
1145            DisplayLineText::Source => self.measured_width.unwrap_or_else(|| {
1146                measurer
1147                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1148                    .width
1149            }),
1150            DisplayLineText::Remapped(annotated) => {
1151                measurer.measure_for_node(node_id, annotated, style).width
1152            }
1153        }
1154    }
1155
1156    fn apply_display_text(&mut self, source: &crate::text::AnnotatedString, display_text: String) {
1157        let source_text = &source.text[self.source_range.clone()];
1158        self.measured_width = None;
1159        self.text = if source_text == display_text {
1160            DisplayLineText::Source
1161        } else {
1162            DisplayLineText::Remapped(remap_annotated_subsequence_for_display(
1163                source,
1164                self.source_range.clone(),
1165                display_text.as_str(),
1166            ))
1167        };
1168    }
1169}
1170
1171fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1172    if text.is_empty() {
1173        return single_line_range(0..0);
1174    }
1175
1176    let mut ranges = Vec::new();
1177    let mut start = 0usize;
1178    for (idx, ch) in text.char_indices() {
1179        if ch == '\n' {
1180            ranges.push(start..idx);
1181            start = idx + ch.len_utf8();
1182        }
1183    }
1184    ranges.push(start..text.len());
1185    ranges
1186}
1187
1188fn build_display_annotated(
1189    source: &crate::text::AnnotatedString,
1190    lines: &[DisplayLine],
1191) -> crate::text::AnnotatedString {
1192    if lines.is_empty() {
1193        return crate::text::AnnotatedString::from("");
1194    }
1195
1196    let mut builder = crate::text::AnnotatedString::builder();
1197    for (idx, line) in lines.iter().enumerate() {
1198        builder = match &line.text {
1199            DisplayLineText::Source => {
1200                builder.append_annotated_subsequence(source, line.source_range.clone())
1201            }
1202            DisplayLineText::Remapped(annotated) => builder.append_annotated(annotated),
1203        };
1204        if idx + 1 < lines.len() {
1205            builder = builder.append("\n");
1206        }
1207    }
1208    builder.to_annotated_string()
1209}
1210
1211fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1212    let mut text = String::new();
1213    for (idx, line) in lines.iter().enumerate() {
1214        text.push_str(line.display_text(source));
1215        if idx + 1 < lines.len() {
1216            text.push('\n');
1217        }
1218    }
1219    text
1220}
1221
1222fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1223    while end > start {
1224        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1225            break;
1226        };
1227        if ch.is_whitespace() {
1228            end = start + idx;
1229        } else {
1230            break;
1231        }
1232    }
1233    end
1234}
1235
1236fn remap_annotated_subsequence_for_display(
1237    source: &crate::text::AnnotatedString,
1238    source_range: Range<usize>,
1239    display_text: &str,
1240) -> crate::text::AnnotatedString {
1241    let source_text = &source.text[source_range.clone()];
1242    if source_text == display_text {
1243        return source.subsequence(source_range);
1244    }
1245
1246    let display_chars = map_display_chars_to_source(source_text, display_text);
1247    crate::text::AnnotatedString {
1248        text: display_text.to_string(),
1249        span_styles: remap_subsequence_range_styles(
1250            &source.span_styles,
1251            source_range.clone(),
1252            &display_chars,
1253        ),
1254        paragraph_styles: remap_subsequence_range_styles(
1255            &source.paragraph_styles,
1256            source_range.clone(),
1257            &display_chars,
1258        ),
1259        string_annotations: remap_subsequence_range_styles(
1260            &source.string_annotations,
1261            source_range.clone(),
1262            &display_chars,
1263        ),
1264        link_annotations: remap_subsequence_range_styles(
1265            &source.link_annotations,
1266            source_range,
1267            &display_chars,
1268        ),
1269    }
1270}
1271
1272#[derive(Clone, Copy)]
1273struct DisplayCharMap {
1274    display_start: usize,
1275    display_end: usize,
1276    source_start: Option<usize>,
1277}
1278
1279fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
1280    let source_chars: Vec<(usize, char)> = source.char_indices().collect();
1281    let mut source_index = 0usize;
1282    let mut maps = Vec::with_capacity(display.chars().count());
1283
1284    for (display_start, display_char) in display.char_indices() {
1285        let display_end = display_start + display_char.len_utf8();
1286        let mut source_start = None;
1287        while source_index < source_chars.len() {
1288            let (candidate_start, candidate_char) = source_chars[source_index];
1289            source_index += 1;
1290            if candidate_char == display_char {
1291                source_start = Some(candidate_start);
1292                break;
1293            }
1294        }
1295        maps.push(DisplayCharMap {
1296            display_start,
1297            display_end,
1298            source_start,
1299        });
1300    }
1301
1302    maps
1303}
1304
1305fn remap_subsequence_range_styles<T: Clone>(
1306    styles: &[crate::text::RangeStyle<T>],
1307    source_range: Range<usize>,
1308    display_chars: &[DisplayCharMap],
1309) -> Vec<crate::text::RangeStyle<T>> {
1310    let mut remapped = Vec::new();
1311
1312    for style in styles {
1313        let overlap_start = style.range.start.max(source_range.start);
1314        let overlap_end = style.range.end.min(source_range.end);
1315        if overlap_start >= overlap_end {
1316            continue;
1317        }
1318        let local_source_range =
1319            (overlap_start - source_range.start)..(overlap_end - source_range.start);
1320        let mut range_start = None;
1321        let mut range_end = 0usize;
1322
1323        for map in display_chars {
1324            let in_range = map.source_start.is_some_and(|source_start| {
1325                source_start >= local_source_range.start && source_start < local_source_range.end
1326            });
1327
1328            if in_range {
1329                if range_start.is_none() {
1330                    range_start = Some(map.display_start);
1331                }
1332                range_end = map.display_end;
1333                continue;
1334            }
1335
1336            if let Some(start) = range_start.take() {
1337                if start < range_end {
1338                    remapped.push(crate::text::RangeStyle {
1339                        item: style.item.clone(),
1340                        range: start..range_end,
1341                    });
1342                }
1343            }
1344        }
1345
1346        if let Some(start) = range_start.take() {
1347            if start < range_end {
1348                remapped.push(crate::text::RangeStyle {
1349                    item: style.item.clone(),
1350                    range: start..range_end,
1351                });
1352            }
1353        }
1354    }
1355
1356    remapped
1357}
1358
1359fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1360    match max_width {
1361        Some(width) if width.is_finite() && width > 0.0 => Some(width),
1362        _ => None,
1363    }
1364}
1365
1366fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1367    (base_start + relative.start)..(base_start + relative.end)
1368}
1369
1370fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1371    boundaries
1372        .binary_search(&byte_offset)
1373        .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1374}
1375
1376fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1377    std::iter::once(range).collect()
1378}
1379
1380struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1381    measurer: &'a M,
1382    text: &'a crate::text::AnnotatedString,
1383    style: &'a TextStyle,
1384    line_start: usize,
1385    prefix_widths: Option<TextLinePrefixWidths>,
1386}
1387
1388impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1389    fn new(
1390        measurer: &'a M,
1391        text: &'a crate::text::AnnotatedString,
1392        line_range: &Range<usize>,
1393        style: &'a TextStyle,
1394        boundary_count: usize,
1395    ) -> Self {
1396        let expected_chars = boundary_count.saturating_sub(1);
1397        let prefix_widths = measurer
1398            .measure_line_prefix_widths(text, line_range.clone(), style)
1399            .filter(|widths| widths.char_count() == expected_chars);
1400        Self {
1401            measurer,
1402            text,
1403            style,
1404            line_start: line_range.start,
1405            prefix_widths,
1406        }
1407    }
1408
1409    fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1410        if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1411            return width;
1412        }
1413        let segment_range =
1414            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1415        self.measurer
1416            .measure_subsequence(self.text, segment_range, self.style)
1417            .width
1418    }
1419
1420    fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1421        if let Some(prefix_widths) = &self.prefix_widths {
1422            if let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx) {
1423                return Some(width);
1424            }
1425        }
1426        None
1427    }
1428
1429    fn display_line_for_char_range(
1430        &self,
1431        boundaries: &[usize],
1432        start_idx: usize,
1433        end_idx: usize,
1434    ) -> DisplayLine {
1435        let source_range =
1436            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1437        let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1438        DisplayLine::from_measured_source_range(source_range, measured_width)
1439    }
1440}
1441
1442fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1443    measurer: &M,
1444    text: &crate::text::AnnotatedString,
1445    line_range: Range<usize>,
1446    style: &TextStyle,
1447    max_width: f32,
1448    line_break: LineBreak,
1449    hyphens: Hyphens,
1450) -> Vec<DisplayLine> {
1451    let line_text = &text.text[line_range.clone()];
1452    if line_text.is_empty() {
1453        return vec![DisplayLine::from_source_range(
1454            line_range.start..line_range.start,
1455        )];
1456    }
1457
1458    if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style) {
1459        if measured_width <= max_width + WRAP_EPSILON {
1460            return vec![DisplayLine::from_measured_source_range(
1461                line_range,
1462                measured_width,
1463            )];
1464        }
1465    }
1466
1467    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1468        && line_text.chars().any(char::is_whitespace)
1469    {
1470        if let Some(balanced) = wrap_line_with_word_balance(
1471            measurer,
1472            text,
1473            line_range.clone(),
1474            style,
1475            max_width,
1476            line_break,
1477        ) {
1478            return balanced;
1479        }
1480    }
1481
1482    wrap_line_greedy(
1483        measurer, text, line_range, style, max_width, line_break, hyphens,
1484    )
1485}
1486
1487fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1488    measurer: &M,
1489    text: &crate::text::AnnotatedString,
1490    line_range: Range<usize>,
1491    style: &TextStyle,
1492    max_width: f32,
1493    line_break: LineBreak,
1494    hyphens: Hyphens,
1495) -> Vec<DisplayLine> {
1496    let line_text = &text.text[line_range.clone()];
1497    let boundaries = char_boundaries(line_text);
1498    let measure_context =
1499        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1500    if let Some(measured_width) =
1501        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1502    {
1503        if measured_width <= max_width + WRAP_EPSILON {
1504            return vec![DisplayLine::from_measured_source_range(
1505                line_range,
1506                measured_width,
1507            )];
1508        }
1509    }
1510    let mut wrapped = Vec::new();
1511    let mut start_idx = 0usize;
1512
1513    while start_idx < boundaries.len() - 1 {
1514        let mut low = start_idx + 1;
1515        let mut high = boundaries.len() - 1;
1516        let mut best = start_idx + 1;
1517
1518        while low <= high {
1519            let mid = (low + high) / 2;
1520            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1521            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1522                best = mid;
1523                low = mid + 1;
1524            } else {
1525                if mid == 0 {
1526                    break;
1527                }
1528                high = mid - 1;
1529            }
1530        }
1531
1532        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1533        let mut effective_wrap_idx = wrap_idx;
1534        let can_hyphenate = hyphens == Hyphens::Auto
1535            && wrap_idx == best
1536            && best < boundaries.len() - 1
1537            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1538        if can_hyphenate {
1539            effective_wrap_idx = resolve_auto_hyphen_break(
1540                measurer,
1541                line_text,
1542                style,
1543                &boundaries,
1544                start_idx,
1545                wrap_idx,
1546            );
1547        }
1548
1549        let segment_start = boundaries[start_idx];
1550        let mut segment_end = boundaries[effective_wrap_idx];
1551        if wrap_idx != best {
1552            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1553        }
1554        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1555        wrapped.push(measure_context.display_line_for_char_range(
1556            &boundaries,
1557            start_idx,
1558            segment_end_idx,
1559        ));
1560
1561        start_idx = if wrap_idx != best {
1562            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1563        } else {
1564            effective_wrap_idx
1565        };
1566    }
1567
1568    if wrapped.is_empty() {
1569        wrapped.push(DisplayLine::from_source_range(
1570            line_range.start..line_range.start,
1571        ));
1572    }
1573
1574    wrapped
1575}
1576
1577fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1578    measurer: &M,
1579    text: &crate::text::AnnotatedString,
1580    line_range: Range<usize>,
1581    style: &TextStyle,
1582    max_width: f32,
1583    line_break: LineBreak,
1584) -> Option<Vec<DisplayLine>> {
1585    let line_text = &text.text[line_range.clone()];
1586    let boundaries = char_boundaries(line_text);
1587    let measure_context =
1588        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1589    if let Some(measured_width) =
1590        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1591    {
1592        if measured_width <= max_width + WRAP_EPSILON {
1593            return Some(vec![DisplayLine::from_measured_source_range(
1594                line_range,
1595                measured_width,
1596            )]);
1597        }
1598    }
1599    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1600    if breakpoints.len() <= 2 {
1601        return None;
1602    }
1603
1604    let node_count = breakpoints.len();
1605    let mut best_cost = vec![f32::INFINITY; node_count];
1606    let mut next_index = vec![None; node_count];
1607    best_cost[node_count - 1] = 0.0;
1608
1609    for start in (0..node_count - 1).rev() {
1610        for end in start + 1..node_count {
1611            let start_byte = boundaries[breakpoints[start]];
1612            let end_byte = boundaries[breakpoints[end]];
1613            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1614            if trimmed_end <= start_byte {
1615                continue;
1616            }
1617            let segment_start_idx = breakpoints[start];
1618            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1619            let segment_width =
1620                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1621            if segment_width > max_width + WRAP_EPSILON {
1622                continue;
1623            }
1624            if !best_cost[end].is_finite() {
1625                continue;
1626            }
1627            let slack = (max_width - segment_width).max(0.0);
1628            let is_last = end == node_count - 1;
1629            let segment_cost = match line_break {
1630                LineBreak::Heading => slack * slack,
1631                LineBreak::Paragraph => {
1632                    if is_last {
1633                        slack * slack * 0.16
1634                    } else {
1635                        slack * slack
1636                    }
1637                }
1638                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1639            };
1640            let candidate = segment_cost + best_cost[end];
1641            if candidate < best_cost[start] {
1642                best_cost[start] = candidate;
1643                next_index[start] = Some(end);
1644            }
1645        }
1646    }
1647
1648    let mut wrapped = Vec::new();
1649    let mut current = 0usize;
1650    while current < node_count - 1 {
1651        let next = next_index[current]?;
1652        let start_byte = boundaries[breakpoints[current]];
1653        let end_byte = boundaries[breakpoints[next]];
1654        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1655        if trimmed_end <= start_byte {
1656            return None;
1657        }
1658        let segment_start_idx = breakpoints[current];
1659        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1660        wrapped.push(measure_context.display_line_for_char_range(
1661            &boundaries,
1662            segment_start_idx,
1663            segment_end_idx,
1664        ));
1665        current = next;
1666    }
1667
1668    if wrapped.is_empty() {
1669        return None;
1670    }
1671
1672    Some(wrapped)
1673}
1674
1675fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1676    let mut points = vec![0usize];
1677    for idx in 1..boundaries.len() - 1 {
1678        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1679        let current = &line[boundaries[idx]..boundaries[idx + 1]];
1680        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1681            points.push(idx);
1682        }
1683    }
1684    let end = boundaries.len() - 1;
1685    if points.last().copied() != Some(end) {
1686        points.push(end);
1687    }
1688    points
1689}
1690
1691fn choose_wrap_break(
1692    line: &str,
1693    boundaries: &[usize],
1694    start_idx: usize,
1695    best: usize,
1696    _line_break: LineBreak,
1697) -> usize {
1698    if best >= boundaries.len() - 1 {
1699        return best;
1700    }
1701
1702    if best <= start_idx + 1 {
1703        return best;
1704    }
1705
1706    for idx in (start_idx + 1..best).rev() {
1707        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1708        if prev.chars().all(char::is_whitespace) {
1709            return idx;
1710        }
1711    }
1712    best
1713}
1714
1715fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1716    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1717        return false;
1718    }
1719    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1720    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1721    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1722}
1723
1724fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1725    measurer: &M,
1726    line: &str,
1727    style: &TextStyle,
1728    boundaries: &[usize],
1729    start_idx: usize,
1730    break_idx: usize,
1731) -> usize {
1732    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
1733        if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
1734            return candidate;
1735        }
1736    }
1737    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1738}
1739
1740fn is_valid_auto_hyphen_break(
1741    line: &str,
1742    boundaries: &[usize],
1743    start_idx: usize,
1744    break_idx: usize,
1745    candidate_idx: usize,
1746) -> bool {
1747    let end_idx = boundaries.len().saturating_sub(1);
1748    candidate_idx > start_idx
1749        && candidate_idx < end_idx
1750        && candidate_idx <= break_idx
1751        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1752        && is_break_inside_word(line, boundaries, candidate_idx)
1753}
1754
1755fn choose_auto_hyphen_break_fallback(
1756    boundaries: &[usize],
1757    start_idx: usize,
1758    break_idx: usize,
1759) -> usize {
1760    let end_idx = boundaries.len().saturating_sub(1);
1761    if break_idx >= end_idx {
1762        return break_idx;
1763    }
1764    let trailing_len = end_idx.saturating_sub(break_idx);
1765    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1766        return break_idx;
1767    }
1768
1769    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1770    let max_break = break_idx.saturating_sub(1);
1771    if min_break > max_break {
1772        return break_idx;
1773    }
1774
1775    let mut best_break = break_idx;
1776    let mut best_penalty = usize::MAX;
1777    for idx in min_break..=max_break {
1778        let candidate_trailing_len = end_idx.saturating_sub(idx);
1779        let candidate_prefix_len = idx.saturating_sub(start_idx);
1780        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1781            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1782        {
1783            continue;
1784        }
1785
1786        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1787        if penalty < best_penalty {
1788            best_penalty = penalty;
1789            best_break = idx;
1790            if penalty == 0 {
1791                break;
1792            }
1793        }
1794    }
1795    best_break
1796}
1797
1798fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1799    while idx < boundaries.len() - 1 {
1800        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1801        if !ch.chars().all(char::is_whitespace) {
1802            break;
1803        }
1804        idx += 1;
1805    }
1806    idx
1807}
1808
1809fn apply_line_overflow<M: TextMeasurer + ?Sized>(
1810    measurer: &M,
1811    line: &str,
1812    style: &TextStyle,
1813    max_width: Option<f32>,
1814    options: TextLayoutOptions,
1815    is_last_visible_line: bool,
1816    single_line_ellipsis: bool,
1817) -> String {
1818    if options.overflow == TextOverflow::Clip || !is_last_visible_line {
1819        return line.to_string();
1820    }
1821
1822    let Some(width_limit) = max_width else {
1823        return match options.overflow {
1824            TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
1825            TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
1826            TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
1827            TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
1828                line.to_string()
1829            }
1830        };
1831    };
1832
1833    match options.overflow {
1834        TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
1835        TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
1836        TextOverflow::StartEllipsis => {
1837            if single_line_ellipsis {
1838                fit_start_ellipsis(measurer, line, style, width_limit)
1839            } else {
1840                line.to_string()
1841            }
1842        }
1843        TextOverflow::MiddleEllipsis => {
1844            if single_line_ellipsis {
1845                fit_middle_ellipsis(measurer, line, style, width_limit)
1846            } else {
1847                line.to_string()
1848            }
1849        }
1850        TextOverflow::ScaleDown { .. } => line.to_string(),
1851    }
1852}
1853
1854fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
1855    measurer: &M,
1856    line: &str,
1857    style: &TextStyle,
1858    max_width: f32,
1859) -> String {
1860    if measurer
1861        .measure(&crate::text::AnnotatedString::from(line), style)
1862        .width
1863        <= max_width + WRAP_EPSILON
1864    {
1865        return line.to_string();
1866    }
1867
1868    let ellipsis_width = measurer
1869        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1870        .width;
1871    if ellipsis_width > max_width + WRAP_EPSILON {
1872        return String::new();
1873    }
1874
1875    let boundaries = char_boundaries(line);
1876    let mut low = 0usize;
1877    let mut high = boundaries.len() - 1;
1878    let mut best = 0usize;
1879
1880    while low <= high {
1881        let mid = (low + high) / 2;
1882        let prefix = &line[..boundaries[mid]];
1883        let candidate = format!("{prefix}{ELLIPSIS}");
1884        let width = measurer
1885            .measure(
1886                &crate::text::AnnotatedString::from(candidate.as_str()),
1887                style,
1888            )
1889            .width;
1890        if width <= max_width + WRAP_EPSILON {
1891            best = mid;
1892            low = mid + 1;
1893        } else if mid == 0 {
1894            break;
1895        } else {
1896            high = mid - 1;
1897        }
1898    }
1899
1900    format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
1901}
1902
1903fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
1904    measurer: &M,
1905    line: &str,
1906    style: &TextStyle,
1907    max_width: f32,
1908) -> String {
1909    if measurer
1910        .measure(&crate::text::AnnotatedString::from(line), style)
1911        .width
1912        <= max_width + WRAP_EPSILON
1913    {
1914        return line.to_string();
1915    }
1916
1917    let ellipsis_width = measurer
1918        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1919        .width;
1920    if ellipsis_width > max_width + WRAP_EPSILON {
1921        return String::new();
1922    }
1923
1924    let boundaries = char_boundaries(line);
1925    let mut low = 0usize;
1926    let mut high = boundaries.len() - 1;
1927    let mut best = boundaries.len() - 1;
1928
1929    while low <= high {
1930        let mid = (low + high) / 2;
1931        let suffix = &line[boundaries[mid]..];
1932        let candidate = format!("{ELLIPSIS}{suffix}");
1933        let width = measurer
1934            .measure(
1935                &crate::text::AnnotatedString::from(candidate.as_str()),
1936                style,
1937            )
1938            .width;
1939        if width <= max_width + WRAP_EPSILON {
1940            best = mid;
1941            if mid == 0 {
1942                break;
1943            }
1944            high = mid - 1;
1945        } else {
1946            low = mid + 1;
1947        }
1948    }
1949
1950    format!("{ELLIPSIS}{}", &line[boundaries[best]..])
1951}
1952
1953fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
1954    measurer: &M,
1955    line: &str,
1956    style: &TextStyle,
1957    max_width: f32,
1958) -> String {
1959    if measurer
1960        .measure(&crate::text::AnnotatedString::from(line), style)
1961        .width
1962        <= max_width + WRAP_EPSILON
1963    {
1964        return line.to_string();
1965    }
1966
1967    let ellipsis_width = measurer
1968        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1969        .width;
1970    if ellipsis_width > max_width + WRAP_EPSILON {
1971        return String::new();
1972    }
1973
1974    let boundaries = char_boundaries(line);
1975    let total_chars = boundaries.len().saturating_sub(1);
1976    for keep in (0..=total_chars).rev() {
1977        let keep_start = keep.div_ceil(2);
1978        let keep_end = keep / 2;
1979        let start = &line[..boundaries[keep_start]];
1980        let end_start = boundaries[total_chars.saturating_sub(keep_end)];
1981        let end = &line[end_start..];
1982        let candidate = format!("{start}{ELLIPSIS}{end}");
1983        if measurer
1984            .measure(
1985                &crate::text::AnnotatedString::from(candidate.as_str()),
1986                style,
1987            )
1988            .width
1989            <= max_width + WRAP_EPSILON
1990        {
1991            return candidate;
1992        }
1993    }
1994
1995    ELLIPSIS.to_string()
1996}
1997
1998fn char_boundaries(text: &str) -> Vec<usize> {
1999    let mut out = Vec::with_capacity(text.chars().count() + 1);
2000    out.push(0);
2001    for (idx, _) in text.char_indices() {
2002        if idx != 0 {
2003            out.push(idx);
2004        }
2005    }
2006    out.push(text.len());
2007    out
2008}
2009
2010#[cfg(test)]
2011mod tests {
2012    use super::*;
2013    use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
2014    use crate::text_layout_result::TextLayoutResult;
2015    use std::cell::Cell;
2016
2017    #[test]
2018    fn text_layout_telemetry_env_flag_is_not_process_cached() {
2019        let source = include_str!("measure.rs");
2020        let once_lock = ["Once", "Lock"].concat();
2021        let cached_init_call = ["get", "_or", "_init"].concat();
2022
2023        assert!(
2024            !source.contains(&once_lock) && !source.contains(&cached_init_call),
2025            "text layout telemetry env flag must be read at the diagnostic boundary"
2026        );
2027    }
2028
2029    #[test]
2030    fn text_service_cache_retains_large_lazy_text_working_set() {
2031        let mut cache = BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY);
2032        let metrics = TextMetrics {
2033            width: 1.0,
2034            height: 1.0,
2035            line_height: 1.0,
2036            line_count: 1,
2037        };
2038
2039        for index in 0..4096u64 {
2040            cache.insert(
2041                TextBaseCacheKey {
2042                    text_hash: index,
2043                    style_hash: 7,
2044                },
2045                metrics,
2046            );
2047        }
2048
2049        for index in 0..4096u64 {
2050            assert!(
2051                cache
2052                    .get(&TextBaseCacheKey {
2053                        text_hash: index,
2054                        style_hash: 7,
2055                    })
2056                    .is_some(),
2057                "large lazy text working-set entry {index} was evicted too early"
2058            );
2059        }
2060    }
2061
2062    struct ContractBreakMeasurer {
2063        retreat: usize,
2064    }
2065
2066    impl TextMeasurer for ContractBreakMeasurer {
2067        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2068            MonospacedTextMeasurer.measure(
2069                &crate::text::AnnotatedString::from(text.text.as_str()),
2070                style,
2071            )
2072        }
2073
2074        fn get_offset_for_position(
2075            &self,
2076            text: &crate::text::AnnotatedString,
2077            style: &TextStyle,
2078            x: f32,
2079            y: f32,
2080        ) -> usize {
2081            MonospacedTextMeasurer.get_offset_for_position(
2082                &crate::text::AnnotatedString::from(text.text.as_str()),
2083                style,
2084                x,
2085                y,
2086            )
2087        }
2088
2089        fn get_cursor_x_for_offset(
2090            &self,
2091            text: &crate::text::AnnotatedString,
2092            style: &TextStyle,
2093            offset: usize,
2094        ) -> f32 {
2095            MonospacedTextMeasurer.get_cursor_x_for_offset(
2096                &crate::text::AnnotatedString::from(text.text.as_str()),
2097                style,
2098                offset,
2099            )
2100        }
2101
2102        fn layout(
2103            &self,
2104            text: &crate::text::AnnotatedString,
2105            style: &TextStyle,
2106        ) -> TextLayoutResult {
2107            MonospacedTextMeasurer.layout(
2108                &crate::text::AnnotatedString::from(text.text.as_str()),
2109                style,
2110            )
2111        }
2112
2113        fn choose_auto_hyphen_break(
2114            &self,
2115            _line: &str,
2116            _style: &TextStyle,
2117            _segment_start_char: usize,
2118            measured_break_char: usize,
2119        ) -> Option<usize> {
2120            measured_break_char.checked_sub(self.retreat)
2121        }
2122    }
2123
2124    struct CountingTextMeasurer {
2125        measure_calls: Rc<Cell<usize>>,
2126        layout_calls: Rc<Cell<usize>>,
2127    }
2128
2129    impl CountingTextMeasurer {
2130        fn new(measure_calls: Rc<Cell<usize>>, layout_calls: Rc<Cell<usize>>) -> Self {
2131            Self {
2132                measure_calls,
2133                layout_calls,
2134            }
2135        }
2136    }
2137
2138    impl TextMeasurer for CountingTextMeasurer {
2139        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2140            self.measure_calls.set(self.measure_calls.get() + 1);
2141            MonospacedTextMeasurer.measure(text, style)
2142        }
2143
2144        fn get_offset_for_position(
2145            &self,
2146            text: &crate::text::AnnotatedString,
2147            style: &TextStyle,
2148            x: f32,
2149            y: f32,
2150        ) -> usize {
2151            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2152        }
2153
2154        fn get_cursor_x_for_offset(
2155            &self,
2156            text: &crate::text::AnnotatedString,
2157            style: &TextStyle,
2158            offset: usize,
2159        ) -> f32 {
2160            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2161        }
2162
2163        fn layout(
2164            &self,
2165            text: &crate::text::AnnotatedString,
2166            style: &TextStyle,
2167        ) -> TextLayoutResult {
2168            self.layout_calls.set(self.layout_calls.get() + 1);
2169            MonospacedTextMeasurer.layout(text, style)
2170        }
2171    }
2172
2173    struct CountingPreparedTextMeasurer {
2174        prepare_calls: Rc<Cell<usize>>,
2175    }
2176
2177    impl CountingPreparedTextMeasurer {
2178        fn new(prepare_calls: Rc<Cell<usize>>) -> Self {
2179            Self { prepare_calls }
2180        }
2181    }
2182
2183    struct PrefixWidthCountingMeasurer {
2184        prefix_calls: Rc<Cell<usize>>,
2185        subsequence_calls: Rc<Cell<usize>>,
2186    }
2187
2188    impl PrefixWidthCountingMeasurer {
2189        fn new(prefix_calls: Rc<Cell<usize>>, subsequence_calls: Rc<Cell<usize>>) -> Self {
2190            Self {
2191                prefix_calls,
2192                subsequence_calls,
2193            }
2194        }
2195    }
2196
2197    impl TextMeasurer for PrefixWidthCountingMeasurer {
2198        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2199            MonospacedTextMeasurer.measure(text, style)
2200        }
2201
2202        fn measure_subsequence(
2203            &self,
2204            text: &crate::text::AnnotatedString,
2205            range: Range<usize>,
2206            style: &TextStyle,
2207        ) -> TextMetrics {
2208            self.subsequence_calls.set(self.subsequence_calls.get() + 1);
2209            MonospacedTextMeasurer.measure_subsequence(text, range, style)
2210        }
2211
2212        fn measure_line_prefix_widths(
2213            &self,
2214            text: &crate::text::AnnotatedString,
2215            line_range: Range<usize>,
2216            style: &TextStyle,
2217        ) -> Option<TextLinePrefixWidths> {
2218            self.prefix_calls.set(self.prefix_calls.get() + 1);
2219            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2220        }
2221
2222        fn get_offset_for_position(
2223            &self,
2224            text: &crate::text::AnnotatedString,
2225            style: &TextStyle,
2226            x: f32,
2227            y: f32,
2228        ) -> usize {
2229            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2230        }
2231
2232        fn get_cursor_x_for_offset(
2233            &self,
2234            text: &crate::text::AnnotatedString,
2235            style: &TextStyle,
2236            offset: usize,
2237        ) -> f32 {
2238            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2239        }
2240
2241        fn layout(
2242            &self,
2243            text: &crate::text::AnnotatedString,
2244            style: &TextStyle,
2245        ) -> TextLayoutResult {
2246            MonospacedTextMeasurer.layout(text, style)
2247        }
2248    }
2249
2250    struct LineHeightCountingMeasurer {
2251        measure_calls: Rc<Cell<usize>>,
2252        line_height_calls: Rc<Cell<usize>>,
2253    }
2254
2255    struct FitProbeCountingMeasurer {
2256        line_width_calls: Rc<Cell<usize>>,
2257        prefix_calls: Rc<Cell<usize>>,
2258    }
2259
2260    impl FitProbeCountingMeasurer {
2261        fn new(line_width_calls: Rc<Cell<usize>>, prefix_calls: Rc<Cell<usize>>) -> Self {
2262            Self {
2263                line_width_calls,
2264                prefix_calls,
2265            }
2266        }
2267    }
2268
2269    impl LineHeightCountingMeasurer {
2270        fn new(measure_calls: Rc<Cell<usize>>, line_height_calls: Rc<Cell<usize>>) -> Self {
2271            Self {
2272                measure_calls,
2273                line_height_calls,
2274            }
2275        }
2276    }
2277
2278    impl TextMeasurer for LineHeightCountingMeasurer {
2279        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2280            self.measure_calls.set(self.measure_calls.get() + 1);
2281            MonospacedTextMeasurer.measure(text, style)
2282        }
2283
2284        fn measure_line_prefix_widths(
2285            &self,
2286            text: &crate::text::AnnotatedString,
2287            line_range: Range<usize>,
2288            style: &TextStyle,
2289        ) -> Option<TextLinePrefixWidths> {
2290            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2291        }
2292
2293        fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
2294            self.line_height_calls.set(self.line_height_calls.get() + 1);
2295            MonospacedTextMeasurer.line_height(text, style)
2296        }
2297
2298        fn get_offset_for_position(
2299            &self,
2300            text: &crate::text::AnnotatedString,
2301            style: &TextStyle,
2302            x: f32,
2303            y: f32,
2304        ) -> usize {
2305            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2306        }
2307
2308        fn get_cursor_x_for_offset(
2309            &self,
2310            text: &crate::text::AnnotatedString,
2311            style: &TextStyle,
2312            offset: usize,
2313        ) -> f32 {
2314            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2315        }
2316
2317        fn layout(
2318            &self,
2319            text: &crate::text::AnnotatedString,
2320            style: &TextStyle,
2321        ) -> TextLayoutResult {
2322            MonospacedTextMeasurer.layout(text, style)
2323        }
2324    }
2325
2326    impl TextMeasurer for FitProbeCountingMeasurer {
2327        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2328            MonospacedTextMeasurer.measure(text, style)
2329        }
2330
2331        fn measure_line_width(
2332            &self,
2333            text: &crate::text::AnnotatedString,
2334            line_range: Range<usize>,
2335            style: &TextStyle,
2336        ) -> Option<f32> {
2337            self.line_width_calls.set(self.line_width_calls.get() + 1);
2338            MonospacedTextMeasurer.measure_line_width(text, line_range, style)
2339        }
2340
2341        fn measure_line_prefix_widths(
2342            &self,
2343            text: &crate::text::AnnotatedString,
2344            line_range: Range<usize>,
2345            style: &TextStyle,
2346        ) -> Option<TextLinePrefixWidths> {
2347            self.prefix_calls.set(self.prefix_calls.get() + 1);
2348            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2349        }
2350
2351        fn get_offset_for_position(
2352            &self,
2353            text: &crate::text::AnnotatedString,
2354            style: &TextStyle,
2355            x: f32,
2356            y: f32,
2357        ) -> usize {
2358            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2359        }
2360
2361        fn get_cursor_x_for_offset(
2362            &self,
2363            text: &crate::text::AnnotatedString,
2364            style: &TextStyle,
2365            offset: usize,
2366        ) -> f32 {
2367            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2368        }
2369
2370        fn layout(
2371            &self,
2372            text: &crate::text::AnnotatedString,
2373            style: &TextStyle,
2374        ) -> TextLayoutResult {
2375            MonospacedTextMeasurer.layout(text, style)
2376        }
2377    }
2378
2379    impl TextMeasurer for CountingPreparedTextMeasurer {
2380        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2381            MonospacedTextMeasurer.measure(text, style)
2382        }
2383
2384        fn prepare_with_options_for_node(
2385            &self,
2386            _node_id: Option<NodeId>,
2387            text: &crate::text::AnnotatedString,
2388            style: &TextStyle,
2389            options: TextLayoutOptions,
2390            max_width: Option<f32>,
2391        ) -> PreparedTextLayout {
2392            self.prepare_calls.set(self.prepare_calls.get() + 1);
2393            MonospacedTextMeasurer.prepare_with_options(text, style, options, max_width)
2394        }
2395
2396        fn get_offset_for_position(
2397            &self,
2398            text: &crate::text::AnnotatedString,
2399            style: &TextStyle,
2400            x: f32,
2401            y: f32,
2402        ) -> usize {
2403            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2404        }
2405
2406        fn get_cursor_x_for_offset(
2407            &self,
2408            text: &crate::text::AnnotatedString,
2409            style: &TextStyle,
2410            offset: usize,
2411        ) -> f32 {
2412            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2413        }
2414
2415        fn layout(
2416            &self,
2417            text: &crate::text::AnnotatedString,
2418            style: &TextStyle,
2419        ) -> TextLayoutResult {
2420            MonospacedTextMeasurer.layout(text, style)
2421        }
2422    }
2423
2424    #[test]
2425    fn text_service_routes_measurement_through_current_measurer() {
2426        let _app_context = crate::render_state::app_context_test_scope();
2427        let service = TextService::from_measurer(Rc::new(MonospacedTextMeasurer));
2428        let text = crate::text::AnnotatedString::from("abc");
2429        let style = TextStyle::default();
2430
2431        let metrics = service.with_measurer(|measurer| measurer.measure(&text, &style));
2432
2433        assert!(metrics.width > 0.0);
2434        assert!(metrics.height > 0.0);
2435    }
2436
2437    #[test]
2438    fn text_service_caches_metrics_and_layouts_per_context() {
2439        let _app_context = crate::render_state::app_context_test_scope();
2440        let measure_calls = Rc::new(Cell::new(0));
2441        let layout_calls = Rc::new(Cell::new(0));
2442        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2443            Rc::clone(&measure_calls),
2444            Rc::clone(&layout_calls),
2445        )));
2446        let text = crate::text::AnnotatedString::from("cached text");
2447        let style = TextStyle::default();
2448
2449        let first_metrics = service.measure(Some(7), &text, &style);
2450        let second_metrics = service.measure(Some(7), &text, &style);
2451        let first_layout = service.layout(&text, &style);
2452        let second_layout = service.layout(&text, &style);
2453
2454        assert_eq!(first_metrics, second_metrics);
2455        assert_eq!(first_layout.width, second_layout.width);
2456        assert_eq!(measure_calls.get(), 1);
2457        assert_eq!(layout_calls.get(), 1);
2458    }
2459
2460    #[test]
2461    fn text_service_reuses_metrics_cache_across_node_ids() {
2462        let _app_context = crate::render_state::app_context_test_scope();
2463        let measure_calls = Rc::new(Cell::new(0));
2464        let layout_calls = Rc::new(Cell::new(0));
2465        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2466            Rc::clone(&measure_calls),
2467            Rc::clone(&layout_calls),
2468        )));
2469        let text = crate::text::AnnotatedString::from("same lazy item text");
2470        let style = TextStyle::default();
2471
2472        let first_metrics = service.measure(Some(7), &text, &style);
2473        let second_metrics = service.measure(Some(8), &text, &style);
2474
2475        assert_eq!(first_metrics, second_metrics);
2476        assert_eq!(measure_calls.get(), 1);
2477    }
2478
2479    #[test]
2480    fn text_service_reuses_prepared_layout_cache_across_node_ids() {
2481        let _app_context = crate::render_state::app_context_test_scope();
2482        let prepare_calls = Rc::new(Cell::new(0));
2483        let service = TextService::from_measurer(Rc::new(CountingPreparedTextMeasurer::new(
2484            Rc::clone(&prepare_calls),
2485        )));
2486        let text = crate::text::AnnotatedString::from("same prepared lazy item text");
2487        let style = TextStyle::default();
2488        let options = TextLayoutOptions::default();
2489
2490        let first = service.prepare_with_options(Some(9), &text, &style, options, Some(120.0));
2491        let second = service.prepare_with_options(Some(10), &text, &style, options, Some(120.0));
2492
2493        assert_eq!(first.metrics, second.metrics);
2494        assert_eq!(prepare_calls.get(), 1);
2495    }
2496
2497    #[test]
2498    fn text_service_clears_caches_when_measurer_changes() {
2499        let _app_context = crate::render_state::app_context_test_scope();
2500        let first_measure_calls = Rc::new(Cell::new(0));
2501        let second_measure_calls = Rc::new(Cell::new(0));
2502        let layout_calls = Rc::new(Cell::new(0));
2503        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2504            Rc::clone(&first_measure_calls),
2505            Rc::clone(&layout_calls),
2506        )));
2507        let text = crate::text::AnnotatedString::from("cached text");
2508        let style = TextStyle::default();
2509
2510        let _ = service.measure(None, &text, &style);
2511        let _ = service.measure(None, &text, &style);
2512        service.set_measurer(Rc::new(CountingTextMeasurer::new(
2513            Rc::clone(&second_measure_calls),
2514            Rc::clone(&layout_calls),
2515        )));
2516        let _ = service.measure(None, &text, &style);
2517
2518        assert_eq!(first_measure_calls.get(), 1);
2519        assert_eq!(second_measure_calls.get(), 1);
2520    }
2521
2522    #[test]
2523    fn text_wrapping_uses_prefix_widths_without_subsequence_measurement() {
2524        let _app_context = crate::render_state::app_context_test_scope();
2525        let prefix_calls = Rc::new(Cell::new(0));
2526        let subsequence_calls = Rc::new(Cell::new(0));
2527        set_text_measurer(PrefixWidthCountingMeasurer::new(
2528            Rc::clone(&prefix_calls),
2529            Rc::clone(&subsequence_calls),
2530        ));
2531        let style = TextStyle {
2532            span_style: crate::text::SpanStyle {
2533                font_size: TextUnit::Sp(10.0),
2534                ..Default::default()
2535            },
2536            ..Default::default()
2537        };
2538        let options = TextLayoutOptions {
2539            overflow: TextOverflow::Clip,
2540            soft_wrap: true,
2541            max_lines: usize::MAX,
2542            min_lines: 1,
2543        };
2544        let text = crate::text::AnnotatedString::from("word ".repeat(80).as_str());
2545
2546        let prepared = prepare_text_layout(&text, &style, options, Some(80.0));
2547
2548        assert!(prepared.metrics.line_count > 1);
2549        assert!(
2550            prefix_calls.get() > 0,
2551            "wrapping should request a line prefix width plan"
2552        );
2553        assert_eq!(
2554            subsequence_calls.get(),
2555            0,
2556            "prefix-capable wrapping should not probe candidate substrings"
2557        );
2558    }
2559
2560    #[test]
2561    fn text_wrapping_skips_prefix_widths_when_fit_probe_says_line_fits() {
2562        let _app_context = crate::render_state::app_context_test_scope();
2563        let line_width_calls = Rc::new(Cell::new(0));
2564        let prefix_calls = Rc::new(Cell::new(0));
2565        set_text_measurer(FitProbeCountingMeasurer::new(
2566            Rc::clone(&line_width_calls),
2567            Rc::clone(&prefix_calls),
2568        ));
2569        let style = TextStyle {
2570            span_style: crate::text::SpanStyle {
2571                font_size: TextUnit::Sp(10.0),
2572                ..Default::default()
2573            },
2574            ..Default::default()
2575        };
2576        let text = crate::text::AnnotatedString::from("fits without per-glyph prefix widths");
2577
2578        let prepared =
2579            prepare_text_layout(&text, &style, TextLayoutOptions::default(), Some(800.0));
2580
2581        assert_eq!(prepared.metrics.line_count, 1);
2582        assert_eq!(line_width_calls.get(), 1);
2583        assert_eq!(
2584            prefix_calls.get(),
2585            0,
2586            "fitting lines should not allocate prefix-width plans"
2587        );
2588    }
2589
2590    #[test]
2591    fn prepare_text_layout_uses_line_height_without_full_text_measurement() {
2592        let _app_context = crate::render_state::app_context_test_scope();
2593        let measure_calls = Rc::new(Cell::new(0));
2594        let line_height_calls = Rc::new(Cell::new(0));
2595        let measurer = LineHeightCountingMeasurer::new(
2596            Rc::clone(&measure_calls),
2597            Rc::clone(&line_height_calls),
2598        );
2599        let text = crate::text::AnnotatedString::from(
2600            "one two three four five six seven eight nine ten eleven twelve",
2601        );
2602
2603        let prepared = prepare_text_layout_with_measurer_for_node(
2604            &measurer,
2605            Some(7),
2606            &text,
2607            &TextStyle::default(),
2608            TextLayoutOptions::default(),
2609            Some(96.0),
2610        );
2611
2612        assert!(prepared.metrics.height > 0.0);
2613        assert_eq!(line_height_calls.get(), 1);
2614        assert_eq!(
2615            measure_calls.get(),
2616            0,
2617            "line-height lookup must not re-measure the whole paragraph"
2618        );
2619    }
2620
2621    fn style_with_line_break(line_break: LineBreak) -> TextStyle {
2622        TextStyle {
2623            span_style: crate::text::SpanStyle {
2624                font_size: TextUnit::Sp(10.0),
2625                ..Default::default()
2626            },
2627            paragraph_style: ParagraphStyle {
2628                line_break,
2629                ..Default::default()
2630            },
2631        }
2632    }
2633
2634    fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
2635        TextStyle {
2636            span_style: crate::text::SpanStyle {
2637                font_size: TextUnit::Sp(10.0),
2638                ..Default::default()
2639            },
2640            paragraph_style: ParagraphStyle {
2641                hyphens,
2642                ..Default::default()
2643            },
2644        }
2645    }
2646
2647    fn assert_f32_close(actual: f32, expected: f32) {
2648        assert!(
2649            (actual - expected).abs() <= 0.01,
2650            "actual={actual}, expected={expected}"
2651        );
2652    }
2653
2654    #[test]
2655    fn text_layout_options_wraps_and_limits_lines() {
2656        let _app_context = crate::render_state::app_context_test_scope();
2657        let style = TextStyle {
2658            span_style: crate::text::SpanStyle {
2659                font_size: TextUnit::Sp(10.0),
2660                ..Default::default()
2661            },
2662            ..Default::default()
2663        };
2664        let options = TextLayoutOptions {
2665            overflow: TextOverflow::Clip,
2666            soft_wrap: true,
2667            max_lines: 2,
2668            min_lines: 1,
2669        };
2670
2671        let prepared = prepare_text_layout(
2672            &crate::text::AnnotatedString::from("A B C D E F"),
2673            &style,
2674            options,
2675            Some(24.0), // roughly 4 chars in monospaced fallback
2676        );
2677
2678        assert!(prepared.did_overflow);
2679        assert!(prepared.metrics.line_count <= 2);
2680    }
2681
2682    #[test]
2683    fn text_layout_options_end_ellipsis_applies() {
2684        let _app_context = crate::render_state::app_context_test_scope();
2685        let style = TextStyle {
2686            span_style: crate::text::SpanStyle {
2687                font_size: TextUnit::Sp(10.0),
2688                ..Default::default()
2689            },
2690            ..Default::default()
2691        };
2692        let options = TextLayoutOptions {
2693            overflow: TextOverflow::Ellipsis,
2694            soft_wrap: false,
2695            max_lines: 1,
2696            min_lines: 1,
2697        };
2698
2699        let prepared = prepare_text_layout(
2700            &crate::text::AnnotatedString::from("Long long line"),
2701            &style,
2702            options,
2703            Some(20.0),
2704        );
2705        assert!(prepared.did_overflow);
2706        assert!(prepared.text.text.contains(ELLIPSIS));
2707    }
2708
2709    #[test]
2710    fn text_layout_options_visible_keeps_full_text() {
2711        let _app_context = crate::render_state::app_context_test_scope();
2712        let style = TextStyle {
2713            span_style: crate::text::SpanStyle {
2714                font_size: TextUnit::Sp(10.0),
2715                ..Default::default()
2716            },
2717            ..Default::default()
2718        };
2719        let options = TextLayoutOptions {
2720            overflow: TextOverflow::Visible,
2721            soft_wrap: false,
2722            max_lines: 1,
2723            min_lines: 1,
2724        };
2725
2726        let input = "This should remain unchanged";
2727        let prepared = prepare_text_layout(
2728            &crate::text::AnnotatedString::from(input),
2729            &style,
2730            options,
2731            Some(10.0),
2732        );
2733        assert_eq!(prepared.text.text, input);
2734    }
2735
2736    #[test]
2737    fn text_layout_options_respects_min_lines() {
2738        let _app_context = crate::render_state::app_context_test_scope();
2739        let style = TextStyle {
2740            span_style: crate::text::SpanStyle {
2741                font_size: TextUnit::Sp(10.0),
2742                ..Default::default()
2743            },
2744            ..Default::default()
2745        };
2746        let options = TextLayoutOptions {
2747            overflow: TextOverflow::Clip,
2748            soft_wrap: true,
2749            max_lines: 4,
2750            min_lines: 3,
2751        };
2752
2753        let prepared = prepare_text_layout(
2754            &crate::text::AnnotatedString::from("short"),
2755            &style,
2756            options,
2757            Some(100.0),
2758        );
2759        assert_eq!(prepared.metrics.line_count, 3);
2760    }
2761
2762    #[test]
2763    fn text_layout_options_middle_ellipsis_for_single_line() {
2764        let _app_context = crate::render_state::app_context_test_scope();
2765        let style = TextStyle {
2766            span_style: crate::text::SpanStyle {
2767                font_size: TextUnit::Sp(10.0),
2768                ..Default::default()
2769            },
2770            ..Default::default()
2771        };
2772        let options = TextLayoutOptions {
2773            overflow: TextOverflow::MiddleEllipsis,
2774            soft_wrap: false,
2775            max_lines: 1,
2776            min_lines: 1,
2777        };
2778
2779        let prepared = prepare_text_layout(
2780            &crate::text::AnnotatedString::from("abcdefghijk"),
2781            &style,
2782            options,
2783            Some(24.0),
2784        );
2785        assert!(prepared.text.text.contains(ELLIPSIS));
2786        assert!(prepared.did_overflow);
2787    }
2788
2789    #[test]
2790    fn text_layout_options_scale_down_fits_without_rewriting_text() {
2791        let _app_context = crate::render_state::app_context_test_scope();
2792        let style = TextStyle {
2793            span_style: crate::text::SpanStyle {
2794                font_size: TextUnit::Sp(20.0),
2795                ..Default::default()
2796            },
2797            ..Default::default()
2798        };
2799        let options = TextLayoutOptions {
2800            overflow: TextOverflow::ScaleDown {
2801                min_font_size_sp: 10.0,
2802            },
2803            soft_wrap: false,
2804            max_lines: 1,
2805            min_lines: 1,
2806        };
2807
2808        let prepared = prepare_text_layout(
2809            &crate::text::AnnotatedString::from("ABCDE"),
2810            &style,
2811            options,
2812            Some(36.0),
2813        );
2814
2815        assert_eq!(prepared.text.text, "ABCDE");
2816        assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
2817        assert!(!prepared.did_overflow);
2818        let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
2819        assert!(visual_font_size < 20.0);
2820        assert!(visual_font_size >= 10.0);
2821    }
2822
2823    #[test]
2824    fn text_layout_options_scale_down_scales_root_shadow() {
2825        let _app_context = crate::render_state::app_context_test_scope();
2826        let style = TextStyle {
2827            span_style: crate::text::SpanStyle {
2828                font_size: TextUnit::Sp(20.0),
2829                shadow: Some(crate::text::Shadow {
2830                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
2831                    offset: crate::modifier::Point::new(8.0, 4.0),
2832                    blur_radius: 6.0,
2833                }),
2834                ..Default::default()
2835            },
2836            ..Default::default()
2837        };
2838        let options = TextLayoutOptions {
2839            overflow: TextOverflow::ScaleDown {
2840                min_font_size_sp: 10.0,
2841            },
2842            soft_wrap: false,
2843            max_lines: 1,
2844            min_lines: 1,
2845        };
2846
2847        let prepared = prepare_text_layout(
2848            &crate::text::AnnotatedString::from("ABCDE"),
2849            &style,
2850            options,
2851            Some(36.0),
2852        );
2853
2854        let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
2855        let shadow = prepared
2856            .visual_style
2857            .span_style
2858            .shadow
2859            .expect("scaled style should retain shadow");
2860        assert_f32_close(shadow.offset.x, 8.0 * font_scale);
2861        assert_f32_close(shadow.offset.y, 4.0 * font_scale);
2862        assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
2863    }
2864
2865    #[test]
2866    fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
2867        let _app_context = crate::render_state::app_context_test_scope();
2868        let style = TextStyle {
2869            span_style: crate::text::SpanStyle {
2870                font_size: TextUnit::Sp(20.0),
2871                ..Default::default()
2872            },
2873            ..Default::default()
2874        };
2875        let options = TextLayoutOptions {
2876            overflow: TextOverflow::ScaleDown {
2877                min_font_size_sp: 10.0,
2878            },
2879            soft_wrap: false,
2880            max_lines: 1,
2881            min_lines: 1,
2882        };
2883
2884        let prepared = prepare_text_layout(
2885            &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
2886            &style,
2887            options,
2888            Some(12.0),
2889        );
2890
2891        assert_eq!(prepared.text.text, "ABCDEFGHIJ");
2892        assert!(prepared.did_overflow);
2893        assert_eq!(prepared.metrics.width, 12.0);
2894        assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
2895    }
2896
2897    #[test]
2898    fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
2899        let _app_context = crate::render_state::app_context_test_scope();
2900        let plain = crate::text::AnnotatedString::from("plain");
2901        assert!(matches!(
2902            scale_annotated_font_sizes(&plain, 0.5),
2903            std::borrow::Cow::Borrowed(_)
2904        ));
2905
2906        let colored = crate::text::annotated_string::Builder::new()
2907            .push_style(crate::text::SpanStyle {
2908                color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
2909                ..Default::default()
2910            })
2911            .append("colored")
2912            .pop()
2913            .to_annotated_string();
2914        assert!(matches!(
2915            scale_annotated_font_sizes(&colored, 0.5),
2916            std::borrow::Cow::Borrowed(_)
2917        ));
2918    }
2919
2920    #[test]
2921    fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
2922        let _app_context = crate::render_state::app_context_test_scope();
2923        let text = crate::text::annotated_string::Builder::new()
2924            .push_style(crate::text::SpanStyle {
2925                shadow: Some(crate::text::Shadow {
2926                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
2927                    offset: crate::modifier::Point::new(6.0, 2.0),
2928                    blur_radius: 4.0,
2929                }),
2930                ..Default::default()
2931            })
2932            .append("shadow")
2933            .pop()
2934            .to_annotated_string();
2935
2936        let scaled = scale_annotated_font_sizes(&text, 0.5);
2937        let std::borrow::Cow::Owned(scaled) = scaled else {
2938            panic!("shadowed span should be scaled into owned text");
2939        };
2940        let shadow = scaled.span_styles[0]
2941            .item
2942            .shadow
2943            .expect("scaled span should retain shadow");
2944        assert_f32_close(shadow.offset.x, 3.0);
2945        assert_f32_close(shadow.offset.y, 1.0);
2946        assert_f32_close(shadow.blur_radius, 2.0);
2947    }
2948
2949    #[test]
2950    fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
2951        let _app_context = crate::render_state::app_context_test_scope();
2952        let style = TextStyle {
2953            span_style: crate::text::SpanStyle {
2954                font_size: TextUnit::Sp(10.0),
2955                ..Default::default()
2956            },
2957            ..Default::default()
2958        };
2959        let options = TextLayoutOptions {
2960            overflow: TextOverflow::Clip,
2961            soft_wrap: true,
2962            max_lines: usize::MAX,
2963            min_lines: 1,
2964        };
2965
2966        let text = "if counter % 2 == 0";
2967        let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
2968        let prepared = prepare_text_layout(
2969            &crate::text::AnnotatedString::from(text),
2970            &style,
2971            options,
2972            Some(exact_width - 0.1),
2973        );
2974
2975        assert!(
2976            !prepared.text.text.contains('\n'),
2977            "unexpected line split: {:?}",
2978            prepared.text
2979        );
2980    }
2981
2982    #[test]
2983    fn line_break_mode_changes_wrap_strategy_contract() {
2984        let _app_context = crate::render_state::app_context_test_scope();
2985        let text = "This is an example text";
2986        let options = TextLayoutOptions {
2987            overflow: TextOverflow::Clip,
2988            soft_wrap: true,
2989            max_lines: usize::MAX,
2990            min_lines: 1,
2991        };
2992
2993        let simple = prepare_text_layout(
2994            &crate::text::AnnotatedString::from(text),
2995            &style_with_line_break(LineBreak::Simple),
2996            options,
2997            Some(120.0),
2998        );
2999        let heading = prepare_text_layout(
3000            &crate::text::AnnotatedString::from(text),
3001            &style_with_line_break(LineBreak::Heading),
3002            options,
3003            Some(120.0),
3004        );
3005        let paragraph = prepare_text_layout(
3006            &crate::text::AnnotatedString::from(text),
3007            &style_with_line_break(LineBreak::Paragraph),
3008            options,
3009            Some(50.0),
3010        );
3011
3012        assert_eq!(
3013            simple.text.text.lines().collect::<Vec<_>>(),
3014            vec!["This is an example", "text"]
3015        );
3016        assert_eq!(
3017            heading.text.text.lines().collect::<Vec<_>>(),
3018            vec!["This is an", "example text"]
3019        );
3020        assert_eq!(
3021            paragraph.text.text.lines().collect::<Vec<_>>(),
3022            vec!["This", "is an", "example", "text"]
3023        );
3024    }
3025
3026    #[test]
3027    fn hyphens_mode_changes_wrap_strategy_contract() {
3028        let _app_context = crate::render_state::app_context_test_scope();
3029        let text = "Transformation";
3030        let options = TextLayoutOptions {
3031            overflow: TextOverflow::Clip,
3032            soft_wrap: true,
3033            max_lines: usize::MAX,
3034            min_lines: 1,
3035        };
3036
3037        let auto = prepare_text_layout(
3038            &crate::text::AnnotatedString::from(text),
3039            &style_with_hyphens(Hyphens::Auto),
3040            options,
3041            Some(24.0),
3042        );
3043        let none = prepare_text_layout(
3044            &crate::text::AnnotatedString::from(text),
3045            &style_with_hyphens(Hyphens::None),
3046            options,
3047            Some(24.0),
3048        );
3049
3050        assert_eq!(
3051            auto.text.text.lines().collect::<Vec<_>>(),
3052            vec!["Tran", "sfor", "ma", "tion"]
3053        );
3054        assert_eq!(
3055            none.text.text.lines().collect::<Vec<_>>(),
3056            vec!["Tran", "sfor", "mati", "on"]
3057        );
3058        assert!(
3059            !auto.text.text.contains('-'),
3060            "automatic hyphenation should influence breaks without mutating source text content"
3061        );
3062    }
3063
3064    #[test]
3065    fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
3066        let _app_context = crate::render_state::app_context_test_scope();
3067        let text = "Transformation";
3068        let style = style_with_hyphens(Hyphens::Auto);
3069        let options = TextLayoutOptions {
3070            overflow: TextOverflow::Clip,
3071            soft_wrap: true,
3072            max_lines: usize::MAX,
3073            min_lines: 1,
3074        };
3075
3076        let prepared = prepare_text_layout_fallback(
3077            &ContractBreakMeasurer { retreat: 1 },
3078            &crate::text::AnnotatedString::from(text),
3079            &style,
3080            options,
3081            Some(24.0),
3082        );
3083
3084        assert_eq!(
3085            prepared.text.text.lines().collect::<Vec<_>>(),
3086            vec!["Tra", "nsf", "orm", "ati", "on"]
3087        );
3088    }
3089
3090    #[test]
3091    fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
3092        let _app_context = crate::render_state::app_context_test_scope();
3093        let text = "Transformation";
3094        let style = style_with_hyphens(Hyphens::Auto);
3095        let options = TextLayoutOptions {
3096            overflow: TextOverflow::Clip,
3097            soft_wrap: true,
3098            max_lines: usize::MAX,
3099            min_lines: 1,
3100        };
3101
3102        let prepared = prepare_text_layout_fallback(
3103            &ContractBreakMeasurer { retreat: 10 },
3104            &crate::text::AnnotatedString::from(text),
3105            &style,
3106            options,
3107            Some(24.0),
3108        );
3109
3110        assert_eq!(
3111            prepared.text.text.lines().collect::<Vec<_>>(),
3112            vec!["Tran", "sfor", "ma", "tion"]
3113        );
3114    }
3115
3116    #[test]
3117    fn transformed_text_keeps_span_ranges_within_display_bounds() {
3118        let _app_context = crate::render_state::app_context_test_scope();
3119        let style = TextStyle {
3120            span_style: crate::text::SpanStyle {
3121                font_size: TextUnit::Sp(10.0),
3122                ..Default::default()
3123            },
3124            ..Default::default()
3125        };
3126        let options = TextLayoutOptions {
3127            overflow: TextOverflow::Ellipsis,
3128            soft_wrap: false,
3129            max_lines: 1,
3130            min_lines: 1,
3131        };
3132        let annotated = crate::text::AnnotatedString::builder()
3133            .push_style(crate::text::SpanStyle {
3134                font_weight: Some(crate::text::FontWeight::BOLD),
3135                ..Default::default()
3136            })
3137            .append("Styled overflow text sample")
3138            .pop()
3139            .to_annotated_string();
3140
3141        let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
3142        assert!(prepared.did_overflow);
3143        for span in &prepared.text.span_styles {
3144            assert!(span.range.start < span.range.end);
3145            assert!(span.range.end <= prepared.text.text.len());
3146            assert!(prepared.text.text.is_char_boundary(span.range.start));
3147            assert!(prepared.text.text.is_char_boundary(span.range.end));
3148        }
3149    }
3150
3151    #[test]
3152    fn wrapped_text_splits_styles_around_inserted_newlines() {
3153        let _app_context = crate::render_state::app_context_test_scope();
3154        let style = TextStyle {
3155            span_style: crate::text::SpanStyle {
3156                font_size: TextUnit::Sp(10.0),
3157                ..Default::default()
3158            },
3159            ..Default::default()
3160        };
3161        let options = TextLayoutOptions {
3162            overflow: TextOverflow::Clip,
3163            soft_wrap: true,
3164            max_lines: usize::MAX,
3165            min_lines: 1,
3166        };
3167        let annotated = crate::text::AnnotatedString::builder()
3168            .push_style(crate::text::SpanStyle {
3169                text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
3170                ..Default::default()
3171            })
3172            .append("Wrapped style text example")
3173            .pop()
3174            .to_annotated_string();
3175
3176        let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
3177        assert!(prepared.text.text.contains('\n'));
3178        assert!(!prepared.text.span_styles.is_empty());
3179        for span in &prepared.text.span_styles {
3180            assert!(span.range.end <= prepared.text.text.len());
3181        }
3182    }
3183
3184    #[test]
3185    fn mixed_font_size_segments_wrap_without_truncation() {
3186        let _app_context = crate::render_state::app_context_test_scope();
3187        let style = TextStyle {
3188            span_style: crate::text::SpanStyle {
3189                font_size: TextUnit::Sp(14.0),
3190                ..Default::default()
3191            },
3192            ..Default::default()
3193        };
3194        let options = TextLayoutOptions {
3195            overflow: TextOverflow::Clip,
3196            soft_wrap: true,
3197            max_lines: usize::MAX,
3198            min_lines: 1,
3199        };
3200        let annotated = crate::text::AnnotatedString::builder()
3201            .append("You can also ")
3202            .push_style(crate::text::SpanStyle {
3203                font_size: TextUnit::Sp(22.0),
3204                ..Default::default()
3205            })
3206            .append("change font size")
3207            .pop()
3208            .append(" dynamically mid-sentence!")
3209            .to_annotated_string();
3210
3211        let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
3212        assert!(prepared.text.text.contains('\n'));
3213        assert!(prepared.text.text.contains("mid-sentence!"));
3214        assert!(!prepared.did_overflow);
3215    }
3216}