hUI/hui/src/draw.rs

369 lines
13 KiB
Rust
Raw Normal View History

2024-02-27 13:31:12 -06:00
//! draw commands, tesselation and UI rendering.
2024-02-20 11:19:10 -06:00
2024-03-01 13:44:37 -06:00
//TODO: 9-slice draw command
2024-02-20 13:56:58 -06:00
use crate::{
rectangle::Corners,
2024-02-24 21:02:10 -06:00
text::{FontHandle, TextRenderer}
2024-02-20 13:56:58 -06:00
};
2024-02-17 14:43:46 -06:00
2024-02-24 16:32:09 -06:00
pub(crate) mod atlas;
2024-02-24 21:02:10 -06:00
use atlas::TextureAtlasManager;
pub use atlas::{TextureHandle, TextureAtlasMeta};
2024-02-19 12:40:18 -06:00
2024-02-24 16:32:09 -06:00
mod corner_radius;
2024-02-20 10:30:26 -06:00
pub use corner_radius::RoundedCorners;
2024-02-24 16:32:09 -06:00
2024-02-17 14:43:46 -06:00
use std::borrow::Cow;
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
2024-03-01 17:33:02 -06:00
use glam::{vec2, Vec2, Affine2, Vec4};
2024-02-17 14:43:46 -06:00
2024-02-25 08:43:38 -06:00
//TODO: circle draw command
2024-02-19 14:12:12 -06:00
/// Available draw commands
2024-02-25 08:43:38 -06:00
/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture
2024-02-19 14:12:12 -06:00
/// - Text: Draw text using the specified font, size, color, and position
2024-02-17 14:43:46 -06:00
#[derive(Clone, Debug, PartialEq)]
pub enum UiDrawCommand {
///Filled, colored rectangle
Rectangle {
///Position in pixels
position: Vec2,
///Size in pixels
size: Vec2,
///Color (RGBA)
2024-02-20 13:56:58 -06:00
color: Corners<Vec4>,
2024-02-25 08:43:38 -06:00
///Texture
texture: Option<TextureHandle>,
2024-02-18 20:41:48 -06:00
///Rounded corners
2024-02-19 12:40:18 -06:00
rounded_corners: Option<RoundedCorners>,
2024-02-17 14:43:46 -06:00
},
2024-02-20 11:19:10 -06:00
/// Draw text using the specified font, size, color, and position
2024-02-17 14:43:46 -06:00
Text {
///Position in pixels
position: Vec2,
///Font size
2024-02-24 21:02:10 -06:00
size: u16,
2024-02-17 14:43:46 -06:00
///Color (RGBA)
color: Vec4,
///Text to draw
text: Cow<'static, str>,
///Font handle to use
font: FontHandle,
},
2024-03-01 17:33:02 -06:00
/// Push a transformation matrix to the stack
PushTransform(Affine2),
/// Pop a transformation matrix from the stack
PopTransform,
2024-02-17 14:43:46 -06:00
}
2024-02-20 11:19:10 -06:00
/// List of draw commands
2024-02-17 14:43:46 -06:00
#[derive(Default)]
2024-02-19 14:12:12 -06:00
pub struct UiDrawCommandList {
2024-02-17 14:43:46 -06:00
pub commands: Vec<UiDrawCommand>,
}
2024-02-19 14:12:12 -06:00
impl UiDrawCommandList {
2024-02-20 11:19:10 -06:00
/// Add a draw command to the list
2024-02-17 14:43:46 -06:00
pub fn add(&mut self, command: UiDrawCommand) {
self.commands.push(command);
}
}
// impl UiDrawCommands {
// pub fn compare(&self, other: &Self) -> bool {
// // if self.commands.len() != other.commands.len() { return false }
// // self.commands.iter().zip(other.commands.iter()).all(|(a, b)| a == b)
// }
// }
2024-02-20 11:19:10 -06:00
/// A vertex for UI rendering
2024-02-18 22:36:38 -06:00
#[derive(Clone, Copy, Debug, PartialEq, Default)]
2024-02-17 14:43:46 -06:00
pub struct UiVertex {
pub position: Vec2,
pub color: Vec4,
pub uv: Vec2,
}
2024-02-20 11:19:10 -06:00
/// Represents a single draw call, should be handled by the render backend
2024-02-17 14:43:46 -06:00
#[derive(Default)]
pub struct UiDrawCall {
pub vertices: Vec<UiVertex>,
pub indices: Vec<u32>,
}
2024-02-21 13:13:58 -06:00
impl UiDrawCall {
2024-02-20 11:19:10 -06:00
/// Tesselate the UI and build a complete draw plan from a list of draw commands
2024-02-24 21:02:10 -06:00
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
2024-03-01 17:33:02 -06:00
let mut trans_stack = Vec::new();
2024-02-21 13:13:58 -06:00
let mut draw_call = UiDrawCall::default();
2024-03-06 14:14:40 -06:00
//HACK: atlas may get resized while creating new glyphs,
//which invalidates all uvs, causing corrupted-looking texture
//so we need to pregenerate font textures before generating any vertices
//we are doing *a lot* of double work here, but it's the easiest way to avoid the issue
for comamnd in &draw_commands.commands {
if let UiDrawCommand::Text { text, font: font_handle, size, .. } = comamnd {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() { continue }
text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
}
}
}
//note to future self:
//RESIZING OR ADDING STUFF TO ATLAS AFTER THIS POINT IS A BIG NO-NO,
//DON'T DO IT EVER AGAIN UNLESS YOU WANT TO SPEND HOURS DEBUGGING
atlas.lock_atlas = true;
2024-02-17 14:43:46 -06:00
for command in &draw_commands.commands {
match command {
2024-03-01 17:33:02 -06:00
UiDrawCommand::PushTransform(trans) => {
//Take note of the current index, and the transformation matrix\
//We will actually apply the transformation matrix when we pop it,
//to all vertices between the current index and the index we pushed
trans_stack.push((trans, draw_call.vertices.len() as u32));
},
UiDrawCommand::PopTransform => {
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
if idx == draw_call.vertices.len() as u32 {
continue
}
//Kinda a hack:
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
//We won't actually do that, we will compute the center of the bounding box of the vertices
let mut min = Vec2::splat(std::f32::INFINITY);
let mut max = Vec2::splat(std::f32::NEG_INFINITY);
for v in &draw_call.vertices[idx as usize..] {
min = min.min(v.position);
max = max.max(v.position);
}
//TODO: make the point of transform configurable
let center = (min + max) / 2.;
//Apply trans mtx to all vertices between idx and the current index
for v in &mut draw_call.vertices[idx as usize..] {
v.position -= center;
v.position = trans.transform_point2(v.position);
v.position += center;
}
},
2024-02-25 08:43:38 -06:00
UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => {
let uvs = texture
.map(|x| atlas.get_uv(x))
.flatten()
.unwrap_or(Corners::all(Vec2::ZERO));
2024-02-21 13:13:58 -06:00
let vidx = draw_call.vertices.len() as u32;
2024-02-20 10:30:26 -06:00
if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) {
2024-02-18 22:36:38 -06:00
//this code is stupid as fuck
2024-03-05 16:47:40 -06:00
//but it works... i think?
//maybe some verts end up missing, but it's close enough...
2024-02-18 22:36:38 -06:00
//Random vert in the center for no reason
//lol
2024-02-21 13:13:58 -06:00
draw_call.vertices.push(UiVertex {
2024-02-18 22:36:38 -06:00
position: *position + *size * vec2(0.5, 0.5),
2024-02-20 13:56:58 -06:00
color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4.,
2024-03-06 14:28:35 -06:00
//TODO: fix this uv
2024-02-18 22:36:38 -06:00
uv: vec2(0., 0.),
});
2024-02-18 21:37:28 -06:00
2024-02-20 13:24:36 -06:00
//TODO: fix some corners tris being invisible (but it's already close enough lol)
2024-02-19 12:40:18 -06:00
let rounded_corner_verts = corner.point_count.get() as u32;
2024-02-18 21:37:28 -06:00
for i in 0..rounded_corner_verts {
let cratio = i as f32 / rounded_corner_verts as f32;
let angle = cratio * std::f32::consts::PI * 0.5;
let x = angle.sin();
let y = angle.cos();
2024-03-05 16:47:40 -06:00
let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| {
2024-03-01 18:19:47 -06:00
let rrp = rp / *size;
let color_at_point =
color.bottom_right * rrp.x * rrp.y +
color.top_right * rrp.x * (1. - rrp.y) +
color.bottom_left * (1. - rrp.x) * rrp.y +
color.top_left * (1. - rrp.x) * (1. - rrp.y);
2024-03-05 16:47:40 -06:00
let uv_at_point =
uvs.bottom_right * rrp.x * rrp.y +
uvs.top_right * rrp.x * (1. - rrp.y) +
uvs.bottom_left * (1. - rrp.x) * rrp.y +
uvs.top_left * (1. - rrp.x) * (1. - rrp.y);
2024-03-01 18:19:47 -06:00
draw_call.vertices.push(UiVertex {
position: *position + rp,
color: color_at_point,
2024-03-05 16:47:40 -06:00
uv: uv_at_point,
2024-03-01 18:19:47 -06:00
});
};
2024-03-05 16:47:40 -06:00
//Top-right corner
2024-03-01 18:19:47 -06:00
corner_impl(
vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.),
color,
);
2024-02-18 22:36:38 -06:00
//Bottom-right corner
2024-03-01 18:19:47 -06:00
corner_impl(
vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right),
color,
);
2024-02-18 21:37:28 -06:00
//Bottom-left corner
2024-03-01 18:19:47 -06:00
corner_impl(
vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left),
color,
);
2024-02-18 22:36:38 -06:00
//Top-left corner
2024-03-01 18:19:47 -06:00
corner_impl(
vec2(1. - x, 1. - y) * corner.radius.top_left,
color,
);
2024-03-05 16:47:40 -06:00
2024-02-18 22:36:38 -06:00
// mental illness:
if i > 0 {
2024-02-21 13:13:58 -06:00
draw_call.indices.extend([
2024-02-18 22:36:38 -06:00
//Top-right corner
vidx,
vidx + 1 + (i - 1) * 4,
vidx + 1 + i * 4,
//Bottom-right corner
vidx,
vidx + 1 + (i - 1) * 4 + 1,
vidx + 1 + i * 4 + 1,
//Bottom-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 2,
vidx + 1 + i * 4 + 2,
//Top-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 3,
vidx + 1 + i * 4 + 3,
]);
}
2024-02-18 21:37:28 -06:00
}
2024-02-18 22:36:38 -06:00
//Fill in the rest
//mental illness 2:
2024-02-21 13:13:58 -06:00
draw_call.indices.extend([
2024-02-18 22:46:43 -06:00
//Top
vidx,
2024-02-18 22:50:46 -06:00
vidx + 4,
2024-02-18 22:46:43 -06:00
vidx + 1,
//Right?, i think
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 1,
//Left???
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 2,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 3,
//Bottom???
vidx,
vidx + 3,
vidx + 2,
]);
2024-02-18 20:41:48 -06:00
} else {
2024-03-05 16:47:40 -06:00
//...Normal rectangle
2024-02-21 13:13:58 -06:00
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
2024-02-18 20:41:48 -06:00
UiVertex {
position: *position,
2024-02-20 13:56:58 -06:00
color: color.top_left,
2024-02-25 08:43:38 -06:00
uv: uvs.top_left,
2024-02-18 20:41:48 -06:00
},
UiVertex {
position: *position + vec2(size.x, 0.0),
2024-02-20 13:56:58 -06:00
color: color.top_right,
2024-02-25 08:43:38 -06:00
uv: uvs.top_right,
2024-02-18 20:41:48 -06:00
},
UiVertex {
position: *position + *size,
2024-02-20 13:56:58 -06:00
color: color.bottom_right,
2024-02-25 08:43:38 -06:00
uv: uvs.bottom_right,
2024-02-18 20:41:48 -06:00
},
UiVertex {
position: *position + vec2(0.0, size.y),
2024-02-20 13:56:58 -06:00
color: color.bottom_left,
2024-02-25 08:43:38 -06:00
uv: uvs.bottom_left,
2024-02-18 20:41:48 -06:00
},
]);
}
2024-02-17 14:43:46 -06:00
},
2024-02-24 21:02:10 -06:00
UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
2024-02-20 10:30:26 -06:00
if text.is_empty() {
continue
}
2024-02-17 14:43:46 -06:00
//XXX: should we be doing this every time?
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
2024-02-24 21:02:10 -06:00
&[text_renderer.internal_font(*font_handle)],
2024-02-17 14:43:46 -06:00
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() {
continue
}
2024-02-21 13:13:58 -06:00
let vidx = draw_call.vertices.len() as u32;
2024-02-24 21:02:10 -06:00
let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
2024-02-25 08:43:38 -06:00
let uv = atlas.get_uv(glyph.texture).unwrap();
2024-02-21 13:13:58 -06:00
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
2024-02-17 14:43:46 -06:00
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y),
color: *color,
2024-02-24 21:02:10 -06:00
uv: uv.top_left,
2024-02-17 14:43:46 -06:00
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
color: *color,
2024-02-24 21:02:10 -06:00
uv: uv.top_right,
2024-02-17 14:43:46 -06:00
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
2024-02-24 21:02:10 -06:00
uv: uv.bottom_right,
2024-02-17 14:43:46 -06:00
},
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
2024-02-24 21:02:10 -06:00
uv: uv.bottom_left,
2024-02-17 14:43:46 -06:00
},
]);
2024-02-20 10:30:26 -06:00
#[cfg(all(
feature = "pixel_perfect_text",
not(feature = "pixel_perfect")
))] {
2024-03-01 17:33:02 -06:00
//Round the position of the vertices to the nearest pixel, unless any transformations are active
if trans_stack.is_empty() {
for vtx in &mut draw_call.vertices[(vidx as usize)..] {
vtx.position = vtx.position.round()
}
2024-02-20 10:30:26 -06:00
}
}
2024-02-17 14:43:46 -06:00
}
}
}
}
2024-03-06 14:14:40 -06:00
atlas.lock_atlas = false;
2024-02-21 13:13:58 -06:00
#[cfg(feature = "pixel_perfect")]
draw_call.vertices.iter_mut().for_each(|v| {
v.position = v.position.round()
});
2024-03-06 14:14:40 -06:00
2024-02-21 13:13:58 -06:00
draw_call
2024-02-17 14:43:46 -06:00
}
}