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