diff --git a/hui-glium/src/lib.rs b/hui-glium/src/lib.rs index 1be5932..7b33b5c 100644 --- a/hui-glium/src/lib.rs +++ b/hui-glium/src/lib.rs @@ -116,7 +116,7 @@ impl BufferPair { pub struct GliumUiRenderer { context: Rc, program: glium::Program, - ui_texture: Option>, + ui_texture: Option, buffer_pair: Option, } @@ -143,17 +143,17 @@ impl GliumUiRenderer { pub fn update_texture_atlas(&mut self, atlas: &TextureAtlasMeta) { log::trace!("updating ui atlas texture"); - self.ui_texture = Some(Rc::new(SrgbTexture2d::new( + self.ui_texture = Some(SrgbTexture2d::new( &self.context, RawImage2d::from_raw_rgba( atlas.data.to_owned(), (atlas.size.x, atlas.size.y) ) - ).unwrap())); + ).unwrap()); } pub fn update(&mut self, hui: &UiInstance) { - if hui.atlas().modified { + if self.ui_texture.is_none() || hui.atlas().modified { self.update_texture_atlas(&hui.atlas()); } if hui.draw_call().0 { @@ -181,7 +181,7 @@ impl GliumUiRenderer { &self.program, &uniform! { resolution: resolution.to_array(), - tex: Sampler(self.ui_texture.as_ref().unwrap().as_ref(), SamplerBehavior { + tex: Sampler(self.ui_texture.as_ref().unwrap(), SamplerBehavior { wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp), ..Default::default() }), diff --git a/hui/src/draw.rs b/hui/src/draw.rs index 4e2e1a3..2153ffb 100644 --- a/hui/src/draw.rs +++ b/hui/src/draw.rs @@ -16,9 +16,10 @@ use std::borrow::Cow; use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; use glam::{Vec2, Vec4, vec2}; +//TODO: circle draw command + /// Available draw commands -/// - Rectangle: Filled, colored rectangle, with optional rounded corners -/// - Circle: Simple filled, colored circle +/// - 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 { @@ -30,18 +31,11 @@ pub enum UiDrawCommand { size: Vec2, ///Color (RGBA) color: Corners, + ///Texture + texture: Option, ///Rounded corners rounded_corners: Option, }, - /// Filled, colored circle - Circle { - ///Position in pixels - position: Vec2, - ///Radius in pixels - radius: f32, - ///Color (RGBA) - color: Vec4, - }, /// Draw text using the specified font, size, color, and position Text { ///Position in pixels @@ -98,7 +92,11 @@ impl UiDrawCall { let mut draw_call = UiDrawCall::default(); for command in &draw_commands.commands { match command { - UiDrawCommand::Rectangle { position, size, color, rounded_corners } => { + UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => { + let uvs = texture + .map(|x| atlas.get_uv(x)) + .flatten() + .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 @@ -122,25 +120,25 @@ impl UiDrawCall { draw_call.vertices.push(UiVertex { position: *position + vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.), color: color.top_right, - uv: vec2(0.0, 0.0), + uv: uvs.top_right, }); //Bottom-right corner draw_call.vertices.push(UiVertex { position: *position + vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right), color: color.bottom_right, - uv: vec2(0.0, 0.0), + uv: uvs.bottom_right, }); //Bottom-left corner draw_call.vertices.push(UiVertex { position: *position + vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left), color: color.bottom_left, - uv: vec2(0.0, 0.0), + uv: uvs.bottom_left, }); //Top-left corner draw_call.vertices.push(UiVertex { position: *position + vec2(1. - x, 1. - y) * corner.radius.top_left, color: color.top_left, - uv: vec2(0.0, 0.0), + uv: uvs.top_left, }); // mental illness: if i > 0 { @@ -190,29 +188,26 @@ impl UiDrawCall { UiVertex { position: *position, color: color.top_left, - uv: vec2(0.0, 0.0), + uv: uvs.top_left, }, UiVertex { position: *position + vec2(size.x, 0.0), color: color.top_right, - uv: vec2(0.0, 0.0), // vec2(1.0, 0.0), + uv: uvs.top_right, }, UiVertex { position: *position + *size, color: color.bottom_right, - uv: vec2(0.0, 0.0), // vec2(1.0, 1.0), + uv: uvs.bottom_right, }, UiVertex { position: *position + vec2(0.0, size.y), color: color.bottom_left, - uv: vec2(0.0, 0.0), // vec2(0.0, 1.0), + uv: uvs.bottom_left, }, ]); } }, - UiDrawCommand::Circle { .. } => { - todo!("circle draw command not implemented yet") - }, UiDrawCommand::Text { position, size, color, text, font: font_handle } => { if text.is_empty() { continue @@ -226,16 +221,13 @@ impl UiDrawCall { ); let glyphs = layout.glyphs(); - //let mut rpos_x = 0.; for layout_glyph in glyphs { if !layout_glyph.char_data.rasterize() { continue } - let atlas_size = atlas.meta().size.as_vec2(); 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); - //rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width; + 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 { diff --git a/hui/src/draw/atlas.rs b/hui/src/draw/atlas.rs index 18f5b38..2c5d657 100644 --- a/hui/src/draw/atlas.rs +++ b/hui/src/draw/atlas.rs @@ -4,8 +4,7 @@ use nohash_hasher::BuildNoHashHasher; use rect_packer::DensePacker; use crate::rectangle::Corners; -const CHANNEL_COUNT: u32 = 4; -//TODO: make this work +const RGBA_CHANNEL_COUNT: u32 = 4; const ALLOW_ROTATION: bool = false; pub struct TextureAtlasMeta<'a> { @@ -14,16 +13,17 @@ pub struct TextureAtlasMeta<'a> { pub modified: bool, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub struct TextureHandle { - //TODO automatic cleanup when handle is dropped - //man: Weak>, + //pub(crate) rc: Rc<()>, pub(crate) index: u32 } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub(crate) struct TextureAllocation { - /// Index of the texture allocation + //pub(crate) rc: Weak<()>, + + /// Unique index of the texture allocation pub index: u32, /// Position in the texture atlas @@ -44,6 +44,8 @@ pub(crate) struct TextureAtlasManager { size: UVec2, data: Vec, allocations: HashMap>, + /// Items that have been removed from the allocation list, but still affect + remove_queue: Vec, /// True if the atlas has been modified in a way which requires a texture reupload /// since the beginning of the current frame modified: bool, @@ -52,18 +54,16 @@ pub(crate) struct TextureAtlasManager { 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 - /// By default, the texture atlas gets initialized with a single white pixel texture pub fn new(size: UVec2) -> Self { - let mut tmp = Self { + Self { packer: DensePacker::new(size.x as i32, size.y as i32), count: 0, size, - data: vec![0; (size.x * size.y * CHANNEL_COUNT) as usize], + data: vec![0; (size.x * size.y * RGBA_CHANNEL_COUNT) as usize], allocations: HashMap::default(), + remove_queue: Vec::new(), modified: true, - }; - tmp.add(1, &[255,255,255,255]); - tmp + } } /// Resize the texture atlas to the new size in-place, preserving the existing data @@ -76,12 +76,12 @@ impl TextureAtlasManager { 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 * CHANNEL_COUNT) as usize, 0); + self.data.resize((new_size.x * new_size.y * RGBA_CHANNEL_COUNT) as usize, 0); for y in (1..self.size.y).rev() { for x in (0..self.size.x).rev() { - let idx = ((y * self.size.x + x) * CHANNEL_COUNT) as usize; - let new_idx = ((y * new_size.x + x) * CHANNEL_COUNT) as usize; - for c in 0..(CHANNEL_COUNT as usize) { + 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]; } } @@ -94,6 +94,22 @@ impl TextureAtlasManager { 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, true) { + 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\ @@ -118,29 +134,22 @@ impl TextureAtlasManager { /// 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) -> TextureHandle { - let mut new_size = self.size; - while !self.packer.can_pack(size.x as i32, size.y as i32, true) { - 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); - } + 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 fn add(&mut self, width: usize, data: &[u8]) -> TextureHandle { - let size = uvec2(width as u32, (data.len() / (width * CHANNEL_COUNT as usize)) as u32); + let size = uvec2(width as u32, (data.len() / (width * RGBA_CHANNEL_COUNT as usize)) as u32); let handle: TextureHandle = 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) * CHANNEL_COUNT; - let dst_idx = ((allocation.position.y + y) * self.size.x + allocation.position.x + x) * CHANNEL_COUNT; - for c in 0..CHANNEL_COUNT as usize { + 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]; } } @@ -160,8 +169,8 @@ impl TextureAtlasManager { 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) * CHANNEL_COUNT) as usize; - self.data[dst_idx..(dst_idx + CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]); + 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; @@ -180,19 +189,19 @@ impl TextureAtlasManager { self.allocations.get(&handle.index) } - pub(crate) fn get_uv(&self, handle: TextureHandle) -> Corners { - let info = self.get(handle).unwrap(); + pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option> { + 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; - Corners { + 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 diff --git a/hui/src/element/builtin/container.rs b/hui/src/element/builtin/container.rs index 23ce8eb..6598cf1 100644 --- a/hui/src/element/builtin/container.rs +++ b/hui/src/element/builtin/container.rs @@ -125,6 +125,7 @@ impl UiElement for Container { position, size: ctx.measure.size, color: corner_colors, + texture: None, rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({ RoundedCorners::from_radius(self.corner_radius) }), diff --git a/hui/src/element/builtin/progress_bar.rs b/hui/src/element/builtin/progress_bar.rs index 0cb4c8e..5bf4135 100644 --- a/hui/src/element/builtin/progress_bar.rs +++ b/hui/src/element/builtin/progress_bar.rs @@ -72,6 +72,7 @@ impl UiElement for ProgressBar { position: ctx.layout.position, size: ctx.measure.size, color: Corners::all(self.color_background), + texture: None, rounded_corners }); } @@ -80,6 +81,7 @@ impl UiElement for ProgressBar { position: ctx.layout.position, size: ctx.measure.size * vec2(value, 1.0), color: Corners::all(self.color_foreground), + texture: None, rounded_corners, }); } diff --git a/hui/src/element/builtin/rect.rs b/hui/src/element/builtin/rect.rs index 5e4c989..9032a2e 100644 --- a/hui/src/element/builtin/rect.rs +++ b/hui/src/element/builtin/rect.rs @@ -48,6 +48,7 @@ impl UiElement for Rect { position: ctx.layout.position, size: ctx.measure.size, color: self.color.corners().unwrap(), + texture: None, rounded_corners: None, }); }