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