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