add 9 patch rendering

This commit is contained in:
griffi-gh 2024-03-25 01:59:13 +01:00
parent c0af88fee8
commit 19ca54b1f3
11 changed files with 305 additions and 13 deletions

View file

@ -14,5 +14,6 @@ glium = "0.34"
winit = "0.29" winit = "0.29"
glam = "0.27" glam = "0.27"
log = "0.4" log = "0.4"
image = { version = "0.25", features = ["jpeg", "png"] }
#created as a workaround for rust-analyzer dependency cycle (which should be allowed) #created as a workaround for rust-analyzer dependency cycle (which should be allowed)

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

View file

@ -1,10 +1,9 @@
use std::time::Instant; use std::time::Instant;
use glam::vec2;
use hui::{ use hui::{
color, element::{ color, element::{
container::Container, container::Container, fill_rect::FillRect, text::Text, UiElementExt
fill_rect::FillRect, }, frame::nine_patch::{NinePatchAsset, NinePatchFrame}, frame_rect, layout::{Alignment, Direction}, rect::Rect, size
UiElementExt
}, frame_rect, layout::{Alignment, Direction}, size
}; };
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
@ -13,21 +12,50 @@ mod boilerplate;
ui_main!( ui_main!(
"hUI: 9-Patch demo", "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() Container::default()
.with_size(size!(100%)) .with_size(size!(100%))
.with_align(Alignment::Center) .with_align(Alignment::Center)
.with_gap(5.)
.with_background(color::WHITE) .with_background(color::WHITE)
.with_children(|ui| { .with_children(|ui| {
FillRect::default() Container::default()
.with_size(size!(300, 100)) .with_size(size!(300, 100))
.with_frame(frame_rect! { .with_background(NinePatchFrame::from_asset(*asset).with_color(color::RED))
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); .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); .add_root(ui, size);
} }

View file

@ -35,6 +35,8 @@ pub enum UiDrawCommand {
color: Corners<Vec4>, color: Corners<Vec4>,
///Texture ///Texture
texture: Option<ImageHandle>, texture: Option<ImageHandle>,
///Sub-UV coordinates for the texture
texture_uv: Option<Corners<Vec2>>,
///Rounded corners ///Rounded corners
rounded_corners: Option<RoundedCorners>, rounded_corners: Option<RoundedCorners>,
}, },
@ -161,10 +163,32 @@ impl UiDrawCall {
v.position += center; v.position += center;
} }
}, },
UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => { UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
let uvs = texture let uvs = texture
.map(|x| atlas.get_uv(x)) .map(|x| atlas.get_uv(x))
.flatten() .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)); .unwrap_or(Corners::all(Vec2::ZERO));
let vidx = draw_call.vertices.len() as u32; let vidx = draw_call.vertices.len() as u32;

View file

@ -79,6 +79,7 @@ impl UiElement for Image {
size: ctx.measure.size, size: ctx.measure.size,
color: self.color.corners(), color: self.color.corners(),
texture: Some(self.image), texture: Some(self.image),
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({ rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
RoundedCorners::from_radius(self.corner_radius) RoundedCorners::from_radius(self.corner_radius)
}), }),

View file

@ -87,6 +87,7 @@ impl UiElement for ProgressBar {
size: ctx.measure.size, size: ctx.measure.size,
color: self.background.corners(), color: self.background.corners(),
texture: None, texture: None,
texture_uv: None,
rounded_corners rounded_corners
}); });
} }
@ -96,6 +97,7 @@ impl UiElement for ProgressBar {
size: ctx.measure.size * vec2(value, 1.0), size: ctx.measure.size * vec2(value, 1.0),
color: self.foreground.corners(), color: self.foreground.corners(),
texture: None, texture: None,
texture_uv: None,
rounded_corners, rounded_corners,
}); });
} }

View file

@ -4,6 +4,7 @@ use crate::draw::UiDrawCommandList;
pub mod point; pub mod point;
mod rect; mod rect;
pub mod stack; pub mod stack;
pub mod nine_patch;
mod impls; mod impls;
pub use rect::FrameRect; pub use rect::FrameRect;

View file

@ -13,6 +13,7 @@ impl Frame for ImageHandle {
size: parent_size, size: parent_size,
color: color::WHITE.into(), color: color::WHITE.into(),
texture: Some(*self), texture: Some(*self),
texture_uv: None,
rounded_corners: None, rounded_corners: None,
}) })
} }
@ -29,6 +30,7 @@ impl Frame for FillColor {
size: parent_size, size: parent_size,
color: self.corners(), color: self.corners(),
texture: None, texture: None,
texture_uv: None,
rounded_corners: None, rounded_corners: None,
}) })
} }

224
hui/src/frame/nine_patch.rs Normal file
View file

@ -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<FillColor>) -> 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<Vec2>| {
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
}
}

View file

@ -95,7 +95,7 @@ impl UiInstance {
/// (this will change to a soft error in the future) /// (this will change to a soft error in the future)
#[cfg(feature = "image")] #[cfg(feature = "image")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> { pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
use std::io::Read; use std::io::{Read, Seek};
// Open the file (and wrap it in a bufreader) // Open the file (and wrap it in a bufreader)
let mut file = std::io::BufReader::new(std::fs::File::open(path)?); let mut file = std::io::BufReader::new(std::fs::File::open(path)?);
@ -106,6 +106,7 @@ impl UiInstance {
let mut magic = [0; 64]; let mut magic = [0; 64];
file.read_exact(&mut magic)?; file.read_exact(&mut magic)?;
let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)"); 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 //Parse the image and read the raw uncompressed rgba data
let image = image::load(file, format).expect("Invalid image data"); let image = image::load(file, format).expect("Invalid image data");

View file

@ -1,5 +1,5 @@
use super::Corners; use super::Corners;
use glam::{Vec3, Vec4, vec4}; use glam::{Vec2, Vec3, Vec4, vec4};
/// Represents the fill color of a rectangle /// Represents the fill color of a rectangle
/// ///
@ -69,6 +69,14 @@ impl FillColor {
pub const fn corners(&self) -> Corners<Vec4> { pub const fn corners(&self) -> Corners<Vec4> {
self.0 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 { impl Default for FillColor {