pub mod draw_ops;
pub mod ir;
pub mod shader;
pub mod shadow;
pub mod slots;
pub mod surface;
#[doc = include_str!("shader_contract.md")]
pub mod shader_contract {}
use bytemuck::{Pod, Zeroable};
use crate::tree::{Color, Rect};
use crate::vector::IconMaterial;
use shader::{ShaderHandle, StockShader, UniformBlock, UniformValue};
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable, Debug)]
pub struct QuadInstance {
pub rect: [f32; 4],
pub slot_a: [f32; 4],
pub slot_b: [f32; 4],
pub slot_c: [f32; 4],
pub inner_rect: [f32; 4],
pub slot_d: [f32; 4],
pub slot_e: [f32; 4],
}
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable, Debug)]
pub struct IconInstance {
pub rect: [f32; 4],
pub line: [f32; 4],
pub color: [f32; 4],
pub params: [f32; 4],
}
#[derive(Clone, Copy)]
pub struct InstanceRun {
pub handle: ShaderHandle,
pub scissor: Option<PhysicalScissor>,
pub first: u32,
pub count: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IconRunKind {
Tess,
Msdf,
}
#[derive(Clone, Copy)]
pub struct IconRun {
pub kind: IconRunKind,
pub scissor: Option<PhysicalScissor>,
pub first: u32,
pub count: u32,
pub page: u32,
pub material: IconMaterial,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PhysicalScissor {
pub x: u32,
pub y: u32,
pub w: u32,
pub h: u32,
}
#[derive(Clone, Copy)]
pub enum PaintItem {
QuadRun(usize),
IconRun(usize),
Text(usize),
Image(usize),
AppTexture(usize),
Vector(usize),
Scene3D(usize),
BackdropSnapshot,
}
pub fn close_run(
runs: &mut Vec<InstanceRun>,
paint_items: &mut Vec<PaintItem>,
run_key: Option<(ShaderHandle, Option<PhysicalScissor>)>,
first: u32,
end: u32,
) {
if let Some((handle, scissor)) = run_key {
let count = end - first;
if count > 0 {
let index = runs.len();
runs.push(InstanceRun {
handle,
scissor,
first,
count,
});
paint_items.push(PaintItem::QuadRun(index));
}
}
}
pub fn physical_scissor(
scissor: Option<Rect>,
scale: f32,
viewport_px: (u32, u32),
) -> Option<PhysicalScissor> {
let r = scissor?;
let x1 = (r.x * scale).floor().clamp(0.0, viewport_px.0 as f32) as u32;
let y1 = (r.y * scale).floor().clamp(0.0, viewport_px.1 as f32) as u32;
let x2 = (r.right() * scale).ceil().clamp(0.0, viewport_px.0 as f32) as u32;
let y2 = (r.bottom() * scale).ceil().clamp(0.0, viewport_px.1 as f32) as u32;
Some(PhysicalScissor {
x: x1,
y: y1,
w: x2.saturating_sub(x1),
h: y2.saturating_sub(y1),
})
}
pub fn pack_instance(rect: Rect, shader: ShaderHandle, uniforms: &UniformBlock) -> QuadInstance {
pack_instance_in(rect, shader, uniforms, DEFAULT_WORKING_COLOR_SPACE)
}
pub fn pack_instance_in(
rect: Rect,
shader: ShaderHandle,
uniforms: &UniformBlock,
working: crate::color::ColorSpace,
) -> QuadInstance {
let rect_arr = [rect.x, rect.y, rect.w, rect.h];
let inner_rect = uniforms
.get("inner_rect")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or(rect_arr);
let to_lin = |c: Color| rgba_f32_in(c, working);
match shader {
ShaderHandle::Stock(StockShader::RoundedRect) => {
let radii = uniforms.get("radii").map(|v| value_to_vec4_in(v, working));
let scalar_radius = uniforms.get("radius").and_then(as_f32).unwrap_or(0.0);
let radii = radii.unwrap_or([scalar_radius; 4]);
let max_radius = radii[0].max(radii[1]).max(radii[2]).max(radii[3]);
QuadInstance {
rect: rect_arr,
inner_rect,
slot_a: uniforms
.get("fill")
.and_then(as_color)
.map(to_lin)
.unwrap_or([0.0; 4]),
slot_b: uniforms
.get("stroke")
.and_then(as_color)
.map(to_lin)
.unwrap_or([0.0; 4]),
slot_c: [
uniforms.get("stroke_width").and_then(as_f32).unwrap_or(0.0),
max_radius,
uniforms.get("shadow").and_then(as_f32).unwrap_or(0.0),
uniforms.get("focus_width").and_then(as_f32).unwrap_or(0.0),
],
slot_d: uniforms
.get("focus_color")
.and_then(as_color)
.map(to_lin)
.unwrap_or([0.0; 4]),
slot_e: radii,
}
}
_ => QuadInstance {
rect: rect_arr,
inner_rect,
slot_a: uniforms
.get("vec_a")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or([0.0; 4]),
slot_b: uniforms
.get("vec_b")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or([0.0; 4]),
slot_c: uniforms
.get("vec_c")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or([0.0; 4]),
slot_d: uniforms
.get("vec_d")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or([0.0; 4]),
slot_e: uniforms
.get("vec_e")
.map(|v| value_to_vec4_in(v, working))
.unwrap_or([0.0; 4]),
},
}
}
pub fn pack_instance_named(
rect: Rect,
uniforms: &UniformBlock,
working: crate::color::ColorSpace,
slot_map: &slots::ShaderSlotMap,
unknown: &mut Vec<&'static str>,
) -> QuadInstance {
let rect_arr = [rect.x, rect.y, rect.w, rect.h];
let mut inst = QuadInstance {
rect: rect_arr,
inner_rect: rect_arr,
slot_a: [0.0; 4],
slot_b: [0.0; 4],
slot_c: [0.0; 4],
slot_d: [0.0; 4],
slot_e: [0.0; 4],
};
for (key, value) in uniforms {
if *key == "inner_rect" {
inst.inner_rect = value_to_vec4_in(value, working);
continue;
}
let location = slots::SLOT_ALIASES
.iter()
.position(|alias| alias == key)
.map(|i| slots::FREE_SLOT_LOCATIONS[i])
.or_else(|| slot_map.location_of(key));
let slot = location.and_then(slots::slot_index_for_location);
let Some(slot) = slot else {
unknown.push(key);
continue;
};
let packed = value_to_vec4_in(value, working);
match slot {
0 => inst.slot_a = packed,
1 => inst.slot_b = packed,
2 => inst.slot_c = packed,
3 => inst.slot_d = packed,
_ => inst.slot_e = packed,
}
}
inst
}
fn as_color(v: &UniformValue) -> Option<Color> {
match v {
UniformValue::Color(c) => Some(*c),
_ => None,
}
}
fn as_f32(v: &UniformValue) -> Option<f32> {
match v {
UniformValue::F32(f) => Some(*f),
_ => None,
}
}
fn value_to_vec4_in(v: &UniformValue, working: crate::color::ColorSpace) -> [f32; 4] {
match v {
UniformValue::Color(c) => rgba_f32_in(*c, working),
UniformValue::Vec4(a) => *a,
UniformValue::Vec2([x, y]) => [*x, *y, 0.0, 0.0],
UniformValue::F32(f) => [*f, 0.0, 0.0, 0.0],
UniformValue::Bool(b) => [if *b { 1.0 } else { 0.0 }, 0.0, 0.0, 0.0],
}
}
pub const DEFAULT_WORKING_COLOR_SPACE: crate::color::ColorSpace =
crate::color::ColorSpace::SRGB_LINEAR;
pub fn rgba_f32(c: Color) -> [f32; 4] {
rgba_f32_in(c, DEFAULT_WORKING_COLOR_SPACE)
}
pub fn rgba_f32_in(c: Color, working: crate::color::ColorSpace) -> [f32; 4] {
let lin = c.convert_to(working);
[lin.r, lin.g, lin.b, lin.a]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shader::UniformBlock;
use crate::tokens;
#[test]
fn focus_uniforms_pack_into_rounded_rect_slots() {
let mut uniforms = UniformBlock::new();
uniforms.insert(
"fill",
UniformValue::Color(Color::srgb_u8a(40, 40, 40, 255)),
);
uniforms.insert("radius", UniformValue::F32(8.0));
uniforms.insert("focus_color", UniformValue::Color(tokens::RING));
uniforms.insert("focus_width", UniformValue::F32(tokens::RING_WIDTH));
let inst = pack_instance(
Rect::new(1.0, 2.0, 30.0, 40.0),
ShaderHandle::Stock(StockShader::RoundedRect),
&uniforms,
);
assert_eq!(inst.rect, [1.0, 2.0, 30.0, 40.0]);
assert_eq!(
inst.inner_rect, inst.rect,
"no inner_rect uniform → fall back to painted rect"
);
assert_eq!(
inst.slot_c[1], 8.0,
"max corner radius in slot_c.y (uniform corners derived from scalar `radius` uniform)"
);
assert_eq!(
inst.slot_e,
[8.0, 8.0, 8.0, 8.0],
"scalar `radius` uniform fills all four corners on slot_e"
);
assert_eq!(
inst.slot_c[3],
tokens::RING_WIDTH,
"focus_width in slot_c.w"
);
assert!(inst.slot_d[3] > 0.0, "focus_color alpha should be visible");
}
#[test]
fn per_corner_radii_uniform_routes_to_slot_e() {
let mut uniforms = UniformBlock::new();
uniforms.insert(
"fill",
UniformValue::Color(Color::srgb_u8a(40, 40, 40, 255)),
);
uniforms.insert("radii", UniformValue::Vec4([12.0, 12.0, 0.0, 0.0]));
uniforms.insert("radius", UniformValue::F32(12.0));
let inst = pack_instance(
Rect::new(0.0, 0.0, 100.0, 40.0),
ShaderHandle::Stock(StockShader::RoundedRect),
&uniforms,
);
assert_eq!(inst.slot_e, [12.0, 12.0, 0.0, 0.0]);
assert_eq!(inst.slot_c[1], 12.0, "max corner radius -> slot_c.y");
}
#[test]
fn physical_scissor_converts_logical_to_physical_pixels() {
let scissor = physical_scissor(Some(Rect::new(10.2, 20.2, 30.2, 40.2)), 2.0, (200, 200))
.expect("scissor");
assert_eq!(
scissor,
PhysicalScissor {
x: 20,
y: 40,
w: 61,
h: 81
}
);
}
#[test]
fn pack_instance_named_routes_by_wgsl_name_and_reports_drift() {
use crate::paint::shader::ShaderBinding;
use crate::paint::slots::ShaderSlotMap;
let slot_map = ShaderSlotMap::from_pairs([
("xyz_to_rgb_c0", 2u32),
("xyz_to_rgb_c1", 3),
("xyz_to_rgb_c2", 4),
("view_bounds", 6),
]);
let m = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
let binding = ShaderBinding::custom("chroma_field")
.mat3(["xyz_to_rgb_c0", "xyz_to_rgb_c1", "xyz_to_rgb_c2"], m)
.vec4("view_bounds", [0.0, 0.0, 0.8, 0.9])
.vec4("vec_e", [1.0, 1.0, 1.0, 1.0]) .f32("typo_bounds", 1.0);
let mut unknown = Vec::new();
let inst = pack_instance_named(
Rect::new(0.0, 0.0, 10.0, 10.0),
&binding.uniforms,
DEFAULT_WORKING_COLOR_SPACE,
&slot_map,
&mut unknown,
);
assert_eq!(inst.slot_a, [1.0, 2.0, 3.0, 0.0]);
assert_eq!(inst.slot_b, [4.0, 5.0, 6.0, 0.0]);
assert_eq!(inst.slot_c, [7.0, 8.0, 9.0, 0.0]);
assert_eq!(inst.slot_d, [0.0, 0.0, 0.8, 0.9]);
assert_eq!(inst.slot_e, [1.0, 1.0, 1.0, 1.0]);
assert_eq!(unknown, vec!["typo_bounds"], "drift is reported");
}
}