use oxiui_core::Color;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Vertex {
pub position: [f32; 2],
pub color: [f32; 4],
pub local: [f32; 2],
pub shape_xy: [f32; 2],
pub shape_r: f32,
pub kind: f32,
pub extra: [f32; 2],
}
pub const KIND_RECT: f32 = 0.0;
pub const KIND_CIRCLE: f32 = 1.0;
pub const KIND_ROUNDED_RECT: f32 = 2.0;
pub const KIND_ROUNDED_RECT_PC: f32 = 3.0;
pub const KIND_ELLIPSE: f32 = 4.0;
pub const KIND_LINE_SEG: f32 = 5.0;
const _: () = assert!(core::mem::size_of::<Vertex>() == 56);
const _: () = assert!(core::mem::align_of::<Vertex>() == 4);
impl Vertex {
#[inline]
pub fn color_to_f32(color: Color) -> [f32; 4] {
[
color.0 as f32 / 255.0,
color.1 as f32 / 255.0,
color.2 as f32 / 255.0,
color.3 as f32 / 255.0,
]
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct Globals {
pub viewport: [f32; 2],
pub _pad: [f32; 2],
}
const _: () = assert!(core::mem::size_of::<Globals>() == 16);
impl Globals {
#[inline]
pub fn new(width: u32, height: u32) -> Self {
Self {
viewport: [width as f32, height as f32],
_pad: [0.0, 0.0],
}
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GradientVertex {
pub position: [f32; 2],
pub local: [f32; 2],
}
const _: () = assert!(core::mem::size_of::<GradientVertex>() == 16);
pub const MAX_GRADIENT_STOPS: usize = 8;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct GradientUniforms {
pub p0: [f32; 2],
pub p1: [f32; 2],
pub radius: f32,
pub gradient_type: u32,
pub stop_count: u32,
pub _pad: u32,
pub stop_offsets: [[f32; 4]; MAX_GRADIENT_STOPS],
pub stop_colors: [[f32; 4]; MAX_GRADIENT_STOPS],
}
const _: () = assert!(core::mem::size_of::<GradientUniforms>() == 288);
#[inline]
pub fn pack_u16_pair(hi: u16, lo: u16) -> f32 {
f32::from_bits(((hi as u32) << 16) | (lo as u32))
}
pub fn push_rect_quad(out: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: Color) {
let rgba = Vertex::color_to_f32(color);
let x1 = x + w;
let y1 = y + h;
let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [0.0, 0.0],
shape_r: 0.0,
kind: KIND_RECT,
extra: [0.0, 0.0],
});
}
}
pub fn push_circle_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, radius: f32, color: Color) {
let rgba = Vertex::color_to_f32(color);
let r = radius + 1.0;
let x0 = cx - r;
let y0 = cy - r;
let x1 = cx + r;
let y1 = cy + r;
let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [cx, cy],
shape_r: radius,
kind: KIND_CIRCLE,
extra: [0.0, 0.0],
});
}
}
pub fn push_rounded_rect_quad(
out: &mut Vec<Vertex>,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
color: Color,
) {
let rgba = Vertex::color_to_f32(color);
let r = radius.min(w * 0.5).min(h * 0.5).max(0.0);
let cx = x + w * 0.5;
let cy = y + h * 0.5;
let hw = w * 0.5;
let hh = h * 0.5;
let pad = 1.0_f32;
let x0 = x - pad;
let y0 = y - pad;
let x1 = x + w + pad;
let y1 = y + h + pad;
let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [cx, cy],
shape_r: r,
kind: KIND_ROUNDED_RECT,
extra: [hw, hh],
});
}
}
pub fn push_rounded_rect_per_corner_quad(
out: &mut Vec<Vertex>,
x: f32,
y: f32,
w: f32,
h: f32,
radii: [f32; 4],
color: Color,
) {
let rgba = Vertex::color_to_f32(color);
let [tl, tr, br, bl] = radii;
let hw = w * 0.5;
let hh = h * 0.5;
let cx = x + hw;
let cy = y + hh;
let clamp_r = |r: f32| r.clamp(0.0, hw.min(hh).min(255.0));
let tl = clamp_r(tl);
let tr = clamp_r(tr);
let br = clamp_r(br);
let bl = clamp_r(bl);
let hw_c = hw.clamp(0.0, 4095.0);
let hh_c = hh.clamp(0.0, 4095.0);
let r_packed = tl.floor() * 256.0 + tr.floor();
let brbl_packed = br.floor() * 256.0 + bl.floor();
let hwhh_packed = hw_c.floor() * 4096.0 + hh_c.floor();
let pad = 1.0_f32;
let x0 = x - pad;
let y0 = y - pad;
let x1 = x + w + pad;
let y1 = y + h + pad;
let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [cx, cy],
shape_r: r_packed,
kind: KIND_ROUNDED_RECT_PC,
extra: [brbl_packed, hwhh_packed],
});
}
}
pub fn push_ellipse_quad(out: &mut Vec<Vertex>, cx: f32, cy: f32, rx: f32, ry: f32, color: Color) {
let rgba = Vertex::color_to_f32(color);
let pad = 1.0_f32;
let x0 = cx - rx - pad;
let y0 = cy - ry - pad;
let x1 = cx + rx + pad;
let y1 = cy + ry + pad;
let corners = [[x0, y0], [x0, y1], [x1, y1], [x0, y0], [x1, y1], [x1, y0]];
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [cx, cy],
shape_r: 0.0,
kind: KIND_ELLIPSE,
extra: [rx, ry],
});
}
}
pub struct LineQuadParams {
pub from_x: f32,
pub from_y: f32,
pub to_x: f32,
pub to_y: f32,
pub half_width: f32,
pub color: Color,
pub aa_smooth: bool,
}
pub fn push_line_quad(out: &mut Vec<Vertex>, params: LineQuadParams) {
let LineQuadParams {
from_x,
from_y,
to_x,
to_y,
half_width,
color,
aa_smooth,
} = params;
let rgba = Vertex::color_to_f32(color);
let dx = to_x - from_x;
let dy = to_y - from_y;
let len = (dx * dx + dy * dy).sqrt().max(1e-6);
let nx = -dy / len;
let ny = dx / len;
let lx = dx / len;
let ly = dy / len;
let expand = half_width + 1.0;
let cap = half_width + 1.0;
let ax = from_x - lx * cap + nx * expand;
let ay = from_y - ly * cap + ny * expand;
let bx = from_x - lx * cap - nx * expand;
let by = from_y - ly * cap - ny * expand;
let cx = to_x + lx * cap - nx * expand;
let cy_v = to_y + ly * cap - ny * expand;
let dxp = to_x + lx * cap + nx * expand;
let dyp = to_y + ly * cap + ny * expand;
let corners = [
[ax, ay],
[bx, by],
[cx, cy_v],
[ax, ay],
[cx, cy_v],
[dxp, dyp],
];
let shape_r_val = if aa_smooth {
half_width + 0.5
} else {
half_width
};
for c in corners {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [from_x, from_y],
shape_r: shape_r_val,
kind: KIND_LINE_SEG,
extra: [to_x, to_y],
});
}
}
pub fn push_triangle(
out: &mut Vec<Vertex>,
p0: [f32; 2],
p1: [f32; 2],
p2: [f32; 2],
color: Color,
) {
let rgba = Vertex::color_to_f32(color);
for c in [p0, p1, p2] {
out.push(Vertex {
position: c,
color: rgba,
local: c,
shape_xy: [0.0, 0.0],
shape_r: 0.0,
kind: KIND_RECT,
extra: [0.0, 0.0],
});
}
}
pub fn push_gradient_quad(out: &mut Vec<GradientVertex>, x: f32, y: f32, w: f32, h: f32) {
let x1 = x + w;
let y1 = y + h;
let corners = [[x, y], [x, y1], [x1, y1], [x, y], [x1, y1], [x1, y]];
for c in corners {
out.push(GradientVertex {
position: c,
local: c,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vertex_size_is_56_bytes() {
assert_eq!(core::mem::size_of::<Vertex>(), 56);
}
#[test]
fn globals_size_is_16_bytes() {
assert_eq!(core::mem::size_of::<Globals>(), 16);
}
#[test]
fn gradient_vertex_size_is_16_bytes() {
assert_eq!(core::mem::size_of::<GradientVertex>(), 16);
}
#[test]
fn gradient_uniforms_size_is_288_bytes() {
assert_eq!(core::mem::size_of::<GradientUniforms>(), 288);
}
#[test]
fn color_to_f32_maps_full_range() {
let white = Vertex::color_to_f32(Color(255, 255, 255, 255));
assert!((white[0] - 1.0).abs() < 1e-6);
assert!((white[3] - 1.0).abs() < 1e-6);
let black = Vertex::color_to_f32(Color(0, 0, 0, 0));
assert_eq!(black, [0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn rect_quad_emits_six_vertices() {
let mut v = Vec::new();
push_rect_quad(&mut v, 1.0, 2.0, 3.0, 4.0, Color(255, 0, 0, 255));
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_RECT);
}
let xs: Vec<f32> = v.iter().map(|vt| vt.position[0]).collect();
assert!(xs.contains(&1.0));
assert!(xs.contains(&4.0));
}
#[test]
fn circle_quad_emits_six_vertices_with_center() {
let mut v = Vec::new();
push_circle_quad(&mut v, 10.0, 10.0, 5.0, Color(0, 255, 0, 255));
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_CIRCLE);
assert_eq!(vert.shape_xy, [10.0, 10.0]);
assert!((vert.shape_r - 5.0).abs() < 1e-6);
}
}
#[test]
fn vertices_are_pod_castable() {
let mut v = Vec::new();
push_rect_quad(&mut v, 0.0, 0.0, 1.0, 1.0, Color(1, 2, 3, 4));
let bytes: &[u8] = bytemuck::cast_slice(&v);
assert_eq!(bytes.len(), 6 * 56);
}
#[test]
fn rounded_rect_quad_emits_six_vertices() {
let mut v = Vec::new();
push_rounded_rect_quad(&mut v, 10.0, 10.0, 80.0, 40.0, 8.0, Color(0, 0, 255, 255));
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_ROUNDED_RECT);
}
}
#[test]
fn rounded_rect_pc_quad_emits_six_vertices() {
let mut v = Vec::new();
push_rounded_rect_per_corner_quad(
&mut v,
10.0,
10.0,
80.0,
40.0,
[4.0, 8.0, 4.0, 8.0],
Color(0, 0, 255, 255),
);
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_ROUNDED_RECT_PC);
}
}
#[test]
fn ellipse_quad_emits_six_vertices() {
let mut v = Vec::new();
push_ellipse_quad(&mut v, 50.0, 50.0, 30.0, 20.0, Color(255, 255, 0, 255));
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_ELLIPSE);
assert!((vert.extra[0] - 30.0).abs() < 1e-4);
assert!((vert.extra[1] - 20.0).abs() < 1e-4);
}
}
#[test]
fn line_quad_emits_six_vertices() {
let mut v = Vec::new();
push_line_quad(
&mut v,
LineQuadParams {
from_x: 0.0,
from_y: 0.0,
to_x: 100.0,
to_y: 0.0,
half_width: 2.0,
color: Color(255, 0, 0, 255),
aa_smooth: true,
},
);
assert_eq!(v.len(), 6);
for vert in &v {
assert_eq!(vert.kind, KIND_LINE_SEG);
}
}
#[test]
fn push_triangle_emits_three_vertices() {
let mut v = Vec::new();
push_triangle(
&mut v,
[0.0, 0.0],
[10.0, 0.0],
[5.0, 8.0],
Color(255, 255, 255, 255),
);
assert_eq!(v.len(), 3);
for vert in &v {
assert_eq!(vert.kind, KIND_RECT);
}
}
#[test]
fn gradient_quad_emits_six_vertices() {
let mut v = Vec::new();
push_gradient_quad(&mut v, 0.0, 0.0, 100.0, 50.0);
assert_eq!(v.len(), 6);
}
#[test]
fn pack_u16_pair_round_trips() {
let packed = pack_u16_pair(42, 1000);
let bits = packed.to_bits();
let hi = (bits >> 16) as u16;
let lo = (bits & 0xffff) as u16;
assert_eq!(hi, 42);
assert_eq!(lo, 1000);
}
}