First commit. Version 0.1.0
This commit is contained in:
545
src/gles_render/primitives.rs
Normal file
545
src/gles_render/primitives.rs
Normal file
@@ -0,0 +1,545 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-only
|
||||
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||
|
||||
//! Primitive draw ops for [`GlesCanvas`]: solid and gradient rect
|
||||
//! fills, inner and outer shadows, stroke, line. All go through the
|
||||
//! shared quad VAO + one of the pre-compiled shader programs from
|
||||
//! [`super::shaders`], with uniforms set per-call.
|
||||
//!
|
||||
//! # Shared `unsafe` invariants
|
||||
//!
|
||||
//! Every `unsafe` block below relies on the same canvas-wide contract
|
||||
//! and only adds one note per block when something specific applies:
|
||||
//!
|
||||
//! * The GL context behind `self.gl` is current on this thread — the
|
||||
//! `GlesCanvas` constructors only return a value when this is true,
|
||||
//! and every `&mut self` method runs on the construction thread.
|
||||
//! * Every `program` / `uniform_*` / `vertex_array` / `texture` handle
|
||||
//! stored on `self` was produced by the same context in `setup.rs`
|
||||
//! and outlives the draw call.
|
||||
//! * Each draw method calls `activate_target` first, which re-binds the
|
||||
//! canvas FBO and re-applies viewport / scissor — so the unsafe block
|
||||
//! never inherits a stranded binding from a sibling canvas.
|
||||
//! * `bind_vertex_array(None)` and `bind_texture(_, None)` at the end
|
||||
//! of the unsafe block leaves the global GL state in the same shape
|
||||
//! the next draw method assumes (no stranded VAO / texture binding).
|
||||
|
||||
use glow::HasContext;
|
||||
|
||||
use crate::theme::{ gradient_lut, BlendMode, InsetShadow, LinearGradient, RadialGradient, Shadow };
|
||||
use crate::types::{ Color, Corners, Rect };
|
||||
|
||||
use super::helpers::ortho_rect;
|
||||
use super::GlesCanvas;
|
||||
|
||||
impl GlesCanvas
|
||||
{
|
||||
/// Returns `true` when `rect` (expanded by `margin` on every side)
|
||||
/// is entirely outside the active scissor — the GPU would cull
|
||||
/// every fragment pre-shader anyway, so skipping the draw saves
|
||||
/// the `activate_target` / `use_program` / uniform / VAO / draw
|
||||
/// sequence. No scissor = no cull (the whole canvas is fair game).
|
||||
fn rect_culled( &self, rect: Rect, margin: f32 ) -> bool
|
||||
{
|
||||
let Some( clip ) = self.clip_scissor else { return false };
|
||||
let r_x0 = rect.x - margin;
|
||||
let r_y0 = rect.y - margin;
|
||||
let r_x1 = rect.x + rect.width + margin;
|
||||
let r_y1 = rect.y + rect.height + margin;
|
||||
let c_x1 = clip.x + clip.width;
|
||||
let c_y1 = clip.y + clip.height;
|
||||
r_x1 <= clip.x || c_x1 <= r_x0 || r_y1 <= clip.y || c_y1 <= r_y0
|
||||
}
|
||||
|
||||
pub fn fill_rect( &mut self, rect: Rect, color: Color, corners: Corners )
|
||||
{
|
||||
if self.rect_culled( rect, 1.0 ) { return; }
|
||||
self.activate_target();
|
||||
// Expand the quad 1 px on each side so the outer half of the SDF
|
||||
// antialiasing band (d ∈ [0, 0.5]) has fragments to cover along the
|
||||
// straight edges of pills / rounded rects. `u_size` and `u_radii`
|
||||
// stay anchored to the original rect — `u_pad` lets the shader
|
||||
// remap `v_uv` from the larger quad back into rect-local space.
|
||||
let pad = 1.0_f32;
|
||||
let expanded = Rect
|
||||
{
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + 2.0 * pad,
|
||||
height: rect.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
let alpha = color.a * self.global_alpha;
|
||||
// SAFETY: see module doc. `u_rect_stroke = 0.0` triggers the fill
|
||||
// branch of `RECT_FRAG_SRC`; the SDF reads `u_size` / `u_radii` of
|
||||
// the original rect while `u_pad` remaps `v_uv` from the expanded
|
||||
// quad — so the rasteriser sees the padded geometry but the shader
|
||||
// computes coverage in original-rect coordinates.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.rect_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_rect_mvp ), false, &mvp );
|
||||
self.gl.uniform_4_f32( Some( &self.u_rect_color ), color.r, color.g, color.b, alpha );
|
||||
self.gl.uniform_2_f32( Some( &self.u_rect_size ), rect.width, rect.height );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_rect_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_rect_stroke ), 0.0 );
|
||||
self.gl.uniform_1_f32( Some( &self.u_rect_pad ), pad );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the cached gradient LUT texture for `lut_bytes`, uploading
|
||||
/// it on the first call for each unique byte sequence. Subsequent calls
|
||||
/// with the same bytes skip `glTexImage2D` entirely. The texture lives
|
||||
/// until `clear_gradient_cache` is called (e.g. on theme change) or
|
||||
/// the canvas is dropped.
|
||||
fn ensure_lut_texture( &mut self, lut_bytes: &[u8] ) -> glow::Texture
|
||||
{
|
||||
use std::hash::{ Hash, Hasher };
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
lut_bytes.hash( &mut h );
|
||||
let key = h.finish();
|
||||
|
||||
if let Some( &tex ) = self.gradient_lut_cache.get( &key )
|
||||
{
|
||||
return tex;
|
||||
}
|
||||
|
||||
// SAFETY: see module doc. `lut_bytes` is the contiguous
|
||||
// `LUT_SAMPLES * 4` byte LUT produced by `gradient_lut::build_lut_bytes`
|
||||
// (RGBA8, one row); the texture allocation matches that exact shape.
|
||||
// We unbind TEXTURE_2D on exit to keep the unit-0 binding shape the
|
||||
// rest of the canvas assumes.
|
||||
unsafe
|
||||
{
|
||||
let tex = self.gl.create_texture().expect( "gradient LUT texture" );
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
|
||||
self.gl.tex_image_2d(
|
||||
glow::TEXTURE_2D,
|
||||
0,
|
||||
glow::RGBA as i32,
|
||||
gradient_lut::LUT_SAMPLES as i32,
|
||||
1,
|
||||
0,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelUnpackData::Slice( Some( lut_bytes ) ),
|
||||
);
|
||||
self.gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MIN_FILTER, glow::LINEAR as i32 );
|
||||
self.gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_MAG_FILTER, glow::LINEAR as i32 );
|
||||
self.gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_S, glow::CLAMP_TO_EDGE as i32 );
|
||||
self.gl.tex_parameter_i32( glow::TEXTURE_2D, glow::TEXTURE_WRAP_T, glow::CLAMP_TO_EDGE as i32 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||
self.gradient_lut_cache.insert( key, tex );
|
||||
tex
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a rectangle with a linear gradient.
|
||||
///
|
||||
/// Bakes a CPU-side LUT from `g.stops` and fetches (or creates) the
|
||||
/// corresponding cached GPU texture via `ensure_lut_texture`,
|
||||
/// then draws the quad with the gradient shader.
|
||||
pub fn fill_linear_gradient_rect( &mut self, rect: Rect, g: &LinearGradient, corners: Corners )
|
||||
{
|
||||
if self.rect_culled( rect, 1.0 ) { return; }
|
||||
let lut_bytes = gradient_lut::build_lut_bytes( &g.stops, g.space );
|
||||
let tex = self.ensure_lut_texture( &lut_bytes );
|
||||
let theta = g.angle_deg.to_radians();
|
||||
// CSS convention: 0° points up. dir.y is negative-up in screen space.
|
||||
let dir_x = theta.sin();
|
||||
let dir_y = -theta.cos();
|
||||
let line_length = ( rect.width * dir_x ).abs() + ( rect.height * dir_y ).abs();
|
||||
let line_length = if line_length.abs() < 1e-3 { 1e-3 } else { line_length };
|
||||
|
||||
self.activate_target();
|
||||
// See fill_rect for the rationale on the 1 px quad pad.
|
||||
let pad = 1.0_f32;
|
||||
let expanded = Rect
|
||||
{
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + 2.0 * pad,
|
||||
height: rect.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
// SAFETY: see module doc. `tex` is the cached LUT for `g.stops`
|
||||
// produced by `ensure_lut_texture` above (RGBA8, sampler unit 0);
|
||||
// `dir_x`, `dir_y`, `line_length` are derived from finite inputs
|
||||
// (`line_length` is clamped above 1e-3 so the shader's divide is
|
||||
// well-defined).
|
||||
unsafe
|
||||
{
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
|
||||
self.gl.use_program( Some( self.linear_gradient_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_lingrad_mvp ), false, &mvp );
|
||||
self.gl.uniform_1_i32( Some( &self.u_lingrad_lut ), 0 );
|
||||
self.gl.uniform_2_f32( Some( &self.u_lingrad_dir ), dir_x, dir_y );
|
||||
self.gl.uniform_2_f32( Some( &self.u_lingrad_size ), rect.width, rect.height );
|
||||
self.gl.uniform_1_f32( Some( &self.u_lingrad_line_length ), line_length );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_lingrad_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_lingrad_pad ), pad );
|
||||
self.gl.uniform_1_f32( Some( &self.u_lingrad_lut_domain_min ), gradient_lut::LUT_DOMAIN.0 );
|
||||
self.gl.uniform_1_f32( Some( &self.u_lingrad_lut_domain_span ), gradient_lut::LUT_DOMAIN.1 - gradient_lut::LUT_DOMAIN.0 );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill a rectangle with a radial gradient.
|
||||
///
|
||||
/// `g.center` is interpreted in box-relative fractions (as declared by
|
||||
/// the theme), `g.radius` is the fractional radial extent. Same cached
|
||||
/// LUT strategy as [`Self::fill_linear_gradient_rect`].
|
||||
pub fn fill_radial_gradient_rect( &mut self, rect: Rect, g: &RadialGradient, corners: Corners )
|
||||
{
|
||||
if self.rect_culled( rect, 1.0 ) { return; }
|
||||
let lut_bytes = gradient_lut::build_lut_bytes( &g.stops, g.space );
|
||||
let tex = self.ensure_lut_texture( &lut_bytes );
|
||||
|
||||
self.activate_target();
|
||||
// See fill_rect for the rationale on the 1 px quad pad.
|
||||
let pad = 1.0_f32;
|
||||
let expanded = Rect
|
||||
{
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + 2.0 * pad,
|
||||
height: rect.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
// SAFETY: see module doc. Same LUT contract as the linear path
|
||||
// above. `g.center` and `g.radius` are finite fractional values
|
||||
// from the theme parser (validated at load time).
|
||||
unsafe
|
||||
{
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
|
||||
self.gl.use_program( Some( self.radial_gradient_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_radgrad_mvp ), false, &mvp );
|
||||
self.gl.uniform_1_i32( Some( &self.u_radgrad_lut ), 0 );
|
||||
self.gl.uniform_2_f32( Some( &self.u_radgrad_center ), g.center[0], g.center[1] );
|
||||
self.gl.uniform_1_f32( Some( &self.u_radgrad_radius_frac ), g.radius );
|
||||
self.gl.uniform_2_f32( Some( &self.u_radgrad_size ), rect.width, rect.height );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_radgrad_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_radgrad_pad ), pad );
|
||||
self.gl.uniform_1_f32( Some( &self.u_radgrad_lut_domain_min ), gradient_lut::LUT_DOMAIN.0 );
|
||||
self.gl.uniform_1_f32( Some( &self.u_radgrad_lut_domain_span ), gradient_lut::LUT_DOMAIN.1 - gradient_lut::LUT_DOMAIN.0 );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint an outer drop shadow behind a rounded rect.
|
||||
///
|
||||
/// Analytic Gaussian approximation over the shape SDF — see the note
|
||||
/// above `SHADOW_OUTER_FRAG_SRC`. The drawing quad is expanded on each
|
||||
/// side by `max(blur, 0) + max(spread, 0) + 1` (the `+ 1` leaves a
|
||||
/// single antialias pixel of slack) and offset by `shadow.offset` so
|
||||
/// the fragment shader sees the full falloff region.
|
||||
///
|
||||
/// Only `BlendMode::Normal` is honoured today; other modes silently
|
||||
/// fall through to `Normal` because the analytic shader only outputs
|
||||
/// `over`.
|
||||
pub fn fill_shadow_outer( &mut self, target: Rect, shadow: &Shadow, corners: Corners )
|
||||
{
|
||||
let blur_margin = shadow.blur.max( 0.0 );
|
||||
let spread_margin = shadow.spread.max( 0.0 );
|
||||
let margin = blur_margin + spread_margin + 1.0;
|
||||
|
||||
// Outer shadows draw a quad expanded by `margin` on each side
|
||||
// (to capture the Gaussian falloff outside the shape); offset
|
||||
// the target by `shadow.offset` for the cull test so a shadow
|
||||
// that sits off-centre is not skipped prematurely.
|
||||
let culled_rect = Rect
|
||||
{
|
||||
x: target.x + shadow.offset[0],
|
||||
y: target.y + shadow.offset[1],
|
||||
width: target.width,
|
||||
height: target.height,
|
||||
};
|
||||
if self.rect_culled( culled_rect, margin ) { return; }
|
||||
|
||||
let quad = Rect
|
||||
{
|
||||
x: target.x + shadow.offset[0] - margin,
|
||||
y: target.y + shadow.offset[1] - margin,
|
||||
width: target.width + 2.0 * margin,
|
||||
height: target.height + 2.0 * margin,
|
||||
};
|
||||
let sigma = shadow.sigma().max( 0.5 );
|
||||
let alpha = shadow.color.a * self.global_alpha;
|
||||
|
||||
self.activate_target();
|
||||
let mvp = ortho_rect( self.width, self.height, quad );
|
||||
// SAFETY: see module doc. `sigma` is clamped above 0.5 so the
|
||||
// shader's divide is well-defined; `margin` covers the full
|
||||
// Gaussian falloff so the rasteriser sees every fragment the
|
||||
// SDF wants to shade.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.shadow_outer_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_shadow_mvp ), false, &mvp );
|
||||
self.gl.uniform_2_f32( Some( &self.u_shadow_size ), target.width, target.height );
|
||||
self.gl.uniform_2_f32( Some( &self.u_shadow_padding ), margin, margin );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_shadow_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_shadow_spread ), shadow.spread );
|
||||
self.gl.uniform_1_f32( Some( &self.u_shadow_sigma ), sigma );
|
||||
self.gl.uniform_4_f32( Some( &self.u_shadow_color ),
|
||||
shadow.color.r, shadow.color.g, shadow.color.b, alpha );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
}
|
||||
}
|
||||
|
||||
/// Paint an inner (inset) shadow inside a rounded rect.
|
||||
///
|
||||
/// Differences versus [`Self::fill_shadow_outer`]:
|
||||
///
|
||||
/// * The drawing quad matches the target rect exactly — the inset is
|
||||
/// clipped to the outer SDF by the shader, so no external padding
|
||||
/// is needed and there is no spatial offset of the geometry.
|
||||
/// * The shader carries the per-shadow `offset` as a uniform rather
|
||||
/// than translating the quad, because the inset is biased *inside*
|
||||
/// the shape rather than cast outside it.
|
||||
/// * The pipeline blend state is switched for the duration of the
|
||||
/// draw to honour `InsetShadow::blend` and restored afterwards.
|
||||
///
|
||||
/// Blend modes: `Normal` stays on the pipeline default
|
||||
/// `(ONE, ONE_MINUS_SRC_ALPHA)`. `PlusLighter` uses `(ONE, ONE)` —
|
||||
/// pure additive on premultiplied inputs, naturally clamped by the
|
||||
/// framebuffer to `[0, 1]`, which is exactly the CSS definition.
|
||||
/// `Multiply` uses `(DST_COLOR, ZERO)` on RGB and `(DST_ALPHA, ZERO)`
|
||||
/// on alpha — a straight multiplicative blend. `Screen` uses
|
||||
/// `(ONE_MINUS_DST_COLOR, ONE)`, the canonical `a + b − a·b` form.
|
||||
/// `Overlay` cannot be expressed with GL's fixed-function blend
|
||||
/// state alone — it needs to read the destination pixel. This
|
||||
/// branch snapshots the current FBO into `aux_a` via
|
||||
/// `glCopyTexSubImage2D`, then draws through
|
||||
/// `shadow_inset_overlay_program` which samples that snapshot at
|
||||
/// `gl_FragCoord.xy / canvas_size`, computes the per-channel CSS
|
||||
/// Overlay formula in-shader, and emits premultiplied
|
||||
/// `(overlay * mask, mask)` — so the usual premul over blend
|
||||
/// composes the blended colour on top of the base. One FBO
|
||||
/// snapshot per Overlay shadow.
|
||||
pub fn fill_shadow_inset( &mut self, target: Rect, shadow: &InsetShadow, corners: Corners )
|
||||
{
|
||||
// Inset shadows draw a quad at `target` ± 1 px AA pad; the
|
||||
// shape lives entirely inside. If that quad is outside the
|
||||
// scissor, every fragment is culled — skip the whole path
|
||||
// (including the Overlay snapshot, which is the expensive
|
||||
// bit).
|
||||
if self.rect_culled( target, 1.0 ) { return; }
|
||||
|
||||
let sigma = shadow.sigma().max( 0.5 );
|
||||
let alpha = shadow.color.a * self.global_alpha;
|
||||
|
||||
// Overlay goes through the framebuffer-fetch path. Everything
|
||||
// else uses the original SDF inset shader with a blend-state
|
||||
// swap.
|
||||
if matches!( shadow.blend, BlendMode::Overlay )
|
||||
{
|
||||
// Snapshot the inset's draw rect plus the 1 px AA pad so the
|
||||
// quad's expanded edge still samples valid snapshot data.
|
||||
// The shader samples `aux_a` at `gl_FragCoord.xy /
|
||||
// canvas_size`, so reads outside the snapshotted region
|
||||
// would pull stale content from a previous frame.
|
||||
//
|
||||
// Use the scissor-tight variant: Overlay samples at exactly
|
||||
// one point per fragment, and any fragment outside the
|
||||
// active scissor is culled before the shader runs, so the
|
||||
// snapshot only needs to cover the intersection.
|
||||
let pad = 1.0_f32;
|
||||
let snap_rect = Rect
|
||||
{
|
||||
x: target.x - pad,
|
||||
y: target.y - pad,
|
||||
width: target.width + 2.0 * pad,
|
||||
height: target.height + 2.0 * pad,
|
||||
};
|
||||
self.snapshot_fbo_region_tight( snap_rect );
|
||||
self.activate_target();
|
||||
let expanded = Rect
|
||||
{
|
||||
x: target.x - pad,
|
||||
y: target.y - pad,
|
||||
width: target.width + 2.0 * pad,
|
||||
height: target.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
let aux_tex = self.aux_a.expect( "snapshotted" ).1;
|
||||
// SAFETY: see module doc. `aux_tex` was just populated by
|
||||
// `snapshot_fbo_region_tight` so it carries a valid copy of
|
||||
// the live FBO at full canvas resolution; the shader samples
|
||||
// it through `gl_FragCoord.xy / canvas_size`. We unbind unit-0
|
||||
// after the draw to avoid stranding the snapshot binding.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.shadow_inset_overlay_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_inset_ov_mvp ), false, &mvp );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_ov_size ), target.width, target.height );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_ov_padding ), pad, pad );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_inset_ov_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_inset_ov_spread ), shadow.spread );
|
||||
self.gl.uniform_1_f32( Some( &self.u_inset_ov_sigma ), sigma );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_ov_offset ), shadow.offset[0], shadow.offset[1] );
|
||||
self.gl.uniform_4_f32( Some( &self.u_inset_ov_color ),
|
||||
shadow.color.r, shadow.color.g, shadow.color.b, alpha );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_ov_canvas_size ), self.width as f32, self.height as f32 );
|
||||
self.gl.active_texture( glow::TEXTURE0 );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( aux_tex ) );
|
||||
self.gl.uniform_1_i32( Some( &self.u_inset_ov_snapshot ), 0 );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.activate_target();
|
||||
// 1 px AA pad on the quad so the outer-silhouette clip
|
||||
// (`outer_coverage`) renders its full smoothstep band instead
|
||||
// of terminating at the surface rect. Same rationale as
|
||||
// fill_rect. `u_size` / `u_radii` stay anchored to `target`.
|
||||
let pad = 1.0_f32;
|
||||
let expanded = Rect
|
||||
{
|
||||
x: target.x - pad,
|
||||
y: target.y - pad,
|
||||
width: target.width + 2.0 * pad,
|
||||
height: target.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
// SAFETY: see module doc. We swap the global blend state for the
|
||||
// duration of one draw and restore the canvas-wide default
|
||||
// `(ONE, ONE_MINUS_SRC_ALPHA)` at the end of the block so the
|
||||
// next draw inherits the expected pipeline blend.
|
||||
unsafe
|
||||
{
|
||||
// Switch the blend state for this one draw.
|
||||
match shadow.blend
|
||||
{
|
||||
BlendMode::Normal => { /* already the pipeline default */ }
|
||||
BlendMode::PlusLighter => self.gl.blend_func( glow::ONE, glow::ONE ),
|
||||
BlendMode::Multiply => self.gl.blend_func_separate
|
||||
(
|
||||
glow::DST_COLOR, glow::ZERO,
|
||||
glow::DST_ALPHA, glow::ZERO,
|
||||
),
|
||||
BlendMode::Screen => self.gl.blend_func( glow::ONE_MINUS_DST_COLOR, glow::ONE ),
|
||||
BlendMode::Overlay => unreachable!( "Overlay handled above via snapshot" ),
|
||||
}
|
||||
|
||||
self.gl.use_program( Some( self.shadow_inset_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_inset_mvp ), false, &mvp );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_size ), target.width, target.height );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_padding ), pad, pad );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_inset_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_inset_spread ), shadow.spread );
|
||||
self.gl.uniform_1_f32( Some( &self.u_inset_sigma ), sigma );
|
||||
self.gl.uniform_2_f32( Some( &self.u_inset_offset ), shadow.offset[0], shadow.offset[1] );
|
||||
self.gl.uniform_4_f32( Some( &self.u_inset_color ),
|
||||
shadow.color.r, shadow.color.g, shadow.color.b, alpha );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
|
||||
// Restore the pipeline default.
|
||||
if !matches!( shadow.blend, BlendMode::Normal )
|
||||
{
|
||||
self.gl.blend_func( glow::ONE, glow::ONE_MINUS_SRC_ALPHA );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stroke a rectangle outline. The stroke is centered on the (rounded)
|
||||
/// boundary, matching tiny-skia's stroke_path so software and GPU paths
|
||||
/// produce the same shape (e.g. a circular focus ring around an icon
|
||||
/// button stays circular).
|
||||
///
|
||||
/// The drawing quad is expanded by `width / 2` so the outer half of the
|
||||
/// stroke — which lies *outside* the original rect — has fragments to
|
||||
/// cover; the SDF in the rect shader then clamps to the ring.
|
||||
pub fn stroke_rect( &mut self, rect: Rect, color: Color, width: f32, corners: Corners )
|
||||
{
|
||||
let half = width * 0.5;
|
||||
if self.rect_culled( rect, half + 1.0 ) { return; }
|
||||
self.activate_target();
|
||||
// Expand the *quad* outward so the outer half of the stroke has
|
||||
// fragments to cover, plus 1 px extra so the 2 px AA band on the
|
||||
// outer side of the stroke (half_w + 1 in the shader) has
|
||||
// fragments too. `u_size` and `u_radii` keep their ORIGINAL
|
||||
// values — they define the SDF, and the stroke's centerline must
|
||||
// sit on the SDF zero-line (the original rect boundary). `u_pad`
|
||||
// tells the fragment shader to remap `v_uv` from the larger quad
|
||||
// back into original-rect space, so the SDF stays anchored to
|
||||
// the original geometry. Growing `u_size`/`u_radii` instead
|
||||
// would shift the zero-line outward and, in the circle case
|
||||
// (radius = size/2), turn the result into a rounded square.
|
||||
let pad = half + 1.0;
|
||||
let expanded = Rect
|
||||
{
|
||||
x: rect.x - pad,
|
||||
y: rect.y - pad,
|
||||
width: rect.width + 2.0 * pad,
|
||||
height: rect.height + 2.0 * pad,
|
||||
};
|
||||
let mvp = ortho_rect( self.width, self.height, expanded );
|
||||
let alpha = color.a * self.global_alpha;
|
||||
// SAFETY: see module doc. `u_rect_stroke = width > 0.0` triggers
|
||||
// the stroke branch of `RECT_FRAG_SRC`. Same SDF-anchored-to-original
|
||||
// remap as `fill_rect`; here the quad is padded by `half + 1.0` so
|
||||
// the outer half of the stroke plus its 1 px AA band have fragments.
|
||||
unsafe
|
||||
{
|
||||
self.gl.use_program( Some( self.rect_program ) );
|
||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_rect_mvp ), false, &mvp );
|
||||
self.gl.uniform_4_f32( Some( &self.u_rect_color ), color.r, color.g, color.b, alpha );
|
||||
self.gl.uniform_2_f32( Some( &self.u_rect_size ), rect.width, rect.height );
|
||||
self.gl.uniform_4_f32_slice( Some( &self.u_rect_radii ), &corners.to_uniform() );
|
||||
self.gl.uniform_1_f32( Some( &self.u_rect_stroke ), width );
|
||||
self.gl.uniform_1_f32( Some( &self.u_rect_pad ), pad );
|
||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||
self.gl.bind_vertex_array( None );
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a line as a thin axis-aligned rect (diagonal lines fall back to
|
||||
/// stamping small squares).
|
||||
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
||||
{
|
||||
let dx = x1 - x0;
|
||||
let dy = y1 - y0;
|
||||
let len = ( dx * dx + dy * dy ).sqrt();
|
||||
if len < 0.1 { return; }
|
||||
let min_x = x0.min( x1 );
|
||||
let min_y = y0.min( y1 );
|
||||
if dy.abs() < 0.1
|
||||
{
|
||||
self.fill_rect( Rect { x: min_x, y: min_y - width / 2.0, width: dx.abs(), height: width }, color, Corners::ZERO );
|
||||
} else if dx.abs() < 0.1 {
|
||||
self.fill_rect( Rect { x: min_x - width / 2.0, y: min_y, width, height: dy.abs() }, color, Corners::ZERO );
|
||||
} else {
|
||||
let steps = len.ceil() as usize;
|
||||
for i in 0..steps
|
||||
{
|
||||
let t = i as f32 / len;
|
||||
let px = x0 + dx * t;
|
||||
let py = y0 + dy * t;
|
||||
self.fill_rect( Rect { x: px - width / 2.0, y: py - width / 2.0, width, height: width }, color, Corners::ZERO );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user