Skip to main content

cranpose_ui/text/
measure.rs

1use std::{
2    borrow::Cow,
3    cell::{Cell, RefCell},
4    collections::{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;
28/// Prepared layouts kept across nodes. Each node keeps its own, so this
29/// serves the items a list composes again, a few screens of them; an entry
30/// holds a whole visual style, and a screen laid out again at a new width
31/// every frame fills every entry with widths it never asks for again.
32const TEXT_PREPARED_CACHE_CAPACITY: usize = 1024;
33const TEXT_LAYOUT_TELEMETRY_ENV: &str = "CRANPOSE_TEXT_LAYOUT_TELEMETRY";
34
35fn text_layout_telemetry_enabled() -> bool {
36    cranpose_core::env_flag!(TEXT_LAYOUT_TELEMETRY_ENV)
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct TextMetrics {
41    pub width: f32,
42    pub height: f32,
43    /// Height of a single line of text
44    pub line_height: f32,
45    /// Number of lines in the text
46    pub line_count: usize,
47}
48
49#[derive(Clone, Debug, PartialEq)]
50pub struct PreparedTextLayout {
51    /// Shared display text after wrapping and overflow have been resolved.
52    pub text: Rc<crate::text::AnnotatedString>,
53    pub visual_style: TextStyle,
54    pub metrics: TextMetrics,
55    pub did_overflow: bool,
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct TextLinePrefixWidths {
60    prefix_widths: Vec<f32>,
61    separator_before: Vec<f32>,
62    non_empty_overhang: f32,
63}
64
65impl TextLinePrefixWidths {
66    pub fn from_parts(
67        prefix_widths: Vec<f32>,
68        separator_before: Vec<f32>,
69        non_empty_overhang: f32,
70    ) -> Option<Self> {
71        if prefix_widths.is_empty() || prefix_widths.len() != separator_before.len() + 1 {
72            return None;
73        }
74        if prefix_widths
75            .iter()
76            .chain(separator_before.iter())
77            .any(|value| !value.is_finite())
78        {
79            return None;
80        }
81        let non_empty_overhang = non_empty_overhang.max(0.0);
82        if !non_empty_overhang.is_finite() {
83            return None;
84        }
85        Some(Self {
86            prefix_widths,
87            separator_before,
88            non_empty_overhang,
89        })
90    }
91
92    pub fn monospaced(char_count: usize, char_width: f32, letter_spacing: f32) -> Option<Self> {
93        if !char_width.is_finite() || !letter_spacing.is_finite() {
94            return None;
95        }
96        let char_width = char_width.max(0.0);
97        let letter_spacing = letter_spacing.max(0.0);
98        let mut prefix_widths = Vec::with_capacity(char_count + 1);
99        let mut separator_before = Vec::with_capacity(char_count);
100        let mut width = 0.0f32;
101        prefix_widths.push(width);
102        for _ in 0..char_count {
103            separator_before.push(0.0);
104            width += char_width + letter_spacing;
105            prefix_widths.push(width);
106        }
107        Self::from_parts(prefix_widths, separator_before, 0.0)
108    }
109
110    pub fn char_count(&self) -> usize {
111        self.separator_before.len()
112    }
113
114    pub fn width_for_char_range(&self, start: usize, end: usize) -> Option<f32> {
115        if start > end || end > self.char_count() {
116            return None;
117        }
118        if start == end {
119            return Some(0.0);
120        }
121        let separator = self.separator_before.get(start).copied().unwrap_or(0.0);
122        Some(
123            (self.prefix_widths[end] - self.prefix_widths[start] - separator).max(0.0)
124                + self.non_empty_overhang,
125        )
126    }
127}
128
129pub trait TextMeasurer: 'static {
130    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics;
131
132    fn measure_for_node(
133        &self,
134        node_id: Option<NodeId>,
135        text: &crate::text::AnnotatedString,
136        style: &TextStyle,
137    ) -> TextMetrics {
138        let _ = node_id;
139        self.measure(text, style)
140    }
141
142    fn measure_subsequence(
143        &self,
144        text: &crate::text::AnnotatedString,
145        range: Range<usize>,
146        style: &TextStyle,
147    ) -> TextMetrics {
148        self.measure(&text.subsequence(range), style)
149    }
150
151    fn measure_subsequence_for_node(
152        &self,
153        node_id: Option<NodeId>,
154        text: &crate::text::AnnotatedString,
155        range: Range<usize>,
156        style: &TextStyle,
157    ) -> TextMetrics {
158        let _ = node_id;
159        self.measure_subsequence(text, range, style)
160    }
161
162    fn measure_line_prefix_widths(
163        &self,
164        text: &crate::text::AnnotatedString,
165        line_range: Range<usize>,
166        style: &TextStyle,
167    ) -> Option<TextLinePrefixWidths> {
168        let _ = text;
169        let _ = line_range;
170        let _ = style;
171        None
172    }
173
174    fn measure_line_width(
175        &self,
176        text: &crate::text::AnnotatedString,
177        line_range: Range<usize>,
178        style: &TextStyle,
179    ) -> Option<f32> {
180        let _ = text;
181        let _ = line_range;
182        let _ = style;
183        None
184    }
185
186    fn line_height(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
187        self.measure(text, style).line_height
188    }
189
190    /// The tight glyph box of a text line inside its `line_height` slot:
191    /// `(top_offset, height)` in logical units, where `height` is the font's
192    /// natural ascent+descent extent and `top_offset` positions it within the
193    /// slot (glyph rows are vertically centered). Selection chrome — the
194    /// highlight, the caret and the finger handles — anchors to THIS box,
195    /// not the full slot: with a paragraph line height above the natural one
196    /// the reference shows gaps between highlighted lines and handles riding
197    /// the glyphs. `None` means the box fills the slot.
198    fn glyph_line_box(&self, style: &TextStyle) -> Option<(f32, f32)> {
199        let _ = style;
200        None
201    }
202
203    /// Distance from the top of a line slot down to that line's baseline, in
204    /// logical units — the number the rasterizer places glyph origins at.
205    ///
206    /// Callers that position text by baseline (rather than by its box) need
207    /// this; `None` means the measurer has no font metrics to answer with.
208    fn first_baseline(&self, style: &TextStyle) -> Option<f32> {
209        let _ = style;
210        None
211    }
212
213    /// One line's whole box for a style: its advance, its baseline and what a
214    /// paragraph of it gives back at its edges, with no string to measure.
215    ///
216    /// A layout that stacks rows of a known style needs the row pitch before it
217    /// has any text to put in them, and a paragraph's height and first baseline
218    /// both depend on the edges. `None` when the measurer has no font metrics.
219    fn line_box(&self, style: &TextStyle) -> Option<crate::text::LineBox> {
220        let _ = style;
221        None
222    }
223
224    fn line_height_for_node(
225        &self,
226        node_id: Option<NodeId>,
227        text: &crate::text::AnnotatedString,
228        style: &TextStyle,
229    ) -> f32 {
230        let _ = node_id;
231        self.line_height(text, style)
232    }
233
234    fn get_offset_for_position(
235        &self,
236        text: &crate::text::AnnotatedString,
237        style: &TextStyle,
238        x: f32,
239        y: f32,
240    ) -> usize;
241
242    fn get_cursor_x_for_offset(
243        &self,
244        text: &crate::text::AnnotatedString,
245        style: &TextStyle,
246        offset: usize,
247    ) -> f32;
248
249    fn layout(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult;
250
251    /// Returns an alternate break boundary for `Hyphens::Auto` when a greedy break
252    /// split lands in the middle of a word.
253    ///
254    /// `segment_start_char` and `measured_break_char` are character-boundary indices
255    /// in `line` (not byte offsets). Return `None` to delegate to fallback behavior.
256    fn choose_auto_hyphen_break(
257        &self,
258        _line: &str,
259        _style: &TextStyle,
260        _segment_start_char: usize,
261        _measured_break_char: usize,
262    ) -> Option<usize> {
263        None
264    }
265
266    fn measure_with_options(
267        &self,
268        text: &crate::text::AnnotatedString,
269        style: &TextStyle,
270        options: TextLayoutOptions,
271        max_width: Option<f32>,
272    ) -> TextMetrics {
273        self.prepare_with_options(text, style, options, max_width)
274            .metrics
275    }
276
277    fn measure_with_options_for_node(
278        &self,
279        node_id: Option<NodeId>,
280        text: &crate::text::AnnotatedString,
281        style: &TextStyle,
282        options: TextLayoutOptions,
283        max_width: Option<f32>,
284    ) -> TextMetrics {
285        self.prepare_with_options_for_node(node_id, text, style, options, max_width)
286            .metrics
287    }
288
289    fn prepare_with_options(
290        &self,
291        text: &crate::text::AnnotatedString,
292        style: &TextStyle,
293        options: TextLayoutOptions,
294        max_width: Option<f32>,
295    ) -> PreparedTextLayout {
296        self.prepare_with_options_fallback(text, style, options, max_width)
297    }
298
299    fn prepare_with_options_for_node(
300        &self,
301        node_id: Option<NodeId>,
302        text: &crate::text::AnnotatedString,
303        style: &TextStyle,
304        options: TextLayoutOptions,
305        max_width: Option<f32>,
306    ) -> PreparedTextLayout {
307        prepare_text_layout_with_measurer_for_node(self, node_id, text, style, options, max_width)
308    }
309
310    fn prepare_with_options_fallback(
311        &self,
312        text: &crate::text::AnnotatedString,
313        style: &TextStyle,
314        options: TextLayoutOptions,
315        max_width: Option<f32>,
316    ) -> PreparedTextLayout {
317        prepare_text_layout_fallback(self, text, style, options, max_width)
318    }
319}
320
321#[derive(Default)]
322struct MonospacedTextMeasurer;
323
324impl MonospacedTextMeasurer {
325    const DEFAULT_SIZE: f32 = 14.0;
326    const CHAR_WIDTH_RATIO: f32 = 0.6;
327
328    fn get_metrics(style: &TextStyle) -> (f32, f32) {
329        let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
330        let line_height = style.resolve_line_height(Self::DEFAULT_SIZE, font_size);
331        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE).max(0.0);
332        (
333            (font_size * Self::CHAR_WIDTH_RATIO) + letter_spacing,
334            line_height,
335        )
336    }
337}
338
339impl TextMeasurer for MonospacedTextMeasurer {
340    fn measure(&self, text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
341        let (char_width, line_height) = Self::get_metrics(style);
342
343        let lines: Vec<&str> = text.text.split('\n').collect();
344        let line_count = lines.len().max(1);
345
346        let width = lines
347            .iter()
348            .map(|line| line.chars().count() as f32 * char_width)
349            .fold(0.0_f32, f32::max);
350
351        TextMetrics {
352            width,
353            height: line_count as f32 * line_height,
354            line_height,
355            line_count,
356        }
357    }
358
359    fn measure_subsequence(
360        &self,
361        text: &crate::text::AnnotatedString,
362        range: Range<usize>,
363        style: &TextStyle,
364    ) -> TextMetrics {
365        let (char_width, line_height) = Self::get_metrics(style);
366        let slice = &text.text[range];
367        let line_count = slice.split('\n').count().max(1);
368        let width = slice
369            .split('\n')
370            .map(|line| line.chars().count() as f32 * char_width)
371            .fold(0.0_f32, f32::max);
372
373        TextMetrics {
374            width,
375            height: line_count as f32 * line_height,
376            line_height,
377            line_count,
378        }
379    }
380
381    fn measure_line_prefix_widths(
382        &self,
383        text: &crate::text::AnnotatedString,
384        line_range: Range<usize>,
385        style: &TextStyle,
386    ) -> Option<TextLinePrefixWidths> {
387        let font_size = style.resolve_font_size(Self::DEFAULT_SIZE);
388        let letter_spacing = style.resolve_letter_spacing(Self::DEFAULT_SIZE);
389        TextLinePrefixWidths::monospaced(
390            text.text[line_range].chars().count(),
391            font_size * Self::CHAR_WIDTH_RATIO,
392            letter_spacing,
393        )
394    }
395
396    fn measure_line_width(
397        &self,
398        text: &crate::text::AnnotatedString,
399        line_range: Range<usize>,
400        style: &TextStyle,
401    ) -> Option<f32> {
402        Some(self.measure_subsequence(text, line_range, style).width)
403    }
404
405    fn line_height(&self, _text: &crate::text::AnnotatedString, style: &TextStyle) -> f32 {
406        let (_, line_height) = Self::get_metrics(style);
407        line_height
408    }
409
410    fn get_offset_for_position(
411        &self,
412        text: &crate::text::AnnotatedString,
413        style: &TextStyle,
414        x: f32,
415        y: f32,
416    ) -> usize {
417        let (char_width, line_height) = Self::get_metrics(style);
418
419        if text.text.is_empty() {
420            return 0;
421        }
422
423        let line_index = (y / line_height).floor().max(0.0) as usize;
424        let lines: Vec<&str> = text.text.split('\n').collect();
425        let target_line = line_index.min(lines.len().saturating_sub(1));
426
427        let mut line_start_byte = 0;
428        for line in lines.iter().take(target_line) {
429            line_start_byte += line.len() + 1;
430        }
431
432        let line_text = lines.get(target_line).unwrap_or(&"");
433        let char_index = (x / char_width).round() as usize;
434        let line_char_count = line_text.chars().count();
435        let clamped_index = char_index.min(line_char_count);
436
437        let offset_in_line = line_text
438            .char_indices()
439            .nth(clamped_index)
440            .map_or(line_text.len(), |(i, _)| i);
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: cranpose_core::collections::map::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: cranpose_core::collections::map::HashMap::default(),
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, Rc<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_PREPARED_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    /// The layout of `text` at `max_width`, shared with the cache: a layout
609    /// animated through widths misses on every frame, and copying each one
610    /// into and out of the cache cost more than laying it out.
611    pub(crate) fn prepare_with_options(
612        &self,
613        node_id: Option<NodeId>,
614        text: &crate::text::AnnotatedString,
615        style: &TextStyle,
616        options: TextLayoutOptions,
617        max_width: Option<f32>,
618    ) -> Rc<PreparedTextLayout> {
619        let metrics_key = text_options_cache_key(text, style, options.normalized(), max_width);
620        let key = TextPreparedCacheKey {
621            base: metrics_key,
622            visual_hash: style.render_hash(),
623        };
624        if let Some(prepared) = self.prepared_cache.borrow().get(&key) {
625            return prepared;
626        }
627        let prepared = Rc::new(self.with_measurer(|m| {
628            m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
629        }));
630        self.prepared_cache
631            .borrow_mut()
632            .insert(key, Rc::clone(&prepared));
633        self.options_metrics_cache
634            .borrow_mut()
635            .insert(metrics_key, prepared.metrics);
636        prepared
637    }
638
639    pub(crate) fn layout(
640        &self,
641        text: &crate::text::AnnotatedString,
642        style: &TextStyle,
643    ) -> TextLayoutResult {
644        let key = text_base_cache_key(text, style);
645        if let Some(layout) = self.layout_cache.borrow().get(&key) {
646            return layout;
647        }
648        let layout = self.with_measurer(|m| m.layout(text, style));
649        self.layout_cache.borrow_mut().insert(key, layout.clone());
650        layout
651    }
652
653    fn clear_caches(&self) {
654        self.generation
655            .set(self.generation.get().wrapping_add(1).max(1));
656        self.metrics_cache.borrow_mut().clear();
657        self.options_metrics_cache.borrow_mut().clear();
658        self.prepared_cache.borrow_mut().clear();
659        self.layout_cache.borrow_mut().clear();
660    }
661}
662
663fn text_base_cache_key(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextBaseCacheKey {
664    TextBaseCacheKey {
665        text_hash: text.render_hash(),
666        style_hash: style.measurement_hash(),
667    }
668}
669
670fn text_options_cache_key(
671    text: &crate::text::AnnotatedString,
672    style: &TextStyle,
673    options: TextLayoutOptions,
674    max_width: Option<f32>,
675) -> TextOptionsCacheKey {
676    TextOptionsCacheKey {
677        base: text_base_cache_key(text, style),
678        options: options.normalized(),
679        max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
680    }
681}
682
683pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
684    crate::render_state::set_current_text_measurer(Rc::new(measurer));
685}
686
687pub(crate) fn current_text_generation() -> u64 {
688    crate::render_state::with_text_service(TextService::generation)
689}
690
691pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
692    with_system_font_scale(text, style, |text, style| {
693        crate::render_state::with_text_service(|service| service.measure(None, text, style))
694    })
695}
696
697pub(crate) fn measure_resolved_text(
698    text: &crate::text::AnnotatedString,
699    style: &TextStyle,
700) -> TextMetrics {
701    crate::render_state::with_text_service(|service| service.measure(None, text, style))
702}
703
704pub(crate) fn resolved_first_baseline(style: &TextStyle) -> Option<f32> {
705    crate::render_state::with_text_service(|service| {
706        service.with_measurer(|m| m.first_baseline(style))
707    })
708}
709
710pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
711    crate::render_state::current_app_context()?;
712    crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(style)))
713}
714
715/// The tight glyph box `(top_offset, height)` of a `style` text line inside
716/// its line slot (see [`TextMeasurer::glyph_line_box`]). Falls back to the
717/// full slot when the active measurer has no font metrics.
718pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
719    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
720    crate::render_state::with_text_service(|service| {
721        service.with_measurer(|m| m.glyph_line_box(&style))
722    })
723    .map_or((0.0, line_height), |(off, h)| {
724        (off.min(line_height), h.min(line_height))
725    })
726}
727
728/// The paragraph line box of `style` (see [`TextMeasurer::line_box`]): its
729/// advance, its baseline and what a paragraph gives back at its edges. `None`
730/// when the active measurer carries no font metrics.
731pub fn text_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
732    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
733    crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(&style)))
734}
735
736/// Distance from the top of a `style` line slot down to its baseline (see
737/// [`TextMeasurer::first_baseline`]). `None` when the active measurer carries
738/// no font metrics.
739pub fn first_baseline(style: &TextStyle) -> Option<f32> {
740    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
741    crate::render_state::with_text_service(|service| {
742        service.with_measurer(|m| m.first_baseline(&style))
743    })
744}
745
746pub fn measure_text_for_node(
747    node_id: Option<NodeId>,
748    text: &crate::text::AnnotatedString,
749    style: &TextStyle,
750) -> TextMetrics {
751    with_system_font_scale(text, style, |text, style| {
752        crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
753    })
754}
755
756pub fn measure_text_with_options(
757    text: &crate::text::AnnotatedString,
758    style: &TextStyle,
759    options: TextLayoutOptions,
760    max_width: Option<f32>,
761) -> TextMetrics {
762    with_system_font_scale(text, style, |text, style| {
763        crate::render_state::with_text_service(|service| {
764            service.measure_with_options(None, text, style, options.normalized(), max_width)
765        })
766    })
767}
768
769pub fn measure_text_with_options_for_node(
770    node_id: Option<NodeId>,
771    text: &crate::text::AnnotatedString,
772    style: &TextStyle,
773    options: TextLayoutOptions,
774    max_width: Option<f32>,
775) -> TextMetrics {
776    with_system_font_scale(text, style, |text, style| {
777        crate::render_state::with_text_service(|service| {
778            service.measure_with_options(node_id, text, style, options.normalized(), max_width)
779        })
780    })
781}
782
783pub fn prepare_text_layout(
784    text: &crate::text::AnnotatedString,
785    style: &TextStyle,
786    options: TextLayoutOptions,
787    max_width: Option<f32>,
788) -> PreparedTextLayout {
789    Rc::unwrap_or_clone(prepare_text_layout_for_node(
790        None, text, style, options, max_width,
791    ))
792}
793
794pub fn prepare_text_layout_for_node(
795    node_id: Option<NodeId>,
796    text: &crate::text::AnnotatedString,
797    style: &TextStyle,
798    options: TextLayoutOptions,
799    max_width: Option<f32>,
800) -> Rc<PreparedTextLayout> {
801    with_system_font_scale(text, style, |text, style| {
802        crate::render_state::with_text_service(|service| {
803            service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
804        })
805    })
806}
807
808pub fn get_offset_for_position(
809    text: &crate::text::AnnotatedString,
810    style: &TextStyle,
811    x: f32,
812    y: f32,
813) -> usize {
814    with_system_font_scale(text, style, |text, style| {
815        crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
816    })
817}
818
819/// Byte offset nearest the local content position (`x`, `y`), **wrap-aware** —
820/// the single hit-test every editable-text pointer path uses (tap-to-place,
821/// drag-select, and selection-handle drag).
822///
823/// It is the inverse of the drawn caret and [`wrapped_line_ranges`]: `y` selects
824/// the VISUAL (wrapped) line (`floor(y / line_height)`), then `x` picks the
825/// nearest char boundary WITHIN that line (delegated to the measurer with `y`
826/// forced to 0). `x`/`y` must already be in text space (padding- and
827/// pan-adjusted). The plain [`get_offset_for_position`] maps `y` through the
828/// measurer's logical `\n` layout, so on wrapped text it lands on the wrong line
829/// (an error that grows with each wrapped line above the finger). With
830/// `wrap_width == None` (single-line fields) this reduces to the one logical
831/// line.
832pub fn offset_for_position_wrapped(
833    text: &str,
834    style: &TextStyle,
835    node_id: Option<NodeId>,
836    wrap_width: Option<f32>,
837    line_height: f32,
838    x: f32,
839    y: f32,
840) -> usize {
841    if text.is_empty() {
842        return 0;
843    }
844    let annotated = crate::text::AnnotatedString::from(text);
845    let line_ranges = wrapped_line_ranges(
846        node_id,
847        &annotated,
848        style,
849        TextLayoutOptions::default(),
850        wrap_width,
851    );
852    if line_ranges.is_empty() {
853        return 0;
854    }
855    let line_idx = if line_height > 0.0 {
856        (y / line_height).floor().max(0.0) as usize
857    } else {
858        0
859    }
860    .min(line_ranges.len() - 1);
861    let range = &line_ranges[line_idx];
862    let line = &text[range.start..range.end];
863    let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
864    range.start + within.min(line.len())
865}
866
867pub fn get_cursor_x_for_offset(
868    text: &crate::text::AnnotatedString,
869    style: &TextStyle,
870    offset: usize,
871) -> f32 {
872    with_system_font_scale(text, style, |text, style| {
873        crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
874    })
875}
876
877pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
878    with_system_font_scale(text, style, |text, style| {
879        crate::render_state::with_text_service(|service| service.layout(text, style))
880    })
881}
882
883/// Returns the source-text byte range covered by each **visual** (wrapped) line
884/// when `text` is laid out at `max_width` with `options`, matching the wrapping
885/// the renderer performs. Each range excludes the trailing `\n`. With
886/// `max_width == None` (or soft-wrap disabled) this is just the logical
887/// `\n`-delimited lines.
888///
889/// The text field uses this to place its caret and selection handles on the
890/// correct visual line for wrapped text: the in-content caret otherwise counts
891/// only logical `\n` lines, so a caret on a wrapped line's second visual line is
892/// drawn on the first (and its x, being the whole logical-line prefix width,
893/// runs off the right edge and is clipped), while typing and the magnifier land
894/// on the correct spot.
895pub fn wrapped_line_ranges(
896    node_id: Option<NodeId>,
897    text: &crate::text::AnnotatedString,
898    style: &TextStyle,
899    options: TextLayoutOptions,
900    max_width: Option<f32>,
901) -> Vec<Range<usize>> {
902    with_system_font_scale(text, style, |text, style| {
903        crate::render_state::with_text_measurer(|m| {
904            wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
905        })
906    })
907}
908
909fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
910    measurer: &M,
911    _node_id: Option<NodeId>,
912    text: &crate::text::AnnotatedString,
913    style: &TextStyle,
914    options: TextLayoutOptions,
915    max_width: Option<f32>,
916) -> Vec<Range<usize>> {
917    let opts = options.normalized();
918    let max_width = normalize_max_width(max_width);
919    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
920        .then_some(max_width)
921        .flatten();
922    let line_break_mode = style
923        .paragraph_style
924        .line_break
925        .take_or_else(|| LineBreak::Simple);
926    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
927
928    let line_ranges = split_line_ranges(text.text.as_str());
929    let Some(width_limit) = wrap_width else {
930        return line_ranges;
931    };
932    let mut ranges = Vec::with_capacity(line_ranges.len());
933    for line_range in line_ranges {
934        for display_line in wrap_line_to_width(
935            measurer,
936            text,
937            line_range,
938            style,
939            width_limit,
940            line_break_mode,
941            hyphens_mode,
942        ) {
943            ranges.push(display_line.source_range.clone());
944        }
945    }
946    ranges
947}
948
949fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
950    measurer: &M,
951    text: &crate::text::AnnotatedString,
952    style: &TextStyle,
953    options: TextLayoutOptions,
954    max_width: Option<f32>,
955) -> PreparedTextLayout {
956    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
957}
958
959pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
960    measurer: &M,
961    node_id: Option<NodeId>,
962    text: &crate::text::AnnotatedString,
963    style: &TextStyle,
964    options: TextLayoutOptions,
965    max_width: Option<f32>,
966) -> PreparedTextLayout {
967    let telemetry = text_layout_telemetry_enabled();
968    let total_start = telemetry.then(Instant::now);
969    let opts = options.normalized();
970    let max_width = normalize_max_width(max_width);
971    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
972        return prepare_scale_down_text_layout(
973            measurer,
974            node_id,
975            text,
976            style,
977            opts,
978            max_width,
979            min_font_size_sp,
980        );
981    }
982
983    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
984        .then_some(max_width)
985        .flatten();
986    let line_break_mode = style
987        .paragraph_style
988        .line_break
989        .take_or_else(|| LineBreak::Simple);
990    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
991
992    let wrap_start = telemetry.then(Instant::now);
993    let line_ranges = split_line_ranges(text.text.as_str());
994    let source_line_count = line_ranges.len();
995    let mut visible_lines: Vec<DisplayLine>;
996    if let Some(width_limit) = wrap_width {
997        visible_lines = Vec::with_capacity(line_ranges.len());
998        for line_range in line_ranges {
999            let wrapped_lines = wrap_line_to_width(
1000                measurer,
1001                text,
1002                line_range,
1003                style,
1004                width_limit,
1005                line_break_mode,
1006                hyphens_mode,
1007            );
1008            visible_lines.extend(wrapped_lines);
1009        }
1010    } else {
1011        visible_lines = line_ranges
1012            .into_iter()
1013            .map(DisplayLine::from_source_range)
1014            .collect();
1015    }
1016    let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1017
1018    let overflow_start = telemetry.then(Instant::now);
1019    let did_overflow = apply_overflow(
1020        measurer,
1021        node_id,
1022        text,
1023        style,
1024        opts,
1025        max_width,
1026        &mut visible_lines,
1027    );
1028    let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1029
1030    let build_start = telemetry.then(Instant::now);
1031    let display_annotated = build_display_annotated(text, &visible_lines);
1032    debug_assert_eq!(
1033        display_annotated.text,
1034        join_display_line_text(text, &visible_lines)
1035    );
1036    let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1037
1038    let metrics_start = telemetry.then(Instant::now);
1039    let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1040    let display_line_count = visible_lines.len().max(1);
1041    let layout_line_count = display_line_count.max(opts.min_lines);
1042
1043    let measured_width = if visible_lines.is_empty() {
1044        0.0
1045    } else {
1046        visible_lines
1047            .iter()
1048            .map(|line| line.measure_width(measurer, node_id, text, style))
1049            .fold(0.0_f32, f32::max)
1050    };
1051    let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1052    let width = if opts.overflow == TextOverflow::Visible {
1053        measured_width
1054    } else if let Some(width_limit) = max_width {
1055        measured_width.min(width_limit)
1056    } else {
1057        measured_width
1058    };
1059
1060    let edges = measurer
1061        .line_box(style)
1062        .unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1063    let prepared = PreparedTextLayout {
1064        text: Rc::new(display_annotated),
1065        visual_style: style.clone(),
1066        metrics: TextMetrics {
1067            width,
1068            height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1069                .max(0.0),
1070            line_height,
1071            line_count: layout_line_count,
1072        },
1073        did_overflow,
1074    };
1075
1076    if let Some(start) = total_start {
1077        eprintln!(
1078            "[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}",
1079            text.text.len(),
1080            text.span_styles.len(),
1081            source_line_count,
1082            display_line_count,
1083            wrap_width.is_some(),
1084            max_width,
1085            wrap_ms.unwrap_or(0.0),
1086            overflow_ms.unwrap_or(0.0),
1087            build_ms.unwrap_or(0.0),
1088            metrics_ms.unwrap_or(0.0),
1089            start.elapsed().as_secs_f64() * 1000.0,
1090        );
1091    }
1092
1093    prepared
1094}
1095
1096fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1097    measurer: &M,
1098    node_id: Option<NodeId>,
1099    text: &crate::text::AnnotatedString,
1100    style: &TextStyle,
1101    options: TextLayoutOptions,
1102    max_width: Option<f32>,
1103    min_font_size_sp: f32,
1104) -> PreparedTextLayout {
1105    let clipped_options = TextLayoutOptions {
1106        overflow: TextOverflow::Clip,
1107        ..options
1108    }
1109    .normalized();
1110
1111    let full_size = prepare_scaled_text_layout(
1112        measurer,
1113        node_id,
1114        text,
1115        style,
1116        clipped_options,
1117        max_width,
1118        FontScaleCurve::linear(1.0),
1119    );
1120    let Some(width_limit) = max_width else {
1121        return full_size;
1122    };
1123    if !full_size.did_overflow {
1124        return full_size;
1125    }
1126
1127    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1128    if !base_font_size.is_finite() || base_font_size <= 0.0 {
1129        return full_size;
1130    }
1131    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1132    if min_scale >= 1.0 {
1133        return full_size;
1134    }
1135
1136    let min_size = prepare_scaled_text_layout(
1137        measurer,
1138        node_id,
1139        text,
1140        style,
1141        clipped_options,
1142        Some(width_limit),
1143        FontScaleCurve::linear(min_scale),
1144    );
1145    if min_size.did_overflow {
1146        return min_size;
1147    }
1148
1149    let mut low = min_scale;
1150    let mut high = 1.0;
1151    let mut best = min_size;
1152    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1153        let mid = (low + high) * 0.5;
1154        let candidate = prepare_scaled_text_layout(
1155            measurer,
1156            node_id,
1157            text,
1158            style,
1159            clipped_options,
1160            Some(width_limit),
1161            FontScaleCurve::linear(mid),
1162        );
1163        if candidate.did_overflow {
1164            high = mid;
1165        } else {
1166            low = mid;
1167            best = candidate;
1168        }
1169    }
1170
1171    best
1172}
1173
1174fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1175    measurer: &M,
1176    node_id: Option<NodeId>,
1177    text: &crate::text::AnnotatedString,
1178    style: &TextStyle,
1179    options: TextLayoutOptions,
1180    max_width: Option<f32>,
1181    shrink: FontScaleCurve,
1182) -> PreparedTextLayout {
1183    let visual_style = scale_text_style_font_sizes(style, shrink);
1184    let visual_text = scale_annotated_font_sizes(text, shrink);
1185    prepare_text_layout_with_measurer_for_node(
1186        measurer,
1187        node_id,
1188        visual_text.as_ref(),
1189        &visual_style,
1190        options,
1191        max_width,
1192    )
1193}
1194
1195fn scale_annotated_font_sizes(
1196    text: &crate::text::AnnotatedString,
1197    curve: FontScaleCurve,
1198) -> Cow<'_, crate::text::AnnotatedString> {
1199    if curve.is_identity() || !annotated_text_needs_scaling(text) {
1200        return Cow::Borrowed(text);
1201    }
1202
1203    let mut scaled = text.clone();
1204    for span in &mut scaled.span_styles {
1205        span.item = scale_span_style_font_sizes(&span.item, curve, None);
1206    }
1207    Cow::Owned(scaled)
1208}
1209
1210fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1211    if curve.is_identity() {
1212        return style.clone();
1213    }
1214
1215    let mut scaled = style.clone();
1216    scaled.span_style =
1217        scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1218    scaled.paragraph_style.line_height =
1219        scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1220    if let Some(mut indent) = scaled.paragraph_style.text_indent {
1221        indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1222        indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1223        scaled.paragraph_style.text_indent = Some(indent);
1224    }
1225    scaled
1226}
1227
1228fn with_system_font_scale<R>(
1229    text: &crate::text::AnnotatedString,
1230    style: &TextStyle,
1231    block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1232) -> R {
1233    let curve = crate::current_font_scale_curve();
1234    let visual_style = scale_text_style_font_sizes(style, curve);
1235    let visual_text = scale_annotated_font_sizes(text, curve);
1236    block(visual_text.as_ref(), &visual_style)
1237}
1238
1239fn scale_span_style_font_sizes(
1240    style: &crate::text::SpanStyle,
1241    curve: FontScaleCurve,
1242    default_font_size_sp: Option<f32>,
1243) -> crate::text::SpanStyle {
1244    let factor = curve.scale();
1245    let mut scaled = style.clone();
1246    scaled.font_size = match (style.font_size, default_font_size_sp) {
1247        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1248            crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1249        }
1250        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1251        (unit, None) => scale_text_unit_sp(unit, curve),
1252    };
1253    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1254    if let Some(mut shadow) = scaled.shadow {
1255        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1256        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1257        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1258        scaled.shadow = Some(shadow);
1259    }
1260    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1261        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1262            width: width * factor,
1263        });
1264    }
1265    scaled
1266}
1267
1268fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1269    text.span_styles
1270        .iter()
1271        .any(|span| span_style_needs_scaling(&span.item))
1272}
1273
1274fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1275    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1276        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1277        || matches!(
1278            style.draw_style,
1279            Some(crate::text::TextDrawStyle::Stroke { .. })
1280        )
1281        || style.shadow.is_some()
1282}
1283
1284fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1285    match unit {
1286        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1287            crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1288        }
1289        other => other,
1290    }
1291}
1292
1293fn scale_text_unit_sp_and_em(
1294    unit: crate::text::TextUnit,
1295    curve: FontScaleCurve,
1296) -> crate::text::TextUnit {
1297    match unit {
1298        crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1299        crate::text::TextUnit::Em(value) if value.is_finite() => {
1300            crate::text::TextUnit::Em(value * curve.scale())
1301        }
1302        other => other,
1303    }
1304}
1305
1306fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1307    if value.is_finite() {
1308        value * factor
1309    } else {
1310        value
1311    }
1312}
1313
1314#[derive(Clone, Debug)]
1315enum DisplayLineText {
1316    Source,
1317    Ellipsized(crate::text::AnnotatedString),
1318}
1319
1320#[derive(Clone, Debug)]
1321struct DisplayLine {
1322    source_range: Range<usize>,
1323    text: DisplayLineText,
1324    measured_width: Option<f32>,
1325}
1326
1327impl DisplayLine {
1328    fn from_source_range(source_range: Range<usize>) -> Self {
1329        Self {
1330            source_range,
1331            text: DisplayLineText::Source,
1332            measured_width: None,
1333        }
1334    }
1335
1336    fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1337        Self {
1338            source_range,
1339            text: DisplayLineText::Source,
1340            measured_width: measured_width
1341                .is_finite()
1342                .then_some(measured_width.max(0.0)),
1343        }
1344    }
1345
1346    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1347        match &self.text {
1348            DisplayLineText::Source => &source.text[self.source_range.clone()],
1349            DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1350        }
1351    }
1352
1353    fn measure_width<M: TextMeasurer + ?Sized>(
1354        &self,
1355        measurer: &M,
1356        node_id: Option<NodeId>,
1357        source: &crate::text::AnnotatedString,
1358        style: &TextStyle,
1359    ) -> f32 {
1360        self.measured_width.unwrap_or_else(|| match &self.text {
1361            DisplayLineText::Source => {
1362                measurer
1363                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1364                    .width
1365            }
1366            DisplayLineText::Ellipsized(annotated) => {
1367                measurer.measure_for_node(node_id, annotated, style).width
1368            }
1369        })
1370    }
1371
1372    fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1373        let start = self.source_range.start;
1374        let end = source.text[start..]
1375            .find('\n')
1376            .map_or(source.text.len(), |offset| start + offset);
1377        self.source_range = start..end;
1378        self.text = DisplayLineText::Source;
1379        self.measured_width = None;
1380    }
1381
1382    fn ellipsize<M: TextMeasurer + ?Sized>(
1383        &mut self,
1384        measurer: &M,
1385        node_id: Option<NodeId>,
1386        source: &crate::text::AnnotatedString,
1387        style: &TextStyle,
1388        max_width: Option<f32>,
1389        placement: EllipsisPlacement,
1390    ) {
1391        *self = fit_ellipsis(
1392            measurer,
1393            node_id,
1394            source,
1395            self.source_range.clone(),
1396            style,
1397            max_width,
1398            placement,
1399        );
1400    }
1401}
1402
1403fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1404    if text.is_empty() {
1405        return single_line_range(0..0);
1406    }
1407
1408    let mut ranges = Vec::new();
1409    let mut start = 0usize;
1410    for (idx, ch) in text.char_indices() {
1411        if ch == '\n' {
1412            ranges.push(start..idx);
1413            start = idx + ch.len_utf8();
1414        }
1415    }
1416    ranges.push(start..text.len());
1417    ranges
1418}
1419
1420fn build_display_annotated(
1421    source: &crate::text::AnnotatedString,
1422    lines: &[DisplayLine],
1423) -> crate::text::AnnotatedString {
1424    if lines.is_empty() {
1425        return crate::text::AnnotatedString::from("");
1426    }
1427
1428    let mut builder = crate::text::AnnotatedString::builder();
1429    for (idx, line) in lines.iter().enumerate() {
1430        builder = match &line.text {
1431            DisplayLineText::Source => {
1432                builder.append_annotated_subsequence(source, line.source_range.clone())
1433            }
1434            DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1435        };
1436        if idx + 1 < lines.len() {
1437            builder = builder.append("\n");
1438        }
1439    }
1440    builder.to_annotated_string()
1441}
1442
1443fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1444    let mut text = String::new();
1445    for (idx, line) in lines.iter().enumerate() {
1446        text.push_str(line.display_text(source));
1447        if idx + 1 < lines.len() {
1448            text.push('\n');
1449        }
1450    }
1451    text
1452}
1453
1454fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1455    while end > start {
1456        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1457            break;
1458        };
1459        if ch.is_whitespace() {
1460            end = start + idx;
1461        } else {
1462            break;
1463        }
1464    }
1465    end
1466}
1467
1468/// The max widths a prepared layout comes out the same for, so a node whose
1469/// width moves can keep one layout instead of preparing it again.
1470#[derive(Clone, Copy, Debug, PartialEq)]
1471pub(crate) enum PreparedWidths {
1472    /// Only the width it was prepared at; `None` is unconstrained.
1473    Exact(Option<u32>),
1474    /// No line wrapped or overflowed: unconstrained, and every width from
1475    /// its measured width up. A narrower width may wrap, so it is not held.
1476    AtLeast(f32),
1477}
1478
1479impl PreparedWidths {
1480    /// The widths `prepared`, made from `text` at `max_width`, holds for.
1481    pub(crate) fn of(
1482        text: &crate::text::AnnotatedString,
1483        options: TextLayoutOptions,
1484        max_width: Option<f32>,
1485        prepared: &PreparedTextLayout,
1486    ) -> Self {
1487        let max_width = normalize_max_width(max_width);
1488        let exact = Self::Exact(max_width.map(f32::to_bits));
1489        let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
1490        // A line's trailing spaces count when it is fitted but not in the
1491        // width it reports, so such a line may wrap at its own width.
1492        let trailing_space = text
1493            .text
1494            .split('\n')
1495            .any(|line| line.ends_with(char::is_whitespace));
1496        if options
1497            .normalized()
1498            .overflow
1499            .scale_down_min_font_size_sp()
1500            .is_some()
1501            || prepared.did_overflow
1502            || wrapped
1503            || trailing_space
1504        {
1505            return exact;
1506        }
1507        match max_width {
1508            Some(width) if prepared.metrics.width >= width => exact,
1509            _ => Self::AtLeast(prepared.metrics.width),
1510        }
1511    }
1512
1513    /// Whether preparing at `max_width` gives the same layout.
1514    pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1515        let max_width = normalize_max_width(max_width);
1516        match self {
1517            Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1518            Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1519        }
1520    }
1521}
1522
1523fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1524    match max_width {
1525        Some(width) if width.is_finite() && width > 0.0 => Some(width),
1526        _ => None,
1527    }
1528}
1529
1530fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1531    (base_start + relative.start)..(base_start + relative.end)
1532}
1533
1534fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1535    boundaries
1536        .binary_search(&byte_offset)
1537        .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1538}
1539
1540fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1541    std::iter::once(range).collect()
1542}
1543
1544struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1545    measurer: &'a M,
1546    text: &'a crate::text::AnnotatedString,
1547    style: &'a TextStyle,
1548    line_start: usize,
1549    prefix_widths: Option<TextLinePrefixWidths>,
1550}
1551
1552impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1553    fn new(
1554        measurer: &'a M,
1555        text: &'a crate::text::AnnotatedString,
1556        line_range: &Range<usize>,
1557        style: &'a TextStyle,
1558        boundary_count: usize,
1559    ) -> Self {
1560        let expected_chars = boundary_count.saturating_sub(1);
1561        let prefix_widths = measurer
1562            .measure_line_prefix_widths(text, line_range.clone(), style)
1563            .filter(|widths| widths.char_count() == expected_chars);
1564        Self {
1565            measurer,
1566            text,
1567            style,
1568            line_start: line_range.start,
1569            prefix_widths,
1570        }
1571    }
1572
1573    fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1574        if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1575            return width;
1576        }
1577        let segment_range =
1578            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1579        self.measurer
1580            .measure_subsequence(self.text, segment_range, self.style)
1581            .width
1582    }
1583
1584    fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1585        if let Some(prefix_widths) = &self.prefix_widths
1586            && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1587        {
1588            return Some(width);
1589        }
1590        None
1591    }
1592
1593    fn display_line_for_char_range(
1594        &self,
1595        boundaries: &[usize],
1596        start_idx: usize,
1597        end_idx: usize,
1598    ) -> DisplayLine {
1599        let source_range =
1600            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1601        let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1602        DisplayLine::from_measured_source_range(source_range, measured_width)
1603    }
1604}
1605
1606fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1607    measurer: &M,
1608    text: &crate::text::AnnotatedString,
1609    line_range: Range<usize>,
1610    style: &TextStyle,
1611    max_width: f32,
1612    line_break: LineBreak,
1613    hyphens: Hyphens,
1614) -> Vec<DisplayLine> {
1615    let line_text = &text.text[line_range.clone()];
1616    if line_text.is_empty() {
1617        return vec![DisplayLine::from_source_range(
1618            line_range.start..line_range.start,
1619        )];
1620    }
1621
1622    if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1623        && measured_width <= max_width + WRAP_EPSILON
1624    {
1625        return vec![DisplayLine::from_measured_source_range(
1626            line_range,
1627            measured_width,
1628        )];
1629    }
1630
1631    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1632        && line_text.chars().any(char::is_whitespace)
1633        && let Some(balanced) = wrap_line_with_word_balance(
1634            measurer,
1635            text,
1636            line_range.clone(),
1637            style,
1638            max_width,
1639            line_break,
1640        )
1641    {
1642        return balanced;
1643    }
1644
1645    wrap_line_greedy(
1646        measurer, text, line_range, style, max_width, line_break, hyphens,
1647    )
1648}
1649
1650fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1651    measurer: &M,
1652    text: &crate::text::AnnotatedString,
1653    line_range: Range<usize>,
1654    style: &TextStyle,
1655    max_width: f32,
1656    line_break: LineBreak,
1657    hyphens: Hyphens,
1658) -> Vec<DisplayLine> {
1659    let line_text = &text.text[line_range.clone()];
1660    let boundaries = char_boundaries(line_text);
1661    let measure_context =
1662        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1663    if let Some(measured_width) =
1664        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1665        && measured_width <= max_width + WRAP_EPSILON
1666    {
1667        return vec![DisplayLine::from_measured_source_range(
1668            line_range,
1669            measured_width,
1670        )];
1671    }
1672    let mut wrapped = Vec::new();
1673    let mut start_idx = 0usize;
1674
1675    while start_idx < boundaries.len() - 1 {
1676        let mut low = start_idx + 1;
1677        let mut high = boundaries.len() - 1;
1678        let mut best = start_idx + 1;
1679
1680        while low <= high {
1681            let mid = (low + high) / 2;
1682            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1683            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1684                best = mid;
1685                low = mid + 1;
1686            } else {
1687                if mid == 0 {
1688                    break;
1689                }
1690                high = mid - 1;
1691            }
1692        }
1693
1694        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1695        let mut effective_wrap_idx = wrap_idx;
1696        let can_hyphenate = hyphens == Hyphens::Auto
1697            && wrap_idx == best
1698            && best < boundaries.len() - 1
1699            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1700        if can_hyphenate {
1701            effective_wrap_idx = resolve_auto_hyphen_break(
1702                measurer,
1703                line_text,
1704                style,
1705                &boundaries,
1706                start_idx,
1707                wrap_idx,
1708            );
1709        }
1710
1711        let broke_at_word_boundary = effective_wrap_idx > start_idx
1712            && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1713                .chars()
1714                .all(char::is_whitespace);
1715        let segment_start = boundaries[start_idx];
1716        let mut segment_end = boundaries[effective_wrap_idx];
1717        if wrap_idx != best || broke_at_word_boundary {
1718            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1719        }
1720        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1721        wrapped.push(measure_context.display_line_for_char_range(
1722            &boundaries,
1723            start_idx,
1724            segment_end_idx,
1725        ));
1726
1727        start_idx = if wrap_idx != best || broke_at_word_boundary {
1728            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1729        } else {
1730            effective_wrap_idx
1731        };
1732    }
1733
1734    if wrapped.is_empty() {
1735        wrapped.push(DisplayLine::from_source_range(
1736            line_range.start..line_range.start,
1737        ));
1738    }
1739
1740    wrapped
1741}
1742
1743fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1744    measurer: &M,
1745    text: &crate::text::AnnotatedString,
1746    line_range: Range<usize>,
1747    style: &TextStyle,
1748    max_width: f32,
1749    line_break: LineBreak,
1750) -> Option<Vec<DisplayLine>> {
1751    let line_text = &text.text[line_range.clone()];
1752    let boundaries = char_boundaries(line_text);
1753    let measure_context =
1754        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1755    if let Some(measured_width) =
1756        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1757        && measured_width <= max_width + WRAP_EPSILON
1758    {
1759        return Some(vec![DisplayLine::from_measured_source_range(
1760            line_range,
1761            measured_width,
1762        )]);
1763    }
1764    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1765    if breakpoints.len() <= 2 {
1766        return None;
1767    }
1768
1769    let node_count = breakpoints.len();
1770    let mut best_cost = vec![f32::INFINITY; node_count];
1771    let mut next_index = vec![None; node_count];
1772    best_cost[node_count - 1] = 0.0;
1773
1774    for start in (0..node_count - 1).rev() {
1775        for end in start + 1..node_count {
1776            let start_byte = boundaries[breakpoints[start]];
1777            let end_byte = boundaries[breakpoints[end]];
1778            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1779            if trimmed_end <= start_byte {
1780                continue;
1781            }
1782            let segment_start_idx = breakpoints[start];
1783            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1784            let segment_width =
1785                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1786            if segment_width > max_width + WRAP_EPSILON {
1787                continue;
1788            }
1789            if !best_cost[end].is_finite() {
1790                continue;
1791            }
1792            let slack = (max_width - segment_width).max(0.0);
1793            let is_last = end == node_count - 1;
1794            let segment_cost = match line_break {
1795                LineBreak::Heading => slack * slack,
1796                LineBreak::Paragraph => {
1797                    if is_last {
1798                        slack * slack * 0.16
1799                    } else {
1800                        slack * slack
1801                    }
1802                }
1803                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1804            };
1805            let candidate = segment_cost + best_cost[end];
1806            if candidate < best_cost[start] {
1807                best_cost[start] = candidate;
1808                next_index[start] = Some(end);
1809            }
1810        }
1811    }
1812
1813    let mut wrapped = Vec::new();
1814    let mut current = 0usize;
1815    while current < node_count - 1 {
1816        let next = next_index[current]?;
1817        let start_byte = boundaries[breakpoints[current]];
1818        let end_byte = boundaries[breakpoints[next]];
1819        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1820        if trimmed_end <= start_byte {
1821            return None;
1822        }
1823        let segment_start_idx = breakpoints[current];
1824        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1825        wrapped.push(measure_context.display_line_for_char_range(
1826            &boundaries,
1827            segment_start_idx,
1828            segment_end_idx,
1829        ));
1830        current = next;
1831    }
1832
1833    if wrapped.is_empty() {
1834        return None;
1835    }
1836
1837    Some(wrapped)
1838}
1839
1840fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1841    let mut points = vec![0usize];
1842    for idx in 1..boundaries.len() - 1 {
1843        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1844        let current = &line[boundaries[idx]..boundaries[idx + 1]];
1845        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1846            points.push(idx);
1847        }
1848    }
1849    let end = boundaries.len() - 1;
1850    if points.last().copied() != Some(end) {
1851        points.push(end);
1852    }
1853    points
1854}
1855
1856fn choose_wrap_break(
1857    line: &str,
1858    boundaries: &[usize],
1859    start_idx: usize,
1860    best: usize,
1861    _line_break: LineBreak,
1862) -> usize {
1863    if best >= boundaries.len() - 1 {
1864        return best;
1865    }
1866
1867    if best <= start_idx + 1 {
1868        return best;
1869    }
1870
1871    for idx in (start_idx + 1..=best).rev() {
1872        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1873        if prev.chars().all(char::is_whitespace) {
1874            return idx;
1875        }
1876    }
1877    best
1878}
1879
1880fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1881    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1882        return false;
1883    }
1884    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1885    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1886    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1887}
1888
1889fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1890    measurer: &M,
1891    line: &str,
1892    style: &TextStyle,
1893    boundaries: &[usize],
1894    start_idx: usize,
1895    break_idx: usize,
1896) -> usize {
1897    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1898        && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1899    {
1900        return candidate;
1901    }
1902    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1903}
1904
1905fn is_valid_auto_hyphen_break(
1906    line: &str,
1907    boundaries: &[usize],
1908    start_idx: usize,
1909    break_idx: usize,
1910    candidate_idx: usize,
1911) -> bool {
1912    let end_idx = boundaries.len().saturating_sub(1);
1913    candidate_idx > start_idx
1914        && candidate_idx < end_idx
1915        && candidate_idx <= break_idx
1916        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1917        && is_break_inside_word(line, boundaries, candidate_idx)
1918}
1919
1920fn choose_auto_hyphen_break_fallback(
1921    boundaries: &[usize],
1922    start_idx: usize,
1923    break_idx: usize,
1924) -> usize {
1925    let end_idx = boundaries.len().saturating_sub(1);
1926    if break_idx >= end_idx {
1927        return break_idx;
1928    }
1929    let trailing_len = end_idx.saturating_sub(break_idx);
1930    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1931        return break_idx;
1932    }
1933
1934    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1935    let max_break = break_idx.saturating_sub(1);
1936    if min_break > max_break {
1937        return break_idx;
1938    }
1939
1940    let mut best_break = break_idx;
1941    let mut best_penalty = usize::MAX;
1942    for idx in min_break..=max_break {
1943        let candidate_trailing_len = end_idx.saturating_sub(idx);
1944        let candidate_prefix_len = idx.saturating_sub(start_idx);
1945        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1946            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1947        {
1948            continue;
1949        }
1950
1951        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1952        if penalty < best_penalty {
1953            best_penalty = penalty;
1954            best_break = idx;
1955            if penalty == 0 {
1956                break;
1957            }
1958        }
1959    }
1960    best_break
1961}
1962
1963fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1964    while idx < boundaries.len() - 1 {
1965        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1966        if !ch.chars().all(char::is_whitespace) {
1967            break;
1968        }
1969        idx += 1;
1970    }
1971    idx
1972}
1973
1974fn apply_overflow<M: TextMeasurer + ?Sized>(
1975    measurer: &M,
1976    node_id: Option<NodeId>,
1977    text: &crate::text::AnnotatedString,
1978    style: &TextStyle,
1979    options: TextLayoutOptions,
1980    max_width: Option<f32>,
1981    visible_lines: &mut Vec<DisplayLine>,
1982) -> bool {
1983    if options.overflow == TextOverflow::Visible {
1984        return false;
1985    }
1986    let ellipsis = EllipsisPlacement::for_options(options);
1987    let mut did_overflow = false;
1988    if visible_lines.len() > options.max_lines {
1989        did_overflow = true;
1990        visible_lines.truncate(options.max_lines);
1991        if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
1992            last_line.extend_to_paragraph_end(text);
1993            last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
1994        }
1995    }
1996
1997    let Some(width_limit) = max_width else {
1998        return did_overflow;
1999    };
2000    let visible_len = visible_lines.len();
2001    for (line_index, line) in visible_lines.iter_mut().enumerate() {
2002        if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2003            continue;
2004        }
2005        did_overflow = true;
2006        if line_index + 1 == visible_len
2007            && let Some(placement) = ellipsis
2008        {
2009            line.ellipsize(measurer, node_id, text, style, max_width, placement);
2010        }
2011    }
2012    did_overflow
2013}
2014
2015#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2016enum EllipsisPlacement {
2017    End,
2018    Start,
2019    Middle,
2020}
2021
2022impl EllipsisPlacement {
2023    fn for_options(options: TextLayoutOptions) -> Option<Self> {
2024        let single_line = options.max_lines == 1;
2025        match options.overflow {
2026            TextOverflow::Ellipsis => Some(Self::End),
2027            TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2028            TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2029            TextOverflow::StartEllipsis
2030            | TextOverflow::MiddleEllipsis
2031            | TextOverflow::Clip
2032            | TextOverflow::Visible
2033            | TextOverflow::ScaleDown { .. } => None,
2034        }
2035    }
2036
2037    fn elide(
2038        self,
2039        source: &crate::text::AnnotatedString,
2040        source_range: Range<usize>,
2041        boundaries: &[usize],
2042        kept_chars: usize,
2043    ) -> crate::text::AnnotatedString {
2044        let char_count = boundaries.len() - 1;
2045        let (head_chars, tail_chars) = match self {
2046            Self::End => (kept_chars, 0),
2047            Self::Start => (0, kept_chars),
2048            Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2049        };
2050        let head_end = source_range.start + boundaries[head_chars];
2051        let tail_start = source_range.start + boundaries[char_count - tail_chars];
2052        crate::text::AnnotatedString::builder()
2053            .append_annotated_subsequence(source, source_range.start..head_end)
2054            .append(ELLIPSIS)
2055            .append_annotated_subsequence(source, tail_start..source_range.end)
2056            .to_annotated_string()
2057    }
2058}
2059
2060fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2061    measurer: &M,
2062    node_id: Option<NodeId>,
2063    source: &crate::text::AnnotatedString,
2064    source_range: Range<usize>,
2065    style: &TextStyle,
2066    max_width: Option<f32>,
2067    placement: EllipsisPlacement,
2068) -> DisplayLine {
2069    let width_limit = max_width.unwrap_or(f32::INFINITY);
2070    let fitting_line = |text: DisplayLineText| {
2071        let mut line = DisplayLine {
2072            source_range: source_range.clone(),
2073            text,
2074            measured_width: None,
2075        };
2076        let width = line.measure_width(measurer, node_id, source, style);
2077        line.measured_width = Some(width);
2078        (width <= width_limit + WRAP_EPSILON).then_some(line)
2079    };
2080    if placement != EllipsisPlacement::End
2081        && let Some(line) = fitting_line(DisplayLineText::Source)
2082    {
2083        return line;
2084    }
2085
2086    let boundaries = char_boundaries(&source.text[source_range.clone()]);
2087    let elided_line = |kept_chars: usize| {
2088        fitting_line(DisplayLineText::Ellipsized(placement.elide(
2089            source,
2090            source_range.clone(),
2091            &boundaries,
2092            kept_chars,
2093        )))
2094    };
2095    let Some(mut best) = elided_line(0) else {
2096        return DisplayLine {
2097            source_range: source_range.clone(),
2098            text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2099            measured_width: None,
2100        };
2101    };
2102
2103    let mut fitting = 0usize;
2104    let mut overflowing = boundaries.len();
2105    while fitting + 1 < overflowing {
2106        let kept_chars = fitting + (overflowing - fitting) / 2;
2107        match elided_line(kept_chars) {
2108            Some(line) => {
2109                fitting = kept_chars;
2110                best = line;
2111            }
2112            None => overflowing = kept_chars,
2113        }
2114    }
2115    best
2116}
2117
2118fn char_boundaries(text: &str) -> Vec<usize> {
2119    let mut out = Vec::with_capacity(text.chars().count() + 1);
2120    out.push(0);
2121    for (idx, _) in text.char_indices() {
2122        if idx != 0 {
2123            out.push(idx);
2124        }
2125    }
2126    out.push(text.len());
2127    out
2128}
2129
2130#[cfg(test)]
2131#[path = "tests/measure_tests.rs"]
2132mod tests;