// 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, PathCmd, 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
	}

	/// Fill an arbitrary vector path (commands in surface coordinates) with a
	/// solid colour. CPU fallback: rasterised with tiny-skia into a bbox-sized
	/// pixmap and blitted as a transient texture — there is no GPU path shader.
	pub fn fill_path( &mut self, cmds: &[ PathCmd ], color: Color )
	{
		self.rasterise_path( cmds, color, None );
	}

	/// Stroke an arbitrary vector path (commands in surface coordinates) with a
	/// centered stroke of `width` px. Same tiny-skia-into-texture CPU fallback as
	/// [`Self::fill_path`].
	pub fn stroke_path( &mut self, cmds: &[ PathCmd ], color: Color, width: f32 )
	{
		self.rasterise_path( cmds, color, Some( width ) );
	}

	/// CPU fallback for vector paths on the GPU backend: rasterise into a
	/// tiny-skia pixmap sized to the path's bounding box, then blit it as a
	/// texture via `draw_image_data`. No GPU path shader.
	fn rasterise_path( &mut self, cmds: &[ PathCmd ], color: Color, stroke: Option<f32> )
	{
		let Some( path ) = crate::render::helpers::build_ts_path( cmds ) else { return };
		let bounds = path.bounds();
		let pad = stroke.map_or( 1.0, |w| w * 0.5 + 1.0 );
		let x0 = ( bounds.left()   - pad ).floor();
		let y0 = ( bounds.top()    - pad ).floor();
		let x1 = ( bounds.right()  + pad ).ceil();
		let y1 = ( bounds.bottom() + pad ).ceil();
		let pw = ( x1 - x0 ).max( 1.0 ) as u32;
		let ph = ( y1 - y0 ).max( 1.0 ) as u32;
		if pw > 8192 || ph > 8192 { return; }
		let Some( mut pixmap ) = tiny_skia::Pixmap::new( pw, ph ) else { return };

		let mut paint = tiny_skia::Paint::default();
		paint.set_color( Color::rgba( color.r, color.g, color.b, color.a * self.global_alpha ).to_tiny_skia() );
		paint.anti_alias = true;
		let tf = tiny_skia::Transform::from_translate( -x0, -y0 );
		match stroke
		{
			Some( w ) =>
			{
				let mut s = tiny_skia::Stroke::default();
				s.width = w;
				pixmap.stroke_path( &path, &paint, &s, tf, None );
			}
			None => { pixmap.fill_path( &path, &paint, tiny_skia::FillRule::Winding, tf, None ); }
		}

		// tiny-skia stores premultiplied RGBA; the texture shader expects straight.
		let mut rgba = pixmap.data().to_vec();
		for px in rgba.chunks_exact_mut( 4 )
		{
			let a = px[ 3 ] as u32;
			if a > 0 && a < 255
			{
				px[ 0 ] = ( px[ 0 ] as u32 * 255 / a ).min( 255 ) as u8;
				px[ 1 ] = ( px[ 1 ] as u32 * 255 / a ).min( 255 ) as u8;
				px[ 2 ] = ( px[ 2 ] as u32 * 255 / a ).min( 255 ) as u8;
			}
		}
		// Transient texture (upload → draw → delete): a path's pixels change
		// every frame for animated vectors, which would make the content-keyed
		// image cache in `draw_image_data` grow without bound and exhaust GL.
		let dest = Rect { x: x0, y: y0, width: pw as f32, height: ph as f32 };
		let tex = super::helpers::upload_rgba_texture( &self.gl, self.version, &rgba, pw as i32, ph as i32 );
		self.draw_external_texture( tex, dest, 1.0 );
		unsafe { self.gl.delete_texture( tex ); }
	}

	/// Fill `rect` with a solid colour, with per-corner rounding from `corners`.
	/// Coverage (including the rounded corners) comes from an SDF in the rect
	/// shader; `color.a` is multiplied by `global_alpha`. Culled early when the
	/// rect falls entirely outside the active scissor.
	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.expand( 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.expand( 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.expand( 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 = target.expand( pad );
			self.snapshot_fbo_region_tight( snap_rect );
			self.activate_target();
			let expanded = target.expand( 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 = target.expand( 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.expand( 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 );
			}
		}
	}

}
