Add Canvas::set_clip_path — anti-aliased arbitrary-path clipping on both backends
Some checks failed
CI / build + test (push) Has been cancelled
CI / cargo audit (push) Has been cancelled

Add `Canvas::set_clip_path(&[PathCmd])`, clipping subsequent draws to an arbitrary vector path with an anti-aliased edge, on both the software and GLES backends. It complements the existing rect clip (`set_clip_rects`) and is what an embedder needs to render a shaped clip — a circular avatar, a rounded card, a `VectorDrawable` mask — rather than a bounding box. Kept general rather than tied to any one consumer.
Software backend: rasterise the path into an anti-aliased tiny-skia coverage `Mask` (Winding fill) and install it as the active clip mask. Every software primitive already threads `clip_mask` through tiny-skia (fills, strokes, lines, paths, images, text, blit), so the path clip applies uniformly with smooth edges. `clip_bounds` reports the path's bounding box while it is active.
GLES backend: a 1-bit stencil would clip exactly but leave a hard, aliased edge, so instead the clipped draws are captured into an offscreen layer and composited back through an anti-aliased coverage mask. `set_clip_path` rasterises the path coverage (tiny-skia, anti-aliased), uploads it as a mask texture, allocates a full-canvas layer FBO on first use, and redirects subsequent draws to it via `activate_target`. Ending the clip (`clear_clip` / `set_clip_rects` / a new `set_clip_path`) composites the layer back onto the canvas FBO with a new two-sampler program (`CLIP_COMPOSITE_FRAG_SRC`) that multiplies the layer colour by the mask coverage and blends it premultiplied-over. The layer attaches to the canvas's own shadow FBO, so it needs no stencil bits in the EGL config; it is freed and reallocated on resize and freed on drop, and shared programs/uniforms are copied to sub-canvases like the rest.
Usage: a path clip is bracketed — `set_clip_path` then, after the clipped draws, `clear_clip` or `set_clip_rects` to flush it (on GLES this is when the layer is composited). Snapshot the prior clip with `clip_bounds` beforehand and restore it with `set_clip_rects` to compose with an outer clip without leaking state.
Add an `examples/clip_path.rs` demo (rounded rect, circle, triangle — same smooth result on both backends) and software-backend unit tests covering the bounding box, the empty-path clear, and a pixel-level check that a triangular clip masks a fill to the path silhouette rather than its bounding box. The GLES layer-composite path needs a live GL context and is exercised by the example.
Also fix three rustdoc intra-doc-link warnings surfaced along the way: a private-item link in `app.rs` (`scroll`) and the new GLES doc (`SoftwareCanvas::set_clip_path`) demoted to code spans, and a redundant explicit link target in `chassis.rs`.
This commit is contained in:
2026-06-18 23:59:38 +02:00
parent b00cf460bb
commit f8c45f0e30
10 changed files with 482 additions and 19 deletions

View File

@@ -3,7 +3,9 @@
//! Clip-mask management for [`SoftwareCanvas`]. The partial-redraw
//! path calls `set_clip_rects` before every repaint so only pixels
//! inside the dirty rects are touched.
//! inside the dirty rects are touched. `set_clip_path` installs an
//! arbitrary anti-aliased vector path as the clip (an exact tiny-skia
//! coverage [`Mask`]) for shaped clipping such as a circular avatar.
use tiny_skia::{ FillRule, Mask, PathBuilder, Transform };
@@ -46,6 +48,37 @@ impl SoftwareCanvas
}
}
/// Clip subsequent paints to an arbitrary vector path (surface
/// coordinates), via an anti-aliased tiny-skia coverage mask. The GLES
/// counterpart composites an offscreen layer through an equivalent
/// anti-aliased mask; both give a smooth clipped edge.
pub fn set_clip_path( &mut self, cmds: &[ crate::types::PathCmd ] )
{
let w = self.pixmap.width();
let h = self.pixmap.height();
let Some( path ) = crate::render::helpers::build_ts_path( cmds ) else
{
self.clear_clip();
return;
};
let Some( mut mask ) = Mask::new( w, h ) else
{
self.clip_mask = None;
self.clip_bounds = Vec::new();
return;
};
mask.fill_path( &path, FillRule::Winding, true, Transform::identity() );
let b = path.bounds();
self.clip_mask = Some( mask );
self.clip_bounds = vec![ Rect
{
x: b.left(),
y: b.top(),
width: b.right() - b.left(),
height: b.bottom() - b.top(),
} ];
}
/// Remove the active clip so subsequent paints cover the full canvas.
pub fn clear_clip( &mut self )
{