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