Skip to main content

cranpose_ui/text/
draw_scope_text.rs

1//! Bridge between [`DrawScope`](cranpose_ui_graphics::DrawScope) text and the
2//! framework text stack.
3//!
4//! `cranpose-ui-graphics` sits below fonts, so a draw scope describes text with
5//! the flat [`DrawTextStyle`] value and delegates measurement back up here. This
6//! module owns the single translation from that value into the full
7//! [`TextStyle`] the measurer and the rasterizer both consume — so a string
8//! measured through [`DrawScope::measure_text`](cranpose_ui_graphics::DrawScope::measure_text)
9//! and the same string rasterized by the renderer are described identically,
10//! down to the cache key.
11
12use std::{rc::Rc, sync::Arc};
13
14use cranpose_ui_graphics::{
15    DrawTextMeasurer, DrawTextStyle, FontStyle as DrawFontStyle, MeasuredText, TextMeasurement,
16    estimate_text_measurement,
17};
18
19use super::{
20    font::{FontFamily, FontStyle, FontWeight},
21    line_box::LineBox,
22    recent_values::RecentValues,
23    style::{SpanStyle, TextStyle},
24    unit::TextUnit,
25};
26
27/// Builds the [`TextStyle`] that describes a draw-scope text run.
28///
29/// Everything a `DrawTextStyle` can say is a span attribute except the line
30/// height and its policy, so the rest of the paragraph style stays at its
31/// defaults — in particular `text_align` is left unspecified, because a draw
32/// scope has already resolved alignment into the primitive's rect.
33///
34/// The policy has to come across. Without it every run drawn through a canvas
35/// takes [`line_box`](fn@super::line_box)'s plain branch while a `Text` composable
36/// of the same style takes the AOSP one, and a screen that does both puts its
37/// two sets of rows a device pixel apart.
38pub fn text_style_for_draw_style(style: &DrawTextStyle) -> TextStyle {
39    let mut span_style = SpanStyle {
40        font_size: TextUnit::Sp(style.resolved_font_size()),
41        font_weight: Some(FontWeight::new(style.font_weight.value())),
42        font_style: Some(match style.font_style {
43            DrawFontStyle::Normal => FontStyle::Normal,
44            DrawFontStyle::Italic | DrawFontStyle::Oblique => FontStyle::Italic,
45        }),
46        ..SpanStyle::default()
47    };
48    if let Some(family) = &style.font_family {
49        span_style.font_family = Some(FontFamily::from_name(family));
50    }
51    let letter_spacing = style.resolved_letter_spacing();
52    if letter_spacing != 0.0 {
53        span_style.letter_spacing = TextUnit::Sp(letter_spacing);
54    }
55
56    let mut text_style = TextStyle::from_span_style(span_style);
57    if let Some(line_height) = style.line_height
58        && line_height.is_finite()
59        && line_height > 0.0
60    {
61        text_style.paragraph_style.line_height = TextUnit::Sp(line_height);
62    }
63    text_style.paragraph_style.line_height_style = style.line_height_style;
64    text_style
65}
66
67// How many draw-scope styles `drawn_text_style` keeps converted.
68const DRAWN_TEXT_STYLES: usize = 8;
69
70// A draw scope's style, the text style it converts to, and that style's
71// render hash.
72pub(crate) struct DrawnStyle {
73    draw: DrawTextStyle,
74    style: Arc<TextStyle>,
75    hash: u64,
76}
77
78// The draw-scope styles recently drawn in, which [`drawn_text_style`] keeps.
79pub(crate) type DrawnTextStyles = RecentValues<DrawnStyle, DRAWN_TEXT_STYLES>;
80
81/// The [`TextStyle`] that [`text_style_for_draw_style`] builds of `style`,
82/// and its render hash, converted once while draw scopes keep drawing in an
83/// equal style: a text measured and drawn every frame neither builds nor
84/// hashes its style again.
85pub fn drawn_text_style(style: &DrawTextStyle) -> (Arc<TextStyle>, u64) {
86    crate::thread_state::with(|state| {
87        let mut drawn = state.drawn_text_styles.borrow_mut();
88        let kept = match drawn.find(|kept| kept.draw == *style) {
89            Some(kept) => kept,
90            None => drawn.keep(drawn_style(style)),
91        };
92        (Arc::clone(&kept.style), kept.hash)
93    })
94}
95
96// `style` converted and hashed. Out of line: a frame that draws in a style
97// it drew in before runs none of it.
98#[cold]
99#[inline(never)]
100fn drawn_style(style: &DrawTextStyle) -> DrawnStyle {
101    let text_style = text_style_for_draw_style(style);
102    DrawnStyle {
103        draw: style.clone(),
104        hash: text_style.render_hash(),
105        style: Arc::new(text_style),
106    }
107}
108
109/// The line box a draw-scope style resolves to against the app's fonts: how
110/// tall one line is and where its baseline sits inside it.
111///
112/// This is the vertical half of [`DrawScope::measure_text`](cranpose_ui_graphics::DrawScope::measure_text),
113/// answerable without a string to measure or a scope to measure in — a layout
114/// that stacks rows of a known style needs the row pitch before it has any text
115/// for them. `None` when no app context owns the fonts.
116///
117/// It resolves the style exactly as the measurer does, which means the sizes are
118/// taken as stated: a `DrawTextStyle` is already resolved, so the system font
119/// scale must not be folded in a second time here.
120pub fn draw_style_line_box(style: &DrawTextStyle) -> Option<LineBox> {
121    super::measure::resolved_line_box(&text_style_for_draw_style(style))
122}
123
124/// Measures draw-scope text against the app's fonts.
125///
126/// Every call lands in the app context's layout cache, so measuring an
127/// unchanged string every frame is a hash lookup, not a shaping pass, and the
128/// layout drawn is the one measured.
129///
130/// "Resolved" is the whole point: a [`DrawTextStyle`] states final sizes, and a
131/// scene lowers a text primitive with `style.resolved_font_size()` untouched,
132/// so the system font scale must not be folded in here. It is applied where an
133/// unresolved size lives instead — the `Text` composable's `Sp` values — and
134/// that path carries the scaled style through to the renderer with it.
135#[derive(Clone, Copy, Debug, Default)]
136pub struct AppContextTextMeasurer;
137
138impl AppContextTextMeasurer {
139    /// A shared measurer to hand to
140    /// [`DrawScopeDefault::with_text_measurer`](cranpose_ui_graphics::DrawScopeDefault::with_text_measurer).
141    pub fn shared() -> Rc<dyn DrawTextMeasurer> {
142        crate::thread_state::with(|state| {
143            Rc::clone(state.draw_text_measurer.get_or_init(|| Rc::new(Self)))
144        })
145    }
146}
147
148impl DrawTextMeasurer for AppContextTextMeasurer {
149    fn measure_text(&self, text: &str, style: &DrawTextStyle) -> MeasuredText {
150        if crate::render_state::current_app_context().is_none() {
151            return MeasuredText {
152                measurement: estimate_text_measurement(text, style),
153                layout: None,
154            };
155        }
156        let prepared = prepare_draw_text(text, style);
157        let layout = &prepared.layout;
158        let line_height = layout.line_advance();
159        let first_baseline = layout.line_baseline(0);
160        let measurement = if text.is_empty() {
161            TextMeasurement::empty(line_height, first_baseline)
162        } else {
163            TextMeasurement {
164                size: prepared.size(),
165                line_height,
166                first_baseline,
167                line_count: prepared.metrics().line_count,
168            }
169        };
170        MeasuredText {
171            measurement,
172            layout: Some(prepared.layout_handle()),
173        }
174    }
175}
176
177// `text` in a draw scope's `style` laid out with the app's fonts, its sizes
178// taken as stated.
179fn prepare_draw_text(text: &str, style: &DrawTextStyle) -> Rc<super::PreparedTextLayout> {
180    let (style, style_hash) = drawn_text_style(style);
181    let text = super::shared_plain_annotated_string(text);
182    super::measure::prepare_resolved(&super::measure::SharedText {
183        text: &text,
184        style: &style,
185        style_hash,
186    })
187}
188
189#[cfg(test)]
190#[path = "tests/draw_scope_text_tests.rs"]
191mod tests;