Skip to main content

cranpose_render_common/
text_paint.rs

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