diff --git a/hui-glium/src/lib.rs b/hui-glium/src/lib.rs index d75651b..ccafb3b 100644 --- a/hui-glium/src/lib.rs +++ b/hui-glium/src/lib.rs @@ -153,11 +153,11 @@ impl GliumUiRenderer { } pub fn update(&mut self, instance: &UiInstance) { - if self.ui_texture.is_none() || instance.atlas().modified { - self.update_texture_atlas(&instance.atlas()); + if self.ui_texture.is_none() || instance.backend_atlas().modified { + self.update_texture_atlas(&instance.backend_atlas()); } - if self.buffer_pair.is_none() || instance.draw_call().0 { - self.update_buffers(instance.draw_call().1); + if self.buffer_pair.is_none() || instance.backend_paint_buffer().0 { + self.update_buffers(instance.backend_paint_buffer().1); } } diff --git a/hui-painter/src/backend.rs b/hui-painter/src/backend.rs new file mode 100644 index 0000000..0ffdd02 --- /dev/null +++ b/hui-painter/src/backend.rs @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/hui-painter/src/lib.rs b/hui-painter/src/lib.rs index 1983e57..56c8e2a 100644 --- a/hui-painter/src/lib.rs +++ b/hui-painter/src/lib.rs @@ -2,6 +2,7 @@ pub mod paint; pub mod texture; pub mod text; pub mod util; +pub mod backend; use text::FontManager; use texture::TextureAtlas; @@ -9,8 +10,8 @@ use texture::TextureAtlas; /// Painter instance, stores textures and fonts needed for rendering #[derive(Default)] pub struct PainterInstance { - pub atlas: TextureAtlas, - pub fonts: FontManager, + pub(crate) textures: TextureAtlas, + pub(crate) fonts: FontManager, } impl PainterInstance { @@ -18,6 +19,26 @@ impl PainterInstance { Self::default() } + /// Get an immutable reference to the texture atlas + pub fn textures(&self) -> &TextureAtlas { + &self.textures + } + + /// Get a mutable reference to the texture atlas + pub fn textures_mut(&mut self) -> &mut TextureAtlas { + &mut self.textures + } + + /// Get an immutable reference to the font manager + pub fn fonts(&self) -> &FontManager { + &self.fonts + } + + /// Get a mutable reference to the font manager + pub fn fonts_mut(&mut self) -> &mut FontManager { + &mut self.fonts + } + // pub fn atlas(&self) -> &TextureAtlas { // &self.atlas // } diff --git a/hui-painter/src/paint/buffer.rs b/hui-painter/src/paint/buffer.rs index 8244049..84f1a34 100644 --- a/hui-painter/src/paint/buffer.rs +++ b/hui-painter/src/paint/buffer.rs @@ -18,6 +18,11 @@ impl PaintBuffer { indices: Vec::new(), } } + + pub fn clear(&mut self) { + self.vertices.clear(); + self.indices.clear(); + } } impl Default for PaintBuffer { diff --git a/hui-painter/src/paint/command.rs b/hui-painter/src/paint/command.rs index 749649e..b459b9c 100644 --- a/hui-painter/src/paint/command.rs +++ b/hui-painter/src/paint/command.rs @@ -1,6 +1,3 @@ -use std::hash::Hash; - -use glam::Vec2; use hui_shared::rect::Rect; use crate::{paint::buffer::PaintBuffer, PainterInstance}; @@ -16,8 +13,7 @@ pub use transform::PaintTransform; mod rectangle; pub use rectangle::PaintRectangle; -mod text; -pub use text::PaintText; +pub mod text; pub trait PaintCommand { /// Called before actual paint command is executed\ diff --git a/hui-painter/src/paint/command/list.rs b/hui-painter/src/paint/command/list.rs index 658638b..4b48039 100644 --- a/hui-painter/src/paint/command/list.rs +++ b/hui-painter/src/paint/command/list.rs @@ -23,6 +23,10 @@ impl PaintList { pub fn add(&mut self, command: impl PaintCommand + 'static) { self.commands.push(Box::new(command)); } + + pub fn clear(&mut self) { + self.commands.clear(); + } } impl Default for PaintList { diff --git a/hui-painter/src/paint/command/rectangle.rs b/hui-painter/src/paint/command/rectangle.rs index 42672d2..a3ca053 100644 --- a/hui-painter/src/paint/command/rectangle.rs +++ b/hui-painter/src/paint/command/rectangle.rs @@ -105,7 +105,7 @@ impl PaintCommand for PaintRectangle { // 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 - .and_then(|handle| ctx.atlas.get_uv(handle)) + .and_then(|handle| ctx.textures.get_uv(handle)) .map(|global_uv| { let texture_uv = self.texture_uv; let texture_uv_is_default = diff --git a/hui-painter/src/paint/command/text.rs b/hui-painter/src/paint/command/text.rs index 251466f..58b76a2 100644 --- a/hui-painter/src/paint/command/text.rs +++ b/hui-painter/src/paint/command/text.rs @@ -66,7 +66,7 @@ impl PaintCommand for PaintText { let layout = self.build_layout(&font_array); for glyph in layout.glyphs() { - ctx.fonts.render_glyph(&mut ctx.atlas, self.text.font, glyph.key); + ctx.fonts.render_glyph(&mut ctx.textures, self.text.font, glyph.key); } } @@ -89,28 +89,28 @@ impl PaintCommand for PaintText { let font_handle = self.text.font; // TODO use font_index here let vidx = into.vertices.len() as u32; - let glyph_texture = ctx.fonts.render_glyph(&mut ctx.atlas, font_handle, glyph.key); - let uv = ctx.atlas.get_uv(glyph_texture).unwrap(); + let glyph_texture = ctx.fonts.render_glyph(&mut ctx.textures, font_handle, glyph.key); + let uv = ctx.textures.get_uv(glyph_texture).unwrap(); into.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); into.vertices.extend([ Vertex { - position: vec2(glyph.x, glyph.y), + position: vec2(glyph.x, glyph.y).round(), color: self.text.color, uv: uv.top_left, }, Vertex { - position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y), + position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y).round().round(), color: self.text.color, uv: uv.top_right, }, Vertex { - position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y + glyph_texture.size().y as f32), + position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y + glyph_texture.size().y as f32).round(), color: self.text.color, uv: uv.bottom_right, }, Vertex { - position: vec2(glyph.x, glyph.y + glyph_texture.size().y as f32), + position: vec2(glyph.x, glyph.y + glyph_texture.size().y as f32).round(), color: self.text.color, uv: uv.bottom_left, }, diff --git a/hui-painter/src/text.rs b/hui-painter/src/text.rs index 03c51b7..f6398c4 100644 --- a/hui-painter/src/text.rs +++ b/hui-painter/src/text.rs @@ -19,10 +19,10 @@ impl FontManager { } } - /// Add a font to the manager. + /// Add a font to the manager from raw font file data. /// /// Panics: - /// - If the font data is invalid. + /// - If the font data is invalid or corrupted pub fn add(&mut self, data: &[u8]) -> FontHandle { let font = self.fonts.add_font(data); self.ftm.init_font(font); diff --git a/hui-painter/src/texture.rs b/hui-painter/src/texture.rs index df970b7..261f273 100644 --- a/hui-painter/src/texture.rs +++ b/hui-painter/src/texture.rs @@ -1,2 +1,417 @@ -mod atlas; -pub use atlas::*; +use glam::{ivec2, uvec2, vec2, UVec2, Vec2}; +use hui_shared::rect::Corners; +use rect_packer::DensePacker; +use hashbrown::HashMap; +use nohash_hasher::BuildNoHashHasher; + +//TODO support rotation +const DEFAULT_ATLAS_SIZE: UVec2 = uvec2(128, 128); +// const ALLOW_ROTATION: bool = false; + +// Destination format is always RGBA +const RGBA_BYTES_PER_PIXEL: usize = 4; + +/// Assert that the passed texture size is valid, panicking if it's not. +/// +/// - The size must be greater than 0. +/// - The size must be less than `i32::MAX`. +fn assert_size(size: UVec2) { + assert!( + size.x > 0 && + size.y > 0, + "size must be greater than 0" + ); + assert!( + size.x <= i32::MAX as u32 && + size.y <= i32::MAX as u32, + "size must be less than i32::MAX" + ); +} + +/// The format of the source texture data to use when updating a texture in the atlas. +#[derive(Clone, Copy, Debug, Default)] +pub enum SourceTextureFormat { + /// RGBA, 8-bit per channel + #[default] + RGBA8, + + //TODO native-endian RGBA32 format + + /// ARGB, 8-bit per channel + ARGB8, + + /// BGRA, 8-bit per channel + BGRA8, + + /// ABGR, 8-bit per channel + ABGR8, + + /// RGB, 8-bit per channel (Alpha = 255) + RGB8, + + /// BGR, 8-bit per channel (Alpha = 255) + BGR8, + + /// Alpha only, 8-bit per channel (RGB = #ffffff) + A8, +} + +impl SourceTextureFormat { + pub const fn bytes_per_pixel(&self) -> usize { + match self { + SourceTextureFormat::RGBA8 | + SourceTextureFormat::ARGB8 | + SourceTextureFormat::BGRA8 | + SourceTextureFormat::ABGR8 => 4, + SourceTextureFormat::RGB8 | + SourceTextureFormat::BGR8 => 3, + SourceTextureFormat::A8 => 1, + } + } +} + +type TextureId = u32; + +/// A handle to a texture in the texture atlas. +/// +/// Can be cheaply copied and passed around.\ +/// The handle is only valid for the texture atlas it was created from. +#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] +pub struct TextureHandle { + pub(crate) id: TextureId, + pub(crate) size: UVec2, +} + +impl TextureHandle { + /// Create a new broken texture handle. + pub fn new_broken() -> Self { + Self { + id: u32::MAX, + size: uvec2(0, 0), + } + } + + pub fn size(&self) -> UVec2 { + self.size + } +} + +/// Represents an area allocated to a specific texture handle in the texture atlas. +struct TextureAllocation { + /// Corresponding copyable texture handle + handle: TextureHandle, + + /// The offset of the allocation in the atlas, in pixels + offset: UVec2, + + /// The requested size of the allocation, in pixels + size: UVec2, + + /// The maximum size of the allocation, used for reusing deallocated allocations + /// + /// Usually equal to `size`, but may be larger than the requested size + /// if the allocation was reused by a smaller texture at some point + max_size: UVec2, +} + +impl TextureAllocation { + /// Create a new texture allocation with the specified parameters. + /// + /// The `max_size` parameter will be set equal to `size`. + pub fn new(handle: TextureHandle, offset: UVec2, size: UVec2) -> Self { + Self { + handle, + offset, + size, + max_size: size, + } + } +} + +pub struct TextureAtlasBackendData<'a> { + pub data: &'a [u8], + pub size: UVec2, + pub version: u64, +} + +/// A texture atlas that can be used to pack multiple textures into a single texture. +pub struct TextureAtlas { + /// The size of the atlas, in pixels + size: UVec2, + + /// The texture data of the atlas, ALWAYS in RGBA8 format + data: Vec<u8>, + + /// The packer used to allocate space for textures in the atlas + packer: DensePacker, + + /// The next id to be used for a texture handle\ + /// Gets incremented every time a new texture is allocated + next_id: TextureId, + + /// Active allocated textures, indexed by id of their handle + allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>, + + /// Deallocated allocations that can be reused, sorted by size + //TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now + reuse_allocations: Vec<TextureAllocation>, + + /// Version of the texture atlas, incremented every time the atlas is modified + version: u64, +} + +impl TextureAtlas { + /// Create a new texture atlas with the specified size. + pub(crate) fn new(size: UVec2) -> Self { + assert_size(size); + let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL; + Self { + size, + data: vec![0; data_bytes], + packer: DensePacker::new( + size.x as i32, + size.y as i32, + ), + next_id: 0, + allocations: HashMap::default(), + reuse_allocations: Vec::new(), + version: 0, + } + } + + /// The version of the atlas, incremented every time the atlas is modified. + pub fn version(&self) -> u64 { + self.version + } + + /// The underlying texture data of the atlas, in RGBA8 format. + pub fn data_rgba(&self) -> &[u8] { + &self.data + } + + /// Get data needed by the backend implementation. + pub fn backend_data(&self) -> TextureAtlasBackendData { + TextureAtlasBackendData { + data: &self.data, + size: self.size, + version: self.version, + } + } + + /// Increment the version of the atlas + fn increment_version(&mut self) { + // XXX: wrapping_add? will this *ever* overflow? + self.version = self.version.wrapping_add(1); + } + + /// Get the next handle + /// + /// Does not allocate a texture associated with it + /// This handle will be invalid until it's associated with a texture. + /// + /// Used internally in `allocate` and `allocate_with_data`. + fn next_handle(&mut self, size: UVec2) -> TextureHandle { + let handle = TextureHandle { + id: self.next_id, + size, + }; + self.next_id += 1; + handle + } + + /// Allocate a texture in the atlas, returning a handle to it.\ + /// The data present in the texture is undefined, and may include garbage data. + /// + /// # Panics + /// - If any of the dimensions of the texture are zero or exceed `i32::MAX`. + pub fn allocate(&mut self, size: UVec2) -> TextureHandle { + assert_size(size); + + // Check if any deallocated allocations can be reused + // Find the smallest allocation that fits the requested size + // (The list is already sorted by size) + for (idx, allocation) in self.reuse_allocations.iter().enumerate() { + if allocation.max_size.x >= size.x && allocation.max_size.y >= size.y { + let allocation = self.reuse_allocations.remove(idx); + let handle = self.next_handle(size); + unsafe { + self.allocations.insert_unique_unchecked(handle.id, TextureAllocation { + handle, + offset: allocation.offset, + size, + max_size: allocation.max_size, + }); + } + return handle; + } + } + + // Pack the texture + let pack = self.packer.pack( + size.x as i32, + size.y as i32, + false + ); + + //TODO: handle pack failure by either resizing the atlas or returning an error + let pack = pack.unwrap(); + let offset = ivec2(pack.x, pack.y).as_uvec2(); + + // Allocate the texture + let handle = self.next_handle(size); + let allocation = TextureAllocation::new(handle, offset, size); + unsafe { + self.allocations.insert_unique_unchecked(handle.id, allocation); + } + + handle + } + + /// Deallocate a texture in the atlas, allowing its space to be reused by future allocations. + /// + /// # Panics + /// - If the texture handle is invalid for this atlas. + pub fn deallocate(&mut self, handle: TextureHandle) { + // Remove the allocation from the active allocations + let allocation = self.allocations + .remove(&handle.id) + .expect("invalid texture handle"); + + // TODO: this is not the most efficient way to do this: + // And put it in the reuse allocations queue + self.reuse_allocations.push(allocation); + self.reuse_allocations.sort_unstable_by_key(|a| a.size.x * a.size.y); + } + + /// Update the data of a texture in the atlas.\ + /// The texture must have been previously allocated with `allocate` or `allocate_with_data`. + /// + /// The source data must be in the format specified by the `format` parameter.\ + /// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.) + /// + /// The function will silently ignore any data that doesn't fit in the texture. + /// + /// # Panics + /// - If the texture handle is invalid for this atlas. + /// - The length of the data array is less than the size of the texture. + pub fn update(&mut self, handle: TextureHandle, format: SourceTextureFormat, data: &[u8]) { + assert!( + data.len() >= handle.size.x as usize * handle.size.y as usize * format.bytes_per_pixel(), + "data length must be at least the size of the texture" + ); + + let bpp = format.bytes_per_pixel(); + + let TextureAllocation { size, offset, ..} = self.allocations + .get(&handle.id) + .expect("invalid texture handle"); + + for y in 0..size.y { + for x in 0..size.x { + let src_idx = (y * size.x + x) as usize * bpp; + let dst_idx: usize = ( + (offset.y + y) * size.x + + (offset.x + x) + ) as usize * RGBA_BYTES_PER_PIXEL; + + let src = &data[src_idx..src_idx + bpp]; + let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL]; + + match format { + SourceTextureFormat::RGBA8 => { + dst.copy_from_slice(src); + }, + SourceTextureFormat::ARGB8 => { + dst[..3].copy_from_slice(&src[1..]); + dst[3] = src[0]; + }, + SourceTextureFormat::BGRA8 => { + dst.copy_from_slice(src); + dst.rotate_right(1); + dst.reverse(); + }, + SourceTextureFormat::ABGR8 => { + dst.copy_from_slice(src); + dst.reverse(); + }, + SourceTextureFormat::RGB8 => { + dst[..3].copy_from_slice(src); + dst[3] = 0xff; + }, + SourceTextureFormat::BGR8 => { + dst[..3].copy_from_slice(src); + dst[..3].reverse(); + dst[3] = 0xff; + }, + SourceTextureFormat::A8 => { + dst[..3].fill(0xff); + dst[3] = src[0]; + }, + } + } + } + + self.increment_version(); + } + + /// Allocate a texture in the atlas, returning a handle to it.\ + /// The texture is initialized with the provided data. + /// + /// The source data must be in the format specified by the `format` parameter.\ + /// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.) + /// + /// # Panics + /// - If any of the dimensions of the texture are zero or exceed `i32::MAX`. + /// - The length of the data array is zero or not a multiple of the stride (stride = width * bytes per pixel). + pub fn allocate_with_data(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle { + assert!( + !data.is_empty(), + "texture data must not be empty" + ); + + // Calculate the stride of the texture + let bytes_per_pixel = format.bytes_per_pixel(); + let stride = bytes_per_pixel * width; + assert_eq!( + data.len() % stride, 0, + "texture data must be a multiple of the stride", + ); + + // Calculate the size of the texture + let size = uvec2( + width as u32, + (data.len() / stride) as u32, + ); + assert_size(size); + + // Allocate the texture + let handle = self.allocate(size); + + // Write the data to the texture + self.update(handle, format, data); + + handle + } + + /// Get uv coordinates for the texture handle. + pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option<Corners<Vec2>> { + 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 { + fn default() -> Self { + Self::new(DEFAULT_ATLAS_SIZE) + } +} diff --git a/hui-painter/src/texture/atlas.rs b/hui-painter/src/texture/atlas.rs deleted file mode 100644 index 55e5cc9..0000000 --- a/hui-painter/src/texture/atlas.rs +++ /dev/null @@ -1,387 +0,0 @@ -use glam::{ivec2, uvec2, vec2, UVec2, Vec2}; -use hui_shared::rect::Corners; -use rect_packer::DensePacker; -use hashbrown::HashMap; -use nohash_hasher::BuildNoHashHasher; - -//TODO support rotation -const DEFAULT_ATLAS_SIZE: UVec2 = uvec2(128, 128); -// const ALLOW_ROTATION: bool = false; - -// Destination format is always RGBA -const RGBA_BYTES_PER_PIXEL: usize = 4; - -/// Assert that the passed texture size is valid, panicking if it's not. -/// -/// - The size must be greater than 0. -/// - The size must be less than `i32::MAX`. -fn assert_size(size: UVec2) { - assert!( - size.x > 0 && - size.y > 0, - "size must be greater than 0" - ); - assert!( - size.x <= i32::MAX as u32 && - size.y <= i32::MAX as u32, - "size must be less than i32::MAX" - ); -} - -/// The format of the source texture data to use when updating a texture in the atlas. -#[derive(Clone, Copy, Debug, Default)] -pub enum SourceTextureFormat { - /// RGBA, 8-bit per channel - #[default] - RGBA8, - - //TODO native-endian RGBA32 format - - /// ARGB, 8-bit per channel - ARGB8, - - /// BGRA, 8-bit per channel - BGRA8, - - /// ABGR, 8-bit per channel - ABGR8, - - /// RGB, 8-bit per channel (Alpha = 255) - RGB8, - - /// BGR, 8-bit per channel (Alpha = 255) - BGR8, - - /// Alpha only, 8-bit per channel (RGB = #ffffff) - A8, -} - -impl SourceTextureFormat { - pub const fn bytes_per_pixel(&self) -> usize { - match self { - SourceTextureFormat::RGBA8 | - SourceTextureFormat::ARGB8 | - SourceTextureFormat::BGRA8 | - SourceTextureFormat::ABGR8 => 4, - SourceTextureFormat::RGB8 | - SourceTextureFormat::BGR8 => 3, - SourceTextureFormat::A8 => 1, - } - } -} - -type TextureId = u32; - -/// A handle to a texture in the texture atlas. -/// -/// Can be cheaply copied and passed around.\ -/// The handle is only valid for the texture atlas it was created from. -#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] -pub struct TextureHandle { - pub(crate) id: TextureId, - pub(crate) size: UVec2, -} - -impl TextureHandle { - pub fn size(&self) -> UVec2 { - self.size - } -} - -/// Represents an area allocated to a specific texture handle in the texture atlas. -struct TextureAllocation { - /// Corresponding copyable texture handle - handle: TextureHandle, - - /// The offset of the allocation in the atlas, in pixels - offset: UVec2, - - /// The requested size of the allocation, in pixels - size: UVec2, - - /// The maximum size of the allocation, used for reusing deallocated allocations - /// - /// Usually equal to `size`, but may be larger than the requested size - /// if the allocation was reused by a smaller texture at some point - max_size: UVec2, -} - -impl TextureAllocation { - /// Create a new texture allocation with the specified parameters. - /// - /// The `max_size` parameter will be set equal to `size`. - pub fn new(handle: TextureHandle, offset: UVec2, size: UVec2) -> Self { - Self { - handle, - offset, - size, - max_size: size, - } - } -} - -/// A texture atlas that can be used to pack multiple textures into a single texture. -pub struct TextureAtlas { - /// The size of the atlas, in pixels - size: UVec2, - - /// The texture data of the atlas, ALWAYS in RGBA8 format - data: Vec<u8>, - - /// The packer used to allocate space for textures in the atlas - packer: DensePacker, - - /// The next id to be used for a texture handle\ - /// Gets incremented every time a new texture is allocated - next_id: TextureId, - - /// Active allocated textures, indexed by id of their handle - allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>, - - /// Deallocated allocations that can be reused, sorted by size - //TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now - reuse_allocations: Vec<TextureAllocation>, - - /// Version of the texture atlas, incremented every time the atlas is modified - version: u64, -} - -impl TextureAtlas { - /// Create a new texture atlas with the specified size. - pub(crate) fn new(size: UVec2) -> Self { - assert_size(size); - let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL; - Self { - size, - data: vec![0; data_bytes], - packer: DensePacker::new( - size.x as i32, - size.y as i32, - ), - next_id: 0, - allocations: HashMap::default(), - reuse_allocations: Vec::new(), - version: 0, - } - } - - pub fn version(&self) -> u64 { - self.version - } - - fn mark_modified(&mut self) { - // XXX: wrapping_add? will this *ever* overflow? - self.version = self.version.wrapping_add(1); - } - - /// Get the next handle - /// - /// Does not allocate a texture associated with it - /// This handle will be invalid until it's associated with a texture. - /// - /// Used internally in `allocate` and `allocate_with_data`. - fn next_handle(&mut self, size: UVec2) -> TextureHandle { - let handle = TextureHandle { - id: self.next_id, - size, - }; - self.next_id += 1; - handle - } - - /// Allocate a texture in the atlas, returning a handle to it.\ - /// The data present in the texture is undefined, and may include garbage data. - /// - /// # Panics - /// - If any of the dimensions of the texture are zero or exceed `i32::MAX`. - pub fn allocate(&mut self, size: UVec2) -> TextureHandle { - assert_size(size); - - // Check if any deallocated allocations can be reused - // Find the smallest allocation that fits the requested size - // (The list is already sorted by size) - for (idx, allocation) in self.reuse_allocations.iter().enumerate() { - if allocation.max_size.x >= size.x && allocation.max_size.y >= size.y { - let allocation = self.reuse_allocations.remove(idx); - let handle = self.next_handle(size); - unsafe { - self.allocations.insert_unique_unchecked(handle.id, TextureAllocation { - handle, - offset: allocation.offset, - size, - max_size: allocation.max_size, - }); - } - return handle; - } - } - - // Pack the texture - let pack = self.packer.pack( - size.x as i32, - size.y as i32, - false - ); - - //TODO: handle pack failure by either resizing the atlas or returning an error - let pack = pack.unwrap(); - let offset = ivec2(pack.x, pack.y).as_uvec2(); - - // Allocate the texture - let handle = self.next_handle(size); - let allocation = TextureAllocation::new(handle, offset, size); - unsafe { - self.allocations.insert_unique_unchecked(handle.id, allocation); - } - - handle - } - - /// Deallocate a texture in the atlas, allowing its space to be reused by future allocations. - /// - /// # Panics - /// - If the texture handle is invalid for this atlas. - pub fn deallocate(&mut self, handle: TextureHandle) { - // Remove the allocation from the active allocations - let allocation = self.allocations - .remove(&handle.id) - .expect("invalid texture handle"); - - // TODO: this is not the most efficient way to do this: - // And put it in the reuse allocations queue - self.reuse_allocations.push(allocation); - self.reuse_allocations.sort_unstable_by_key(|a| a.size.x * a.size.y); - } - - /// Update the data of a texture in the atlas.\ - /// The texture must have been previously allocated with `allocate` or `allocate_with_data`. - /// - /// The source data must be in the format specified by the `format` parameter.\ - /// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.) - /// - /// The function will silently ignore any data that doesn't fit in the texture. - /// - /// # Panics - /// - If the texture handle is invalid for this atlas. - /// - The length of the data array is less than the size of the texture. - pub fn update(&mut self, handle: TextureHandle, format: SourceTextureFormat, data: &[u8]) { - assert!( - data.len() >= handle.size.x as usize * handle.size.y as usize * format.bytes_per_pixel(), - "data length must be at least the size of the texture" - ); - - let bpp = format.bytes_per_pixel(); - - let TextureAllocation { size, offset, ..} = self.allocations - .get(&handle.id) - .expect("invalid texture handle"); - - for y in 0..size.y { - for x in 0..size.x { - let src_idx = (y * size.x + x) as usize * bpp; - let dst_idx: usize = ( - (offset.y + y) * size.x + - (offset.x + x) - ) as usize * RGBA_BYTES_PER_PIXEL; - - let src = &data[src_idx..src_idx + bpp]; - let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL]; - - match format { - SourceTextureFormat::RGBA8 => { - dst.copy_from_slice(src); - }, - SourceTextureFormat::ARGB8 => { - dst[..3].copy_from_slice(&src[1..]); - dst[3] = src[0]; - }, - SourceTextureFormat::BGRA8 => { - dst.copy_from_slice(src); - dst.rotate_right(1); - dst.reverse(); - }, - SourceTextureFormat::ABGR8 => { - dst.copy_from_slice(src); - dst.reverse(); - }, - SourceTextureFormat::RGB8 => { - dst[..3].copy_from_slice(src); - dst[3] = 0xff; - }, - SourceTextureFormat::BGR8 => { - dst[..3].copy_from_slice(src); - dst[..3].reverse(); - dst[3] = 0xff; - }, - SourceTextureFormat::A8 => { - dst[..3].fill(0xff); - dst[3] = src[0]; - }, - } - } - } - - self.mark_modified(); - } - - /// Allocate a texture in the atlas, returning a handle to it.\ - /// The texture is initialized with the provided data. - /// - /// The source data must be in the format specified by the `format` parameter.\ - /// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.) - /// - /// # Panics - /// - If any of the dimensions of the texture are zero or exceed `i32::MAX`. - /// - The length of the data array is zero or not a multiple of the stride (stride = width * bytes per pixel). - pub fn allocate_with_data(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle { - assert!( - !data.is_empty(), - "texture data must not be empty" - ); - - // Calculate the stride of the texture - let bytes_per_pixel = format.bytes_per_pixel(); - let stride = bytes_per_pixel * width; - assert_eq!( - data.len() % stride, 0, - "texture data must be a multiple of the stride", - ); - - // Calculate the size of the texture - let size = uvec2( - width as u32, - (data.len() / stride) as u32, - ); - assert_size(size); - - // Allocate the texture - let handle = self.allocate(size); - - // Write the data to the texture - self.update(handle, format, data); - - handle - } - - /// Get uv coordinates for the texture handle. - pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option<Corners<Vec2>> { - 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 { - fn default() -> Self { - Self::new(DEFAULT_ATLAS_SIZE) - } -} diff --git a/hui-wgpu/src/lib.rs b/hui-wgpu/src/lib.rs index 3c2fbed..389f5d0 100644 --- a/hui-wgpu/src/lib.rs +++ b/hui-wgpu/src/lib.rs @@ -290,12 +290,12 @@ impl WgpuUiRenderer { device: &wgpu::Device, resolution: Vec2, ) { - let (modified, call) = instance.draw_call(); + let (modified, call) = instance.backend_paint_buffer(); if self.modified || modified { self.update_buffers(call, queue, device, resolution); } - let meta = instance.atlas(); + let meta = instance.backend_atlas(); if self.modified || meta.modified { self.update_texture(meta, queue, device); } diff --git a/hui/Cargo.toml b/hui/Cargo.toml index d948023..e15eda8 100644 --- a/hui/Cargo.toml +++ b/hui/Cargo.toml @@ -18,7 +18,7 @@ include = [ [dependencies] hui-derive = { version = "0.1.0-alpha.6", path = "../hui-derive", optional = true } hui-shared = { version = "0.1.0-alpha.6", path = "../hui-shared" } -# hui-painter = { version = "0.1.0-alpha.6", path = "../hui-painter" } +hui-painter = { version = "0.1.0-alpha.6", path = "../hui-painter" } hashbrown = "0.15" nohash-hasher = "0.2" glam = "0.30" @@ -33,7 +33,7 @@ image = { version = "0.25", default-features = false, optional = true } rustc-hash = "2.0" [features] -default = ["el_all", "image", "builtin_font", "pixel_perfect_text", "derive"] +default = ["el_all", "image", "builtin_font", "derive"] ## Enable derive macros derive = ["dep:hui-derive"] @@ -44,17 +44,6 @@ image = ["dep:image"] ## Enable the built-in font (ProggyTiny, adds *35kb* to the executable) builtin_font = [] -#! #### Pixel-perfect rendering: - -## Round all vertex positions to nearest integer coordinates (not recommended) -pixel_perfect = ["pixel_perfect_text"] - -## Apply pixel-perfect rendering hack to text (fixes blurry text rendering) -pixel_perfect_text = [] - -#! Make sure to disable both features if you are not rendering UI "as-is" at 1:1 scale\ -#! For exmaple, you should disable them if using DPI (or any other form of) scaling while passing the virtual resolution to the ui or rendering it in 3d space - #! #### Built-in elements: ## Enable all built-in elements diff --git a/hui/src/draw.rs b/hui/src/draw.rs deleted file mode 100644 index 580da98..0000000 --- a/hui/src/draw.rs +++ /dev/null @@ -1,396 +0,0 @@ -//! draw commands, tesselation and UI rendering. - -use crate::{ - rect::Corners, - text::{FontHandle, TextRenderer} -}; - -pub(crate) mod atlas; -use atlas::TextureAtlasManager; -pub use atlas::{ImageHandle, TextureAtlasMeta, TextureFormat, ImageCtx}; - -mod corner_radius; -pub use corner_radius::RoundedCorners; - -use std::borrow::Cow; -use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; -use glam::{vec2, Vec2, Affine2, Vec4}; - -//TODO: circle draw command - -/// Available draw commands -/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture -/// - Text: Draw text using the specified font, size, color, and position -#[derive(Clone, Debug, PartialEq)] -pub enum UiDrawCommand { - ///Filled, colored rectangle - Rectangle { - ///Position in pixels - position: Vec2, - ///Size in pixels - size: Vec2, - ///Color (RGBA) - color: Corners<Vec4>, - ///Texture - texture: Option<ImageHandle>, - ///Sub-UV coordinates for the texture - texture_uv: Option<Corners<Vec2>>, - ///Rounded corners - rounded_corners: Option<RoundedCorners>, - }, - /// Draw text using the specified font, size, color, and position - Text { - ///Position in pixels - position: Vec2, - ///Font size - size: u16, - ///Color (RGBA) - color: Vec4, - ///Text to draw - text: Cow<'static, str>, - ///Font handle to use - font: FontHandle, - }, - /// Push a transformation matrix to the stack - PushTransform(Affine2), - /// Pop a transformation matrix from the stack - PopTransform, - //TODO PushClip PopClip -} - -/// List of draw commands -#[derive(Default)] -pub struct UiDrawCommandList { - pub commands: Vec<UiDrawCommand>, -} - -impl UiDrawCommandList { - /// Add a draw command to the list - 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) -// } -// } - -/// A vertex for UI rendering -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub struct UiVertex { - pub position: Vec2, - pub color: Vec4, - pub uv: Vec2, -} - -/// Represents a single draw call (vertices + indices), should be handled by the render backend -#[derive(Default)] -pub struct UiDrawCall { - pub vertices: Vec<UiVertex>, - pub indices: Vec<u32>, -} - -impl UiDrawCall { - /// Tesselate the UI and build a complete draw plan from a list of draw commands - pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self { - let mut trans_stack = Vec::new(); - let mut draw_call = UiDrawCall::default(); - - //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; - - for command in &draw_commands.commands { - match command { - 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(f32::INFINITY); - let mut max = Vec2::splat(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; - } - }, - UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => { - let uvs = texture - .map(|x| atlas.get_uv(x)) - .flatten() - .map(|guv| { - if let Some(texture_uv) = texture_uv { - //XXX: this may not work if the texture is rotated - //also is this slow? - - let top = guv.top_left.lerp(guv.top_right, texture_uv.top_left.x); - let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_left.x); - let top_left = top.lerp(bottom, texture_uv.top_left.y); - - let top = guv.top_left.lerp(guv.top_right, texture_uv.top_right.x); - let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_right.x); - let top_right = top.lerp(bottom, texture_uv.top_right.y); - - let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_left.x); - let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_left.x); - let bottom_left = top.lerp(bottom, texture_uv.bottom_left.y); - - let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_right.x); - let bottom = guv.bottom_left.lerp(guv.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 } - } else { - guv - } - }) - .unwrap_or(Corners::all(Vec2::ZERO)); - - let vidx = draw_call.vertices.len() as u32; - if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) { - //this code is stupid as fuck - //but it works... i think? - //maybe some verts end up missing, but it's close enough... - - //Random vert in the center for no reason - //lol - draw_call.vertices.push(UiVertex { - position: *position + *size * vec2(0.5, 0.5), - color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4., - //TODO: fix this uv - uv: vec2(0., 0.), - }); - - //TODO: fix some corners tris being invisible (but it's already close enough lol) - let rounded_corner_verts = corner.point_count.get() as u32; - for i in 0..rounded_corner_verts { - let cratio = i as f32 / (rounded_corner_verts - 1) as f32; - let angle = cratio * std::f32::consts::PI * 0.5; - let x = angle.sin(); - let y = angle.cos(); - - let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| { - 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); - 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); - draw_call.vertices.push(UiVertex { - position: *position + rp, - color: color_at_point, - uv: uv_at_point, - }); - }; - - //Top-right corner - corner_impl( - vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.), - color, - ); - //Bottom-right corner - corner_impl( - vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right), - color, - ); - //Bottom-left corner - corner_impl( - vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left), - color, - ); - //Top-left corner - corner_impl( - vec2(1. - x, 1. - y) * corner.radius.top_left, - color, - ); - - // mental illness: - if i > 0 { - draw_call.indices.extend([ - //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, - ]); - } - } - //Fill in the rest - //mental illness 2: - draw_call.indices.extend([ - //Top - vidx, - vidx + 4, - 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, - ]); - } else { - //...Normal rectangle - draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); - draw_call.vertices.extend([ - UiVertex { - position: *position, - color: color.top_left, - uv: uvs.top_left, - }, - UiVertex { - position: *position + vec2(size.x, 0.0), - color: color.top_right, - uv: uvs.top_right, - }, - UiVertex { - position: *position + *size, - color: color.bottom_right, - uv: uvs.bottom_right, - }, - UiVertex { - position: *position + vec2(0.0, size.y), - color: color.bottom_left, - uv: uvs.bottom_left, - }, - ]); - } - }, - UiDrawCommand::Text { position, size, color, text, font: font_handle } => { - if text.is_empty() { - continue - } - - //XXX: should we be doing this every time? - 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 - } - let vidx = draw_call.vertices.len() as u32; - let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8); - let uv = atlas.get_uv(glyph.texture).unwrap(); - draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); - draw_call.vertices.extend([ - UiVertex { - position: *position + vec2(layout_glyph.x, layout_glyph.y), - color: *color, - uv: uv.top_left, - }, - UiVertex { - position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y), - color: *color, - uv: uv.top_right, - }, - UiVertex { - position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32), - color: *color, - uv: uv.bottom_right, - }, - UiVertex { - position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32), - color: *color, - uv: uv.bottom_left, - }, - ]); - #[cfg(all( - feature = "pixel_perfect_text", - not(feature = "pixel_perfect") - ))] { - //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() - } - } - } - } - } - } - } - - atlas.lock_atlas = false; - - #[cfg(feature = "pixel_perfect")] - draw_call.vertices.iter_mut().for_each(|v| { - v.position = v.position.round() - }); - - draw_call - } -} diff --git a/hui/src/draw/atlas.rs b/hui/src/draw/atlas.rs deleted file mode 100644 index 73bc2aa..0000000 --- a/hui/src/draw/atlas.rs +++ /dev/null @@ -1,299 +0,0 @@ -use glam::{uvec2, vec2, UVec2, Vec2}; -use hashbrown::HashMap; -use nohash_hasher::BuildNoHashHasher; -use rect_packer::DensePacker; -use crate::rect::Corners; - -const RGBA_CHANNEL_COUNT: u32 = 4; -//TODO make this work -const ALLOW_ROTATION: bool = false; - -/// Texture format of the source texture data -#[derive(Default, Clone, Copy, PartialEq, Eq)] -pub enum TextureFormat { - /// The data is stored in RGBA format, with 1 byte (8 bits) per channel - #[default] - Rgba, - - /// The data is copied into the Alpha channel, with 1 byte (8 bits) per channel\ - /// Remaining channels are set to 255 (which can be easily shaded to any color) - /// - /// This format is useful for storing grayscale textures such as icons\ - /// (Please note that the internal representation is still RGBA, this is just a convenience feature) - Grayscale, -} - -/// Contains a reference to the texture data, and metadata associated with it -pub struct TextureAtlasMeta<'a> { - /// Texture data\ - /// The data is stored in RGBA format, with 1 byte (8 bits) per channel - pub data: &'a [u8], - /// Current size of the texture atlas\ - /// Please note that this value might change - pub size: UVec2, - /// True if the atlas has been modified since the beginning of the current frame\ - /// If this function returns true, the texture atlas should be re-uploaded to the GPU before rendering\ - pub modified: bool, -} - -/// Texture handle, stores the internal index of a texture within the texture atlas and can be cheaply copied. -/// -/// Please note that dropping a handle does not deallocate the texture from the atlas, you must do it manually. -/// -/// Only valid for the `UiInstance` that created it.\ -/// Using it with other instances may result in panics or unexpected behavior. -/// -/// Handle values are not guaranteed to be valid.\ -/// Creating or transmuting an invalid handle is allowed and is *not* UB. -/// -/// Internal value is an implementation detail and should not be relied upon. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] -pub struct ImageHandle { - pub(crate) index: u32, -} - -#[derive(Clone, Copy, Debug)] -pub(crate) struct TextureAllocation { - /// Position in the texture atlas\ - /// (This is an implementation detail and should not be exposed to the user) - pub(crate) position: UVec2, - - /// Requested texture size - pub size: UVec2, - - /// True if the texture was rotated by 90 degrees\ - /// (This is an implementation detail and should not be exposed to the user) - pub(crate) rotated: bool, -} - -/// Manages a texture atlas and the allocation of space within it\ -/// The atlas is alllowed to grow and resize dynamically, as needed -pub(crate) struct TextureAtlasManager { - packer: DensePacker, - count: u32, - size: UVec2, - data: Vec<u8>, - allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>, - /// Items that have been removed from the allocation list, but still affect - remove_queue: Vec<TextureAllocation>, - /// True if the atlas has been modified in a way which requires a texture reupload - /// since the beginning of the current frame - modified: bool, - - /// If true, attempting to modify the atlas in a way which invalidates UVs will cause a panic\ - /// Used internally to ensure that the UVs do not become invalidated mid-render - pub(crate) lock_atlas: bool, -} - -impl TextureAtlasManager { - /// Create a new texture atlas with the specified size\ - /// 512x512 is a good default size for most applications, and the texture atlas can grow dynamically as needed - pub fn new(size: UVec2) -> Self { - Self { - packer: DensePacker::new(size.x as i32, size.y as i32), - count: 0, - size, - data: vec![0; (size.x * size.y * RGBA_CHANNEL_COUNT) as usize], - allocations: HashMap::default(), - remove_queue: Vec::new(), - modified: true, - lock_atlas: false, - } - } - - /// Resize the texture atlas to the new size in-place, preserving the existing data - pub fn resize(&mut self, new_size: UVec2) { - if self.lock_atlas { - panic!("Attempted to resize the texture atlas while the atlas is locked"); - } - log::trace!("resizing texture atlas to {:?}", new_size); - if self.size == new_size { - log::warn!("Texture atlas is already the requested size"); - return - } - if new_size.x > self.size.x && new_size.y > self.size.y { - self.packer.resize(new_size.x as i32, new_size.y as i32); - //Resize the data array in-place - self.data.resize((new_size.x * new_size.y * RGBA_CHANNEL_COUNT) as usize, 0); - for y in (0..self.size.y).rev() { - for x in (1..self.size.x).rev() { - let idx = ((y * self.size.x + x) * RGBA_CHANNEL_COUNT) as usize; - let new_idx = ((y * new_size.x + x) * RGBA_CHANNEL_COUNT) as usize; - for c in 0..(RGBA_CHANNEL_COUNT as usize) { - self.data[new_idx + c] = self.data[idx + c]; - } - } - } - } else { - //If scaling down, just recreate the atlas from scratch (since we need to re-pack everything anyway) - todo!("Atlas downscaling is not implemented yet"); - } - self.size = new_size; - self.modified = true; - } - - /// Ensure that a texture with specified size would fit without resizing on the next allocation attempt\ - pub fn ensure_fits(&mut self, size: UVec2) { - // Plan A: try if any of the existing items in the remove queue would fit the texture - // Plan B: purge the remove queue, recreate the packer and try again (might be expensive...!) - // TODO: implement these - // Plan C: resize the atlas - let mut new_size = self.size; - while !self.packer.can_pack(size.x as i32, size.y as i32, ALLOW_ROTATION) { - new_size *= 2; - self.packer.resize(new_size.x as i32, new_size.y as i32); - } - if new_size != self.size { - self.resize(new_size); - } - } - - /// Allocate a new texture region in the atlas and return a handle to it\ - /// Returns None if the texture could not be allocated due to lack of space\ - /// Use `allocate` to allocate a texture and resize the atlas if necessary\ - /// Does not modify the texture data - fn try_allocate(&mut self, size: UVec2) -> Option<ImageHandle> { - log::trace!("Allocating texture of size {:?}", size); - let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?; - let index = self.count; - self.count += 1; - let allocation = TextureAllocation { - position: UVec2::new(result.x as u32, result.y as u32), - size, - //If the size does not match the requested size, the texture was rotated - rotated: ALLOW_ROTATION && (result.width != size.x as i32), - }; - unsafe { - self.allocations.insert_unique_unchecked(index, allocation); - } - Some(ImageHandle { index }) - } - - /// Allocate a new texture region in the atlas and resize the atlas if necessary\ - /// This function should never fail under normal circumstances.\ - /// May modify the texture data if the atlas is resized - pub fn allocate(&mut self, size: UVec2) -> ImageHandle { - self.ensure_fits(size); - self.try_allocate(size).unwrap() - } - - /// Allocate a new texture region in the atlas and copy the data into it\ - /// This function may resize the atlas as needed, and should never fail under normal circumstances. - pub(crate) fn add_rgba(&mut self, width: usize, data: &[u8]) -> ImageHandle { - let size = uvec2(width as u32, (data.len() / (width * RGBA_CHANNEL_COUNT as usize)) as u32); - let handle: ImageHandle = self.allocate(size); - let allocation = self.allocations.get(&handle.index).unwrap(); - assert!(!allocation.rotated, "Rotated textures are not implemented yet"); - for y in 0..size.y { - for x in 0..size.x { - let src_idx = (y * size.x + x) * RGBA_CHANNEL_COUNT; - let dst_idx = ((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT; - for c in 0..RGBA_CHANNEL_COUNT as usize { - self.data[dst_idx as usize + c] = data[src_idx as usize + c]; - } - } - } - self.modified = true; - handle - } - - /// Works the same way as [`TextureAtlasManager::add`], but the input data is assumed to be grayscale (1 channel per pixel)\ - /// The data is copied into the alpha channel of the texture, while all the other channels are set to 255\ - /// May resize the atlas as needed, and should never fail under normal circumstances. - pub(crate) fn add_grayscale(&mut self, width: usize, data: &[u8]) -> ImageHandle { - let size = uvec2(width as u32, (data.len() / width) as u32); - let handle = self.allocate(size); - let allocation = self.allocations.get(&handle.index).unwrap(); - assert!(!allocation.rotated, "Rotated textures are not implemented yet"); - for y in 0..size.y { - for x in 0..size.x { - let src_idx = (y * size.x + x) as usize; - let dst_idx = (((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT) as usize; - self.data[dst_idx..(dst_idx + RGBA_CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]); - } - } - self.modified = true; - handle - } - - pub fn add(&mut self, width: usize, data: &[u8], format: TextureFormat) -> ImageHandle { - match format { - TextureFormat::Rgba => self.add_rgba(width, data), - TextureFormat::Grayscale => self.add_grayscale(width, data), - } - } - - pub(crate) fn add_dummy(&mut self) { - let handle = self.allocate((1, 1).into()); - assert!(handle.index == 0, "Dummy texture handle is not 0"); - assert!(self.get(handle).unwrap().position == (0, 0).into(), "Dummy texture position is not (0, 0)"); - self.data[0..4].copy_from_slice(&[255, 255, 255, 255]); - self.modified = true; - } - - pub fn modify(&mut self, handle: ImageHandle) { - todo!() - } - - pub fn remove(&mut self, handle: ImageHandle) { - todo!() - } - - pub fn get(&self, handle: ImageHandle) -> Option<&TextureAllocation> { - self.allocations.get(&handle.index) - } - - pub(crate) fn get_uv(&self, handle: ImageHandle) -> Option<Corners<Vec2>> { - let info = self.get(handle)?; - let atlas_size = self.meta().size.as_vec2(); - let p0x = info.position.x as f32 / atlas_size.x; - let p1x = (info.position.x as f32 + info.size.x as f32) / atlas_size.x; - let p0y = info.position.y as f32 / atlas_size.y; - let p1y = (info.position.y as f32 + info.size.y as f32) / atlas_size.y; - Some(Corners { - top_left: vec2(p0x, p0y), - top_right: vec2(p1x, p0y), - bottom_left: vec2(p0x, p1y), - bottom_right: vec2(p1x, p1y), - }) - } - - /// Reset the `is_modified` flag - pub(crate) fn reset_modified(&mut self) { - self.modified = false; - } - - pub fn meta(&self) -> TextureAtlasMeta { - TextureAtlasMeta { - data: &self.data, - size: self.size, - modified: self.modified, - } - } - - pub fn context(&self) -> ImageCtx { - ImageCtx { atlas: self } - } -} - -impl Default for TextureAtlasManager { - /// Create a new texture atlas with a default size of 512x512 - fn default() -> Self { - Self::new(UVec2::new(512, 512)) - } -} - -/// Context that allows read-only accss to image metadata -#[derive(Clone, Copy)] -pub struct ImageCtx<'a> { - pub(crate) atlas: &'a TextureAtlasManager, -} - -impl ImageCtx<'_> { - /// Get size of the image with the specified handle - /// - /// Returns None if the handle is invalid for the current context - pub fn get_size(&self, handle: ImageHandle) -> Option<UVec2> { - self.atlas.get(handle).map(|a| a.size) - } -} diff --git a/hui/src/draw/corner_radius.rs b/hui/src/draw/corner_radius.rs deleted file mode 100644 index 4448712..0000000 --- a/hui/src/draw/corner_radius.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::num::NonZeroU16; -use crate::rect::Corners; - -//TODO uneven corners (separate width/height for each corner) - -/// Calculate the number of points based on the maximum corner radius -fn point_count(corners: Corners<f32>) -> 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() -} - -/// Low-level options for rendering rounded corners -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct RoundedCorners { - /// Corner radius of each corner - pub radius: Corners<f32>, - - /// Number of points to use for each corner - /// - /// This value affects all corners, regardless of their individual radius - pub point_count: NonZeroU16, -} - -impl From<Corners<f32>> for RoundedCorners { - /// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners) - /// - /// Point count will be calculated automatically based on the maximum radius - fn from(radius: Corners<f32>) -> Self { - Self::from_radius(radius) - } -} - -impl RoundedCorners { - /// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners) - /// - /// Point count will be calculated automatically based on the maximum radius - pub fn from_radius(radius: Corners<f32>) -> Self { - Self { - radius, - point_count: point_count(radius), - } - } -} - -impl Default for RoundedCorners { - fn default() -> Self { - Self { - radius: Corners::default(), - point_count: NonZeroU16::new(8).unwrap(), - } - } -} diff --git a/hui/src/element.rs b/hui/src/element.rs index 1d31f1d..7cc1a5f 100644 --- a/hui/src/element.rs +++ b/hui/src/element.rs @@ -1,40 +1,37 @@ //! element API and built-in elements like `Container`, `Button`, `Text`, etc. use crate::{ - draw::{atlas::ImageCtx, UiDrawCommandList}, input::InputCtx, layout::{LayoutInfo, Size2d}, measure::Response, rect::Rect, signal::SignalStore, state::StateRepo, - text::{FontHandle, TextMeasure}, UiInstance, }; mod builtin; pub use builtin::*; +use hui_painter::{paint::command::PaintList, text::FontHandle, PainterInstance}; /// Context for the `Element::measure` function pub struct MeasureContext<'a> { + pub painter: &'a PainterInstance, + pub current_font: FontHandle, pub layout: &'a LayoutInfo, pub state: &'a StateRepo, - pub text_measure: TextMeasure<'a>, - pub current_font: FontHandle, - pub images: ImageCtx<'a>, //XXX: should measure have a reference to input? //pub input: InputCtx<'a>, } /// Context for the `Element::process` function pub struct ProcessContext<'a> { + pub painter: &'a mut PainterInstance, + pub paint_target: &'a mut PaintList, pub measure: &'a Response, pub layout: &'a LayoutInfo, - pub draw: &'a mut UiDrawCommandList, pub state: &'a mut StateRepo, - pub text_measure: TextMeasure<'a>, pub current_font: FontHandle, - pub images: ImageCtx<'a>, pub input: InputCtx<'a>, pub signal: &'a mut SignalStore, } diff --git a/hui/src/element/builtin/container.rs b/hui/src/element/builtin/container.rs index 0c70905..d6dd13e 100644 --- a/hui/src/element/builtin/container.rs +++ b/hui/src/element/builtin/container.rs @@ -187,7 +187,8 @@ impl UiElement for Container { } } - let measure = element.measure(MeasureContext{ + let measure = element.measure(MeasureContext { + painter: ctx.painter, state: ctx.state, layout: &LayoutInfo { //XXX: if the element gets wrapped, this will be inaccurate. @@ -201,9 +202,7 @@ impl UiElement for Container { direction: self.direction, remaining_space: None, }, - text_measure: ctx.text_measure, current_font: ctx.current_font, - images: ctx.images, }); //Check the position of the side of element closest to the end on the primary axis @@ -375,7 +374,7 @@ impl UiElement for Container { // }); // } - self.background_frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into()); + self.background_frame.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into()); //padding position += vec2(self.padding.left, self.padding.top); @@ -444,11 +443,10 @@ impl UiElement for Container { //measure let el_measure = element.measure(MeasureContext { + painter: ctx.painter, layout: &el_layout, state: ctx.state, - text_measure: ctx.text_measure, current_font: ctx.current_font, - images: ctx.images, }); //align (on sec. axis) @@ -485,13 +483,12 @@ impl UiElement for Container { //process element.process(ProcessContext { + painter: ctx.painter, measure: &el_measure, layout: &el_layout, - draw: ctx.draw, + paint_target: ctx.paint_target, state: ctx.state, - text_measure: ctx.text_measure, current_font: ctx.current_font, - images: ctx.images, input: ctx.input, signal: ctx.signal, }); diff --git a/hui/src/element/builtin/frame_view.rs b/hui/src/element/builtin/frame_view.rs index d6b1e1f..fed8191 100644 --- a/hui/src/element/builtin/frame_view.rs +++ b/hui/src/element/builtin/frame_view.rs @@ -63,6 +63,6 @@ impl UiElement for FrameView { } fn process(&self, ctx: ProcessContext) { - self.frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into()); + self.frame.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into()); } } diff --git a/hui/src/element/builtin/image.rs b/hui/src/element/builtin/image.rs index 87c1a53..fb7a83a 100644 --- a/hui/src/element/builtin/image.rs +++ b/hui/src/element/builtin/image.rs @@ -1,7 +1,7 @@ use derive_setters::Setters; -use glam::vec2; +use glam::{vec2, Affine2}; +use hui_painter::{paint::command::{PaintRectangle, PaintTransform}, texture::TextureHandle}; use crate::{ - draw::{ImageHandle, RoundedCorners, UiDrawCommand}, element::{MeasureContext, ProcessContext, UiElement}, layout::{compute_size, Size, Size2d}, measure::Response, @@ -13,7 +13,7 @@ use crate::{ pub struct Image { /// Image handle to draw #[setters(skip)] - pub image: ImageHandle, + pub image: TextureHandle, /// Size of the image. /// @@ -36,7 +36,7 @@ pub struct Image { } impl Image { - pub fn new(handle: ImageHandle) -> Self { + pub fn new(handle: TextureHandle) -> Self { Self { image: handle, size: Size2d { @@ -59,7 +59,7 @@ impl UiElement for Image { } fn measure(&self, ctx: MeasureContext) -> Response { - let dim = ctx.images.get_size(self.image).expect("invalid image handle"); + let dim = self.image.size(); let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2()); Response { size: compute_size(ctx.layout, self.size, vec2( @@ -78,16 +78,17 @@ impl UiElement for Image { fn process(&self, ctx: ProcessContext) { if !self.color.is_transparent() { - ctx.draw.add(UiDrawCommand::Rectangle { - position: ctx.layout.position, - size: ctx.measure.size, - color: self.color.corners(), - texture: Some(self.image), - texture_uv: None, - rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({ - RoundedCorners::from_radius(self.corner_radius) - }), - }); + ctx.paint_target.add( + PaintTransform { + transform: Affine2::from_translation(ctx.layout.position), + child: PaintRectangle { + size: ctx.measure.size, + color: self.color, + texture: Some(self.image), + ..Default::default() + }, + } + ); } } } diff --git a/hui/src/element/builtin/progress_bar.rs b/hui/src/element/builtin/progress_bar.rs index a432956..8003c82 100644 --- a/hui/src/element/builtin/progress_bar.rs +++ b/hui/src/element/builtin/progress_bar.rs @@ -75,10 +75,10 @@ impl UiElement for ProgressBar { //FIXME: these optimizations may not be valid if value < 1. || !self.foreground.covers_opaque() { - self.background.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into()); + self.background.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into()); } if value > 0. { - self.foreground.draw(ctx.draw, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into()); + self.foreground.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into()); } // let rounded_corners = diff --git a/hui/src/element/builtin/slider.rs b/hui/src/element/builtin/slider.rs index 14a5710..c4396ca 100644 --- a/hui/src/element/builtin/slider.rs +++ b/hui/src/element/builtin/slider.rs @@ -158,7 +158,7 @@ impl UiElement for Slider { // if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) { if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) { self.track.draw( - ctx.draw, + ctx.paint_target, ( ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.), ctx.measure.size * vec2(1., self.track_height), @@ -172,7 +172,7 @@ impl UiElement for Slider { // if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) { if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) { self.track_active.draw( - ctx.draw, + ctx.paint_target, ( ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.), (ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2., @@ -193,7 +193,7 @@ impl UiElement for Slider { // } if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) { self.handle.draw( - ctx.draw, + ctx.paint_target, ( ctx.layout.position + ((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X + diff --git a/hui/src/element/builtin/text.rs b/hui/src/element/builtin/text.rs index 303c859..8c5e2a0 100644 --- a/hui/src/element/builtin/text.rs +++ b/hui/src/element/builtin/text.rs @@ -2,16 +2,17 @@ use std::borrow::Cow; use derive_setters::Setters; -use glam::Vec4; +use glam::{Affine2, Vec4}; +use hui_painter::{ + paint::command::{text::{PaintText, TextChunk}, PaintCommand, PaintTransform}, + text::FontHandle, +}; use crate::{ - draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, layout::{compute_size, Size, Size2d}, measure::Response, - text::FontHandle, }; - //TODO: text fit // pub enum TextSize { // FitToWidthRatio(f32), @@ -41,7 +42,7 @@ pub struct Text { pub font: Option<FontHandle>, /// Size of the text, in points (these are not pixels) - pub text_size: u16, + pub text_size: f32, } impl Default for Text { @@ -51,7 +52,7 @@ impl Default for Text { size: (Size::Auto, Size::Auto).into(), color: Vec4::new(1., 1., 1., 1.), font: None, - text_size: 16, + text_size: 16., } } } @@ -69,6 +70,19 @@ impl Text { } } +impl Text { + fn paint_cmd(&self, current_font: FontHandle) -> PaintText { + PaintText { + text: TextChunk { + text: self.text.clone(), + font: self.font.unwrap_or(current_font), + size: self.text_size as f32, + color: self.color.into(), + } + } + } +} + impl UiElement for Text { fn name(&self) -> &'static str { "text" @@ -82,9 +96,13 @@ impl UiElement for Text { let mut size = (0., 0.); if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) { //TODO optimized measure if only one of the sizes is auto - let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text); - size.0 = res.max_width; - size.1 = res.height; + // let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text); + // size.0 = res.max_width; + // size.1 = res.height; + let cmd = self.paint_cmd(ctx.current_font); + let cmd_size = cmd.bounds(ctx.painter).size; + size.0 = cmd_size.x; + size.1 = cmd_size.y; } Response { size: compute_size(ctx.layout, self.size, size.into()), @@ -96,12 +114,9 @@ impl UiElement for Text { if self.text.is_empty() || self.color.w == 0. { return } - ctx.draw.add(UiDrawCommand::Text { - text: self.text.clone(), - position: ctx.layout.position, - size: self.text_size, - color: self.color, - font: self.font(ctx.current_font), + ctx.paint_target.add(PaintTransform { + transform: Affine2::from_translation(ctx.layout.position), + child: self.paint_cmd(ctx.current_font), }); } } diff --git a/hui/src/element/builtin/transformer.rs b/hui/src/element/builtin/transformer.rs index 0467ae0..f6f35f6 100644 --- a/hui/src/element/builtin/transformer.rs +++ b/hui/src/element/builtin/transformer.rs @@ -1,8 +1,10 @@ //! wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling use glam::{Affine2, Vec2}; +use hui_painter::paint::command::{PaintList, PaintTransform}; use crate::{ - draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response + element::{MeasureContext, ProcessContext, UiElement}, + measure::Response, }; pub struct Transformer { @@ -46,20 +48,27 @@ impl UiElement for Transformer { } fn process(&self, ctx: ProcessContext) { - ctx.draw.add(UiDrawCommand::PushTransform(self.transform)); - //This is stupid: + if self.transform == Affine2::IDENTITY { + self.element.process(ctx); + return; + } + + let mut sub_list = PaintList::new_empty(); self.element.process(ProcessContext { + painter: ctx.painter, measure: ctx.measure, state: ctx.state, layout: ctx.layout, - draw: ctx.draw, - text_measure: ctx.text_measure, + paint_target: &mut sub_list, current_font: ctx.current_font, - images: ctx.images, input: ctx.input, signal: ctx.signal, }); - ctx.draw.add(UiDrawCommand::PopTransform); + + ctx.paint_target.add(PaintTransform { + transform: self.transform, + child: sub_list, + }); } } diff --git a/hui/src/text/stack.rs b/hui/src/font.rs similarity index 51% rename from hui/src/text/stack.rs rename to hui/src/font.rs index a61f5e9..65d2ee3 100644 --- a/hui/src/text/stack.rs +++ b/hui/src/font.rs @@ -1,4 +1,5 @@ -use super::FontHandle; + +use hui_painter::text::FontHandle; pub struct FontStack { fonts: Vec<FontHandle>, @@ -7,10 +8,12 @@ pub struct FontStack { impl FontStack { pub fn new() -> Self { Self { - #[cfg(not(feature = "builtin_font"))] fonts: Vec::new(), - #[cfg(feature = "builtin_font")] - fonts: vec![super::BUILTIN_FONT], + // TODO builtin_font + // #[cfg(not(feature = "builtin_font"))] + // fonts: Vec::new(), + // #[cfg(feature = "builtin_font")] + // fonts: vec![super::BUILTIN_FONT], } } @@ -26,7 +29,7 @@ impl FontStack { self.fonts.last().copied() } - pub fn current_or_default(&self) -> FontHandle { - self.current().unwrap_or_default() - } + // pub fn current_or_default(&self) -> FontHandle { + // self.current().unwrap_or_default() + // } } diff --git a/hui/src/frame.rs b/hui/src/frame.rs index 64e3237..2708cd6 100644 --- a/hui/src/frame.rs +++ b/hui/src/frame.rs @@ -1,6 +1,7 @@ //! modular procedural background system -use crate::{draw::UiDrawCommandList, rect::Rect}; +use crate::rect::Rect; +use hui_painter::paint::command::PaintList; pub mod point; mod rect; @@ -9,11 +10,10 @@ pub mod nine_patch; mod impls; pub use rect::RectFrame; - /// Trait for a drawable frame pub trait Frame { /// Draw the frame at the given rect's position and size - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect); + fn draw(&self, draw: &mut PaintList, rect: Rect); /// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size /// diff --git a/hui/src/frame/impls.rs b/hui/src/frame/impls.rs index b906816..c38a78b 100644 --- a/hui/src/frame/impls.rs +++ b/hui/src/frame/impls.rs @@ -1,21 +1,22 @@ -use glam::{Vec3, Vec4}; +use glam::{Affine2, Vec3, Vec4}; +use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle}; use super::Frame; use crate::{ color, - draw::{ImageHandle, UiDrawCommand, UiDrawCommandList}, rect::{Rect, Corners, FillColor}, }; -impl Frame for ImageHandle { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { - draw.add(UiDrawCommand::Rectangle { - position: rect.position, - size: rect.size, - color: color::WHITE.into(), - texture: Some(*self), - texture_uv: None, - rounded_corners: None, - }) +impl Frame for TextureHandle { + fn draw(&self, draw: &mut PaintList, rect: Rect) { + draw.add(PaintTransform { + transform: Affine2::from_translation(rect.position), + child: PaintRectangle { + size: rect.size.into(), + color: color::WHITE.into(), + texture: Some(*self), + ..Default::default() + }, + }); } fn covers_opaque(&self) -> bool { @@ -24,17 +25,17 @@ impl Frame for ImageHandle { } impl Frame for FillColor { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { if self.is_transparent() { return } - draw.add(UiDrawCommand::Rectangle { - position: rect.position, - size: rect.size, - color: self.corners(), - texture: None, - texture_uv: None, - rounded_corners: None, + draw.add(PaintTransform { + transform: Affine2::from_translation(rect.position), + child: PaintRectangle { + size: rect.size, + color: *self, + ..Default::default() + }, }) } @@ -48,7 +49,7 @@ impl Frame for FillColor { // Corners (RGBA): impl Frame for Corners<Vec4> { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -57,7 +58,7 @@ impl Frame for Corners<Vec4> { } impl Frame for (Vec4, Vec4, Vec4, Vec4) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -66,7 +67,7 @@ impl Frame for (Vec4, Vec4, Vec4, Vec4) { } impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -75,7 +76,7 @@ impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32) } impl Frame for [[f32; 4]; 4] { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -86,7 +87,7 @@ impl Frame for [[f32; 4]; 4] { // Corners (RGB): impl Frame for Corners<Vec3> { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -95,7 +96,7 @@ impl Frame for Corners<Vec3> { } impl Frame for (Vec3, Vec3, Vec3, Vec3) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -104,7 +105,7 @@ impl Frame for (Vec3, Vec3, Vec3, Vec3) { } impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -113,7 +114,7 @@ impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f3 } impl Frame for [[f32; 3]; 4] { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -124,7 +125,7 @@ impl Frame for [[f32; 3]; 4] { // RGBA: impl Frame for Vec4 { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -133,7 +134,7 @@ impl Frame for Vec4 { } impl Frame for (f32, f32, f32, f32) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -142,7 +143,7 @@ impl Frame for (f32, f32, f32, f32) { } impl Frame for [f32; 4] { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -153,7 +154,7 @@ impl Frame for [f32; 4] { // RGB: impl Frame for Vec3 { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -162,7 +163,7 @@ impl Frame for Vec3 { } impl Frame for (f32, f32, f32) { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { @@ -171,7 +172,7 @@ impl Frame for (f32, f32, f32) { } impl Frame for [f32; 3] { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { FillColor::from(*self).draw(draw, rect) } fn covers_opaque(&self) -> bool { diff --git a/hui/src/frame/nine_patch.rs b/hui/src/frame/nine_patch.rs index 09a58c1..352b0d1 100644 --- a/hui/src/frame/nine_patch.rs +++ b/hui/src/frame/nine_patch.rs @@ -3,10 +3,10 @@ //! A 9-patch image is an image that can be scaled in a way that preserves the corners and edges of the image while scaling the center. //! This is useful for creating scalable UI elements like buttons, windows, etc. -use glam::{vec2, UVec2, Vec2}; +use glam::{vec2, Affine2, UVec2, Vec2}; +use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle}; use crate::{ color, - draw::{ImageHandle, UiDrawCommand, UiDrawCommandList}, rect::{Rect, Corners, FillColor} }; use super::Frame; @@ -14,7 +14,7 @@ use super::Frame; /// Represents a 9-patch image asset #[derive(Clone, Copy, Debug)] pub struct NinePatchAsset { - pub image: ImageHandle, + pub image: TextureHandle, //TODO: remove this: pub size: (u32, u32), pub scalable_region: Rect, @@ -46,14 +46,14 @@ impl Default for NinePatchFrame { fn default() -> Self { Self { //This is not supposed to be left out as the default, so just set it to whatever :p - asset: NinePatchAsset { image: ImageHandle::default(), size: (0, 0), scalable_region: Rect::default() }, + asset: NinePatchAsset { image: TextureHandle::new_broken(), size: (0, 0), scalable_region: Rect::default() }, color: color::WHITE.into(), } } } impl Frame for NinePatchFrame { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { // without this, shїt gets messed up when the position is not a whole number //XXX: should we round the size as well? let position = rect.position.round(); @@ -100,13 +100,15 @@ impl Frame for NinePatchFrame { bottom_left: vec2(0., region_uv.top_left.y), bottom_right: region_uv.top_left, }; - draw.add(UiDrawCommand::Rectangle { - position, - size: vec2(size_h.0, size_v.0), - color: interpolate_color_rect(top_left_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(top_left_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position), + child: PaintRectangle { + size: vec2(size_h.0, size_v.0), + color: interpolate_color_rect(top_left_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: top_left_patch_uv, + ..Default::default() + }, }); //Top patch @@ -116,13 +118,15 @@ impl Frame for NinePatchFrame { bottom_left: region_uv.top_left, bottom_right: region_uv.top_right, }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0, 0.), - size: vec2(size_h.1, size_v.0), - color: interpolate_color_rect(top_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(top_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0, 0.)), + child: PaintRectangle { + size: vec2(size_h.1, size_v.0), + color: interpolate_color_rect(top_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: top_patch_uv, + ..Default::default() + }, }); //Top-right patch @@ -132,13 +136,15 @@ impl Frame for NinePatchFrame { bottom_left: region_uv.top_right, bottom_right: vec2(1., region_uv.top_right.y), }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0 + size_h.1, 0.), - size: vec2(size_h.2, size_v.0), - color: interpolate_color_rect(top_right_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(top_right_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, 0.)), + child: PaintRectangle { + size: vec2(size_h.2, size_v.0), + color: interpolate_color_rect(top_right_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: top_right_patch_uv, + ..Default::default() + }, }); //Left patch @@ -148,23 +154,27 @@ impl Frame for NinePatchFrame { bottom_left: vec2(0., region_uv.bottom_left.y), bottom_right: region_uv.bottom_left, }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(0., size_v.0), - size: vec2(size_h.0, size_v.1), - color: interpolate_color_rect(left_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(left_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(0., size_v.0)), + child: PaintRectangle { + size: vec2(size_h.0, size_v.1), + color: interpolate_color_rect(left_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: left_patch_uv, + ..Default::default() + }, }); // Center patch - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0, size_v.0), - size: vec2(size_h.1, size_v.1), - color: interpolate_color_rect(region_uv), - texture: Some(self.asset.image), - texture_uv: Some(region_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0, size_v.0)), + child: PaintRectangle { + size: vec2(size_h.1, size_v.1), + color: interpolate_color_rect(region_uv).into(), + texture: Some(self.asset.image), + texture_uv: region_uv, + ..Default::default() + }, }); //Right patch @@ -174,13 +184,15 @@ impl Frame for NinePatchFrame { bottom_left: region_uv.bottom_right, bottom_right: vec2(1., region_uv.bottom_right.y), }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0 + size_h.1, size_v.0), - size: vec2(size_h.2, size_v.1), - color: interpolate_color_rect(right_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(right_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, size_v.0)), + child: PaintRectangle { + size: vec2(size_h.2, size_v.1), + color: interpolate_color_rect(right_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: right_patch_uv, + ..Default::default() + }, }); //Bottom-left patch @@ -190,13 +202,15 @@ impl Frame for NinePatchFrame { bottom_left: vec2(0., 1.), bottom_right: vec2(region_uv.bottom_left.x, 1.), }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(0., size_v.0 + size_v.1), - size: vec2(size_h.0, size_v.2), - color: interpolate_color_rect(bottom_left_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(bottom_left_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(0., size_v.0 + size_v.1)), + child: PaintRectangle { + size: vec2(size_h.0, size_v.2), + color: interpolate_color_rect(bottom_left_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: bottom_left_patch_uv, + ..Default::default() + }, }); //Bottom patch @@ -206,13 +220,15 @@ impl Frame for NinePatchFrame { bottom_left: vec2(region_uv.bottom_left.x, 1.), bottom_right: vec2(region_uv.bottom_right.x, 1.), }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0, size_v.0 + size_v.1), - size: vec2(size_h.1, size_v.2), - color: interpolate_color_rect(bottom_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(bottom_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0, size_v.0 + size_v.1)), + child: PaintRectangle { + size: vec2(size_h.1, size_v.2), + color: interpolate_color_rect(bottom_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: bottom_patch_uv, + ..Default::default() + }, }); //Bottom-right patch @@ -222,13 +238,15 @@ impl Frame for NinePatchFrame { bottom_left: vec2(region_uv.bottom_right.x, 1.), bottom_right: vec2(1., 1.), }; - draw.add(UiDrawCommand::Rectangle { - position: position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1), - size: vec2(size_h.2, size_v.2), - color: interpolate_color_rect(bottom_right_patch_uv), - texture: Some(self.asset.image), - texture_uv: Some(bottom_right_patch_uv), - rounded_corners: None + draw.add(PaintTransform { + transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1)), + child: PaintRectangle { + size: vec2(size_h.2, size_v.2), + color: interpolate_color_rect(bottom_right_patch_uv).into(), + texture: Some(self.asset.image), + texture_uv: bottom_right_patch_uv, + ..Default::default() + }, }); } diff --git a/hui/src/frame/rect.rs b/hui/src/frame/rect.rs index d6e5d5e..746876f 100644 --- a/hui/src/frame/rect.rs +++ b/hui/src/frame/rect.rs @@ -1,7 +1,7 @@ -use glam::Vec2; +use glam::{Affine2, Vec2}; +use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle}; use crate::{ color, - draw::{ImageHandle, RoundedCorners, UiDrawCommand, UiDrawCommandList}, rect::{Rect, Corners, FillColor}, }; use super::{Frame, point::FramePoint2d}; @@ -23,7 +23,7 @@ pub struct RectFrame { /// /// Please note that if the background color is NOT set (or set to transparent), the texture will NOT be visible\ /// This is because the texture is multiplied by the color, and if the color is transparent, the texture will be too\ - pub image: Option<ImageHandle>, + pub image: Option<TextureHandle>, /// Top left corner of the rectangle pub top_left: FramePoint2d, @@ -47,8 +47,8 @@ impl From<FillColor> for RectFrame { } } -impl From<ImageHandle> for RectFrame { - fn from(image: ImageHandle) -> Self { +impl From<TextureHandle> for RectFrame { + fn from(image: TextureHandle) -> Self { Self::image(image) } } @@ -65,7 +65,7 @@ impl RectFrame { /// Create a new [`RectFrame`] with the given image\ /// /// Color will be set to [`WHITE`](crate::color::WHITE) to ensure the image is visible - pub fn image(image: ImageHandle) -> Self { + pub fn image(image: TextureHandle) -> Self { Self { color: color::WHITE.into(), image: Some(image), @@ -74,7 +74,7 @@ impl RectFrame { } /// Create a new [`RectFrame`] with the given color and image - pub fn color_image(color: impl Into<FillColor>, image: ImageHandle) -> Self { + pub fn color_image(color: impl Into<FillColor>, image: TextureHandle) -> Self { Self { color: color.into(), image: Some(image), @@ -115,22 +115,21 @@ impl Default for RectFrame { } impl Frame for RectFrame { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { if self.color.is_transparent() { return } //TODO: handle bottom_right < top_left let top_left = self.top_left.resolve(rect.size); let bottom_right = self.bottom_right.resolve(rect.size); - draw.add(UiDrawCommand::Rectangle { - position: rect.position + top_left, - size: bottom_right - top_left, - color: self.color.corners(), - texture: self.image, - texture_uv: None, - rounded_corners: (self.corner_radius.max_f32() > 0.).then_some( - RoundedCorners::from_radius(self.corner_radius) - ), + draw.add(PaintTransform{ + transform: Affine2::from_translation(rect.position + top_left), + child: PaintRectangle { + size: bottom_right - top_left, + color: self.color, + texture: self.image, + ..Default::default() + }, }); } diff --git a/hui/src/frame/stack.rs b/hui/src/frame/stack.rs index a2dfd5d..fd47f79 100644 --- a/hui/src/frame/stack.rs +++ b/hui/src/frame/stack.rs @@ -1,13 +1,14 @@ //! allows stacking two frames on top of each other -use crate::{draw::UiDrawCommandList, rect::Rect}; +use hui_painter::paint::command::PaintList; +use crate::rect::Rect; use super::Frame; /// A frame that draws two frames on top of each other pub struct FrameStack(pub Box<dyn Frame>, pub Box<dyn Frame>); impl Frame for FrameStack { - fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) { + fn draw(&self, draw: &mut PaintList, rect: Rect) { self.0.draw(draw, rect); self.1.draw(draw, rect); } diff --git a/hui/src/instance.rs b/hui/src/instance.rs index 6050e5f..b098556 100644 --- a/hui/src/instance.rs +++ b/hui/src/instance.rs @@ -1,18 +1,17 @@ +use hui_painter::{ + paint::{buffer::PaintBuffer, command::{PaintCommand, PaintList, PaintRoot}}, + text::FontHandle, + texture::{SourceTextureFormat, TextureHandle}, + PainterInstance, +}; use crate::{ element::{MeasureContext, ProcessContext, UiElement}, - layout::{Direction, LayoutInfo}, - text::{FontHandle, TextRenderer}, - draw::{ - ImageHandle, - TextureFormat, - UiDrawCall, - UiDrawCommandList, - atlas::{TextureAtlasManager, TextureAtlasMeta}, - }, - signal::{Signal, SignalStore}, event::{EventQueue, UiEvent}, + font::FontStack, input::UiInputState, + layout::{Direction, LayoutInfo}, rect::Rect, + signal::{Signal, SignalStore}, state::StateRepo, }; @@ -21,16 +20,16 @@ use crate::{ /// In most cases, you should only have one instance of this struct, but multiple instances are allowed\ /// (Please note that it's possible to render multiple UI "roots" using a single instance) pub struct UiInstance { + painter: PainterInstance, + prev_draw_command_hash: Option<u64>, + cur_draw_command_hash: Option<u64>, + draw_commands: PaintList, + paint_buffer: PaintBuffer, stateful_state: StateRepo, - prev_draw_commands: UiDrawCommandList, - draw_commands: UiDrawCommandList, - draw_call: UiDrawCall, - draw_call_modified: bool, - text_renderer: TextRenderer, - atlas: TextureAtlasManager, events: EventQueue, input: UiInputState, signal: SignalStore, + font_stack: FontStack, /// True if in the middle of a laying out a frame state: bool, } @@ -41,21 +40,13 @@ impl UiInstance { /// In most cases, you should only do this *once*, during the initialization of your application pub fn new() -> Self { UiInstance { - //mouse_position: Vec2::ZERO, + painter: PainterInstance::new(), + prev_draw_command_hash: None, + cur_draw_command_hash: None, + draw_commands: PaintList::default(), + paint_buffer: PaintBuffer::new(), + font_stack: FontStack::new(), stateful_state: StateRepo::new(), - //event_queue: VecDeque::new(), - // root_elements: Vec::new(), - prev_draw_commands: UiDrawCommandList::default(), - draw_commands: UiDrawCommandList::default(), - draw_call: UiDrawCall::default(), - draw_call_modified: false, - // ftm: FontTextureManager::default(), - text_renderer: TextRenderer::new(), - atlas: { - let mut atlas = TextureAtlasManager::default(); - atlas.add_dummy(); - atlas - }, events: EventQueue::new(), input: UiInputState::new(), signal: SignalStore::new(), @@ -70,8 +61,9 @@ impl UiInstance { /// /// ## Panics: /// If the font data is invalid or corrupt + #[deprecated(note = "use painter.fonts_mut().add instead")] pub fn add_font(&mut self, font: &[u8]) -> FontHandle { - self.text_renderer.add_font_from_bytes(font) + self.painter.fonts_mut().add(font) } /// Add an image to the texture atlas\ @@ -80,8 +72,10 @@ impl UiInstance { /// Returns an image handle ([`ImageHandle`])\ /// This handle can be used to reference the texture in draw commands\ /// It's a light reference and can be cloned/copied freely, but will not be cleaned up even when dropped - pub fn add_image(&mut self, format: TextureFormat, data: &[u8], width: usize) -> ImageHandle { - self.atlas.add(width, data, format) + #[deprecated(note = "use painter.textures_mut().atlas_mut().allocate_with_data instead")] + pub fn add_image(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle { + // self.atlas().add(width, data, format) + self.painter.textures_mut().allocate_with_data(format, data, width) } //TODO better error handling @@ -95,7 +89,8 @@ impl UiInstance { /// - If the file exists but contains invalid image data\ /// (this will change to a soft error in the future) #[cfg(feature = "image")] - pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> { + #[deprecated] + pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureHandle, std::io::Error> { use std::io::{Read, Seek}; // Open the file (and wrap it in a bufreader) @@ -115,7 +110,7 @@ impl UiInstance { //Add the image to the atlas let handle = self.add_image( - TextureFormat::Rgba, + SourceTextureFormat::RGBA8, image_rgba, image.width() as usize ); @@ -129,7 +124,7 @@ impl UiInstance { /// This function is useful for replacing the default font, use sparingly\ /// (This library attempts to be stateless, however passing the font to every text element is not very practical) pub fn push_font(&mut self, font: FontHandle) { - self.text_renderer.push_font(font); + self.font_stack.push(font); } /// Pop a font from the font stack\ @@ -137,12 +132,12 @@ impl UiInstance { /// ## Panics: /// If the font stack is empty pub fn pop_font(&mut self) { - self.text_renderer.pop_font(); + self.font_stack.pop(); } /// Get the current default font - pub fn current_font(&self) -> FontHandle { - self.text_renderer.current_font() + pub fn current_font(&self) -> Option<FontHandle> { + self.font_stack.current() } /// Add an element or an element tree to the UI @@ -161,23 +156,23 @@ impl UiInstance { direction: Direction::Vertical, remaining_space: None, }; + // TODO handle font_stack.current() font being None + let current_font = self.font_stack.current().expect("No current font"); let measure = element.measure(MeasureContext { + painter: &self.painter, state: &self.stateful_state, layout: &layout, - text_measure: self.text_renderer.to_measure(), - current_font: self.text_renderer.current_font(), - images: self.atlas.context(), + current_font, }); element.process(ProcessContext { + painter: &mut self.painter, measure: &measure, state: &mut self.stateful_state, layout: &layout, - draw: &mut self.draw_commands, - text_measure: self.text_renderer.to_measure(), - current_font: self.text_renderer.current_font(), - images: self.atlas.context(), + paint_target: &mut self.draw_commands, input: self.input.ctx(), signal: &mut self.signal, + current_font, }); } @@ -198,13 +193,19 @@ impl UiInstance { //then, reset the (remaining) signals self.signal.clear(); + // Compute the hash of the current commands + self.prev_draw_command_hash = Some(self.draw_commands.cache_hash()); + + // Clear the draw commands + self.draw_commands.clear(); + //then, reset the draw commands - std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands); - self.draw_commands.commands.clear(); - self.draw_call_modified = false; + // std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands); + // self.draw_commands.commands.clear(); + // self.draw_call_modified = false; //reset atlas modification flag - self.atlas.reset_modified(); + // self.atlas.reset_modified(); } /// End the frame and prepare the UI for rendering\ @@ -219,13 +220,17 @@ impl UiInstance { self.state = false; //check if the draw commands have been modified - if self.draw_commands.commands == self.prev_draw_commands.commands { - return + if let Some(prev_hash) = self.prev_draw_command_hash { + let cur_hash = self.draw_commands.cache_hash(); + self.cur_draw_command_hash = Some(cur_hash); + if cur_hash == prev_hash { + return + } } //if they have, rebuild the draw call and set the modified flag - self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer); - self.draw_call_modified = true; + self.paint_buffer.clear(); + self.draw_commands.paint_root(&mut self.painter, &mut self.paint_buffer); } /// Get the draw call information for the current frame @@ -233,18 +238,18 @@ impl UiInstance { /// This function should only be used by the render backend.\ /// You should not call this directly unless you're implementing a custom render backend /// - /// Returns a tuple with a boolean indicating if the buffers have been modified since the last frame + /// Returns a tuple with a unique hash of the draw commands and the draw call data\ /// /// You should only call this function *after* [`UiInstance::end`]\ /// Calling it in the middle of a frame will result in a warning but will not cause a panic\ /// (please note that doing so is probably a mistake and should be fixed in your code)\ - /// Doing so anyway will return draw call data for the previous frame, but the `modified` flag will *always* be incorrect until [`UiInstance::end`] is called + /// Doing so anyway will return draw call data for the previous frame /// - pub fn draw_call(&self) -> (bool, &UiDrawCall) { + pub fn backend_paint_buffer(&self) -> (u64, &PaintBuffer) { if self.state { log::warn!("UiInstance::draw_call called while in the middle of a frame, this is probably a mistake"); } - (self.draw_call_modified, &self.draw_call) + (self.cur_draw_command_hash.unwrap_or_default(), &self.paint_buffer) } /// Get the texture atlas size and data for the current frame @@ -260,11 +265,13 @@ impl UiInstance { /// /// Make sure to check [`TextureAtlasMeta::modified`] to see if the texture has been modified /// since the beginning of the current frame before uploading it to the GPU - pub fn atlas(&self) -> TextureAtlasMeta { + pub fn backend_atlas(&self) -> TextureAtlasMeta { if self.state { log::warn!("UiInstance::atlas called while in the middle of a frame, this is probably a mistake"); } - self.atlas.meta() + // unimplemented!() + // self.painter.textures_mut(). + // self.backend_atlas.meta() } /// Push a platform event to the UI event queue diff --git a/hui/src/lib.rs b/hui/src/lib.rs index c5dbff8..0701b7d 100644 --- a/hui/src/lib.rs +++ b/hui/src/lib.rs @@ -12,6 +12,7 @@ #![allow(unused_parens)] pub use hui_shared::*; +pub use hui_painter as painter; mod instance; mod macros; @@ -19,11 +20,10 @@ pub mod layout; pub mod element; pub mod event; pub mod input; -pub mod draw; pub mod measure; pub mod state; -pub mod text; pub mod signal; pub mod frame; +pub mod font; pub use instance::UiInstance; diff --git a/hui/src/text.rs b/hui/src/text.rs deleted file mode 100644 index 55760a1..0000000 --- a/hui/src/text.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! text rendering, styling, measuring - -use std::sync::Arc; -use fontdue::{Font, FontSettings}; -use crate::draw::atlas::TextureAtlasManager; - -mod font; -mod ftm; -mod stack; - -/// Built-in font handle -#[cfg(feature="builtin_font")] -pub use font::BUILTIN_FONT; -pub use font::FontHandle; - -use font::FontManager; -use ftm::FontTextureManager; -use ftm::GlyphCacheEntry; -use stack::FontStack; - -pub(crate) struct TextRenderer { - manager: FontManager, - ftm: FontTextureManager, - stack: FontStack, -} - -impl TextRenderer { - pub fn new() -> Self { - Self { - manager: FontManager::new(), - ftm: FontTextureManager::default(), - stack: FontStack::new(), - } - } - - pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle { - self.manager.add_font(Font::from_bytes(font, FontSettings::default()).unwrap()) - } - - pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> { - self.ftm.glyph(atlas, &self.manager, font_handle, character, size) - } - - pub fn push_font(&mut self, font: FontHandle) { - self.stack.push(font); - } - - pub fn pop_font(&mut self) { - self.stack.pop(); - } - - pub fn current_font(&self) -> FontHandle { - self.stack.current_or_default() - } - - pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font { - self.manager.get(handle).unwrap() - } -} - -impl Default for TextRenderer { - fn default() -> Self { - Self::new() - } -} - -/// Size of measured text -pub struct TextMeasureResponse { - pub max_width: f32, - pub height: f32, -} - -/// Context for measuring text -#[derive(Clone, Copy)] -pub struct TextMeasure<'a>(&'a TextRenderer); - -impl TextMeasure<'_> { - /// Measure the given string of text with the given font and size - pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse { - use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; - let mut layout = Layout::new(CoordinateSystem::PositiveYDown); - layout.append( - &[self.0.internal_font(font)], - &TextStyle::new(text, size as f32, 0) - ); - TextMeasureResponse { - max_width: layout.lines().map(|lines| { - lines.iter().fold(0.0_f32, |acc, x| { - let glyph = layout.glyphs().get(x.glyph_end).unwrap(); - acc.max(glyph.x + glyph.width as f32) - }) - }).unwrap_or(0.), - height: layout.height(), - } - } -} - -impl TextRenderer { - pub fn to_measure(&self) -> TextMeasure { - TextMeasure(self) - } - - pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse { - TextMeasure(self).measure(font, size, text) - } -} diff --git a/hui/src/text/font.rs b/hui/src/text/font.rs deleted file mode 100644 index e9e89e5..0000000 --- a/hui/src/text/font.rs +++ /dev/null @@ -1,65 +0,0 @@ -use fontdue::Font; - -/// Font handle, stores the internal font id and can be cheaply copied. -/// -/// Only valid for the `UiInstance` that created it.\ -/// Using it with other instances may result in panics or unexpected behavior. -/// -/// Handle values are not guaranteed to be valid.\ -/// Creating or transmuting an invalid handle is allowed and is *not* UB. -/// -/// Internal value is an implementation detail and should not be relied upon. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct FontHandle(pub(crate) usize); - -#[cfg(feature = "builtin_font")] -pub const BUILTIN_FONT: FontHandle = FontHandle(0); - -impl Default for FontHandle { - /// Default font handle is the builtin font, if the feature is enabled;\ - /// Otherwise returns an invalid handle. - fn default() -> Self { - #[cfg(feature = "builtin_font")] { BUILTIN_FONT } - #[cfg(not(feature = "builtin_font"))] { Self(usize::MAX) } - } -} - -#[cfg(feature = "builtin_font")] -const BUILTIN_FONT_DATA: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf"); - -pub struct FontManager { - fonts: Vec<Font>, -} - -impl FontManager { - pub fn new() -> Self { - let mut this = Self { - fonts: Vec::new(), - }; - #[cfg(feature = "builtin_font")] - { - let font = Font::from_bytes( - BUILTIN_FONT_DATA, - fontdue::FontSettings::default() - ).unwrap(); - this.add_font(font); - }; - this - } - - /// Add a (fontdue) font to the renderer. - pub fn add_font(&mut self, font: Font) -> FontHandle { - self.fonts.push(font); - FontHandle(self.fonts.len() - 1) - } - - pub fn get(&self, handle: FontHandle) -> Option<&Font> { - self.fonts.get(handle.0) - } -} - -impl Default for FontManager { - fn default() -> Self { - Self::new() - } -} diff --git a/hui/src/text/ftm.rs b/hui/src/text/ftm.rs deleted file mode 100644 index 348c982..0000000 --- a/hui/src/text/ftm.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::sync::Arc; -use fontdue::Metrics; -use hashbrown::HashMap; -use crate::draw::atlas::{TextureAtlasManager, ImageHandle}; - -use super::font::{FontHandle, FontManager}; - -#[derive(PartialEq, Eq, Hash)] -struct GlyphCacheKey { - font_index: usize, - character: char, - size: u8, -} - -pub struct GlyphCacheEntry { - pub metrics: Metrics, - pub texture: ImageHandle, -} - -pub struct FontTextureManager { - glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>> -} - -impl FontTextureManager { - pub fn new() -> Self { - FontTextureManager { - glyph_cache: HashMap::new(), - } - } - - /// Either looks up the glyph in the cache or renders it and adds it to the cache. - pub fn glyph( - &mut self, - atlas: &mut TextureAtlasManager, - font_manager: &FontManager, - font_handle: FontHandle, - character: char, - size: u8 - ) -> Arc<GlyphCacheEntry> { - let key = GlyphCacheKey { - font_index: font_handle.0, - character, - size, - }; - if let Some(entry) = self.glyph_cache.get(&key) { - return Arc::clone(entry); - } - let font = font_manager.get(font_handle).unwrap(); - let (metrics, bitmap) = font.rasterize(character, size as f32); - log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap); - let texture = atlas.add_grayscale(metrics.width, &bitmap); - let entry = Arc::new(GlyphCacheEntry { - metrics, - texture - }); - unsafe { - self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry)); - } - entry - } - - // pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> { - // let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size); - // if is_new { - // self.glyph_place(&glyph); - // self.modified = true; - // } - // glyph - // } -} - -impl Default for FontTextureManager { - fn default() -> Self { Self::new() } -}