Skip to main content

cranpose_render_common/
text_paint.rs

1//! What a renderer reads of a text's style and string to draw its glyphs,
2//! worked out once per text node so that drawing a plain text never loads
3//! its style, and drawing a cached run never loads its string.
4
5use std::cell::OnceCell;
6
7use cranpose_ui::{
8    Brush, Color, TextStyle,
9    text::{RangeStyle, SpanStyle, TextDecoration, TextDrawStyle, TextMotion},
10};
11
12use crate::glyph_reach::InkSpread;
13
14/// The paint of a text node's glyphs.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct TextPaint {
17    /// Whether the glyphs in `color` are all the text draws: no background,
18    /// shadow, brush, stroke or decoration, and no span that paints its own
19    /// range.
20    pub plain: bool,
21    /// The style's colour, or its solid brush, times its alpha; white when
22    /// it names neither.
23    pub color: Color,
24    /// How far the style's baseline shift moves the glyphs down.
25    pub baseline_shift: f32,
26    /// The style's [`TextStyle::render_hash`], which keys the glyph caches
27    /// a renderer draws the text from, so no frame hashes the style again.
28    pub style_hash: u64,
29    /// The string's `RenderString::render_hash`, which keys those caches
30    /// beside the style's, so that drawing a cached run never loads the
31    /// string.
32    pub text_hash: u64,
33    /// Whether the string has no characters.
34    pub text_empty: bool,
35    /// Whether the style's motion is static, so its glyphs stay on whole
36    /// pixels.
37    pub static_motion: bool,
38    /// Whether the style or a span casts a shadow, which spreads past the
39    /// glyphs.
40    pub casts_shadow: bool,
41    /// How far past their outlines the glyphs put ink: a stroke's outer
42    /// half, and the bold or italic a face of the layout synthesizes.
43    pub spread: InkSpread,
44}
45
46impl TextPaint {
47    /// The paint of a text in `style`, whose [`TextStyle::render_hash`] is
48    /// `style_hash`, with `spans`, at `font_size`, laid out as `layout`, whose
49    /// string's `RenderString::render_hash` and emptiness are `text`.
50    pub fn of(
51        style: &TextStyle,
52        style_hash: u64,
53        spans: &[RangeStyle<SpanStyle>],
54        (font_size, layout): (f32, &cranpose_text::TextLayout),
55        (text_hash, text_empty): (u64, bool),
56    ) -> Self {
57        let span_style = &style.span_style;
58        let span_paint = SpanPaint::of(spans);
59        let plain = span_style.background.is_none()
60            && span_style.shadow.is_none()
61            && span_style.brush.is_none()
62            && !matches!(
63                span_style.draw_style,
64                Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0
65            )
66            && !span_paint.foreground
67            && !decorates(span_style.text_decoration)
68            && !span_paint.decoration;
69        Self {
70            plain,
71            color: resolve_text_color_without_gradient_fallback(style, Color::WHITE),
72            baseline_shift: span_style
73                .baseline_shift
74                .filter(|shift| shift.is_specified())
75                .map_or(0.0, |shift| -(shift.0 * font_size)),
76            style_hash,
77            text_hash,
78            text_empty,
79            static_motion: style
80                .paragraph_style
81                .text_motion
82                .unwrap_or(TextMotion::Static)
83                == TextMotion::Static,
84            casts_shadow: span_style.shadow.is_some() || span_paint.shadow,
85            // A plain text and its spans draw no stroke, and most cast no
86            // shadow.
87            spread: if plain && !span_paint.shadow {
88                InkSpread::of(0.0, layout.synthesized_style())
89            } else {
90                InkSpread::of_text(style, spans, layout)
91            },
92        }
93    }
94}
95
96// What the spans of a text paint of their own.
97struct SpanPaint {
98    // A span paints its range with a gradient, an alpha or a draw style.
99    foreground: bool,
100    // A span draws a visible decoration.
101    decoration: bool,
102    // A span casts a shadow.
103    shadow: bool,
104}
105
106impl SpanPaint {
107    // What `spans` paint, found in one pass.
108    fn of(spans: &[RangeStyle<SpanStyle>]) -> Self {
109        let none = Self {
110            foreground: false,
111            decoration: false,
112            shadow: false,
113        };
114        spans.iter().fold(none, |paint, span| Self {
115            foreground: paint.foreground | span_has_foreground_override(&span.item),
116            decoration: paint.decoration | decorates(span.item.text_decoration),
117            shadow: paint.shadow | span.item.shadow.is_some(),
118        })
119    }
120}
121
122// Whether `decoration` draws a line.
123fn decorates(decoration: Option<TextDecoration>) -> bool {
124    decoration.is_some_and(|decoration| decoration != TextDecoration::NONE)
125}
126
127/// A text node's [`TextPaint`], set when the scene builds the node or when a
128/// renderer first asks for it. It is derived data, so two caches always
129/// compare equal.
130#[derive(Clone, Debug, Default)]
131pub struct TextPaintCache(OnceCell<TextPaint>);
132
133impl TextPaintCache {
134    /// A cache holding `paint`.
135    pub fn holding(paint: TextPaint) -> Self {
136        Self(OnceCell::from(paint))
137    }
138
139    /// The cached paint, or the one `compute` returns, which the cache then
140    /// keeps.
141    pub fn get_or(&self, compute: impl FnOnce() -> TextPaint) -> &TextPaint {
142        self.0.get_or_init(compute)
143    }
144}
145
146impl PartialEq for TextPaintCache {
147    fn eq(&self, _other: &Self) -> bool {
148        true
149    }
150}
151
152/// The text colour a style names, its solid brush's colour, or `default`,
153/// times the style's alpha.
154pub fn resolve_text_color_without_gradient_fallback(
155    text_style: &TextStyle,
156    default: Color,
157) -> Color {
158    let mut color = text_style
159        .span_style
160        .color
161        .or(match text_style.span_style.brush.as_ref() {
162            Some(Brush::Solid(color)) => Some(*color),
163            _ => None,
164        })
165        .unwrap_or(default);
166    if let Some(alpha) = text_style.span_style.alpha {
167        color.3 *= alpha.clamp(0.0, 1.0);
168    }
169    color
170}
171
172fn span_has_foreground_override(span_style: &SpanStyle) -> bool {
173    matches!(
174        span_style.brush.as_ref(),
175        Some(
176            Brush::LinearGradient { .. }
177                | Brush::RadialGradient { .. }
178                | Brush::SweepGradient { .. }
179        )
180    ) || span_style.alpha.is_some()
181        || span_style.draw_style.is_some()
182}
183
184/// Whether a span paints its range with a gradient, an alpha or a draw
185/// style of its own.
186pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
187    spans
188        .iter()
189        .any(|span| span_has_foreground_override(&span.item))
190}
191
192/// Whether a span casts a shadow of its own.
193pub fn spans_cast_shadow(spans: &[RangeStyle<SpanStyle>]) -> bool {
194    spans.iter().any(|span| span.item.shadow.is_some())
195}
196
197/// Whether a span gives its range a colour or a solid brush of its own.
198pub fn spans_override_foreground_color(spans: &[RangeStyle<SpanStyle>]) -> bool {
199    spans
200        .iter()
201        .any(|span| span.item.color.is_some() || matches!(span.item.brush, Some(Brush::Solid(_))))
202}