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}