Skip to main content

aetna_core/paint/
shader.rs

1//! Shader handles, uniform values, and bindings.
2//!
3//! ## Stock shader source
4//!
5//! WGSL source for stock shaders is exposed under [`stock_wgsl`] so
6//! backend crates can `include_str!`-equivalent it without reaching
7//! across crate directories. See `crates/aetna-core/shaders/`.
8//!
9//! Sits between the grammar layer (where users write `.fill(c)`,
10//! `.radius(r)`) and the renderer (which consumes typed [`crate::DrawOp`]s with
11//! shader handles + uniform blocks).
12//!
13//! The grammar layer doesn't speak shaders directly. The renderer walks
14//! the tree and constructs a [`ShaderBinding`] per visual fact, defaulting
15//! to a stock shader (e.g. [`StockShader::RoundedRect`] for rect-shaped
16//! surfaces). A user crate can override that default by setting
17//! [`crate::tree::El::shader_override`].
18//!
19//! Stock shaders are pre-compiled wgsl modules shipped with the crate.
20//! Custom shaders are user-registered wgsl source identified by name.
21//! The SVG fallback renderer interprets stock shaders best-effort and
22//! emits placeholder rects for custom ones.
23//!
24//! See `docs/SHADER_VISION.md` for the rendering-layer contract.
25//!
26//! # Uniform packing
27//!
28//! [`UniformBlock`] is a `BTreeMap` keyed by `&'static str` for stable
29//! iteration order — important so that the bundle's
30//! `shader_manifest.txt` artifact is deterministic and grep-friendly.
31//! Backend runners pack the block to the target GPU ABI using their
32//! per-shader layout metadata. Bundle/SVG paths consume the typed map
33//! directly when producing diagnostics.
34//!
35//! # Stock-shader status
36//!
37//! Focus indicators ride on each focusable node's own `RoundedRect` quad via
38//! `focus_color`/`focus_width` uniforms. Most surface variation should remain
39//! uniform/theme driven rather than creating more stock shaders.
40
41use std::collections::BTreeMap;
42
43use crate::tree::Color;
44
45/// Where a draw op's pixels come from.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
47pub enum ShaderHandle {
48    Stock(StockShader),
49    /// User-registered shader. The string is the name passed to the backend
50    /// runner at host-integration time.
51    Custom(&'static str),
52}
53
54impl ShaderHandle {
55    pub fn name(&self) -> String {
56        match self {
57            ShaderHandle::Stock(s) => s.name().to_string(),
58            ShaderHandle::Custom(n) => format!("custom::{n}"),
59        }
60    }
61}
62
63/// Shipped shader inventory. See `docs/SHADER_VISION.md` for the shader model.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub enum StockShader {
66    /// Flat colored rect. Fallback / debug.
67    SolidQuad,
68    /// Fill + stroke + radius + shadow + focus ring. The workhorse —
69    /// handles ~80% of UI surfaces. Focus indicator is a uniform on
70    /// this shader, not a separate pipeline (see `widget_kit.md`).
71    RoundedRect,
72    /// Alpha-mask glyph rendering. Backends sample per-glyph bitmaps
73    /// from a [`crate::text::atlas::GlyphAtlas`] page texture and tint
74    /// by per-glyph color. The historical `TextSdf` name was aspirational;
75    /// the actual rasterization is alpha-coverage via swash.
76    Text,
77    /// Antialiased 1px line.
78    DividerLine,
79    /// Per-image raster sampling. Backend binds a per-image texture at
80    /// group 1 and the fragment shader composes `sampled * tint` with
81    /// rounded-corner AA. See `crate::image::Image` for the data side.
82    Image,
83    /// Indeterminate loading spinner — circular SDF arc swept around a
84    /// dim track, animated by `frame.time`. Continuous: any node bound
85    /// to this shader keeps `needs_redraw` set so the host idle loop
86    /// keeps ticking.
87    Spinner,
88    /// Pulsing loading placeholder — a rounded rect with a cosine
89    /// alpha breathe (0.5 → 1.0 → 0.5 over 2 s by default) matching
90    /// shadcn's `animate-pulse`. Continuous.
91    Skeleton,
92    /// Indeterminate linear progress — a track with a small bar
93    /// sliding left-to-right on loop, for in-line "still working…"
94    /// feedback when no completion ratio is known. Continuous.
95    ProgressIndeterminate,
96}
97
98impl StockShader {
99    pub fn name(self) -> &'static str {
100        match self {
101            StockShader::SolidQuad => "stock::solid_quad",
102            StockShader::RoundedRect => "stock::rounded_rect",
103            StockShader::Text => "stock::text",
104            StockShader::DividerLine => "stock::divider_line",
105            StockShader::Image => "stock::image",
106            StockShader::Spinner => "stock::spinner",
107            StockShader::Skeleton => "stock::skeleton",
108            StockShader::ProgressIndeterminate => "stock::progress_indeterminate",
109        }
110    }
111
112    /// Whether this shader's output depends on `frame.time`, i.e. the
113    /// host must keep redrawing for it to animate. Read by
114    /// [`crate::runtime::RunnerCore::prepare_layout`] when computing
115    /// `needs_redraw`.
116    pub fn is_continuous(self) -> bool {
117        matches!(
118            self,
119            StockShader::Spinner | StockShader::Skeleton | StockShader::ProgressIndeterminate
120        )
121    }
122}
123
124/// A single uniform's value. Keep small and concrete; this is the wire
125/// format between the grammar layer and the renderer.
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub enum UniformValue {
128    F32(f32),
129    Vec2([f32; 2]),
130    Vec4([f32; 4]),
131    Color(Color),
132    Bool(bool),
133}
134
135impl UniformValue {
136    /// Compact form for tree dump / shader manifest.
137    pub fn debug_short(&self) -> String {
138        match self {
139            UniformValue::F32(v) => format!("{v:.2}"),
140            UniformValue::Vec2([x, y]) => format!("({x:.2},{y:.2})"),
141            UniformValue::Vec4([x, y, z, w]) => format!("({x:.2},{y:.2},{z:.2},{w:.2})"),
142            UniformValue::Color(c) => match c.token {
143                Some(name) => name.to_string(),
144                None => format!("rgba({},{},{},{})", c.r, c.g, c.b, c.a),
145            },
146            UniformValue::Bool(b) => b.to_string(),
147        }
148    }
149}
150
151/// Named uniform values for a single draw. `BTreeMap` for deterministic
152/// iteration in artifacts.
153pub type UniformBlock = BTreeMap<&'static str, UniformValue>;
154
155/// A shader handle plus the uniforms to bind for one draw.
156#[derive(Clone, Debug)]
157pub struct ShaderBinding {
158    pub handle: ShaderHandle,
159    pub uniforms: UniformBlock,
160}
161
162impl ShaderBinding {
163    pub fn stock(shader: StockShader) -> Self {
164        Self {
165            handle: ShaderHandle::Stock(shader),
166            uniforms: UniformBlock::new(),
167        }
168    }
169    pub fn custom(name: &'static str) -> Self {
170        Self {
171            handle: ShaderHandle::Custom(name),
172            uniforms: UniformBlock::new(),
173        }
174    }
175    pub fn with(mut self, key: &'static str, value: UniformValue) -> Self {
176        self.uniforms.insert(key, value);
177        self
178    }
179    pub fn set(&mut self, key: &'static str, value: UniformValue) {
180        self.uniforms.insert(key, value);
181    }
182
183    // Typed sugar for the common cases — saves the user from typing
184    // `UniformValue::Color(...)` at every call site.
185
186    pub fn color(self, key: &'static str, c: Color) -> Self {
187        self.with(key, UniformValue::Color(c))
188    }
189    pub fn f32(self, key: &'static str, v: f32) -> Self {
190        self.with(key, UniformValue::F32(v))
191    }
192    pub fn vec4(self, key: &'static str, v: [f32; 4]) -> Self {
193        self.with(key, UniformValue::Vec4(v))
194    }
195}
196
197/// WGSL source for stock shaders. Backend crates compile these into
198/// pipelines; the source lives here so the asset shipping is centralised.
199pub mod stock_wgsl {
200    pub const ROUNDED_RECT: &str = include_str!("../../shaders/rounded_rect.wgsl");
201    pub const TEXT: &str = include_str!("../../shaders/text.wgsl");
202    pub const TEXT_MSDF: &str = include_str!("../../shaders/text_msdf.wgsl");
203    pub const TEXT_HIGHLIGHT: &str = include_str!("../../shaders/text_highlight.wgsl");
204    pub const ICON_LINE: &str = include_str!("../../shaders/icon_line.wgsl");
205    pub const VECTOR: &str = include_str!("../../shaders/vector.wgsl");
206    pub const VECTOR_RELIEF: &str = include_str!("../../shaders/vector_relief.wgsl");
207    pub const VECTOR_GLASS: &str = include_str!("../../shaders/vector_glass.wgsl");
208    pub const IMAGE: &str = include_str!("../../shaders/image.wgsl");
209    pub const SURFACE: &str = include_str!("../../shaders/surface.wgsl");
210    pub const SPINNER: &str = include_str!("../../shaders/spinner.wgsl");
211    pub const SKELETON: &str = include_str!("../../shaders/skeleton.wgsl");
212    pub const PROGRESS_INDETERMINATE: &str =
213        include_str!("../../shaders/progress_indeterminate.wgsl");
214}