ltk/gles_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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! `glScissor`-based clipping + whole-canvas fill / clear for
//! [`GlesCanvas`]. 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 1–3 rects
//! so the union is barely larger than the sum. Disjoint regions
//! would want a stencil-buffer path; not implemented today.

use glow::HasContext;

use crate::types::{ Color, Rect };

use super::GlesCanvas;

impl GlesCanvas
{
	pub fn set_clip_rects( &mut self, rects: &[Rect] )
	{
		// Scissor is global GL state; rebind our FBO first so the scissor
		// applies to this canvas and not whatever target was active before.
		self.activate_target();
		if rects.is_empty()
		{
			self.clear_clip();
			return;
		}
		let mut x0 =  f32::INFINITY;
		let mut y0 =  f32::INFINITY;
		let mut x1 = -f32::INFINITY;
		let mut y1 = -f32::INFINITY;
		for r in rects
		{
			x0 = x0.min( r.x );
			y0 = y0.min( r.y );
			x1 = x1.max( r.x + r.width  );
			y1 = y1.max( r.y + r.height );
		}
		x0 = x0.max( 0.0 );
		y0 = y0.max( 0.0 );
		x1 = x1.min( self.width  as f32 );
		y1 = y1.min( self.height as f32 );
		if x1 <= x0 || y1 <= y0
		{
			// Empty union — install a zero-area scissor so subsequent draws
			// are no-ops without disabling the test.
			self.set_scissor( Rect { x: 0.0, y: 0.0, width: 0.0, height: 0.0 } );
			return;
		}
		self.set_scissor( Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } );
	}

	pub fn clear_clip( &mut self )
	{
		// SAFETY: see `primitives.rs` module doc. `disable( SCISSOR_TEST )` is
		// pure global-state mutation; the cached `clip_scissor` is updated
		// to match below.
		unsafe { self.gl.disable( glow::SCISSOR_TEST ); }
		self.clip_scissor = None;
	}

	/// Snapshot of the active scissor as a `Vec<Rect>` (empty when no
	/// scissor is set).
	pub fn clip_bounds_snapshot( &self ) -> Vec<Rect>
	{
		self.clip_scissor.map_or_else( Vec::new, |r| vec![ r ] )
	}

	/// Apply `rect` as the current scissor (top-left coords, GL bottom-left).
	fn set_scissor( &mut self, rect: Rect )
	{
		let ( x, y, w, h ) = self.scissor_pixels( rect );
		// SAFETY: `scissor_pixels` clamps to non-negative integers; GL accepts
		// arbitrary scissor rects (regions outside the framebuffer simply
		// cull all fragments). State change is mirrored in `clip_scissor`.
		unsafe
		{
			self.gl.enable( glow::SCISSOR_TEST );
			self.gl.scissor( x, y, w, h );
		}
		self.clip_scissor = Some( rect );
	}

	/// Convert a top-left rect to the bottom-left integer pixel scissor that
	/// GL expects.
	pub( super ) fn scissor_pixels( &self, rect: Rect ) -> ( i32, i32, i32, i32 )
	{
		let x = rect.x.floor() as i32;
		let w = rect.width.ceil()  as i32;
		let h = rect.height.ceil() as i32;
		// GL origin is bottom-left, our coords are top-left.
		let y_top    = rect.y.floor() as i32;
		let y_bottom = self.height as i32 - y_top - h;
		( x, y_bottom.max( 0 ), w.max( 0 ), h.max( 0 ) )
	}

	/// Clear to a solid color. Honours the active scissor — if a clip is set,
	/// only the clipped region is filled.
	pub fn fill( &mut self, color: Color )
	{
		self.activate_target();
		// SAFETY: `clear` writes the configured `clear_color` into every
		// fragment that survives the scissor test (which `activate_target`
		// has already configured to match `clip_scissor`).
		unsafe
		{
			self.gl.clear_color( color.r, color.g, color.b, color.a );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
	}

	/// Clear to fully transparent. Honours the active scissor.
	pub fn clear( &mut self )
	{
		self.activate_target();
		// SAFETY: same as `fill`, with a transparent clear colour.
		unsafe
		{
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
			self.gl.clear( glow::COLOR_BUFFER_BIT );
		}
	}

	/// Zero the pixels inside each rect (alpha+RGB → 0).
	pub fn clear_rects_transparent( &mut self, rects: &[Rect] )
	{
		self.activate_target();
		let saved = self.clip_scissor;
		// SAFETY: enable scissor + set transparent clear colour once for
		// the whole loop; per-rect we rewrite `scissor` and clear. After
		// the loop we restore `saved` via `set_scissor` / `clear_clip`
		// so the cached `clip_scissor` matches the actual GL state again.
		unsafe
		{
			self.gl.enable( glow::SCISSOR_TEST );
			self.gl.clear_color( 0.0, 0.0, 0.0, 0.0 );
		}
		for r in rects
		{
			let ( x, y, w, h ) = self.scissor_pixels( *r );
			if w <= 0 || h <= 0 { continue; }
			// SAFETY: per-rect scissor + clear; same invariants as above.
			unsafe
			{
				self.gl.scissor( x, y, w, h );
				self.gl.clear( glow::COLOR_BUFFER_BIT );
			}
		}
		// Restore the previous scissor (or disable if none was active).
		match saved
		{
			Some( r ) => self.set_scissor( r ),
			None      => self.clear_clip(),
		}
	}

}