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
12#[derive(Clone, Copy, Debug, PartialEq)]
14pub struct TextPaint {
15 pub plain: bool,
19 pub color: Color,
22 pub baseline_shift: f32,
24 pub style_hash: u64,
27 pub text_hash: u64,
31 pub text_empty: bool,
33 pub static_motion: bool,
36}
37
38impl TextPaint {
39 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#[derive(Clone, Debug, Default)]
82pub struct TextPaintCache(Cell<Option<TextPaint>>);
83
84impl TextPaintCache {
85 pub fn holding(paint: TextPaint) -> Self {
87 Self(Cell::new(Some(paint)))
88 }
89
90 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
108pub 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
140pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
143 spans
144 .iter()
145 .any(|span| span_has_foreground_override(&span.item))
146}
147
148pub 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
155pub 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}