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, Point, 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    // Exactly `width`, the width of a node's content, which the renderer
369    // draws its text at. A node not laid out yet has no width, and no limit.
370    pub(crate) fn drawn(width: f32, options: TextLayoutOptions) -> Self {
371        if width.is_finite() && width > 0.0 {
372            Self {
373                min_width: width,
374                max_width: width,
375                options,
376            }
377        } else {
378            Self::up_to(None, options)
379        }
380    }
381}
382
383/// The app's fonts and the text engine its text lays out with, and the
384/// layouts and intrinsic widths recent passes used.
385pub(crate) struct TextService {
386    generation: Cell<u64>,
387    /// Whether a frame started since the last pass began: the first use of
388    /// the service outside a layout pass then begins one.
389    pass_owed: Cell<bool>,
390    fonts: RefCell<FontFamilyResolver>,
391    layouter: RefCell<TextLayouter>,
392    layouts: RefCell<LayoutCache>,
393    intrinsics: RefCell<PassAgedCache<TextKey, ParagraphIntrinsics>>,
394}
395
396impl TextService {
397    pub(crate) fn new() -> Self {
398        Self {
399            generation: Cell::new(1),
400            pass_owed: Cell::new(false),
401            fonts: RefCell::new(default_fonts()),
402            layouter: RefCell::new(TextLayouter::default()),
403            layouts: RefCell::new(LayoutCache::new()),
404            intrinsics: RefCell::new(PassAgedCache::with_capacity_at_least_one(
405                TEXT_INTRINSICS_CACHE_CAPACITY,
406            )),
407        }
408    }
409
410    /// Lays text out with `fonts` from now on.
411    pub(crate) fn set_fonts(&self, fonts: FontFamilyResolver) {
412        *self.fonts.borrow_mut() = fonts;
413        self.generation
414            .set(self.generation.get().wrapping_add(1).max(1));
415        self.layouts.borrow_mut().clear();
416        self.intrinsics.borrow_mut().clear();
417    }
418
419    pub(crate) fn generation(&self) -> u64 {
420        self.generation.get()
421    }
422
423    // The layout of `source` in `frame` with sizes in `units`: a cached one
424    // that breaks into the same lines, aligned to the frame's widths, or a
425    // new one.
426    fn prepare(
427        &self,
428        source: LayoutSource<'_>,
429        frame: TextFrame,
430        (units, admission): (TextUnits, CacheAdmission),
431    ) -> Rc<PreparedTextLayout> {
432        let (text, style) = (source.text(), source.style());
433        let key = TextKey::of(text, source.style_hash(), frame.options, units);
434        let cached = key.map(|key| {
435            let cached = self
436                .layouts
437                .borrow_mut()
438                .cached(key, frame, (style, admission));
439            (key, cached)
440        });
441        let missed = match cached {
442            Some((_, Cached::Held(prepared))) => return prepared,
443            Some((key, Cached::Missed(miss))) => Some((key, miss)),
444            None => None,
445        };
446        let (text, style) = source.shared();
447        let prepared = Rc::new(self.lay_out(text, style, frame, (units, source.shaped())));
448        if let Some((key, miss)) = missed {
449            self.layouts
450                .borrow_mut()
451                .keep(key, &prepared, (miss, admission));
452        }
453        prepared
454    }
455
456    // Lays `text` out in `style`, auto-sizing it when its options ask, with
457    // the glyphs of `shaped` when given.
458    fn lay_out(
459        &self,
460        text: Rc<AnnotatedString>,
461        style: Arc<TextStyle>,
462        frame: TextFrame,
463        (units, shaped): (TextUnits, Option<&TextLayout>),
464    ) -> PreparedTextLayout {
465        let options = frame.options.normalized();
466        let min_lines = options.min_lines;
467        let Some(min_font_size_sp) = options.overflow.scale_down_min_font_size_sp() else {
468            let layout = self.layout(&text, &style, frame, (units, shaped));
469            return PreparedTextLayout::new(text, style, layout, (frame.max_width, min_lines));
470        };
471        self.lay_out_scaled_down(text, style, frame, (units, shaped), min_font_size_sp)
472    }
473
474    // Lays `text` out in `style` at the largest size from `min_font_size_sp`
475    // up that fits `frame`. Out of line: its layouts stay off the stack of
476    // the common layout, which shaping runs at the bottom of.
477    #[inline(never)]
478    fn lay_out_scaled_down(
479        &self,
480        text: Rc<AnnotatedString>,
481        style: Arc<TextStyle>,
482        frame: TextFrame,
483        (units, shaped): (TextUnits, Option<&TextLayout>),
484        min_font_size_sp: f32,
485    ) -> PreparedTextLayout {
486        let options = frame.options.normalized();
487        let min_lines = options.min_lines;
488        let frame = TextFrame {
489            options: TextLayoutOptions {
490                overflow: TextOverflow::Clip,
491                ..options
492            },
493            ..frame
494        };
495        let fits =
496            |layout: &TextLayout| !layout.did_overflow_height() && !layout.did_overflow_width();
497        let full = self.layout(&text, &style, frame, (units, shaped));
498        let base = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
499        let min_factor = (min_font_size_sp.min(base) / base).clamp(0.0, 1.0);
500        if fits(&full) || !frame.max_width.is_finite() || min_factor >= 1.0 {
501            return PreparedTextLayout::new(text, style, full, (frame.max_width, min_lines));
502        }
503        // Each size takes the glyphs of the first: shaping is the same at
504        // every size.
505        let at = |factor: f32| {
506            let units = TextUnits {
507                factor: units.factor * factor,
508                ..units
509            };
510            self.layout(&text, &style, frame, (units, Some(&full)))
511        };
512        let (mut best, mut factor) = (at(min_factor), min_factor);
513        if fits(&best) {
514            let (mut low, mut high) = (min_factor, 1.0);
515            for _ in 0..SCALE_DOWN_SEARCH_STEPS {
516                let mid = (low + high) * 0.5;
517                let candidate = at(mid);
518                if fits(&candidate) {
519                    low = mid;
520                    (best, factor) = (candidate, mid);
521                } else {
522                    high = mid;
523                }
524            }
525        }
526        let (text, style) = scaled_drawing(&text, &style, factor);
527        PreparedTextLayout::new(text, style, best, (frame.max_width, min_lines))
528    }
529
530    // One layout of `text` in `style` in `frame`, with sizes in `units`, with
531    // the glyphs of `shaped` when given.
532    fn layout(
533        &self,
534        text: &AnnotatedString,
535        style: &TextStyle,
536        frame: TextFrame,
537        (units, shaped): (TextUnits, Option<&TextLayout>),
538    ) -> TextLayout {
539        let paragraph = ParagraphSpans::new(text, style, units);
540        let fonts = self.fonts.borrow();
541        let spans = paragraph.spans(&fonts);
542        let input = LayoutInput {
543            text: &text.text,
544            spans: &spans,
545            paragraph: &style.paragraph_style,
546            lengths: paragraph.lengths,
547            min_width: frame.min_width,
548            max_width: frame.max_width,
549            options: frame.options,
550        };
551        let mut layouter = self.layouter.borrow_mut();
552        match shaped {
553            Some(shaped) => layouter.relayout(&input, &fonts, shaped),
554            None => layouter.layout(&input, &fonts),
555        }
556    }
557
558    // The intrinsic widths of `text` in `style`, with sizes in `units`.
559    fn intrinsics(
560        &self,
561        text: &AnnotatedString,
562        style: &TextStyle,
563        units: TextUnits,
564    ) -> ParagraphIntrinsics {
565        let key = TextKey::of(
566            text,
567            style.measurement_hash(),
568            TextLayoutOptions::default(),
569            units,
570        );
571        if let Some(key) = key
572            && let Some(found) = self.intrinsics.borrow_mut().get(&key).copied()
573        {
574            return found;
575        }
576        let paragraph = ParagraphSpans::new(text, style, units);
577        let fonts = self.fonts.borrow();
578        let spans = paragraph.spans(&fonts);
579        let input = LayoutInput {
580            text: &text.text,
581            spans: &spans,
582            paragraph: &style.paragraph_style,
583            lengths: paragraph.lengths,
584            min_width: 0.0,
585            max_width: f32::INFINITY,
586            options: TextLayoutOptions::default(),
587        };
588        let found = self.layouter.borrow_mut().intrinsics(&input, &fonts);
589        if let Some(key) = key {
590            self.intrinsics.borrow_mut().push(key, found);
591        }
592        found
593    }
594
595    // The extent of `style`'s own font with sizes in `units`: its ascent,
596    // descent and line gap in pixels. `None` with no font.
597    fn extent(&self, style: &TextStyle, units: TextUnits) -> Option<FontExtent> {
598        let span = &style.span_style;
599        let size = units
600            .density
601            .font_scale_curve()
602            .sp_to_dp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP))
603            * units.factor;
604        let fonts = self.fonts.borrow();
605        let typeface = fonts.resolve(FontRequest {
606            family: span.font_family.as_ref().unwrap_or(&FontFamily::Default),
607            weight: span.font_weight.unwrap_or(FontWeight::NORMAL),
608            style: span.font_style.unwrap_or(FontStyle::Normal),
609        })?;
610        let scale = size / f32::from(typeface.units_per_em());
611        let metrics = typeface.line_metrics();
612        Some(FontExtent::new(
613            metrics.ascent * scale,
614            -metrics.descent * scale,
615            metrics.line_gap * scale,
616        ))
617    }
618
619    // The box of a line of `style`'s own font with sizes in `units`, before
620    // any text: its advance, baseline and edges. `None` with no font.
621    fn line_box(&self, style: &TextStyle, units: TextUnits) -> Option<LineBox> {
622        let extent = self.extent(style, units)?;
623        let empty = AnnotatedString::from("");
624        let paragraph = ParagraphSpans::new(&empty, style, units);
625        let asked = paragraph.lengths.line_height.unwrap_or(f32::NAN);
626        Some(paragraph_line_box(
627            &style.paragraph_style,
628            extent,
629            asked,
630            units.density.density(),
631        ))
632    }
633
634    /// Starts a layout pass: the caches drop what recent passes did not use.
635    pub(crate) fn begin_layout_pass(&self) {
636        self.pass_owed.set(false);
637        let headroom = crate::current_memory_headroom();
638        // A tight heap keeps only what the last pass asked for.
639        let idle_passes = match headroom {
640            MemoryHeadroom::Ample => IDLE_PASSES,
641            MemoryHeadroom::Tight => 1,
642        };
643        self.layouts.borrow_mut().begin_pass(idle_passes, headroom);
644        self.intrinsics.borrow_mut().begin_pass(idle_passes, drop);
645        self.layouter.borrow_mut().set_memory_headroom(headroom);
646    }
647
648    /// A frame starts: the first use of the service in it outside a layout
649    /// pass begins a pass.
650    pub(crate) fn owe_pass(&self) {
651        self.pass_owed.set(true);
652    }
653
654    /// Begins the pass a frame owes, if no layout pass began one: what a
655    /// frame that only draws measures then ages out as a layout pass's does.
656    pub(crate) fn pay_owed_pass(&self) {
657        if self.pass_owed.get() {
658            self.begin_layout_pass();
659        }
660    }
661}
662
663// The fonts text lays out with before a renderer hands the app its own:
664// none, and Cranpose Test Mono in tests.
665fn default_fonts() -> FontFamilyResolver {
666    #[cfg(any(test, feature = "test-helpers"))]
667    {
668        super::test_fonts()
669    }
670    #[cfg(not(any(test, feature = "test-helpers")))]
671    {
672        FontFamilyResolver::new([])
673    }
674}
675
676// `text` and `style` as a text scaled down by `factor` draws them: sizes,
677// shadows and strokes `factor` of what they say, as its glyphs are. A text
678// whose spans scale nothing keeps its own string.
679fn scaled_drawing(
680    text: &Rc<AnnotatedString>,
681    style: &TextStyle,
682    factor: f32,
683) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
684    let mut scaled_style = style.clone();
685    scaled_style.span_style.font_size =
686        TextUnit::Sp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP) * factor);
687    scale_span_drawing(&mut scaled_style.span_style, factor);
688    let text = if text.span_styles.is_empty() {
689        Rc::clone(text)
690    } else {
691        let mut scaled = AnnotatedString::clone(text);
692        for span in &mut scaled.span_styles {
693            if let TextUnit::Sp(size) = span.item.font_size {
694                span.item.font_size = TextUnit::Sp(size * factor);
695            }
696            scale_span_drawing(&mut span.item, factor);
697        }
698        Rc::new(scaled)
699    };
700    (text, Arc::new(scaled_style))
701}
702
703// Scales the letter spacing, shadow and stroke of `span` by `factor`.
704fn scale_span_drawing(span: &mut SpanStyle, factor: f32) {
705    if let TextUnit::Sp(spacing) = span.letter_spacing {
706        span.letter_spacing = TextUnit::Sp(spacing * factor);
707    }
708    if let Some(shadow) = &mut span.shadow {
709        shadow.offset.x *= factor;
710        shadow.offset.y *= factor;
711        shadow.blur_radius *= factor;
712    }
713    if let Some(TextDrawStyle::Stroke { width }) = &mut span.draw_style {
714        *width *= factor;
715    }
716}
717
718// Text sizes at the host's density and font scale.
719fn host_units() -> TextUnits {
720    TextUnits {
721        density: Density::from_host(),
722        factor: 1.0,
723    }
724}
725
726// Text sizes taken as stated, on the host's pixel grid: a draw scope's text
727// style has its sizes resolved already.
728fn resolved_units() -> TextUnits {
729    TextUnits {
730        density: Density::with_curve(Density::from_host().density(), FontScaleCurve::linear(1.0)),
731        factor: 1.0,
732    }
733}
734
735fn prepare_borrowed(
736    text: &AnnotatedString,
737    style: &TextStyle,
738    frame: TextFrame,
739    units: TextUnits,
740) -> Rc<PreparedTextLayout> {
741    crate::render_state::with_text_service(|service| {
742        service.prepare(
743            LayoutSource::Borrowed { text, style },
744            frame,
745            (units, CacheAdmission::First),
746        )
747    })
748}
749
750/// Lays text out with `fonts` in the active app context from now on. Panics
751/// when no app context is active.
752pub fn set_text_fonts(fonts: FontFamilyResolver) {
753    let Some(context) = crate::render_state::current_app_context() else {
754        panic!("set_text_fonts requires an active AppContext");
755    };
756    context.set_text_fonts(fonts);
757}
758
759/// Measures `text` in `style` with line breaks only at its line breaks, at
760/// the system font scale.
761pub fn measure_text(text: &AnnotatedString, style: &TextStyle) -> TextMetrics {
762    prepare_borrowed(
763        text,
764        style,
765        TextFrame::unbounded(TextLayoutOptions::default()),
766        host_units(),
767    )
768    .metrics()
769}
770
771// `text` laid out in its style with lines ending only at its line breaks,
772// the style's sizes taken as stated, not scaled by the system font scale, as
773// a draw scope draws it: the cache keeps it while the last pass drew it,
774// and longer once it comes back, so a number drawn anew each frame takes
775// little room in it.
776pub(crate) fn prepare_resolved(text: &SharedText<'_>) -> Rc<PreparedTextLayout> {
777    crate::render_state::with_text_service(|service| {
778        service.prepare(
779            LayoutSource::Shared { text, shaped: None },
780            TextFrame::unbounded(TextLayoutOptions::default()),
781            (resolved_units(), CacheAdmission::Drawn),
782        )
783    })
784}
785
786/// Measures `text` wrapped to `max_width` under `options`. `None` and widths
787/// that are not positive and finite mean no width limit.
788pub fn measure_text_with_options(
789    text: &AnnotatedString,
790    style: &TextStyle,
791    options: TextLayoutOptions,
792    max_width: Option<f32>,
793) -> TextMetrics {
794    prepare_borrowed(
795        text,
796        style,
797        TextFrame::up_to(max_width, options),
798        host_units(),
799    )
800    .metrics()
801}
802
803/// Lays `text` out in `style` wrapped to `max_width` under `options`.
804pub fn prepare_text_layout(
805    text: &AnnotatedString,
806    style: &TextStyle,
807    options: TextLayoutOptions,
808    max_width: Option<f32>,
809) -> Rc<PreparedTextLayout> {
810    prepare_borrowed(
811        text,
812        style,
813        TextFrame::up_to(max_width, options),
814        host_units(),
815    )
816}
817
818// Lays out `text` in `style` between the widths of `frame`, sharing them,
819// kept by the shared cache as `admission` says.
820pub(crate) fn prepare_shared(
821    text: &SharedText<'_>,
822    frame: TextFrame,
823    admission: CacheAdmission,
824    shaped: Option<&TextLayout>,
825) -> Rc<PreparedTextLayout> {
826    crate::render_state::with_text_service(|service| {
827        service.prepare(
828            LayoutSource::Shared { text, shaped },
829            frame,
830            (host_units(), admission),
831        )
832    })
833}
834
835/// The byte offset of the caret nearest to the point (`x`, `y`) of `text`
836/// laid out in `style` with lines ending only at its line breaks, in logical
837/// pixels from its top left corner.
838pub fn get_offset_for_position(text: &AnnotatedString, style: &TextStyle, x: f32, y: f32) -> usize {
839    prepare_borrowed(
840        text,
841        style,
842        TextFrame::unbounded(TextLayoutOptions::default()),
843        host_units(),
844    )
845    .layout
846    .offset_for_position(x, y)
847}
848
849// The byte offset nearest `point` of `text` as a node's content `width`
850// wide draws it, in the content's coordinates.
851pub(crate) fn offset_in_drawn_text(
852    text: &AnnotatedString,
853    style: &TextStyle,
854    options: TextLayoutOptions,
855    width: f32,
856    point: Point,
857) -> usize {
858    prepare_borrowed(text, style, TextFrame::drawn(width, options), host_units())
859        .layout
860        .offset_for_position(point.x, point.y)
861}
862
863/// The byte offset of the caret nearest to the point (`x`, `y`) of `text`
864/// laid out in `style` and wrapped to `wrap_width`: the hit test of every
865/// editable text's pointer path. `x` and `y` are in text space, padding and
866/// scrolling taken off.
867pub fn offset_for_position_wrapped(
868    text: &str,
869    style: &TextStyle,
870    wrap_width: Option<f32>,
871    x: f32,
872    y: f32,
873) -> usize {
874    prepare_borrowed(
875        &AnnotatedString::from(text),
876        style,
877        TextFrame::up_to(wrap_width, TextLayoutOptions::default()),
878        host_units(),
879    )
880    .layout
881    .offset_for_position(x, y)
882}
883
884/// The x of the caret at byte `offset` of `text` laid out in `style` with
885/// lines ending only at its line breaks.
886pub fn get_cursor_x_for_offset(text: &AnnotatedString, style: &TextStyle, offset: usize) -> f32 {
887    prepare_borrowed(
888        text,
889        style,
890        TextFrame::unbounded(TextLayoutOptions::default()),
891        host_units(),
892    )
893    .layout
894    .horizontal_position(offset, CaretDirection::Primary)
895}
896
897/// Lays `text` out in `style` with lines ending only at its line breaks.
898pub fn layout_text(text: &AnnotatedString, style: &TextStyle) -> TextLayoutResult {
899    prepare_borrowed(
900        text,
901        style,
902        TextFrame::unbounded(TextLayoutOptions::default()),
903        host_units(),
904    )
905    .result()
906}
907
908/// The byte range of each line of `text` laid out at `max_width` with
909/// `options`, without its line break.
910pub fn wrapped_line_ranges(
911    text: &AnnotatedString,
912    style: &TextStyle,
913    options: TextLayoutOptions,
914    max_width: Option<f32>,
915) -> Vec<Range<usize>> {
916    let prepared = prepare_borrowed(
917        text,
918        style,
919        TextFrame::up_to(max_width, options),
920        host_units(),
921    );
922    let layout = &prepared.layout;
923    (0..layout.line_count())
924        .map(|line| {
925            let (start, end) = (layout.line_start(line), layout.line_end(line));
926            let line_text = text.text.get(start..end).unwrap_or_default();
927            let without_break = line_text.trim_end_matches(['\n', '\r', '\u{2028}', '\u{2029}']);
928            start..start + without_break.len()
929        })
930        .collect()
931}
932
933/// The widest word of `text` in `style`: Compose's
934/// `ParagraphIntrinsics.minIntrinsicWidth`.
935pub(crate) fn min_intrinsic_text_width(text: &AnnotatedString, style: &TextStyle) -> f32 {
936    crate::render_state::with_text_service(|service| service.intrinsics(text, style, host_units()))
937        .min_intrinsic_width
938}
939
940/// The line box of `style`'s own font: its advance, its baseline and what a
941/// paragraph gives back at its edges. `None` with no font.
942pub fn text_line_box(style: &TextStyle) -> Option<LineBox> {
943    crate::render_state::with_text_service(|service| service.line_box(style, host_units()))
944}
945
946/// Like [`text_line_box`] with `style`'s sizes taken as stated.
947pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<LineBox> {
948    crate::render_state::current_app_context()?;
949    crate::render_state::with_text_service(|service| service.line_box(style, resolved_units()))
950}
951
952/// Distance from the top of a `style` paragraph down to its first baseline.
953/// `None` with no font.
954pub fn first_baseline(style: &TextStyle) -> Option<f32> {
955    text_line_box(style).map(LineBox::first_baseline)
956}
957
958/// The glyph box `(top_offset, height)` of a `style` line inside a line
959/// `line_height` tall, where lines stack from the paragraph's top one advance
960/// apart: the font's ascent and descent under the first line's baseline. The
961/// whole line with no font.
962pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
963    let units = host_units();
964    crate::render_state::with_text_service(|service| {
965        let extent = service.extent(style, units)?;
966        let line_box = service.line_box(style, units)?;
967        Some((
968            line_box.first_baseline() - extent.ascent,
969            extent.ascent + extent.descent,
970        ))
971    })
972    .map_or((0.0, line_height), |(top, height)| {
973        (top.clamp(0.0, line_height), height.min(line_height))
974    })
975}
976
977#[cfg(test)]
978#[path = "tests/measure_tests.rs"]
979mod tests;