Add Canvas::set_clip_path — anti-aliased arbitrary-path clipping on both backends
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:
@@ -9,12 +9,14 @@
|
||||
//! [`crate::egl_context`] — this module is just the renderer that runs
|
||||
//! once a context is current.
|
||||
//!
|
||||
//! Clipping is implemented with `glScissor`. When
|
||||
//! Rect clipping is implemented with `glScissor`. When
|
||||
//! [`GlesCanvas::set_clip_rects`] receives multiple rects the
|
||||
//! bounding-box union is used as the scissor — coarse, but the
|
||||
//! partial-redraw path normally clusters the dirty rects of 1–3
|
||||
//! widgets so the union is barely larger than the sum. Disjoint
|
||||
//! regions would want a stencil-buffer path; not implemented today.
|
||||
//! widgets so the union is barely larger than the sum. Arbitrary
|
||||
//! path clipping ([`GlesCanvas::set_clip_path`]) captures the clipped
|
||||
//! draws into an offscreen layer and composites it back through an
|
||||
//! anti-aliased coverage mask, for a smooth clipped edge.
|
||||
//!
|
||||
//! # Submodule layout
|
||||
//!
|
||||
@@ -354,6 +356,23 @@ pub struct GlesCanvas
|
||||
/// (GL_SCISSOR_TEST is enabled), `None` when cleared.
|
||||
clip_scissor: Option<Rect>,
|
||||
|
||||
/// Anti-aliased path clip (`set_clip_path`). While `clip_layer_active`,
|
||||
/// `activate_target` redirects draws to `clip_layer` (a full-canvas
|
||||
/// offscreen FBO); ending the clip composites that layer back onto `fbo`
|
||||
/// multiplied by `clip_mask_tex` (an anti-aliased coverage texture covering
|
||||
/// `clip_bbox`). The layer FBO is allocated lazily on the first path clip.
|
||||
clip_layer: Option<( glow::Framebuffer, glow::Texture )>,
|
||||
clip_layer_active: bool,
|
||||
clip_mask_tex: Option<glow::Texture>,
|
||||
clip_bbox: Rect,
|
||||
/// Composite program (layer × coverage mask). Shared with sub-canvases.
|
||||
clip_composite_program: glow::Program,
|
||||
u_clip_mvp: glow::UniformLocation,
|
||||
u_clip_layer: glow::UniformLocation,
|
||||
u_clip_mask: glow::UniformLocation,
|
||||
u_clip_canvas: glow::UniformLocation,
|
||||
u_clip_bbox: glow::UniformLocation,
|
||||
|
||||
/// Persistent shadow framebuffer. All draw methods bind this;
|
||||
/// [`Self::present`](framebuffer) is the only call that switches
|
||||
/// to the default framebuffer.
|
||||
@@ -398,6 +417,15 @@ impl Drop for GlesCanvas
|
||||
{
|
||||
self.gl.delete_framebuffer( self.fbo );
|
||||
self.gl.delete_texture( self.fbo_tex );
|
||||
if let Some( ( fbo, tex ) ) = self.clip_layer.take()
|
||||
{
|
||||
self.gl.delete_framebuffer( fbo );
|
||||
self.gl.delete_texture( tex );
|
||||
}
|
||||
if let Some( tex ) = self.clip_mask_tex.take()
|
||||
{
|
||||
self.gl.delete_texture( tex );
|
||||
}
|
||||
if let Some( ( fbo, tex ) ) = self.aux_a.take()
|
||||
{
|
||||
self.gl.delete_framebuffer( fbo );
|
||||
|
||||
Reference in New Issue
Block a user