From 1666b088737c4284b2088f9bc750610321bc012f Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Mon, 25 Mar 2024 01:59:13 +0100 Subject: [PATCH] add 9 patch rendering --- hui-examples/Cargo.toml | 1 + hui-examples/assets/ninepatch_button.png | Bin 0 -> 638 bytes hui-examples/examples/ui_test_7_9patch.rs | 48 ++++- hui/src/draw.rs | 26 ++- hui/src/element/builtin/image.rs | 1 + hui/src/element/builtin/progress_bar.rs | 2 + hui/src/frame.rs | 1 + hui/src/frame/impls.rs | 2 + hui/src/frame/nine_patch.rs | 224 ++++++++++++++++++++++ hui/src/instance.rs | 3 +- hui/src/rect/color.rs | 10 +- 11 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 hui-examples/assets/ninepatch_button.png create mode 100644 hui/src/frame/nine_patch.rs diff --git a/hui-examples/Cargo.toml b/hui-examples/Cargo.toml index 797483c..fe069b4 100644 --- a/hui-examples/Cargo.toml +++ b/hui-examples/Cargo.toml @@ -14,5 +14,6 @@ glium = "0.34" winit = "0.29" glam = "0.27" log = "0.4" +image = { version = "0.25", features = ["jpeg", "png"] } #created as a workaround for rust-analyzer dependency cycle (which should be allowed) diff --git a/hui-examples/assets/ninepatch_button.png b/hui-examples/assets/ninepatch_button.png new file mode 100644 index 0000000000000000000000000000000000000000..87c7d44252f966ced8ce0cba32de919acdcfd819 GIT binary patch literal 638 zcmeAS@N?(olHy`uVBq!ia0vp^`+(SxgAGWgOcI^|q!^2X+?^QKos)S9a~60+7BevL9R^{>J!ZADMdX{MpvdVvPVW>lx1NY=G%TNRr&;0U8dt?1&5PdUJ)6BlvxxoNce@Sg zbz5qV8{ds8HkaYMACVn@XN&G@jjnsAIBYiQoUXgGe6`K_$uCQO1&i7JxtQ_gYOCHw zX2)W7#hE^8@@sxCt(QMus5JX*+GP_d-v6yxTVJ_9m?+!tZvU>#dh^}9zc#*?r|-D_ zTJ>Gm=UayU{{G8i^nSB*I9@E?8#n#+Zc7=yV_)xWd;HO2dT-wL*ZVcp5C63`*na!$ z`bPIKt*JF1j{2r;jQC+V$zG_+FiuZ=k-Nc?)XZHOdmbFtKXH=5=}Z0XDST5oJ&xcms{{IF2yp|k3tcCQwGYDz3J@%`%(H(g0uYx0@rpY=EDOgsIwT>Sqj-}!eLD^#l;JU-dP?|l9^$Lx1Xk6}H-R*xNW_n4wf QfN6%o)78&qol`;+03B5#YXATM literal 0 HcmV?d00001 diff --git a/hui-examples/examples/ui_test_7_9patch.rs b/hui-examples/examples/ui_test_7_9patch.rs index 2546653..d85b7b0 100644 --- a/hui-examples/examples/ui_test_7_9patch.rs +++ b/hui-examples/examples/ui_test_7_9patch.rs @@ -1,10 +1,9 @@ use std::time::Instant; +use glam::vec2; use hui::{ color, element::{ - container::Container, - fill_rect::FillRect, - UiElementExt - }, frame_rect, layout::{Alignment, Direction}, size + container::Container, fill_rect::FillRect, text::Text, UiElementExt + }, frame::nine_patch::{NinePatchAsset, NinePatchFrame}, frame_rect, layout::{Alignment, Direction}, rect::Rect, size }; #[path = "../boilerplate.rs"] @@ -13,21 +12,50 @@ mod boilerplate; ui_main!( "hUI: 9-Patch demo", - init: |_| { - + init: |ui| { + NinePatchAsset { + image: ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(), + size: (190, 49), + scalable_region: Rect { + position: vec2(8. / 190., 8. / 49.), + size: vec2(1. - 16. / 190., 1. - 18. / 49.), + }, + } }, - run: |ui, size, _| { + run: |ui, size, asset| { Container::default() .with_size(size!(100%)) .with_align(Alignment::Center) + .with_gap(5.) .with_background(color::WHITE) .with_children(|ui| { - FillRect::default() + Container::default() .with_size(size!(300, 100)) - .with_frame(frame_rect! { - color: color::RED + .with_background(NinePatchFrame::from_asset(*asset).with_color(color::RED)) + .with_padding(10.) + .with_children(|ui| { + Text::new("Hello, world!\nThis is a 9-patch frame used as a background \nfor Container with a Text element.\nIt's scalable and looks great!\nBelow, there are two FillRects with the same \n9-patch frame used as the background.") + .with_text_size(16) + .add_child(ui); }) .add_child(ui); + FillRect::default() + .with_size(size!(600, 75)) + .with_frame(NinePatchFrame::from_asset(*asset).with_color(color::GREEN)) + .add_child(ui); + Text::new("This one's fancy:") + .with_color(color::BLACK) + .with_text_size(32) + .add_child(ui); + FillRect::default() + .with_size(size!(800, 50)) + .with_frame(NinePatchFrame::from_asset(*asset).with_color(( + (1., 0., 1.), + (0., 1., 1.), + (1., 1., 0.), + (0., 0., 1.), + ))) + .add_child(ui); }) .add_root(ui, size); } diff --git a/hui/src/draw.rs b/hui/src/draw.rs index a29bc01..a01afa4 100644 --- a/hui/src/draw.rs +++ b/hui/src/draw.rs @@ -35,6 +35,8 @@ pub enum UiDrawCommand { color: Corners, ///Texture texture: Option, + ///Sub-UV coordinates for the texture + texture_uv: Option>, ///Rounded corners rounded_corners: Option, }, @@ -161,10 +163,32 @@ impl UiDrawCall { v.position += center; } }, - UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => { + 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 assumes that it's not rotated :p + //hell will break loose if it is + //seriously, i fvcking despise this code, and i hope to never touch this file ever again + //FIXME: this is only valid if top_left is acutally the min (e.g. only for rectangular crops) + //We currently only need rectangular crops so i don't give a fvck + let uv_size = guv.bottom_right - guv.top_left; + let mut uv_mapped = *texture_uv; + uv_mapped.top_left *= uv_size; + uv_mapped.top_right *= uv_size; + uv_mapped.bottom_left *= uv_size; + uv_mapped.bottom_right *= uv_size; + uv_mapped.top_left += guv.top_left; + uv_mapped.top_right += guv.top_left; + uv_mapped.bottom_left += guv.top_left; + uv_mapped.bottom_right += guv.top_left; + uv_mapped + } else { + guv + } + }) .unwrap_or(Corners::all(Vec2::ZERO)); let vidx = draw_call.vertices.len() as u32; diff --git a/hui/src/element/builtin/image.rs b/hui/src/element/builtin/image.rs index f46e193..1b71579 100644 --- a/hui/src/element/builtin/image.rs +++ b/hui/src/element/builtin/image.rs @@ -79,6 +79,7 @@ impl UiElement for Image { 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) }), diff --git a/hui/src/element/builtin/progress_bar.rs b/hui/src/element/builtin/progress_bar.rs index c0fd72b..d08e8b5 100644 --- a/hui/src/element/builtin/progress_bar.rs +++ b/hui/src/element/builtin/progress_bar.rs @@ -87,6 +87,7 @@ impl UiElement for ProgressBar { size: ctx.measure.size, color: self.background.corners(), texture: None, + texture_uv: None, rounded_corners }); } @@ -96,6 +97,7 @@ impl UiElement for ProgressBar { size: ctx.measure.size * vec2(value, 1.0), color: self.foreground.corners(), texture: None, + texture_uv: None, rounded_corners, }); } diff --git a/hui/src/frame.rs b/hui/src/frame.rs index aa866a3..2671209 100644 --- a/hui/src/frame.rs +++ b/hui/src/frame.rs @@ -4,6 +4,7 @@ use crate::draw::UiDrawCommandList; pub mod point; mod rect; pub mod stack; +pub mod nine_patch; mod impls; pub use rect::FrameRect; diff --git a/hui/src/frame/impls.rs b/hui/src/frame/impls.rs index 05f102b..96d24a4 100644 --- a/hui/src/frame/impls.rs +++ b/hui/src/frame/impls.rs @@ -13,6 +13,7 @@ impl Frame for ImageHandle { size: parent_size, color: color::WHITE.into(), texture: Some(*self), + texture_uv: None, rounded_corners: None, }) } @@ -29,6 +30,7 @@ impl Frame for FillColor { size: parent_size, color: self.corners(), texture: None, + texture_uv: None, rounded_corners: None, }) } diff --git a/hui/src/frame/nine_patch.rs b/hui/src/frame/nine_patch.rs new file mode 100644 index 0000000..1af14dd --- /dev/null +++ b/hui/src/frame/nine_patch.rs @@ -0,0 +1,224 @@ +use glam::{vec2, UVec2, Vec2, Vec4}; +use crate::{color, draw::{ImageHandle, UiDrawCommand}, rect::{Corners, FillColor, Rect}}; +use super::Frame; + +#[derive(Clone, Copy, Debug)] +pub struct NinePatchAsset { + pub image: ImageHandle, + //TODO: remove this + pub size: (u32, u32), + pub scalable_region: Rect, +} + +//TODO allow scaling/moving corners +#[derive(Clone, Copy, Debug)] +pub struct NinePatchFrame { + pub asset: NinePatchAsset, + pub color: FillColor, +} + +impl NinePatchFrame { + pub fn from_asset(asset: NinePatchAsset) -> Self { + Self { asset, ..Default::default() } + } + + pub fn with_color(mut self, color: impl Into) -> Self { + self.color = color.into(); + self + } +} + +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() }, + color: color::WHITE.into(), + } + } +} + +impl Frame for NinePatchFrame { + fn draw(&self, draw: &mut crate::draw::UiDrawCommandList, position: glam::Vec2, parent_size: glam::Vec2) { + // 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 = position.round(); + + let img_sz = UVec2::from(self.asset.size).as_vec2(); + + //Color stuff + let interpolate_color_rect = |uvs: Corners| { + Corners { + top_left: self.color.interpolate(uvs.top_left), + top_right: self.color.interpolate(uvs.top_right), + bottom_left: self.color.interpolate(uvs.bottom_left), + bottom_right: self.color.interpolate(uvs.bottom_right), + } + }; + + // Inset coords, in UV space + let region_uv = self.asset.scalable_region.corners(); + + // Inset coords, in image (px) space + let corners_image_px = Corners { + top_left: img_sz * region_uv.top_left, + top_right: img_sz * region_uv.top_right, + bottom_left: img_sz * region_uv.bottom_left, + bottom_right: img_sz * region_uv.bottom_right, + }; + + let size_h = ( + corners_image_px.top_left.x, + parent_size.x - corners_image_px.top_left.x - (img_sz.x - corners_image_px.top_right.x), + img_sz.x - corners_image_px.top_right.x, + ); + + let size_v = ( + corners_image_px.top_left.y, + parent_size.y - corners_image_px.top_left.y - (img_sz.y - corners_image_px.bottom_left.y), + img_sz.y - corners_image_px.bottom_left.y, + ); + + //Top-left patch + let top_left_patch_uv = Corners { + top_left: vec2(0., 0.), + top_right: vec2(region_uv.top_left.x, 0.), + 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 + }); + + //Top patch + let top_patch_uv = Corners { + top_left: vec2(region_uv.top_left.x, 0.), + top_right: vec2(region_uv.top_right.x, 0.), + 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 + }); + + //Top-right patch + let top_right_patch_uv = Corners { + top_left: vec2(region_uv.top_right.x, 0.), + top_right: vec2(1., 0.), + 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 + }); + + //Left patch + let left_patch_uv = Corners { + top_left: vec2(0., region_uv.top_left.y), + top_right: region_uv.top_left, + 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 + }); + + // 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 + }); + + //Right patch + let right_patch_uv = Corners { + top_left: region_uv.top_right, + top_right: vec2(1., region_uv.top_right.y), + 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 + }); + + //Bottom-left patch + let bottom_left_patch_uv = Corners { + top_left: vec2(0., region_uv.bottom_left.y), + top_right: region_uv.bottom_left, + 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 + }); + + //Bottom patch + let bottom_patch_uv = Corners { + top_left: region_uv.bottom_left, + top_right: region_uv.bottom_right, + 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 + }); + + //Bottom-right patch + let bottom_right_patch_uv = Corners { + top_left: region_uv.bottom_right, + top_right: vec2(1., region_uv.bottom_right.y), + 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 + }); + } + + fn covers_opaque(&self) -> bool { + false + } +} diff --git a/hui/src/instance.rs b/hui/src/instance.rs index fef361c..c488844 100644 --- a/hui/src/instance.rs +++ b/hui/src/instance.rs @@ -95,7 +95,7 @@ impl UiInstance { /// (this will change to a soft error in the future) #[cfg(feature = "image")] pub fn add_image_file_path(&mut self, path: impl AsRef) -> Result { - use std::io::Read; + use std::io::{Read, Seek}; // Open the file (and wrap it in a bufreader) let mut file = std::io::BufReader::new(std::fs::File::open(path)?); @@ -106,6 +106,7 @@ impl UiInstance { let mut magic = [0; 64]; file.read_exact(&mut magic)?; let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)"); + file.seek(std::io::SeekFrom::Start(0))?; //Parse the image and read the raw uncompressed rgba data let image = image::load(file, format).expect("Invalid image data"); diff --git a/hui/src/rect/color.rs b/hui/src/rect/color.rs index f19470e..2c77466 100644 --- a/hui/src/rect/color.rs +++ b/hui/src/rect/color.rs @@ -1,5 +1,5 @@ use super::Corners; -use glam::{Vec3, Vec4, vec4}; +use glam::{Vec2, Vec3, Vec4, vec4}; /// Represents the fill color of a rectangle /// @@ -69,6 +69,14 @@ impl FillColor { pub const fn corners(&self) -> Corners { self.0 } + + /// Interpolate color on position, assuming a linear gradient + pub fn interpolate(&self, uv: Vec2) -> Vec4 { + let c = self.corners(); + let top = c.top_left.lerp(c.top_right, uv.x); + let bottom = c.bottom_left.lerp(c.bottom_right, uv.x); + top.lerp(bottom, uv.y) + } } impl Default for FillColor {