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