cranpose_render_common/
text_paint.rs1use std::cell::Cell;
5
6use cranpose_ui::{
7 Brush, Color, TextStyle,
8 text::{RangeStyle, SpanStyle, TextDecoration, TextDrawStyle},
9};
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct TextPaint {
14 pub plain: bool,
18 pub color: Color,
21 pub baseline_shift: f32,
23 pub style_hash: u64,
26}
27
28impl TextPaint {
29 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#[derive(Clone, Debug, Default)]
63pub struct TextPaintCache(Cell<Option<TextPaint>>);
64
65impl TextPaintCache {
66 pub fn holding(paint: TextPaint) -> Self {
68 Self(Cell::new(Some(paint)))
69 }
70
71 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
89pub 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
121pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
124 spans
125 .iter()
126 .any(|span| span_has_foreground_override(&span.item))
127}
128
129pub 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
136pub 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}