cranpose_render_common/
text_paint.rs1use std::cell::Cell;
6
7use cranpose_ui::{
8 Brush, Color, TextStyle,
9 text::{RangeStyle, SpanStyle, TextDecoration, TextDrawStyle, TextMotion},
10};
11
12use crate::glyph_reach::InkSpread;
13
14#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct TextPaint {
17 pub plain: bool,
21 pub color: Color,
24 pub baseline_shift: f32,
26 pub style_hash: u64,
29 pub text_hash: u64,
33 pub text_empty: bool,
35 pub static_motion: bool,
38 pub casts_shadow: bool,
41 pub spread: InkSpread,
44}
45
46impl TextPaint {
47 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 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
96struct SpanPaint {
98 foreground: bool,
100 decoration: bool,
102 shadow: bool,
104}
105
106impl SpanPaint {
107 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
122fn decorates(decoration: Option<TextDecoration>) -> bool {
124 decoration.is_some_and(|decoration| decoration != TextDecoration::NONE)
125}
126
127#[derive(Clone, Debug, Default)]
131pub struct TextPaintCache(Cell<Option<TextPaint>>);
132
133impl TextPaintCache {
134 pub fn holding(paint: TextPaint) -> Self {
136 Self(Cell::new(Some(paint)))
137 }
138
139 pub fn get_or(&self, compute: impl FnOnce() -> TextPaint) -> TextPaint {
142 if let Some(paint) = self.0.get() {
143 return paint;
144 }
145 let paint = compute();
146 self.0.set(Some(paint));
147 paint
148 }
149}
150
151impl PartialEq for TextPaintCache {
152 fn eq(&self, _other: &Self) -> bool {
153 true
154 }
155}
156
157pub fn resolve_text_color_without_gradient_fallback(
160 text_style: &TextStyle,
161 default: Color,
162) -> Color {
163 let mut color = text_style
164 .span_style
165 .color
166 .or(match text_style.span_style.brush.as_ref() {
167 Some(Brush::Solid(color)) => Some(*color),
168 _ => None,
169 })
170 .unwrap_or(default);
171 if let Some(alpha) = text_style.span_style.alpha {
172 color.3 *= alpha.clamp(0.0, 1.0);
173 }
174 color
175}
176
177fn span_has_foreground_override(span_style: &SpanStyle) -> bool {
178 matches!(
179 span_style.brush.as_ref(),
180 Some(
181 Brush::LinearGradient { .. }
182 | Brush::RadialGradient { .. }
183 | Brush::SweepGradient { .. }
184 )
185 ) || span_style.alpha.is_some()
186 || span_style.draw_style.is_some()
187}
188
189pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
192 spans
193 .iter()
194 .any(|span| span_has_foreground_override(&span.item))
195}
196
197pub fn spans_cast_shadow(spans: &[RangeStyle<SpanStyle>]) -> bool {
199 spans.iter().any(|span| span.item.shadow.is_some())
200}
201
202pub fn spans_override_foreground_color(spans: &[RangeStyle<SpanStyle>]) -> bool {
204 spans
205 .iter()
206 .any(|span| span.item.color.is_some() || matches!(span.item.brush, Some(Brush::Solid(_))))
207}