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::Cell;
6
7use cranpose_ui::{
8    Brush, Color, TextStyle,
9    text::{RangeStyle, SpanStyle, TextDecoration, TextDrawStyle, TextMotion},
10};
11
12/// The paint of a text node's glyphs.
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct TextPaint {
15    /// Whether the glyphs in `color` are all the text draws: no background,
16    /// shadow, brush, stroke or decoration, and no span that paints its own
17    /// range.
18    pub plain: bool,
19    /// The style's colour, or its solid brush, times its alpha; white when
20    /// it names neither.
21    pub color: Color,
22    /// How far the style's baseline shift moves the glyphs down.
23    pub baseline_shift: f32,
24    /// The style's [`TextStyle::render_hash`], which keys the glyph caches
25    /// a renderer draws the text from, so no frame hashes the style again.
26    pub style_hash: u64,
27    /// The string's `RenderString::render_hash`, which keys those caches
28    /// beside the style's, so that drawing a cached run never loads the
29    /// string.
30    pub text_hash: u64,
31    /// Whether the string has no characters.
32    pub text_empty: bool,
33    /// Whether the style's motion is static, so its glyphs stay on whole
34    /// pixels.
35    pub static_motion: bool,
36}
37
38impl TextPaint {
39    /// The paint of a text in `style`, whose [`TextStyle::render_hash`] is
40    /// `style_hash`, with `spans`, at `font_size`, whose string's
41    /// `RenderString::render_hash` and emptiness are `text`.
42    pub fn of(
43        style: &TextStyle,
44        style_hash: u64,
45        spans: &[RangeStyle<SpanStyle>],
46        font_size: f32,
47        (text_hash, text_empty): (u64, bool),
48    ) -> Self {
49        let span_style = &style.span_style;
50        let plain = span_style.background.is_none()
51            && span_style.shadow.is_none()
52            && span_style.brush.is_none()
53            && !matches!(
54                span_style.draw_style,
55                Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0
56            )
57            && !spans_override_foreground(spans)
58            && !has_visible_decoration(spans, style);
59        Self {
60            plain,
61            color: resolve_text_color_without_gradient_fallback(style, Color::WHITE),
62            baseline_shift: span_style
63                .baseline_shift
64                .filter(|shift| shift.is_specified())
65                .map_or(0.0, |shift| -(shift.0 * font_size)),
66            style_hash,
67            text_hash,
68            text_empty,
69            static_motion: style
70                .paragraph_style
71                .text_motion
72                .unwrap_or(TextMotion::Static)
73                == TextMotion::Static,
74        }
75    }
76}
77
78/// A text node's [`TextPaint`], set when the scene builds the node or when a
79/// renderer first asks for it. It is derived data, so two caches always
80/// compare equal.
81#[derive(Clone, Debug, Default)]
82pub struct TextPaintCache(Cell<Option<TextPaint>>);
83
84impl TextPaintCache {
85    /// A cache holding `paint`.
86    pub fn holding(paint: TextPaint) -> Self {
87        Self(Cell::new(Some(paint)))
88    }
89
90    /// The cached paint, or the one `compute` returns, which the cache then
91    /// keeps.
92    pub fn get_or(&self, compute: impl FnOnce() -> TextPaint) -> TextPaint {
93        if let Some(paint) = self.0.get() {
94            return paint;
95        }
96        let paint = compute();
97        self.0.set(Some(paint));
98        paint
99    }
100}
101
102impl PartialEq for TextPaintCache {
103    fn eq(&self, _other: &Self) -> bool {
104        true
105    }
106}
107
108/// The text colour a style names, its solid brush's colour, or `default`,
109/// times the style's alpha.
110pub fn resolve_text_color_without_gradient_fallback(
111    text_style: &TextStyle,
112    default: Color,
113) -> Color {
114    let mut color = text_style
115        .span_style
116        .color
117        .or(match text_style.span_style.brush.as_ref() {
118            Some(Brush::Solid(color)) => Some(*color),
119            _ => None,
120        })
121        .unwrap_or(default);
122    if let Some(alpha) = text_style.span_style.alpha {
123        color.3 *= alpha.clamp(0.0, 1.0);
124    }
125    color
126}
127
128fn span_has_foreground_override(span_style: &SpanStyle) -> bool {
129    matches!(
130        span_style.brush.as_ref(),
131        Some(
132            Brush::LinearGradient { .. }
133                | Brush::RadialGradient { .. }
134                | Brush::SweepGradient { .. }
135        )
136    ) || span_style.alpha.is_some()
137        || span_style.draw_style.is_some()
138}
139
140/// Whether a span paints its range with a gradient, an alpha or a draw
141/// style of its own.
142pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
143    spans
144        .iter()
145        .any(|span| span_has_foreground_override(&span.item))
146}
147
148/// Whether a span gives its range a colour or a solid brush of its own.
149pub fn spans_override_foreground_color(spans: &[RangeStyle<SpanStyle>]) -> bool {
150    spans
151        .iter()
152        .any(|span| span.item.color.is_some() || matches!(span.item.brush, Some(Brush::Solid(_))))
153}
154
155/// Whether the style or a span draws an underline or a line-through.
156pub fn has_visible_decoration(spans: &[RangeStyle<SpanStyle>], global_style: &TextStyle) -> bool {
157    let visible = |decoration: Option<TextDecoration>| {
158        decoration.is_some_and(|decoration| decoration != TextDecoration::NONE)
159    };
160    visible(global_style.span_style.text_decoration)
161        || spans.iter().any(|span| visible(span.item.text_decoration))
162}