use wgpu::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode};
pub const NO_CLIP_RECT: [f32; 4] = [0.0, 0.0, -1.0, -1.0];
pub const EMPTY_CLIP_RECT: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
pub const SHARPEST_SHADOW_BLUR: f32 = 1e-3;
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ShaderUniforms {
pub screen_size: [f32; 2],
pub scale_factor: f32,
pub _pad: f32,
}
impl ShaderUniforms {
pub fn new(screen_width: f32, screen_height: f32, scale_factor: f32) -> Self {
Self {
screen_size: [screen_width, screen_height],
scale_factor,
_pad: 0.0,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct QuadVertex {
pub position: [f32; 2],
}
impl QuadVertex {
pub fn desc() -> VertexBufferLayout<'static> {
VertexBufferLayout {
array_stride: std::mem::size_of::<QuadVertex>() as u64,
step_mode: VertexStepMode::Vertex,
attributes: &[VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
}],
}
}
}
pub const QUAD_VERTICES: &[QuadVertex] = &[
QuadVertex {
position: [0.0, 0.0],
}, QuadVertex {
position: [1.0, 0.0],
}, QuadVertex {
position: [0.0, 1.0],
}, QuadVertex {
position: [1.0, 1.0],
}, ];
pub const QUAD_INDICES: &[u16] = &[
0, 1, 2, 1, 3, 2, ];
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ShapeInstance {
pub rect: [f32; 4],
pub corner_radii: [f32; 4],
pub fill_color: [f32; 4],
pub border_color: [f32; 4],
pub shadow_color: [f32; 4],
pub shadow_offset: [f32; 2],
pub shadow_blur: f32,
pub shadow_spread: f32,
pub transform: [f32; 6],
pub _pad_transform: [f32; 2],
pub clip_rect: [f32; 4],
pub clip_radii: [f32; 4],
pub clip_inverse: [f32; 6],
pub clip_curvature: f32,
pub clip_chain: u32,
pub gradient_start: [f32; 4],
pub gradient_end: [f32; 4],
pub border_width: f32,
pub shape_curvature: f32,
pub gradient_type: u32,
pub _pad_tail: u32,
}
impl Default for ShapeInstance {
fn default() -> Self {
Self {
rect: [0.0, 0.0, 0.0, 0.0],
corner_radii: [0.0; 4],
fill_color: [0.0, 0.0, 0.0, 0.0],
border_color: [0.0, 0.0, 0.0, 0.0],
shadow_color: [0.0, 0.0, 0.0, 0.0],
shadow_offset: [0.0, 0.0],
shadow_blur: 0.0,
shadow_spread: 0.0,
transform: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], _pad_transform: [0.0, 0.0],
clip_rect: NO_CLIP_RECT,
clip_radii: [0.0; 4],
clip_inverse: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0], clip_curvature: 1.0,
clip_chain: super::clip::NO_CHAIN,
gradient_start: [0.0, 0.0, 0.0, 0.0],
gradient_end: [0.0, 0.0, 0.0, 0.0],
border_width: 0.0,
shape_curvature: 1.0,
gradient_type: 0, _pad_tail: 0,
}
}
}
impl ShapeInstance {
pub fn from_rect(
rect: [f32; 4],
fill_color: [f32; 4],
corner_radii: [f32; 4],
curvature: f32,
) -> Self {
Self {
rect,
corner_radii,
shape_curvature: curvature,
fill_color,
..Default::default()
}
}
pub fn with_transform(mut self, transform: &crate::transform::Transform, scale: f32) -> Self {
if !transform.is_identity() {
self.transform = transform.data;
self.transform[2] *= scale;
self.transform[5] *= scale;
}
self
}
#[inline]
pub fn with_clip(mut self, resolved: &super::clip::ResolvedClip, scale: f32) -> Self {
let clip = &resolved.first;
self.clip_chain = resolved.rest;
let Some(to_clip) = clip.to_local(scale) else {
self.clip_rect = EMPTY_CLIP_RECT;
return self;
};
self.clip_rect = [clip.rect.x, clip.rect.y, clip.rect.width, clip.rect.height];
self.clip_radii = clip.radii.to_array();
self.clip_curvature = clip.curvature;
self.clip_inverse = to_clip.data;
self
}
pub fn with_border(mut self, border: &super::commands::Border, scale: f32) -> Self {
self.border_width = border.width * scale;
self.border_color = [
border.color.r,
border.color.g,
border.color.b,
border.color.a,
];
self
}
pub fn with_shadow(mut self, shadow: &super::types::Shadow, scale: f32) -> Self {
let scaled = shadow.scaled(scale);
self.shadow_offset = [scaled.offset.0, scaled.offset.1];
self.shadow_blur = scaled.blur.max(SHARPEST_SHADOW_BLUR);
self.shadow_spread = scaled.spread;
self.shadow_color = [
scaled.color.r,
scaled.color.g,
scaled.color.b,
scaled.color.a,
];
self
}
pub fn with_gradient(mut self, gradient: &super::types::Gradient) -> Self {
self.gradient_start = [
gradient.start_color.r,
gradient.start_color.g,
gradient.start_color.b,
gradient.start_color.a,
];
self.gradient_end = [
gradient.end_color.r,
gradient.end_color.g,
gradient.end_color.b,
gradient.end_color.a,
];
self.gradient_type = match gradient.direction {
super::types::GradientDir::Horizontal => 1,
super::types::GradientDir::Vertical => 2,
super::types::GradientDir::Diagonal => 3,
super::types::GradientDir::DiagonalReverse => 4,
};
self
}
pub fn faded(mut self, opacity: f32) -> Self {
for colour in [
&mut self.fill_color,
&mut self.border_color,
&mut self.shadow_color,
&mut self.gradient_start,
&mut self.gradient_end,
] {
colour[3] *= opacity;
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_shadow_reaches_the_instance_buffer_scaled_but_not_faded() {
let shadow = super::super::types::Shadow::new(
(3.0, -4.0),
8.0,
2.0,
crate::widgets::Color::rgba(1.0, 0.0, 0.0, 0.5),
);
let i = ShapeInstance::default().with_shadow(&shadow, 2.0);
assert_eq!(i.shadow_offset, [6.0, -8.0], "both axes, sign kept");
assert_eq!(i.shadow_blur, 16.0);
assert_eq!(i.shadow_spread, 4.0);
assert_eq!(
i.shadow_color,
[1.0, 0.0, 0.0, 0.5],
"the colour is not a length"
);
}
#[test]
fn a_blur_at_or_below_zero_reaches_the_instance_buffer_as_the_sharpest() {
for blur in [-1.0, 0.0] {
let shadow = super::super::types::Shadow::new(
(0.0, 6.0),
blur,
2.0,
crate::widgets::Color::rgba(0.0, 0.0, 0.0, 0.6),
);
let i = ShapeInstance::default().with_shadow(&shadow, 2.0);
assert_eq!(i.shadow_blur, SHARPEST_SHADOW_BLUR, "a blur of {blur}");
}
}
#[test]
fn a_faded_instance_keeps_its_colours_and_scales_their_alpha() {
let colours = [
[1.0, 0.0, 0.0, 0.8],
[0.0, 1.0, 0.0, 0.6],
[0.0, 0.0, 1.0, 0.4],
[1.0, 1.0, 0.0, 1.0],
[0.0, 1.0, 1.0, 0.2],
];
let solid = ShapeInstance {
fill_color: colours[0],
border_color: colours[1],
shadow_color: colours[2],
gradient_start: colours[3],
gradient_end: colours[4],
shadow_blur: 7.0,
..ShapeInstance::default()
};
let faded = solid.faded(0.5);
let drawn = [
faded.fill_color,
faded.border_color,
faded.shadow_color,
faded.gradient_start,
faded.gradient_end,
];
for (before, after) in colours.iter().zip(drawn) {
assert_eq!(after[..3], before[..3], "the colour is left alone");
assert_eq!(after[3], before[3] * 0.5, "its alpha is halved");
}
assert_eq!(faded.shadow_blur, 7.0, "and a fade is not a length");
}
#[test]
fn a_clip_hands_its_chain_to_the_instance() {
use crate::renderer::clip::{NO_CHAIN, ResolvedClip};
let shape = crate::shape::PlacedShape::placed(
crate::widgets::Rect::new(0.0, 0.0, 10.0, 10.0),
crate::renderer::CornerRadii::uniform(2.0),
1.0,
crate::transform::Transform::IDENTITY,
);
assert_eq!(ShapeInstance::default().clip_chain, NO_CHAIN);
let chained = ShapeInstance::default().with_clip(
&ResolvedClip {
rest: 3,
..ResolvedClip::alone(shape)
},
2.0,
);
assert_eq!(chained.clip_chain, 3);
assert_eq!(chained.clip_rect, [0.0, 0.0, 10.0, 10.0]);
}
#[test]
fn test_shape_instance_size() {
let size = std::mem::size_of::<ShapeInstance>();
println!("ShapeInstance size: {} bytes", size);
assert!(size <= 256, "ShapeInstance is too large: {} bytes", size);
assert_eq!(size, 240, "ShapeInstance size changed unexpectedly");
}
#[test]
fn test_quad_vertices() {
assert_eq!(QUAD_VERTICES.len(), 4);
assert_eq!(QUAD_INDICES.len(), 6);
}
#[test]
fn test_default_instance() {
let instance = ShapeInstance::default();
assert_eq!(instance.transform, [1.0, 0.0, 0.0, 0.0, 1.0, 0.0]);
assert_eq!(instance.shape_curvature, 1.0);
}
}
#[cfg(test)]
pub(super) mod instance_layout {
use super::*;
use std::mem::offset_of;
fn layout_of(ty: &str) -> Option<(usize, usize)> {
match ty {
"f32" | "u32" | "i32" => Some((4, 4)),
"vec2<f32>" => Some((8, 8)),
"vec3<f32>" => Some((16, 12)),
"vec4<f32>" | "vec4<u32>" => Some((16, 16)),
_ => ty
.strip_prefix("array<f32,")
.and_then(|rest| rest.strip_suffix('>'))
.and_then(|n| n.trim().parse::<usize>().ok())
.map(|n| (4, 4 * n)),
}
}
pub(in crate::renderer) fn wgsl_fields(
file: &str,
name: &str,
) -> (Vec<(String, usize)>, usize) {
let source =
std::fs::read_to_string(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(file))
.unwrap_or_else(|e| panic!("{file}: {e}"));
let start = source
.find(&format!("struct {name} {{"))
.unwrap_or_else(|| panic!("{file} declares no `{name}`"));
let body = &source[start..];
let body = &body[..body
.find('}')
.unwrap_or_else(|| panic!("`{name}` is never closed"))];
let mut offset = 0usize;
let mut align = 1usize;
let mut fields = Vec::new();
for line in body.lines().skip(1) {
let line = line.trim();
let Some(line) = line.strip_suffix(',') else {
continue; };
if line.starts_with("//") {
continue;
}
let (name, ty) = line
.split_once(':')
.unwrap_or_else(|| panic!("cannot read `{line}` as a WGSL struct member"));
let (name, ty) = (name.trim(), ty.trim());
let (a, size) =
layout_of(ty).unwrap_or_else(|| panic!("no WGSL layout recorded for type `{ty}`"));
offset = offset.next_multiple_of(a);
fields.push((name.to_owned(), offset));
offset += size;
align = align.max(a);
}
(fields, offset.next_multiple_of(align))
}
#[test]
fn the_shader_and_the_struct_agree_field_for_field() {
let rust: Vec<(&str, usize)> = vec![
("rect", offset_of!(ShapeInstance, rect)),
("corner_radii", offset_of!(ShapeInstance, corner_radii)),
("fill_color", offset_of!(ShapeInstance, fill_color)),
("border_color", offset_of!(ShapeInstance, border_color)),
("shadow_color", offset_of!(ShapeInstance, shadow_color)),
("shadow_offset", offset_of!(ShapeInstance, shadow_offset)),
("shadow_blur", offset_of!(ShapeInstance, shadow_blur)),
("shadow_spread", offset_of!(ShapeInstance, shadow_spread)),
("transform", offset_of!(ShapeInstance, transform)),
("clip_rect", offset_of!(ShapeInstance, clip_rect)),
("clip_radii", offset_of!(ShapeInstance, clip_radii)),
("clip_inverse", offset_of!(ShapeInstance, clip_inverse)),
("clip_curvature", offset_of!(ShapeInstance, clip_curvature)),
("clip_chain", offset_of!(ShapeInstance, clip_chain)),
("gradient_start", offset_of!(ShapeInstance, gradient_start)),
("gradient_end", offset_of!(ShapeInstance, gradient_end)),
("border_width", offset_of!(ShapeInstance, border_width)),
(
"shape_curvature",
offset_of!(ShapeInstance, shape_curvature),
),
("gradient_type", offset_of!(ShapeInstance, gradient_type)),
];
let (wgsl, wgsl_size) = wgsl_fields("src/renderer/shader.wgsl", "InstanceInput");
assert_eq!(
wgsl.iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(),
rust.iter().map(|(n, _)| *n).collect::<Vec<_>>(),
"`InstanceInput` and `ShapeInstance` do not declare the same fields in \
the same order. Every field after the first difference is read as its \
neighbour."
);
for ((name, shader), (_, host)) in wgsl.iter().zip(&rust) {
assert_eq!(
shader, host,
"`{name}` is at {host} in `ShapeInstance` and {shader} in \
`InstanceInput` — the shader reads it from the wrong bytes"
);
}
assert_eq!(
wgsl_size,
size_of::<ShapeInstance>(),
"the two structs are different sizes, so every instance after the \
first is read from the wrong offset"
);
}
}