1use std::ops::Range;
5
6use cranpose_text::{FontSynthesis, GlyphId, TextLayout, Typeface};
7use cranpose_ui::{
8 Brush, Color,
9 text::{
10 FontStyle, FontWeight, RangeStyle, RenderString, Shadow, SpanBoundaries, SpanStyle,
11 TextDrawStyle, TextMotion, TextStyle,
12 },
13};
14use cranpose_ui_graphics::{Point, whole};
15use smallvec::SmallVec;
16
17use crate::glyph_mask::GlyphRaster;
18
19#[derive(Clone, Copy, Debug)]
21pub struct StyledTextRef<'a> {
22 pub text: &'a str,
24 pub span_styles: &'a [RangeStyle<SpanStyle>],
26}
27
28impl StyledTextRef<'_> {
29 pub(crate) fn is_empty(&self) -> bool {
30 self.text.is_empty()
31 }
32
33 fn span_boundaries(&self) -> SpanBoundaries {
35 let mut boundaries: SpanBoundaries = smallvec::smallvec![0, self.text.len()];
36 for span in self.span_styles {
37 boundaries.push(span.range.start);
38 boundaries.push(span.range.end);
39 }
40 boundaries.sort_unstable();
41 boundaries.dedup();
42 boundaries.retain(|at| *at <= self.text.len() && self.text.is_char_boundary(*at));
43 boundaries
44 }
45}
46
47impl<'a> From<&'a str> for StyledTextRef<'a> {
48 fn from(text: &'a str) -> Self {
49 Self {
50 text,
51 span_styles: &[],
52 }
53 }
54}
55
56impl<'a> From<&'a cranpose_ui::text::AnnotatedString> for StyledTextRef<'a> {
57 fn from(text: &'a cranpose_ui::text::AnnotatedString) -> Self {
58 Self {
59 text: text.text.as_str(),
60 span_styles: &text.span_styles,
61 }
62 }
63}
64
65impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
66 fn from(text: &'a RenderString) -> Self {
67 Self {
68 text: text.text(),
69 span_styles: text.span_styles(),
70 }
71 }
72}
73
74pub(crate) fn visit_span_styles(
78 text: StyledTextRef<'_>,
79 style: &TextStyle,
80 mut visit: impl FnMut(Range<usize>, &SpanStyle),
81) {
82 if text.span_styles.is_empty() || text.text.is_empty() {
83 visit(0..text.text.len(), &style.span_style);
84 return;
85 }
86 for edges in text.span_boundaries().windows(2) {
87 let range = edges[0]..edges[1];
88 let merged = text
89 .span_styles
90 .iter()
91 .filter(|span| span.range.start <= range.start && range.end <= span.range.end)
92 .fold(style.span_style.clone(), |merged, span| {
93 merged.merge(&span.item)
94 });
95 visit(range, &merged);
96 }
97}
98
99#[derive(Clone, Debug, PartialEq)]
101pub(crate) struct SpanPaint {
102 pub(crate) brush: Brush,
103 pub(crate) alpha: f32,
105 pub(crate) shadow: Option<Shadow>,
106 pub(crate) stroke: Option<f32>,
108 weight: FontWeight,
109 style: FontStyle,
110 synthesis: FontSynthesis,
111}
112
113impl SpanPaint {
114 pub(crate) fn of(style: &SpanStyle, fallback: Color) -> Self {
117 let (brush, alpha) = match &style.brush {
118 Some(brush) => (brush.clone(), style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
119 None => (Brush::solid(style.resolve_foreground_color(fallback)), 1.0),
120 };
121 Self {
122 brush,
123 alpha,
124 shadow: style.shadow.filter(|shadow| shadow.color.3 > 0.0),
125 stroke: match style.draw_style {
126 Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0 => {
127 Some(width)
128 }
129 _ => None,
130 },
131 weight: style.font_weight.unwrap_or_default(),
132 style: style.font_style.unwrap_or_default(),
133 synthesis: style.font_synthesis.unwrap_or(FontSynthesis::All),
134 }
135 }
136
137 pub(crate) fn solid_color(&self) -> Option<Color> {
139 match self.brush {
140 Brush::Solid(color) => Some(Color(color.0, color.1, color.2, color.3 * self.alpha)),
141 _ => None,
142 }
143 }
144
145 pub(crate) fn raster(&self, typeface: &Typeface, size: f32, scale: f32) -> GlyphRaster {
149 let synthesis = typeface.synthesis(self.weight, self.style, self.synthesis);
150 GlyphRaster {
151 stroke: self.stroke.map(|width| width * scale),
152 embolden: if synthesis.bold {
153 size * fake_bold_ratio(size)
154 } else {
155 0.0
156 },
157 skew: if synthesis.italic {
158 FAKE_ITALIC_SKEW
159 } else {
160 0.0
161 },
162 }
163 }
164}
165
166pub(crate) const FAKE_ITALIC_SKEW: f32 = 0.25;
168
169pub(crate) const FAKE_BOLD_MAX_RATIO: f32 = 1.0 / 24.0;
172
173fn fake_bold_ratio(size: f32) -> f32 {
176 let along = ((size - 9.0) / 27.0).clamp(0.0, 1.0);
177 FAKE_BOLD_MAX_RATIO + along * (1.0 / 32.0 - FAKE_BOLD_MAX_RATIO)
178}
179
180pub(crate) struct SpanPaints {
182 paints: SmallVec<[(Range<usize>, SpanPaint); 2]>,
183}
184
185impl SpanPaints {
186 pub(crate) fn of(text: StyledTextRef<'_>, style: &TextStyle, fallback: Color) -> Self {
188 let mut paints: SmallVec<[(Range<usize>, SpanPaint); 2]> = SmallVec::new();
189 visit_span_styles(text, style, |range, merged| {
190 let paint = SpanPaint::of(merged, fallback);
191 match paints.last_mut() {
192 Some((last, last_paint)) if *last_paint == paint => last.end = range.end,
193 _ => paints.push((range, paint)),
194 }
195 });
196 Self { paints }
197 }
198
199 pub(crate) fn span_at(&self, offset: usize) -> (&Range<usize>, &SpanPaint) {
201 let at = self
202 .paints
203 .partition_point(|(range, _)| range.end <= offset)
204 .min(self.paints.len() - 1);
205 let (range, paint) = &self.paints[at];
206 (range, paint)
207 }
208
209 pub(crate) fn all(&self, test: impl Fn(&SpanPaint) -> bool) -> bool {
211 self.paints.iter().all(|(_, paint)| test(paint))
212 }
213}
214
215pub(crate) struct PlacedGlyph<'a> {
217 pub(crate) typeface: &'a Typeface,
218 pub(crate) glyph: GlyphId,
219 pub(crate) size: f32,
221 pub(crate) raster: GlyphRaster,
222 pub(crate) x: f32,
225 pub(crate) y: f32,
226 pub(crate) paint: &'a SpanPaint,
227}
228
229pub(crate) fn visit_placed_glyphs<'a>(
233 layout: &'a TextLayout,
234 paints: &'a SpanPaints,
235 (origin, scale): (Point, f32),
236 motion: TextMotion,
237 mut visit: impl FnMut(PlacedGlyph<'a>),
238) {
239 let place = |value: f32, from: f32| match motion {
240 TextMotion::Static => whole::round(from + value * scale),
241 TextMotion::Animated => from + whole::round(value * scale),
242 };
243 for line in 0..layout.line_count() {
244 for run in layout.glyph_runs(line) {
245 let size = run.size * scale;
246 let mut span: Option<(&Range<usize>, &SpanPaint, GlyphRaster)> = None;
249 for glyph in run.positions() {
250 let (_, paint, raster) = match span {
251 Some(found) if found.0.contains(&glyph.offset) => found,
252 _ => {
253 let (range, paint) = paints.span_at(glyph.offset);
254 *span.insert((range, paint, paint.raster(run.typeface, size, scale)))
255 }
256 };
257 visit(PlacedGlyph {
258 typeface: run.typeface,
259 glyph: glyph.id,
260 size,
261 raster,
262 x: place(glyph.x, origin.x),
263 y: place(glyph.y, origin.y),
264 paint,
265 });
266 }
267 }
268 }
269}