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