use std::ops::{Add, Mul, Sub};
use bytemuck::{Pod, Zeroable};
use wgpu::util::DeviceExt;
#[derive(Debug, Clone, Copy)]
pub struct UICornerRadii {
pub top_left: f32,
pub top_right: f32,
pub bottom_left: f32,
pub bottom_right: f32,
}
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct UIColor {
pub r: f32,
pub g: f32,
pub b: f32,
}
pub struct UIBorderThickness {
pub top: f32,
pub left: f32,
pub bottom: f32,
pub right: f32,
}
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct UIPosition {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Default for UIPosition {
fn default() -> Self {
Self::new()
}
}
impl UIPosition {
pub const fn new() -> Self {
Self {
x: 0.0,
y: 0.0,
z: 0.0,
}
}
pub fn rotate(&mut self, mut degrees: f32) {
degrees = -degrees;
degrees *= std::f32::consts::PI / 180.0;
let (sn, cs) = degrees.sin_cos();
*self = Self {
x: self.x * cs - self.y * sn,
y: self.x * sn + self.y * cs,
z: self.z,
};
}
pub fn with_x(self, x: f32) -> Self {
Self {
x: self.x + x,
y: self.y,
z: self.z,
}
}
pub fn with_y(self, y: f32) -> Self {
Self {
x: self.x,
y: self.y + y,
z: self.z,
}
}
}
impl Add for UIPosition {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
x: self.x + other.x,
y: self.y + other.y,
z: self.z,
}
}
}
impl Add<f32> for UIPosition {
type Output = Self;
fn add(self, rhs: f32) -> Self {
Self {
x: self.x + rhs,
y: self.y + rhs,
z: self.z,
}
}
}
impl Sub<f32> for UIPosition {
type Output = Self;
fn sub(self, rhs: f32) -> Self {
Self {
x: self.x - rhs,
y: self.y - rhs,
z: self.z,
}
}
}
impl Mul<f32> for UIPosition {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self {
x: self.x * rhs,
y: self.y * rhs,
z: self.z,
}
}
}
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct UISize {
pub width: f32,
pub height: f32,
}
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
#[repr(C)]
pub struct UIVertex {
pub position: UIPosition,
pub color: UIColor,
pub size: UISize,
}
impl UIVertex {
pub fn new(size: (i32, i32)) -> Self {
Self {
position: UIPosition::new(),
color: UIColor {
r: 0.0,
g: 0.0,
b: 0.0,
},
size: UISize {
width: size.0 as f32,
height: size.1 as f32,
},
}
}
pub fn layout() -> wgpu::VertexBufferLayout<'static> {
const ATTR: [wgpu::VertexAttribute; 3] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3, 2 => Float32x2];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<UIVertex>() as u64,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &ATTR,
}
}
}
pub struct GeometryBatch {
vertices: Vec<UIVertex>,
buffer: wgpu::Buffer,
number_of_vertices: usize,
render_pipeline: wgpu::RenderPipeline,
}
impl GeometryBatch {
pub fn new(device: &wgpu::Device, pixel_format: wgpu::TextureFormat, size: (i32, i32)) -> Self {
let (buffer, vertices) = make_vertex_buffer(device, "cotis-wgpu ui vertices", 10_000, size);
let mut pipeline_builder = UIPipeline::new(pixel_format);
pipeline_builder.add_buffer_layout(UIVertex::layout());
let render_pipeline = pipeline_builder.build_pipeline(device);
Self {
vertices,
buffer,
number_of_vertices: 0,
render_pipeline,
}
}
pub fn resize_vertices(&mut self, size: (i32, i32)) {
for vertex in &mut self.vertices {
vertex.size.width = size.0 as f32;
vertex.size.height = size.1 as f32;
}
}
pub fn flush(&mut self, render_pass: &mut wgpu::RenderPass<'_>, queue: &wgpu::Queue) {
if self.number_of_vertices == 0 {
return;
}
render_pass.set_pipeline(&self.render_pipeline);
queue.write_buffer(
&self.buffer,
0,
bytemuck::cast_slice(&self.vertices[..self.number_of_vertices]),
);
render_pass.set_vertex_buffer(0, self.buffer.slice(..));
render_pass.draw(0..self.number_of_vertices as u32, 0..1);
self.number_of_vertices = 0;
}
pub fn triangle(&mut self, positions: &[UIPosition; 3], color: UIColor) {
let Some(vertices) = self
.vertices
.get_mut(self.number_of_vertices..self.number_of_vertices + 3)
else {
return;
};
for (vertex, position) in vertices.iter_mut().zip(positions.iter()) {
vertex.position = *position;
vertex.color = color;
self.number_of_vertices += 1;
}
}
pub fn quad(&mut self, positions: &[UIPosition; 4], color: UIColor) {
let Some(vertices) = self
.vertices
.get_mut(self.number_of_vertices..self.number_of_vertices + 6)
else {
return;
};
vertices[0].position = positions[0];
vertices[0].color = color;
vertices[1].position = positions[1];
vertices[1].color = color;
vertices[2].position = positions[2];
vertices[2].color = color;
vertices[3].position = positions[0];
vertices[3].color = color;
vertices[4].position = positions[2];
vertices[4].color = color;
vertices[5].position = positions[3];
vertices[5].color = color;
self.number_of_vertices += 6;
}
pub fn line(
&mut self,
position: UIPosition,
length: f32,
angle: f32,
thickness: f32,
color: UIColor,
) {
let mut line = [UIPosition::new(); 4];
line[0].y -= thickness / 2.0;
line[1].y += thickness / 2.0;
line[2].x += length;
line[2].y += thickness / 2.0;
line[3].x += length;
line[3].y -= thickness / 2.0;
for point in &mut line {
point.rotate(angle);
*point = *point + position;
}
self.quad(&line, color);
}
pub fn arc(
&mut self,
origin: UIPosition,
radius: f32,
degree_begin: f32,
degree_end: f32,
thickness: f32,
color: UIColor,
) {
let arc_length = (degree_end - degree_begin).abs();
let number_of_segments = 10.0;
let arc_segment_length = arc_length / number_of_segments;
let arc_segment_distance =
(2.0 * std::f32::consts::PI * radius) * (arc_segment_length / 360.0);
let mut arc_point = UIPosition::new();
for i in 0..number_of_segments as i32 {
arc_point.x = radius;
arc_point.y = 0.0;
arc_point.rotate(degree_begin + arc_segment_length * i as f32);
arc_point = arc_point + origin;
self.line(
arc_point,
arc_segment_distance,
degree_begin + 90.0 + arc_segment_length * i as f32 + arc_segment_length / 2.0,
thickness,
color,
);
}
}
pub fn filled_arc(
&mut self,
origin: UIPosition,
radius: f32,
degree_begin: f32,
degree_end: f32,
color: UIColor,
) {
let arc_length = (degree_end - degree_begin).abs();
let number_of_segments = 10.0;
let arc_segment_length = arc_length / number_of_segments;
let mut current_point = UIPosition::new();
let mut next_point = UIPosition::new();
for i in 0..number_of_segments as i32 {
current_point.x = radius;
current_point.y = 0.0;
current_point.rotate(degree_begin + arc_segment_length * (i as f32 + 1.0));
next_point.x = radius;
next_point.y = 0.0;
next_point.rotate(degree_begin + arc_segment_length * i as f32);
self.triangle(
&[current_point + origin, origin, next_point + origin],
color,
);
}
}
pub fn rectangle(
&mut self,
position: UIPosition,
size: UIPosition,
thickness: UIBorderThickness,
color: UIColor,
radii: UICornerRadii,
) {
self.arc(
position + radii.top_left,
radii.top_left,
90.0,
180.0,
thickness.top,
color,
);
self.arc(
position
.with_x(size.x - radii.top_right)
.with_y(radii.top_right),
radii.top_right,
0.0,
90.0,
thickness.top,
color,
);
self.arc(
position
.with_y(size.y - radii.bottom_left)
.with_x(radii.bottom_left),
radii.bottom_left,
180.0,
270.0,
thickness.bottom,
color,
);
self.arc(
position + (size - radii.bottom_right),
radii.bottom_right,
270.0,
360.0,
thickness.bottom,
color,
);
self.line(
position.with_x(radii.top_left),
size.x - (radii.top_left + radii.top_right),
0.0,
thickness.top,
color,
);
self.line(
position.with_y(radii.top_left),
size.y - (radii.top_left + radii.bottom_left),
270.0,
thickness.left,
color,
);
self.line(
position.with_x(radii.bottom_left).with_y(size.y),
size.x - (radii.bottom_left + radii.bottom_right),
0.0,
thickness.bottom,
color,
);
self.line(
position.with_x(size.x).with_y(radii.top_right),
size.y - (radii.top_right + radii.bottom_right),
270.0,
thickness.right,
color,
);
}
pub fn filled_rectangle(
&mut self,
position: UIPosition,
size: UIPosition,
color: UIColor,
radii: UICornerRadii,
) {
self.filled_arc(
position + radii.top_left,
radii.top_left,
90.0,
180.0,
color,
);
self.filled_arc(
position
.with_x(size.x - radii.top_right)
.with_y(radii.top_right),
radii.top_right,
0.0,
90.0,
color,
);
self.filled_arc(
position
.with_y(size.y - radii.bottom_left)
.with_x(radii.bottom_left),
radii.bottom_left,
180.0,
270.0,
color,
);
self.filled_arc(
position + (size - radii.bottom_right),
radii.bottom_right,
270.0,
360.0,
color,
);
self.quad(
&[
position.with_x(radii.top_left),
position + radii.top_left,
position
.with_x(size.x - radii.top_right)
.with_y(radii.top_right),
position.with_x(size.x - radii.top_right),
],
color,
);
self.quad(
&[
position
.with_x(radii.bottom_left)
.with_y(size.y - radii.bottom_left),
position.with_x(radii.bottom_left).with_y(size.y),
position.with_x(size.x - radii.bottom_right).with_y(size.y),
position
.with_x(size.x - radii.bottom_right)
.with_y(size.y - radii.bottom_right),
],
color,
);
self.quad(
&[
position.with_y(radii.top_left),
position.with_y(size.y - radii.bottom_left),
position
.with_x(radii.bottom_left)
.with_y(size.y - radii.bottom_left),
position + radii.top_left,
],
color,
);
self.quad(
&[
position.with_x(size.x - radii.top_right),
position
.with_x(size.x - radii.bottom_right)
.with_y(size.y - radii.bottom_right),
position.with_x(size.x).with_y(size.y - radii.bottom_right),
position.with_x(size.x).with_y(radii.top_right),
],
color,
);
self.quad(
&[
position + radii.top_left,
position
.with_x(radii.bottom_left)
.with_y(size.y - radii.bottom_left),
position
.with_x(size.x - radii.bottom_right)
.with_y(size.y - radii.bottom_right),
position
.with_x(size.x - radii.top_right)
.with_y(radii.top_right),
],
color,
);
}
}
fn make_vertex_buffer(
device: &wgpu::Device,
label: &str,
triangle_capacity: usize,
size: (i32, i32),
) -> (wgpu::Buffer, Vec<UIVertex>) {
let vertices = vec![UIVertex::new(size); triangle_capacity * 3];
let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some(label),
contents: bytemuck::cast_slice(&vertices),
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
});
(buffer, vertices)
}
pub struct UIPipeline {
pixel_format: wgpu::TextureFormat,
vertex_buffer_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
}
impl UIPipeline {
pub fn new(pixel_format: wgpu::TextureFormat) -> Self {
Self {
pixel_format,
vertex_buffer_layouts: Vec::new(),
}
}
pub fn add_buffer_layout(&mut self, layout: wgpu::VertexBufferLayout<'static>) {
self.vertex_buffer_layouts.push(layout);
}
pub fn build_pipeline(&self, device: &wgpu::Device) -> wgpu::RenderPipeline {
let source_code = include_str!("ui_shader.wgsl");
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("cotis-wgpu ui shader"),
source: wgpu::ShaderSource::Wgsl(source_code.into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("cotis-wgpu ui pipeline layout"),
bind_group_layouts: &[],
push_constant_ranges: &[],
});
let render_targets = [Some(wgpu::ColorTargetState {
format: self.pixel_format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::ALL,
})];
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("cotis-wgpu ui pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader_module,
entry_point: Some("vs_main"),
buffers: &self.vertex_buffer_layouts,
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader_module,
entry_point: Some("fs_main"),
targets: &render_targets,
compilation_options: wgpu::PipelineCompilationOptions::default(),
}),
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Always,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
cache: None,
})
}
}