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 = cluster_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 breaks = LineBreaks::new(line_text, &boundaries);
2147    let mut start_idx = 0usize;
2148
2149    while start_idx < boundaries.len() - 1 {
2150        if limit.reached(out.len()) {
2151            out.push(DisplayLine::from_source_range(
2152                line_range.start + boundaries[start_idx]..line_range.end,
2153            ));
2154            return;
2155        }
2156        let mut low = start_idx + 1;
2157        let mut high = boundaries.len() - 1;
2158        let mut best = start_idx + 1;
2159
2160        while low <= high {
2161            let mid = (low + high) / 2;
2162            let width = measure_context.measure_char_range(&boundaries, start_idx, mid);
2163            if width <= max_width + WRAP_EPSILON || mid == start_idx + 1 {
2164                best = mid;
2165                low = mid + 1;
2166            } else {
2167                if mid == 0 {
2168                    break;
2169                }
2170                high = mid - 1;
2171            }
2172        }
2173
2174        let wrap_idx = choose_wrap_break(&breaks, start_idx, best, line_break);
2175        let mut effective_wrap_idx = wrap_idx;
2176        let can_hyphenate = hyphens == Hyphens::Auto
2177            && wrap_idx == best
2178            && best < boundaries.len() - 1
2179            && is_break_inside_word(&breaks, wrap_idx);
2180        narrow_to_break(
2181            hold,
2182            &measure_context,
2183            &breaks,
2184            (start_idx, best),
2185            (can_hyphenate, limit.elides_next(out.len())),
2186        );
2187        if can_hyphenate {
2188            effective_wrap_idx =
2189                resolve_auto_hyphen_break(measurer, style, &breaks, start_idx, wrap_idx);
2190        }
2191
2192        let broke_at_word_boundary = effective_wrap_idx > start_idx
2193            && line_text[boundaries[effective_wrap_idx - 1]..boundaries[effective_wrap_idx]]
2194                .chars()
2195                .all(char::is_whitespace);
2196        let segment_start = boundaries[start_idx];
2197        let mut segment_end = boundaries[effective_wrap_idx];
2198        if wrap_idx != best || broke_at_word_boundary {
2199            segment_end = trim_segment_end_whitespace(line_text, segment_start, segment_end);
2200        }
2201        let segment_end_idx = boundary_index_for_byte(&boundaries, segment_end);
2202        out.push(measure_context.display_line_for_char_range(
2203            &boundaries,
2204            start_idx,
2205            segment_end_idx,
2206        ));
2207
2208        start_idx = if wrap_idx != best || broke_at_word_boundary {
2209            skip_leading_whitespace(line_text, &boundaries, wrap_idx)
2210        } else {
2211            effective_wrap_idx
2212        };
2213    }
2214
2215    if out.len() == first {
2216        out.push(DisplayLine::from_source_range(
2217            line_range.start..line_range.start,
2218        ));
2219    }
2220}
2221
2222/// Appends the word-balanced lines of `line_range` to `out` and returns
2223/// whether it could balance them; when it cannot, `out` is left as it was.
2224fn wrap_line_with_word_balance<M: TextMeasurer + ?Sized>(
2225    measurer: &M,
2226    text: &crate::text::AnnotatedString,
2227    line_range: Range<usize>,
2228    style: &TextStyle,
2229    max_width: f32,
2230    line_break: LineBreak,
2231    out: &mut DisplayLines,
2232) -> bool {
2233    let line_text = &text.text[line_range.clone()];
2234    let boundaries = cluster_boundaries(line_text);
2235    let measure_context =
2236        LineMeasureContext::new(measurer, text, &line_range, style, boundaries.len());
2237    if let Some(measured_width) =
2238        measure_context.prefix_width_for_char_range(0, boundaries.len() - 1)
2239        && measured_width <= max_width + WRAP_EPSILON
2240    {
2241        out.push(DisplayLine::from_measured_source_range(
2242            line_range,
2243            measured_width,
2244        ));
2245        return true;
2246    }
2247    let breakpoints = collect_word_breakpoints(&LineBreaks::new(line_text, &boundaries));
2248    if breakpoints.len() <= 2 {
2249        return false;
2250    }
2251
2252    let node_count = breakpoints.len();
2253    let mut best_cost = vec![f32::INFINITY; node_count];
2254    let mut next_index = vec![None; node_count];
2255    best_cost[node_count - 1] = 0.0;
2256
2257    for start in (0..node_count - 1).rev() {
2258        for end in start + 1..node_count {
2259            let start_byte = boundaries[breakpoints[start]];
2260            let end_byte = boundaries[breakpoints[end]];
2261            let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2262            if trimmed_end <= start_byte {
2263                continue;
2264            }
2265            let segment_start_idx = breakpoints[start];
2266            let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2267            let segment_width =
2268                measure_context.measure_char_range(&boundaries, segment_start_idx, segment_end_idx);
2269            if segment_width > max_width + WRAP_EPSILON {
2270                continue;
2271            }
2272            if !best_cost[end].is_finite() {
2273                continue;
2274            }
2275            let slack = (max_width - segment_width).max(0.0);
2276            let is_last = end == node_count - 1;
2277            let segment_cost = match line_break {
2278                LineBreak::Heading => slack * slack,
2279                LineBreak::Paragraph => {
2280                    if is_last {
2281                        slack * slack * 0.16
2282                    } else {
2283                        slack * slack
2284                    }
2285                }
2286                LineBreak::Simple | LineBreak::Unspecified => slack * slack,
2287            };
2288            let candidate = segment_cost + best_cost[end];
2289            if candidate < best_cost[start] {
2290                best_cost[start] = candidate;
2291                next_index[start] = Some(end);
2292            }
2293        }
2294    }
2295
2296    let first = out.len();
2297    let mut current = 0usize;
2298    while current < node_count - 1 {
2299        let Some(next) = next_index[current] else {
2300            out.truncate(first);
2301            return false;
2302        };
2303        let start_byte = boundaries[breakpoints[current]];
2304        let end_byte = boundaries[breakpoints[next]];
2305        let trimmed_end = trim_segment_end_whitespace(line_text, start_byte, end_byte);
2306        if trimmed_end <= start_byte {
2307            out.truncate(first);
2308            return false;
2309        }
2310        let segment_start_idx = breakpoints[current];
2311        let segment_end_idx = boundary_index_for_byte(&boundaries, trimmed_end);
2312        out.push(measure_context.display_line_for_char_range(
2313            &boundaries,
2314            segment_start_idx,
2315            segment_end_idx,
2316        ));
2317        current = next;
2318    }
2319    out.len() > first
2320}
2321
2322/// Where a line may wrap: after whitespace and, in Thai and the other scripts
2323/// written without spaces between words, between its words.
2324struct LineBreaks<'a> {
2325    line: &'a str,
2326    boundaries: &'a [usize],
2327    /// Character indexes inside words of a script written without spaces
2328    /// before which the line may wrap, ascending. Empty for other text.
2329    words: Vec<usize>,
2330}
2331
2332impl<'a> LineBreaks<'a> {
2333    fn new(line: &'a str, boundaries: &'a [usize]) -> Self {
2334        Self {
2335            line,
2336            boundaries,
2337            words: super::word_breaks::unspaced_word_breaks(line, boundaries),
2338        }
2339    }
2340
2341    /// Whether character `idx` is whitespace.
2342    fn is_space(&self, idx: usize) -> bool {
2343        self.line[self.boundaries[idx]..self.boundaries[idx + 1]]
2344            .chars()
2345            .all(char::is_whitespace)
2346    }
2347
2348    /// Whether the line may wrap before character `idx`, which is not the
2349    /// first.
2350    fn before(&self, idx: usize) -> bool {
2351        self.is_space(idx - 1) || self.words.binary_search(&idx).is_ok()
2352    }
2353}
2354
2355fn collect_word_breakpoints(breaks: &LineBreaks<'_>) -> Vec<usize> {
2356    let boundaries = breaks.boundaries;
2357    let mut points = vec![0usize];
2358    for idx in 1..boundaries.len() - 1 {
2359        if breaks.before(idx) && !breaks.is_space(idx) {
2360            points.push(idx);
2361        }
2362    }
2363    let end = boundaries.len() - 1;
2364    if points.last().copied() != Some(end) {
2365        points.push(end);
2366    }
2367    points
2368}
2369
2370fn choose_wrap_break(
2371    breaks: &LineBreaks<'_>,
2372    start_idx: usize,
2373    best: usize,
2374    _line_break: LineBreak,
2375) -> usize {
2376    if best >= breaks.boundaries.len() - 1 {
2377        return best;
2378    }
2379
2380    if best <= start_idx + 1 {
2381        return best;
2382    }
2383
2384    (start_idx + 1..=best)
2385        .rev()
2386        .find(|&idx| breaks.before(idx))
2387        .unwrap_or(best)
2388}
2389
2390/// Narrows `hold` to the widths a line from `start_idx` breaks the same at
2391/// as it does where `best` characters fit, or clears it when the break was
2392/// hyphenated, which no width range describes.
2393/// Narrows `hold` to the widths that break the line from `start_idx` at
2394/// `best` the same. A line that runs on to be elided holds while the rest of
2395/// its paragraph does not fit on it, wherever it breaks.
2396fn narrow_to_break<M: TextMeasurer + ?Sized>(
2397    hold: &mut Option<WrapHold>,
2398    measure_context: &LineMeasureContext<'_, M>,
2399    breaks: &LineBreaks<'_>,
2400    (start_idx, best): (usize, usize),
2401    (hyphenated, elided): (bool, bool),
2402) {
2403    let boundaries = breaks.boundaries;
2404    let end = boundaries.len() - 1;
2405    if elided && best < end {
2406        if let Some(hold) = hold {
2407            let rest = measure_context.measure_char_range(boundaries, start_idx, end);
2408            hold.narrow(f32::NEG_INFINITY, rest);
2409        }
2410        return;
2411    }
2412    if hyphenated {
2413        *hold = None;
2414    }
2415    let Some(hold) = hold else {
2416        return;
2417    };
2418    let (fits, pulls_up) = wrap_break_widths(breaks, start_idx, best);
2419    let width = |idx| measure_context.measure_char_range(boundaries, start_idx, idx);
2420    hold.narrow(
2421        fits.map_or(f32::NEG_INFINITY, width),
2422        pulls_up.map_or(f32::INFINITY, width),
2423    );
2424}
2425
2426/// The character counts, from `start_idx`, whose widths bound the widths
2427/// [`choose_wrap_break`] picks the same break at as it does for `best`: the
2428/// first a line must fit to keep its break (`None` when any width does),
2429/// and the first that would take the break past it (`None` when none
2430/// would).
2431fn wrap_break_widths(
2432    breaks: &LineBreaks<'_>,
2433    start_idx: usize,
2434    best: usize,
2435) -> (Option<usize>, Option<usize>) {
2436    let end = breaks.boundaries.len() - 1;
2437    // A line takes its first character whether it fits or not.
2438    if best <= start_idx + 1 {
2439        return (None, (best < end).then_some(best + 1));
2440    }
2441    if best >= end {
2442        return (Some(end), None);
2443    }
2444    match (start_idx + 1..=best).rev().find(|&idx| breaks.before(idx)) {
2445        Some(wrap_idx) => {
2446            let next = (best + 1..end)
2447                .find(|&idx| breaks.before(idx))
2448                .unwrap_or(end);
2449            (Some(wrap_idx), Some(next))
2450        }
2451        None => (Some(best), Some(best + 1)),
2452    }
2453}
2454
2455fn is_break_inside_word(breaks: &LineBreaks<'_>, break_idx: usize) -> bool {
2456    if break_idx == 0 || break_idx >= breaks.boundaries.len() - 1 {
2457        return false;
2458    }
2459    !breaks.before(break_idx) && !breaks.is_space(break_idx)
2460}
2461
2462fn resolve_auto_hyphen_break<M: TextMeasurer + ?Sized>(
2463    measurer: &M,
2464    style: &TextStyle,
2465    breaks: &LineBreaks<'_>,
2466    start_idx: usize,
2467    break_idx: usize,
2468) -> usize {
2469    let boundaries = breaks.boundaries;
2470    if let Some(candidate) =
2471        measurer.choose_auto_hyphen_break(breaks.line, style, start_idx, break_idx)
2472        && is_valid_auto_hyphen_break(breaks, start_idx, break_idx, candidate)
2473    {
2474        return candidate;
2475    }
2476    choose_auto_hyphen_break_fallback(boundaries, start_idx, break_idx)
2477}
2478
2479fn is_valid_auto_hyphen_break(
2480    breaks: &LineBreaks<'_>,
2481    start_idx: usize,
2482    break_idx: usize,
2483    candidate_idx: usize,
2484) -> bool {
2485    let end_idx = breaks.boundaries.len().saturating_sub(1);
2486    candidate_idx > start_idx
2487        && candidate_idx < end_idx
2488        && candidate_idx <= break_idx
2489        && candidate_idx >= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS
2490        && is_break_inside_word(breaks, candidate_idx)
2491}
2492
2493fn choose_auto_hyphen_break_fallback(
2494    boundaries: &[usize],
2495    start_idx: usize,
2496    break_idx: usize,
2497) -> usize {
2498    let end_idx = boundaries.len().saturating_sub(1);
2499    if break_idx >= end_idx {
2500        return break_idx;
2501    }
2502    let trailing_len = end_idx.saturating_sub(break_idx);
2503    if trailing_len > 2 || break_idx <= start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS {
2504        return break_idx;
2505    }
2506
2507    let min_break = start_idx + AUTO_HYPHEN_MIN_SEGMENT_CHARS;
2508    let max_break = break_idx.saturating_sub(1);
2509    if min_break > max_break {
2510        return break_idx;
2511    }
2512
2513    let mut best_break = break_idx;
2514    let mut best_penalty = usize::MAX;
2515    for idx in min_break..=max_break {
2516        let candidate_trailing_len = end_idx.saturating_sub(idx);
2517        let candidate_prefix_len = idx.saturating_sub(start_idx);
2518        if candidate_prefix_len < AUTO_HYPHEN_MIN_SEGMENT_CHARS
2519            || candidate_trailing_len < AUTO_HYPHEN_MIN_TRAILING_CHARS
2520        {
2521            continue;
2522        }
2523
2524        let penalty = candidate_trailing_len.abs_diff(AUTO_HYPHEN_PREFERRED_TRAILING_CHARS);
2525        if penalty < best_penalty {
2526            best_penalty = penalty;
2527            best_break = idx;
2528            if penalty == 0 {
2529                break;
2530            }
2531        }
2532    }
2533    best_break
2534}
2535
2536fn skip_leading_whitespace(line: &str, boundaries: &[usize], mut idx: usize) -> usize {
2537    while idx < boundaries.len() - 1 {
2538        let ch = &line[boundaries[idx]..boundaries[idx + 1]];
2539        if !ch.chars().all(char::is_whitespace) {
2540            break;
2541        }
2542        idx += 1;
2543    }
2544    idx
2545}
2546
2547/// Cuts `visible_lines` to the options' line limit and elides what does not
2548/// fit, and returns whether anything did not. Narrows `hold` to the widths
2549/// that cut the ellipsis at the same character, or clears it when a line
2550/// overflows its width.
2551fn apply_overflow<M: TextMeasurer + ?Sized>(
2552    measurer: &M,
2553    node_id: Option<NodeId>,
2554    (text, style): (&crate::text::AnnotatedString, &TextStyle),
2555    options: TextLayoutOptions,
2556    max_width: Option<f32>,
2557    (visible_lines, hold): (&mut DisplayLines, &mut Option<WrapHold>),
2558) -> bool {
2559    if options.overflow == TextOverflow::Visible {
2560        return false;
2561    }
2562    let ellipsis = EllipsisPlacement::for_options(options);
2563    let mut did_overflow = false;
2564    if visible_lines.len() > options.max_lines {
2565        did_overflow = true;
2566        visible_lines.truncate(options.max_lines);
2567        if let (Some(placement), Some(last_line)) = (ellipsis, visible_lines.last_mut()) {
2568            last_line.extend_to_paragraph_end(text);
2569            let cut = last_line.ellipsize(measurer, node_id, text, style, max_width, placement);
2570            if let Some(hold) = hold {
2571                hold.narrow(cut.fits, cut.pulls_up);
2572            }
2573        }
2574    }
2575
2576    let Some(width_limit) = max_width else {
2577        return did_overflow;
2578    };
2579    let visible_len = visible_lines.len();
2580    for (line_index, line) in visible_lines.iter_mut().enumerate() {
2581        if line.measure_width(measurer, node_id, text, style) <= width_limit + WRAP_EPSILON {
2582            continue;
2583        }
2584        did_overflow = true;
2585        *hold = None;
2586        if line_index + 1 == visible_len
2587            && let Some(placement) = ellipsis
2588        {
2589            line.ellipsize(measurer, node_id, text, style, max_width, placement);
2590        }
2591    }
2592    did_overflow
2593}
2594
2595#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2596enum EllipsisPlacement {
2597    End,
2598    Start,
2599    Middle,
2600}
2601
2602impl EllipsisPlacement {
2603    /// How many of `kept_chars` stay before the ellipsis and how many after.
2604    fn split(self, kept_chars: usize) -> (usize, usize) {
2605        match self {
2606            Self::End => (kept_chars, 0),
2607            Self::Start => (0, kept_chars),
2608            Self::Middle => (kept_chars.div_ceil(2), kept_chars / 2),
2609        }
2610    }
2611
2612    /// The most characters an elided line can keep within `width_limit`,
2613    /// estimated from the line's prefix widths and the ellipsis's width
2614    /// without measuring any elided string. Shaping across the cut can move
2615    /// the real width a little either way, so callers confirm it.
2616    fn estimated_kept_chars(
2617        self,
2618        prefix_widths: &TextLinePrefixWidths,
2619        ellipsis_width: f32,
2620        width_limit: f32,
2621    ) -> Option<usize> {
2622        let char_count = prefix_widths.char_count();
2623        let width = |kept_chars: usize| {
2624            let (head_chars, tail_chars) = self.split(kept_chars);
2625            Some(
2626                prefix_widths.width_for_char_range(0, head_chars)?
2627                    + ellipsis_width
2628                    + prefix_widths.width_for_char_range(char_count - tail_chars, char_count)?,
2629            )
2630        };
2631        let (mut fitting, mut overflowing) = (0usize, char_count + 1);
2632        while fitting + 1 < overflowing {
2633            let kept_chars = fitting + (overflowing - fitting) / 2;
2634            if width(kept_chars)? <= width_limit + WRAP_EPSILON {
2635                fitting = kept_chars;
2636            } else {
2637                overflowing = kept_chars;
2638            }
2639        }
2640        Some(fitting)
2641    }
2642
2643    fn for_options(options: TextLayoutOptions) -> Option<Self> {
2644        let single_line = options.max_lines == 1;
2645        match options.overflow {
2646            TextOverflow::Ellipsis => Some(Self::End),
2647            TextOverflow::StartEllipsis if single_line => Some(Self::Start),
2648            TextOverflow::MiddleEllipsis if single_line => Some(Self::Middle),
2649            TextOverflow::StartEllipsis
2650            | TextOverflow::MiddleEllipsis
2651            | TextOverflow::Clip
2652            | TextOverflow::Visible
2653            | TextOverflow::ScaleDown { .. } => None,
2654        }
2655    }
2656
2657    fn elide(
2658        self,
2659        source: &crate::text::AnnotatedString,
2660        source_range: Range<usize>,
2661        boundaries: &[usize],
2662        kept_chars: usize,
2663    ) -> crate::text::AnnotatedString {
2664        let char_count = boundaries.len() - 1;
2665        let (head_chars, tail_chars) = self.split(kept_chars);
2666        let head_end = source_range.start + boundaries[head_chars];
2667        let tail_start = source_range.start + boundaries[char_count - tail_chars];
2668        crate::text::AnnotatedString::builder()
2669            .append_annotated_subsequence(source, source_range.start..head_end)
2670            .append(ELLIPSIS)
2671            .append_annotated_subsequence(source, tail_start..source_range.end)
2672            .to_annotated_string()
2673    }
2674}
2675
2676/// The line `source_range` elided at `placement` to fit `max_width`, and the
2677/// widths the same elision fits: from its own width up to the width of the
2678/// elision keeping one character more.
2679fn fit_ellipsis<M: TextMeasurer + ?Sized>(
2680    measurer: &M,
2681    node_id: Option<NodeId>,
2682    source: &crate::text::AnnotatedString,
2683    source_range: Range<usize>,
2684    style: &TextStyle,
2685    max_width: Option<f32>,
2686    placement: EllipsisPlacement,
2687) -> (DisplayLine, WrapHold) {
2688    let width_limit = max_width.unwrap_or(f32::INFINITY);
2689    // The line when it fits, or the width it overflows at.
2690    let fitting_line = |text: DisplayLineText| {
2691        let mut line = DisplayLine {
2692            source_range: source_range.clone(),
2693            text,
2694            measured_width: None,
2695        };
2696        let width = line.measure_width(measurer, node_id, source, style);
2697        if width <= width_limit + WRAP_EPSILON {
2698            Ok(line)
2699        } else {
2700            Err(width)
2701        }
2702    };
2703    // Keeping every character elides nothing, so the whole line bounds the
2704    // widths an elision holds.
2705    let whole = if placement == EllipsisPlacement::End {
2706        f32::INFINITY
2707    } else {
2708        match fitting_line(DisplayLineText::Source) {
2709            Ok(line) => return ElisionSearch::found(line, f32::INFINITY),
2710            Err(width) => width,
2711        }
2712    };
2713
2714    let boundaries = cluster_boundaries(&source.text[source_range.clone()]);
2715    let elided_line = |kept_chars: usize| {
2716        fitting_line(DisplayLineText::Ellipsized(placement.elide(
2717            source,
2718            source_range.clone(),
2719            &boundaries,
2720            kept_chars,
2721        )))
2722    };
2723    let best = match elided_line(0) {
2724        Ok(line) => line,
2725        Err(ellipsis_width) => {
2726            let empty = DisplayLine {
2727                source_range: source_range.clone(),
2728                text: DisplayLineText::Ellipsized(crate::text::AnnotatedString::default()),
2729                measured_width: None,
2730            };
2731            return ElisionSearch::found(empty, ellipsis_width);
2732        }
2733    };
2734
2735    // The line's prefix widths place the cut without measuring an elided
2736    // string per guess; measuring the guess and the one past it confirms it,
2737    // and the search below only runs when shaping across the cut moved it.
2738    let guess = best.measured_width.and_then(|ellipsis_width| {
2739        measurer
2740            .measure_line_prefix_widths(source, source_range.clone(), style)
2741            .filter(|widths| widths.char_count() + 1 == boundaries.len())
2742            .and_then(|widths| placement.estimated_kept_chars(&widths, ellipsis_width, width_limit))
2743    });
2744    let mut search = ElisionSearch {
2745        best,
2746        fitting: 0,
2747        overflowing: boundaries.len(),
2748        overflow_width: whole,
2749    };
2750    if let Some(guess) = guess.filter(|guess| *guess > 0) {
2751        for kept_chars in [guess, guess + 1] {
2752            if kept_chars <= search.fitting || kept_chars >= search.overflowing {
2753                break;
2754            }
2755            search.probe(kept_chars, elided_line(kept_chars));
2756        }
2757    }
2758    while search.fitting + 1 < search.overflowing {
2759        let kept_chars = search.fitting + (search.overflowing - search.fitting) / 2;
2760        search.probe(kept_chars, elided_line(kept_chars));
2761    }
2762    ElisionSearch::found(search.best, search.overflow_width)
2763}
2764
2765/// The most characters an elision was found to keep within the width, and
2766/// the fewest it was found to overflow at.
2767struct ElisionSearch {
2768    best: DisplayLine,
2769    fitting: usize,
2770    overflowing: usize,
2771    overflow_width: f32,
2772}
2773
2774impl ElisionSearch {
2775    fn probe(&mut self, kept_chars: usize, line: Result<DisplayLine, f32>) {
2776        match line {
2777            Ok(line) => (self.fitting, self.best) = (kept_chars, line),
2778            Err(width) => (self.overflowing, self.overflow_width) = (kept_chars, width),
2779        }
2780    }
2781
2782    /// `line`, and the widths it fits while the next wider elision, which is
2783    /// `overflow_width` wide, does not.
2784    fn found(line: DisplayLine, overflow_width: f32) -> (DisplayLine, WrapHold) {
2785        let cut = WrapHold {
2786            fits: line.measured_width.unwrap_or(f32::NEG_INFINITY),
2787            pulls_up: overflow_width,
2788        };
2789        (line, cut)
2790    }
2791}
2792
2793/// The byte offsets a line may split `text` at: before each character but
2794/// the combining marks, which stay with the letter they sit on (a Thai vowel
2795/// or tone mark, a Devanagari vowel sign, an accent), and at its end.
2796fn cluster_boundaries(text: &str) -> Vec<usize> {
2797    let mut out = Vec::with_capacity(text.chars().count() + 1);
2798    out.push(0);
2799    for (idx, ch) in text.char_indices() {
2800        if idx != 0 && !is_combining_mark(ch) {
2801            out.push(idx);
2802        }
2803    }
2804    out.push(text.len());
2805    out
2806}
2807
2808/// Whether `ch` combines with the character before it.
2809fn is_combining_mark(ch: char) -> bool {
2810    use icu_properties::{
2811        CodePointMapData,
2812        props::{GeneralCategory, GeneralCategoryGroup},
2813    };
2814    // No character before the combining diacritics block is a mark.
2815    ch >= '\u{300}'
2816        && GeneralCategoryGroup::Mark.contains(CodePointMapData::<GeneralCategory>::new().get(ch))
2817}
2818
2819#[cfg(test)]
2820#[path = "tests/measure_tests.rs"]
2821mod tests;