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(),
}
}
}