Skip to main content

cranpose_ui/text/
measure.rs

1use std::{
2    cell::{Cell, OnceCell, RefCell},
3    ops::Range,
4    rc::Rc,
5    sync::Arc,
6};
7
8use cranpose_core::{
9    PackedU64,
10    collections::pass_aged::{IDLE_PASSES, PassAgedCache},
11};
12use cranpose_text::{
13    CaretDirection, FontExtent, FontFamily, FontFamilyResolver, FontRequest, FontStyle, FontWeight,
14    LayoutInput, ParagraphIntrinsics, TextLayout, TextLayouter, paragraph_line_box,
15};
16use cranpose_ui_graphics::{MemoryHeadroom, Size, TextLayoutHandle};
17
18use super::{
19    AnnotatedString, LineBox, RenderString, SpanStyle, TextAlign, TextDrawStyle, TextUnit,
20    layout_cache::{CacheAdmission, Cached, LayoutCache},
21    layout_input::{DEFAULT_FONT_SIZE_SP, ParagraphSpans, TextUnits},
22    layout_options::{TextLayoutOptions, TextOverflow},
23    style::TextStyle,
24    text_key::TextKey,
25};
26use crate::{density::Density, font_scale::FontScaleCurve, text_layout_result::TextLayoutResult};
27
28const TEXT_INTRINSICS_CACHE_CAPACITY: usize = 1024;
29const SCALE_DOWN_SEARCH_STEPS: usize = 14;
30
31/// Size of a laid-out text. Distances are in logical pixels.
32#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct TextMetrics {
34    /// The paragraph's width, within the widths it was laid out between.
35    pub width: f32,
36    /// Height of all lines together.
37    pub height: f32,
38    /// The distance from the first line's baseline down to the next line's.
39    pub line_height: f32,
40    /// Number of lines, at least the minimum the options ask for.
41    pub line_count: usize,
42}
43
44/// A text laid out between two widths: its glyphs and lines, which every
45/// query, measurement and renderer reads, and the size it measures.
46#[derive(Clone, Debug)]
47pub struct PreparedTextLayout {
48    /// The text that was laid out.
49    pub text: Rc<AnnotatedString>,
50    /// The style the text draws in, shared with every render node drawn
51    /// from this layout.
52    pub visual_style: Arc<TextStyle>,
53    /// The lines and glyphs.
54    pub layout: TextLayout,
55    // The widest the text could be, and the fewest lines its height covers,
56    // which its metrics follow from with its layout.
57    max_width: f32,
58    min_lines: u32,
59    /// `text` as a renderer draws it, and its
60    /// [`RenderString::render_hash`](crate::text::RenderString::render_hash),
61    /// converted on first use: see [`PreparedTextLayout::render_text`].
62    pub render_text: OnceCell<(Arc<RenderString>, PackedU64)>,
63    pub(crate) visual_style_hash: OnceCell<PackedU64>,
64    // The layout as a draw scope carries it, made on first use.
65    layout_handle: OnceCell<TextLayoutHandle>,
66}
67
68impl PreparedTextLayout {
69    // The prepared layout of `text` in `style` laid out as `layout`, `max_width`
70    // the widest it could be.
71    fn new(
72        text: Rc<AnnotatedString>,
73        style: Arc<TextStyle>,
74        layout: TextLayout,
75        (max_width, min_lines): (f32, usize),
76    ) -> Self {
77        Self {
78            max_width,
79            min_lines: u32::try_from(min_lines).unwrap_or(u32::MAX),
80            text,
81            visual_style: style,
82            layout,
83            render_text: OnceCell::new(),
84            visual_style_hash: OnceCell::new(),
85            layout_handle: OnceCell::new(),
86        }
87    }
88
89    /// The paragraph's width, within the widths it was laid out between, and
90    /// the height of all its lines together.
91    pub fn size(&self) -> Size {
92        Size::new(
93            self.layout.width().min(self.max_width),
94            self.layout.height(),
95        )
96    }
97
98    // The max widths at which the text laid out from `min_width` gives this
99    // layout again: its lines, its width and its size the same.
100    pub(crate) fn max_widths_kept(&self, min_width: f32) -> cranpose_ui_layout::BoundRange {
101        let width = self.layout.width();
102        // A paragraph wider than its max measures as the max, which moves
103        // with it.
104        if self.max_width < width {
105            return cranpose_ui_layout::BoundRange::exactly(self.max_width);
106        }
107        let (low, high) = self.layout.max_widths_kept(min_width);
108        cranpose_ui_layout::BoundRange {
109            low: low.max(width),
110            high,
111        }
112    }
113
114    /// Size and line count.
115    pub fn metrics(&self) -> TextMetrics {
116        let layout = &self.layout;
117        let size = self.size();
118        TextMetrics {
119            width: size.width,
120            height: size.height,
121            line_height: layout.line_advance(),
122            line_count: layout
123                .line_count()
124                .max(usize::try_from(self.min_lines).unwrap_or(usize::MAX)),
125        }
126    }
127
128    /// The first and last baselines.
129    pub fn alignment_lines(&self) -> cranpose_ui_layout::AlignmentLines {
130        let layout = &self.layout;
131        cranpose_ui_layout::AlignmentLines::new(
132            Some(layout.line_baseline(0)),
133            Some(layout.line_baseline(layout.line_count() - 1)),
134        )
135    }
136
137    /// True when the text did not fit: it had more lines than the line limit,
138    /// or was wider than the widest it could be.
139    pub fn did_overflow(&self) -> bool {
140        self.layout.did_overflow_height() || self.layout.did_overflow_width()
141    }
142
143    // The layout as a draw scope carries it to a renderer, made once for
144    // every frame that draws the same text.
145    pub(crate) fn layout_handle(&self) -> TextLayoutHandle {
146        self.layout_handle
147            .get_or_init(|| TextLayoutHandle::from(Arc::new(self.layout.clone())))
148            .clone()
149    }
150
151    // This layout at `widths`, at which it breaks into the same lines: itself when its width there is its own, else a copy whose
152    // lines `align` places across that width. The copy keeps the text as a
153    // renderer draws it and the style's hash, and takes the allocation of
154    // `spare` when nothing else holds it.
155    pub(crate) fn at_widths(
156        self: &Rc<Self>,
157        widths: (f32, f32),
158        align: TextAlign,
159        spare: Option<Rc<Self>>,
160    ) -> Rc<Self> {
161        let width = self.layout.width_between(widths.0, widths.1);
162        if width.to_bits() == self.layout.width().to_bits() {
163            return Rc::clone(self);
164        }
165        self.aligned_at(widths, align, spare)
166    }
167
168    // The copy `at_widths` makes. Out of line: the copy costs more than the
169    // call, and a layout held at its own width takes none.
170    #[inline(never)]
171    fn aligned_at(
172        self: &Rc<Self>,
173        widths: (f32, f32),
174        align: TextAlign,
175        spare: Option<Rc<Self>>,
176    ) -> Rc<Self> {
177        let mut aligned = Self {
178            min_lines: self.min_lines,
179            render_text: self.render_text.clone(),
180            visual_style_hash: self.visual_style_hash.clone(),
181            ..Self::new(
182                Rc::clone(&self.text),
183                Arc::clone(&self.visual_style),
184                self.layout.clone(),
185                (self.max_width, 0),
186            )
187        };
188        aligned.realign(widths, align);
189        if let Some(mut spare) = spare
190            && let Some(slot) = Rc::get_mut(&mut spare)
191        {
192            *slot = aligned;
193            return spare;
194        }
195        Rc::new(aligned)
196    }
197
198    // Places the lines across `(min_width, max_width)`, at which the layout
199    // breaks into the same lines, as `align` says.
200    pub(crate) fn realign(&mut self, (min_width, max_width): (f32, f32), align: TextAlign) {
201        self.layout.align(min_width, max_width, align);
202        self.max_width = max_width;
203        self.layout_handle = OnceCell::new();
204    }
205
206    // `realign` when the layout breaks into the same lines between
207    // `(min_width, max_width)`; `false`, leaving it as it was, when not.
208    pub(crate) fn try_realign(
209        &mut self,
210        (min_width, max_width): (f32, f32),
211        align: TextAlign,
212    ) -> bool {
213        let held = self.layout.try_align(min_width, max_width, align);
214        if held {
215            self.max_width = max_width;
216            self.layout_handle = OnceCell::new();
217        }
218        held
219    }
220
221    /// The display text as a renderer draws it. Converted once for the layout
222    /// and shared after, so every frame and scene rebuild drawing this layout
223    /// hands over the same allocation.
224    pub fn render_text(&self) -> Arc<RenderString> {
225        if let Some((converted, _)) = self.render_text.get() {
226            return Arc::clone(converted);
227        }
228        self.convert_render_text().0
229    }
230
231    /// [`RenderString::render_hash`](crate::text::RenderString::render_hash)
232    /// of [`PreparedTextLayout::render_text`], kept beside it so that reading
233    /// it never loads the string.
234    pub fn render_text_hash(&self) -> u64 {
235        if let Some((_, hash)) = self.render_text.get() {
236            return hash.get();
237        }
238        self.convert_render_text().1
239    }
240
241    fn convert_render_text(&self) -> (Arc<RenderString>, u64) {
242        let converted = Arc::new(self.text.render_string());
243        let hash = converted.render_hash();
244        let _ = self
245            .render_text
246            .set((Arc::clone(&converted), PackedU64::new(hash)));
247        (converted, hash)
248    }
249
250    /// [`TextStyle::render_hash`] of `visual_style`, hashed once for the
251    /// layout, or handed over by the node that knows its style's hash.
252    pub fn visual_style_hash(&self) -> u64 {
253        if let Some(hash) = self.visual_style_hash.get() {
254            return hash.get();
255        }
256        let hash = self.visual_style.render_hash();
257        let _ = self.visual_style_hash.set(PackedU64::new(hash));
258        hash
259    }
260
261    /// The layout's queries, as Compose's `TextLayoutResult` gives them.
262    pub fn result(&self) -> TextLayoutResult {
263        TextLayoutResult::new(self.layout.clone())
264    }
265}
266
267impl PartialEq for PreparedTextLayout {
268    fn eq(&self, other: &Self) -> bool {
269        self.text == other.text
270            && self.visual_style == other.visual_style
271            && self.metrics() == other.metrics()
272            && self.alignment_lines() == other.alignment_lines()
273            && self.did_overflow() == other.did_overflow()
274    }
275}
276
277/// The text and style a layout is prepared from: borrowed, which the layout
278/// copies, or shared with the node that holds them.
279#[derive(Clone, Copy)]
280enum LayoutSource<'a> {
281    Borrowed {
282        text: &'a AnnotatedString,
283        style: &'a TextStyle,
284    },
285    Shared {
286        text: &'a SharedText<'a>,
287        // A layout of the text in the style at other widths, whose glyphs a
288        // new layout takes in place of shaping the text again.
289        shaped: Option<&'a TextLayout>,
290    },
291}
292
293// A text and style a node holds, and the style's render hash.
294pub(crate) struct SharedText<'a> {
295    pub(crate) text: &'a Rc<AnnotatedString>,
296    pub(crate) style: &'a Arc<TextStyle>,
297    pub(crate) style_hash: u64,
298}
299
300impl LayoutSource<'_> {
301    fn text(&self) -> &AnnotatedString {
302        match self {
303            Self::Borrowed { text, .. } => text,
304            Self::Shared { text, .. } => text.text,
305        }
306    }
307
308    fn style(&self) -> &TextStyle {
309        match self {
310            Self::Borrowed { style, .. } => style,
311            Self::Shared { text, .. } => text.style,
312        }
313    }
314
315    fn shared(&self) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
316        match self {
317            Self::Borrowed { text, style } => {
318                (Rc::new((*text).clone()), Arc::new((*style).clone()))
319            }
320            Self::Shared { text, .. } => (Rc::clone(text.text), Arc::clone(text.style)),
321        }
322    }
323
324    fn style_hash(&self) -> u64 {
325        match self {
326            Self::Borrowed { style, .. } => style.render_hash(),
327            Self::Shared { text, .. } => text.style_hash,
328        }
329    }
330
331    fn shaped(&self) -> Option<&TextLayout> {
332        match self {
333            Self::Borrowed { .. } => None,
334            Self::Shared { shaped, .. } => *shaped,
335        }
336    }
337}
338
339/// The widths a text lays out between, and its options.
340#[derive(Clone, Copy, Debug, PartialEq)]
341pub(crate) struct TextFrame {
342    pub(crate) min_width: f32,
343    pub(crate) max_width: f32,
344    pub(crate) options: TextLayoutOptions,
345}
346
347impl TextFrame {
348    // No width limit, lines ending only at line breaks.
349    fn unbounded(options: TextLayoutOptions) -> Self {
350        Self {
351            min_width: 0.0,
352            max_width: f32::INFINITY,
353            options,
354        }
355    }
356
357    // From zero up to `max_width`; `None`, and a width that is not positive
358    // and finite, is no limit.
359    pub(crate) fn up_to(max_width: Option<f32>, options: TextLayoutOptions) -> Self {
360        Self {
361            max_width: max_width
362                .filter(|width| width.is_finite() && *width > 0.0)
363                .unwrap_or(f32::INFINITY),
364            ..Self::unbounded(options)
365        }
366    }
367}
368
369/// The app's fonts and the text engine its text lays out with, and the
370/// layouts and intrinsic widths recent passes used.
371pub(crate) struct TextService {
372    generation: Cell<u64>,
373    /// Whether a frame started since the last pass began: the first use of
374    /// the service outside a layout pass then begins one.
375    pass_owed: Cell<bool>,
376    fonts: RefCell<FontFamilyResolver>,
377    layouter: RefCell<TextLayouter>,
378    layouts: RefCell<LayoutCache>,
379    intrinsics: RefCell<PassAgedCache<TextKey, ParagraphIntrinsics>>,
380}
381
382impl TextService {
383    pub(crate) fn new() -> Self {
384        Self {
385            generation: Cell::new(1),
386            pass_owed: Cell::new(false),
387            fonts: RefCell::new(default_fonts()),
388            layouter: RefCell::new(TextLayouter::default()),
389            layouts: RefCell::new(LayoutCache::new()),
390            intrinsics: RefCell::new(PassAgedCache::with_capacity_at_least_one(
391                TEXT_INTRINSICS_CACHE_CAPACITY,
392            )),
393        }
394    }
395
396    /// Lays text out with `fonts` from now on.
397    pub(crate) fn set_fonts(&self, fonts: FontFamilyResolver) {
398        *self.fonts.borrow_mut() = fonts;
399        self.generation
400            .set(self.generation.get().wrapping_add(1).max(1));
401        self.layouts.borrow_mut().clear();
402        self.intrinsics.borrow_mut().clear();
403    }
404
405    pub(crate) fn generation(&self) -> u64 {
406        self.generation.get()
407    }
408
409    // The layout of `source` in `frame` with sizes in `units`: a cached one
410    // that breaks into the same lines, aligned to the frame's widths, or a
411    // new one.
412    fn prepare(
413        &self,
414        source: LayoutSource<'_>,
415        frame: TextFrame,
416        (units, admission): (TextUnits, CacheAdmission),
417    ) -> Rc<PreparedTextLayout> {
418        let (text, style) = (source.text(), source.style());
419        let key = TextKey::of(text, source.style_hash(), frame.options, units);
420        let cached = key.map(|key| {
421            let cached = self
422                .layouts
423                .borrow_mut()
424                .cached(key, frame, (style, admission));
425            (key, cached)
426        });
427        let missed = match cached {
428            Some((_, Cached::Held(prepared))) => return prepared,
429            Some((key, Cached::Missed(miss))) => Some((key, miss)),
430            None => None,
431        };
432        let (text, style) = source.shared();
433        let prepared = Rc::new(self.lay_out(text, style, frame, (units, source.shaped())));
434        if let Some((key, miss)) = missed {
435            self.layouts
436                .borrow_mut()
437                .keep(key, &prepared, (miss, admission));
438        }
439        prepared
440    }
441
442    // Lays `text` out in `style`, auto-sizing it when its options ask, with
443    // the glyphs of `shaped` when given.
444    fn lay_out(
445        &self,
446        text: Rc<AnnotatedString>,
447        style: Arc<TextStyle>,
448        frame: TextFrame,
449        (units, shaped): (TextUnits, Option<&TextLayout>),
450    ) -> PreparedTextLayout {
451        let options = frame.options.normalized();
452        let min_lines = options.min_lines;
453        let Some(min_font_size_sp) = options.overflow.scale_down_min_font_size_sp() else {
454            let layout = self.layout(&text, &style, frame, (units, shaped));
455            return PreparedTextLayout::new(text, style, layout, (frame.max_width, min_lines));
456        };
457        self.lay_out_scaled_down(text, style, frame, (units, shaped), min_font_size_sp)
458    }
459
460    // Lays `text` out in `style` at the largest size from `min_font_size_sp`
461    // up that fits `frame`. Out of line: its layouts stay off the stack of
462    // the common layout, which shaping runs at the bottom of.
463    #[inline(never)]
464    fn lay_out_scaled_down(
465        &self,
466        text: Rc<AnnotatedString>,
467        style: Arc<TextStyle>,
468        frame: TextFrame,
469        (units, shaped): (TextUnits, Option<&TextLayout>),
470        min_font_size_sp: f32,
471    ) -> PreparedTextLayout {
472        let options = frame.options.normalized();
473        let min_lines = options.min_lines;
474        let frame = TextFrame {
475            options: TextLayoutOptions {
476                overflow: TextOverflow::Clip,
477                ..options
478            },
479            ..frame
480        };
481        let fits =
482            |layout: &TextLayout| !layout.did_overflow_height() && !layout.did_overflow_width();
483        let full = self.layout(&text, &style, frame, (units, shaped));
484        let base = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
485        let min_factor = (min_font_size_sp.min(base) / base).clamp(0.0, 1.0);
486        if fits(&full) || !frame.max_width.is_finite() || min_factor >= 1.0 {
487            return PreparedTextLayout::new(text, style, full, (frame.max_width, min_lines));
488        }
489        // Each size takes the glyphs of the first: shaping is the same at
490        // every size.
491        let at = |factor: f32| {
492            let units = TextUnits {
493                factor: units.factor * factor,
494                ..units
495            };
496            self.layout(&text, &style, frame, (units, Some(&full)))
497        };
498        let (mut best, mut factor) = (at(min_factor), min_factor);
499        if fits(&best) {
500            let (mut low, mut high) = (min_factor, 1.0);
501            for _ in 0..SCALE_DOWN_SEARCH_STEPS {
502                let mid = (low + high) * 0.5;
503                let candidate = at(mid);
504                if fits(&candidate) {
505                    low = mid;
506                    (best, factor) = (candidate, mid);
507                } else {
508                    high = mid;
509                }
510            }
511        }
512        let (text, style) = scaled_drawing(&text, &style, factor);
513        PreparedTextLayout::new(text, style, best, (frame.max_width, min_lines))
514    }
515
516    // One layout of `text` in `style` in `frame`, with sizes in `units`, with
517    // the glyphs of `shaped` when given.
518    fn layout(
519        &self,
520        text: &AnnotatedString,
521        style: &TextStyle,
522        frame: TextFrame,
523        (units, shaped): (TextUnits, Option<&TextLayout>),
524    ) -> TextLayout {
525        let paragraph = ParagraphSpans::new(text, style, units);
526        let fonts = self.fonts.borrow();
527        let spans = paragraph.spans(&fonts);
528        let input = LayoutInput {
529            text: &text.text,
530            spans: &spans,
531            paragraph: &style.paragraph_style,
532            lengths: paragraph.lengths,
533            min_width: frame.min_width,
534            max_width: frame.max_width,
535            options: frame.options,
536        };
537        let mut layouter = self.layouter.borrow_mut();
538        match shaped {
539            Some(shaped) => layouter.relayout(&input, &fonts, shaped),
540            None => layouter.layout(&input, &fonts),
541        }
542    }
543
544    // The intrinsic widths of `text` in `style`, with sizes in `units`.
545    fn intrinsics(
546        &self,
547        text: &AnnotatedString,
548        style: &TextStyle,
549        units: TextUnits,
550    ) -> ParagraphIntrinsics {
551        let key = TextKey::of(
552            text,
553            style.measurement_hash(),
554            TextLayoutOptions::default(),
555            units,
556        );
557        if let Some(key) = key
558            && let Some(found) = self.intrinsics.borrow_mut().get(&key).copied()
559        {
560            return found;
561        }
562        let paragraph = ParagraphSpans::new(text, style, units);
563        let fonts = self.fonts.borrow();
564        let spans = paragraph.spans(&fonts);
565        let input = LayoutInput {
566            text: &text.text,
567            spans: &spans,
568            paragraph: &style.paragraph_style,
569            lengths: paragraph.lengths,
570            min_width: 0.0,
571            max_width: f32::INFINITY,
572            options: TextLayoutOptions::default(),
573        };
574        let found = self.layouter.borrow_mut().intrinsics(&input, &fonts);
575        if let Some(key) = key {
576            self.intrinsics.borrow_mut().push(key, found);
577        }
578        found
579    }
580
581    // The extent of `style`'s own font with sizes in `units`: its ascent,
582    // descent and line gap in pixels. `None` with no font.
583    fn extent(&self, style: &TextStyle, units: TextUnits) -> Option<FontExtent> {
584        let span = &style.span_style;
585        let size = units
586            .density
587            .font_scale_curve()
588            .sp_to_dp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP))
589            * units.factor;
590        let fonts = self.fonts.borrow();
591        let typeface = fonts.resolve(FontRequest {
592            family: span.font_family.as_ref().unwrap_or(&FontFamily::Default),
593            weight: span.font_weight.unwrap_or(FontWeight::NORMAL),
594            style: span.font_style.unwrap_or(FontStyle::Normal),
595        })?;
596        let scale = size / f32::from(typeface.units_per_em());
597        let metrics = typeface.line_metrics();
598        Some(FontExtent::new(
599            metrics.ascent * scale,
600            -metrics.descent * scale,
601            metrics.line_gap * scale,
602        ))
603    }
604
605    // The box of a line of `style`'s own font with sizes in `units`, before
606    // any text: its advance, baseline and edges. `None` with no font.
607    fn line_box(&self, style: &TextStyle, units: TextUnits) -> Option<LineBox> {
608        let extent = self.extent(style, units)?;
609        let empty = AnnotatedString::from("");
610        let paragraph = ParagraphSpans::new(&empty, style, units);
611        let asked = paragraph.lengths.line_height.unwrap_or(f32::NAN);
612        Some(paragraph_line_box(
613            &style.paragraph_style,
614            extent,
615            asked,
616            units.density.density(),
617        ))
618    }
619
620    /// Starts a layout pass: the caches drop what recent passes did not use.
621    pub(crate) fn begin_layout_pass(&self) {
622        self.pass_owed.set(false);
623        let headroom = crate::current_memory_headroom();
624        // A tight heap keeps only what the last pass asked for.
625        let idle_passes = match headroom {
626            MemoryHeadroom::Ample => IDLE_PASSES,
627            MemoryHeadroom::Tight => 1,
628        };
629        self.layouts.borrow_mut().begin_pass(idle_passes, headroom);
630        self.intrinsics.borrow_mut().begin_pass(idle_passes, drop);
631        self.layouter.borrow_mut().set_memory_headroom(headroom);
632    }
633
634    /// A frame starts: the first use of the service in it outside a layout
635    /// pass begins a pass.
636    pub(crate) fn owe_pass(&self) {
637        self.pass_owed.set(true);
638    }
639
640    /// Begins the pass a frame owes, if no layout pass began one: what a
641    /// frame that only draws measures then ages out as a layout pass's does.
642    pub(crate) fn pay_owed_pass(&self) {
643        if self.pass_owed.get() {
644            self.begin_layout_pass();
645        }
646    }
647}
648
649// The fonts text lays out with before a renderer hands the app its own:
650// none, and Cranpose Test Mono in tests.
651fn default_fonts() -> FontFamilyResolver {
652    #[cfg(any(test, feature = "test-helpers"))]
653    {
654        super::test_fonts()
655    }
656    #[cfg(not(any(test, feature = "test-helpers")))]
657    {
658        FontFamilyResolver::new([])
659    }
660}
661
662// `text` and `style` as a text scaled down by `factor` draws them: sizes,
663// shadows and strokes `factor` of what they say, as its glyphs are. A text
664// whose spans scale nothing keeps its own string.
665fn scaled_drawing(
666    text: &Rc<AnnotatedString>,
667    style: &TextStyle,
668    factor: f32,
669) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
670    let mut scaled_style = style.clone();
671    scaled_style.span_style.font_size =
672        TextUnit::Sp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP) * factor);
673    scale_span_drawing(&mut scaled_style.span_style, factor);
674    let text = if text.span_styles.is_empty() {
675        Rc::clone(text)
676    } else {
677        let mut scaled = AnnotatedString::clone(text);
678        for span in &mut scaled.span_styles {
679            if let TextUnit::Sp(size) = span.item.font_size {
680                span.item.font_size = TextUnit::Sp(size * factor);
681            }
682            scale_span_drawing(&mut span.item, factor);
683        }
684        Rc::new(scaled)
685    };
686    (text, Arc::new(scaled_style))
687}
688
689// Scales the letter spacing, shadow and stroke of `span` by `factor`.
690fn scale_span_drawing(span: &mut SpanStyle, factor: f32) {
691    if let TextUnit::Sp(spacing) = span.letter_spacing {
692        span.letter_spacing = TextUnit::Sp(spacing * factor);
693    }
694    if let Some(shadow) = &mut span.shadow {
695        shadow.offset.x *= factor;
696        shadow.offset.y *= factor;
697        shadow.blur_radius *= factor;
698    }
699    if let Some(TextDrawStyle::Stroke { width }) = &mut span.draw_style {
700        *width *= factor;
701    }
702}
703
704// Text sizes at the host's density and font scale.
705fn host_units() -> TextUnits {
706    TextUnits {
707        density: Density::from_host(),
708        factor: 1.0,
709    }
710}
711
712// Text sizes taken as stated, on the host's pixel grid: a draw scope's text
713// style has its sizes resolved already.
714fn resolved_units() -> TextUnits {
715    TextUnits {
716        density: Density::with_curve(Density::from_host().density(), FontScaleCurve::linear(1.0)),
717        factor: 1.0,
718    }
719}
720
721fn prepare_borrowed(
722    text: &AnnotatedString,
723    style: &TextStyle,
724    frame: TextFrame,
725    units: TextUnits,
726) -> Rc<PreparedTextLayout> {
727    crate::render_state::with_text_service(|service| {
728        service.prepare(
729            LayoutSource::Borrowed { text, style },
730            frame,
731            (units, CacheAdmission::First),
732        )
733    })
734}
735
736/// Lays text out with `fonts` in the active app context from now on. Panics
737/// when no app context is active.
738pub fn set_text_fonts(fonts: FontFamilyResolver) {
739    let Some(context) = crate::render_state::current_app_context() else {
740        panic!("set_text_fonts requires an active AppContext");
741    };
742    context.set_text_fonts(fonts);
743}
744
745/// Measures `text` in `style` with line breaks only at its line breaks, at
746/// the system font scale.
747pub fn measure_text(text: &AnnotatedString, style: &TextStyle) -> TextMetrics {
748    prepare_borrowed(
749        text,
750        style,
751        TextFrame::unbounded(TextLayoutOptions::default()),
752        host_units(),
753    )
754    .metrics()
755}
756
757// `text` laid out in its style with lines ending only at its line breaks,
758// the style's sizes taken as stated, not scaled by the system font scale, as
759// a draw scope draws it: the cache keeps it while the last pass drew it,
760// and longer once it comes back, so a number drawn anew each frame takes
761// little room in it.
762pub(crate) fn prepare_resolved(text: &SharedText<'_>) -> Rc<PreparedTextLayout> {
763    crate::render_state::with_text_service(|service| {
764        service.prepare(
765            LayoutSource::Shared { text, shaped: None },
766            TextFrame::unbounded(TextLayoutOptions::default()),
767            (resolved_units(), CacheAdmission::Drawn),
768        )
769    })
770}
771
772/// Measures `text` wrapped to `max_width` under `options`. `None` and widths
773/// that are not positive and finite mean no width limit.
774pub fn measure_text_with_options(
775    text: &AnnotatedString,
776    style: &TextStyle,
777    options: TextLayoutOptions,
778    max_width: Option<f32>,
779) -> TextMetrics {
780    prepare_borrowed(
781        text,
782        style,
783        TextFrame::up_to(max_width, options),
784        host_units(),
785    )
786    .metrics()
787}
788
789/// Lays `text` out in `style` wrapped to `max_width` under `options`.
790pub fn prepare_text_layout(
791    text: &AnnotatedString,
792    style: &TextStyle,
793    options: TextLayoutOptions,
794    max_width: Option<f32>,
795) -> Rc<PreparedTextLayout> {
796    prepare_borrowed(
797        text,
798        style,
799        TextFrame::up_to(max_width, options),
800        host_units(),
801    )
802}
803
804// Lays out `text` in `style` between the widths of `frame`, sharing them,
805// kept by the shared cache as `admission` says.
806pub(crate) fn prepare_shared(
807    text: &SharedText<'_>,
808    frame: TextFrame,
809    admission: CacheAdmission,
810    shaped: Option<&TextLayout>,
811) -> Rc<PreparedTextLayout> {
812    crate::render_state::with_text_service(|service| {
813        service.prepare(
814            LayoutSource::Shared { text, shaped },
815            frame,
816            (host_units(), admission),
817        )
818    })
819}
820
821/// The byte offset of the caret nearest to the point (`x`, `y`) of `text`
822/// laid out in `style` with lines ending only at its line breaks, in logical
823/// pixels from its top left corner.
824pub fn get_offset_for_position(text: &AnnotatedString, style: &TextStyle, x: f32, y: f32) -> usize {
825    prepare_borrowed(
826        text,
827        style,
828        TextFrame::unbounded(TextLayoutOptions::default()),
829        host_units(),
830    )
831    .layout
832    .offset_for_position(x, y)
833}
834
835/// The byte offset of the caret nearest to the point (`x`, `y`) of `text`
836/// laid out in `style` and wrapped to `wrap_width`: the hit test of every
837/// editable text's pointer path. `x` and `y` are in text space, padding and
838/// scrolling taken off.
839pub fn offset_for_position_wrapped(
840    text: &str,
841    style: &TextStyle,
842    wrap_width: Option<f32>,
843    x: f32,
844    y: f32,
845) -> usize {
846    prepare_borrowed(
847        &AnnotatedString::from(text),
848        style,
849        TextFrame::up_to(wrap_width, TextLayoutOptions::default()),
850        host_units(),
851    )
852    .layout
853    .offset_for_position(x, y)
854}
855
856/// The x of the caret at byte `offset` of `text` laid out in `style` with
857/// lines ending only at its line breaks.
858pub fn get_cursor_x_for_offset(text: &AnnotatedString, style: &TextStyle, offset: usize) -> f32 {
859    prepare_borrowed(
860        text,
861        style,
862        TextFrame::unbounded(TextLayoutOptions::default()),
863        host_units(),
864    )
865    .layout
866    .horizontal_position(offset, CaretDirection::Primary)
867}
868
869/// Lays `text` out in `style` with lines ending only at its line breaks.
870pub fn layout_text(text: &AnnotatedString, style: &TextStyle) -> TextLayoutResult {
871    prepare_borrowed(
872        text,
873        style,
874        TextFrame::unbounded(TextLayoutOptions::default()),
875        host_units(),
876    )
877    .result()
878}
879
880/// The byte range of each line of `text` laid out at `max_width` with
881/// `options`, without its line break.
882pub fn wrapped_line_ranges(
883    text: &AnnotatedString,
884    style: &TextStyle,
885    options: TextLayoutOptions,
886    max_width: Option<f32>,
887) -> Vec<Range<usize>> {
888    let prepared = prepare_borrowed(
889        text,
890        style,
891        TextFrame::up_to(max_width, options),
892        host_units(),
893    );
894    let layout = &prepared.layout;
895    (0..layout.line_count())
896        .map(|line| {
897            let (start, end) = (layout.line_start(line), layout.line_end(line));
898            let line_text = text.text.get(start..end).unwrap_or_default();
899            let without_break = line_text.trim_end_matches(['\n', '\r', '\u{2028}', '\u{2029}']);
900            start..start + without_break.len()
901        })
902        .collect()
903}
904
905/// The widest word of `text` in `style`: Compose's
906/// `ParagraphIntrinsics.minIntrinsicWidth`.
907pub(crate) fn min_intrinsic_text_width(text: &AnnotatedString, style: &TextStyle) -> f32 {
908    crate::render_state::with_text_service(|service| service.intrinsics(text, style, host_units()))
909        .min_intrinsic_width
910}
911
912/// The line box of `style`'s own font: its advance, its baseline and what a
913/// paragraph gives back at its edges. `None` with no font.
914pub fn text_line_box(style: &TextStyle) -> Option<LineBox> {
915    crate::render_state::with_text_service(|service| service.line_box(style, host_units()))
916}
917
918/// Like [`text_line_box`] with `style`'s sizes taken as stated.
919pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<LineBox> {
920    crate::render_state::current_app_context()?;
921    crate::render_state::with_text_service(|service| service.line_box(style, resolved_units()))
922}
923
924/// Distance from the top of a `style` paragraph down to its first baseline.
925/// `None` with no font.
926pub fn first_baseline(style: &TextStyle) -> Option<f32> {
927    text_line_box(style).map(LineBox::first_baseline)
928}
929
930/// The glyph box `(top_offset, height)` of a `style` line inside a line
931/// `line_height` tall, where lines stack from the paragraph's top one advance
932/// apart: the font's ascent and descent under the first line's baseline. The
933/// whole line with no font.
934pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
935    let units = host_units();
936    crate::render_state::with_text_service(|service| {
937        let extent = service.extent(style, units)?;
938        let line_box = service.line_box(style, units)?;
939        Some((
940            line_box.first_baseline() - extent.ascent,
941            extent.ascent + extent.descent,
942        ))
943    })
944    .map_or((0.0, line_height), |(top, height)| {
945        (top.clamp(0.0, line_height), height.min(line_height))
946    })
947}
948
949#[cfg(test)]
950#[path = "tests/measure_tests.rs"]
951mod tests;