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