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 web_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
702/// Byte offset nearest the local content position (`x`, `y`), **wrap-aware** —
703/// the single hit-test every editable-text pointer path uses (tap-to-place,
704/// drag-select, and selection-handle drag).
705///
706/// It is the inverse of the drawn caret and [`wrapped_line_ranges`]: `y` selects
707/// the VISUAL (wrapped) line (`floor(y / line_height)`), then `x` picks the
708/// nearest char boundary WITHIN that line (delegated to the measurer with `y`
709/// forced to 0). `x`/`y` must already be in text space (padding- and
710/// pan-adjusted). The plain [`get_offset_for_position`] maps `y` through the
711/// measurer's logical `\n` layout, so on wrapped text it lands on the wrong line
712/// (an error that grows with each wrapped line above the finger). With
713/// `wrap_width == None` (single-line fields) this reduces to the one logical
714/// line.
715pub fn offset_for_position_wrapped(
716    text: &str,
717    style: &TextStyle,
718    node_id: Option<NodeId>,
719    wrap_width: Option<f32>,
720    line_height: f32,
721    x: f32,
722    y: f32,
723) -> usize {
724    if text.is_empty() {
725        return 0;
726    }
727    let annotated = crate::text::AnnotatedString::from(text);
728    let line_ranges = wrapped_line_ranges(
729        node_id,
730        &annotated,
731        style,
732        TextLayoutOptions::default(),
733        wrap_width,
734    );
735    if line_ranges.is_empty() {
736        return 0;
737    }
738    let line_idx = if line_height > 0.0 {
739        (y / line_height).floor().max(0.0) as usize
740    } else {
741        0
742    }
743    .min(line_ranges.len() - 1);
744    let range = &line_ranges[line_idx];
745    let line = &text[range.start..range.end];
746    let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
747    range.start + within.min(line.len())
748}
749
750pub fn get_cursor_x_for_offset(
751    text: &crate::text::AnnotatedString,
752    style: &TextStyle,
753    offset: usize,
754) -> f32 {
755    crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
756}
757
758pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
759    crate::render_state::with_text_service(|service| service.layout(text, style))
760}
761
762/// Returns the source-text byte range covered by each **visual** (wrapped) line
763/// when `text` is laid out at `max_width` with `options`, matching the wrapping
764/// the renderer performs. Each range excludes the trailing `\n`. With
765/// `max_width == None` (or soft-wrap disabled) this is just the logical
766/// `\n`-delimited lines.
767///
768/// The text field uses this to place its caret and selection handles on the
769/// correct visual line for wrapped text: the in-content caret otherwise counts
770/// only logical `\n` lines, so a caret on a wrapped line's second visual line is
771/// drawn on the first (and its x, being the whole logical-line prefix width,
772/// runs off the right edge and is clipped), while typing and the magnifier land
773/// on the correct spot.
774pub fn wrapped_line_ranges(
775    node_id: Option<NodeId>,
776    text: &crate::text::AnnotatedString,
777    style: &TextStyle,
778    options: TextLayoutOptions,
779    max_width: Option<f32>,
780) -> Vec<Range<usize>> {
781    crate::render_state::with_text_measurer(|m| {
782        wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
783    })
784}
785
786fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
787    measurer: &M,
788    _node_id: Option<NodeId>,
789    text: &crate::text::AnnotatedString,
790    style: &TextStyle,
791    options: TextLayoutOptions,
792    max_width: Option<f32>,
793) -> Vec<Range<usize>> {
794    let opts = options.normalized();
795    let max_width = normalize_max_width(max_width);
796    // Mirror the wrap decision in `prepare_text_layout_with_measurer_for_node`.
797    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
798        .then_some(max_width)
799        .flatten();
800    let line_break_mode = style
801        .paragraph_style
802        .line_break
803        .take_or_else(|| LineBreak::Simple);
804    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
805
806    let line_ranges = split_line_ranges(text.text.as_str());
807    let Some(width_limit) = wrap_width else {
808        return line_ranges;
809    };
810    let mut ranges = Vec::with_capacity(line_ranges.len());
811    for line_range in line_ranges {
812        for display_line in wrap_line_to_width(
813            measurer,
814            text,
815            line_range,
816            style,
817            width_limit,
818            line_break_mode,
819            hyphens_mode,
820        ) {
821            ranges.push(display_line.source_range.clone());
822        }
823    }
824    ranges
825}
826
827fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
828    measurer: &M,
829    text: &crate::text::AnnotatedString,
830    style: &TextStyle,
831    options: TextLayoutOptions,
832    max_width: Option<f32>,
833) -> PreparedTextLayout {
834    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
835}
836
837pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
838    measurer: &M,
839    node_id: Option<NodeId>,
840    text: &crate::text::AnnotatedString,
841    style: &TextStyle,
842    options: TextLayoutOptions,
843    max_width: Option<f32>,
844) -> PreparedTextLayout {
845    let telemetry = text_layout_telemetry_enabled();
846    let total_start = telemetry.then(Instant::now);
847    let opts = options.normalized();
848    let max_width = normalize_max_width(max_width);
849    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
850        return prepare_scale_down_text_layout(
851            measurer,
852            node_id,
853            text,
854            style,
855            opts,
856            max_width,
857            min_font_size_sp,
858        );
859    }
860
861    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
862        .then_some(max_width)
863        .flatten();
864    let line_break_mode = style
865        .paragraph_style
866        .line_break
867        .take_or_else(|| LineBreak::Simple);
868    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
869
870    let wrap_start = telemetry.then(Instant::now);
871    let line_ranges = split_line_ranges(text.text.as_str());
872    let source_line_count = line_ranges.len();
873    let mut visible_lines: Vec<DisplayLine>;
874    if let Some(width_limit) = wrap_width {
875        visible_lines = Vec::with_capacity(line_ranges.len());
876        for line_range in line_ranges {
877            let wrapped_lines = wrap_line_to_width(
878                measurer,
879                text,
880                line_range,
881                style,
882                width_limit,
883                line_break_mode,
884                hyphens_mode,
885            );
886            visible_lines.extend(wrapped_lines);
887        }
888    } else {
889        visible_lines = line_ranges
890            .into_iter()
891            .map(DisplayLine::from_source_range)
892            .collect();
893    }
894    let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
895
896    let overflow_start = telemetry.then(Instant::now);
897    let mut did_overflow = false;
898    if opts.overflow != TextOverflow::Visible && visible_lines.len() > opts.max_lines {
899        did_overflow = true;
900        visible_lines.truncate(opts.max_lines);
901        if let Some(last_line) = visible_lines.last_mut() {
902            let overflowed = apply_line_overflow(
903                measurer,
904                last_line.display_text(text),
905                style,
906                max_width,
907                opts,
908                true,
909                true,
910            );
911            last_line.apply_display_text(text, overflowed);
912        }
913    }
914
915    if let Some(width_limit) = max_width {
916        let single_line_ellipsis = opts.max_lines == 1 || !opts.soft_wrap;
917        let visible_len = visible_lines.len();
918        for (line_index, line) in visible_lines.iter_mut().enumerate() {
919            let width = line.measure_width(measurer, node_id, text, style);
920            if width > width_limit + WRAP_EPSILON {
921                if opts.overflow == TextOverflow::Visible {
922                    continue;
923                }
924                did_overflow = true;
925                let overflowed = apply_line_overflow(
926                    measurer,
927                    line.display_text(text),
928                    style,
929                    Some(width_limit),
930                    opts,
931                    line_index + 1 == visible_len,
932                    single_line_ellipsis,
933                );
934                line.apply_display_text(text, overflowed);
935            }
936        }
937    }
938    let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
939
940    let build_start = telemetry.then(Instant::now);
941    let display_annotated = build_display_annotated(text, &visible_lines);
942    debug_assert_eq!(
943        display_annotated.text,
944        join_display_line_text(text, &visible_lines)
945    );
946    let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
947
948    let metrics_start = telemetry.then(Instant::now);
949    let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
950    let display_line_count = visible_lines.len().max(1);
951    let layout_line_count = display_line_count.max(opts.min_lines);
952
953    let measured_width = if visible_lines.is_empty() {
954        0.0
955    } else {
956        visible_lines
957            .iter()
958            .map(|line| line.measure_width(measurer, node_id, text, style))
959            .fold(0.0_f32, f32::max)
960    };
961    let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
962    let width = if opts.overflow == TextOverflow::Visible {
963        measured_width
964    } else if let Some(width_limit) = max_width {
965        measured_width.min(width_limit)
966    } else {
967        measured_width
968    };
969
970    let prepared = PreparedTextLayout {
971        text: display_annotated,
972        visual_style: style.clone(),
973        metrics: TextMetrics {
974            width,
975            height: layout_line_count as f32 * line_height,
976            line_height,
977            line_count: layout_line_count,
978        },
979        did_overflow,
980    };
981
982    if let Some(start) = total_start {
983        eprintln!(
984            "[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}",
985            text.text.len(),
986            text.span_styles.len(),
987            source_line_count,
988            display_line_count,
989            wrap_width.is_some(),
990            max_width,
991            wrap_ms.unwrap_or(0.0),
992            overflow_ms.unwrap_or(0.0),
993            build_ms.unwrap_or(0.0),
994            metrics_ms.unwrap_or(0.0),
995            start.elapsed().as_secs_f64() * 1000.0,
996        );
997    }
998
999    prepared
1000}
1001
1002fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1003    measurer: &M,
1004    node_id: Option<NodeId>,
1005    text: &crate::text::AnnotatedString,
1006    style: &TextStyle,
1007    options: TextLayoutOptions,
1008    max_width: Option<f32>,
1009    min_font_size_sp: f32,
1010) -> PreparedTextLayout {
1011    let clipped_options = TextLayoutOptions {
1012        overflow: TextOverflow::Clip,
1013        ..options
1014    }
1015    .normalized();
1016
1017    let full_size = prepare_scaled_text_layout(
1018        measurer,
1019        node_id,
1020        text,
1021        style,
1022        clipped_options,
1023        max_width,
1024        1.0,
1025    );
1026    let Some(width_limit) = max_width else {
1027        return full_size;
1028    };
1029    if !full_size.did_overflow {
1030        return full_size;
1031    }
1032
1033    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1034    if !base_font_size.is_finite() || base_font_size <= 0.0 {
1035        return full_size;
1036    }
1037    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1038    if min_scale >= 1.0 {
1039        return full_size;
1040    }
1041
1042    let min_size = prepare_scaled_text_layout(
1043        measurer,
1044        node_id,
1045        text,
1046        style,
1047        clipped_options,
1048        Some(width_limit),
1049        min_scale,
1050    );
1051    if min_size.did_overflow {
1052        return min_size;
1053    }
1054
1055    let mut low = min_scale;
1056    let mut high = 1.0;
1057    let mut best = min_size;
1058    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1059        let mid = (low + high) * 0.5;
1060        let candidate = prepare_scaled_text_layout(
1061            measurer,
1062            node_id,
1063            text,
1064            style,
1065            clipped_options,
1066            Some(width_limit),
1067            mid,
1068        );
1069        if candidate.did_overflow {
1070            high = mid;
1071        } else {
1072            low = mid;
1073            best = candidate;
1074        }
1075    }
1076
1077    best
1078}
1079
1080fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1081    measurer: &M,
1082    node_id: Option<NodeId>,
1083    text: &crate::text::AnnotatedString,
1084    style: &TextStyle,
1085    options: TextLayoutOptions,
1086    max_width: Option<f32>,
1087    font_scale: f32,
1088) -> PreparedTextLayout {
1089    let visual_style = scale_text_style_font_sizes(style, font_scale);
1090    let visual_text = scale_annotated_font_sizes(text, font_scale);
1091    prepare_text_layout_with_measurer_for_node(
1092        measurer,
1093        node_id,
1094        visual_text.as_ref(),
1095        &visual_style,
1096        options,
1097        max_width,
1098    )
1099}
1100
1101fn scale_annotated_font_sizes(
1102    text: &crate::text::AnnotatedString,
1103    factor: f32,
1104) -> Cow<'_, crate::text::AnnotatedString> {
1105    if is_identity_scale(factor) || !annotated_text_needs_scaling(text) {
1106        return Cow::Borrowed(text);
1107    }
1108
1109    let mut scaled = text.clone();
1110    for span in &mut scaled.span_styles {
1111        span.item = scale_span_style_font_sizes(&span.item, factor, None);
1112    }
1113    Cow::Owned(scaled)
1114}
1115
1116fn scale_text_style_font_sizes(style: &TextStyle, factor: f32) -> TextStyle {
1117    if is_identity_scale(factor) {
1118        return style.clone();
1119    }
1120
1121    let mut scaled = style.clone();
1122    scaled.span_style =
1123        scale_span_style_font_sizes(&style.span_style, factor, Some(DEFAULT_FONT_SIZE_SP));
1124    scaled.paragraph_style.line_height =
1125        scale_text_unit_sp(scaled.paragraph_style.line_height, factor);
1126    if let Some(mut indent) = scaled.paragraph_style.text_indent {
1127        indent.first_line = scale_text_unit_sp(indent.first_line, factor);
1128        indent.rest_line = scale_text_unit_sp(indent.rest_line, factor);
1129        scaled.paragraph_style.text_indent = Some(indent);
1130    }
1131    scaled
1132}
1133
1134fn scale_span_style_font_sizes(
1135    style: &crate::text::SpanStyle,
1136    factor: f32,
1137    default_font_size_sp: Option<f32>,
1138) -> crate::text::SpanStyle {
1139    let mut scaled = style.clone();
1140    scaled.font_size = match (style.font_size, default_font_size_sp) {
1141        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1142            crate::text::TextUnit::Sp(default_size * factor)
1143        }
1144        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, factor),
1145        (unit, None) => scale_text_unit_sp(unit, factor),
1146    };
1147    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, factor);
1148    if let Some(mut shadow) = scaled.shadow {
1149        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1150        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1151        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1152        scaled.shadow = Some(shadow);
1153    }
1154    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1155        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1156            width: width * factor,
1157        });
1158    }
1159    scaled
1160}
1161
1162fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1163    text.span_styles
1164        .iter()
1165        .any(|span| span_style_needs_scaling(&span.item))
1166}
1167
1168fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1169    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1170        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1171        || matches!(
1172            style.draw_style,
1173            Some(crate::text::TextDrawStyle::Stroke { .. })
1174        )
1175        || style.shadow.is_some()
1176}
1177
1178fn scale_text_unit_sp(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
1179    match unit {
1180        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1181            crate::text::TextUnit::Sp(value * factor)
1182        }
1183        other => other,
1184    }
1185}
1186
1187fn scale_text_unit_sp_and_em(unit: crate::text::TextUnit, factor: f32) -> crate::text::TextUnit {
1188    match unit {
1189        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1190            crate::text::TextUnit::Sp(value * factor)
1191        }
1192        crate::text::TextUnit::Em(value) if value.is_finite() => {
1193            crate::text::TextUnit::Em(value * factor)
1194        }
1195        other => other,
1196    }
1197}
1198
1199fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1200    if value.is_finite() {
1201        value * factor
1202    } else {
1203        value
1204    }
1205}
1206
1207fn is_identity_scale(factor: f32) -> bool {
1208    (factor - 1.0).abs() <= f32::EPSILON
1209}
1210
1211#[derive(Clone, Debug)]
1212enum DisplayLineText {
1213    Source,
1214    Remapped(crate::text::AnnotatedString),
1215}
1216
1217#[derive(Clone, Debug)]
1218struct DisplayLine {
1219    source_range: Range<usize>,
1220    text: DisplayLineText,
1221    measured_width: Option<f32>,
1222}
1223
1224impl DisplayLine {
1225    fn from_source_range(source_range: Range<usize>) -> Self {
1226        Self {
1227            source_range,
1228            text: DisplayLineText::Source,
1229            measured_width: None,
1230        }
1231    }
1232
1233    fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1234        Self {
1235            source_range,
1236            text: DisplayLineText::Source,
1237            measured_width: measured_width
1238                .is_finite()
1239                .then_some(measured_width.max(0.0)),
1240        }
1241    }
1242
1243    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1244        match &self.text {
1245            DisplayLineText::Source => &source.text[self.source_range.clone()],
1246            DisplayLineText::Remapped(annotated) => annotated.text.as_str(),
1247        }
1248    }
1249
1250    fn measure_width<M: TextMeasurer + ?Sized>(
1251        &self,
1252        measurer: &M,
1253        node_id: Option<NodeId>,
1254        source: &crate::text::AnnotatedString,
1255        style: &TextStyle,
1256    ) -> f32 {
1257        match &self.text {
1258            DisplayLineText::Source => self.measured_width.unwrap_or_else(|| {
1259                measurer
1260                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1261                    .width
1262            }),
1263            DisplayLineText::Remapped(annotated) => {
1264                measurer.measure_for_node(node_id, annotated, style).width
1265            }
1266        }
1267    }
1268
1269    fn apply_display_text(&mut self, source: &crate::text::AnnotatedString, display_text: String) {
1270        let source_text = &source.text[self.source_range.clone()];
1271        self.measured_width = None;
1272        self.text = if source_text == display_text {
1273            DisplayLineText::Source
1274        } else {
1275            DisplayLineText::Remapped(remap_annotated_subsequence_for_display(
1276                source,
1277                self.source_range.clone(),
1278                display_text.as_str(),
1279            ))
1280        };
1281    }
1282}
1283
1284fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1285    if text.is_empty() {
1286        return single_line_range(0..0);
1287    }
1288
1289    let mut ranges = Vec::new();
1290    let mut start = 0usize;
1291    for (idx, ch) in text.char_indices() {
1292        if ch == '\n' {
1293            ranges.push(start..idx);
1294            start = idx + ch.len_utf8();
1295        }
1296    }
1297    ranges.push(start..text.len());
1298    ranges
1299}
1300
1301fn build_display_annotated(
1302    source: &crate::text::AnnotatedString,
1303    lines: &[DisplayLine],
1304) -> crate::text::AnnotatedString {
1305    if lines.is_empty() {
1306        return crate::text::AnnotatedString::from("");
1307    }
1308
1309    let mut builder = crate::text::AnnotatedString::builder();
1310    for (idx, line) in lines.iter().enumerate() {
1311        builder = match &line.text {
1312            DisplayLineText::Source => {
1313                builder.append_annotated_subsequence(source, line.source_range.clone())
1314            }
1315            DisplayLineText::Remapped(annotated) => builder.append_annotated(annotated),
1316        };
1317        if idx + 1 < lines.len() {
1318            builder = builder.append("\n");
1319        }
1320    }
1321    builder.to_annotated_string()
1322}
1323
1324fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1325    let mut text = String::new();
1326    for (idx, line) in lines.iter().enumerate() {
1327        text.push_str(line.display_text(source));
1328        if idx + 1 < lines.len() {
1329            text.push('\n');
1330        }
1331    }
1332    text
1333}
1334
1335fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1336    while end > start {
1337        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1338            break;
1339        };
1340        if ch.is_whitespace() {
1341            end = start + idx;
1342        } else {
1343            break;
1344        }
1345    }
1346    end
1347}
1348
1349fn remap_annotated_subsequence_for_display(
1350    source: &crate::text::AnnotatedString,
1351    source_range: Range<usize>,
1352    display_text: &str,
1353) -> crate::text::AnnotatedString {
1354    let source_text = &source.text[source_range.clone()];
1355    if source_text == display_text {
1356        return source.subsequence(source_range);
1357    }
1358
1359    let display_chars = map_display_chars_to_source(source_text, display_text);
1360    crate::text::AnnotatedString {
1361        text: display_text.to_string(),
1362        span_styles: remap_subsequence_range_styles(
1363            &source.span_styles,
1364            source_range.clone(),
1365            &display_chars,
1366        ),
1367        paragraph_styles: remap_subsequence_range_styles(
1368            &source.paragraph_styles,
1369            source_range.clone(),
1370            &display_chars,
1371        ),
1372        string_annotations: remap_subsequence_range_styles(
1373            &source.string_annotations,
1374            source_range.clone(),
1375            &display_chars,
1376        ),
1377        link_annotations: remap_subsequence_range_styles(
1378            &source.link_annotations,
1379            source_range,
1380            &display_chars,
1381        ),
1382    }
1383}
1384
1385#[derive(Clone, Copy)]
1386struct DisplayCharMap {
1387    display_start: usize,
1388    display_end: usize,
1389    source_start: Option<usize>,
1390}
1391
1392fn map_display_chars_to_source(source: &str, display: &str) -> Vec<DisplayCharMap> {
1393    let source_chars: Vec<(usize, char)> = source.char_indices().collect();
1394    let mut source_index = 0usize;
1395    let mut maps = Vec::with_capacity(display.chars().count());
1396
1397    for (display_start, display_char) in display.char_indices() {
1398        let display_end = display_start + display_char.len_utf8();
1399        let mut source_start = None;
1400        while source_index < source_chars.len() {
1401            let (candidate_start, candidate_char) = source_chars[source_index];
1402            source_index += 1;
1403            if candidate_char == display_char {
1404                source_start = Some(candidate_start);
1405                break;
1406            }
1407        }
1408        maps.push(DisplayCharMap {
1409            display_start,
1410            display_end,
1411            source_start,
1412        });
1413    }
1414
1415    maps
1416}
1417
1418fn remap_subsequence_range_styles<T: Clone>(
1419    styles: &[crate::text::RangeStyle<T>],
1420    source_range: Range<usize>,
1421    display_chars: &[DisplayCharMap],
1422) -> Vec<crate::text::RangeStyle<T>> {
1423    let mut remapped = Vec::new();
1424
1425    for style in styles {
1426        let overlap_start = style.range.start.max(source_range.start);
1427        let overlap_end = style.range.end.min(source_range.end);
1428        if overlap_start >= overlap_end {
1429            continue;
1430        }
1431        let local_source_range =
1432            (overlap_start - source_range.start)..(overlap_end - source_range.start);
1433        let mut range_start = None;
1434        let mut range_end = 0usize;
1435
1436        for map in display_chars {
1437            let in_range = map.source_start.is_some_and(|source_start| {
1438                source_start >= local_source_range.start && source_start < local_source_range.end
1439            });
1440
1441            if in_range {
1442                if range_start.is_none() {
1443                    range_start = Some(map.display_start);
1444                }
1445                range_end = map.display_end;
1446                continue;
1447            }
1448
1449            if let Some(start) = range_start.take() {
1450                if start < range_end {
1451                    remapped.push(crate::text::RangeStyle {
1452                        item: style.item.clone(),
1453                        range: start..range_end,
1454                    });
1455                }
1456            }
1457        }
1458
1459        if let Some(start) = range_start.take() {
1460            if start < range_end {
1461                remapped.push(crate::text::RangeStyle {
1462                    item: style.item.clone(),
1463                    range: start..range_end,
1464                });
1465            }
1466        }
1467    }
1468
1469    remapped
1470}
1471
1472fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1473    match max_width {
1474        Some(width) if width.is_finite() && width > 0.0 => Some(width),
1475        _ => None,
1476    }
1477}
1478
1479fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1480    (base_start + relative.start)..(base_start + relative.end)
1481}
1482
1483fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1484    boundaries
1485        .binary_search(&byte_offset)
1486        .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1487}
1488
1489fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1490    std::iter::once(range).collect()
1491}
1492
1493struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1494    measurer: &'a M,
1495    text: &'a crate::text::AnnotatedString,
1496    style: &'a TextStyle,
1497    line_start: usize,
1498    prefix_widths: Option<TextLinePrefixWidths>,
1499}
1500
1501impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1502    fn new(
1503        measurer: &'a M,
1504        text: &'a crate::text::AnnotatedString,
1505        line_range: &Range<usize>,
1506        style: &'a TextStyle,
1507        boundary_count: usize,
1508    ) -> Self {
1509        let expected_chars = boundary_count.saturating_sub(1);
1510        let prefix_widths = measurer
1511            .measure_line_prefix_widths(text, line_range.clone(), style)
1512            .filter(|widths| widths.char_count() == expected_chars);
1513        Self {
1514            measurer,
1515            text,
1516            style,
1517            line_start: line_range.start,
1518            prefix_widths,
1519        }
1520    }
1521
1522    fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1523        if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1524            return width;
1525        }
1526        let segment_range =
1527            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1528        self.measurer
1529            .measure_subsequence(self.text, segment_range, self.style)
1530            .width
1531    }
1532
1533    fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1534        if let Some(prefix_widths) = &self.prefix_widths {
1535            if let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx) {
1536                return Some(width);
1537            }
1538        }
1539        None
1540    }
1541
1542    fn display_line_for_char_range(
1543        &self,
1544        boundaries: &[usize],
1545        start_idx: usize,
1546        end_idx: usize,
1547    ) -> DisplayLine {
1548        let source_range =
1549            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1550        let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1551        DisplayLine::from_measured_source_range(source_range, measured_width)
1552    }
1553}
1554
1555fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1556    measurer: &M,
1557    text: &crate::text::AnnotatedString,
1558    line_range: Range<usize>,
1559    style: &TextStyle,
1560    max_width: f32,
1561    line_break: LineBreak,
1562    hyphens: Hyphens,
1563) -> Vec<DisplayLine> {
1564    let line_text = &text.text[line_range.clone()];
1565    if line_text.is_empty() {
1566        return vec![DisplayLine::from_source_range(
1567            line_range.start..line_range.start,
1568        )];
1569    }
1570
1571    if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style) {
1572        if measured_width <= max_width + WRAP_EPSILON {
1573            return vec![DisplayLine::from_measured_source_range(
1574                line_range,
1575                measured_width,
1576            )];
1577        }
1578    }
1579
1580    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1581        && line_text.chars().any(char::is_whitespace)
1582    {
1583        if let Some(balanced) = wrap_line_with_word_balance(
1584            measurer,
1585            text,
1586            line_range.clone(),
1587            style,
1588            max_width,
1589            line_break,
1590        ) {
1591            return balanced;
1592        }
1593    }
1594
1595    wrap_line_greedy(
1596        measurer, text, line_range, style, max_width, line_break, hyphens,
1597    )
1598}
1599
1600fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1601    measurer: &M,
1602    text: &crate::text::AnnotatedString,
1603    line_range: Range<usize>,
1604    style: &TextStyle,
1605    max_width: f32,
1606    line_break: LineBreak,
1607    hyphens: Hyphens,
1608) -> Vec<DisplayLine> {
1609    let line_text = &text.text[line_range.clone()];
1610    let boundaries = char_boundaries(line_text);
1611    let measure_context =
1612        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1613    if let Some(measured_width) =
1614        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1615    {
1616        if measured_width <= max_width + WRAP_EPSILON {
1617            return vec![DisplayLine::from_measured_source_range(
1618                line_range,
1619                measured_width,
1620            )];
1621        }
1622    }
1623    let mut wrapped = Vec::new();
1624    let mut start_idx = 0usize;
1625
1626    while start_idx < boundaries.len() - 1 {
1627        let mut low = start_idx + 1;
1628        let mut high = boundaries.len() - 1;
1629        let mut best = start_idx + 1;
1630
1631        while low <= high {
1632            let mid = (low + high) / 2;
1633            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1634            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1635                best = mid;
1636                low = mid + 1;
1637            } else {
1638                if mid == 0 {
1639                    break;
1640                }
1641                high = mid - 1;
1642            }
1643        }
1644
1645        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1646        let mut effective_wrap_idx = wrap_idx;
1647        let can_hyphenate = hyphens == Hyphens::Auto
1648            && wrap_idx == best
1649            && best < boundaries.len() - 1
1650            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1651        if can_hyphenate {
1652            effective_wrap_idx = resolve_auto_hyphen_break(
1653                measurer,
1654                line_text,
1655                style,
1656                &boundaries,
1657                start_idx,
1658                wrap_idx,
1659            );
1660        }
1661
1662        let segment_start = boundaries[start_idx];
1663        let mut segment_end = boundaries[effective_wrap_idx];
1664        if wrap_idx != best {
1665            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1666        }
1667        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1668        wrapped.push(measure_context.display_line_for_char_range(
1669            &boundaries,
1670            start_idx,
1671            segment_end_idx,
1672        ));
1673
1674        start_idx = if wrap_idx != best {
1675            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1676        } else {
1677            effective_wrap_idx
1678        };
1679    }
1680
1681    if wrapped.is_empty() {
1682        wrapped.push(DisplayLine::from_source_range(
1683            line_range.start..line_range.start,
1684        ));
1685    }
1686
1687    wrapped
1688}
1689
1690fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1691    measurer: &M,
1692    text: &crate::text::AnnotatedString,
1693    line_range: Range<usize>,
1694    style: &TextStyle,
1695    max_width: f32,
1696    line_break: LineBreak,
1697) -> Option<Vec<DisplayLine>> {
1698    let line_text = &text.text[line_range.clone()];
1699    let boundaries = char_boundaries(line_text);
1700    let measure_context =
1701        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1702    if let Some(measured_width) =
1703        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1704    {
1705        if measured_width <= max_width + WRAP_EPSILON {
1706            return Some(vec![DisplayLine::from_measured_source_range(
1707                line_range,
1708                measured_width,
1709            )]);
1710        }
1711    }
1712    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1713    if breakpoints.len() <= 2 {
1714        return None;
1715    }
1716
1717    let node_count = breakpoints.len();
1718    let mut best_cost = vec![f32::INFINITY; node_count];
1719    let mut next_index = vec![None; node_count];
1720    best_cost[node_count - 1] = 0.0;
1721
1722    for start in (0..node_count - 1).rev() {
1723        for end in start + 1..node_count {
1724            let start_byte = boundaries[breakpoints[start]];
1725            let end_byte = boundaries[breakpoints[end]];
1726            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1727            if trimmed_end <= start_byte {
1728                continue;
1729            }
1730            let segment_start_idx = breakpoints[start];
1731            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1732            let segment_width =
1733                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1734            if segment_width > max_width + WRAP_EPSILON {
1735                continue;
1736            }
1737            if !best_cost[end].is_finite() {
1738                continue;
1739            }
1740            let slack = (max_width - segment_width).max(0.0);
1741            let is_last = end == node_count - 1;
1742            let segment_cost = match line_break {
1743                LineBreak::Heading => slack * slack,
1744                LineBreak::Paragraph => {
1745                    if is_last {
1746                        slack * slack * 0.16
1747                    } else {
1748                        slack * slack
1749                    }
1750                }
1751                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1752            };
1753            let candidate = segment_cost + best_cost[end];
1754            if candidate < best_cost[start] {
1755                best_cost[start] = candidate;
1756                next_index[start] = Some(end);
1757            }
1758        }
1759    }
1760
1761    let mut wrapped = Vec::new();
1762    let mut current = 0usize;
1763    while current < node_count - 1 {
1764        let next = next_index[current]?;
1765        let start_byte = boundaries[breakpoints[current]];
1766        let end_byte = boundaries[breakpoints[next]];
1767        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1768        if trimmed_end <= start_byte {
1769            return None;
1770        }
1771        let segment_start_idx = breakpoints[current];
1772        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1773        wrapped.push(measure_context.display_line_for_char_range(
1774            &boundaries,
1775            segment_start_idx,
1776            segment_end_idx,
1777        ));
1778        current = next;
1779    }
1780
1781    if wrapped.is_empty() {
1782        return None;
1783    }
1784
1785    Some(wrapped)
1786}
1787
1788fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1789    let mut points = vec![0usize];
1790    for idx in 1..boundaries.len() - 1 {
1791        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1792        let current = &line[boundaries[idx]..boundaries[idx + 1]];
1793        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1794            points.push(idx);
1795        }
1796    }
1797    let end = boundaries.len() - 1;
1798    if points.last().copied() != Some(end) {
1799        points.push(end);
1800    }
1801    points
1802}
1803
1804fn choose_wrap_break(
1805    line: &str,
1806    boundaries: &[usize],
1807    start_idx: usize,
1808    best: usize,
1809    _line_break: LineBreak,
1810) -> usize {
1811    if best >= boundaries.len() - 1 {
1812        return best;
1813    }
1814
1815    if best <= start_idx + 1 {
1816        return best;
1817    }
1818
1819    for idx in (start_idx + 1..best).rev() {
1820        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1821        if prev.chars().all(char::is_whitespace) {
1822            return idx;
1823        }
1824    }
1825    best
1826}
1827
1828fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1829    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1830        return false;
1831    }
1832    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1833    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1834    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1835}
1836
1837fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1838    measurer: &M,
1839    line: &str,
1840    style: &TextStyle,
1841    boundaries: &[usize],
1842    start_idx: usize,
1843    break_idx: usize,
1844) -> usize {
1845    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx) {
1846        if is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate) {
1847            return candidate;
1848        }
1849    }
1850    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1851}
1852
1853fn is_valid_auto_hyphen_break(
1854    line: &str,
1855    boundaries: &[usize],
1856    start_idx: usize,
1857    break_idx: usize,
1858    candidate_idx: usize,
1859) -> bool {
1860    let end_idx = boundaries.len().saturating_sub(1);
1861    candidate_idx > start_idx
1862        && candidate_idx < end_idx
1863        && candidate_idx <= break_idx
1864        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1865        && is_break_inside_word(line, boundaries, candidate_idx)
1866}
1867
1868fn choose_auto_hyphen_break_fallback(
1869    boundaries: &[usize],
1870    start_idx: usize,
1871    break_idx: usize,
1872) -> usize {
1873    let end_idx = boundaries.len().saturating_sub(1);
1874    if break_idx >= end_idx {
1875        return break_idx;
1876    }
1877    let trailing_len = end_idx.saturating_sub(break_idx);
1878    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1879        return break_idx;
1880    }
1881
1882    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1883    let max_break = break_idx.saturating_sub(1);
1884    if min_break > max_break {
1885        return break_idx;
1886    }
1887
1888    let mut best_break = break_idx;
1889    let mut best_penalty = usize::MAX;
1890    for idx in min_break..=max_break {
1891        let candidate_trailing_len = end_idx.saturating_sub(idx);
1892        let candidate_prefix_len = idx.saturating_sub(start_idx);
1893        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1894            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1895        {
1896            continue;
1897        }
1898
1899        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1900        if penalty < best_penalty {
1901            best_penalty = penalty;
1902            best_break = idx;
1903            if penalty == 0 {
1904                break;
1905            }
1906        }
1907    }
1908    best_break
1909}
1910
1911fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1912    while idx < boundaries.len() - 1 {
1913        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1914        if !ch.chars().all(char::is_whitespace) {
1915            break;
1916        }
1917        idx += 1;
1918    }
1919    idx
1920}
1921
1922fn apply_line_overflow<M: TextMeasurer + ?Sized>(
1923    measurer: &M,
1924    line: &str,
1925    style: &TextStyle,
1926    max_width: Option<f32>,
1927    options: TextLayoutOptions,
1928    is_last_visible_line: bool,
1929    single_line_ellipsis: bool,
1930) -> String {
1931    if options.overflow == TextOverflow::Clip || !is_last_visible_line {
1932        return line.to_string();
1933    }
1934
1935    let Some(width_limit) = max_width else {
1936        return match options.overflow {
1937            TextOverflow::Ellipsis => format!("{line}{ELLIPSIS}"),
1938            TextOverflow::StartEllipsis => format!("{ELLIPSIS}{line}"),
1939            TextOverflow::MiddleEllipsis => format!("{ELLIPSIS}{line}"),
1940            TextOverflow::Clip | TextOverflow::Visible | TextOverflow::ScaleDown { .. } => {
1941                line.to_string()
1942            }
1943        };
1944    };
1945
1946    match options.overflow {
1947        TextOverflow::Clip | TextOverflow::Visible => line.to_string(),
1948        TextOverflow::Ellipsis => fit_end_ellipsis(measurer, line, style, width_limit),
1949        TextOverflow::StartEllipsis => {
1950            if single_line_ellipsis {
1951                fit_start_ellipsis(measurer, line, style, width_limit)
1952            } else {
1953                line.to_string()
1954            }
1955        }
1956        TextOverflow::MiddleEllipsis => {
1957            if single_line_ellipsis {
1958                fit_middle_ellipsis(measurer, line, style, width_limit)
1959            } else {
1960                line.to_string()
1961            }
1962        }
1963        TextOverflow::ScaleDown { .. } => line.to_string(),
1964    }
1965}
1966
1967fn fit_end_ellipsis<M: TextMeasurer + ?Sized>(
1968    measurer: &M,
1969    line: &str,
1970    style: &TextStyle,
1971    max_width: f32,
1972) -> String {
1973    if measurer
1974        .measure(&crate::text::AnnotatedString::from(line), style)
1975        .width
1976        <= max_width + WRAP_EPSILON
1977    {
1978        return line.to_string();
1979    }
1980
1981    let ellipsis_width = measurer
1982        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
1983        .width;
1984    if ellipsis_width > max_width + WRAP_EPSILON {
1985        return String::new();
1986    }
1987
1988    let boundaries = char_boundaries(line);
1989    let mut low = 0usize;
1990    let mut high = boundaries.len() - 1;
1991    let mut best = 0usize;
1992
1993    while low <= high {
1994        let mid = (low + high) / 2;
1995        let prefix = &line[..boundaries[mid]];
1996        let candidate = format!("{prefix}{ELLIPSIS}");
1997        let width = measurer
1998            .measure(
1999                &crate::text::AnnotatedString::from(candidate.as_str()),
2000                style,
2001            )
2002            .width;
2003        if width <= max_width + WRAP_EPSILON {
2004            best = mid;
2005            low = mid + 1;
2006        } else if mid == 0 {
2007            break;
2008        } else {
2009            high = mid - 1;
2010        }
2011    }
2012
2013    format!("{}{}", &line[..boundaries[best]], ELLIPSIS)
2014}
2015
2016fn fit_start_ellipsis<M: TextMeasurer + ?Sized>(
2017    measurer: &M,
2018    line: &str,
2019    style: &TextStyle,
2020    max_width: f32,
2021) -> String {
2022    if measurer
2023        .measure(&crate::text::AnnotatedString::from(line), style)
2024        .width
2025        <= max_width + WRAP_EPSILON
2026    {
2027        return line.to_string();
2028    }
2029
2030    let ellipsis_width = measurer
2031        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
2032        .width;
2033    if ellipsis_width > max_width + WRAP_EPSILON {
2034        return String::new();
2035    }
2036
2037    let boundaries = char_boundaries(line);
2038    let mut low = 0usize;
2039    let mut high = boundaries.len() - 1;
2040    let mut best = boundaries.len() - 1;
2041
2042    while low <= high {
2043        let mid = (low + high) / 2;
2044        let suffix = &line[boundaries[mid]..];
2045        let candidate = format!("{ELLIPSIS}{suffix}");
2046        let width = measurer
2047            .measure(
2048                &crate::text::AnnotatedString::from(candidate.as_str()),
2049                style,
2050            )
2051            .width;
2052        if width <= max_width + WRAP_EPSILON {
2053            best = mid;
2054            if mid == 0 {
2055                break;
2056            }
2057            high = mid - 1;
2058        } else {
2059            low = mid + 1;
2060        }
2061    }
2062
2063    format!("{ELLIPSIS}{}", &line[boundaries[best]..])
2064}
2065
2066fn fit_middle_ellipsis<M: TextMeasurer + ?Sized>(
2067    measurer: &M,
2068    line: &str,
2069    style: &TextStyle,
2070    max_width: f32,
2071) -> String {
2072    if measurer
2073        .measure(&crate::text::AnnotatedString::from(line), style)
2074        .width
2075        <= max_width + WRAP_EPSILON
2076    {
2077        return line.to_string();
2078    }
2079
2080    let ellipsis_width = measurer
2081        .measure(&crate::text::AnnotatedString::from(ELLIPSIS), style)
2082        .width;
2083    if ellipsis_width > max_width + WRAP_EPSILON {
2084        return String::new();
2085    }
2086
2087    let boundaries = char_boundaries(line);
2088    let total_chars = boundaries.len().saturating_sub(1);
2089    for keep in (0..=total_chars).rev() {
2090        let keep_start = keep.div_ceil(2);
2091        let keep_end = keep / 2;
2092        let start = &line[..boundaries[keep_start]];
2093        let end_start = boundaries[total_chars.saturating_sub(keep_end)];
2094        let end = &line[end_start..];
2095        let candidate = format!("{start}{ELLIPSIS}{end}");
2096        if measurer
2097            .measure(
2098                &crate::text::AnnotatedString::from(candidate.as_str()),
2099                style,
2100            )
2101            .width
2102            <= max_width + WRAP_EPSILON
2103        {
2104            return candidate;
2105        }
2106    }
2107
2108    ELLIPSIS.to_string()
2109}
2110
2111fn char_boundaries(text: &str) -> Vec<usize> {
2112    let mut out = Vec::with_capacity(text.chars().count() + 1);
2113    out.push(0);
2114    for (idx, _) in text.char_indices() {
2115        if idx != 0 {
2116            out.push(idx);
2117        }
2118    }
2119    out.push(text.len());
2120    out
2121}
2122
2123#[cfg(test)]
2124mod tests {
2125    use super::*;
2126    use crate::text::{Hyphens, LineBreak, ParagraphStyle, TextUnit};
2127    use crate::text_layout_result::TextLayoutResult;
2128    use std::cell::Cell;
2129
2130    #[test]
2131    fn text_layout_telemetry_env_flag_is_not_process_cached() {
2132        let source = include_str!("measure.rs");
2133        let once_lock = ["Once", "Lock"].concat();
2134        let cached_init_call = ["get", "_or", "_init"].concat();
2135
2136        assert!(
2137            !source.contains(&once_lock) && !source.contains(&cached_init_call),
2138            "text layout telemetry env flag must be read at the diagnostic boundary"
2139        );
2140    }
2141
2142    #[test]
2143    fn text_service_cache_retains_large_lazy_text_working_set() {
2144        let mut cache = BoundedTextCache::new(TEXT_SERVICE_CACHE_CAPACITY);
2145        let metrics = TextMetrics {
2146            width: 1.0,
2147            height: 1.0,
2148            line_height: 1.0,
2149            line_count: 1,
2150        };
2151
2152        for index in 0..4096u64 {
2153            cache.insert(
2154                TextBaseCacheKey {
2155                    text_hash: index,
2156                    style_hash: 7,
2157                },
2158                metrics,
2159            );
2160        }
2161
2162        for index in 0..4096u64 {
2163            assert!(
2164                cache
2165                    .get(&TextBaseCacheKey {
2166                        text_hash: index,
2167                        style_hash: 7,
2168                    })
2169                    .is_some(),
2170                "large lazy text working-set entry {index} was evicted too early"
2171            );
2172        }
2173    }
2174
2175    struct ContractBreakMeasurer {
2176        retreat: usize,
2177    }
2178
2179    impl TextMeasurer for ContractBreakMeasurer {
2180        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2181            MonospacedTextMeasurer.measure(
2182                &crate::text::AnnotatedString::from(text.text.as_str()),
2183                style,
2184            )
2185        }
2186
2187        fn get_offset_for_position(
2188            &self,
2189            text: &crate::text::AnnotatedString,
2190            style: &TextStyle,
2191            x: f32,
2192            y: f32,
2193        ) -> usize {
2194            MonospacedTextMeasurer.get_offset_for_position(
2195                &crate::text::AnnotatedString::from(text.text.as_str()),
2196                style,
2197                x,
2198                y,
2199            )
2200        }
2201
2202        fn get_cursor_x_for_offset(
2203            &self,
2204            text: &crate::text::AnnotatedString,
2205            style: &TextStyle,
2206            offset: usize,
2207        ) -> f32 {
2208            MonospacedTextMeasurer.get_cursor_x_for_offset(
2209                &crate::text::AnnotatedString::from(text.text.as_str()),
2210                style,
2211                offset,
2212            )
2213        }
2214
2215        fn layout(
2216            &self,
2217            text: &crate::text::AnnotatedString,
2218            style: &TextStyle,
2219        ) -> TextLayoutResult {
2220            MonospacedTextMeasurer.layout(
2221                &crate::text::AnnotatedString::from(text.text.as_str()),
2222                style,
2223            )
2224        }
2225
2226        fn choose_auto_hyphen_break(
2227            &self,
2228            _line: &str,
2229            _style: &TextStyle,
2230            _segment_start_char: usize,
2231            measured_break_char: usize,
2232        ) -> Option<usize> {
2233            measured_break_char.checked_sub(self.retreat)
2234        }
2235    }
2236
2237    struct CountingTextMeasurer {
2238        measure_calls: Rc<Cell<usize>>,
2239        layout_calls: Rc<Cell<usize>>,
2240    }
2241
2242    impl CountingTextMeasurer {
2243        fn new(measure_calls: Rc<Cell<usize>>, layout_calls: Rc<Cell<usize>>) -> Self {
2244            Self {
2245                measure_calls,
2246                layout_calls,
2247            }
2248        }
2249    }
2250
2251    impl TextMeasurer for CountingTextMeasurer {
2252        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2253            self.measure_calls.set(self.measure_calls.get() + 1);
2254            MonospacedTextMeasurer.measure(text, style)
2255        }
2256
2257        fn get_offset_for_position(
2258            &self,
2259            text: &crate::text::AnnotatedString,
2260            style: &TextStyle,
2261            x: f32,
2262            y: f32,
2263        ) -> usize {
2264            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2265        }
2266
2267        fn get_cursor_x_for_offset(
2268            &self,
2269            text: &crate::text::AnnotatedString,
2270            style: &TextStyle,
2271            offset: usize,
2272        ) -> f32 {
2273            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2274        }
2275
2276        fn layout(
2277            &self,
2278            text: &crate::text::AnnotatedString,
2279            style: &TextStyle,
2280        ) -> TextLayoutResult {
2281            self.layout_calls.set(self.layout_calls.get() + 1);
2282            MonospacedTextMeasurer.layout(text, style)
2283        }
2284    }
2285
2286    struct CountingPreparedTextMeasurer {
2287        prepare_calls: Rc<Cell<usize>>,
2288    }
2289
2290    impl CountingPreparedTextMeasurer {
2291        fn new(prepare_calls: Rc<Cell<usize>>) -> Self {
2292            Self { prepare_calls }
2293        }
2294    }
2295
2296    struct PrefixWidthCountingMeasurer {
2297        prefix_calls: Rc<Cell<usize>>,
2298        subsequence_calls: Rc<Cell<usize>>,
2299    }
2300
2301    impl PrefixWidthCountingMeasurer {
2302        fn new(prefix_calls: Rc<Cell<usize>>, subsequence_calls: Rc<Cell<usize>>) -> Self {
2303            Self {
2304                prefix_calls,
2305                subsequence_calls,
2306            }
2307        }
2308    }
2309
2310    impl TextMeasurer for PrefixWidthCountingMeasurer {
2311        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2312            MonospacedTextMeasurer.measure(text, style)
2313        }
2314
2315        fn measure_subsequence(
2316            &self,
2317            text: &crate::text::AnnotatedString,
2318            range: Range<usize>,
2319            style: &TextStyle,
2320        ) -> TextMetrics {
2321            self.subsequence_calls.set(self.subsequence_calls.get() + 1);
2322            MonospacedTextMeasurer.measure_subsequence(text, range, style)
2323        }
2324
2325        fn measure_line_prefix_widths(
2326            &self,
2327            text: &crate::text::AnnotatedString,
2328            line_range: Range<usize>,
2329            style: &TextStyle,
2330        ) -> Option<TextLinePrefixWidths> {
2331            self.prefix_calls.set(self.prefix_calls.get() + 1);
2332            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2333        }
2334
2335        fn get_offset_for_position(
2336            &self,
2337            text: &crate::text::AnnotatedString,
2338            style: &TextStyle,
2339            x: f32,
2340            y: f32,
2341        ) -> usize {
2342            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2343        }
2344
2345        fn get_cursor_x_for_offset(
2346            &self,
2347            text: &crate::text::AnnotatedString,
2348            style: &TextStyle,
2349            offset: usize,
2350        ) -> f32 {
2351            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2352        }
2353
2354        fn layout(
2355            &self,
2356            text: &crate::text::AnnotatedString,
2357            style: &TextStyle,
2358        ) -> TextLayoutResult {
2359            MonospacedTextMeasurer.layout(text, style)
2360        }
2361    }
2362
2363    struct LineHeightCountingMeasurer {
2364        measure_calls: Rc<Cell<usize>>,
2365        line_height_calls: Rc<Cell<usize>>,
2366    }
2367
2368    struct FitProbeCountingMeasurer {
2369        line_width_calls: Rc<Cell<usize>>,
2370        prefix_calls: Rc<Cell<usize>>,
2371    }
2372
2373    impl FitProbeCountingMeasurer {
2374        fn new(line_width_calls: Rc<Cell<usize>>, prefix_calls: Rc<Cell<usize>>) -> Self {
2375            Self {
2376                line_width_calls,
2377                prefix_calls,
2378            }
2379        }
2380    }
2381
2382    impl LineHeightCountingMeasurer {
2383        fn new(measure_calls: Rc<Cell<usize>>, line_height_calls: Rc<Cell<usize>>) -> Self {
2384            Self {
2385                measure_calls,
2386                line_height_calls,
2387            }
2388        }
2389    }
2390
2391    impl TextMeasurer for LineHeightCountingMeasurer {
2392        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2393            self.measure_calls.set(self.measure_calls.get() + 1);
2394            MonospacedTextMeasurer.measure(text, style)
2395        }
2396
2397        fn measure_line_prefix_widths(
2398            &self,
2399            text: &crate::text::AnnotatedString,
2400            line_range: Range<usize>,
2401            style: &TextStyle,
2402        ) -> Option<TextLinePrefixWidths> {
2403            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2404        }
2405
2406        fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
2407            self.line_height_calls.set(self.line_height_calls.get() + 1);
2408            MonospacedTextMeasurer.line_height(text, style)
2409        }
2410
2411        fn get_offset_for_position(
2412            &self,
2413            text: &crate::text::AnnotatedString,
2414            style: &TextStyle,
2415            x: f32,
2416            y: f32,
2417        ) -> usize {
2418            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2419        }
2420
2421        fn get_cursor_x_for_offset(
2422            &self,
2423            text: &crate::text::AnnotatedString,
2424            style: &TextStyle,
2425            offset: usize,
2426        ) -> f32 {
2427            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2428        }
2429
2430        fn layout(
2431            &self,
2432            text: &crate::text::AnnotatedString,
2433            style: &TextStyle,
2434        ) -> TextLayoutResult {
2435            MonospacedTextMeasurer.layout(text, style)
2436        }
2437    }
2438
2439    impl TextMeasurer for FitProbeCountingMeasurer {
2440        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2441            MonospacedTextMeasurer.measure(text, style)
2442        }
2443
2444        fn measure_line_width(
2445            &self,
2446            text: &crate::text::AnnotatedString,
2447            line_range: Range<usize>,
2448            style: &TextStyle,
2449        ) -> Option<f32> {
2450            self.line_width_calls.set(self.line_width_calls.get() + 1);
2451            MonospacedTextMeasurer.measure_line_width(text, line_range, style)
2452        }
2453
2454        fn measure_line_prefix_widths(
2455            &self,
2456            text: &crate::text::AnnotatedString,
2457            line_range: Range<usize>,
2458            style: &TextStyle,
2459        ) -> Option<TextLinePrefixWidths> {
2460            self.prefix_calls.set(self.prefix_calls.get() + 1);
2461            MonospacedTextMeasurer.measure_line_prefix_widths(text, line_range, style)
2462        }
2463
2464        fn get_offset_for_position(
2465            &self,
2466            text: &crate::text::AnnotatedString,
2467            style: &TextStyle,
2468            x: f32,
2469            y: f32,
2470        ) -> usize {
2471            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2472        }
2473
2474        fn get_cursor_x_for_offset(
2475            &self,
2476            text: &crate::text::AnnotatedString,
2477            style: &TextStyle,
2478            offset: usize,
2479        ) -> f32 {
2480            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2481        }
2482
2483        fn layout(
2484            &self,
2485            text: &crate::text::AnnotatedString,
2486            style: &TextStyle,
2487        ) -> TextLayoutResult {
2488            MonospacedTextMeasurer.layout(text, style)
2489        }
2490    }
2491
2492    impl TextMeasurer for CountingPreparedTextMeasurer {
2493        fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
2494            MonospacedTextMeasurer.measure(text, style)
2495        }
2496
2497        fn prepare_with_options_for_node(
2498            &self,
2499            _node_id: Option<NodeId>,
2500            text: &crate::text::AnnotatedString,
2501            style: &TextStyle,
2502            options: TextLayoutOptions,
2503            max_width: Option<f32>,
2504        ) -> PreparedTextLayout {
2505            self.prepare_calls.set(self.prepare_calls.get() + 1);
2506            MonospacedTextMeasurer.prepare_with_options(text, style, options, max_width)
2507        }
2508
2509        fn get_offset_for_position(
2510            &self,
2511            text: &crate::text::AnnotatedString,
2512            style: &TextStyle,
2513            x: f32,
2514            y: f32,
2515        ) -> usize {
2516            MonospacedTextMeasurer.get_offset_for_position(text, style, x, y)
2517        }
2518
2519        fn get_cursor_x_for_offset(
2520            &self,
2521            text: &crate::text::AnnotatedString,
2522            style: &TextStyle,
2523            offset: usize,
2524        ) -> f32 {
2525            MonospacedTextMeasurer.get_cursor_x_for_offset(text, style, offset)
2526        }
2527
2528        fn layout(
2529            &self,
2530            text: &crate::text::AnnotatedString,
2531            style: &TextStyle,
2532        ) -> TextLayoutResult {
2533            MonospacedTextMeasurer.layout(text, style)
2534        }
2535    }
2536
2537    #[test]
2538    fn text_service_routes_measurement_through_current_measurer() {
2539        let _app_context = crate::render_state::app_context_test_scope();
2540        let service = TextService::from_measurer(Rc::new(MonospacedTextMeasurer));
2541        let text = crate::text::AnnotatedString::from("abc");
2542        let style = TextStyle::default();
2543
2544        let metrics = service.with_measurer(|measurer| measurer.measure(&text, &style));
2545
2546        assert!(metrics.width > 0.0);
2547        assert!(metrics.height > 0.0);
2548    }
2549
2550    #[test]
2551    fn text_service_caches_metrics_and_layouts_per_context() {
2552        let _app_context = crate::render_state::app_context_test_scope();
2553        let measure_calls = Rc::new(Cell::new(0));
2554        let layout_calls = Rc::new(Cell::new(0));
2555        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2556            Rc::clone(&measure_calls),
2557            Rc::clone(&layout_calls),
2558        )));
2559        let text = crate::text::AnnotatedString::from("cached text");
2560        let style = TextStyle::default();
2561
2562        let first_metrics = service.measure(Some(7), &text, &style);
2563        let second_metrics = service.measure(Some(7), &text, &style);
2564        let first_layout = service.layout(&text, &style);
2565        let second_layout = service.layout(&text, &style);
2566
2567        assert_eq!(first_metrics, second_metrics);
2568        assert_eq!(first_layout.width, second_layout.width);
2569        assert_eq!(measure_calls.get(), 1);
2570        assert_eq!(layout_calls.get(), 1);
2571    }
2572
2573    #[test]
2574    fn text_service_reuses_metrics_cache_across_node_ids() {
2575        let _app_context = crate::render_state::app_context_test_scope();
2576        let measure_calls = Rc::new(Cell::new(0));
2577        let layout_calls = Rc::new(Cell::new(0));
2578        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2579            Rc::clone(&measure_calls),
2580            Rc::clone(&layout_calls),
2581        )));
2582        let text = crate::text::AnnotatedString::from("same lazy item text");
2583        let style = TextStyle::default();
2584
2585        let first_metrics = service.measure(Some(7), &text, &style);
2586        let second_metrics = service.measure(Some(8), &text, &style);
2587
2588        assert_eq!(first_metrics, second_metrics);
2589        assert_eq!(measure_calls.get(), 1);
2590    }
2591
2592    #[test]
2593    fn text_service_reuses_prepared_layout_cache_across_node_ids() {
2594        let _app_context = crate::render_state::app_context_test_scope();
2595        let prepare_calls = Rc::new(Cell::new(0));
2596        let service = TextService::from_measurer(Rc::new(CountingPreparedTextMeasurer::new(
2597            Rc::clone(&prepare_calls),
2598        )));
2599        let text = crate::text::AnnotatedString::from("same prepared lazy item text");
2600        let style = TextStyle::default();
2601        let options = TextLayoutOptions::default();
2602
2603        let first = service.prepare_with_options(Some(9), &text, &style, options, Some(120.0));
2604        let second = service.prepare_with_options(Some(10), &text, &style, options, Some(120.0));
2605
2606        assert_eq!(first.metrics, second.metrics);
2607        assert_eq!(prepare_calls.get(), 1);
2608    }
2609
2610    #[test]
2611    fn text_service_clears_caches_when_measurer_changes() {
2612        let _app_context = crate::render_state::app_context_test_scope();
2613        let first_measure_calls = Rc::new(Cell::new(0));
2614        let second_measure_calls = Rc::new(Cell::new(0));
2615        let layout_calls = Rc::new(Cell::new(0));
2616        let service = TextService::from_measurer(Rc::new(CountingTextMeasurer::new(
2617            Rc::clone(&first_measure_calls),
2618            Rc::clone(&layout_calls),
2619        )));
2620        let text = crate::text::AnnotatedString::from("cached text");
2621        let style = TextStyle::default();
2622
2623        let _ = service.measure(None, &text, &style);
2624        let _ = service.measure(None, &text, &style);
2625        service.set_measurer(Rc::new(CountingTextMeasurer::new(
2626            Rc::clone(&second_measure_calls),
2627            Rc::clone(&layout_calls),
2628        )));
2629        let _ = service.measure(None, &text, &style);
2630
2631        assert_eq!(first_measure_calls.get(), 1);
2632        assert_eq!(second_measure_calls.get(), 1);
2633    }
2634
2635    #[test]
2636    fn text_wrapping_uses_prefix_widths_without_subsequence_measurement() {
2637        let _app_context = crate::render_state::app_context_test_scope();
2638        let prefix_calls = Rc::new(Cell::new(0));
2639        let subsequence_calls = Rc::new(Cell::new(0));
2640        set_text_measurer(PrefixWidthCountingMeasurer::new(
2641            Rc::clone(&prefix_calls),
2642            Rc::clone(&subsequence_calls),
2643        ));
2644        let style = TextStyle {
2645            span_style: crate::text::SpanStyle {
2646                font_size: TextUnit::Sp(10.0),
2647                ..Default::default()
2648            },
2649            ..Default::default()
2650        };
2651        let options = TextLayoutOptions {
2652            overflow: TextOverflow::Clip,
2653            soft_wrap: true,
2654            max_lines: usize::MAX,
2655            min_lines: 1,
2656        };
2657        let text = crate::text::AnnotatedString::from("word ".repeat(80).as_str());
2658
2659        let prepared = prepare_text_layout(&text, &style, options, Some(80.0));
2660
2661        assert!(prepared.metrics.line_count > 1);
2662        assert!(
2663            prefix_calls.get() > 0,
2664            "wrapping should request a line prefix width plan"
2665        );
2666        assert_eq!(
2667            subsequence_calls.get(),
2668            0,
2669            "prefix-capable wrapping should not probe candidate substrings"
2670        );
2671    }
2672
2673    #[test]
2674    fn text_wrapping_skips_prefix_widths_when_fit_probe_says_line_fits() {
2675        let _app_context = crate::render_state::app_context_test_scope();
2676        let line_width_calls = Rc::new(Cell::new(0));
2677        let prefix_calls = Rc::new(Cell::new(0));
2678        set_text_measurer(FitProbeCountingMeasurer::new(
2679            Rc::clone(&line_width_calls),
2680            Rc::clone(&prefix_calls),
2681        ));
2682        let style = TextStyle {
2683            span_style: crate::text::SpanStyle {
2684                font_size: TextUnit::Sp(10.0),
2685                ..Default::default()
2686            },
2687            ..Default::default()
2688        };
2689        let text = crate::text::AnnotatedString::from("fits without per-glyph prefix widths");
2690
2691        let prepared =
2692            prepare_text_layout(&text, &style, TextLayoutOptions::default(), Some(800.0));
2693
2694        assert_eq!(prepared.metrics.line_count, 1);
2695        assert_eq!(line_width_calls.get(), 1);
2696        assert_eq!(
2697            prefix_calls.get(),
2698            0,
2699            "fitting lines should not allocate prefix-width plans"
2700        );
2701    }
2702
2703    #[test]
2704    fn prepare_text_layout_uses_line_height_without_full_text_measurement() {
2705        let _app_context = crate::render_state::app_context_test_scope();
2706        let measure_calls = Rc::new(Cell::new(0));
2707        let line_height_calls = Rc::new(Cell::new(0));
2708        let measurer = LineHeightCountingMeasurer::new(
2709            Rc::clone(&measure_calls),
2710            Rc::clone(&line_height_calls),
2711        );
2712        let text = crate::text::AnnotatedString::from(
2713            "one two three four five six seven eight nine ten eleven twelve",
2714        );
2715
2716        let prepared = prepare_text_layout_with_measurer_for_node(
2717            &measurer,
2718            Some(7),
2719            &text,
2720            &TextStyle::default(),
2721            TextLayoutOptions::default(),
2722            Some(96.0),
2723        );
2724
2725        assert!(prepared.metrics.height > 0.0);
2726        assert_eq!(line_height_calls.get(), 1);
2727        assert_eq!(
2728            measure_calls.get(),
2729            0,
2730            "line-height lookup must not re-measure the whole paragraph"
2731        );
2732    }
2733
2734    fn style_with_line_break(line_break: LineBreak) -> TextStyle {
2735        TextStyle {
2736            span_style: crate::text::SpanStyle {
2737                font_size: TextUnit::Sp(10.0),
2738                ..Default::default()
2739            },
2740            paragraph_style: ParagraphStyle {
2741                line_break,
2742                ..Default::default()
2743            },
2744        }
2745    }
2746
2747    fn style_with_hyphens(hyphens: Hyphens) -> TextStyle {
2748        TextStyle {
2749            span_style: crate::text::SpanStyle {
2750                font_size: TextUnit::Sp(10.0),
2751                ..Default::default()
2752            },
2753            paragraph_style: ParagraphStyle {
2754                hyphens,
2755                ..Default::default()
2756            },
2757        }
2758    }
2759
2760    fn assert_f32_close(actual: f32, expected: f32) {
2761        assert!(
2762            (actual - expected).abs() <= 0.01,
2763            "actual={actual}, expected={expected}"
2764        );
2765    }
2766
2767    #[test]
2768    fn text_layout_options_wraps_and_limits_lines() {
2769        let _app_context = crate::render_state::app_context_test_scope();
2770        let style = TextStyle {
2771            span_style: crate::text::SpanStyle {
2772                font_size: TextUnit::Sp(10.0),
2773                ..Default::default()
2774            },
2775            ..Default::default()
2776        };
2777        let options = TextLayoutOptions {
2778            overflow: TextOverflow::Clip,
2779            soft_wrap: true,
2780            max_lines: 2,
2781            min_lines: 1,
2782        };
2783
2784        let prepared = prepare_text_layout(
2785            &crate::text::AnnotatedString::from("A B C D E F"),
2786            &style,
2787            options,
2788            Some(24.0), // roughly 4 chars in monospaced fallback
2789        );
2790
2791        assert!(prepared.did_overflow);
2792        assert!(prepared.metrics.line_count <= 2);
2793    }
2794
2795    #[test]
2796    fn text_layout_options_end_ellipsis_applies() {
2797        let _app_context = crate::render_state::app_context_test_scope();
2798        let style = TextStyle {
2799            span_style: crate::text::SpanStyle {
2800                font_size: TextUnit::Sp(10.0),
2801                ..Default::default()
2802            },
2803            ..Default::default()
2804        };
2805        let options = TextLayoutOptions {
2806            overflow: TextOverflow::Ellipsis,
2807            soft_wrap: false,
2808            max_lines: 1,
2809            min_lines: 1,
2810        };
2811
2812        let prepared = prepare_text_layout(
2813            &crate::text::AnnotatedString::from("Long long line"),
2814            &style,
2815            options,
2816            Some(20.0),
2817        );
2818        assert!(prepared.did_overflow);
2819        assert!(prepared.text.text.contains(ELLIPSIS));
2820    }
2821
2822    #[test]
2823    fn text_layout_options_visible_keeps_full_text() {
2824        let _app_context = crate::render_state::app_context_test_scope();
2825        let style = TextStyle {
2826            span_style: crate::text::SpanStyle {
2827                font_size: TextUnit::Sp(10.0),
2828                ..Default::default()
2829            },
2830            ..Default::default()
2831        };
2832        let options = TextLayoutOptions {
2833            overflow: TextOverflow::Visible,
2834            soft_wrap: false,
2835            max_lines: 1,
2836            min_lines: 1,
2837        };
2838
2839        let input = "This should remain unchanged";
2840        let prepared = prepare_text_layout(
2841            &crate::text::AnnotatedString::from(input),
2842            &style,
2843            options,
2844            Some(10.0),
2845        );
2846        assert_eq!(prepared.text.text, input);
2847    }
2848
2849    #[test]
2850    fn text_layout_options_respects_min_lines() {
2851        let _app_context = crate::render_state::app_context_test_scope();
2852        let style = TextStyle {
2853            span_style: crate::text::SpanStyle {
2854                font_size: TextUnit::Sp(10.0),
2855                ..Default::default()
2856            },
2857            ..Default::default()
2858        };
2859        let options = TextLayoutOptions {
2860            overflow: TextOverflow::Clip,
2861            soft_wrap: true,
2862            max_lines: 4,
2863            min_lines: 3,
2864        };
2865
2866        let prepared = prepare_text_layout(
2867            &crate::text::AnnotatedString::from("short"),
2868            &style,
2869            options,
2870            Some(100.0),
2871        );
2872        assert_eq!(prepared.metrics.line_count, 3);
2873    }
2874
2875    #[test]
2876    fn text_layout_options_middle_ellipsis_for_single_line() {
2877        let _app_context = crate::render_state::app_context_test_scope();
2878        let style = TextStyle {
2879            span_style: crate::text::SpanStyle {
2880                font_size: TextUnit::Sp(10.0),
2881                ..Default::default()
2882            },
2883            ..Default::default()
2884        };
2885        let options = TextLayoutOptions {
2886            overflow: TextOverflow::MiddleEllipsis,
2887            soft_wrap: false,
2888            max_lines: 1,
2889            min_lines: 1,
2890        };
2891
2892        let prepared = prepare_text_layout(
2893            &crate::text::AnnotatedString::from("abcdefghijk"),
2894            &style,
2895            options,
2896            Some(24.0),
2897        );
2898        assert!(prepared.text.text.contains(ELLIPSIS));
2899        assert!(prepared.did_overflow);
2900    }
2901
2902    #[test]
2903    fn text_layout_options_scale_down_fits_without_rewriting_text() {
2904        let _app_context = crate::render_state::app_context_test_scope();
2905        let style = TextStyle {
2906            span_style: crate::text::SpanStyle {
2907                font_size: TextUnit::Sp(20.0),
2908                ..Default::default()
2909            },
2910            ..Default::default()
2911        };
2912        let options = TextLayoutOptions {
2913            overflow: TextOverflow::ScaleDown {
2914                min_font_size_sp: 10.0,
2915            },
2916            soft_wrap: false,
2917            max_lines: 1,
2918            min_lines: 1,
2919        };
2920
2921        let prepared = prepare_text_layout(
2922            &crate::text::AnnotatedString::from("ABCDE"),
2923            &style,
2924            options,
2925            Some(36.0),
2926        );
2927
2928        assert_eq!(prepared.text.text, "ABCDE");
2929        assert!(prepared.metrics.width <= 36.0 + WRAP_EPSILON);
2930        assert!(!prepared.did_overflow);
2931        let visual_font_size = prepared.visual_style.resolve_font_size(14.0);
2932        assert!(visual_font_size < 20.0);
2933        assert!(visual_font_size >= 10.0);
2934    }
2935
2936    #[test]
2937    fn text_layout_options_scale_down_scales_root_shadow() {
2938        let _app_context = crate::render_state::app_context_test_scope();
2939        let style = TextStyle {
2940            span_style: crate::text::SpanStyle {
2941                font_size: TextUnit::Sp(20.0),
2942                shadow: Some(crate::text::Shadow {
2943                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
2944                    offset: crate::modifier::Point::new(8.0, 4.0),
2945                    blur_radius: 6.0,
2946                }),
2947                ..Default::default()
2948            },
2949            ..Default::default()
2950        };
2951        let options = TextLayoutOptions {
2952            overflow: TextOverflow::ScaleDown {
2953                min_font_size_sp: 10.0,
2954            },
2955            soft_wrap: false,
2956            max_lines: 1,
2957            min_lines: 1,
2958        };
2959
2960        let prepared = prepare_text_layout(
2961            &crate::text::AnnotatedString::from("ABCDE"),
2962            &style,
2963            options,
2964            Some(36.0),
2965        );
2966
2967        let font_scale = prepared.visual_style.resolve_font_size(14.0) / 20.0;
2968        let shadow = prepared
2969            .visual_style
2970            .span_style
2971            .shadow
2972            .expect("scaled style should retain shadow");
2973        assert_f32_close(shadow.offset.x, 8.0 * font_scale);
2974        assert_f32_close(shadow.offset.y, 4.0 * font_scale);
2975        assert_f32_close(shadow.blur_radius, 6.0 * font_scale);
2976    }
2977
2978    #[test]
2979    fn text_layout_options_scale_down_stops_at_minimum_and_clips() {
2980        let _app_context = crate::render_state::app_context_test_scope();
2981        let style = TextStyle {
2982            span_style: crate::text::SpanStyle {
2983                font_size: TextUnit::Sp(20.0),
2984                ..Default::default()
2985            },
2986            ..Default::default()
2987        };
2988        let options = TextLayoutOptions {
2989            overflow: TextOverflow::ScaleDown {
2990                min_font_size_sp: 10.0,
2991            },
2992            soft_wrap: false,
2993            max_lines: 1,
2994            min_lines: 1,
2995        };
2996
2997        let prepared = prepare_text_layout(
2998            &crate::text::AnnotatedString::from("ABCDEFGHIJ"),
2999            &style,
3000            options,
3001            Some(12.0),
3002        );
3003
3004        assert_eq!(prepared.text.text, "ABCDEFGHIJ");
3005        assert!(prepared.did_overflow);
3006        assert_eq!(prepared.metrics.width, 12.0);
3007        assert_eq!(prepared.visual_style.resolve_font_size(14.0), 10.0);
3008    }
3009
3010    #[test]
3011    fn scale_annotated_font_sizes_borrows_when_spans_need_no_scaling() {
3012        let _app_context = crate::render_state::app_context_test_scope();
3013        let plain = crate::text::AnnotatedString::from("plain");
3014        assert!(matches!(
3015            scale_annotated_font_sizes(&plain, 0.5),
3016            std::borrow::Cow::Borrowed(_)
3017        ));
3018
3019        let colored = crate::text::annotated_string::Builder::new()
3020            .push_style(crate::text::SpanStyle {
3021                color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
3022                ..Default::default()
3023            })
3024            .append("colored")
3025            .pop()
3026            .to_annotated_string();
3027        assert!(matches!(
3028            scale_annotated_font_sizes(&colored, 0.5),
3029            std::borrow::Cow::Borrowed(_)
3030        ));
3031    }
3032
3033    #[test]
3034    fn scale_annotated_font_sizes_scales_span_shadow_geometry() {
3035        let _app_context = crate::render_state::app_context_test_scope();
3036        let text = crate::text::annotated_string::Builder::new()
3037            .push_style(crate::text::SpanStyle {
3038                shadow: Some(crate::text::Shadow {
3039                    color: crate::modifier::Color(0.0, 0.0, 0.0, 1.0),
3040                    offset: crate::modifier::Point::new(6.0, 2.0),
3041                    blur_radius: 4.0,
3042                }),
3043                ..Default::default()
3044            })
3045            .append("shadow")
3046            .pop()
3047            .to_annotated_string();
3048
3049        let scaled = scale_annotated_font_sizes(&text, 0.5);
3050        let std::borrow::Cow::Owned(scaled) = scaled else {
3051            panic!("shadowed span should be scaled into owned text");
3052        };
3053        let shadow = scaled.span_styles[0]
3054            .item
3055            .shadow
3056            .expect("scaled span should retain shadow");
3057        assert_f32_close(shadow.offset.x, 3.0);
3058        assert_f32_close(shadow.offset.y, 1.0);
3059        assert_f32_close(shadow.blur_radius, 2.0);
3060    }
3061
3062    #[test]
3063    fn text_layout_options_does_not_wrap_on_tiny_width_delta() {
3064        let _app_context = crate::render_state::app_context_test_scope();
3065        let style = TextStyle {
3066            span_style: crate::text::SpanStyle {
3067                font_size: TextUnit::Sp(10.0),
3068                ..Default::default()
3069            },
3070            ..Default::default()
3071        };
3072        let options = TextLayoutOptions {
3073            overflow: TextOverflow::Clip,
3074            soft_wrap: true,
3075            max_lines: usize::MAX,
3076            min_lines: 1,
3077        };
3078
3079        let text = "if counter % 2 == 0";
3080        let exact_width = measure_text(&crate::text::AnnotatedString::from(text), &style).width;
3081        let prepared = prepare_text_layout(
3082            &crate::text::AnnotatedString::from(text),
3083            &style,
3084            options,
3085            Some(exact_width - 0.1),
3086        );
3087
3088        assert!(
3089            !prepared.text.text.contains('\n'),
3090            "unexpected line split: {:?}",
3091            prepared.text
3092        );
3093    }
3094
3095    #[test]
3096    fn line_break_mode_changes_wrap_strategy_contract() {
3097        let _app_context = crate::render_state::app_context_test_scope();
3098        let text = "This is an example text";
3099        let options = TextLayoutOptions {
3100            overflow: TextOverflow::Clip,
3101            soft_wrap: true,
3102            max_lines: usize::MAX,
3103            min_lines: 1,
3104        };
3105
3106        let simple = prepare_text_layout(
3107            &crate::text::AnnotatedString::from(text),
3108            &style_with_line_break(LineBreak::Simple),
3109            options,
3110            Some(120.0),
3111        );
3112        let heading = prepare_text_layout(
3113            &crate::text::AnnotatedString::from(text),
3114            &style_with_line_break(LineBreak::Heading),
3115            options,
3116            Some(120.0),
3117        );
3118        let paragraph = prepare_text_layout(
3119            &crate::text::AnnotatedString::from(text),
3120            &style_with_line_break(LineBreak::Paragraph),
3121            options,
3122            Some(50.0),
3123        );
3124
3125        assert_eq!(
3126            simple.text.text.lines().collect::<Vec<_>>(),
3127            vec!["This is an example", "text"]
3128        );
3129        assert_eq!(
3130            heading.text.text.lines().collect::<Vec<_>>(),
3131            vec!["This is an", "example text"]
3132        );
3133        assert_eq!(
3134            paragraph.text.text.lines().collect::<Vec<_>>(),
3135            vec!["This", "is an", "example", "text"]
3136        );
3137    }
3138
3139    #[test]
3140    fn hyphens_mode_changes_wrap_strategy_contract() {
3141        let _app_context = crate::render_state::app_context_test_scope();
3142        let text = "Transformation";
3143        let options = TextLayoutOptions {
3144            overflow: TextOverflow::Clip,
3145            soft_wrap: true,
3146            max_lines: usize::MAX,
3147            min_lines: 1,
3148        };
3149
3150        let auto = prepare_text_layout(
3151            &crate::text::AnnotatedString::from(text),
3152            &style_with_hyphens(Hyphens::Auto),
3153            options,
3154            Some(24.0),
3155        );
3156        let none = prepare_text_layout(
3157            &crate::text::AnnotatedString::from(text),
3158            &style_with_hyphens(Hyphens::None),
3159            options,
3160            Some(24.0),
3161        );
3162
3163        assert_eq!(
3164            auto.text.text.lines().collect::<Vec<_>>(),
3165            vec!["Tran", "sfor", "ma", "tion"]
3166        );
3167        assert_eq!(
3168            none.text.text.lines().collect::<Vec<_>>(),
3169            vec!["Tran", "sfor", "mati", "on"]
3170        );
3171        assert!(
3172            !auto.text.text.contains('-'),
3173            "automatic hyphenation should influence breaks without mutating source text content"
3174        );
3175    }
3176
3177    #[test]
3178    fn hyphens_auto_uses_measurer_hyphen_contract_when_valid() {
3179        let _app_context = crate::render_state::app_context_test_scope();
3180        let text = "Transformation";
3181        let style = style_with_hyphens(Hyphens::Auto);
3182        let options = TextLayoutOptions {
3183            overflow: TextOverflow::Clip,
3184            soft_wrap: true,
3185            max_lines: usize::MAX,
3186            min_lines: 1,
3187        };
3188
3189        let prepared = prepare_text_layout_fallback(
3190            &ContractBreakMeasurer { retreat: 1 },
3191            &crate::text::AnnotatedString::from(text),
3192            &style,
3193            options,
3194            Some(24.0),
3195        );
3196
3197        assert_eq!(
3198            prepared.text.text.lines().collect::<Vec<_>>(),
3199            vec!["Tra", "nsf", "orm", "ati", "on"]
3200        );
3201    }
3202
3203    #[test]
3204    fn hyphens_auto_falls_back_when_measurer_hyphen_contract_is_invalid() {
3205        let _app_context = crate::render_state::app_context_test_scope();
3206        let text = "Transformation";
3207        let style = style_with_hyphens(Hyphens::Auto);
3208        let options = TextLayoutOptions {
3209            overflow: TextOverflow::Clip,
3210            soft_wrap: true,
3211            max_lines: usize::MAX,
3212            min_lines: 1,
3213        };
3214
3215        let prepared = prepare_text_layout_fallback(
3216            &ContractBreakMeasurer { retreat: 10 },
3217            &crate::text::AnnotatedString::from(text),
3218            &style,
3219            options,
3220            Some(24.0),
3221        );
3222
3223        assert_eq!(
3224            prepared.text.text.lines().collect::<Vec<_>>(),
3225            vec!["Tran", "sfor", "ma", "tion"]
3226        );
3227    }
3228
3229    #[test]
3230    fn transformed_text_keeps_span_ranges_within_display_bounds() {
3231        let _app_context = crate::render_state::app_context_test_scope();
3232        let style = TextStyle {
3233            span_style: crate::text::SpanStyle {
3234                font_size: TextUnit::Sp(10.0),
3235                ..Default::default()
3236            },
3237            ..Default::default()
3238        };
3239        let options = TextLayoutOptions {
3240            overflow: TextOverflow::Ellipsis,
3241            soft_wrap: false,
3242            max_lines: 1,
3243            min_lines: 1,
3244        };
3245        let annotated = crate::text::AnnotatedString::builder()
3246            .push_style(crate::text::SpanStyle {
3247                font_weight: Some(crate::text::FontWeight::BOLD),
3248                ..Default::default()
3249            })
3250            .append("Styled overflow text sample")
3251            .pop()
3252            .to_annotated_string();
3253
3254        let prepared = prepare_text_layout(&annotated, &style, options, Some(40.0));
3255        assert!(prepared.did_overflow);
3256        for span in &prepared.text.span_styles {
3257            assert!(span.range.start < span.range.end);
3258            assert!(span.range.end <= prepared.text.text.len());
3259            assert!(prepared.text.text.is_char_boundary(span.range.start));
3260            assert!(prepared.text.text.is_char_boundary(span.range.end));
3261        }
3262    }
3263
3264    #[test]
3265    fn wrapped_text_splits_styles_around_inserted_newlines() {
3266        let _app_context = crate::render_state::app_context_test_scope();
3267        let style = TextStyle {
3268            span_style: crate::text::SpanStyle {
3269                font_size: TextUnit::Sp(10.0),
3270                ..Default::default()
3271            },
3272            ..Default::default()
3273        };
3274        let options = TextLayoutOptions {
3275            overflow: TextOverflow::Clip,
3276            soft_wrap: true,
3277            max_lines: usize::MAX,
3278            min_lines: 1,
3279        };
3280        let annotated = crate::text::AnnotatedString::builder()
3281            .push_style(crate::text::SpanStyle {
3282                text_decoration: Some(crate::text::TextDecoration::UNDERLINE),
3283                ..Default::default()
3284            })
3285            .append("Wrapped style text example")
3286            .pop()
3287            .to_annotated_string();
3288
3289        let prepared = prepare_text_layout(&annotated, &style, options, Some(32.0));
3290        assert!(prepared.text.text.contains('\n'));
3291        assert!(!prepared.text.span_styles.is_empty());
3292        for span in &prepared.text.span_styles {
3293            assert!(span.range.end <= prepared.text.text.len());
3294        }
3295    }
3296
3297    #[test]
3298    fn mixed_font_size_segments_wrap_without_truncation() {
3299        let _app_context = crate::render_state::app_context_test_scope();
3300        let style = TextStyle {
3301            span_style: crate::text::SpanStyle {
3302                font_size: TextUnit::Sp(14.0),
3303                ..Default::default()
3304            },
3305            ..Default::default()
3306        };
3307        let options = TextLayoutOptions {
3308            overflow: TextOverflow::Clip,
3309            soft_wrap: true,
3310            max_lines: usize::MAX,
3311            min_lines: 1,
3312        };
3313        let annotated = crate::text::AnnotatedString::builder()
3314            .append("You can also ")
3315            .push_style(crate::text::SpanStyle {
3316                font_size: TextUnit::Sp(22.0),
3317                ..Default::default()
3318            })
3319            .append("change font size")
3320            .pop()
3321            .append(" dynamically mid-sentence!")
3322            .to_annotated_string();
3323
3324        let prepared = prepare_text_layout(&annotated, &style, options, Some(260.0));
3325        assert!(prepared.text.text.contains('\n'));
3326        assert!(prepared.text.text.contains("mid-sentence!"));
3327        assert!(!prepared.did_overflow);
3328    }
3329}