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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// 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.
//! [`interior_clip`] narrows whatever clip is active to a widget's rect, on either backend.

use tiny_skia::{ FillRule, Mask, PathBuilder, Transform };

use crate::types::Rect;

use super::SoftwareCanvas;

/// The active clip `outer` narrowed to `inner`. An empty intersection must still clip everything: an empty rect list would lift the clip.
pub( crate ) fn interior_clip( outer: &[ Rect ], inner: Rect ) -> Vec<Rect>
{
	if outer.is_empty()
	{
		return vec![ inner ];
	}
	let clip: Vec<Rect> = outer.iter()
		.filter_map( |o| rect_intersect( *o, inner ) )
		.collect();
	if clip.is_empty()
	{
		vec![ Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } ]
	} else {
		clip
	}
}

fn rect_intersect( a: Rect, b: Rect ) -> Option<Rect>
{
	let x0 = a.x.max( b.x );
	let y0 = a.y.max( b.y );
	let x1 = ( a.x + a.width  ).min( b.x + b.width  );
	let y1 = ( a.y + a.height ).min( b.y + b.height );
	if x1 > x0 && y1 > y0
	{
		Some( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } )
	} else {
		None
	}
}

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 if rects.is_empty() {
			self.clip_mask   = None;
			self.clip_bounds = Vec::new();
		} else {
			// Only zero-area rects: clip everything, like the GLES zero scissor.
			self.clip_mask   = Some( mask );
			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 );
			}
		}
	}
}