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