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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! 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},
};
use crate::glyph_reach::InkSpread;
/// 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,
/// Whether the style or a span casts a shadow, which spreads past the
/// glyphs.
pub casts_shadow: bool,
/// How far past their outlines the glyphs put ink: a stroke's outer
/// half, and the bold or italic a face of the layout synthesizes.
pub spread: InkSpread,
}
impl TextPaint {
/// The paint of a text in `style`, whose [`TextStyle::render_hash`] is
/// `style_hash`, with `spans`, at `font_size`, laid out as `layout`, whose
/// string's `RenderString::render_hash` and emptiness are `text`.
pub fn of(
style: &TextStyle,
style_hash: u64,
spans: &[RangeStyle<SpanStyle>],
(font_size, layout): (f32, &cranpose_text::TextLayout),
(text_hash, text_empty): (u64, bool),
) -> Self {
let span_style = &style.span_style;
let span_paint = SpanPaint::of(spans);
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
)
&& !span_paint.foreground
&& !decorates(span_style.text_decoration)
&& !span_paint.decoration;
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,
casts_shadow: span_style.shadow.is_some() || span_paint.shadow,
// A plain text and its spans draw no stroke, and most cast no
// shadow.
spread: if plain && !span_paint.shadow {
InkSpread::of(0.0, layout.synthesized_style())
} else {
InkSpread::of_text(style, spans, layout)
},
}
}
}
// What the spans of a text paint of their own.
struct SpanPaint {
// A span paints its range with a gradient, an alpha or a draw style.
foreground: bool,
// A span draws a visible decoration.
decoration: bool,
// A span casts a shadow.
shadow: bool,
}
impl SpanPaint {
// What `spans` paint, found in one pass.
fn of(spans: &[RangeStyle<SpanStyle>]) -> Self {
let none = Self {
foreground: false,
decoration: false,
shadow: false,
};
spans.iter().fold(none, |paint, span| Self {
foreground: paint.foreground | span_has_foreground_override(&span.item),
decoration: paint.decoration | decorates(span.item.text_decoration),
shadow: paint.shadow | span.item.shadow.is_some(),
})
}
}
// Whether `decoration` draws a line.
fn decorates(decoration: Option<TextDecoration>) -> bool {
decoration.is_some_and(|decoration| decoration != TextDecoration::NONE)
}
/// 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 casts a shadow of its own.
pub fn spans_cast_shadow(spans: &[RangeStyle<SpanStyle>]) -> bool {
spans.iter().any(|span| span.item.shadow.is_some())
}
/// 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(_))))
}