ltk/render/
clip.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! 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. `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 };

use crate::types::Rect;

use super::SoftwareCanvas;

impl SoftwareCanvas
{
	/// Set the active clip region to the union of `rects` (physical pixels).
	pub fn set_clip_rects( &mut self, rects: &[Rect] )
	{
		let w = self.pixmap.width();
		let h = self.pixmap.height();
		let Some( mut mask ) = Mask::new( w, h ) else
		{
			self.clip_mask   = None;
			self.clip_bounds = Vec::new();
			return;
		};
		let mut pb = PathBuilder::new();
		for r in rects
		{
			let x0 = r.x.max( 0.0 ).min( w as f32 );
			let y0 = r.y.max( 0.0 ).min( h as f32 );
			let x1 = ( r.x + r.width  ).max( 0.0 ).min( w as f32 );
			let y1 = ( r.y + r.height ).max( 0.0 ).min( h as f32 );
			if x1 <= x0 || y1 <= y0 { continue; }
			pb.push_rect( tiny_skia::Rect::from_ltrb( x0, y0, x1, y1 )
				.expect( "valid rect" ) );
		}
		if let Some( path ) = pb.finish()
		{
			mask.fill_path( &path, FillRule::Winding, false, Transform::identity() );
			self.clip_mask   = Some( mask );
			self.clip_bounds = rects.to_vec();
		} else {
			self.clip_mask   = None;
			self.clip_bounds = Vec::new();
		}
	}

	/// 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 )
	{
		self.clip_mask   = None;
		self.clip_bounds = Vec::new();
	}

	pub ( super ) fn has_clip( &self ) -> bool
	{
		self.clip_mask.is_some()
	}

	/// Snapshot of the active clip bounds (empty when no clip is set).
	pub fn clip_bounds_snapshot( &self ) -> Vec<Rect>
	{
		if self.has_clip() { self.clip_bounds.clone() } else { Vec::new() }
	}

	/// True when a horizontal strip `y` in `[y0, y1]` touches any clip bound.
	pub ( super ) fn strip_intersects_clip( &self, y0: f32, y1: f32 ) -> bool
	{
		if self.clip_bounds.is_empty() { return !self.has_clip(); }
		self.clip_bounds.iter().any( |r|
		{
			y1 > r.y && y0 < r.y + r.height
		} )
	}

	/// Zero the alpha+RGB bytes inside each rect, used by the
	/// partial-redraw path when the surface background is fully
	/// transparent.
	pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
	{
		let pw    = self.pixmap.width()  as i32;
		let ph    = self.pixmap.height() as i32;
		let bytes = self.pixmap.data_mut();
		for r in rects
		{
			let x0 = ( r.x as i32 ).max( 0 );
			let y0 = ( r.y as i32 ).max( 0 );
			let x1 = ( ( r.x + r.width  ).ceil() as i32 ).min( pw );
			let y1 = ( ( r.y + r.height ).ceil() as i32 ).min( ph );
			if x1 <= x0 || y1 <= y0 { continue; }
			for py in y0..y1
			{
				let row_start = ( py * pw + x0 ) as usize * 4;
				let row_end   = ( py * pw + x1 ) as usize * 4;
				bytes[ row_start..row_end ].fill( 0 );
			}
		}
	}
}