Skip to main content

cranpose_ui/text/
measure.rs

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