diff --git a/hui-examples/Cargo.toml b/hui-examples/Cargo.toml index c6dfadf..4521a28 100644 --- a/hui-examples/Cargo.toml +++ b/hui-examples/Cargo.toml @@ -7,6 +7,7 @@ publish = false [dev-dependencies] hui = { path = "../hui" } +hui-painter = { path = "../hui-painter" } hui-glium = { path = "../hui-glium" } hui-winit = { path = "../hui-winit" } kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "be1e24ba0c9e6d24128e7d0e74bebd8b90c23be7" } diff --git a/hui-painter/Cargo.toml b/hui-painter/Cargo.toml index 37bf613..c5eef95 100644 --- a/hui-painter/Cargo.toml +++ b/hui-painter/Cargo.toml @@ -21,3 +21,4 @@ log = "0.4" rect_packer = "0.2" # TODO: use sth else like `crunch` instead? hashbrown = "0.14" nohash-hasher = "0.2" +fontdue = "0.9" diff --git a/hui-painter/src/lib.rs b/hui-painter/src/lib.rs index aa909f7..c27d1d6 100644 --- a/hui-painter/src/lib.rs +++ b/hui-painter/src/lib.rs @@ -5,7 +5,8 @@ use texture::TextureAtlas; #[derive(Default)] pub struct Painter { - atlas: TextureAtlas, + pub(crate) atlas: TextureAtlas, + // ftm: FontTextureManager, } impl Painter { diff --git a/hui-painter/src/paint/command.rs b/hui-painter/src/paint/command.rs index fc195f1..338fae4 100644 --- a/hui-painter/src/paint/command.rs +++ b/hui-painter/src/paint/command.rs @@ -1,4 +1,4 @@ -use crate::paint::buffer::PaintBuffer; +use crate::{paint::buffer::PaintBuffer, Painter}; mod transform; pub use transform::PaintTransform; @@ -10,5 +10,5 @@ mod text; pub use text::PaintText; pub trait PaintCommand { - fn paint(&self, into: &mut PaintBuffer); + fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer); } diff --git a/hui-painter/src/paint/command/rectangle.rs b/hui-painter/src/paint/command/rectangle.rs index 75d2656..f18d64c 100644 --- a/hui-painter/src/paint/command/rectangle.rs +++ b/hui-painter/src/paint/command/rectangle.rs @@ -1,16 +1,37 @@ -use glam::{Vec2, vec2}; +use std::num::NonZeroU16; +use glam::{vec2, Vec2, Vec4}; use hui_shared::{color, rect::{Corners, FillColor}}; -use crate::paint::{ - buffer::PaintBuffer, - command::PaintCommand, +use crate::{ + paint::{ + buffer::{PaintBuffer, Vertex}, + command::PaintCommand, + }, + texture::TextureHandle, + Painter }; +/// Calculate the number of points based on the maximum corner radius +fn point_count(corners: Corners) -> NonZeroU16 { + //Increase for higher quality + const VTX_PER_CORER_RADIUS_PIXEL: f32 = 0.5; + NonZeroU16::new( + (corners.max_f32() * VTX_PER_CORER_RADIUS_PIXEL).round() as u16 + 2 + ).unwrap() +} + pub struct PaintRectangle { /// Color of the rectangle. pub color: FillColor, + /// Size of the rectangle. + /// + /// (Only different from using transform if the rectangle has border radius.) + pub size: Vec2, + /// Texture to use for the rectangle. - pub texture: Option, + /// + /// Invalid handles will be ignored. + pub texture: Option, /// UV coords inside the texture pub texture_uv: Corners, @@ -18,14 +39,20 @@ pub struct PaintRectangle { /// Border width. pub border_radius: Corners, - /// Border color. - pub border_radius_points_override: Option, + // TODO per-corner border radius point count override + + /// Border radius point count. + /// + /// - If not set, it will be calculated based on the maximum radius. + /// - If set, it will be used for all corners. + pub border_radius_points_override: Option, } impl Default for PaintRectangle { fn default() -> Self { Self { color: color::WHITE.into(), + size: Vec2::ONE, texture: None, texture_uv: Corners { top_left: vec2(0., 0.), @@ -39,8 +66,213 @@ impl Default for PaintRectangle { } } -impl PaintCommand for PaintRectangle { - fn paint(&self, into: &mut PaintBuffer) { - todo!() +impl PaintRectangle { + pub fn from_color(color: impl Into) -> Self { + Self { + color: color.into(), + ..Default::default() + } + } + + pub fn from_texture(texture: TextureHandle) -> Self { + Self { + texture: Some(texture), + color: color::WHITE.into(), + ..Default::default() + } + } + + pub fn from_texture_color(texture: TextureHandle, color: impl Into) -> Self { + Self { + texture: Some(texture), + color: color.into(), + ..Default::default() + } + } +} + +impl PaintCommand for PaintRectangle { + fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) { + // If texture is set: + // - Get texture UV + // - Map local UVs to texture UV coords + // Otherwise, if texture handle is not set or invalid, use the bottom left + // corner of the texture which contains a white pixel. + let uvs = self.texture + .map(|handle| ctx.atlas.get_uv(handle)) + .flatten() + .map(|global_uv| { + let texture_uv = self.texture_uv; + let texture_uv_is_default = + texture_uv.top_left == vec2(0., 0.) && + texture_uv.top_right == vec2(1., 0.) && + texture_uv.bottom_left == vec2(0., 1.) && + texture_uv.bottom_right == vec2(1., 1.); + + if texture_uv_is_default { + global_uv + } else { + let top = global_uv.top_left + .lerp(global_uv.top_right, texture_uv.top_left.x); + let bottom = global_uv.bottom_left + .lerp(global_uv.bottom_right, texture_uv.top_left.x); + let top_left = top + .lerp(bottom, texture_uv.top_left.y); + + let top = global_uv.top_left + .lerp(global_uv.top_right, texture_uv.top_right.x); + let bottom = global_uv.bottom_left + .lerp(global_uv.bottom_right, texture_uv.top_right.x); + let top_right = top + .lerp(bottom, texture_uv.top_right.y); + + let top = global_uv.top_left + .lerp(global_uv.top_right, texture_uv.bottom_left.x); + let bottom = global_uv.bottom_left + .lerp(global_uv.bottom_right, texture_uv.bottom_left.x); + let bottom_left = top + .lerp(bottom, texture_uv.bottom_left.y); + + let top = global_uv.top_left + .lerp(global_uv.top_right, texture_uv.bottom_right.x); + let bottom = global_uv.bottom_left + .lerp(global_uv.bottom_right, texture_uv.bottom_right.x); + let bottom_right = top + .lerp(bottom, texture_uv.bottom_right.y); + + Corners { top_left, top_right, bottom_left, bottom_right } + } + }) + .unwrap_or(Corners::all(Vec2::ZERO)); // For non-textured rectangles + + // Get corner colors + let colors = self.color.corners(); + + // Get the base index for the vertices + let idx_base = into.vertices.len() as u32; + + if self.border_radius.max_f32() == 0. { + // No border radius: + // Draw a simple quad (2 tris) + let indices = Corners { + top_left: idx_base + 0, + top_right: idx_base + 1, + bottom_left: idx_base + 2, + bottom_right: idx_base + 3, + }; + into.indices.extend([ + indices.top_left, indices.bottom_left, indices.top_right, + indices.top_right, indices.bottom_left, indices.bottom_right, + ]); + into.vertices.extend([ + Vertex { + position: vec2(0., 0.) * self.size, + uv: uvs.top_left, + color: colors.top_left, + }, + Vertex { + position: vec2(1., 0.) * self.size, + uv: uvs.top_right, + color: colors.top_right, + }, + Vertex { + position: vec2(0., 1.) * self.size, + uv: uvs.bottom_left, + color: colors.bottom_left, + }, + Vertex { + position: vec2(1., 1.) * self.size, + uv: uvs.bottom_right, + color: colors.bottom_right, + }, + ]); + } else { + // Yes border radius :3 + // Draw a rounded rectangle with the given border radius and point count + + let point_count = self.border_radius_points_override + .unwrap_or(point_count(self.border_radius)) + .get(); + + // Get vertex for a point in scaled pixel space + let point_impl = |point: Vec2| { + let point_uv = point / self.size; + let color_at_point = + colors.bottom_right * point_uv.x * point_uv.y + + colors.top_right * point_uv.x * (1. - point_uv.y) + + colors.bottom_left * (1. - point_uv.x) * point_uv.y + + colors.top_left * (1. - point_uv.x) * (1. - point_uv.y); + let uv_at_point = + uvs.bottom_right * point_uv.x * point_uv.y + + uvs.top_right * point_uv.x * (1. - point_uv.y) + + uvs.bottom_left * (1. - point_uv.x) * point_uv.y + + uvs.top_left * (1. - point_uv.x) * (1. - point_uv.y); + Vertex { + position: point, + color: color_at_point, + uv: uv_at_point, + } + }; + + into.vertices.reserve(point_count as usize * 4); + into.indices.reserve((point_count as usize - 1) * 12 * 4); + + for i in 0..point_count as u32 { + let frac = i as f32 / (point_count - 1) as f32; + let angle = frac * std::f32::consts::PI * 0.5; + let x = angle.sin(); + let y = angle.cos(); + into.vertices.extend([ + point_impl(vec2(x, 1. - y) * self.border_radius.top_right + vec2(self.size.x - self.border_radius.top_right, 0.)), + point_impl(vec2(x - 1., y) * self.border_radius.bottom_right + vec2(self.size.x, self.size.y - self.border_radius.bottom_right)), + point_impl(vec2(1. - x, y) * self.border_radius.bottom_left + vec2(0., self.size.y - self.border_radius.bottom_left)), + point_impl(vec2(1. - x, 1. - y) * self.border_radius.top_left), + ]); + if i > 0 { + // mental illness: + into.indices.extend([ + //Top-right corner + idx_base, + idx_base + 1 + (i - 1) * 4, + idx_base + 1 + i * 4, + //Bottom-right corner + idx_base, + idx_base + 1 + (i - 1) * 4 + 1, + idx_base + 1 + i * 4 + 1, + //Bottom-left corner + idx_base, + idx_base + 1 + (i - 1) * 4 + 2, + idx_base + 1 + i * 4 + 2, + //Top-left corner + idx_base, + idx_base + 1 + (i - 1) * 4 + 3, + idx_base + 1 + i * 4 + 3, + ]); + } + + //Fill in the rest + //mental illness 2: + into.indices.extend([ + //Top + idx_base, + idx_base + 4, + idx_base + 1, + //Right?, i think + idx_base, + idx_base + 1 + (point_count as u32 - 1) * 4, + idx_base + 1 + (point_count as u32 - 1) * 4 + 1, + //Left??? + idx_base, + idx_base + 1 + (point_count as u32 - 1) * 4 + 2, + idx_base + 1 + (point_count as u32 - 1) * 4 + 3, + //Bottom??? + idx_base, + idx_base + 3, + idx_base + 2, + ]); + } + + unimplemented!("Border radius is not supported yet"); + } } } diff --git a/hui-painter/src/paint/command/text.rs b/hui-painter/src/paint/command/text.rs index 51c9fdd..c4c2ff5 100644 --- a/hui-painter/src/paint/command/text.rs +++ b/hui-painter/src/paint/command/text.rs @@ -1,14 +1,43 @@ -use crate::paint::{ +use std::{borrow::Cow, sync::Arc}; +use fontdue::layout::{CoordinateSystem, Layout}; +use crate::{paint::{ buffer::PaintBuffer, command::PaintCommand, -}; +}, Painter}; + +pub struct FontHandle(Arc); + +pub struct TextChunk { + pub text: Cow<'static, str>, + pub font: FontHandle, + pub size: f32, +} pub struct PaintText { - //TODO: PaintText command + // TODO multiple text chunks + pub text: TextChunk, +} + +impl PaintText { + pub fn new(text: impl Into>, size: f32) -> Self { + Self { + text: TextChunk { + text: text.into(), + font: todo!(), + size, + } + } + } } impl PaintCommand for PaintText { - fn paint(&self, into: &mut PaintBuffer) { + fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) { + // let mut layout = Layout::new(CoordinateSystem::PositiveYDown); + // layout.append( + // &[text_renderer.internal_font(*font_handle)], + // &TextStyle::new(text, *size as f32, 0) + // ); + todo!() } } diff --git a/hui-painter/src/paint/command/transform.rs b/hui-painter/src/paint/command/transform.rs index 709cac7..403fc10 100644 --- a/hui-painter/src/paint/command/transform.rs +++ b/hui-painter/src/paint/command/transform.rs @@ -1,7 +1,7 @@ -use crate::paint::{ +use crate::{paint::{ buffer::PaintBuffer, command::PaintCommand, -}; +}, Painter}; //TODO: use generics instead @@ -11,19 +11,30 @@ pub struct PaintTransform { } impl PaintCommand for PaintTransform { - fn paint(&self, into: &mut PaintBuffer) { + fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) { // remember the starting index let starting_index = into.vertices.len(); // paint children nodes for child in &self.children { - child.paint(into); + child.paint(ctx, into); + } + + let mut min_point = glam::Vec2::splat(f32::MAX); + let mut max_point = glam::Vec2::splat(f32::MIN); + for vtx in &into.vertices[starting_index..] { + min_point = min_point.min(vtx.position); + max_point = max_point.max(vtx.position); } // trans the children in-place for vtx in &mut into.vertices[starting_index..] { - //TODO fix for rotation around the center of the object + //HACK: to match the old behavior: + //(shift the origin to the center before transforming) + let offset = (max_point + min_point) / 2.0; + vtx.position -= offset; vtx.position = self.transform.transform_point2(vtx.position); + vtx.position += offset; } } } diff --git a/hui-painter/src/texture/atlas.rs b/hui-painter/src/texture/atlas.rs index ba949bc..1a2e1d1 100644 --- a/hui-painter/src/texture/atlas.rs +++ b/hui-painter/src/texture/atlas.rs @@ -1,4 +1,5 @@ -use glam::{UVec2, uvec2, ivec2}; +use glam::{ivec2, uvec2, vec2, UVec2, Vec2}; +use hui_shared::rect::Corners; use rect_packer::DensePacker; use hashbrown::HashMap; use nohash_hasher::BuildNoHashHasher; @@ -342,6 +343,22 @@ impl TextureAtlas { handle } + + /// Get uv coordinates for the texture handle. + pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option> { + let TextureAllocation { offset, size, .. } = self.allocations + .get(&handle.id)?; + let p0x = offset.x as f32 / self.size.x as f32; + let p1x = (offset.x as f32 + size.x as f32) / self.size.x as f32; + let p0y = offset.y as f32 / self.size.y as f32; + let p1y = (offset.y as f32 + size.y as f32) / self.size.y as f32; + Some(Corners { + top_left: vec2(p0x, p0y), + top_right: vec2(p1x, p0y), + bottom_left: vec2(p0x, p1y), + bottom_right: vec2(p1x, p1y), + }) + } } impl Default for TextureAtlas {