Skip to main content

cranpose_ui/text/
measure.rs

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