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(key) = key
643            && let Some(metrics) = self.metrics_cache.borrow_mut().get(&key).copied()
644        {
645            return metrics;
646        }
647        let metrics = self.with_measurer(|m| m.measure_for_node(node_id, text, style));
648        if let Some(key) = key {
649            self.metrics_cache.borrow_mut().push(key, metrics);
650        }
651        metrics
652    }
653
654    pub(crate) fn measure_with_options(
655        &self,
656        node_id: Option<NodeId>,
657        text: &crate::text::AnnotatedString,
658        style: &TextStyle,
659        options: TextLayoutOptions,
660        max_width: Option<f32>,
661    ) -> TextMetrics {
662        let key = text_options_cache_key(text, style, options.normalized(), max_width);
663        if let Some(key) = key {
664            if let Some(metrics) = self.options_metrics_cache.borrow_mut().get(&key).copied() {
665                return metrics;
666            }
667            let prepared_key = TextPreparedCacheKey {
668                base: key,
669                visual_hash: style.render_hash(),
670            };
671            if let Some(prepared) = self.prepared_cache.borrow_mut().get(&prepared_key) {
672                return prepared.metrics;
673            }
674        }
675        let metrics = self.with_measurer(|m| {
676            m.measure_with_options_for_node(node_id, text, style, options.normalized(), max_width)
677        });
678        if let Some(key) = key {
679            self.options_metrics_cache.borrow_mut().push(key, metrics);
680        }
681        metrics
682    }
683
684    /// The layout of `text` at `max_width`, shared with the cache: a layout
685    /// animated through widths misses on every frame, and copying each one
686    /// into and out of the cache cost more than laying it out.
687    pub(crate) fn prepare_with_options(
688        &self,
689        node_id: Option<NodeId>,
690        text: &crate::text::AnnotatedString,
691        style: &TextStyle,
692        options: TextLayoutOptions,
693        max_width: Option<f32>,
694    ) -> Rc<PreparedTextLayout> {
695        let key =
696            text_options_cache_key(text, style, options.normalized(), max_width).map(|base| {
697                TextPreparedCacheKey {
698                    base,
699                    visual_hash: style.render_hash(),
700                }
701            });
702        if let Some(key) = key
703            && let Some(prepared) = self.prepared_cache.borrow_mut().get(&key).map(Rc::clone)
704        {
705            return prepared;
706        }
707        let prepared = Rc::new(self.with_measurer(|m| {
708            m.prepare_with_options_for_node(node_id, text, style, options.normalized(), max_width)
709        }));
710        if let Some(key) = key {
711            self.prepared_cache
712                .borrow_mut()
713                .push(key, Rc::clone(&prepared));
714        }
715        prepared
716    }
717
718    pub(crate) fn layout(
719        &self,
720        text: &crate::text::AnnotatedString,
721        style: &TextStyle,
722    ) -> TextLayoutResult {
723        let key = text_base_cache_key(text, style);
724        if let Some(key) = key
725            && let Some(layout) = self.layout_cache.borrow_mut().get(&key).cloned()
726        {
727            return layout;
728        }
729        let layout = self.with_measurer(|m| m.layout(text, style));
730        if let Some(key) = key {
731            self.layout_cache.borrow_mut().push(key, layout.clone());
732        }
733        layout
734    }
735
736    /// Starts a layout pass: the measurer and these caches drop what recent
737    /// passes did not use.
738    pub(crate) fn begin_layout_pass(&self) {
739        self.with_measurer(TextMeasurer::begin_layout_pass);
740        self.metrics_cache.borrow_mut().begin_pass(drop);
741        self.options_metrics_cache.borrow_mut().begin_pass(drop);
742        self.prepared_cache.borrow_mut().begin_pass(drop);
743        self.layout_cache.borrow_mut().begin_pass(drop);
744    }
745
746    fn clear_caches(&self) {
747        self.generation
748            .set(self.generation.get().wrapping_add(1).max(1));
749        self.metrics_cache.borrow_mut().clear();
750        self.options_metrics_cache.borrow_mut().clear();
751        self.prepared_cache.borrow_mut().clear();
752        self.layout_cache.borrow_mut().clear();
753    }
754}
755
756fn text_base_cache_key(
757    text: &crate::text::AnnotatedString,
758    style: &TextStyle,
759) -> Option<TextBaseCacheKey> {
760    (text.string_annotations.is_empty() && text.link_annotations.is_empty()).then(|| {
761        TextBaseCacheKey {
762            text_hash: text.render_hash(),
763            style_hash: style.measurement_hash(),
764        }
765    })
766}
767
768fn text_options_cache_key(
769    text: &crate::text::AnnotatedString,
770    style: &TextStyle,
771    options: TextLayoutOptions,
772    max_width: Option<f32>,
773) -> Option<TextOptionsCacheKey> {
774    Some(TextOptionsCacheKey {
775        base: text_base_cache_key(text, style)?,
776        options: options.normalized(),
777        max_width_bits: normalize_max_width(max_width).map(f32::to_bits),
778    })
779}
780
781pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
782    crate::render_state::set_current_text_measurer(Rc::new(measurer));
783}
784
785pub(crate) fn current_text_generation() -> u64 {
786    crate::render_state::with_text_service(TextService::generation)
787}
788
789pub fn measure_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextMetrics {
790    with_system_font_scale(text, style, |text, style| {
791        crate::render_state::with_text_service(|service| service.measure(None, text, style))
792    })
793}
794
795pub(crate) fn measure_resolved_text(
796    text: &crate::text::AnnotatedString,
797    style: &TextStyle,
798) -> TextMetrics {
799    crate::render_state::with_text_service(|service| service.measure(None, text, style))
800}
801
802pub(crate) fn resolved_first_baseline(style: &TextStyle) -> Option<f32> {
803    crate::render_state::with_text_service(|service| {
804        service.with_measurer(|m| m.first_baseline(style))
805    })
806}
807
808pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
809    crate::render_state::current_app_context()?;
810    crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(style)))
811}
812
813/// The tight glyph box `(top_offset, height)` of a `style` text line inside
814/// its line slot (see [`TextMeasurer::glyph_line_box`]). Falls back to the
815/// full slot when the active measurer has no font metrics.
816pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
817    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
818    crate::render_state::with_text_service(|service| {
819        service.with_measurer(|m| m.glyph_line_box(&style))
820    })
821    .map_or((0.0, line_height), |(off, h)| {
822        (off.min(line_height), h.min(line_height))
823    })
824}
825
826/// The paragraph line box of `style` (see [`TextMeasurer::line_box`]): its
827/// advance, its baseline and what a paragraph gives back at its edges. `None`
828/// when the active measurer carries no font metrics.
829pub fn text_line_box(style: &TextStyle) -> Option<crate::text::LineBox> {
830    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
831    crate::render_state::with_text_service(|service| service.with_measurer(|m| m.line_box(&style)))
832}
833
834/// Distance from the top of a `style` line slot down to its baseline (see
835/// [`TextMeasurer::first_baseline`]). `None` when the active measurer carries
836/// no font metrics.
837pub fn first_baseline(style: &TextStyle) -> Option<f32> {
838    let style = scale_text_style_font_sizes(style, crate::current_font_scale_curve());
839    crate::render_state::with_text_service(|service| {
840        service.with_measurer(|m| m.first_baseline(&style))
841    })
842}
843
844pub fn measure_text_for_node(
845    node_id: Option<NodeId>,
846    text: &crate::text::AnnotatedString,
847    style: &TextStyle,
848) -> TextMetrics {
849    with_system_font_scale(text, style, |text, style| {
850        crate::render_state::with_text_service(|service| service.measure(node_id, text, style))
851    })
852}
853
854pub fn measure_text_with_options(
855    text: &crate::text::AnnotatedString,
856    style: &TextStyle,
857    options: TextLayoutOptions,
858    max_width: Option<f32>,
859) -> TextMetrics {
860    with_system_font_scale(text, style, |text, style| {
861        crate::render_state::with_text_service(|service| {
862            service.measure_with_options(None, text, style, options.normalized(), max_width)
863        })
864    })
865}
866
867pub fn measure_text_with_options_for_node(
868    node_id: Option<NodeId>,
869    text: &crate::text::AnnotatedString,
870    style: &TextStyle,
871    options: TextLayoutOptions,
872    max_width: Option<f32>,
873) -> TextMetrics {
874    with_system_font_scale(text, style, |text, style| {
875        crate::render_state::with_text_service(|service| {
876            service.measure_with_options(node_id, text, style, options.normalized(), max_width)
877        })
878    })
879}
880
881pub fn prepare_text_layout(
882    text: &crate::text::AnnotatedString,
883    style: &TextStyle,
884    options: TextLayoutOptions,
885    max_width: Option<f32>,
886) -> PreparedTextLayout {
887    Rc::unwrap_or_clone(prepare_text_layout_for_node(
888        None, text, style, options, max_width,
889    ))
890}
891
892pub fn prepare_text_layout_for_node(
893    node_id: Option<NodeId>,
894    text: &crate::text::AnnotatedString,
895    style: &TextStyle,
896    options: TextLayoutOptions,
897    max_width: Option<f32>,
898) -> Rc<PreparedTextLayout> {
899    with_system_font_scale(text, style, |text, style| {
900        crate::render_state::with_text_service(|service| {
901            service.prepare_with_options(node_id, text, style, options.normalized(), max_width)
902        })
903    })
904}
905
906pub fn get_offset_for_position(
907    text: &crate::text::AnnotatedString,
908    style: &TextStyle,
909    x: f32,
910    y: f32,
911) -> usize {
912    with_system_font_scale(text, style, |text, style| {
913        crate::render_state::with_text_measurer(|m| m.get_offset_for_position(text, style, x, y))
914    })
915}
916
917/// Byte offset nearest the local content position (`x`, `y`), **wrap-aware** —
918/// the single hit-test every editable-text pointer path uses (tap-to-place,
919/// drag-select, and selection-handle drag).
920///
921/// It is the inverse of the drawn caret and [`wrapped_line_ranges`]: `y` selects
922/// the VISUAL (wrapped) line (`floor(y / line_height)`), then `x` picks the
923/// nearest char boundary WITHIN that line (delegated to the measurer with `y`
924/// forced to 0). `x`/`y` must already be in text space (padding- and
925/// pan-adjusted). The plain [`get_offset_for_position`] maps `y` through the
926/// measurer's logical `\n` layout, so on wrapped text it lands on the wrong line
927/// (an error that grows with each wrapped line above the finger). With
928/// `wrap_width == None` (single-line fields) this reduces to the one logical
929/// line.
930pub fn offset_for_position_wrapped(
931    text: &str,
932    style: &TextStyle,
933    node_id: Option<NodeId>,
934    wrap_width: Option<f32>,
935    line_height: f32,
936    x: f32,
937    y: f32,
938) -> usize {
939    if text.is_empty() {
940        return 0;
941    }
942    let annotated = crate::text::AnnotatedString::from(text);
943    let line_ranges = wrapped_line_ranges(
944        node_id,
945        &annotated,
946        style,
947        TextLayoutOptions::default(),
948        wrap_width,
949    );
950    if line_ranges.is_empty() {
951        return 0;
952    }
953    let line_idx = if line_height > 0.0 {
954        (y / line_height).floor().max(0.0) as usize
955    } else {
956        0
957    }
958    .min(line_ranges.len() - 1);
959    let range = &line_ranges[line_idx];
960    let line = &text[range.start..range.end];
961    let within = get_offset_for_position(&crate::text::AnnotatedString::from(line), style, x, 0.0);
962    range.start + within.min(line.len())
963}
964
965pub fn get_cursor_x_for_offset(
966    text: &crate::text::AnnotatedString,
967    style: &TextStyle,
968    offset: usize,
969) -> f32 {
970    with_system_font_scale(text, style, |text, style| {
971        crate::render_state::with_text_measurer(|m| m.get_cursor_x_for_offset(text, style, offset))
972    })
973}
974
975pub fn layout_text(text: &crate::text::AnnotatedString, style: &TextStyle) -> TextLayoutResult {
976    with_system_font_scale(text, style, |text, style| {
977        crate::render_state::with_text_service(|service| service.layout(text, style))
978    })
979}
980
981/// Returns the source-text byte range covered by each **visual** (wrapped) line
982/// when `text` is laid out at `max_width` with `options`, matching the wrapping
983/// the renderer performs. Each range excludes the trailing `\n`. With
984/// `max_width == None` (or soft-wrap disabled) this is just the logical
985/// `\n`-delimited lines.
986///
987/// The text field uses this to place its caret and selection handles on the
988/// correct visual line for wrapped text: the in-content caret otherwise counts
989/// only logical `\n` lines, so a caret on a wrapped line's second visual line is
990/// drawn on the first (and its x, being the whole logical-line prefix width,
991/// runs off the right edge and is clipped), while typing and the magnifier land
992/// on the correct spot.
993pub fn wrapped_line_ranges(
994    node_id: Option<NodeId>,
995    text: &crate::text::AnnotatedString,
996    style: &TextStyle,
997    options: TextLayoutOptions,
998    max_width: Option<f32>,
999) -> Vec<Range<usize>> {
1000    with_system_font_scale(text, style, |text, style| {
1001        crate::render_state::with_text_measurer(|m| {
1002            wrapped_line_ranges_with_measurer(m, node_id, text, style, options, max_width)
1003        })
1004    })
1005}
1006
1007fn wrapped_line_ranges_with_measurer<M: TextMeasurer + ?Sized>(
1008    measurer: &M,
1009    _node_id: Option<NodeId>,
1010    text: &crate::text::AnnotatedString,
1011    style: &TextStyle,
1012    options: TextLayoutOptions,
1013    max_width: Option<f32>,
1014) -> Vec<Range<usize>> {
1015    let opts = options.normalized();
1016    let max_width = normalize_max_width(max_width);
1017    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1018        .then_some(max_width)
1019        .flatten();
1020    let line_break_mode = style
1021        .paragraph_style
1022        .line_break
1023        .take_or_else(|| LineBreak::Simple);
1024    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1025
1026    let line_ranges = split_line_ranges(text.text.as_str());
1027    let Some(width_limit) = wrap_width else {
1028        return line_ranges;
1029    };
1030    let mut lines = Vec::with_capacity(line_ranges.len());
1031    for line_range in line_ranges {
1032        wrap_line_to_width(
1033            measurer,
1034            text,
1035            line_range,
1036            style,
1037            (width_limit, &mut None),
1038            (line_break_mode, hyphens_mode),
1039            &mut lines,
1040        );
1041    }
1042    lines.into_iter().map(|line| line.source_range).collect()
1043}
1044
1045fn prepare_text_layout_fallback<M: TextMeasurer + ?Sized>(
1046    measurer: &M,
1047    text: &crate::text::AnnotatedString,
1048    style: &TextStyle,
1049    options: TextLayoutOptions,
1050    max_width: Option<f32>,
1051) -> PreparedTextLayout {
1052    prepare_text_layout_with_measurer_for_node(measurer, None, text, style, options, max_width)
1053}
1054
1055pub fn prepare_text_layout_with_measurer_for_node<M: TextMeasurer + ?Sized>(
1056    measurer: &M,
1057    node_id: Option<NodeId>,
1058    text: &crate::text::AnnotatedString,
1059    style: &TextStyle,
1060    options: TextLayoutOptions,
1061    max_width: Option<f32>,
1062) -> PreparedTextLayout {
1063    let telemetry = text_layout_telemetry_enabled();
1064    let total_start = telemetry.then(Instant::now);
1065    let opts = options.normalized();
1066    let max_width = normalize_max_width(max_width);
1067    if let Some(min_font_size_sp) = opts.overflow.scale_down_min_font_size_sp() {
1068        return prepare_scale_down_text_layout(
1069            measurer,
1070            node_id,
1071            text,
1072            style,
1073            opts,
1074            max_width,
1075            min_font_size_sp,
1076        );
1077    }
1078
1079    let wrap_width = (opts.soft_wrap && opts.overflow != TextOverflow::Visible)
1080        .then_some(max_width)
1081        .flatten();
1082    let line_break_mode = style
1083        .paragraph_style
1084        .line_break
1085        .take_or_else(|| LineBreak::Simple);
1086    let hyphens_mode = style.paragraph_style.hyphens.take_or_else(|| Hyphens::None);
1087
1088    let wrap_start = telemetry.then(Instant::now);
1089    let line_ranges = split_line_ranges(text.text.as_str());
1090    let source_line_count = line_ranges.len();
1091    let mut visible_lines: Vec<DisplayLine>;
1092    let mut wrap_hold = None;
1093    if let Some(width_limit) = wrap_width {
1094        (visible_lines, wrap_hold) = wrap_lines(
1095            measurer,
1096            text,
1097            line_ranges,
1098            style,
1099            width_limit,
1100            (line_break_mode, hyphens_mode),
1101        );
1102    } else {
1103        visible_lines = line_ranges
1104            .into_iter()
1105            .map(DisplayLine::from_source_range)
1106            .collect();
1107    }
1108    let wrap_ms = wrap_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1109
1110    let overflow_start = telemetry.then(Instant::now);
1111    let did_overflow = apply_overflow(
1112        measurer,
1113        node_id,
1114        text,
1115        style,
1116        opts,
1117        max_width,
1118        &mut visible_lines,
1119    );
1120    let overflow_ms = overflow_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1121
1122    let build_start = telemetry.then(Instant::now);
1123    let display_annotated = build_display_annotated(text, &visible_lines);
1124    debug_assert_eq!(
1125        display_annotated.text,
1126        join_display_line_text(text, &visible_lines)
1127    );
1128    let build_ms = build_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1129
1130    let metrics_start = telemetry.then(Instant::now);
1131    let display_line_count = visible_lines.len().max(1);
1132    let layout_line_count = display_line_count.max(opts.min_lines);
1133
1134    let measured_width = if visible_lines.is_empty() {
1135        0.0
1136    } else {
1137        visible_lines
1138            .iter()
1139            .map(|line| line.measure_width(measurer, node_id, text, style))
1140            .fold(0.0_f32, f32::max)
1141    };
1142    let metrics_ms = metrics_start.map(|start| start.elapsed().as_secs_f64() * 1000.0);
1143    let width = if opts.overflow == TextOverflow::Visible {
1144        measured_width
1145    } else if let Some(width_limit) = max_width {
1146        measured_width.min(width_limit)
1147    } else {
1148        measured_width
1149    };
1150    let wrap_hold = WrapHold::settle(wrap_hold, measured_width, wrap_width);
1151
1152    let vertical = prepared_line_metrics(
1153        measurer,
1154        node_id,
1155        text,
1156        &display_annotated,
1157        style,
1158        opts.min_lines,
1159    );
1160    let prepared = PreparedTextLayout {
1161        text: Rc::new(display_annotated),
1162        visual_style: std::sync::Arc::new(style.clone()),
1163        alignment_lines: vertical.alignment_lines,
1164        metrics: TextMetrics {
1165            width,
1166            height: vertical.height,
1167            line_height: vertical.line_height,
1168            line_count: layout_line_count,
1169        },
1170        did_overflow,
1171        render_text: Default::default(),
1172        wrap_hold,
1173    };
1174
1175    if let Some(start) = total_start {
1176        eprintln!(
1177            "[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}",
1178            text.text.len(),
1179            text.span_styles.len(),
1180            source_line_count,
1181            display_line_count,
1182            wrap_width.is_some(),
1183            max_width,
1184            wrap_ms.unwrap_or(0.0),
1185            overflow_ms.unwrap_or(0.0),
1186            build_ms.unwrap_or(0.0),
1187            metrics_ms.unwrap_or(0.0),
1188            start.elapsed().as_secs_f64() * 1000.0,
1189        );
1190    }
1191
1192    prepared
1193}
1194
1195struct PreparedLineMetrics {
1196    height: f32,
1197    line_height: f32,
1198    alignment_lines: cranpose_ui_layout::AlignmentLines,
1199}
1200
1201fn prepared_line_metrics<M: TextMeasurer + ?Sized>(
1202    measurer: &M,
1203    node_id: Option<NodeId>,
1204    source: &crate::text::AnnotatedString,
1205    display: &crate::text::AnnotatedString,
1206    style: &TextStyle,
1207    min_lines: usize,
1208) -> PreparedLineMetrics {
1209    let base_box = measurer.line_box(style);
1210    if !display.span_styles.is_empty() {
1211        let mut top = 0.0;
1212        let mut trim_bottom = 0.0;
1213        let mut first = None;
1214        let mut last = None;
1215        let mut line_height = 0.0_f32;
1216        let resolved = measurer.visit_line_boxes(display, style, &mut |line| {
1217            if first.is_none() {
1218                top = -line.trim_top;
1219                first = Some(top + line.baseline);
1220            }
1221            last = Some(top + line.baseline);
1222            top += line.height;
1223            trim_bottom = line.trim_bottom;
1224            line_height = line_height.max(line.height);
1225        });
1226        if resolved.is_some() {
1227            let min_height = if min_lines > 1 {
1228                base_box.map_or(0.0, |line| line.block_height(min_lines))
1229            } else {
1230                0.0
1231            };
1232            return PreparedLineMetrics {
1233                height: (top - trim_bottom).max(min_height),
1234                line_height,
1235                alignment_lines: cranpose_ui_layout::AlignmentLines::new(first, last),
1236            };
1237        }
1238    }
1239    let measured_text = if source.span_styles.is_empty() {
1240        source
1241    } else {
1242        display
1243    };
1244    let line_height = measurer
1245        .line_height_for_node(node_id, measured_text, style)
1246        .max(0.0);
1247    let first = base_box
1248        .map(crate::text::LineBox::first_baseline)
1249        .or_else(|| measurer.first_baseline(style));
1250    let displayed_lines = display.text.split('\n').count().max(1);
1251    let layout_line_count = displayed_lines.max(min_lines);
1252    let edges = base_box.unwrap_or_else(|| crate::text::LineBox::untrimmed(line_height, 0.0));
1253    PreparedLineMetrics {
1254        height: (layout_line_count as f32 * line_height - edges.trim_top - edges.trim_bottom)
1255            .max(0.0),
1256        line_height,
1257        alignment_lines: cranpose_ui_layout::AlignmentLines::new(
1258            first,
1259            first.map(|first| first + (displayed_lines - 1) as f32 * line_height),
1260        ),
1261    }
1262}
1263
1264fn prepare_scale_down_text_layout<M: TextMeasurer + ?Sized>(
1265    measurer: &M,
1266    node_id: Option<NodeId>,
1267    text: &crate::text::AnnotatedString,
1268    style: &TextStyle,
1269    options: TextLayoutOptions,
1270    max_width: Option<f32>,
1271    min_font_size_sp: f32,
1272) -> PreparedTextLayout {
1273    let clipped_options = TextLayoutOptions {
1274        overflow: TextOverflow::Clip,
1275        ..options
1276    }
1277    .normalized();
1278
1279    let full_size = prepare_scaled_text_layout(
1280        measurer,
1281        node_id,
1282        text,
1283        style,
1284        clipped_options,
1285        max_width,
1286        FontScaleCurve::linear(1.0),
1287    );
1288    let Some(width_limit) = max_width else {
1289        return full_size;
1290    };
1291    if !full_size.did_overflow {
1292        return full_size;
1293    }
1294
1295    let base_font_size = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
1296    if !base_font_size.is_finite() || base_font_size <= 0.0 {
1297        return full_size;
1298    }
1299    let min_scale = (min_font_size_sp.min(base_font_size) / base_font_size).clamp(0.0, 1.0);
1300    if min_scale >= 1.0 {
1301        return full_size;
1302    }
1303
1304    let min_size = prepare_scaled_text_layout(
1305        measurer,
1306        node_id,
1307        text,
1308        style,
1309        clipped_options,
1310        Some(width_limit),
1311        FontScaleCurve::linear(min_scale),
1312    );
1313    if min_size.did_overflow {
1314        return min_size;
1315    }
1316
1317    let mut low = min_scale;
1318    let mut high = 1.0;
1319    let mut best = min_size;
1320    for _ in 0..SCALE_DOWN_SEARCH_STEPS {
1321        let mid = (low + high) * 0.5;
1322        let candidate = prepare_scaled_text_layout(
1323            measurer,
1324            node_id,
1325            text,
1326            style,
1327            clipped_options,
1328            Some(width_limit),
1329            FontScaleCurve::linear(mid),
1330        );
1331        if candidate.did_overflow {
1332            high = mid;
1333        } else {
1334            low = mid;
1335            best = candidate;
1336        }
1337    }
1338
1339    best
1340}
1341
1342fn prepare_scaled_text_layout<M: TextMeasurer + ?Sized>(
1343    measurer: &M,
1344    node_id: Option<NodeId>,
1345    text: &crate::text::AnnotatedString,
1346    style: &TextStyle,
1347    options: TextLayoutOptions,
1348    max_width: Option<f32>,
1349    shrink: FontScaleCurve,
1350) -> PreparedTextLayout {
1351    let visual_style = scale_text_style_font_sizes(style, shrink);
1352    let visual_text = scale_annotated_font_sizes(text, shrink);
1353    prepare_text_layout_with_measurer_for_node(
1354        measurer,
1355        node_id,
1356        visual_text.as_ref(),
1357        &visual_style,
1358        options,
1359        max_width,
1360    )
1361}
1362
1363fn scale_annotated_font_sizes(
1364    text: &crate::text::AnnotatedString,
1365    curve: FontScaleCurve,
1366) -> Cow<'_, crate::text::AnnotatedString> {
1367    if curve.is_identity() || !annotated_text_needs_scaling(text) {
1368        return Cow::Borrowed(text);
1369    }
1370
1371    let mut scaled = text.clone();
1372    for span in &mut scaled.span_styles {
1373        span.item = scale_span_style_font_sizes(&span.item, curve, None);
1374    }
1375    Cow::Owned(scaled)
1376}
1377
1378fn scale_text_style_font_sizes(style: &TextStyle, curve: FontScaleCurve) -> TextStyle {
1379    if curve.is_identity() {
1380        return style.clone();
1381    }
1382
1383    let mut scaled = style.clone();
1384    scaled.span_style =
1385        scale_span_style_font_sizes(&style.span_style, curve, Some(DEFAULT_FONT_SIZE_SP));
1386    scaled.paragraph_style.line_height =
1387        scale_text_unit_sp(scaled.paragraph_style.line_height, curve);
1388    if let Some(mut indent) = scaled.paragraph_style.text_indent {
1389        indent.first_line = scale_text_unit_sp(indent.first_line, curve);
1390        indent.rest_line = scale_text_unit_sp(indent.rest_line, curve);
1391        scaled.paragraph_style.text_indent = Some(indent);
1392    }
1393    scaled
1394}
1395
1396fn with_system_font_scale<R>(
1397    text: &crate::text::AnnotatedString,
1398    style: &TextStyle,
1399    block: impl FnOnce(&crate::text::AnnotatedString, &TextStyle) -> R,
1400) -> R {
1401    let curve = crate::current_font_scale_curve();
1402    let visual_style = scale_text_style_font_sizes(style, curve);
1403    let visual_text = scale_annotated_font_sizes(text, curve);
1404    block(visual_text.as_ref(), &visual_style)
1405}
1406
1407fn scale_span_style_font_sizes(
1408    style: &crate::text::SpanStyle,
1409    curve: FontScaleCurve,
1410    default_font_size_sp: Option<f32>,
1411) -> crate::text::SpanStyle {
1412    let factor = curve.scale();
1413    let mut scaled = style.clone();
1414    scaled.font_size = match (style.font_size, default_font_size_sp) {
1415        (crate::text::TextUnit::Unspecified, Some(default_size)) => {
1416            crate::text::TextUnit::Sp(curve.sp_to_dp(default_size))
1417        }
1418        (unit, Some(_)) => scale_text_unit_sp_and_em(unit, curve),
1419        (unit, None) => scale_text_unit_sp(unit, curve),
1420    };
1421    scaled.letter_spacing = scale_text_unit_sp(scaled.letter_spacing, curve);
1422    if let Some(mut shadow) = scaled.shadow {
1423        shadow.offset.x = scale_finite_dimension(shadow.offset.x, factor);
1424        shadow.offset.y = scale_finite_dimension(shadow.offset.y, factor);
1425        shadow.blur_radius = scale_finite_dimension(shadow.blur_radius, factor);
1426        scaled.shadow = Some(shadow);
1427    }
1428    if let Some(crate::text::TextDrawStyle::Stroke { width }) = scaled.draw_style {
1429        scaled.draw_style = Some(crate::text::TextDrawStyle::Stroke {
1430            width: width * factor,
1431        });
1432    }
1433    scaled
1434}
1435
1436fn annotated_text_needs_scaling(text: &crate::text::AnnotatedString) -> bool {
1437    text.span_styles
1438        .iter()
1439        .any(|span| span_style_needs_scaling(&span.item))
1440}
1441
1442fn span_style_needs_scaling(style: &crate::text::SpanStyle) -> bool {
1443    matches!(style.font_size, crate::text::TextUnit::Sp(value) if value.is_finite())
1444        || matches!(style.letter_spacing, crate::text::TextUnit::Sp(value) if value.is_finite())
1445        || matches!(
1446            style.draw_style,
1447            Some(crate::text::TextDrawStyle::Stroke { .. })
1448        )
1449        || style.shadow.is_some()
1450}
1451
1452fn scale_text_unit_sp(unit: crate::text::TextUnit, curve: FontScaleCurve) -> crate::text::TextUnit {
1453    match unit {
1454        crate::text::TextUnit::Sp(value) if value.is_finite() => {
1455            crate::text::TextUnit::Sp(curve.sp_to_dp(value))
1456        }
1457        other => other,
1458    }
1459}
1460
1461fn scale_text_unit_sp_and_em(
1462    unit: crate::text::TextUnit,
1463    curve: FontScaleCurve,
1464) -> crate::text::TextUnit {
1465    match unit {
1466        crate::text::TextUnit::Sp(_) => scale_text_unit_sp(unit, curve),
1467        crate::text::TextUnit::Em(value) if value.is_finite() => {
1468            crate::text::TextUnit::Em(value * curve.scale())
1469        }
1470        other => other,
1471    }
1472}
1473
1474fn scale_finite_dimension(value: f32, factor: f32) -> f32 {
1475    if value.is_finite() {
1476        value * factor
1477    } else {
1478        value
1479    }
1480}
1481
1482#[derive(Clone, Debug)]
1483enum DisplayLineText {
1484    Source,
1485    Ellipsized(crate::text::AnnotatedString),
1486}
1487
1488#[derive(Clone, Debug)]
1489struct DisplayLine {
1490    source_range: Range<usize>,
1491    text: DisplayLineText,
1492    measured_width: Option<f32>,
1493}
1494
1495impl DisplayLine {
1496    fn from_source_range(source_range: Range<usize>) -> Self {
1497        Self {
1498            source_range,
1499            text: DisplayLineText::Source,
1500            measured_width: None,
1501        }
1502    }
1503
1504    fn from_measured_source_range(source_range: Range<usize>, measured_width: f32) -> Self {
1505        Self {
1506            source_range,
1507            text: DisplayLineText::Source,
1508            measured_width: measured_width
1509                .is_finite()
1510                .then_some(measured_width.max(0.0)),
1511        }
1512    }
1513
1514    fn display_text<'a>(&'a self, source: &'a crate::text::AnnotatedString) -> &'a str {
1515        match &self.text {
1516            DisplayLineText::Source => &source.text[self.source_range.clone()],
1517            DisplayLineText::Ellipsized(annotated) => annotated.text.as_str(),
1518        }
1519    }
1520
1521    fn measure_width<M: TextMeasurer + ?Sized>(
1522        &self,
1523        measurer: &M,
1524        node_id: Option<NodeId>,
1525        source: &crate::text::AnnotatedString,
1526        style: &TextStyle,
1527    ) -> f32 {
1528        self.measured_width.unwrap_or_else(|| match &self.text {
1529            DisplayLineText::Source => {
1530                measurer
1531                    .measure_subsequence_for_node(node_id, source, self.source_range.clone(), style)
1532                    .width
1533            }
1534            DisplayLineText::Ellipsized(annotated) => {
1535                measurer.measure_for_node(node_id, annotated, style).width
1536            }
1537        })
1538    }
1539
1540    fn extend_to_paragraph_end(&mut self, source: &crate::text::AnnotatedString) {
1541        let start = self.source_range.start;
1542        let end = source.text[start..]
1543            .find('\n')
1544            .map_or(source.text.len(), |offset| start + offset);
1545        self.source_range = start..end;
1546        self.text = DisplayLineText::Source;
1547        self.measured_width = None;
1548    }
1549
1550    fn ellipsize<M: TextMeasurer + ?Sized>(
1551        &mut self,
1552        measurer: &M,
1553        node_id: Option<NodeId>,
1554        source: &crate::text::AnnotatedString,
1555        style: &TextStyle,
1556        max_width: Option<f32>,
1557        placement: EllipsisPlacement,
1558    ) {
1559        *self = fit_ellipsis(
1560            measurer,
1561            node_id,
1562            source,
1563            self.source_range.clone(),
1564            style,
1565            max_width,
1566            placement,
1567        );
1568    }
1569}
1570
1571fn split_line_ranges(text: &str) -> Vec<Range<usize>> {
1572    if text.is_empty() {
1573        return single_line_range(0..0);
1574    }
1575
1576    let mut ranges = Vec::new();
1577    let mut start = 0usize;
1578    for (idx, ch) in text.char_indices() {
1579        if ch == '\n' {
1580            ranges.push(start..idx);
1581            start = idx + ch.len_utf8();
1582        }
1583    }
1584    ranges.push(start..text.len());
1585    ranges
1586}
1587
1588fn build_display_annotated(
1589    source: &crate::text::AnnotatedString,
1590    lines: &[DisplayLine],
1591) -> crate::text::AnnotatedString {
1592    if lines.is_empty() {
1593        return crate::text::AnnotatedString::from("");
1594    }
1595
1596    let mut builder = crate::text::AnnotatedString::builder();
1597    for (idx, line) in lines.iter().enumerate() {
1598        builder = match &line.text {
1599            DisplayLineText::Source => {
1600                builder.append_annotated_subsequence(source, line.source_range.clone())
1601            }
1602            DisplayLineText::Ellipsized(annotated) => builder.append_annotated(annotated),
1603        };
1604        if idx + 1 < lines.len() {
1605            builder = builder.append("\n");
1606        }
1607    }
1608    builder.to_annotated_string()
1609}
1610
1611fn join_display_line_text(source: &crate::text::AnnotatedString, lines: &[DisplayLine]) -> String {
1612    let mut text = String::new();
1613    for (idx, line) in lines.iter().enumerate() {
1614        text.push_str(line.display_text(source));
1615        if idx + 1 < lines.len() {
1616            text.push('\n');
1617        }
1618    }
1619    text
1620}
1621
1622fn trim_segment_end_whitespace(line: &str, start: usize, mut end: usize) -> usize {
1623    while end > start {
1624        let Some((idx, ch)) = line[start..end].char_indices().next_back() else {
1625            break;
1626        };
1627        if ch.is_whitespace() {
1628            end = start + idx;
1629        } else {
1630            break;
1631        }
1632    }
1633    end
1634}
1635
1636/// The max widths a prepared layout comes out the same for, so a node whose
1637/// width moves can keep one layout instead of preparing it again.
1638#[derive(Clone, Copy, Debug, PartialEq)]
1639pub(crate) enum PreparedWidths {
1640    /// Only the width it was prepared at; `None` is unconstrained.
1641    Exact(Option<u32>),
1642    /// No line wrapped or overflowed: unconstrained, and every width from
1643    /// its measured width up. A narrower width may wrap, so it is not held.
1644    AtLeast(f32),
1645    /// Lines wrapped greedily, and every width that breaks them the same.
1646    Wrapped(WrapHold),
1647}
1648
1649impl PreparedWidths {
1650    /// The widths `prepared`, made from `text` at `max_width`, holds for.
1651    pub(crate) fn of(
1652        text: &crate::text::AnnotatedString,
1653        options: TextLayoutOptions,
1654        max_width: Option<f32>,
1655        prepared: &PreparedTextLayout,
1656    ) -> Self {
1657        let max_width = normalize_max_width(max_width);
1658        let exact = Self::Exact(max_width.map(f32::to_bits));
1659        let wrapped = prepared.text.text.matches('\n').count() != text.text.matches('\n').count();
1660        // A line's trailing spaces count when it is fitted but not in the
1661        // width it reports, so such a line may wrap at its own width.
1662        let trailing_space = text
1663            .text
1664            .split('\n')
1665            .any(|line| line.ends_with(char::is_whitespace));
1666        if options
1667            .normalized()
1668            .overflow
1669            .scale_down_min_font_size_sp()
1670            .is_some()
1671            || prepared.did_overflow
1672            || trailing_space
1673        {
1674            return exact;
1675        }
1676        if wrapped {
1677            return match (prepared.wrap_hold, max_width) {
1678                (Some(hold), Some(width)) if hold.holds(width) => Self::Wrapped(hold),
1679                _ => exact,
1680            };
1681        }
1682        match max_width {
1683            Some(width) if prepared.metrics.width >= width => exact,
1684            _ => Self::AtLeast(prepared.metrics.width),
1685        }
1686    }
1687
1688    /// Whether preparing at `max_width` gives the same layout.
1689    pub(crate) fn hold(self, max_width: Option<f32>) -> bool {
1690        let max_width = normalize_max_width(max_width);
1691        match self {
1692            Self::Exact(bits) => max_width.map(f32::to_bits) == bits,
1693            Self::AtLeast(min) => max_width.is_none_or(|width| width >= min),
1694            Self::Wrapped(hold) => max_width.is_some_and(|width| hold.holds(width)),
1695        }
1696    }
1697}
1698
1699fn normalize_max_width(max_width: Option<f32>) -> Option<f32> {
1700    match max_width {
1701        Some(width) if width.is_finite() && width > 0.0 => Some(width),
1702        _ => None,
1703    }
1704}
1705
1706fn absolute_range_from_start(base_start: usize, relative: Range<usize>) -> Range<usize> {
1707    (base_start + relative.start)..(base_start + relative.end)
1708}
1709
1710fn boundary_index_for_byte(boundaries: &[usize], byte_offset: usize) -> usize {
1711    boundaries
1712        .binary_search(&byte_offset)
1713        .unwrap_or_else(|index| index.min(boundaries.len().saturating_sub(1)))
1714}
1715
1716fn single_line_range(range: Range<usize>) -> Vec<Range<usize>> {
1717    std::iter::once(range).collect()
1718}
1719
1720struct LineMeasureContext<'a, M: TextMeasurer + ?Sized> {
1721    measurer: &'a M,
1722    text: &'a crate::text::AnnotatedString,
1723    style: &'a TextStyle,
1724    line_start: usize,
1725    prefix_widths: Option<TextLinePrefixWidths>,
1726}
1727
1728impl<'a, M: TextMeasurer + ?Sized> LineMeasureContext<'a, M> {
1729    fn new(
1730        measurer: &'a M,
1731        text: &'a crate::text::AnnotatedString,
1732        line_range: &Range<usize>,
1733        style: &'a TextStyle,
1734        boundary_count: usize,
1735    ) -> Self {
1736        let expected_chars = boundary_count.saturating_sub(1);
1737        let prefix_widths = measurer
1738            .measure_line_prefix_widths(text, line_range.clone(), style)
1739            .filter(|widths| widths.char_count() == expected_chars);
1740        Self {
1741            measurer,
1742            text,
1743            style,
1744            line_start: line_range.start,
1745            prefix_widths,
1746        }
1747    }
1748
1749    fn measure_char_range(&self, boundaries: &[usize], start_idx: usize, end_idx: usize) -> f32 {
1750        if let Some(width) = self.prefix_width_for_char_range(start_idx, end_idx) {
1751            return width;
1752        }
1753        let segment_range =
1754            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1755        self.measurer
1756            .measure_subsequence(self.text, segment_range, self.style)
1757            .width
1758    }
1759
1760    fn prefix_width_for_char_range(&self, start_idx: usize, end_idx: usize) -> Option<f32> {
1761        if let Some(prefix_widths) = &self.prefix_widths
1762            && let Some(width) = prefix_widths.width_for_char_range(start_idx, end_idx)
1763        {
1764            return Some(width);
1765        }
1766        None
1767    }
1768
1769    fn display_line_for_char_range(
1770        &self,
1771        boundaries: &[usize],
1772        start_idx: usize,
1773        end_idx: usize,
1774    ) -> DisplayLine {
1775        let source_range =
1776            absolute_range_from_start(self.line_start, boundaries[start_idx]..boundaries[end_idx]);
1777        let measured_width = self.measure_char_range(boundaries, start_idx, end_idx);
1778        DisplayLine::from_measured_source_range(source_range, measured_width)
1779    }
1780}
1781
1782/// The display lines `line_ranges` wrap into at `max_width`, and when any
1783/// wrapped greedily, the widths that wrap them the same.
1784fn wrap_lines<M: TextMeasurer + ?Sized>(
1785    measurer: &M,
1786    text: &crate::text::AnnotatedString,
1787    line_ranges: Vec<Range<usize>>,
1788    style: &TextStyle,
1789    max_width: f32,
1790    modes: (LineBreak, Hyphens),
1791) -> (Vec<DisplayLine>, Option<WrapHold>) {
1792    let source_lines = line_ranges.len();
1793    let mut lines = Vec::with_capacity(source_lines);
1794    let mut hold = Some(WrapHold::ANY);
1795    for line_range in line_ranges {
1796        wrap_line_to_width(
1797            measurer,
1798            text,
1799            line_range,
1800            style,
1801            (max_width, &mut hold),
1802            modes,
1803            &mut lines,
1804        );
1805    }
1806    let wrapped = lines.len() != source_lines;
1807    (lines, hold.filter(|_| wrapped))
1808}
1809
1810/// Appends the display lines `line_range` wraps into at `max_width` to
1811/// `out`: most lines fit whole, and take no allocation of their own. Narrows
1812/// `hold` to the widths that wrap it the same, or clears it when the wrap is
1813/// not greedy.
1814fn wrap_line_to_width<M: TextMeasurer + ?Sized>(
1815    measurer: &M,
1816    text: &crate::text::AnnotatedString,
1817    line_range: Range<usize>,
1818    style: &TextStyle,
1819    (max_width, hold): (f32, &mut Option<WrapHold>),
1820    (line_break, hyphens): (LineBreak, Hyphens),
1821    out: &mut Vec<DisplayLine>,
1822) {
1823    let line_text = &text.text[line_range.clone()];
1824    if line_text.is_empty() {
1825        out.push(DisplayLine::from_source_range(
1826            line_range.start..line_range.start,
1827        ));
1828        return;
1829    }
1830
1831    if let Some(measured_width) = measurer.measure_line_width(text, line_range.clone(), style)
1832        && measured_width <= max_width + WRAP_EPSILON
1833    {
1834        WrapHold::fit_whole(hold, measured_width);
1835        out.push(DisplayLine::from_measured_source_range(
1836            line_range,
1837            measured_width,
1838        ));
1839        return;
1840    }
1841
1842    if matches!(line_break, LineBreak::Heading | LineBreak::Paragraph)
1843        && line_text.chars().any(char::is_whitespace)
1844        && wrap_line_with_word_balance(
1845            measurer,
1846            text,
1847            line_range.clone(),
1848            style,
1849            max_width,
1850            line_break,
1851            out,
1852        )
1853    {
1854        *hold = None;
1855        return;
1856    }
1857
1858    wrap_line_greedy(
1859        measurer,
1860        text,
1861        line_range,
1862        style,
1863        (max_width, hold),
1864        (line_break, hyphens),
1865        out,
1866    );
1867}
1868
1869fn wrap_line_greedy<M: TextMeasurer + ?Sized>(
1870    measurer: &M,
1871    text: &crate::text::AnnotatedString,
1872    line_range: Range<usize>,
1873    style: &TextStyle,
1874    (max_width, hold): (f32, &mut Option<WrapHold>),
1875    (line_break, hyphens): (LineBreak, Hyphens),
1876    out: &mut Vec<DisplayLine>,
1877) {
1878    let line_text = &text.text[line_range.clone()];
1879    let boundaries = char_boundaries(line_text);
1880    let measure_context =
1881        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1882    if let Some(measured_width) =
1883        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1884        && measured_width <= max_width + WRAP_EPSILON
1885    {
1886        WrapHold::fit_whole(hold, measured_width);
1887        out.push(DisplayLine::from_measured_source_range(
1888            line_range,
1889            measured_width,
1890        ));
1891        return;
1892    }
1893    let first = out.len();
1894    let mut start_idx = 0usize;
1895
1896    while start_idx < boundaries.len() - 1 {
1897        let mut low = start_idx + 1;
1898        let mut high = boundaries.len() - 1;
1899        let mut best = start_idx + 1;
1900
1901        while low <= high {
1902            let mid = (low + high) / 2;
1903            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
1904            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
1905                best = mid;
1906                low = mid + 1;
1907            } else {
1908                if mid == 0 {
1909                    break;
1910                }
1911                high = mid - 1;
1912            }
1913        }
1914
1915        let wrap_idx = choose_wrap_break(line_text, &boundaries, start_idx, best, line_break);
1916        let mut effective_wrap_idx = wrap_idx;
1917        let can_hyphenate = hyphens == Hyphens::Auto
1918            && wrap_idx == best
1919            && best < boundaries.len() - 1
1920            && is_break_inside_word(line_text, &boundaries, wrap_idx);
1921        narrow_to_break(
1922            hold,
1923            &measure_context,
1924            (line_text, &boundaries),
1925            (start_idx, best),
1926            can_hyphenate,
1927        );
1928        if can_hyphenate {
1929            effective_wrap_idx = resolve_auto_hyphen_break(
1930                measurer,
1931                line_text,
1932                style,
1933                &boundaries,
1934                start_idx,
1935                wrap_idx,
1936            );
1937        }
1938
1939        let broke_at_word_boundary = effective_wrap_idx > start_idx
1940            && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
1941                .chars()
1942                .all(char::is_whitespace);
1943        let segment_start = boundaries[start_idx];
1944        let mut segment_end = boundaries[effective_wrap_idx];
1945        if wrap_idx != best || broke_at_word_boundary {
1946            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
1947        }
1948        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
1949        out.push(measure_context.display_line_for_char_range(
1950            &boundaries,
1951            start_idx,
1952            segment_end_idx,
1953        ));
1954
1955        start_idx = if wrap_idx != best || broke_at_word_boundary {
1956            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
1957        } else {
1958            effective_wrap_idx
1959        };
1960    }
1961
1962    if out.len() == first {
1963        out.push(DisplayLine::from_source_range(
1964            line_range.start..line_range.start,
1965        ));
1966    }
1967}
1968
1969/// Appends the word-balanced lines of `line_range` to `out` and returns
1970/// whether it could balance them; when it cannot, `out` is left as it was.
1971fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
1972    measurer: &M,
1973    text: &crate::text::AnnotatedString,
1974    line_range: Range<usize>,
1975    style: &TextStyle,
1976    max_width: f32,
1977    line_break: LineBreak,
1978    out: &mut Vec<DisplayLine>,
1979) -> bool {
1980    let line_text = &text.text[line_range.clone()];
1981    let boundaries = char_boundaries(line_text);
1982    let measure_context =
1983        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
1984    if let Some(measured_width) =
1985        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
1986        && measured_width <= max_width + WRAP_EPSILON
1987    {
1988        out.push(DisplayLine::from_measured_source_range(
1989            line_range,
1990            measured_width,
1991        ));
1992        return true;
1993    }
1994    let breakpoints = collect_word_breakpoints(line_text, &boundaries);
1995    if breakpoints.len() <= 2 {
1996        return false;
1997    }
1998
1999    let node_count = breakpoints.len();
2000    let mut best_cost = vec![f32::INFINITY; node_count];
2001    let mut next_index = vec![None; node_count];
2002    best_cost[node_count - 1] = 0.0;
2003
2004    for start in (0..node_count - 1).rev() {
2005        for end in start + 1..node_count {
2006            let start_byte = boundaries[breakpoints[start]];
2007            let end_byte = boundaries[breakpoints[end]];
2008            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2009            if trimmed_end <= start_byte {
2010                continue;
2011            }
2012            let segment_start_idx = breakpoints[start];
2013            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2014            let segment_width =
2015                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
2016            if segment_width > max_width + WRAP_EPSILON {
2017                continue;
2018            }
2019            if !best_cost[end].is_finite() {
2020                continue;
2021            }
2022            let slack = (max_width - segment_width).max(0.0);
2023            let is_last = end == node_count - 1;
2024            let segment_cost = match line_break {
2025                LineBreak::Heading => slack * slack,
2026                LineBreak::Paragraph => {
2027                    if is_last {
2028                        slack * slack * 0.16
2029                    } else {
2030                        slack * slack
2031                    }
2032                }
2033                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
2034            };
2035            let candidate = segment_cost + best_cost[end];
2036            if candidate < best_cost[start] {
2037                best_cost[start] = candidate;
2038                next_index[start] = Some(end);
2039            }
2040        }
2041    }
2042
2043    let first = out.len();
2044    let mut current = 0usize;
2045    while current < node_count - 1 {
2046        let Some(next) = next_index[current] else {
2047            out.truncate(first);
2048            return false;
2049        };
2050        let start_byte = boundaries[breakpoints[current]];
2051        let end_byte = boundaries[breakpoints[next]];
2052        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2053        if trimmed_end <= start_byte {
2054            out.truncate(first);
2055            return false;
2056        }
2057        let segment_start_idx = breakpoints[current];
2058        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2059        out.push(measure_context.display_line_for_char_range(
2060            &boundaries,
2061            segment_start_idx,
2062            segment_end_idx,
2063        ));
2064        current = next;
2065    }
2066    out.len() > first
2067}
2068
2069fn collect_word_breakpoints(line: &str, boundaries: &[usize]) -> Vec<usize> {
2070    let mut points = vec![0usize];
2071    for idx in 1..boundaries.len() - 1 {
2072        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2073        let current = &line[boundaries[idx]..boundaries[idx + 1]];
2074        if prev.chars().all(char::is_whitespace) && !current.chars().all(char::is_whitespace) {
2075            points.push(idx);
2076        }
2077    }
2078    let end = boundaries.len() - 1;
2079    if points.last().copied() != Some(end) {
2080        points.push(end);
2081    }
2082    points
2083}
2084
2085fn choose_wrap_break(
2086    line: &str,
2087    boundaries: &[usize],
2088    start_idx: usize,
2089    best: usize,
2090    _line_break: LineBreak,
2091) -> usize {
2092    if best >= boundaries.len() - 1 {
2093        return best;
2094    }
2095
2096    if best <= start_idx + 1 {
2097        return best;
2098    }
2099
2100    for idx in (start_idx + 1..=best).rev() {
2101        let prev = &line[boundaries[idx - 1]..boundaries[idx]];
2102        if prev.chars().all(char::is_whitespace) {
2103            return idx;
2104        }
2105    }
2106    best
2107}
2108
2109/// Narrows `hold` to the widths a line from `start_idx` breaks the same at
2110/// as it does where `best` characters fit, or clears it when the break was
2111/// hyphenated, which no width range describes.
2112fn narrow_to_break<M: TextMeasurer + ?Sized>(
2113    hold: &mut Option<WrapHold>,
2114    measure_context: &LineMeasureContext<'_, M>,
2115    (line, boundaries): (&str, &[usize]),
2116    (start_idx, best): (usize, usize),
2117    hyphenated: bool,
2118) {
2119    if hyphenated {
2120        *hold = None;
2121    }
2122    let Some(hold) = hold else {
2123        return;
2124    };
2125    let (fits, pulls_up) = wrap_break_widths(line, boundaries, start_idx, best);
2126    let width = |idx| measure_context.measure_char_range(boundaries, start_idx, idx);
2127    hold.narrow(
2128        fits.map_or(f32::NEG_INFINITY, width),
2129        pulls_up.map_or(f32::INFINITY, width),
2130    );
2131}
2132
2133/// The character counts, from `start_idx`, whose widths bound the widths
2134/// [`choose_wrap_break`] picks the same break at as it does for `best`: the
2135/// first a line must fit to keep its break (`None` when any width does),
2136/// and the first that would take the break past it (`None` when none
2137/// would).
2138fn wrap_break_widths(
2139    line: &str,
2140    boundaries: &[usize],
2141    start_idx: usize,
2142    best: usize,
2143) -> (Option<usize>, Option<usize>) {
2144    let end = boundaries.len() - 1;
2145    // A line takes its first character whether it fits or not.
2146    if best <= start_idx + 1 {
2147        return (None, (best < end).then_some(best + 1));
2148    }
2149    if best >= end {
2150        return (Some(end), None);
2151    }
2152    let after_space = |idx: usize| {
2153        line[boundaries[idx - 1]..boundaries[idx]]
2154            .chars()
2155            .all(char::is_whitespace)
2156    };
2157    match (start_idx + 1..=best).rev().find(|&idx| after_space(idx)) {
2158        Some(wrap_idx) => {
2159            let next = (best + 1..end).find(|&idx| after_space(idx)).unwrap_or(end);
2160            (Some(wrap_idx), Some(next))
2161        }
2162        None => (Some(best), Some(best + 1)),
2163    }
2164}
2165
2166fn is_break_inside_word(line: &str, boundaries: &[usize], break_idx: usize) -> bool {
2167    if break_idx == 0 || break_idx >= boundaries.len() - 1 {
2168        return false;
2169    }
2170    let prev = &line[boundaries[break_idx - 1]..boundaries[break_idx]];
2171    let next = &line[boundaries[break_idx]..boundaries[break_idx + 1]];
2172    !prev.chars().all(char::is_whitespace) && !next.chars().all(char::is_whitespace)
2173}
2174
2175fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2176    measurer: &M,
2177    line: &str,
2178    style: &TextStyle,
2179    boundaries: &[usize],
2180    start_idx: usize,
2181    break_idx: usize,
2182) -> usize {
2183    if let Some(candidate) = measurer.choose_auto_hyphen_break(line, style, start_idx, break_idx)
2184        && is_valid_auto_hyphen_break(line, boundaries, start_idx, break_idx, candidate)
2185    {
2186        return candidate;
2187    }
2188    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2189}
2190
2191fn is_valid_auto_hyphen_break(
2192    line: &str,
2193    boundaries: &[usize],
2194    start_idx: usize,
2195    break_idx: usize,
2196    candidate_idx: usize,
2197) -> bool {
2198    let end_idx = boundaries.len().saturating_sub(1);
2199    candidate_idx > start_idx
2200        && candidate_idx < end_idx
2201        && candidate_idx <= break_idx
2202        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2203        && is_break_inside_word(line, boundaries, candidate_idx)
2204}
2205
2206fn choose_auto_hyphen_break_fallback(
2207    boundaries: &[usize],
2208    start_idx: usize,
2209    break_idx: usize,
2210) -> usize {
2211    let end_idx = boundaries.len().saturating_sub(1);
2212    if break_idx >= end_idx {
2213        return break_idx;
2214    }
2215    let trailing_len = end_idx.saturating_sub(break_idx);
2216    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2217        return break_idx;
2218    }
2219
2220    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2221    let max_break = break_idx.saturating_sub(1);
2222    if min_break > max_break {
2223        return break_idx;
2224    }
2225
2226    let mut best_break = break_idx;
2227    let mut best_penalty = usize::MAX;
2228    for idx in min_break..=max_break {
2229        let candidate_trailing_len = end_idx.saturating_sub(idx);
2230        let candidate_prefix_len = idx.saturating_sub(start_idx);
2231        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2232            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2233        {
2234            continue;
2235        }
2236
2237        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2238        if penalty < best_penalty {
2239            best_penalty = penalty;
2240            best_break = idx;
2241            if penalty == 0 {
2242                break;
2243            }
2244        }
2245    }
2246    best_break
2247}
2248
2249fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2250    while idx < boundaries.len() - 1 {
2251        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2252        if !ch.chars().all(char::is_whitespace) {
2253            break;
2254        }
2255        idx += 1;
2256    }
2257    idx
2258}
2259
2260fn apply_overflow<M: TextMeasurer + ?Sized>(
2261    measurer: &M,
2262    node_id: Option<NodeId>,
2263    text: &crate::text::AnnotatedString,
2264    style: &TextStyle,
2265    options: TextLayoutOptions,
2266    max_width: Option<f32>,
2267    visible_lines: &mut Vec<DisplayLine>,
2268) -> bool {
2269    if options.overflow == TextOverflow::Visible {
2270        return false;
2271    }
2272    let ellipsis = EllipsisPlacement::for_options(options);
2273    let mut did_overflow = false;
2274    if visible_lines.len() > options.max_lines {
2275        did_overflow = true;
2276        visible_lines.truncate(options.max_lines);
2277        if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2278            last_line.extend_to_paragraph_end(text);
2279            last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2280        }
2281    }
2282
2283    let Some(width_limit) = max_width else {
2284        return did_overflow;
2285    };
2286    let visible_len = visible_lines.len();
2287    for (line_index, line) in visible_lines.iter_mut().enumerate() {
2288        if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2289            continue;
2290        }
2291        did_overflow = true;
2292        if line_index + 1 == visible_len
2293            && let Some(placement) = ellipsis
2294        {
2295            line.ellipsize(measurer, node_id, text, style, max_width, placement);
2296        }
2297    }
2298    did_overflow
2299}
2300
2301#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2302enum EllipsisPlacement {
2303    End,
2304    Start,
2305    Middle,
2306}
2307
2308impl EllipsisPlacement {
2309    /// How many of `kept_chars` stay before the ellipsis and how many after.
2310    fn split(self, kept_chars: usize) -> (usize, usize) {
2311        match self {
2312            Self::End => (kept_chars, 0),
2313            Self::Start => (0, kept_chars),
2314            Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2315        }
2316    }
2317
2318    /// The most characters an elided line can keep within `width_limit`,
2319    /// estimated from the line's prefix widths and the ellipsis's width
2320    /// without measuring any elided string. Shaping across the cut can move
2321    /// the real width a little either way, so callers confirm it.
2322    fn estimated_kept_chars(
2323        self,
2324        prefix_widths: &TextLinePrefixWidths,
2325        ellipsis_width: f32,
2326        width_limit: f32,
2327    ) -> Option<usize> {
2328        let char_count = prefix_widths.char_count();
2329        let width = |kept_chars: usize| {
2330            let (head_chars, tail_chars) = self.split(kept_chars);
2331            Some(
2332                prefix_widths.width_for_char_range(0, head_chars)?
2333                    + ellipsis_width
2334                    + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2335            )
2336        };
2337        let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2338        while fitting + 1 < overflowing {
2339            let kept_chars = fitting + (overflowing - fitting) / 2;
2340            if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2341                fitting = kept_chars;
2342            } else {
2343                overflowing = kept_chars;
2344            }
2345        }
2346        Some(fitting)
2347    }
2348
2349    fn for_options(options: TextLayoutOptions) -> Option<Self> {
2350        let single_line = options.max_lines == 1;
2351        match options.overflow {
2352            TextOverflow::Ellipsis => Some(Self::End),
2353            TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2354            TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2355            TextOverflow::StartEllipsis
2356            | TextOverflow::MiddleEllipsis
2357            | TextOverflow::Clip
2358            | TextOverflow::Visible
2359            | TextOverflow::ScaleDown { .. } => None,
2360        }
2361    }
2362
2363    fn elide(
2364        self,
2365        source: &crate::text::AnnotatedString,
2366        source_range: Range<usize>,
2367        boundaries: &[usize],
2368        kept_chars: usize,
2369    ) -> crate::text::AnnotatedString {
2370        let char_count = boundaries.len() - 1;
2371        let (head_chars, tail_chars) = self.split(kept_chars);
2372        let head_end = source_range.start + boundaries[head_chars];
2373        let tail_start = source_range.start + boundaries[char_count - tail_chars];
2374        crate::text::AnnotatedString::builder()
2375            .append_annotated_subsequence(source, source_range.start..head_end)
2376            .append(ELLIPSIS)
2377            .append_annotated_subsequence(source, tail_start..source_range.end)
2378            .to_annotated_string()
2379    }
2380}
2381
2382fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2383    measurer: &M,
2384    node_id: Option<NodeId>,
2385    source: &crate::text::AnnotatedString,
2386    source_range: Range<usize>,
2387    style: &TextStyle,
2388    max_width: Option<f32>,
2389    placement: EllipsisPlacement,
2390) -> DisplayLine {
2391    let width_limit = max_width.unwrap_or(f32::INFINITY);
2392    let fitting_line = |text: DisplayLineText| {
2393        let mut line = DisplayLine {
2394            source_range: source_range.clone(),
2395            text,
2396            measured_width: None,
2397        };
2398        let width = line.measure_width(measurer, node_id, source, style);
2399        line.measured_width = Some(width);
2400        (width <= width_limit + WRAP_EPSILON).then_some(line)
2401    };
2402    if placement != EllipsisPlacement::End
2403        && let Some(line) = fitting_line(DisplayLineText::Source)
2404    {
2405        return line;
2406    }
2407
2408    let boundaries = char_boundaries(&source.text[source_range.clone()]);
2409    let elided_line = |kept_chars: usize| {
2410        fitting_line(DisplayLineText::Ellipsized(placement.elide(
2411            source,
2412            source_range.clone(),
2413            &boundaries,
2414            kept_chars,
2415        )))
2416    };
2417    let Some(mut best) = elided_line(0) else {
2418        return DisplayLine {
2419            source_range: source_range.clone(),
2420            text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2421            measured_width: None,
2422        };
2423    };
2424
2425    let mut fitting = 0usize;
2426    let mut overflowing = boundaries.len();
2427    // The line's prefix widths place the cut without measuring an elided
2428    // string per guess; measuring the guess and the one past it confirms it,
2429    // and the search below only runs when shaping across the cut moved it.
2430    let guess = best.measured_width.and_then(|ellipsis_width| {
2431        measurer
2432            .measure_line_prefix_widths(source, source_range.clone(), style)
2433            .filter(|widths| widths.char_count() + 1 == boundaries.len())
2434            .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2435    });
2436    if let Some(guess) = guess.filter(|guess| *guess > 0) {
2437        for kept_chars in [guess, guess + 1] {
2438            if kept_chars <= fitting || kept_chars >= overflowing {
2439                break;
2440            }
2441            match elided_line(kept_chars) {
2442                Some(line) => {
2443                    fitting = kept_chars;
2444                    best = line;
2445                }
2446                None => overflowing = kept_chars,
2447            }
2448        }
2449    }
2450    while fitting + 1 < overflowing {
2451        let kept_chars = fitting + (overflowing - fitting) / 2;
2452        match elided_line(kept_chars) {
2453            Some(line) => {
2454                fitting = kept_chars;
2455                best = line;
2456            }
2457            None => overflowing = kept_chars,
2458        }
2459    }
2460    best
2461}
2462
2463fn char_boundaries(text: &str) -> Vec<usize> {
2464    let mut out = Vec::with_capacity(text.chars().count() + 1);
2465    out.push(0);
2466    for (idx, _) in text.char_indices() {
2467        if idx != 0 {
2468            out.push(idx);
2469        }
2470    }
2471    out.push(text.len());
2472    out
2473}
2474
2475#[cfg(test)]
2476#[path = "tests/measure_tests.rs"]
2477mod tests;