Skip to main content

cranpose_ui/text/
measure.rs

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