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