use std::ops::Range;
use cranpose_text::{FontSynthesis, GlyphId, TextLayout, Typeface};
use cranpose_ui::{
Brush, Color,
text::{
FontStyle, FontWeight, RangeStyle, RenderString, Shadow, SpanBoundaries, SpanStyle,
TextDrawStyle, TextMotion, TextStyle,
},
};
use cranpose_ui_graphics::{Point, whole};
use smallvec::SmallVec;
use crate::glyph_mask::GlyphRaster;
#[derive(Clone, Copy, Debug)]
pub struct StyledTextRef<'a> {
pub text: &'a str,
pub span_styles: &'a [RangeStyle<SpanStyle>],
}
impl StyledTextRef<'_> {
pub(crate) fn is_empty(&self) -> bool {
self.text.is_empty()
}
fn span_boundaries(&self) -> SpanBoundaries {
let mut boundaries: SpanBoundaries = smallvec::smallvec![0, self.text.len()];
for span in self.span_styles {
boundaries.push(span.range.start);
boundaries.push(span.range.end);
}
boundaries.sort_unstable();
boundaries.dedup();
boundaries.retain(|at| *at <= self.text.len() && self.text.is_char_boundary(*at));
boundaries
}
}
impl<'a> From<&'a str> for StyledTextRef<'a> {
fn from(text: &'a str) -> Self {
Self {
text,
span_styles: &[],
}
}
}
impl<'a> From<&'a cranpose_ui::text::AnnotatedString> for StyledTextRef<'a> {
fn from(text: &'a cranpose_ui::text::AnnotatedString) -> Self {
Self {
text: text.text.as_str(),
span_styles: &text.span_styles,
}
}
}
impl<'a> From<&'a RenderString> for StyledTextRef<'a> {
fn from(text: &'a RenderString) -> Self {
Self {
text: text.text(),
span_styles: text.span_styles(),
}
}
}
pub(crate) fn visit_span_styles(
text: StyledTextRef<'_>,
style: &TextStyle,
mut visit: impl FnMut(Range<usize>, &SpanStyle),
) {
if text.span_styles.is_empty() || text.text.is_empty() {
visit(0..text.text.len(), &style.span_style);
return;
}
for edges in text.span_boundaries().windows(2) {
let range = edges[0]..edges[1];
let merged = text
.span_styles
.iter()
.filter(|span| span.range.start <= range.start && range.end <= span.range.end)
.fold(style.span_style.clone(), |merged, span| {
merged.merge(&span.item)
});
visit(range, &merged);
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct SpanPaint {
pub(crate) brush: Brush,
pub(crate) alpha: f32,
pub(crate) shadow: Option<Shadow>,
pub(crate) stroke: Option<f32>,
weight: FontWeight,
style: FontStyle,
synthesis: FontSynthesis,
}
impl SpanPaint {
pub(crate) fn of(style: &SpanStyle, fallback: Color) -> Self {
let (brush, alpha) = match &style.brush {
Some(brush) => (brush.clone(), style.alpha.unwrap_or(1.0).clamp(0.0, 1.0)),
None => (Brush::solid(style.resolve_foreground_color(fallback)), 1.0),
};
Self {
brush,
alpha,
shadow: style.shadow.filter(|shadow| shadow.color.3 > 0.0),
stroke: match style.draw_style {
Some(TextDrawStyle::Stroke { width }) if width.is_finite() && width > 0.0 => {
Some(width)
}
_ => None,
},
weight: style.font_weight.unwrap_or_default(),
style: style.font_style.unwrap_or_default(),
synthesis: style.font_synthesis.unwrap_or(FontSynthesis::All),
}
}
pub(crate) fn solid_color(&self) -> Option<Color> {
match self.brush {
Brush::Solid(color) => Some(Color(color.0, color.1, color.2, color.3 * self.alpha)),
_ => None,
}
}
pub(crate) fn raster(&self, typeface: &Typeface, size: f32, scale: f32) -> GlyphRaster {
let synthesis = typeface.synthesis(self.weight, self.style, self.synthesis);
GlyphRaster {
stroke: self.stroke.map(|width| width * scale),
embolden: if synthesis.bold {
size * fake_bold_ratio(size)
} else {
0.0
},
skew: if synthesis.italic {
FAKE_ITALIC_SKEW
} else {
0.0
},
}
}
}
pub(crate) const FAKE_ITALIC_SKEW: f32 = 0.25;
pub(crate) const FAKE_BOLD_MAX_RATIO: f32 = 1.0 / 24.0;
fn fake_bold_ratio(size: f32) -> f32 {
let along = ((size - 9.0) / 27.0).clamp(0.0, 1.0);
FAKE_BOLD_MAX_RATIO + along * (1.0 / 32.0 - FAKE_BOLD_MAX_RATIO)
}
pub(crate) struct SpanPaints {
paints: SmallVec<[(Range<usize>, SpanPaint); 2]>,
}
impl SpanPaints {
pub(crate) fn of(text: StyledTextRef<'_>, style: &TextStyle, fallback: Color) -> Self {
let mut paints: SmallVec<[(Range<usize>, SpanPaint); 2]> = SmallVec::new();
visit_span_styles(text, style, |range, merged| {
let paint = SpanPaint::of(merged, fallback);
match paints.last_mut() {
Some((last, last_paint)) if *last_paint == paint => last.end = range.end,
_ => paints.push((range, paint)),
}
});
Self { paints }
}
pub(crate) fn span_at(&self, offset: usize) -> (&Range<usize>, &SpanPaint) {
let at = self
.paints
.partition_point(|(range, _)| range.end <= offset)
.min(self.paints.len() - 1);
let (range, paint) = &self.paints[at];
(range, paint)
}
pub(crate) fn all(&self, test: impl Fn(&SpanPaint) -> bool) -> bool {
self.paints.iter().all(|(_, paint)| test(paint))
}
}
pub(crate) struct PlacedGlyph<'a> {
pub(crate) typeface: &'a Typeface,
pub(crate) glyph: GlyphId,
pub(crate) size: f32,
pub(crate) raster: GlyphRaster,
pub(crate) x: f32,
pub(crate) y: f32,
pub(crate) paint: &'a SpanPaint,
}
pub(crate) fn visit_placed_glyphs<'a>(
layout: &'a TextLayout,
paints: &'a SpanPaints,
(origin, scale): (Point, f32),
motion: TextMotion,
mut visit: impl FnMut(PlacedGlyph<'a>),
) {
let place = |value: f32, from: f32| match motion {
TextMotion::Static => whole::round(from + value * scale),
TextMotion::Animated => from + whole::round(value * scale),
};
for line in 0..layout.line_count() {
for run in layout.glyph_runs(line) {
let size = run.size * scale;
let mut span: Option<(&Range<usize>, &SpanPaint, GlyphRaster)> = None;
for glyph in run.positions() {
let (_, paint, raster) = match span {
Some(found) if found.0.contains(&glyph.offset) => found,
_ => {
let (range, paint) = paints.span_at(glyph.offset);
*span.insert((range, paint, paint.raster(run.typeface, size, scale)))
}
};
visit(PlacedGlyph {
typeface: run.typeface,
glyph: glyph.id,
size,
raster,
x: place(glyph.x, origin.x),
y: place(glyph.y, origin.y),
paint,
});
}
}
}
}