use std::sync::Arc;
use valo_dl::{GlyphPos, Paint, PaintStyle};
use valo_geometry::{Color, FillRule, Matrix, MatrixKind, Point, Stroke};
use valo_text::{Font, FontSynthesis, GlyphStroke};
use crate::glyphs::{AtlasGlyph, Coverage, PageRef, TextTiers, SDF_BUCKETS};
use crate::pipelines::{Frag, PipelineBlend};
use super::draw_state::DrawState;
use super::drawing::Drawing;
use super::emit::{Cover, Entity, Role};
use super::primitives::subpixel_stroke_alpha;
use super::shading::{scaled_premul, Shading};
use super::source::{GlyphRun, Shape};
const MAX_COLOR_GLYPH_PX: f32 = 256.0;
const MAX_STROKED_MASK_PX: f32 = 1024.0;
enum GlyphTier {
Mask { coverage: Coverage, alpha: f32 },
Sdf,
Outline,
}
impl GlyphTier {
fn stats_index(&self) -> usize {
match self {
GlyphTier::Mask { .. } => 0,
GlyphTier::Sdf => 1,
GlyphTier::Outline => 2,
}
}
}
struct Strike<'f> {
font: &'f Arc<Font>,
synthesis: FontSynthesis,
px: f32,
coverage: Coverage,
}
impl Drawing<'_, '_> {
pub fn draw_glyphs(
&mut self,
run: &GlyphRun<'_>,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
let (font, synthesis, size) = (run.font, run.synthesis, run.size);
let glyphs = run.glyphs.as_slice();
let paint = &Paint {
color_filter: run.colour_filter,
..paint.clone()
};
let device_px = size * at.transform.max_scale();
let scale = quantize_scale(at.transform.max_scale());
let tier = glyph_tier(self.caches.glyphs.tiers, paint, scale, device_px);
self.stats.text_tiers[tier.stats_index()] += 1;
match tier {
GlyphTier::Outline => {
let strike = Strike {
font,
synthesis,
px: size,
coverage: Coverage::Fill,
};
self.draw_glyph_outlines(&strike, paint, glyphs, at, blend)
}
GlyphTier::Sdf => {
let strike = Strike {
font,
synthesis,
px: sdf_bucket(device_px),
coverage: Coverage::Sdf,
};
self.draw_glyph_quads(&strike, size, paint, glyphs, at, blend);
}
GlyphTier::Mask { coverage, alpha } => {
let paint = Paint {
color: paint.color.with_alpha(paint.color.a * alpha),
..paint.clone()
};
let strike = Strike {
font,
synthesis,
px: size * scale,
coverage,
};
self.draw_glyph_masks(&strike, size, &paint, glyphs, at, blend);
}
}
}
fn draw_glyph_masks(
&mut self,
strike: &Strike,
size: f32,
paint: &Paint,
glyphs: &[GlyphPos],
at: &DrawState,
blend: PipelineBlend,
) {
if is_uniform_axis_aligned(&at.transform) {
self.draw_glyph_quads_snapped(strike, paint, glyphs, at, blend);
} else {
self.draw_glyph_quads(strike, size, paint, glyphs, at, blend);
}
}
fn draw_glyph_quads(
&mut self,
strike: &Strike,
size: f32,
paint: &Paint,
glyphs: &[GlyphPos],
at: &DrawState,
blend: PipelineBlend,
) {
let hide_notdef = self.caches.glyphs.hides_missing_glyphs();
let shown: Vec<&GlyphPos> = shown_glyphs(glyphs, hide_notdef).collect();
let keys: Vec<(u32, u8)> = shown.iter().map(|g| (g.id, 0)).collect();
self.ensure_strike(strike, &keys);
let mut batches: Vec<((Frag, PageRef), Vec<f32>)> = Vec::new();
for g in shown {
let Some((raster_px, page, entry)) = self.atlas_glyph(strike, g.id, 0) else {
continue;
};
let batch = batch_for(&mut batches, glyph_frag(page, strike.coverage), page);
push_glyph_quad(batch, g.x, g.y, &entry, size / raster_px);
}
self.push_text_batches(batches, paint, at, blend);
}
fn draw_glyph_quads_snapped(
&mut self,
strike: &Strike,
paint: &Paint,
glyphs: &[GlyphPos],
at: &DrawState,
blend: PipelineBlend,
) {
let hide_notdef = self.caches.glyphs.hides_missing_glyphs();
let placed: Vec<(f32, f32, u8, u32)> = shown_glyphs(glyphs, hide_notdef)
.map(|g| {
let device = at.transform.map_point(Point::new(g.x, g.y));
let (x, phase) = snap_quarter(device.x);
(x, device.y.round(), phase, g.id)
})
.collect();
let keys: Vec<(u32, u8)> = placed
.iter()
.map(|&(_, _, phase, id)| (id, phase))
.collect();
self.ensure_strike(strike, &keys);
let mut batches: Vec<((Frag, PageRef), Vec<f32>)> = Vec::new();
for (x, y, phase, id) in placed {
let Some((raster_px, page, entry)) = self.atlas_glyph(strike, id, phase) else {
continue;
};
let batch = batch_for(&mut batches, glyph_frag(page, strike.coverage), page);
push_glyph_quad(batch, x, y, &entry, strike.px / raster_px);
}
self.push_text_batches(batches, paint, &at.with_transform(Matrix::IDENTITY), blend);
}
fn ensure_strike(&mut self, strike: &Strike, keys: &[(u32, u8)]) {
self.caches.glyphs.ensure_run(
strike.font,
strike.synthesis,
strike.px,
strike.coverage,
keys,
);
}
fn atlas_glyph(
&mut self,
strike: &Strike,
id: u32,
phase: u8,
) -> Option<(f32, PageRef, AtlasGlyph)> {
let (font, synthesis) = (strike.font.uid().0, strike.synthesis);
let glyphs = &mut self.caches.glyphs;
match glyphs.entry(font, synthesis, id, strike.px, strike.coverage, phase) {
Some((page, entry)) => Some((strike.px, page, entry)),
None => glyphs.resident_stand_in(font, synthesis, id, strike.coverage, strike.px),
}
}
fn push_text_batches(
&mut self,
batches: Vec<((Frag, PageRef), Vec<f32>)>,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
for ((frag, page), vertices) in batches {
let page = self.emit.atlas_bind(&mut self.caches.glyphs, page);
let entity = Entity {
stencil: None,
cover: Cover::Mesh {
transform: at.transform,
mesh: self.emit.alloc_glyph_mesh(&vertices),
},
role: Role::Glyphs,
shading: glyph_shading(frag, paint, at.alpha, page),
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
}
fn draw_glyph_outlines(
&mut self,
strike: &Strike,
paint: &Paint,
glyphs: &[GlyphPos],
at: &DrawState,
blend: PipelineBlend,
) {
let outline_paint = Paint {
color: coverage_colour(paint),
style: paint.style.clone(),
..Default::default()
};
let (font, size) = (strike.font, strike.px);
let hide_notdef = self.caches.glyphs.hides_missing_glyphs();
let mut no_outline: Vec<GlyphPos> = Vec::new();
for g in shown_glyphs(glyphs, hide_notdef) {
let Some(path) = self.caches.glyphs.path(font, strike.synthesis, g.id, size) else {
no_outline.push(*g);
continue;
};
let glyph = at.with_transform(at.transform.then(&Matrix::translation(g.x, g.y)));
let shape = Shape::of_path(&path, FillRule::NonZero, &outline_paint.style);
self.draw_shape(&shape, &outline_paint, &glyph, blend);
}
if !no_outline.is_empty() {
let px = (size * at.transform.max_scale()).min(MAX_COLOR_GLYPH_PX);
let bitmap_paint = Paint {
style: PaintStyle::Fill,
..paint.clone()
};
let strike = Strike { px, ..*strike };
self.draw_glyph_quads(&strike, size, &bitmap_paint, &no_outline, at, blend);
}
}
}
fn shown_glyphs(glyphs: &[GlyphPos], hide_notdef: bool) -> impl Iterator<Item = &GlyphPos> {
glyphs.iter().filter(move |g| g.id != 0 || !hide_notdef)
}
fn glyph_tier(tiers: TextTiers, paint: &Paint, scale: f32, device_px: f32) -> GlyphTier {
if device_px >= tiers.path_min {
return GlyphTier::Outline;
}
let stroke = match &paint.style {
PaintStyle::Fill if device_px >= tiers.sdf_min => return GlyphTier::Sdf,
PaintStyle::Fill => {
return GlyphTier::Mask {
coverage: Coverage::Fill,
alpha: 1.0,
}
}
PaintStyle::Stroke(stroke) => stroke,
};
match atlas_stroke(stroke, scale, device_px) {
Some((stroke, alpha)) => GlyphTier::Mask {
coverage: Coverage::Stroke(stroke),
alpha,
},
None => GlyphTier::Outline,
}
}
fn atlas_stroke(stroke: &Stroke, scale: f32, device_px: f32) -> Option<(GlyphStroke, f32)> {
if stroke.dash.is_some() {
return None;
}
let device_width = stroke.width * scale;
let width = device_width.max(1.0);
let reach = width * 0.5 * stroke.miter_limit.max(1.0);
if 2.0 * (device_px + reach) > MAX_STROKED_MASK_PX {
return None;
}
Some((
GlyphStroke {
width,
cap: stroke.cap,
join: stroke.join,
miter_limit: stroke.miter_limit,
},
subpixel_stroke_alpha(device_width),
))
}
fn sdf_bucket(device_px: f32) -> f32 {
for bucket in SDF_BUCKETS {
if device_px <= bucket {
return bucket;
}
}
SDF_BUCKETS[SDF_BUCKETS.len() - 1]
}
fn quantize_scale(scale: f32) -> f32 {
((scale * 200.0).round() / 200.0).clamp(1.0 / 200.0, 48.0)
}
fn snap_quarter(x: f32) -> (f32, u8) {
let quarters = (x * 4.0).round();
let base = (quarters * 0.25).floor();
let phase = (quarters - base * 4.0) as u8 % 4;
(base, phase)
}
fn is_axis_aligned(transform: &Matrix) -> bool {
transform.kind() == MatrixKind::AxisAligned
}
fn is_uniform_axis_aligned(transform: &Matrix) -> bool {
if !is_axis_aligned(transform) {
return false;
}
let [scale_x, _, _, scale_y, ..] = transform.to_affine();
(scale_x - scale_y).abs() <= 1e-6 * scale_x.max(scale_y).max(1.0)
}
fn glyph_frag(page: PageRef, coverage: Coverage) -> Frag {
match (page.color, coverage) {
(true, _) => Frag::GlyphColor,
(false, Coverage::Sdf) => Frag::GlyphSdf,
(false, _) => Frag::GlyphMask,
}
}
fn glyph_shading(frag: Frag, paint: &Paint, group_alpha: f32, page: wgpu::BindGroup) -> Shading {
match frag {
Frag::GlyphColor => {
Shading::colour_glyphs(page, paint.color_filter, paint.color.a, group_alpha)
}
_ => Shading::glyphs(
frag,
scaled_premul(coverage_colour(paint), group_alpha),
page,
),
}
}
fn coverage_colour(paint: &Paint) -> Color {
paint
.color_filter
.map_or(paint.color, |filter| filter.folded_into(paint.color))
}
fn batch_for(
batches: &mut Vec<((Frag, PageRef), Vec<f32>)>,
frag: Frag,
page: PageRef,
) -> &mut Vec<f32> {
let key = (frag, page);
if let Some(at) = batches.iter().position(|(k, _)| *k == key) {
return &mut batches[at].1;
}
batches.push((key, Vec::new()));
&mut batches.last_mut().expect("just pushed").1
}
fn push_glyph_quad(out: &mut Vec<f32>, gx: f32, gy: f32, entry: &AtlasGlyph, scale: f32) {
let x0 = gx + entry.left * scale;
let y0 = gy - entry.top * scale;
let x1 = x0 + entry.width * scale;
let y1 = y0 + entry.height * scale;
let [u0, v0, u1, v1] = entry.uv;
let quad = [
[x0, y0, u0, v0],
[x1, y0, u1, v0],
[x0, y1, u0, v1],
[x1, y0, u1, v0],
[x1, y1, u1, v1],
[x0, y1, u0, v1],
];
for vertex in quad {
out.extend_from_slice(&vertex);
}
}
#[cfg(test)]
mod tests {
use super::is_uniform_axis_aligned;
use valo_geometry::Matrix;
#[test]
fn snapped_text_requires_uniform_scale() {
assert!(is_uniform_axis_aligned(&Matrix::scale(0.5, 0.5)));
assert!(!is_uniform_axis_aligned(&Matrix::scale(0.5, 1.0)));
assert!(!is_uniform_axis_aligned(&Matrix::rotation(0.1)));
}
}