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