use std::sync::Arc;
use valo_dl::{ClipOp, Image, Paint, Sampling};
use valo_geometry::{
dash_contours, local_tolerance, stroke_strip, FillRule, Matrix, Path, PathBuilder, Rect, Stroke,
};
use crate::frame::Mesh;
use crate::pipelines::{AdvancedBlend, PipelineBlend};
use super::draw_state::DrawState;
use super::drawing::Drawing;
use super::emit::{Cover, Entity, Marking, Role, StencilWrite};
use super::shading::Shading;
use super::source::Shape;
pub(super) enum BlendedSolid {
Rect(Rect),
Path {
bounds: Rect,
fan: Mesh,
rule: FillRule,
},
}
impl Drawing<'_, '_> {
pub fn draw_shape(
&mut self,
shape: &Shape<'_>,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
match *shape {
Shape::Rect(rect) => self.draw_rect(&rect, paint, at, blend),
Shape::Path { path, rule } => self.fill_path(path, rule, paint, at, blend),
Shape::Stroke { path, stroke } => self.draw_stroke(path, stroke, paint, at, blend),
}
}
fn draw_rect(&mut self, rect: &Rect, paint: &Paint, at: &DrawState, blend: PipelineBlend) {
let shading = self.emit.paint_shading(paint, at.alpha);
let entity = Entity {
stencil: None,
cover: Cover::Quad {
transform: at.transform,
rect: *rect,
},
role: Role::Fill,
shading,
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
fn fill_path(
&mut self,
path: &Arc<Path>,
rule: FillRule,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
let Some(fan) = self.fan_mesh(path, &at.transform) else {
return;
};
let shading = self.emit.paint_shading(paint, at.alpha);
let entity = Entity {
stencil: Some(StencilWrite {
transform: at.transform,
mesh: fan,
marking: Marking::Fan(rule),
}),
cover: Cover::Quad {
transform: at.transform,
rect: path.bounds(),
},
role: Role::StencilledFill,
shading,
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
fn draw_stroke(
&mut self,
path: &Arc<Path>,
stroke: &Stroke,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
let vertices = self.stroke_vertices(path, stroke, &at.transform);
if vertices.is_empty() {
return;
}
let coverage = stroke_alpha_coverage(&at.transform, stroke.width);
let shading = self.emit.paint_shading(paint, at.alpha * coverage);
let entity = Entity {
stencil: None,
cover: Cover::Mesh {
transform: at.transform,
mesh: self.emit.alloc_mesh(&vertices),
},
role: Role::Stroke,
shading,
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
fn stroke_vertices(&mut self, path: &Arc<Path>, stroke: &Stroke, current: &Matrix) -> Vec<f32> {
let tolerance = local_tolerance(current);
let contours = self.caches.contours.contours(path, tolerance);
let mut stroke = stroke.clone();
stroke.width = stroke.width.max(1.0 / current.max_scale().max(1e-3));
match &stroke.dash {
Some(dash) => {
let dashed = dash_contours(&contours, dash);
stroke_strip(&dashed, &stroke, tolerance)
}
None => stroke_strip(&contours, &stroke, tolerance),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the DrawImage op's fields, the state and the blend"
)]
pub fn draw_image(
&mut self,
image: &Image,
src: &Rect,
dst: &Rect,
sampling: Sampling,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
let alpha = paint.color.a * at.alpha;
let shading = self
.emit
.image_shading(image, src, dst, sampling, paint.color_filter, alpha);
let entity = Entity {
stencil: None,
cover: Cover::Quad {
transform: at.transform,
rect: *dst,
},
role: Role::Fill,
shading,
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
pub fn draw_blended_solid(
&mut self,
solid: BlendedSolid,
paint: &Paint,
at: &DrawState,
mode: AdvancedBlend,
destination: &wgpu::TextureView,
) {
let size = self.context.area.size();
let shading =
self.emit
.blended_solid_shading(paint.color, at.alpha, mode, destination, size);
let (stencil, rect, role) = match solid {
BlendedSolid::Rect(rect) => (None, rect, Role::Fill),
BlendedSolid::Path { bounds, fan, rule } => {
let fan = StencilWrite {
transform: at.transform,
mesh: fan,
marking: Marking::Fan(rule),
};
(Some(fan), bounds, Role::StencilledFill)
}
};
let entity = Entity {
stencil,
cover: Cover::Quad {
transform: at.transform,
rect,
},
role,
shading,
blend: PipelineBlend::SrcOver,
z: at.z,
};
self.emit.push(self.context, entity);
}
pub fn draw_rrect_blur(
&mut self,
rect: &Rect,
radii: [f32; 4],
blur: valo_dl::MaskBlur,
paint: &Paint,
at: &DrawState,
blend: PipelineBlend,
) {
let shading = Shading::rrect_blur(rect, radii, paint.color, blur, at.alpha);
let entity = Entity {
stencil: None,
cover: Cover::Quad {
transform: at.transform,
rect: rect.expand((blur.sigma * 3.0).ceil()),
},
role: Role::Fill,
shading,
blend,
z: at.z,
};
self.emit.push(self.context, entity);
}
pub fn clip(&mut self, path: &Arc<Path>, rule: FillRule, op: ClipOp, at: &DrawState) {
if self.excludes_nothing_visible(&path.bounds(), op, &at.transform) {
self.stats.culled += 1;
return;
}
let stencil = self.fan_mesh(path, &at.transform).map(|mesh| StencilWrite {
transform: at.transform,
mesh,
marking: Marking::Fan(rule),
});
self.push_stencil_clip(stencil, &path.bounds(), op, at);
}
pub fn clip_to_shape(&mut self, shape: &Shape<'_>, op: ClipOp, at: &DrawState) {
let expiry = DrawState {
z: self.context.depth.half_slot_above(at.z),
..*at
};
match *shape {
Shape::Rect(rect) => self.clip(&rect_path(&rect), FillRule::NonZero, op, &expiry),
Shape::Path { path, rule } => self.clip(path, rule, op, &expiry),
Shape::Stroke { path, stroke } => self.clip_stroke(path, stroke, op, &expiry),
}
}
fn clip_stroke(&mut self, path: &Arc<Path>, stroke: &Stroke, op: ClipOp, at: &DrawState) {
let vertices = self.stroke_vertices(path, stroke, &at.transform);
let bounds = vertex_bounds(&vertices);
if self.excludes_nothing_visible(&bounds, op, &at.transform) {
self.stats.culled += 1;
return;
}
let stencil = (!vertices.is_empty()).then(|| StencilWrite {
transform: at.transform,
mesh: self.emit.alloc_mesh(&vertices),
marking: Marking::Strip,
});
self.push_stencil_clip(stencil, &bounds, op, at);
}
fn excludes_nothing_visible(&self, bounds: &Rect, op: ClipOp, current: &Matrix) -> bool {
op == ClipOp::Difference && self.context.area.culls(¤t.map_rect(bounds))
}
fn push_stencil_clip(
&mut self,
stencil: Option<StencilWrite>,
bounds: &Rect,
op: ClipOp,
at: &DrawState,
) {
if stencil.is_none() && op == ClipOp::Difference {
return;
}
let (cover, role) = match op {
ClipOp::Intersect => (Cover::Texels, Role::Clip { difference: false }),
ClipOp::Difference => (
Cover::Quad {
transform: at.transform,
rect: *bounds,
},
Role::Clip { difference: true },
),
};
self.stats.clips += 1;
let entity = Entity {
stencil,
cover,
role,
shading: Shading::none(),
blend: PipelineBlend::SrcOver,
z: at.z,
};
self.emit.push_clip(self.context, entity);
}
pub fn fan_mesh(&mut self, path: &Arc<Path>, current: &Matrix) -> Option<Mesh> {
let contours = self
.caches
.contours
.contours(path, local_tolerance(current));
let vertices = fan_vertices(&contours);
if vertices.is_empty() {
return None;
}
Some(self.emit.alloc_mesh(&vertices))
}
}
fn rect_path(rect: &Rect) -> Arc<Path> {
let mut builder = PathBuilder::new();
builder.rect(*rect);
builder.build()
}
fn vertex_bounds(vertices: &[f32]) -> Rect {
let mut points = vertices.chunks_exact(2);
let Some(first) = points.next() else {
return Rect::default();
};
let [mut left, mut top] = [first[0], first[1]];
let [mut right, mut bottom] = [left, top];
for point in points {
left = left.min(point[0]);
top = top.min(point[1]);
right = right.max(point[0]);
bottom = bottom.max(point[1]);
}
Rect::from_ltrb(left, top, right, bottom)
}
fn fan_vertices(contours: &[valo_geometry::Contour]) -> Vec<f32> {
let triangles: usize = contours
.iter()
.map(|c| c.points.len().saturating_sub(2))
.sum();
let mut out = Vec::with_capacity(triangles * 6);
for contour in contours {
let contour = &contour.points;
let p0 = contour[0];
for pair in contour[1..].windows(2) {
out.extend_from_slice(&[p0.x, p0.y, pair[0].x, pair[0].y, pair[1].x, pair[1].y]);
}
}
out
}
fn stroke_alpha_coverage(transform: &Matrix, width: f32) -> f32 {
subpixel_stroke_alpha(transform.max_scale() * width)
}
pub(super) fn subpixel_stroke_alpha(device_width: f32) -> f32 {
if device_width == 0.0 || device_width >= 1.0 {
1.0
} else {
(device_width * 2.0).clamp(0.0, 1.0)
}
}
#[cfg(test)]
mod tests {
use super::stroke_alpha_coverage;
use valo_geometry::Matrix;
#[test]
fn hairline_coverage_matches_impeller() {
assert_eq!(stroke_alpha_coverage(&Matrix::IDENTITY, 0.0), 1.0);
assert_eq!(stroke_alpha_coverage(&Matrix::IDENTITY, 0.25), 0.5);
assert_eq!(stroke_alpha_coverage(&Matrix::IDENTITY, 0.5), 1.0);
assert_eq!(stroke_alpha_coverage(&Matrix::scale(2.0, 2.0), 0.25), 1.0);
}
}