use std::sync::Arc;
use valo_dl::{GlyphPos, Paint, PaintStyle};
use valo_geometry::{FillRule, Matrix, MatrixKind, Point, Stroke};
use valo_text::{Font, GlyphStroke};
use crate::glyphs::{AtlasGlyph, Coverage, PageRef, TextTiers, SDF_BUCKETS};
use crate::pipelines::TextMode;
use super::emit::{alpha_tint, scaled_premul};
use super::primitives::subpixel_stroke_alpha;
use super::Planner;
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 Planner<'_> {
pub(super) fn plan_glyph_tiers(
&mut self,
font: &Arc<Font>,
size: f32,
paint: &Paint,
glyphs: &[GlyphPos],
current: &Matrix,
z: f32,
) {
let device_px = size * current.max_scale();
let scale = quantize_scale(current.max_scale());
match glyph_tier(self.glyphs.tiers, paint, scale, device_px) {
GlyphTier::Outline => {
self.stats.text_tiers[2] += 1;
self.plan_glyph_outlines(font, size, paint, glyphs, current, z);
}
GlyphTier::Sdf => {
self.stats.text_tiers[1] += 1;
self.plan_glyph_quads(
font,
sdf_bucket(device_px),
Coverage::Sdf,
size,
paint,
glyphs,
current,
z,
);
}
GlyphTier::Mask { coverage, alpha } => {
self.stats.text_tiers[0] += 1;
let paint = Paint {
color: paint.color.with_alpha(paint.color.a * alpha),
..paint.clone()
};
self.plan_glyph_masks(font, size, scale, coverage, &paint, glyphs, current, z);
}
}
}
#[expect(clippy::too_many_arguments, reason = "mirrors plan_glyph_tiers")]
fn plan_glyph_masks(
&mut self,
font: &Arc<Font>,
size: f32,
scale: f32,
coverage: Coverage,
paint: &Paint,
glyphs: &[GlyphPos],
current: &Matrix,
z: f32,
) {
if is_uniform_axis_aligned(current) {
self.plan_glyph_quads_snapped(font, scale, size, coverage, paint, glyphs, current, z);
} else {
self.plan_glyph_quads(
font,
size * scale,
coverage,
size,
paint,
glyphs,
current,
z,
);
}
}
#[expect(clippy::too_many_arguments, reason = "mirrors the GlyphRun op + tier")]
fn plan_glyph_quads(
&mut self,
font: &Arc<Font>,
px: f32,
coverage: Coverage,
size: f32,
paint: &Paint,
glyphs: &[GlyphPos],
current: &Matrix,
z: f32,
) {
let hide_notdef = self.glyphs.hides_missing_glyphs();
let keys: Vec<(u32, u8)> = glyphs
.iter()
.filter(|g| g.id != 0 || !hide_notdef)
.map(|g| (g.id, 0))
.collect();
self.glyphs.ensure_run(font, px, coverage, &keys);
let mut batches: Vec<((TextMode, PageRef), Vec<f32>)> = Vec::new();
for g in glyphs.iter().filter(|g| g.id != 0 || !hide_notdef) {
let (got_px, page, entry) = match self.glyphs.entry(font.uid().0, g.id, px, coverage, 0)
{
Some((page, entry)) => (px, page, entry),
None => match self
.glyphs
.resident_stand_in(font.uid().0, g.id, coverage, px)
{
Some(hit) => hit,
None => continue,
},
};
let batch = batch_for(&mut batches, text_mode(page, coverage), page);
push_glyph_quad(batch, g.x, g.y, &entry, size / got_px);
}
self.push_text_batches(batches, paint, current, z);
}
#[expect(clippy::too_many_arguments, reason = "mirrors the GlyphRun op + tier")]
fn plan_glyph_quads_snapped(
&mut self,
font: &Arc<Font>,
scale: f32,
size: f32,
coverage: Coverage,
paint: &Paint,
glyphs: &[GlyphPos],
current: &Matrix,
z: f32,
) {
let px = size * scale;
let hide_notdef = self.glyphs.hides_missing_glyphs();
let placed: Vec<(f32, f32, u8, u32)> = glyphs
.iter()
.filter(|g| g.id != 0 || !hide_notdef)
.map(|g| {
let device = current.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.glyphs.ensure_run(font, px, coverage, &keys);
let mut batches: Vec<((TextMode, PageRef), Vec<f32>)> = Vec::new();
for (x, y, phase, id) in placed {
let (scale, page, entry) =
match self.glyphs.entry(font.uid().0, id, px, coverage, phase) {
Some((page, entry)) => (1.0, page, entry),
None => match self
.glyphs
.resident_stand_in(font.uid().0, id, coverage, px)
{
Some((got_px, page, entry)) => (px / got_px, page, entry),
None => continue,
},
};
let batch = batch_for(&mut batches, text_mode(page, coverage), page);
push_glyph_quad(batch, x, y, &entry, scale);
}
self.push_text_batches(batches, paint, &Matrix::IDENTITY, z);
}
fn push_text_batches(
&mut self,
batches: Vec<((TextMode, PageRef), Vec<f32>)>,
paint: &Paint,
model: &Matrix,
z: f32,
) {
for ((mode, page), vertices) in batches {
let tint = text_tint(mode, paint, self.elision_alpha());
let mesh = self.emit.alloc_text_mesh(&vertices);
let bind = self.emit.atlas_bind(self.glyphs, page);
let frame = self.frames.last_mut().expect("frame stack never empty");
self.emit
.text_step(frame, mode, tint, paint.blend_mode, model, mesh, bind, z);
}
}
fn plan_glyph_outlines(
&mut self,
font: &Arc<Font>,
size: f32,
paint: &Paint,
glyphs: &[GlyphPos],
current: &Matrix,
z: f32,
) {
let paint = Paint {
color: paint.color,
blend_mode: paint.blend_mode,
style: paint.style.clone(),
..Default::default()
};
let hide_notdef = self.glyphs.hides_missing_glyphs();
let mut no_outline: Vec<GlyphPos> = Vec::new();
for g in glyphs.iter().filter(|g| g.id != 0 || !hide_notdef) {
let Some(path) = self.glyphs.path(font, g.id, size) else {
no_outline.push(*g);
continue;
};
let at = current.then(&Matrix::translation(g.x, g.y));
self.emit_path(&path, FillRule::NonZero, &paint, &at, z);
}
if !no_outline.is_empty() {
let px = (size * current.max_scale()).min(MAX_COLOR_GLYPH_PX);
let bitmap_paint = Paint {
style: PaintStyle::Fill,
..paint.clone()
};
self.plan_glyph_quads(
font,
px,
Coverage::Fill,
size,
&bitmap_paint,
&no_outline,
current,
z,
);
}
}
}
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 text_mode(page: PageRef, coverage: Coverage) -> TextMode {
match (page.color, coverage) {
(true, _) => TextMode::Color,
(false, Coverage::Sdf) => TextMode::Sdf,
(false, _) => TextMode::Mask,
}
}
fn text_tint(mode: TextMode, paint: &Paint, group_alpha: f32) -> [f32; 4] {
match mode {
TextMode::Color => alpha_tint(paint.color.a * group_alpha),
_ => scaled_premul(paint.color, group_alpha),
}
}
fn batch_for(
batches: &mut Vec<((TextMode, PageRef), Vec<f32>)>,
mode: TextMode,
page: PageRef,
) -> &mut Vec<f32> {
let key = (mode, 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)));
}
}