Skip to main content

cranpose_ui/text/
measure.rs

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