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}
24
25impl TextPaint {
26 pub fn of(style: &TextStyle, spans: &[RangeStyle<SpanStyle>], font_size: f32) -> Self {
28 let span_style = &style.span_style;
29 let plain = span_style.background.is_none()
30 && span_style.shadow.is_none()
31 && span_style.brush.is_none()
32 && !matches!(
33 span_style.draw_style,
34 Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0
35 )
36 && !spans_override_foreground(spans)
37 && !has_visible_decoration(spans, style);
38 Self {
39 plain,
40 color: resolve_text_color_without_gradient_fallback(style, Color::WHITE),
41 baseline_shift: span_style
42 .baseline_shift
43 .filter(|shift| shift.is_specified())
44 .map_or(0.0, |shift| -(shift.0 * font_size)),
45 }
46 }
47}
48
49#[derive(Clone, Debug, Default)]
53pub struct TextPaintCache(Cell<Option<TextPaint>>);
54
55impl TextPaintCache {
56 pub fn holding(paint: TextPaint) -> Self {
58 Self(Cell::new(Some(paint)))
59 }
60
61 pub fn get_or(&self, compute: impl FnOnce() -> TextPaint) -> TextPaint {
64 if let Some(paint) = self.0.get() {
65 return paint;
66 }
67 let paint = compute();
68 self.0.set(Some(paint));
69 paint
70 }
71}
72
73impl PartialEq for TextPaintCache {
74 fn eq(&self, _other: &Self) -> bool {
75 true
76 }
77}
78
79pub fn resolve_text_color_without_gradient_fallback(
82 text_style: &TextStyle,
83 default: Color,
84) -> Color {
85 let mut color = text_style
86 .span_style
87 .color
88 .or(match text_style.span_style.brush.as_ref() {
89 Some(Brush::Solid(color)) => Some(*color),
90 _ => None,
91 })
92 .unwrap_or(default);
93 if let Some(alpha) = text_style.span_style.alpha {
94 color.3 *= alpha.clamp(0.0, 1.0);
95 }
96 color
97}
98
99fn span_has_foreground_override(span_style: &SpanStyle) -> bool {
100 matches!(
101 span_style.brush.as_ref(),
102 Some(
103 Brush::LinearGradient { .. }
104 | Brush::RadialGradient { .. }
105 | Brush::SweepGradient { .. }
106 )
107 ) || span_style.alpha.is_some()
108 || span_style.draw_style.is_some()
109}
110
111pub fn spans_override_foreground(spans: &[RangeStyle<SpanStyle>]) -> bool {
114 spans
115 .iter()
116 .any(|span| span_has_foreground_override(&span.item))
117}
118
119pub fn spans_override_foreground_color(spans: &[RangeStyle<SpanStyle>]) -> bool {
121 spans
122 .iter()
123 .any(|span| span.item.color.is_some() || matches!(span.item.brush, Some(Brush::Solid(_))))
124}
125
126pub fn has_visible_decoration(spans: &[RangeStyle<SpanStyle>], global_style: &TextStyle) -> bool {
128 let visible = |decoration: Option<TextDecoration>| {
129 decoration.is_some_and(|decoration| decoration != TextDecoration::NONE)
130 };
131 visible(global_style.span_style.text_decoration)
132 || spans.iter().any(|span| visible(span.item.text_decoration))
133}