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:
@@ -19,8 +19,9 @@
|
||||
//!
|
||||
//! * [`setup`] — `SoftwareCanvas::{new, sub_canvas, resize, blit,
|
||||
//! set_font_registry, font_for}` (construction + accessors).
|
||||
//! * [`clip`] — `SoftwareCanvas::{set_clip_rects, clear_clip,
|
||||
//! has_clip, strip_intersects_clip, clear_rects_transparent}`.
|
||||
//! * [`clip`] — `SoftwareCanvas::{set_clip_rects, set_clip_path,
|
||||
//! clear_clip, has_clip, strip_intersects_clip,
|
||||
//! clear_rects_transparent}`.
|
||||
//! * [`primitives`] — `SoftwareCanvas::{clear, fill, fill_rect,
|
||||
//! stroke_rect, draw_line}`.
|
||||
//! * [`text`] — `SoftwareCanvas::{draw_text, measure_text}`.
|
||||
@@ -417,6 +418,22 @@ impl Canvas
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip subsequent draws to an arbitrary vector path (in surface
|
||||
/// coordinates). Anti-aliased per-path clipping: the software backend uses a
|
||||
/// tiny-skia coverage mask; the GLES backend captures the clipped draws into
|
||||
/// an offscreen layer and composites it back through an anti-aliased coverage
|
||||
/// mask. Replaces any active clip; restore the previous clip via
|
||||
/// [`Self::set_clip_rects`] with
|
||||
/// a [`Self::clip_bounds`] snapshot taken beforehand, or [`Self::clear_clip`].
|
||||
pub fn set_clip_path( &mut self, cmds: &[ crate::types::PathCmd ] )
|
||||
{
|
||||
match self
|
||||
{
|
||||
Canvas::Software( c ) => c.set_clip_path( cmds ),
|
||||
Canvas::Gles( c ) => c.set_clip_path( cmds ),
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the currently installed clip bounds (empty when no clip
|
||||
/// is active). Used by widgets that need to install a tighter clip
|
||||
/// for a single primitive and then restore whatever the outer
|
||||
@@ -749,3 +766,67 @@ mod viewport_tests
|
||||
assert_eq!( c.viewport_layout(), ( 720.0, 1440.0 ) );
|
||||
}
|
||||
}
|
||||
|
||||
#[ cfg( test ) ]
|
||||
mod clip_path_tests
|
||||
{
|
||||
// Exercised on the software backend (`Canvas::new`); the GLES layer-composite
|
||||
// path needs a live GL context and is covered by the `clip_path` example.
|
||||
use super::Canvas;
|
||||
use crate::types::{ Color, PathCmd, Rect };
|
||||
|
||||
fn square( x: f32, y: f32, side: f32 ) -> Vec<PathCmd>
|
||||
{
|
||||
vec![
|
||||
PathCmd::MoveTo( x, y ),
|
||||
PathCmd::LineTo( x + side, y ),
|
||||
PathCmd::LineTo( x + side, y + side ),
|
||||
PathCmd::LineTo( x, y + side ),
|
||||
PathCmd::Close,
|
||||
]
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn set_clip_path_bounds_match_the_path_bounding_box()
|
||||
{
|
||||
let mut c = Canvas::new( 200, 200 );
|
||||
c.set_clip_path( &square( 10.0, 20.0, 100.0 ) );
|
||||
let b = c.clip_bounds();
|
||||
assert_eq!( b.len(), 1 );
|
||||
let r = b[ 0 ];
|
||||
assert!( ( r.x - 10.0 ).abs() < 0.5 && ( r.y - 20.0 ).abs() < 0.5, "origin {r:?}" );
|
||||
assert!( ( r.width - 100.0 ).abs() < 0.5 && ( r.height - 100.0 ).abs() < 0.5, "size {r:?}" );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn set_clip_path_with_no_segments_clears_the_clip()
|
||||
{
|
||||
let mut c = Canvas::new( 100, 100 );
|
||||
c.set_clip_path( &[] );
|
||||
assert!( c.clip_bounds().is_empty() );
|
||||
}
|
||||
|
||||
#[ test ]
|
||||
fn set_clip_path_masks_a_fill_to_the_path_shape_not_its_bbox()
|
||||
{
|
||||
let mut c = Canvas::new( 100, 100 );
|
||||
// Downward triangle: apex top-centre, base along the bottom.
|
||||
let tri = vec![
|
||||
PathCmd::MoveTo( 50.0, 5.0 ),
|
||||
PathCmd::LineTo( 95.0, 95.0 ),
|
||||
PathCmd::LineTo( 5.0, 95.0 ),
|
||||
PathCmd::Close,
|
||||
];
|
||||
c.set_clip_path( &tri );
|
||||
c.fill_rect( Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }, Color::rgba( 1.0, 0.0, 0.0, 1.0 ), 0.0 );
|
||||
|
||||
let Canvas::Software( sc ) = &c else { panic!( "Canvas::new builds a software canvas" ) };
|
||||
// A point well inside the triangle is painted; a top corner — inside the
|
||||
// bounding box but outside the triangle — stays clear, proving the clip
|
||||
// follows the path silhouette rather than its bounding rect.
|
||||
let inside = sc.pixmap.pixel( 50, 70 ).expect( "in-bounds pixel" );
|
||||
let corner = sc.pixmap.pixel( 8, 8 ).expect( "in-bounds pixel" );
|
||||
assert!( inside.red() > 200 && inside.alpha() > 200, "inside triangle must be filled" );
|
||||
assert_eq!( corner.alpha(), 0, "bbox corner outside the triangle must stay clear" );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user