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 ranges = Vec::with_capacity(line_ranges.len());
962    for line_range in line_ranges {
963        for display_line in wrap_line_to_width(
964            measurer,
965            text,
966            line_range,
967            style,
968            width_limit,
969            line_break_mode,
970            hyphens_mode,
971        ) {
972            ranges.push(display_line.source_range.clone());
973        }
974    }
975    ranges
976}
977
978fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
979    measurer: &M,
980    text: &crate::text::AnnotatedString,
981    style: &TextStyle,
982    options: TextLayoutOptions,
983    max_width: Option<f32>,
984) -> PreparedTextLayout {
985    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
986}
987
988pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
989    measurer: &M,
990    node_id: Option<NodeId>,
991    text: &crate::text::AnnotatedString,
992    style: &TextStyle,
993    options: TextLayoutOptions,
994    max_width: Option<f32>,
995) -> PreparedTextLayout {
996    let telemetry = text_layout_telemetry_enabled();
997    let total_start = telemetry.then(Instant::now);
998    let opts = options.normalized();
999    let max_width = normalize_max_width(max_width);
1000    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
1001        return prepare_scale_down_text_layout(
1002            measurer,
1003            node_id,
1004            text,
1005            style,
1006            opts,
1007            max_width,
1008            min_font_size_sp,
1009        );
1010    }
1011
1012    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1013        .then_some(max_width)
1014        .flatten();
1015    let line_break_mode = style
1016        .paragraph_style
1017        .line_break
1018        .take_or_else(|| LineBreak::Simple);
1019    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1020
1021    let wrap_start = telemetry.then(Instant::now);
1022    let line_ranges = split_line_ranges(text.text.as_str());
1023    let source_line_count = line_ranges.len();
1024    let mut visible_lines: Vec<DisplayLine>;
1025    if let Some(width_limit) = wrap_width {
1026        visible_lines = Vec::with_capacity(line_ranges.len());
1027        for line_range in line_ranges {
1028            let wrapped_lines = wrap_line_to_width(
1029                measurer,
1030                text,
1031                line_range,
1032                style,
1033                width_limit,
1034                line_break_mode,
1035                hyphens_mode,
1036            );
1037            visible_lines.extend(wrapped_lines);
1038        }
1039    } else {
1040        visible_lines = line_ranges
1041            .into_iter()
1042            .map(DisplayLine::from_source_range)
1043            .collect();
1044    }
1045    let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1046
1047    let overflow_start = telemetry.then(Instant::now);
1048    let did_overflow = apply_overflow(
1049        measurer,
1050        node_id,
1051        text,
1052        style,
1053        opts,
1054        max_width,
1055        &mut visible_lines,
1056    );
1057    let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1058
1059    let build_start = telemetry.then(Instant::now);
1060    let display_annotated = build_display_annotated(text, &visible_lines);
1061    debug_assert_eq!(
1062        display_annotated.text,
1063        join_display_line_text(text, &visible_lines)
1064    );
1065    let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1066
1067    let metrics_start = telemetry.then(Instant::now);
1068    let line_height = measurer.line_height_for_node(node_id, text, style).max(0.0);
1069    let display_line_count = visible_lines.len().max(1);
1070    let layout_line_count = display_line_count.max(opts.min_lines);
1071
1072    let measured_width = if visible_lines.is_empty() {
1073        0.0
1074    } else {
1075        visible_lines
1076            .iter()
1077            .map(|line| line.measure_width(measurer, node_id, text, style))
1078            .fold(0.0_f32, f32::max)
1079    };
1080    let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1081    let width = if opts.overflow == TextOverflow::Visible {
1082        measured_width
1083    } else if let Some(width_limit) = max_width {
1084        measured_width.min(width_limit)
1085    } else {
1086        measured_width
1087    };
1088
1089    let edges = measurer
1090        .line_box(style)
1091        .unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1092    let prepared = PreparedTextLayout {
1093        text: Rc::new(display_annotated),
1094        visual_style: style.clone(),
1095        metrics: TextMetrics {
1096            width,
1097            height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1098                .max(0.0),
1099            line_height,
1100            line_count: layout_line_count,
1101        },
1102        did_overflow,
1103        render_text: Default::default(),
1104    };
1105
1106    if let Some(start) = total_start {
1107        eprintln!(
1108            "[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}",
1109            text.text.len(),
1110            text.span_styles.len(),
1111            source_line_count,
1112            display_line_count,
1113            wrap_width.is_some(),
1114            max_width,
1115            wrap_ms.unwrap_or(0.0),
1116            overflow_ms.unwrap_or(0.0),
1117            build_ms.unwrap_or(0.0),
1118            metrics_ms.unwrap_or(0.0),
1119            start.elapsed().as_secs_f64() * 1000.0,
1120        );
1121    }
1122
1123    prepared
1124}
1125
1126fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1127    measurer: &M,
1128    node_id: Option<NodeId>,
1129    text: &crate::text::AnnotatedString,
1130    style: &TextStyle,
1131    options: TextLayoutOptions,
1132    max_width: Option<f32>,
1133    min_font_size_sp: f32,
1134) -> PreparedTextLayout {
1135    let clipped_options = TextLayoutOptions {
1136        overflow: TextOverflow::Clip,
1137        ..options
1138    }
1139    .normalized();
1140
1141    let full_size = prepare_scaled_text_layout(
1142        measurer,
1143        node_id,
1144        text,
1145        style,
1146        clipped_options,
1147        max_width,
1148        FontScaleCurve::linear(1.0),
1149    );
1150    let Some(width_limit) = max_width else {
1151        return full_size;
1152    };
1153    if !full_size.did_overflow {
1154        return full_size;
1155    }
1156
1157    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1158    if !base_font_size.is_finite() || base_font_size <= 0.0 {
1159        return full_size;
1160    }
1161    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1162    if min_scale >= 1.0 {
1163        return full_size;
1164    }
1165
1166    let min_size = prepare_scaled_text_layout(
1167        measurer,
1168        node_id,
1169        text,
1170        style,
1171        clipped_options,
1172        Some(width_limit),
1173        FontScaleCurve::linear(min_scale),
1174    );
1175    if min_size.did_overflow {
1176        return min_size;
1177    }
1178
1179    let mut low = min_scale;
1180    let mut high = 1.0;
1181    let mut best = min_size;
1182    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1183        let mid = (low + high) * 0.5;
1184        let candidate = prepare_scaled_text_layout(
1185            measurer,
1186            node_id,
1187            text,
1188            style,
1189            clipped_options,
1190            Some(width_limit),
1191            FontScaleCurve::linear(mid),
1192        );
1193        if candidate.did_overflow {
1194            high = mid;
1195        } else {
1196            low = mid;
1197            best = candidate;
1198        }
1199    }
1200
1201    best
1202}
1203
1204fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1205    measurer: &M,
1206    node_id: Option<NodeId>,
1207    text: &crate::text::AnnotatedString,
1208    style: &TextStyle,
1209    options: TextLayoutOptions,
1210    max_width: Option<f32>,
1211    shrink: FontScaleCurve,
1212) -> PreparedTextLayout {
1213    let visual_style = scale_text_style_font_sizes(style, shrink);
1214    let visual_text = scale_annotated_font_sizes(text, shrink);
1215    prepare_text_layout_with_measurer_for_node(
1216        measurer,
1217        node_id,
1218        visual_text.as_ref(),
1219        &visual_style,
1220        options,
1221        max_width,
1222    )
1223}
1224
1225fn scale_annotated_font_sizes(
1226    text: &crate::text::AnnotatedString,
1227    curve: FontScaleCurve,
1228) -> Cow<'_, crate::text::AnnotatedString> {
1229    if curve.is_identity() || !annotated_text_needs_scaling(text) {
1230        return Cow::Borrowed(text);
1231    }
1232
1233    let mut scaled = text.clone();
1234    for span in &mut scaled.span_styles {
1235        span.item = scale_span_style_font_sizes(&span.item, curve, None);
1236    }
1237    Cow::Owned(scaled)
1238}
1239
1240fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1241    if curve.is_identity() {
1242        return style.clone();
1243    }
1244
1245    let mut scaled = style.clone();
1246    scaled.span_style =
1247        scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1248    scaled.paragraph_style.line_height =
1249        scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1250    if let Some(mut indent) = scaled.paragraph_style.text_indent {
1251        indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1252        indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1253        scaled.paragraph_style.text_indent = Some(indent);
1254    }
1255    scaled
1256}
1257
1258fn with_system_font_scale<R>(
1259    text: &crate::text::AnnotatedString,
1260    style: &TextStyle,
1261    block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1262) -> R {
1263    let curve = crate::current_font_scale_curve();
1264    let visual_style = scale_text_style_font_sizes(style, curve);
1265    let visual_text = scale_annotated_font_sizes(text, curve);
1266    block(visual_text.as_ref(), &visual_style)
1267}
1268
1269fn scale_span_style_font_sizes(
1270    style: &crate::text::SpanStyle,
1271    curve: FontScaleCurve,
1272    default_font_size_sp: Option<f32>,
1273) -> crate::text::SpanStyle {
1274    let factor = curve.scale();
1275    let mut scaled = style.clone();
1276    scaled.font_size = match (style.font_size, default_font_size_sp) {
1277        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1278            crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1279        }
1280        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1281        (unit, None) => scale_text_unit_sp(unit, curve),
1282    };
1283    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1284    if let Some(mut shadow) = scaled.shadow {
1285        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1286        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1287        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1288        scaled.shadow = Some(shadow);
1289    }
1290    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1291        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1292            width: width * factor,
1293        });
1294    }
1295    scaled
1296}
1297
1298fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1299    text.span_styles
1300        .iter()
1301        .any(|span| span_style_needs_scaling(&span.item))
1302}
1303
1304fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1305    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1306        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1307        || matches!(
1308            style.draw_style,
1309            Some(crate::text::TextDrawStyle::Stroke { .. })
1310        )
1311        || style.shadow.is_some()
1312}
1313
1314fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1315    match unit {
1316        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1317            crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1318        }
1319        other => other,
1320    }
1321}
1322
1323fn scale_text_unit_sp_and_em(
1324    unit: crate::text::TextUnit,
1325    curve: FontScaleCurve,
1326) -> crate::text::TextUnit {
1327    match unit {
1328        crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1329        crate::text::TextUnit::Em(value) if value.is_finite() => {
1330            crate::text::TextUnit::Em(value * curve.scale())
1331        }
1332        other => other,
1333    }
1334}
1335
1336fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1337    if value.is_finite() {
1338        value * factor
1339    } else {
1340        value
1341    }
1342}
1343
1344#[derive(Clone, Debug)]
1345enum DisplayLineText {
1346    Source,
1347    Ellipsized(crate::text::AnnotatedString),
1348}
1349
1350#[derive(Clone, Debug)]
1351struct DisplayLine {
1352    source_range: Range<usize>,
1353    text: DisplayLineText,
1354    measured_width: Option<f32>,
1355}
1356
1357impl DisplayLine {
1358    fn from_source_range(source_range: Range<usize>) -> Self {
1359        Self {
1360            source_range,
1361            text: DisplayLineText::Source,
1362            measured_width: None,
1363        }
1364    }
1365
1366    fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1367        Self {
1368            source_range,
1369            text: DisplayLineText::Source,
1370            measured_width: measured_width
1371                .is_finite()
1372                .then_some(measured_width.max(0.0)),
1373        }
1374    }
1375
1376    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1377        match &self.text {
1378            DisplayLineText::Source => &source.text[self.source_range.clone()],
1379            DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1380        }
1381    }
1382
1383    fn measure_width<M: TextMeasurer + ?Sized>(
1384        &self,
1385        measurer: &M,
1386        node_id: Option<NodeId>,
1387        source: &crate::text::AnnotatedString,
1388        style: &TextStyle,
1389    ) -> f32 {
1390        self.measured_width.unwrap_or_else(|| match &self.text {
1391            DisplayLineText::Source => {
1392                measurer
1393                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1394                    .width
1395            }
1396            DisplayLineText::Ellipsized(annotated) => {
1397                measurer.measure_for_node(node_id, annotated, style).width
1398            }
1399        })
1400    }
1401
1402    fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1403        let start = self.source_range.start;
1404        let end = source.text[start..]
1405            .find('\n')
1406            .map_or(source.text.len(), |offset| start + offset);
1407        self.source_range = start..end;
1408        self.text = DisplayLineText::Source;
1409        self.measured_width = None;
1410    }
1411
1412    fn ellipsize<M: TextMeasurer + ?Sized>(
1413        &mut self,
1414        measurer: &M,
1415        node_id: Option<NodeId>,
1416        source: &crate::text::AnnotatedString,
1417        style: &TextStyle,
1418        max_width: Option<f32>,
1419        placement: EllipsisPlacement,
1420    ) {
1421        *self = fit_ellipsis(
1422            measurer,
1423            node_id,
1424            source,
1425            self.source_range.clone(),
1426            style,
1427            max_width,
1428            placement,
1429        );
1430    }
1431}
1432
1433fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1434    if text.is_empty() {
1435        return single_line_range(0..0);
1436    }
1437
1438    let mut ranges = Vec::new();
1439    let mut start = 0usize;
1440    for (idx, ch) in text.char_indices() {
1441        if ch == '\n' {
1442            ranges.push(start..idx);
1443            start = idx + ch.len_utf8();
1444        }
1445    }
1446    ranges.push(start..text.len());
1447    ranges
1448}
1449
1450fn build_display_annotated(
1451    source: &crate::text::AnnotatedString,
1452    lines: &[DisplayLine],
1453) -> crate::text::AnnotatedString {
1454    if lines.is_empty() {
1455        return crate::text::AnnotatedString::from("");
1456    }
1457
1458    let mut builder = crate::text::AnnotatedString::builder();
1459    for (idx, line) in lines.iter().enumerate() {
1460        builder = match &line.text {
1461            DisplayLineText::Source => {
1462                builder.append_annotated_subsequence(source, line.source_range.clone())
1463            }
1464            DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1465        };
1466        if idx + 1 < lines.len() {
1467            builder = builder.append("\n");
1468        }
1469    }
1470    builder.to_annotated_string()
1471}
1472
1473fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1474    let mut text = String::new();
1475    for (idx, line) in lines.iter().enumerate() {
1476        text.push_str(line.display_text(source));
1477        if idx + 1 < lines.len() {
1478            text.push('\n');
1479        }
1480    }
1481    text
1482}
1483
1484fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1485    while end > start {
1486        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1487            break;
1488        };
1489        if ch.is_whitespace() {
1490            end = start + idx;
1491        } else {
1492            break;
1493        }
1494    }
1495    end
1496}
1497
1498/// The max widths a prepared layout comes out the same for, so a node whose
1499/// width moves can keep one layout instead of preparing it again.
1500#[derive(Clone, Copy, Debug, PartialEq)]
1501pub(crate) enum PreparedWidths {
1502    /// Only the width it was prepared at; `None` is unconstrained.
1503    Exact(Option<u32>),
1504    /// No line wrapped or overflowed: unconstrained, and every width from
1505    /// its measured width up. A narrower width may wrap, so it is not held.
1506    AtLeast(f32),
1507}
1508
1509impl PreparedWidths {
1510    /// The widths `prepared`, made from `text` at `max_width`, holds for.
1511    pub(crate) fn of(
1512        text: &crate::text::AnnotatedString,
1513        options: TextLayoutOptions,
1514        max_width: Option<f32>,
1515        prepared: &PreparedTextLayout,
1516    ) -> Self {
1517        let max_width = normalize_max_width(max_width);
1518        let exact = Self::Exact(max_width.map(f32::to_bits));
1519        let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
1520        // A line's trailing spaces count when it is fitted but not in the
1521        // width it reports, so such a line may wrap at its own width.
1522        let trailing_space = text
1523            .text
1524            .split('\n')
1525            .any(|line| line.ends_with(char::is_whitespace));
1526        if options
1527            .normalized()
1528            .overflow
1529            .scale_down_min_font_size_sp()
1530            .is_some()
1531            || prepared.did_overflow
1532            || wrapped
1533            || trailing_space
1534        {
1535            return exact;
1536        }
1537        match max_width {
1538            Some(width) if prepared.metrics.width >= width => exact,
1539            _ => Self::AtLeast(prepared.metrics.width),
1540        }
1541    }
1542
1543    /// Whether preparing at `max_width` gives the same layout.
1544    pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1545        let max_width = normalize_max_width(max_width);
1546        match self {
1547            Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1548            Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1549        }
1550    }
1551}
1552
1553fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1554    match max_width {
1555        Some(width) if width.is_finite() && width > 0.0 => Some(width),
1556        _ => None,
1557    }
1558}
1559
1560fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1561    (base_start + relative.start)..(base_start + relative.end)
1562}
1563
1564fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1565    boundaries
1566        .binary_search(&byte_offset)
1567        .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1568}
1569
1570fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1571    std::iter::once(range).collect()
1572}
1573
1574struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1575    measurer: &'a M,
1576    text: &'a crate::text::AnnotatedString,
1577    style: &'a TextStyle,
1578    line_start: usize,
1579    prefix_widths: Option<Rc<TextLinePrefixWidths>>,
1580}
1581
1582impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1583    fn new(
1584        measurer: &'a M,
1585        text: &'a crate::text::AnnotatedString,
1586        line_range: &Range<usize>,
1587        style: &'a TextStyle,
1588        boundary_count: usize,
1589    ) -> Self {
1590        let expected_chars = boundary_count.saturating_sub(1);
1591        let prefix_widths = measurer
1592            .measure_line_prefix_widths(text, line_range.clone(), style)
1593            .filter(|widths| widths.char_count() == expected_chars);
1594        Self {
1595            measurer,
1596            text,
1597            style,
1598            line_start: line_range.start,
1599            prefix_widths,
1600        }
1601    }
1602
1603    fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1604        if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1605            return width;
1606        }
1607        let segment_range =
1608            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1609        self.measurer
1610            .measure_subsequence(self.text, segment_range, self.style)
1611            .width
1612    }
1613
1614    fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1615        if let Some(prefix_widths) = &self.prefix_widths
1616            && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1617        {
1618            return Some(width);
1619        }
1620        None
1621    }
1622
1623    fn display_line_for_char_range(
1624        &self,
1625        boundaries: &[usize],
1626        start_idx: usize,
1627        end_idx: usize,
1628    ) -> DisplayLine {
1629        let source_range =
1630            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1631        let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1632        DisplayLine::from_measured_source_range(source_range, measured_width)
1633    }
1634}
1635
1636fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1637    measurer: &M,
1638    text: &crate::text::AnnotatedString,
1639    line_range: Range<usize>,
1640    style: &TextStyle,
1641    max_width: f32,
1642    line_break: LineBreak,
1643    hyphens: Hyphens,
1644) -> Vec<DisplayLine> {
1645    let line_text = &text.text[line_range.clone()];
1646    if line_text.is_empty() {
1647        return vec![DisplayLine::from_source_range(
1648            line_range.start..line_range.start,
1649        )];
1650    }
1651
1652    if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1653        && measured_width <= max_width + WRAP_EPSILON
1654    {
1655        return vec![DisplayLine::from_measured_source_range(
1656            line_range,
1657            measured_width,
1658        )];
1659    }
1660
1661    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1662        && line_text.chars().any(char::is_whitespace)
1663        && let Some(balanced) = wrap_line_with_word_balance(
1664            measurer,
1665            text,
1666            line_range.clone(),
1667            style,
1668            max_width,
1669            line_break,
1670        )
1671    {
1672        return balanced;
1673    }
1674
1675    wrap_line_greedy(
1676        measurer, text, line_range, style, max_width, line_break, hyphens,
1677    )
1678}
1679
1680fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1681    measurer: &M,
1682    text: &crate::text::AnnotatedString,
1683    line_range: Range<usize>,
1684    style: &TextStyle,
1685    max_width: f32,
1686    line_break: LineBreak,
1687    hyphens: Hyphens,
1688) -> Vec<DisplayLine> {
1689    let line_text = &text.text[line_range.clone()];
1690    let boundaries = char_boundaries(line_text);
1691    let measure_context =
1692        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1693    if let Some(measured_width) =
1694        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1695        && measured_width <= max_width + WRAP_EPSILON
1696    {
1697        return vec![DisplayLine::from_measured_source_range(
1698            line_range,
1699            measured_width,
1700        )];
1701    }
1702    let mut wrapped = Vec::new();
1703    let mut start_idx = 0usize;
1704
1705    while start_idx < boundaries.len() - 1 {
1706        let mut low = start_idx + 1;
1707        let mut high = boundaries.len() - 1;
1708        let mut best = start_idx + 1;
1709
1710        while low <= high {
1711            let mid = (low + high) / 2;
1712            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1713            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1714                best = mid;
1715                low = mid + 1;
1716            } else {
1717                if mid == 0 {
1718                    break;
1719                }
1720                high = mid - 1;
1721            }
1722        }
1723
1724        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1725        let mut effective_wrap_idx = wrap_idx;
1726        let can_hyphenate = hyphens == Hyphens::Auto
1727            && wrap_idx == best
1728            && best < boundaries.len() - 1
1729            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1730        if can_hyphenate {
1731            effective_wrap_idx = resolve_auto_hyphen_break(
1732                measurer,
1733                line_text,
1734                style,
1735                &boundaries,
1736                start_idx,
1737                wrap_idx,
1738            );
1739        }
1740
1741        let broke_at_word_boundary = effective_wrap_idx > start_idx
1742            && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1743                .chars()
1744                .all(char::is_whitespace);
1745        let segment_start = boundaries[start_idx];
1746        let mut segment_end = boundaries[effective_wrap_idx];
1747        if wrap_idx != best || broke_at_word_boundary {
1748            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1749        }
1750        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1751        wrapped.push(measure_context.display_line_for_char_range(
1752            &boundaries,
1753            start_idx,
1754            segment_end_idx,
1755        ));
1756
1757        start_idx = if wrap_idx != best || broke_at_word_boundary {
1758            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1759        } else {
1760            effective_wrap_idx
1761        };
1762    }
1763
1764    if wrapped.is_empty() {
1765        wrapped.push(DisplayLine::from_source_range(
1766            line_range.start..line_range.start,
1767        ));
1768    }
1769
1770    wrapped
1771}
1772
1773fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1774    measurer: &M,
1775    text: &crate::text::AnnotatedString,
1776    line_range: Range<usize>,
1777    style: &TextStyle,
1778    max_width: f32,
1779    line_break: LineBreak,
1780) -> Option<Vec<DisplayLine>> {
1781    let line_text = &text.text[line_range.clone()];
1782    let boundaries = char_boundaries(line_text);
1783    let measure_context =
1784        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1785    if let Some(measured_width) =
1786        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1787        && measured_width <= max_width + WRAP_EPSILON
1788    {
1789        return Some(vec![DisplayLine::from_measured_source_range(
1790            line_range,
1791            measured_width,
1792        )]);
1793    }
1794    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1795    if breakpoints.len() <= 2 {
1796        return None;
1797    }
1798
1799    let node_count = breakpoints.len();
1800    let mut best_cost = vec![f32::INFINITY; node_count];
1801    let mut next_index = vec![None; node_count];
1802    best_cost[node_count - 1] = 0.0;
1803
1804    for start in (0..node_count - 1).rev() {
1805        for end in start + 1..node_count {
1806            let start_byte = boundaries[breakpoints[start]];
1807            let end_byte = boundaries[breakpoints[end]];
1808            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1809            if trimmed_end <= start_byte {
1810                continue;
1811            }
1812            let segment_start_idx = breakpoints[start];
1813            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1814            let segment_width =
1815                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
1816            if segment_width > max_width + WRAP_EPSILON {
1817                continue;
1818            }
1819            if !best_cost[end].is_finite() {
1820                continue;
1821            }
1822            let slack = (max_width - segment_width).max(0.0);
1823            let is_last = end == node_count - 1;
1824            let segment_cost = match line_break {
1825                LineBreak::Heading => slack * slack,
1826                LineBreak::Paragraph => {
1827                    if is_last {
1828                        slack * slack * 0.16
1829                    } else {
1830                        slack * slack
1831                    }
1832                }
1833                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
1834            };
1835            let candidate = segment_cost + best_cost[end];
1836            if candidate < best_cost[start] {
1837                best_cost[start] = candidate;
1838                next_index[start] = Some(end);
1839            }
1840        }
1841    }
1842
1843    let mut wrapped = Vec::new();
1844    let mut current = 0usize;
1845    while current < node_count - 1 {
1846        let next = next_index[current]?;
1847        let start_byte = boundaries[breakpoints[current]];
1848        let end_byte = boundaries[breakpoints[next]];
1849        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
1850        if trimmed_end <= start_byte {
1851            return None;
1852        }
1853        let segment_start_idx = breakpoints[current];
1854        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
1855        wrapped.push(measure_context.display_line_for_char_range(
1856            &boundaries,
1857            segment_start_idx,
1858            segment_end_idx,
1859        ));
1860        current = next;
1861    }
1862
1863    if wrapped.is_empty() {
1864        return None;
1865    }
1866
1867    Some(wrapped)
1868}
1869
1870fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
1871    let mut points = vec![0usize];
1872    for idx in 1..boundaries.len() - 1 {
1873        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1874        let current = &line[boundaries[idx]..boundaries[idx + 1]];
1875        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
1876            points.push(idx);
1877        }
1878    }
1879    let end = boundaries.len() - 1;
1880    if points.last().copied() != Some(end) {
1881        points.push(end);
1882    }
1883    points
1884}
1885
1886fn choose_wrap_break(
1887    line: &str,
1888    boundaries: &[usize],
1889    start_idx: usize,
1890    best: usize,
1891    _line_break: LineBreak,
1892) -> usize {
1893    if best >= boundaries.len() - 1 {
1894        return best;
1895    }
1896
1897    if best <= start_idx + 1 {
1898        return best;
1899    }
1900
1901    for idx in (start_idx + 1..=best).rev() {
1902        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
1903        if prev.chars().all(char::is_whitespace) {
1904            return idx;
1905        }
1906    }
1907    best
1908}
1909
1910fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
1911    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
1912        return false;
1913    }
1914    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
1915    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
1916    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
1917}
1918
1919fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
1920    measurer: &M,
1921    line: &str,
1922    style: &TextStyle,
1923    boundaries: &[usize],
1924    start_idx: usize,
1925    break_idx: usize,
1926) -> usize {
1927    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
1928        && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
1929    {
1930        return candidate;
1931    }
1932    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
1933}
1934
1935fn is_valid_auto_hyphen_break(
1936    line: &str,
1937    boundaries: &[usize],
1938    start_idx: usize,
1939    break_idx: usize,
1940    candidate_idx: usize,
1941) -> bool {
1942    let end_idx = boundaries.len().saturating_sub(1);
1943    candidate_idx > start_idx
1944        && candidate_idx < end_idx
1945        && candidate_idx <= break_idx
1946        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
1947        && is_break_inside_word(line, boundaries, candidate_idx)
1948}
1949
1950fn choose_auto_hyphen_break_fallback(
1951    boundaries: &[usize],
1952    start_idx: usize,
1953    break_idx: usize,
1954) -> usize {
1955    let end_idx = boundaries.len().saturating_sub(1);
1956    if break_idx >= end_idx {
1957        return break_idx;
1958    }
1959    let trailing_len = end_idx.saturating_sub(break_idx);
1960    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
1961        return break_idx;
1962    }
1963
1964    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
1965    let max_break = break_idx.saturating_sub(1);
1966    if min_break > max_break {
1967        return break_idx;
1968    }
1969
1970    let mut best_break = break_idx;
1971    let mut best_penalty = usize::MAX;
1972    for idx in min_break..=max_break {
1973        let candidate_trailing_len = end_idx.saturating_sub(idx);
1974        let candidate_prefix_len = idx.saturating_sub(start_idx);
1975        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
1976            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
1977        {
1978            continue;
1979        }
1980
1981        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
1982        if penalty < best_penalty {
1983            best_penalty = penalty;
1984            best_break = idx;
1985            if penalty == 0 {
1986                break;
1987            }
1988        }
1989    }
1990    best_break
1991}
1992
1993fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
1994    while idx < boundaries.len() - 1 {
1995        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
1996        if !ch.chars().all(char::is_whitespace) {
1997            break;
1998        }
1999        idx += 1;
2000    }
2001    idx
2002}
2003
2004fn apply_overflow<M: TextMeasurer + ?Sized>(
2005    measurer: &M,
2006    node_id: Option<NodeId>,
2007    text: &crate::text::AnnotatedString,
2008    style: &TextStyle,
2009    options: TextLayoutOptions,
2010    max_width: Option<f32>,
2011    visible_lines: &mut Vec<DisplayLine>,
2012) -> bool {
2013    if options.overflow == TextOverflow::Visible {
2014        return false;
2015    }
2016    let ellipsis = EllipsisPlacement::for_options(options);
2017    let mut did_overflow = false;
2018    if visible_lines.len() > options.max_lines {
2019        did_overflow = true;
2020        visible_lines.truncate(options.max_lines);
2021        if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2022            last_line.extend_to_paragraph_end(text);
2023            last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2024        }
2025    }
2026
2027    let Some(width_limit) = max_width else {
2028        return did_overflow;
2029    };
2030    let visible_len = visible_lines.len();
2031    for (line_index, line) in visible_lines.iter_mut().enumerate() {
2032        if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2033            continue;
2034        }
2035        did_overflow = true;
2036        if line_index + 1 == visible_len
2037            && let Some(placement) = ellipsis
2038        {
2039            line.ellipsize(measurer, node_id, text, style, max_width, placement);
2040        }
2041    }
2042    did_overflow
2043}
2044
2045#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2046enum EllipsisPlacement {
2047    End,
2048    Start,
2049    Middle,
2050}
2051
2052impl EllipsisPlacement {
2053    /// How many of `kept_chars` stay before the ellipsis and how many after.
2054    fn split(self, kept_chars: usize) -> (usize, usize) {
2055        match self {
2056            Self::End => (kept_chars, 0),
2057            Self::Start => (0, kept_chars),
2058            Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2059        }
2060    }
2061
2062    /// The most characters an elided line can keep within `width_limit`,
2063    /// estimated from the line's prefix widths and the ellipsis's width
2064    /// without measuring any elided string. Shaping across the cut can move
2065    /// the real width a little either way, so callers confirm it.
2066    fn estimated_kept_chars(
2067        self,
2068        prefix_widths: &TextLinePrefixWidths,
2069        ellipsis_width: f32,
2070        width_limit: f32,
2071    ) -> Option<usize> {
2072        let char_count = prefix_widths.char_count();
2073        let width = |kept_chars: usize| {
2074            let (head_chars, tail_chars) = self.split(kept_chars);
2075            Some(
2076                prefix_widths.width_for_char_range(0, head_chars)?
2077                    + ellipsis_width
2078                    + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2079            )
2080        };
2081        let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2082        while fitting + 1 < overflowing {
2083            let kept_chars = fitting + (overflowing - fitting) / 2;
2084            if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2085                fitting = kept_chars;
2086            } else {
2087                overflowing = kept_chars;
2088            }
2089        }
2090        Some(fitting)
2091    }
2092
2093    fn for_options(options: TextLayoutOptions) -> Option<Self> {
2094        let single_line = options.max_lines == 1;
2095        match options.overflow {
2096            TextOverflow::Ellipsis => Some(Self::End),
2097            TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2098            TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2099            TextOverflow::StartEllipsis
2100            | TextOverflow::MiddleEllipsis
2101            | TextOverflow::Clip
2102            | TextOverflow::Visible
2103            | TextOverflow::ScaleDown { .. } => None,
2104        }
2105    }
2106
2107    fn elide(
2108        self,
2109        source: &crate::text::AnnotatedString,
2110        source_range: Range<usize>,
2111        boundaries: &[usize],
2112        kept_chars: usize,
2113    ) -> crate::text::AnnotatedString {
2114        let char_count = boundaries.len() - 1;
2115        let (head_chars, tail_chars) = self.split(kept_chars);
2116        let head_end = source_range.start + boundaries[head_chars];
2117        let tail_start = source_range.start + boundaries[char_count - tail_chars];
2118        crate::text::AnnotatedString::builder()
2119            .append_annotated_subsequence(source, source_range.start..head_end)
2120            .append(ELLIPSIS)
2121            .append_annotated_subsequence(source, tail_start..source_range.end)
2122            .to_annotated_string()
2123    }
2124}
2125
2126fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2127    measurer: &M,
2128    node_id: Option<NodeId>,
2129    source: &crate::text::AnnotatedString,
2130    source_range: Range<usize>,
2131    style: &TextStyle,
2132    max_width: Option<f32>,
2133    placement: EllipsisPlacement,
2134) -> DisplayLine {
2135    let width_limit = max_width.unwrap_or(f32::INFINITY);
2136    let fitting_line = |text: DisplayLineText| {
2137        let mut line = DisplayLine {
2138            source_range: source_range.clone(),
2139            text,
2140            measured_width: None,
2141        };
2142        let width = line.measure_width(measurer, node_id, source, style);
2143        line.measured_width = Some(width);
2144        (width <= width_limit + WRAP_EPSILON).then_some(line)
2145    };
2146    if placement != EllipsisPlacement::End
2147        && let Some(line) = fitting_line(DisplayLineText::Source)
2148    {
2149        return line;
2150    }
2151
2152    let boundaries = char_boundaries(&source.text[source_range.clone()]);
2153    let elided_line = |kept_chars: usize| {
2154        fitting_line(DisplayLineText::Ellipsized(placement.elide(
2155            source,
2156            source_range.clone(),
2157            &boundaries,
2158            kept_chars,
2159        )))
2160    };
2161    let Some(mut best) = elided_line(0) else {
2162        return DisplayLine {
2163            source_range: source_range.clone(),
2164            text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2165            measured_width: None,
2166        };
2167    };
2168
2169    let mut fitting = 0usize;
2170    let mut overflowing = boundaries.len();
2171    // The line's prefix widths place the cut without measuring an elided
2172    // string per guess; measuring the guess and the one past it confirms it,
2173    // and the search below only runs when shaping across the cut moved it.
2174    let guess = best.measured_width.and_then(|ellipsis_width| {
2175        measurer
2176            .measure_line_prefix_widths(source, source_range.clone(), style)
2177            .filter(|widths| widths.char_count() + 1 == boundaries.len())
2178            .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2179    });
2180    if let Some(guess) = guess.filter(|guess| *guess > 0) {
2181        for kept_chars in [guess, guess + 1] {
2182            if kept_chars <= fitting || kept_chars >= overflowing {
2183                break;
2184            }
2185            match elided_line(kept_chars) {
2186                Some(line) => {
2187                    fitting = kept_chars;
2188                    best = line;
2189                }
2190                None => overflowing = kept_chars,
2191            }
2192        }
2193    }
2194    while fitting + 1 < overflowing {
2195        let kept_chars = fitting + (overflowing - fitting) / 2;
2196        match elided_line(kept_chars) {
2197            Some(line) => {
2198                fitting = kept_chars;
2199                best = line;
2200            }
2201            None => overflowing = kept_chars,
2202        }
2203    }
2204    best
2205}
2206
2207fn char_boundaries(text: &str) -> Vec<usize> {
2208    let mut out = Vec::with_capacity(text.chars().count() + 1);
2209    out.push(0);
2210    for (idx, _) in text.char_indices() {
2211        if idx != 0 {
2212            out.push(idx);
2213        }
2214    }
2215    out.push(text.len());
2216    out
2217}
2218
2219#[cfg(test)]
2220#[path = "tests/measure_tests.rs"]
2221mod tests;