// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! GLES 2/3 shader sources used by `GlesCanvas`. All fragment shaders //! emit premultiplied-alpha colour so they compose correctly under //! the pipeline's default `glBlendFunc(ONE, ONE_MINUS_SRC_ALPHA)`. //! //! Every constant is `pub(super)` so only files inside //! `crate::gles_render` can reach them — callers always go through //! `GlesCanvas`'s public methods, not the raw shader source. // Vertex shader shared by both programs (just transforms a unit quad). // GLSL ES 1.00 — works on both GLES2 and GLES3 contexts (forward compatible // when no `#version` directive is present). pub( super ) const VERT_SRC: &str = r#" attribute vec2 a_pos; varying vec2 v_uv; uniform mat4 u_mvp; void main() { v_uv = a_pos; gl_Position = u_mvp * vec4(a_pos, 0.0, 1.0); } "#; // Fragment shader for solid/rounded rects (signed-distance smoothstep at edge). // // `u_stroke == 0` ⇒ filled rect. The shader fills the rounded shape with // `u_color`, antialiased over a 1-pixel band at the edge. // `u_stroke > 0` ⇒ stroked outline of width `u_stroke`, centered on the // rounded boundary, so outlines keep their corner radius. // // Distance formula is Iñigo Quílez's exact SDF for a rounded box, extended // to per-corner radii by picking `r` per-quadrant: // r = u_radii[ corner_index_from_sign( p - center ) ] // q = abs(p - center) - (size/2 - r) // d = min(max(q.x, q.y), 0) + length(max(q, 0)) - r // `u_radii` is ordered `(tl, tr, br, bl)` clockwise from top-left and is // uploaded as `Corners::to_uniform()`. A uniform-radii fill is the // degenerate case where every component is equal — the per-quadrant // branch collapses to the single-`r` formula. // // `u_pad` lets the caller draw the quad larger than `u_size` (used by // `stroke_rect`, which expands the quad by `stroke/2` on each side so the // outer half of the stroke has fragments to cover). The shader maps `v_uv` // from the larger quad back into the original-rect space: // p = v_uv * (size + 2*pad) - pad // so `p` ranges `[-pad, size+pad]`. Keeping `u_size` and `u_radii` at the // *original* values is essential — expanding them shifts the SDF zero-line // outward and breaks the circle case (radius = size/2). // // Each component of `u_radii` is clamped to `min(size.x, size.y) * 0.5` // before use. Callers frequently pass very large values (e.g. // `theme::RADIUS = 100`) as a "please make this a pill / capsule" // sentinel. Without the clamp, `size/2 - r` goes negative in the shorter // dimension and the rounded-box formula degenerates — rendering an // ellipse in the middle of the rect instead of a pill. // // Emits premultiplied-alpha colour. The pipeline blend is // `(ONE, ONE_MINUS_SRC_ALPHA)`, so every shader that writes colour must // premultiply its RGB by the final alpha. This matters for non-opaque // fills (coverage from the SDF, translucent `u_color.a`) where straight- // alpha output would under-saturate antialiased edges. pub( super ) const RECT_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform vec4 u_color; uniform vec2 u_size; uniform vec4 u_radii; uniform float u_stroke; uniform float u_pad; // Per-fragment corner radius lookup. `c` is the fragment position // relative to the rect's centre. Inside the shader, p.y grows UPWARD: // `ortho_rect` flips the v_uv axis so v_uv.y=0 maps to the bottom edge // of the rect in screen space and v_uv.y=1 to the top. So with `c = // p - size/2`: // c.x < 0, c.y > 0 → top-left → r.x // c.x > 0, c.y > 0 → top-right → r.y // c.x > 0, c.y < 0 → bottom-right → r.z // c.x < 0, c.y < 0 → bottom-left → r.w float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { float r_max = max(max(u_radii.x, u_radii.y), max(u_radii.z, u_radii.w)); if (r_max <= 0.0 && u_stroke <= 0.0 && u_pad <= 0.0) { float a = u_color.a; gl_FragColor = vec4(u_color.rgb * a, a); return; } vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad); vec2 c = p - u_size * 0.5; float r = corner_radius(c, u_radii); r = min(r, min(u_size.x, u_size.y) * 0.5); vec2 q = abs(c) - (u_size * 0.5 - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; // 2-px-wide AA band (half-width 1.0). A narrower (±0.5) band is // only sampled when pixel centres happen to fall inside it — when // the rasterizer grid aligns with the edge at subpixel ±0.5 the // band is jumped over and the transition becomes a binary step // (visible stair-stepping on curved edges). Doubling the band // guarantees at least one partial-coverage sample per scanline // regardless of alignment. The quad pad set by the caller is 1 px // so this still fits inside the geometry at d ≤ 1. float coverage; if (u_stroke > 0.0) { float half_w = u_stroke * 0.5; coverage = 1.0 - smoothstep(half_w - 1.0, half_w + 1.0, abs(d)); } else { coverage = 1.0 - smoothstep(-1.0, 1.0, d); } float a = u_color.a * coverage; gl_FragColor = vec4(u_color.rgb * a, a); } "##; // Fragment shader for RGBA textured quads (images / icons). // // Flips uv.y because images are uploaded top-down (CPU convention: row 0 = // top of source) but `glTexImage2D` puts the first row at the texture's // lower-left. With the quad orientation produced by `ortho_rect`, sampling // `v_uv` directly would draw the source upside-down on screen. // // Texture data arrives PREMULTIPLIED — `upload_rgba_texture` premuls the // straight-alpha CPU buffer once at upload. GL_LINEAR sampling can then // blend across texel boundaries without halo artefacts (a fully opaque // black texel next to a fully transparent white texel interpolates to // `( 0, 0, 0, 0.5 )` instead of the halo-producing `( 0.5, 0.5, 0.5, 0.5 )` // of straight-alpha interpolation). Multiplying premul by uniform opacity // preserves the invariant `rgb == rgb_straight * a`. pub( super ) const TEX_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_sampler; uniform float u_opacity; void main() { gl_FragColor = texture2D(u_sampler, vec2(v_uv.x, 1.0 - v_uv.y)) * u_opacity; } "##; // Clip-layer composite shader for `set_clip_path`. Path-clipped content is // drawn to an offscreen layer; this composites that layer onto the canvas // multiplied by an anti-aliased coverage mask, giving a smooth clipped edge. // `u_layer` is the full-canvas layer texture (premultiplied), `u_mask` the // path coverage rasterised over `u_bbox` (x, y, w, h in canvas pixels). The // quad is drawn over the bbox; `v_uv` runs 0..1 across it. Both FBO-backed // textures are sampled y-flipped (GL's lower-left origin). pub( super ) const CLIP_COMPOSITE_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_layer; uniform sampler2D u_mask; uniform vec2 u_canvas; uniform vec4 u_bbox; void main() { // `v_uv.y` runs bottom-to-top in screen space (FBO origin is lower-left, as // `ortho_rect` + the texture shader's flip establish), so the screen Y of // this fragment is `bbox.y + (1 - v_uv.y) * bbox.h`. vec2 sp = vec2( u_bbox.x + v_uv.x * u_bbox.z, u_bbox.y + ( 1.0 - v_uv.y ) * u_bbox.w ); vec2 luv = vec2( sp.x / u_canvas.x, 1.0 - sp.y / u_canvas.y ); vec4 col = texture2D( u_layer, luv ); float cov = texture2D( u_mask, vec2( v_uv.x, 1.0 - v_uv.y ) ).a; gl_FragColor = col * cov; } "##; // Fragment shader for single-channel glyph textures with color tint. // // Glyphs live in a shared GL_LUMINANCE atlas. `u_uv_scale` and `u_uv_offset` // map the unit quad's (0,0)–(1,1) UV range into the glyph's sub-region: // tex_uv = vec2(v_uv.x, 1 - v_uv.y) * u_uv_scale + u_uv_offset // The Y-flip (`1 - v_uv.y`) corrects for fontdue bitmaps being stored // top-row-first while GL tex coords have V=0 at the bottom. // // The atlas is GL_R8 on ES3 and GL_LUMINANCE on ES2 (see // `gles_render/text.rs` module doc and `GlesCanvas::atlas_format`). // Both formats put the coverage byte in `.r` when sampled, so this // shader is identical for both. pub( super ) const GLYPH_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_sampler; uniform vec4 u_color; uniform float u_opacity; uniform vec2 u_uv_offset; uniform vec2 u_uv_scale; void main() { vec2 uv = vec2(v_uv.x, 1.0 - v_uv.y) * u_uv_scale + u_uv_offset; float coverage = texture2D(u_sampler, uv).r; float a = u_color.a * coverage * u_opacity; gl_FragColor = vec4(u_color.rgb * a, a); } "##; // Batched glyph shader. `a_pos` is pre-transformed into NDC by the // CPU side (we know the surface size) so the shader has no MVP to // apply — that lets us upload one VBO and draw every glyph of the // text run in a single `glDrawArrays`. `a_uv` carries the per-vertex // atlas coordinate ready to sample. pub( super ) const GLYPH_BATCH_VERT_SRC: &str = r#" attribute vec2 a_pos; attribute vec2 a_uv; varying vec2 v_uv; void main() { v_uv = a_uv; gl_Position = vec4(a_pos, 0.0, 1.0); } "#; pub( super ) const GLYPH_BATCH_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_sampler; uniform vec4 u_color; uniform float u_opacity; void main() { float coverage = texture2D(u_sampler, v_uv).r; float a = u_color.a * coverage * u_opacity; gl_FragColor = vec4(u_color.rgb * a, a); } "##; // Vertex shader for the present-blit: maps the unit quad to fullscreen NDC // without going through an MVP. UVs are equal to a_pos so the FBO appears // the same way up on the default framebuffer as it was rendered into the FBO // (both use GL pixel coords with origin at bottom-left). pub( super ) const BLIT_VERT_SRC: &str = r#" attribute vec2 a_pos; varying vec2 v_uv; void main() { v_uv = a_pos; gl_Position = vec4(a_pos * 2.0 - 1.0, 0.0, 1.0); } "#; // Fragment shader for the present-blit: straight texture sample, no opacity. pub( super ) const BLIT_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_sampler; void main() { gl_FragColor = texture2D(u_sampler, v_uv); } "#; // Fragment shader for inter-FBO blits (used by `blit` to composite a sub-canvas // back onto its parent). Reuses `VERT_SRC` so the quad is positioned via // `u_mvp` like any other textured draw. No Y-flip: source and destination are // both FBOs storing content in GL's native bottom-up convention, so sampling // at `v_uv` puts the visually-top row of the source on the top of the dest. // // `u_fade_bottom_px` feathers the bottom edge of the blit: the last // `u_fade_bottom_px` visible rows ramp the source alpha linearly from 1.0 // (at the inner edge of the band) to 0.0 (at the very bottom row), so a // growing viewport does not look like a knife cut against whatever is // behind it. `v_uv.y == 1.0` is the visually-top row of the source (FBO // origin is bottom-left), so `v_uv.y * u_height_px` is the distance in // source pixels from the bottom; the linear ramp falls out of dividing // that by `u_fade_bottom_px` and clamping. With `u_fade_bottom_px == 0` // the divide is skipped and the blit stays hard-edged. Linear (not // smoothstep) because premultiplied output multiplied by a linear alpha // already reads as a soft fade — smoothstep would compress the ramp into // fewer effective rows and reintroduce a faint shoulder. pub( super ) const SUB_BLIT_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_sampler; uniform float u_opacity; uniform float u_fade_bottom_px; uniform float u_height_px; void main() { vec4 c = texture2D(u_sampler, v_uv); float fade = 1.0; if (u_fade_bottom_px > 0.0) { float y_from_bottom = v_uv.y * u_height_px; fade = clamp(y_from_bottom / u_fade_bottom_px, 0.0, 1.0); } gl_FragColor = c * (u_opacity * fade); } "#; // Linear gradient shader. Samples a CPU-baked 1D LUT (`u_lut`, size // `gradient_lut::LUT_SAMPLES × 1`, RGBA8, straight-alpha) along the // CSS linear-gradient convention: the gradient line passes through the // centre of the rect, `0°` points up, positive angles rotate clockwise. // Stop positions outside `[0, 1]` are already baked into the LUT via // linear extrapolation, so the shader only remaps `t` from the extended // domain `[u_lut_domain_min, u_lut_domain_min + u_lut_domain_span]` // into the texture's `[0, 1]` sampling range. // // The same SDF as `RECT_FRAG_SRC` is reused for the rounded-rect / pill // silhouette; the coverage multiplies the fragment's alpha before // premultiplying. pub( super ) const LINEAR_GRADIENT_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_lut; uniform vec2 u_dir; uniform vec2 u_size; uniform float u_line_length; uniform vec4 u_radii; uniform float u_pad; uniform float u_lut_domain_min; uniform float u_lut_domain_span; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad); vec2 c = p - u_size * 0.5; float r = corner_radius(c, u_radii); r = min(r, min(u_size.x, u_size.y) * 0.5); vec2 q = abs(c) - (u_size * 0.5 - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; // 2-px AA band — see RECT_FRAG_SRC for the grid-alignment rationale. float coverage = 1.0 - smoothstep(-1.0, 1.0, d); // Gradient evaluated in rect-local pixel space. `p` already accounts // for `u_pad` (set by the caller to expand the quad for AA), so the // gradient direction stays anchored to the original rect even when // the quad spills outside. float dist = dot(c, u_dir); float t = 0.5 + dist / u_line_length; float t_lut = (t - u_lut_domain_min) / u_lut_domain_span; vec4 grad = texture2D(u_lut, vec2(clamp(t_lut, 0.0, 1.0), 0.5)); float a = grad.a * coverage; gl_FragColor = vec4(grad.rgb * a, a); } "##; // Radial gradient shader. `u_center` is in box-relative fractions // (`[0, 1]` on each axis), `u_radius_frac` is the radial extent in the // same fractional space. `t = distance(v_uv, u_center) / u_radius_frac` // so stops at `position == 1.0` fall exactly at the chosen radius. pub( super ) const RADIAL_GRADIENT_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform sampler2D u_lut; uniform vec2 u_center; uniform float u_radius_frac; uniform vec2 u_size; uniform vec4 u_radii; uniform float u_pad; uniform float u_lut_domain_min; uniform float u_lut_domain_span; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { vec2 p = v_uv * (u_size + 2.0 * vec2(u_pad)) - vec2(u_pad); vec2 c = p - u_size * 0.5; float r = corner_radius(c, u_radii); r = min(r, min(u_size.x, u_size.y) * 0.5); vec2 q = abs(c) - (u_size * 0.5 - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; // 2-px AA band — see RECT_FRAG_SRC for the grid-alignment rationale. float coverage = 1.0 - smoothstep(-1.0, 1.0, d); // Centre/extent in box-relative fractions evaluated against `p` // (which already accounts for `u_pad`), so the radial centre stays // anchored to the original rect when the quad spills outside. vec2 t_uv = p / u_size; float t = distance(t_uv, u_center) / max(u_radius_frac, 1e-6); float t_lut = (t - u_lut_domain_min) / u_lut_domain_span; vec4 grad = texture2D(u_lut, vec2(clamp(t_lut, 0.0, 1.0), 0.5)); float a = grad.a * coverage; gl_FragColor = vec4(grad.rgb * a, a); } "##; // Outer drop shadow shader. // // Rather than baking a blurred shape into an intermediate texture via a // separable Gaussian pass, this uses the analytical soft-shadow // approximation `exp(-d² / (2σ²))` over the rounded-rect SDF. The maths: // // • `d` is the signed distance from the fragment to the (possibly // spread-expanded) shape. `d < 0` is inside, `d > 0` is outside. // • `intensity = 1.0` inside the shape, `exp(-d²/2σ²)` outside. // • `σ = shadow.blur / 2` (CSS blur radius → Gaussian sigma). // // The result is visually indistinguishable from a true Gaussian blur for // small to moderate σ. For very large σ the tail of `exp()` departs from // a real Gaussian, at which point a real two-pass blur or a correction // factor would be needed. // // Running the shadow as a single analytic pass means there's no need for // a per-shadow FBO, no ping-pong, and no framebuffer readback: it is all // one cheap draw call. // Inner (inset) shadow shader. // // Two SDFs cooperate here: // // • `d_outer` — the signed distance to the surface itself. Positive // outside, negative inside. We multiply the final intensity by the // smooth-step coverage of this SDF so the inset never leaks past the // shape's own silhouette. // // • `d_inner` — the signed distance to the shape shifted by // `shadow.offset` and eroded by `shadow.spread`. This is the "hole" // the inset falls into: `d_inner ≥ 0` means the pixel is on the // shadow side of the offset edge (full intensity); `d_inner < 0` // means the pixel is deeper into the unshadowed interior, where the // intensity decays as `exp(-d_inner² / (2σ²))`. // // Together they reproduce the CSS `inset` semantics: the shadow sits // inside the rounded rect, biased toward the side opposite `offset`, // and fades toward the middle with a Gaussian falloff. Premul output // matches the rest of the pipeline. pub( super ) const SHADOW_INSET_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform vec2 u_size; uniform vec2 u_padding; uniform vec4 u_radii; uniform float u_spread; uniform float u_sigma; uniform vec2 u_offset; uniform vec4 u_color; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding; vec2 c = p - u_size * 0.5; // Outer SDF: the shape itself, untransformed. Used to clip the inset // to the silhouette. 2-px AA band — see RECT_FRAG_SRC. vec2 half_outer = u_size * 0.5; float r_outer = corner_radius(c, u_radii); r_outer = min(r_outer, min(half_outer.x, half_outer.y)); vec2 q_outer = abs(c) - (half_outer - vec2(r_outer)); float d_outer = min(max(q_outer.x, q_outer.y), 0.0) + length(max(q_outer, 0.0)) - r_outer; float outer_coverage = 1.0 - smoothstep(-1.0, 1.0, d_outer); // Inner SDF: the shape shifted by offset and eroded by spread. Its // distance drives the Gaussian falloff. The inner radii match the // outer per-corner shape, eroded by `u_spread` (clamped at zero). vec2 p_shifted = p - u_offset; vec2 c_shifted = p_shifted - u_size * 0.5; vec2 half_inner = half_outer - vec2(u_spread); // Degenerate case: spread larger than half the shape erodes it to a // point. Guard so `half_inner` never goes negative. half_inner = max(half_inner, vec2(1e-3)); vec4 inner_radii = max(u_radii - vec4(u_spread), vec4(0.0)); float r_inner = corner_radius(c_shifted, inner_radii); r_inner = min(r_inner, min(half_inner.x, half_inner.y)); vec2 q_inner = abs(c_shifted) - (half_inner - vec2(r_inner)); float d_inner = min(max(q_inner.x, q_inner.y), 0.0) + length(max(q_inner, 0.0)) - r_inner; float intensity; if (d_inner >= 0.0) { intensity = 1.0; } else { float s = max(u_sigma, 0.5); intensity = exp(-(d_inner * d_inner) / (2.0 * s * s)); } intensity *= outer_coverage; float a = u_color.a * intensity; gl_FragColor = vec4(u_color.rgb * a, a); } "##; pub( super ) const SHADOW_OUTER_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform vec2 u_size; uniform vec2 u_padding; uniform vec4 u_radii; uniform float u_spread; uniform float u_sigma; uniform vec4 u_color; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding; vec2 c = p - u_size * 0.5; vec2 half_sz = u_size * 0.5 + vec2(u_spread); // Per-corner shadow radius — outer shape grown uniformly by spread. vec4 r_base = max(u_radii + vec4(u_spread), vec4(0.0)); float r = corner_radius(c, r_base); r = min(r, min(half_sz.x, half_sz.y)); vec2 q = abs(c) - (half_sz - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; float intensity; if (d <= 0.0) { intensity = 1.0; } else { float s = max(u_sigma, 0.5); intensity = exp(-(d * d) / (2.0 * s * s)); } float a = u_color.a * intensity; gl_FragColor = vec4(u_color.rgb * a, a); } "##; // Horizontal Gaussian blur for the backdrop pipeline. Part one of the // separable-Gaussian pair: samples the aux_a snapshot horizontally and // writes to aux_b at the fragment's pixel position. Drawn over a // vertically-extended surface rect so the vertical pass — which reads // aux_b up to ±3σ away from each target pixel — has valid data wherever // it samples. `u_canvas_size` maps `gl_FragCoord` into the aux texture, // and the aux pair is sized to the main FBO so that mapping is trivial. // // Kernel: 41 taps (`RADIUS = 20`), weights computed inline as // `exp(-i²/(2σ²))` and normalized by the sum. At σ ≈ 11 this captures // ~93 % of the Gaussian mass — the remainder is a faint tail that is // visually imperceptible. For σ past ~15 the kernel radius would need // to grow or a downsample pass would be required; everything else in // the pipeline already deals in σ and would not change. pub( super ) const BACKDROP_BLUR_H_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_source; uniform vec2 u_texel; uniform vec2 u_canvas_size; uniform float u_sigma; void main() { const int RADIUS = 20; vec2 uv = gl_FragCoord.xy / u_canvas_size; vec4 total = vec4(0.0); float total_w = 0.0; for (int i = -RADIUS; i <= RADIUS; i++) { float fi = float(i); float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma)); total += texture2D(u_source, uv + fi * u_texel) * w; total_w += w; } gl_FragColor = total / total_w; } "#; // Vertical Gaussian blur + SDF clip + tint, the last pass of the // backdrop pipeline. Runs on a quad matching the surface rect and // writes to the main FBO. // // Per fragment: (1) computes the rounded-rect SDF coverage just like // the rect shader so the backdrop is clipped to the surface shape with // a 1-pixel anti-aliased edge; (2) samples `u_source` (the H-blurred // aux_b) vertically with the same 41-tap Gaussian as the H pass; (3) // applies the optional `u_tint` over the blurred sample using standard // premul-over math; (4) outputs premultiplied with `alpha = coverage`. // // Alpha handling. The output is `(result_rgb * coverage, coverage)` — // not `(result_rgb * coverage * result_a, coverage * result_a)`. The // snapshot holds premultiplied content that is effectively opaque // inside the canvas (every pixel has been written by some draw, even // if that draw was `clear(0,0,0,0)`; the FBO is never sampled outside // the canvas bounds). Treating the blurred sample's alpha as 1.0 at // output time means the composite pass REPLACES the base pixel inside // the surface shape with the tinted blurred content, rather than // alpha-blending on top of it. That is the correct semantics for // `backdrop-filter`: you want the original content to disappear // entirely where the surface covers it, replaced by the blurred // version. // // `fill_backdrop` runs this before outer shadows / fill / insets so // later passes composite on top of the blurred backdrop. pub( super ) const BACKDROP_COMPOSITE_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_source; uniform vec2 u_canvas_size; uniform vec2 u_texel; uniform float u_sigma; uniform vec2 u_size; uniform vec2 u_padding; uniform vec4 u_radii; uniform vec4 u_tint; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { const int RADIUS = 20; // Rounded-rect SDF clip. 2-px AA band — see RECT_FRAG_SRC. vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding; vec2 c = p - u_size * 0.5; vec2 half_sz = u_size * 0.5; float r = corner_radius(c, u_radii); r = min(r, min(half_sz.x, half_sz.y)); vec2 q = abs(c) - (half_sz - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; float coverage = 1.0 - smoothstep(-1.0, 1.0, d); if (coverage <= 0.0) { discard; } // Vertical Gaussian on the H-blurred source. vec2 uv = gl_FragCoord.xy / u_canvas_size; vec4 total = vec4(0.0); float total_w = 0.0; for (int i = -RADIUS; i <= RADIUS; i++) { float fi = float(i); float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma)); total += texture2D(u_source, uv + fi * u_texel) * w; total_w += w; } vec4 blurred = total / total_w; // Optional tint, "tint over blurred" in premul space. vec3 tint_premul = u_tint.rgb * u_tint.a; vec3 result_rgb = tint_premul + blurred.rgb * (1.0 - u_tint.a); gl_FragColor = vec4(result_rgb * coverage, coverage); } "#; // ── Fast (low-quality) variants of the backdrop shaders ──────────────── // // Same separable-Gaussian pipeline as the full-quality pair above, but // with `RADIUS = 4` instead of `RADIUS = 20`. That cuts each pass from // 41 taps to 9 — a ~4.5× reduction in fragment-shader work — and // shrinks the snapshot region the renderer has to copy from the main // FBO from `target ± 21` to `target ± 5` pixels. Used during motion // via [`super::low_quality_paint`]; the static frame is painted with // the full-quality pair. // // `u_sigma` is still a uniform so the CPU side can clamp it to a // value compatible with the smaller kernel (typically ≤ 2.0), keeping // the kernel a sensible Gaussian rather than a sharply truncated one. // Visually this means a thinner blur band during motion, which fades // back to the full blur on the static frame. pub( super ) const BACKDROP_FAST_BLUR_H_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_source; uniform vec2 u_texel; uniform vec2 u_canvas_size; uniform float u_sigma; void main() { const int RADIUS = 4; vec2 uv = gl_FragCoord.xy / u_canvas_size; vec4 total = vec4(0.0); float total_w = 0.0; for (int i = -RADIUS; i <= RADIUS; i++) { float fi = float(i); float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma)); total += texture2D(u_source, uv + fi * u_texel) * w; total_w += w; } gl_FragColor = total / total_w; } "#; pub( super ) const BACKDROP_FAST_COMPOSITE_FRAG_SRC: &str = r#" precision mediump float; varying vec2 v_uv; uniform sampler2D u_source; uniform vec2 u_canvas_size; uniform vec2 u_texel; uniform float u_sigma; uniform vec2 u_size; uniform vec2 u_padding; uniform vec4 u_radii; uniform vec4 u_tint; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { const int RADIUS = 4; // Rounded-rect SDF clip — identical to the full-quality variant. vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding; vec2 c = p - u_size * 0.5; vec2 half_sz = u_size * 0.5; float r = corner_radius(c, u_radii); r = min(r, min(half_sz.x, half_sz.y)); vec2 q = abs(c) - (half_sz - vec2(r)); float d = min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r; float coverage = 1.0 - smoothstep(-1.0, 1.0, d); if (coverage <= 0.0) { discard; } vec2 uv = gl_FragCoord.xy / u_canvas_size; vec4 total = vec4(0.0); float total_w = 0.0; for (int i = -RADIUS; i <= RADIUS; i++) { float fi = float(i); float w = exp(-(fi * fi) / (2.0 * u_sigma * u_sigma)); total += texture2D(u_source, uv + fi * u_texel) * w; total_w += w; } vec4 blurred = total / total_w; vec3 tint_premul = u_tint.rgb * u_tint.a; vec3 result_rgb = tint_premul + blurred.rgb * (1.0 - u_tint.a); gl_FragColor = vec4(result_rgb * coverage, coverage); } "#; // Inset shadow with CSS `Overlay` blend. Uses the same SDF dance as // `SHADOW_INSET_FRAG_SRC` to compute `intensity` (outer-silhouette clip // + Gaussian falloff from the offset-shifted inner SDF), then samples // the snapshotted FBO content under the fragment and applies the // per-channel Overlay formula: // // overlay(base, src) = base < 0.5 ? 2 * base * src // : 1 - 2 * (1 - base) * (1 - src) // // Output is premultiplied with `mask = u_color.a * intensity` — so the // standard `(ONE, ONE_MINUS_SRC_ALPHA)` blend on top of the main FBO // yields `result = overlay_rgb * mask + base * (1 - mask)`, i.e. the // Overlay effect modulated by the shadow mask, composed onto the // original backdrop. The snapshot texture holds premultiplied content // matching the main FBO, so we unpremultiply before applying Overlay. // // `u_canvas_size` is the main FBO's pixel size. `gl_FragCoord` has its // origin at bottom-left in GLES, which matches the FBO's native // orientation, so `gl_FragCoord.xy / u_canvas_size` gives the texture // coordinates of the pixel we're about to write to. No Y-flip needed. pub( super ) const SHADOW_INSET_OVERLAY_FRAG_SRC: &str = r##" precision mediump float; varying vec2 v_uv; uniform vec2 u_size; uniform vec2 u_padding; uniform vec4 u_radii; uniform float u_spread; uniform float u_sigma; uniform vec2 u_offset; uniform vec4 u_color; uniform sampler2D u_snapshot; uniform vec2 u_canvas_size; // Per-fragment corner radius lookup — see RECT_FRAG_SRC for the // quadrant convention. float corner_radius(vec2 c, vec4 r) { float top = (c.x < 0.0) ? r.x : r.y; float bottom = (c.x < 0.0) ? r.w : r.z; return (c.y > 0.0) ? top : bottom; } void main() { vec2 p = v_uv * (u_size + 2.0 * u_padding) - u_padding; vec2 c = p - u_size * 0.5; // Outer silhouette clip — same as SHADOW_INSET_FRAG_SRC, 2-px AA band. vec2 half_outer = u_size * 0.5; float r_outer = corner_radius(c, u_radii); r_outer = min(r_outer, min(half_outer.x, half_outer.y)); vec2 q_outer = abs(c) - (half_outer - vec2(r_outer)); float d_outer = min(max(q_outer.x, q_outer.y), 0.0) + length(max(q_outer, 0.0)) - r_outer; float outer_coverage = 1.0 - smoothstep(-1.0, 1.0, d_outer); // Offset-shifted inner SDF → Gaussian intensity. Per-corner inner // radii match the outer shape eroded by `u_spread`. vec2 p_shifted = p - u_offset; vec2 c_shifted = p_shifted - u_size * 0.5; vec2 half_inner = half_outer - vec2(u_spread); half_inner = max(half_inner, vec2(1e-3)); vec4 inner_radii = max(u_radii - vec4(u_spread), vec4(0.0)); float r_inner = corner_radius(c_shifted, inner_radii); r_inner = min(r_inner, min(half_inner.x, half_inner.y)); vec2 q_inner = abs(c_shifted) - (half_inner - vec2(r_inner)); float d_inner = min(max(q_inner.x, q_inner.y), 0.0) + length(max(q_inner, 0.0)) - r_inner; float intensity; if (d_inner >= 0.0) { intensity = 1.0; } else { float s = max(u_sigma, 0.5); intensity = exp(-(d_inner * d_inner) / (2.0 * s * s)); } intensity *= outer_coverage; float mask = u_color.a * intensity; // Sample the snapshot at the fragment's FBO position. The snapshot // holds premultiplied content; divide by alpha before applying the // straight-alpha Overlay formula. Guard alpha=0 to avoid NaNs. vec2 snap_uv = gl_FragCoord.xy / u_canvas_size; vec4 snap = texture2D(u_snapshot, snap_uv); vec3 base = snap.a > 0.0 ? snap.rgb / snap.a : vec3(0.0); vec3 src = u_color.rgb; // Per-channel Overlay. `step(0.5, base)` yields 0 where base < 0.5 // and 1 otherwise; `mix` picks multiply vs screen accordingly. vec3 multiply = 2.0 * base * src; vec3 screen = 1.0 - 2.0 * (1.0 - base) * (1.0 - src); vec3 overlay = mix(multiply, screen, step(0.5, base)); // Output premul: `(overlay * mask, mask)`. Combined with the premul // over blend this replaces the base by `overlay` wherever mask == 1 // and leaves it untouched where mask == 0. gl_FragColor = vec4(overlay * mask, mask); } "##;