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