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