use crate::transform::Transform;
use wgpu::{VertexAttribute, VertexBufferLayout, VertexFormat, VertexStepMode};
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
pub struct TexturedVertex {
pub position: [f32; 2],
pub uv: [f32; 2],
pub clip_pos: [f32; 2],
pub clip_rect: [f32; 4],
pub clip_params: [f32; 4],
pub curvature_opacity: [f32; 4],
pub tint: [f32; 4],
}
#[derive(Clone, Copy, Debug)]
pub struct QuadClip {
pub rect: [f32; 4],
pub radii: [f32; 4],
pub curvature: f32,
pub to_clip_space: Transform,
}
impl QuadClip {
pub const NONE: Self = Self {
rect: crate::renderer::gpu::NO_CLIP_RECT,
radii: [0.0; 4],
curvature: 1.0,
to_clip_space: Transform::IDENTITY,
};
pub(crate) const EMPTY: Self = Self {
rect: crate::renderer::gpu::EMPTY_CLIP_RECT,
radii: [0.0; 4],
curvature: 1.0,
to_clip_space: Transform::IDENTITY,
};
pub fn shape(clip: &crate::shape::PlacedShape, scale: f32) -> Self {
let Some(to_clip_space) = clip.to_local(scale) else {
return Self::EMPTY;
};
let r = clip.rect;
Self {
rect: [r.x, r.y, r.width, r.height],
radii: clip.radii.to_array(),
curvature: clip.curvature,
to_clip_space,
}
}
#[inline]
pub(crate) fn place(&self, x: f32, y: f32) -> [f32; 2] {
let (x, y) = self.to_clip_space.transform_point(x, y);
[x, y]
}
}
impl TexturedVertex {
#[inline]
pub fn corner(
ndc: [f32; 2],
uv: [f32; 2],
screen: (f32, f32),
clip: &QuadClip,
opacity: f32,
tint: [f32; 4],
) -> Self {
Self {
position: ndc,
uv,
clip_pos: clip.place(screen.0, screen.1),
clip_rect: clip.rect,
clip_params: clip.radii,
curvature_opacity: [clip.curvature, opacity, 0.0, 0.0],
tint,
}
}
pub fn desc() -> VertexBufferLayout<'static> {
VertexBufferLayout {
array_stride: std::mem::size_of::<TexturedVertex>() as u64,
step_mode: VertexStepMode::Vertex,
attributes: &[
VertexAttribute {
offset: 0,
shader_location: 0,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: 8,
shader_location: 1,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: 16,
shader_location: 2,
format: VertexFormat::Float32x2,
},
VertexAttribute {
offset: 24,
shader_location: 3,
format: VertexFormat::Float32x4,
},
VertexAttribute {
offset: 40,
shader_location: 4,
format: VertexFormat::Float32x4,
},
VertexAttribute {
offset: 56,
shader_location: 5,
format: VertexFormat::Float32x4,
},
VertexAttribute {
offset: 72,
shader_location: 6,
format: VertexFormat::Float32x4,
},
],
}
}
}
pub const NO_TINT: [f32; 4] = [0.0; 4];
#[inline]
pub fn to_ndc(x: f32, y: f32, screen_width: f32, screen_height: f32) -> [f32; 2] {
[
(x / screen_width) * 2.0 - 1.0,
1.0 - (y / screen_height) * 2.0,
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::renderer::CornerRadii;
use crate::shape::PlacedShape;
use crate::widgets::Rect;
#[test]
fn a_clip_is_scaled_by_its_map_and_not_by_its_rect() {
let rect = Rect::new(10.0, 20.0, 100.0, 50.0);
let shaped = QuadClip::shape(
&PlacedShape {
rect,
radii: CornerRadii::uniform(4.0),
curvature: 1.0,
placement: crate::transform::Transform::IDENTITY,
},
2.0,
);
assert_eq!(
shaped.rect,
[10.0, 20.0, 100.0, 50.0],
"the declaration, unscaled — the map is what carries the scale"
);
let [x, y] = shaped.place(20.0, 40.0);
assert!(
(x - 10.0).abs() < 0.01 && (y - 20.0).abs() < 0.01,
"so the same physical corner arrives at the logical one, got ({x}, {y})"
);
assert_eq!(shaped.radii, [4.0; 4], "radii travel with the rect");
}
#[test]
fn a_collapsed_clip_cuts_it_all() {
let collapsed = QuadClip::shape(
&PlacedShape {
rect: Rect::new(0.0, 0.0, 100.0, 50.0),
radii: CornerRadii::uniform(0.0),
curvature: 1.0,
placement: crate::transform::Transform::scale(0.0),
},
1.0,
);
assert_eq!(collapsed.rect, crate::renderer::gpu::EMPTY_CLIP_RECT);
assert_ne!(
collapsed.rect,
crate::renderer::gpu::NO_CLIP_RECT,
"the sentinel for `clip everything`, not the one for `clip nothing`"
);
}
}