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