mirror of
https://github.com/griffi-gh/hUI.git
synced 2025-04-13 11:17:21 -05:00
Compare commits
30 commits
aa4aa9ac8a
...
1b71488df8
Author | SHA1 | Date | |
---|---|---|---|
|
1b71488df8 | ||
|
25f5abf5fd | ||
|
c7c66d5e39 | ||
|
a1b23ff13b | ||
|
a446b6d282 | ||
|
659d6a6fd5 | ||
|
1a8dd5baea | ||
|
8920a42c0c | ||
|
6297f004e3 | ||
|
68f4f2859c | ||
|
7b41ac24b6 | ||
|
72760eba54 | ||
|
0f56de482c | ||
|
e43998419a | ||
|
119b9ad077 | ||
|
0856bb620f | ||
|
e23874f67d | ||
|
3e03c36eac | ||
|
c57740b8dd | ||
|
5aaa7bb282 | ||
|
287b6f4144 | ||
|
82b2960adf | ||
|
c30bacf26c | ||
|
537d138651 | ||
|
6fd0402a28 | ||
|
e8851ee356 | ||
|
b49f0c2dba | ||
|
13503dbcb0 | ||
|
72ce1a5fa5 | ||
|
d1cb6cf5f4 |
Cargo.toml
bevy_hui
flake.lockflake.nixhui-derive
hui-examples
hui-glium
hui-loaders
hui-painter-wip/src
hui-painter
hui-shared
hui-wgpu
hui-winit
hui
Cargo.toml
src
draw.rs
draw
element.rselement/builtin
event.rsfont.rsframe.rsframe
input.rsinstance.rslib.rsmeasure.rssignal.rssignal
state.rstext.rstext
|
@ -5,8 +5,10 @@ members = [
|
|||
"hui-derive",
|
||||
"hui-examples",
|
||||
"hui-glium",
|
||||
"hui-painter-wip",
|
||||
"hui-painter",
|
||||
"hui-shared",
|
||||
"hui-wgpu",
|
||||
"hui-winit"
|
||||
"hui-winit",
|
||||
"hui-loaders",
|
||||
"bevy_hui",
|
||||
]
|
||||
|
|
16
bevy_hui/Cargo.toml
Normal file
16
bevy_hui/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "bevy_hui"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
publish = false # WIP
|
||||
|
||||
[dependencies]
|
||||
bevy = { version = "0.15", default-features = false }
|
||||
hui-painter = { version = "0.1.0-alpha.7", path = "../hui-painter", default-features = false }
|
||||
hui = { version = "0.1.0-alpha.7", path = "../hui", default-features = false, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["plugin-all"]
|
||||
plugin-all = ["plugin-painter", "plugin-input"]
|
||||
plugin-painter = []
|
||||
plugin-input = ["dep:hui"]
|
1
bevy_hui/src/input.rs
Normal file
1
bevy_hui/src/input.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub struct UiInputPlugin {}
|
7
bevy_hui/src/lib.rs
Normal file
7
bevy_hui/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod input;
|
||||
pub use input::UiInputPlugin;
|
||||
|
||||
mod render;
|
||||
pub use render::UiRenderPlugin;
|
||||
|
||||
pub struct UiPlugin {}
|
1
bevy_hui/src/render.rs
Normal file
1
bevy_hui/src/render.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub struct UiRenderPlugin {}
|
18
flake.lock
18
flake.lock
|
@ -8,11 +8,11 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1738391509,
|
||||
"narHash": "sha256-TC3xA++KgprECm/WPsLUd+a77MObZPElCW6eAsjVW1k=",
|
||||
"lastModified": 1740810935,
|
||||
"narHash": "sha256-6RzWfxENGlO73jQb3uQNgOvubUFwvveeIg+PZxhAu6s=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "de3ea31eb651b663449361f77d9c1e8835290470",
|
||||
"rev": "f44d7c3596ff028ad9f7fcc31d1941ed585f11b3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -42,11 +42,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1740547748,
|
||||
"narHash": "sha256-Ly2fBL1LscV+KyCqPRufUBuiw+zmWrlJzpWOWbahplg=",
|
||||
"lastModified": 1741494726,
|
||||
"narHash": "sha256-OGQeoY72/881aar76xEn4j1BP9s5Ezdi4BxLgYVlqv0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3a05eebede89661660945da1f151959900903b6a",
|
||||
"rev": "9004cb4a05a42b15a6a2eef84acc72152e45ff33",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -66,11 +66,11 @@
|
|||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1738224931,
|
||||
"narHash": "sha256-1zhfA5NBqin0Z79Se85juvqQteq7uClJMEb7l2pdDUY=",
|
||||
"lastModified": 1740737930,
|
||||
"narHash": "sha256-2AW/FJQI/i6bbRB/8HR9l9SjxjuiukJpHdMPgwApPKA=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "3c2aca1e5e9fbabb4e05fc4baa62e807aadc476a",
|
||||
"rev": "fe8444616679f8e50ff9696f4750df1f10e7433d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"rust-analyzer"
|
||||
"clippy"
|
||||
])
|
||||
cargo-nextest
|
||||
pkg-config
|
||||
cmake
|
||||
];
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
[package]
|
||||
name = "hui-derive"
|
||||
description = "Derive macros for hUI"
|
||||
description = "internal crate: Provides derive macros for hUI"
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["no-std", "gui", "game-development"]
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = true
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
#![no_std]
|
||||
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
|
|
@ -7,7 +7,7 @@ publish = false
|
|||
|
||||
[dev-dependencies]
|
||||
hui = { path = "../hui" }
|
||||
# hui-painter = { path = "../hui-painter" }
|
||||
hui-painter = { path = "../hui-painter" }
|
||||
hui-glium = { path = "../hui-glium" }
|
||||
hui-winit = { path = "../hui-winit" }
|
||||
kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "1e051c47b64c967305e4bbbd464ef5da2cc56bbb" }
|
||||
|
|
|
@ -60,14 +60,14 @@ pub fn ui<T>(
|
|||
let mut frame = display.draw();
|
||||
frame.clear_color_srgb(0.5, 0.5, 0.5, 1.);
|
||||
|
||||
hui.begin();
|
||||
hui.begin_frame();
|
||||
|
||||
let size = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
||||
draw(&mut hui, size, &mut result);
|
||||
|
||||
hui.end();
|
||||
hui.end_frame();
|
||||
|
||||
backend.update(&hui);
|
||||
backend.update(&hui.backend_data());
|
||||
backend.draw(&mut frame, size);
|
||||
|
||||
frame.finish().unwrap();
|
||||
|
|
|
@ -36,7 +36,7 @@ fn main() {
|
|||
|
||||
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
||||
|
||||
hui.begin();
|
||||
hui.begin_frame();
|
||||
|
||||
let z = instant.elapsed().as_secs_f32().sin().powi(2);
|
||||
|
||||
|
@ -135,9 +135,9 @@ fn main() {
|
|||
..Default::default()
|
||||
}, resolution);
|
||||
|
||||
hui.end();
|
||||
hui.end_frame();
|
||||
|
||||
backend.update(&hui);
|
||||
backend.update(&hui.backend_data());
|
||||
backend.draw(&mut frame, resolution);
|
||||
|
||||
frame.finish().unwrap();
|
||||
|
|
|
@ -19,8 +19,8 @@ mod boilerplate;
|
|||
ui_main!{
|
||||
"Mom downloader 2000",
|
||||
init: |ui| {
|
||||
let font_handle = ui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||
ui.push_font(font_handle);
|
||||
let font_handle = ui.fonts_mut().add(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||
ui.font_stack_push(font_handle);
|
||||
Instant::now()
|
||||
},
|
||||
run: |ui, max_size, instant| {
|
||||
|
@ -43,7 +43,7 @@ ui_main!{
|
|||
if instant.elapsed().as_secs_f32() < 5. {
|
||||
Text::default()
|
||||
.with_text("Downloading your mom...")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.add_child(ui);
|
||||
ProgressBar::default()
|
||||
.with_value(mom_ratio)
|
||||
|
@ -63,7 +63,7 @@ ui_main!{
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text(format!("{:.2}% ({:.1} GB)", mom_ratio * 100., mom_ratio * 10000.))
|
||||
.with_text_size(16)
|
||||
.with_text_size(16.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
@ -71,11 +71,11 @@ ui_main!{
|
|||
Text::default()
|
||||
.with_text("Error 413: Request Entity Too Large")
|
||||
.with_color((1., 0.125, 0.125, 1.))
|
||||
.with_text_size(20)
|
||||
.with_text_size(20.)
|
||||
.add_child(ui);
|
||||
Text::default()
|
||||
.with_text(format!("Exiting in {}...", 10 - instant.elapsed().as_secs()))
|
||||
.with_text_size(16)
|
||||
.with_text_size(16.)
|
||||
.add_child(ui);
|
||||
} else {
|
||||
std::process::exit(0);
|
||||
|
|
|
@ -30,7 +30,7 @@ fn main() {
|
|||
let mut hui = UiInstance::new();
|
||||
let mut backend = GliumUiRenderer::new(&display);
|
||||
|
||||
let font_handle = hui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||
let font_handle = hui.fonts_mut().add(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
|
||||
let instant = Instant::now();
|
||||
|
||||
event_loop.run(|event, window_target| {
|
||||
|
@ -45,7 +45,7 @@ fn main() {
|
|||
|
||||
let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2();
|
||||
|
||||
hui.begin();
|
||||
hui.begin_frame();
|
||||
|
||||
hui.add(Container {
|
||||
size: (Size::Relative(1.), Size::Relative(1.)).into(),
|
||||
|
@ -57,14 +57,14 @@ fn main() {
|
|||
}));
|
||||
elem.push(Box::new(Text {
|
||||
text: "THIS LINE SHOULD BE SHARP!".into(),
|
||||
text_size: 32,
|
||||
text_size: 32.,
|
||||
..Default::default()
|
||||
}));
|
||||
elem.push(Box::new(Text {
|
||||
text: "All lines except 3 and 6 below will be blurry:".into(),
|
||||
..Default::default()
|
||||
}));
|
||||
for size in [9, 12, 16, 18, 24, 32] {
|
||||
for size in [9., 12., 16., 18., 24., 32.] {
|
||||
elem.push(Box::new(Text {
|
||||
text: "Testing default font, Proggy Tiny".into(),
|
||||
text_size: size,
|
||||
|
@ -82,22 +82,34 @@ fn main() {
|
|||
elem.push(Box::new(Text {
|
||||
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
|
||||
font: Some(font_handle),
|
||||
text_size: 32,
|
||||
text_size: 32.,
|
||||
..Default::default()
|
||||
}));
|
||||
if instant.elapsed().as_secs() & 1 != 0 {
|
||||
elem.push(Box::new(FrameView {
|
||||
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||
frame: Box::new(vec4(1., 0., 0., 1.)),
|
||||
size: (Size::Relative(1.), Size::Absolute(5.)).into(),
|
||||
frame: Box::new(vec4(0.3569, 0.8078, 0.9804, 1.)), // Light Blue (#5BCEFA)
|
||||
}));
|
||||
elem.push(Box::new(FrameView {
|
||||
size: (Size::Relative(1.), Size::Absolute(10.)).into(),
|
||||
frame: Box::new(vec4(0., 0., 0., 1.)),
|
||||
size: (Size::Relative(1.), Size::Absolute(5.)).into(),
|
||||
frame: Box::new(vec4(0.9608, 0.6627, 0.7216, 1.)), // Pink (#F5A9B8)
|
||||
}));
|
||||
elem.push(Box::new(FrameView {
|
||||
size: (Size::Relative(1.), Size::Absolute(5.)).into(),
|
||||
frame: Box::new(vec4(1.0, 1.0, 1.0, 1.)), // White
|
||||
}));
|
||||
elem.push(Box::new(FrameView {
|
||||
size: (Size::Relative(1.), Size::Absolute(5.)).into(),
|
||||
frame: Box::new(vec4(0.9608, 0.6627, 0.7216, 1.)), // Pink (#F5A9B8)
|
||||
}));
|
||||
elem.push(Box::new(FrameView {
|
||||
size: (Size::Relative(1.), Size::Absolute(5.)).into(),
|
||||
frame: Box::new(vec4(0.3569, 0.8078, 0.9804, 1.)), // Light Blue (#5BCEFA)
|
||||
}));
|
||||
elem.push(Box::new(Spacer(100.)));
|
||||
elem.push(Box::new(Text {
|
||||
text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(),
|
||||
text_size: 64,
|
||||
text_size: 64.,
|
||||
color: vec4(1., 0., 1., 1.),
|
||||
..Default::default()
|
||||
}));
|
||||
|
@ -106,9 +118,9 @@ fn main() {
|
|||
..Default::default()
|
||||
}, resolution);
|
||||
|
||||
hui.end();
|
||||
hui.end_frame();
|
||||
|
||||
backend.update(&hui);
|
||||
backend.update(&hui.backend_data());
|
||||
backend.draw(&mut frame, resolution);
|
||||
|
||||
frame.finish().unwrap();
|
||||
|
|
|
@ -21,7 +21,7 @@ ui_main!(|ui, size, _| {
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text("Hello, world")
|
||||
.with_text_size(100)
|
||||
.with_text_size(100.)
|
||||
.with_color(color::BLACK)
|
||||
.add_child(ui);
|
||||
Container::default()
|
||||
|
@ -33,7 +33,7 @@ ui_main!(|ui, size, _| {
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
|
|
@ -10,8 +10,8 @@ use hui::{
|
|||
},
|
||||
layout::Alignment,
|
||||
rect::Corners,
|
||||
text::FontHandle
|
||||
};
|
||||
use hui_painter::text::FontHandle;
|
||||
|
||||
#[path = "../boilerplate.rs"]
|
||||
#[macro_use]
|
||||
|
@ -20,8 +20,8 @@ mod boilerplate;
|
|||
ui_main!(
|
||||
"hUI: Loading screen demo",
|
||||
init: |ui| {
|
||||
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||
ui.push_font(font);
|
||||
let font = ui.fonts_mut().add(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||
ui.font_stack_push(font);
|
||||
(std::time::Instant::now(),)
|
||||
},
|
||||
run: |ui, size, (instant,)| {
|
||||
|
@ -53,7 +53,7 @@ ui_main!(
|
|||
Text::default()
|
||||
.with_text("Loading...")
|
||||
.with_color((1., 1., 1., flash))
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
@ -76,11 +76,11 @@ ui_main!(
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text("Did you know?")
|
||||
.with_text_size(18)
|
||||
.with_text_size(18.)
|
||||
.add_child(ui);
|
||||
Text::default()
|
||||
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.with_font(FontHandle::default())
|
||||
.add_child(ui);
|
||||
})
|
||||
|
@ -117,11 +117,11 @@ ui_main!(
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text("Level 5")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.add_child(ui);
|
||||
Text::default()
|
||||
.with_text("XP: 1234 / 5000")
|
||||
.with_text_size(18)
|
||||
.with_text_size(18.)
|
||||
.with_font(FontHandle::default())
|
||||
.add_child(ui);
|
||||
})
|
||||
|
|
|
@ -10,10 +10,10 @@ use hui::{
|
|||
},
|
||||
layout::Alignment,
|
||||
rect::Corners,
|
||||
text::FontHandle,
|
||||
rect_frame,
|
||||
size,
|
||||
};
|
||||
use hui_painter::text::FontHandle;
|
||||
|
||||
#[path = "../boilerplate.rs"]
|
||||
#[macro_use]
|
||||
|
@ -22,8 +22,8 @@ mod boilerplate;
|
|||
ui_main!(
|
||||
"hUI: Transform API demo",
|
||||
init: |ui| {
|
||||
let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||
ui.push_font(font);
|
||||
let font = ui.fonts_mut().add(include_bytes!("../assets/blink/Blink-ynYZ.otf"));
|
||||
ui.font_stack_push(font);
|
||||
(std::time::Instant::now(),)
|
||||
},
|
||||
run: |ui, size, (instant,)| {
|
||||
|
@ -49,11 +49,11 @@ ui_main!(
|
|||
.with_children(|ui| {
|
||||
Text::default()
|
||||
.with_text("Did you know?")
|
||||
.with_text_size(18)
|
||||
.with_text_size(18.)
|
||||
.add_child(ui);
|
||||
Text::default()
|
||||
.with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.with_font(FontHandle::default())
|
||||
.add_child(ui);
|
||||
})
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use hui::{
|
||||
color, size,
|
||||
signal::Signal,
|
||||
draw::TextureFormat,
|
||||
layout::{Alignment, Direction},
|
||||
element::{
|
||||
container::Container,
|
||||
|
@ -12,6 +11,7 @@ use hui::{
|
|||
UiElementExt,
|
||||
},
|
||||
};
|
||||
use hui_painter::texture::SourceTextureFormat;
|
||||
|
||||
#[derive(Signal)]
|
||||
enum CounterSignal {
|
||||
|
@ -28,7 +28,10 @@ const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-ico
|
|||
ui_main!(
|
||||
"hUI: Internal input test",
|
||||
init: |ui| {
|
||||
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
|
||||
let image = ui.textures_mut().add_with_data(
|
||||
SourceTextureFormat::RGBA8,
|
||||
IMAGE_DATA, 32,
|
||||
);
|
||||
(0, image)
|
||||
},
|
||||
run: |ui, size, &mut (ref mut counter, image)| {
|
||||
|
@ -42,7 +45,7 @@ ui_main!(
|
|||
.with_wrap(true)
|
||||
.with_children(|ui| {
|
||||
Text::new("Number of images:")
|
||||
.with_text_size(24)
|
||||
.with_text_size(24.)
|
||||
.add_child(ui);
|
||||
Break.add_child(ui);
|
||||
Container::default()
|
||||
|
@ -50,7 +53,7 @@ ui_main!(
|
|||
.with_background(color::ORANGE)
|
||||
.with_children(|ui| {
|
||||
Text::new("-")
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.on_click(|| CounterSignal::Decrement)
|
||||
|
@ -60,7 +63,7 @@ ui_main!(
|
|||
.with_align(Alignment::Center)
|
||||
.with_children(|ui| {
|
||||
Text::new(counter.to_string())
|
||||
.with_text_size(64)
|
||||
.with_text_size(64.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
@ -69,7 +72,7 @@ ui_main!(
|
|||
.with_background(color::ORANGE)
|
||||
.with_children(|ui| {
|
||||
Text::new("+")
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.on_click(|| CounterSignal::Increment)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use hui::{
|
||||
draw::TextureFormat,
|
||||
element::{
|
||||
br::Break,
|
||||
container::Container,
|
||||
|
@ -12,6 +11,7 @@ use hui::{
|
|||
layout::{Alignment, Direction},
|
||||
size,
|
||||
};
|
||||
use hui_painter::texture::SourceTextureFormat;
|
||||
|
||||
#[derive(Signal)]
|
||||
enum CounterSignal {
|
||||
|
@ -27,7 +27,7 @@ const IMAGE_DATA: &[u8] = include_bytes!("../assets/icons/visual-studio-code-ico
|
|||
ui_main!(
|
||||
"hUI: Internal input test",
|
||||
init: |ui| {
|
||||
let image = ui.add_image(TextureFormat::Rgba, IMAGE_DATA, 32);
|
||||
let image = ui.textures_mut().add_with_data(SourceTextureFormat::RGBA8, IMAGE_DATA, 32);
|
||||
(0, image)
|
||||
},
|
||||
run: |ui, size, &mut (ref mut counter, image)| {
|
||||
|
@ -41,11 +41,11 @@ ui_main!(
|
|||
.with_wrap(true)
|
||||
.with_children(|ui| {
|
||||
Text::new(format!("Number of images: {counter}"))
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
Break.add_child(ui);
|
||||
Text::new("Absolute tracking slider:")
|
||||
.with_text_size(16)
|
||||
.with_text_size(16.)
|
||||
.add_child(ui);
|
||||
Break.add_child(ui);
|
||||
Slider::new(*counter as f32 / 100.)
|
||||
|
@ -56,7 +56,7 @@ ui_main!(
|
|||
.add_child(ui);
|
||||
Break.add_child(ui);
|
||||
Text::new("Relative tracking slider (Experimental):")
|
||||
.with_text_size(16)
|
||||
.with_text_size(16.)
|
||||
.add_child(ui);
|
||||
Break.add_child(ui);
|
||||
Slider::new(*counter as f32 / 100.)
|
||||
|
|
|
@ -24,10 +24,11 @@ struct SetValue(f32);
|
|||
|
||||
ui_main!(
|
||||
"hUI: 9-Patch demo",
|
||||
init: |ui| {
|
||||
init: |_ui| {
|
||||
(
|
||||
NinePatchAsset {
|
||||
image: ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(),
|
||||
// FIXME add image loader here
|
||||
image: todo!("FIXME add image loader here"), //ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(),
|
||||
size: (190, 49),
|
||||
scalable_region: Rect {
|
||||
position: vec2(8. / 190., 8. / 49.),
|
||||
|
@ -50,7 +51,7 @@ ui_main!(
|
|||
.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)
|
||||
.with_text_size(16.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
@ -60,7 +61,7 @@ ui_main!(
|
|||
.add_child(ui);
|
||||
Text::new("This one's fancy:")
|
||||
.with_color(color::BLACK)
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
FrameView::default()
|
||||
.with_size(size!(700, 50))
|
||||
|
@ -73,7 +74,7 @@ ui_main!(
|
|||
.add_child(ui);
|
||||
Text::new("Slider customized with `NinePatchFrame`s:")
|
||||
.with_color(color::BLACK)
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
Slider::new(*value)
|
||||
.with_size(size!(50%, 30))
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
use hui::{
|
||||
color, size,
|
||||
draw::{ImageHandle, TextureFormat},
|
||||
layout::{Alignment, Direction},
|
||||
element::{
|
||||
container::Container,
|
||||
|
@ -12,22 +11,27 @@ use hui::{
|
|||
UiElementExt
|
||||
},
|
||||
};
|
||||
use hui_painter::texture::{SourceTextureFormat, TextureHandle};
|
||||
|
||||
#[path = "../boilerplate.rs"]
|
||||
#[macro_use]
|
||||
mod boilerplate;
|
||||
|
||||
struct Stuff {
|
||||
vscode_icon: ImageHandle,
|
||||
vscode_icon: TextureHandle,
|
||||
}
|
||||
|
||||
ui_main!(
|
||||
"hUI: vscode demo",
|
||||
init: |ui| {
|
||||
let handle = ui.add_font(include_bytes!("../assets/fira/FiraSans-Light.ttf"));
|
||||
ui.push_font(handle);
|
||||
let handle = ui.fonts_mut().add(include_bytes!("../assets/fira/FiraSans-Light.ttf"));
|
||||
ui.font_stack_push(handle);
|
||||
Stuff {
|
||||
vscode_icon: ui.add_image(TextureFormat::Rgba, include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba"), 32),
|
||||
vscode_icon: ui.textures_mut().add_with_data(
|
||||
SourceTextureFormat::RGBA8,
|
||||
include_bytes!("../assets/icons/visual-studio-code-icon_32x32.rgba"),
|
||||
32
|
||||
),
|
||||
}
|
||||
},
|
||||
run: |ui, size, stuff| {
|
||||
|
@ -48,7 +52,7 @@ ui_main!(
|
|||
.add_child(ui);
|
||||
for item in ["File", "Edit", "Selection", "View", "Go", "Run", "Terminal", "Help"] {
|
||||
Text::new(item)
|
||||
.with_text_size(15)
|
||||
.with_text_size(15.)
|
||||
.add_child(ui);
|
||||
}
|
||||
Container::default()
|
||||
|
@ -56,7 +60,7 @@ ui_main!(
|
|||
.with_align((Alignment::End, Alignment::Center))
|
||||
.with_children(|ui| {
|
||||
Text::new("- ×")
|
||||
.with_text_size(32)
|
||||
.with_text_size(32.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
@ -111,12 +115,12 @@ ui_main!(
|
|||
.with_padding((10., 2.))
|
||||
.with_children(|ui| {
|
||||
Text::new("><")
|
||||
.with_text_size(13)
|
||||
.with_text_size(13.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
Text::new("master")
|
||||
.with_text_size(15)
|
||||
.with_text_size(15.)
|
||||
.add_child(ui);
|
||||
})
|
||||
.add_child(ui);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
[package]
|
||||
name = "hui-glium"
|
||||
description = "glium render backend for `hui`"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
rust-version = "1.85"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
@ -16,7 +18,7 @@ include = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
hui = { version = "=0.1.0-alpha.6", path = "../hui", default-features = false }
|
||||
hui-painter = { version = "=0.1.0-alpha.7", path = "../hui-painter", default-features = false }
|
||||
glium = { version = "0.36", default-features = false }
|
||||
glam = "0.30"
|
||||
log = "0.4"
|
||||
|
|
|
@ -1,11 +1,27 @@
|
|||
use std::rc::Rc;
|
||||
use glam::Vec2;
|
||||
use glium::{
|
||||
backend::{Context, Facade}, implement_vertex, index::PrimitiveType, texture::{RawImage2d, Texture2d}, uniform, uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction}, Api, Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer
|
||||
};
|
||||
use hui::{
|
||||
draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
|
||||
backend::{Context, Facade},
|
||||
index::PrimitiveType,
|
||||
texture::{RawImage2d, Texture2d},
|
||||
uniforms::{
|
||||
MagnifySamplerFilter,
|
||||
MinifySamplerFilter,
|
||||
Sampler,
|
||||
SamplerBehavior,
|
||||
SamplerWrapFunction
|
||||
},
|
||||
Api,
|
||||
Blend,
|
||||
DrawParameters,
|
||||
IndexBuffer,
|
||||
Program,
|
||||
Surface,
|
||||
VertexBuffer,
|
||||
implement_vertex,
|
||||
uniform,
|
||||
};
|
||||
use hui_painter::{backend::BackendData, paint::buffer::Vertex, presentation::PresentatationBackendData, texture::TextureAtlasBackendData};
|
||||
|
||||
const VERTEX_SHADER_GLES3: &str = include_str!("../shaders/vertex.es.vert");
|
||||
const FRAGMENT_SHADER_GLES3: &str = include_str!("../shaders/fragment.es.frag");
|
||||
|
@ -15,14 +31,14 @@ const FRAGMENT_SHADER_150: &str = include_str!("../shaders/fragment.150.frag");
|
|||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
struct Vertex {
|
||||
struct GlVertex {
|
||||
position: [f32; 2],
|
||||
color: [f32; 4],
|
||||
uv: [f32; 2],
|
||||
}
|
||||
|
||||
impl From<UiVertex> for Vertex {
|
||||
fn from(v: UiVertex) -> Self {
|
||||
impl From<Vertex> for GlVertex {
|
||||
fn from(v: Vertex) -> Self {
|
||||
Self {
|
||||
position: v.position.to_array(),
|
||||
color: v.color.to_array(),
|
||||
|
@ -31,17 +47,17 @@ impl From<UiVertex> for Vertex {
|
|||
}
|
||||
}
|
||||
|
||||
implement_vertex!(Vertex, position, color, uv);
|
||||
implement_vertex!(GlVertex, position, color, uv);
|
||||
|
||||
struct BufferPair {
|
||||
pub vertex_buffer: glium::VertexBuffer<Vertex>,
|
||||
pub vertex_buffer: glium::VertexBuffer<GlVertex>,
|
||||
pub index_buffer: glium::IndexBuffer<u32>,
|
||||
pub vertex_count: usize,
|
||||
pub index_count: usize,
|
||||
}
|
||||
|
||||
impl BufferPair {
|
||||
pub fn new<F: Facade>(facade: &F) -> Self {
|
||||
pub fn new_empty<F: Facade>(facade: &F) -> Self {
|
||||
log::debug!("init ui buffers (empty)...");
|
||||
Self {
|
||||
vertex_buffer: VertexBuffer::empty_dynamic(facade, 1024).unwrap(),
|
||||
|
@ -51,7 +67,7 @@ impl BufferPair {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_with_data<F: Facade>(facade: &F, vtx: &[Vertex], idx: &[u32]) -> Self {
|
||||
pub fn new_with_data<F: Facade>(facade: &F, vtx: &[GlVertex], idx: &[u32]) -> Self {
|
||||
log::debug!("init ui buffers (data)...");
|
||||
Self {
|
||||
vertex_buffer: VertexBuffer::dynamic(facade, vtx).unwrap(),
|
||||
|
@ -62,7 +78,7 @@ impl BufferPair {
|
|||
}
|
||||
|
||||
pub fn ensure_buffer_size(&mut self, need_vtx: usize, need_idx: usize) {
|
||||
let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::<Vertex>();
|
||||
let current_vtx_size = self.vertex_buffer.get_size() / std::mem::size_of::<GlVertex>();
|
||||
let current_idx_size = self.index_buffer.get_size() / std::mem::size_of::<u32>();
|
||||
//log::debug!("current vtx size: {}, current idx size: {}", current_vtx_size, current_idx_size);
|
||||
if current_vtx_size >= need_vtx && current_idx_size >= need_idx {
|
||||
|
@ -86,7 +102,7 @@ impl BufferPair {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn write_data(&mut self, vtx: &[Vertex], idx: &[u32]) {
|
||||
pub fn write_data(&mut self, vtx: &[GlVertex], idx: &[u32]) {
|
||||
//log::trace!("uploading {} vertices and {} indices", vtx.len(), idx.len());
|
||||
|
||||
self.vertex_count = vtx.len();
|
||||
|
@ -113,7 +129,9 @@ pub struct GliumUiRenderer {
|
|||
context: Rc<Context>,
|
||||
program: glium::Program,
|
||||
ui_texture: Option<Texture2d>,
|
||||
ui_texture_version: u64,
|
||||
buffer_pair: Option<BufferPair>,
|
||||
buffer_hash: u64,
|
||||
}
|
||||
|
||||
impl GliumUiRenderer {
|
||||
|
@ -126,22 +144,25 @@ impl GliumUiRenderer {
|
|||
},
|
||||
context: Rc::clone(facade.get_context()),
|
||||
ui_texture: None,
|
||||
ui_texture_version: 0,
|
||||
buffer_pair: None,
|
||||
buffer_hash: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_buffers(&mut self, call: &UiDrawCall) {
|
||||
log::trace!("updating ui buffers (tris: {})", call.indices.len() / 3);
|
||||
let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::<Vec<_>>()[..];
|
||||
let data_idx = &call.indices[..];
|
||||
fn update_buffers(&mut self, data: &PresentatationBackendData) {
|
||||
log::trace!("updating ui buffers (tris: {})", data.buffer.indices.len() / 3);
|
||||
let data_vtx = &data.buffer.vertices.iter().copied().map(GlVertex::from).collect::<Vec<_>>()[..];
|
||||
let data_idx = &data.buffer.indices[..];
|
||||
if let Some(buffer) = &mut self.buffer_pair {
|
||||
buffer.write_data(data_vtx, data_idx);
|
||||
} else if !call.indices.is_empty() {
|
||||
} else if !data.buffer.indices.is_empty() {
|
||||
self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx));
|
||||
}
|
||||
self.buffer_hash = data.hash;
|
||||
}
|
||||
|
||||
fn update_texture_atlas(&mut self, atlas: &TextureAtlasMeta) {
|
||||
fn update_texture_atlas(&mut self, atlas: &TextureAtlasBackendData) {
|
||||
log::trace!("updating ui atlas texture");
|
||||
self.ui_texture = Some(Texture2d::new(
|
||||
&self.context,
|
||||
|
@ -150,14 +171,15 @@ impl GliumUiRenderer {
|
|||
(atlas.size.x, atlas.size.y)
|
||||
)
|
||||
).unwrap());
|
||||
self.ui_texture_version = atlas.version;
|
||||
}
|
||||
|
||||
pub fn update(&mut self, instance: &UiInstance) {
|
||||
if self.ui_texture.is_none() || instance.atlas().modified {
|
||||
self.update_texture_atlas(&instance.atlas());
|
||||
pub fn update(&mut self, data: &BackendData) {
|
||||
if self.ui_texture_version != data.atlas.version {
|
||||
self.update_texture_atlas(&data.atlas);
|
||||
}
|
||||
if self.buffer_pair.is_none() || instance.draw_call().0 {
|
||||
self.update_buffers(instance.draw_call().1);
|
||||
if self.buffer_hash != data.presentation.hash {
|
||||
self.update_buffers(&data.presentation);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
28
hui-loaders/Cargo.toml
Normal file
28
hui-loaders/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "hui-loaders"
|
||||
description = "Image loader providers for hUI-painter"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = false # WIP
|
||||
include = [
|
||||
"src/**/*.rs",
|
||||
"Cargo.toml",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
hui-painter = { version = "=0.1.0-alpha.7", path = "../hui-painter", default-features = false }
|
||||
image = { version = "0.25", default-features = false, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["loader-file", "loader-image", "image-default-formats"]
|
||||
# hui-painter = ["dep:hui-painter"]
|
||||
loader-file = []
|
||||
loader-image = ["dep:image"]
|
||||
image-default-formats = ["loader-image", "image/default-formats"]
|
43
hui-loaders/src/lib.rs
Normal file
43
hui-loaders/src/lib.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use hui_painter::texture::{SourceTextureFormat, TextureAtlas, TextureHandle};
|
||||
|
||||
pub mod loaders {
|
||||
#[cfg(feature = "loader-file")]
|
||||
mod file;
|
||||
#[cfg(feature = "loader-file")]
|
||||
pub use file::FileLoader;
|
||||
|
||||
#[cfg(feature = "loader-image")]
|
||||
mod image;
|
||||
#[cfg(feature = "loader-image")]
|
||||
pub use image::ImageLoader;
|
||||
}
|
||||
|
||||
pub trait RawDataLoader {
|
||||
/// Syncronously load the raw data from the source
|
||||
fn load(&self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
pub struct TextureData {
|
||||
/// Texture data in the RGBA8 format
|
||||
pub data: Vec<u8>,
|
||||
|
||||
/// Texture width in pixel
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
pub trait TextureLoader {
|
||||
/// Syncronously load the texture data
|
||||
fn load(&self) -> TextureData;
|
||||
}
|
||||
|
||||
pub trait AtlasLoadersExt {
|
||||
fn add_with_loader(&mut self, loader: impl TextureLoader) -> TextureHandle;
|
||||
}
|
||||
|
||||
impl AtlasLoadersExt for TextureAtlas {
|
||||
fn add_with_loader(&mut self, loader: impl TextureLoader) -> TextureHandle {
|
||||
let data = loader.load();
|
||||
self.add_with_data(SourceTextureFormat::RGBA8, &data.data, data.width)
|
||||
}
|
||||
}
|
||||
|
3
hui-loaders/src/loaders/file.rs
Normal file
3
hui-loaders/src/loaders/file.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub struct FileLoader {
|
||||
|
||||
}
|
16
hui-loaders/src/loaders/image.rs
Normal file
16
hui-loaders/src/loaders/image.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
use crate::{RawDataLoader, TextureData, TextureLoader};
|
||||
|
||||
pub struct ImageLoader<T: RawDataLoader>(pub T);
|
||||
|
||||
impl<T: RawDataLoader> From<T> for ImageLoader<T> {
|
||||
fn from(loader: T) -> Self {
|
||||
Self(loader)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RawDataLoader> TextureLoader for ImageLoader<T> {
|
||||
fn load(&self) -> TextureData {
|
||||
let data = self.0.load();
|
||||
todo!()
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
pub mod paint;
|
||||
pub mod texture;
|
||||
pub mod text;
|
||||
pub mod util;
|
||||
|
||||
use text::FontManager;
|
||||
use texture::TextureAtlas;
|
||||
|
||||
/// Painter instance, stores textures and fonts needed for rendering
|
||||
#[derive(Default)]
|
||||
pub struct PainterInstance {
|
||||
pub atlas: TextureAtlas,
|
||||
pub fonts: FontManager,
|
||||
}
|
||||
|
||||
impl PainterInstance {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// pub fn atlas(&self) -> &TextureAtlas {
|
||||
// &self.atlas
|
||||
// }
|
||||
|
||||
// pub fn atlas_mut(&mut self) -> &mut TextureAtlas {
|
||||
// &mut self.atlas
|
||||
// }
|
||||
|
||||
// pub fn fonts(&self) -> &FontManager {
|
||||
// &self.fonts
|
||||
// }
|
||||
|
||||
// pub fn fonts_mut(&mut self) -> &mut FontManager {
|
||||
// &mut self.fonts
|
||||
// }
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
mod atlas;
|
||||
pub use atlas::*;
|
|
@ -1,25 +1,31 @@
|
|||
[package]
|
||||
name = "hui-painter-wip"
|
||||
name = "hui-painter"
|
||||
description = "UI rendering middleware for hUI, abstracts away triangulation, text layout and rendering and all the other hard stuff."
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["no-std", "gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = false # TODO: change to true once ready
|
||||
publish = true
|
||||
include = [
|
||||
"assets/**/*",
|
||||
"src/**/*.rs",
|
||||
"Cargo.toml",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
hui-shared = { version = "0.1.0-alpha.6", path = "../hui-shared" }
|
||||
hui-shared = { version = "0.1.0-alpha.7", path = "../hui-shared" }
|
||||
glam = "0.30"
|
||||
log = "0.4"
|
||||
rect_packer = "0.2" # TODO: use sth else like `crunch` instead?
|
||||
hashbrown = "0.15"
|
||||
nohash-hasher = "0.2"
|
||||
fontdue = "0.9"
|
||||
rustc-hash = "2.0"
|
||||
|
||||
[features]
|
||||
default = ["default-font"]
|
||||
default-font = []
|
22
hui-painter/src/backend.rs
Normal file
22
hui-painter/src/backend.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use crate::{
|
||||
presentation::{Presentatation, PresentatationBackendData},
|
||||
texture::TextureAtlasBackendData,
|
||||
PainterInstance,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct BackendData<'a> {
|
||||
pub presentation: PresentatationBackendData<'a>,
|
||||
pub atlas: TextureAtlasBackendData<'a>,
|
||||
}
|
||||
|
||||
impl PainterInstance {
|
||||
pub fn backend_data<'a>(&'a self, presentation: &'a Presentatation) -> BackendData<'a> {
|
||||
BackendData {
|
||||
presentation: presentation.backend_data(),
|
||||
atlas: self.textures.backend_data(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
46
hui-painter/src/lib.rs
Normal file
46
hui-painter/src/lib.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
#![no_std]
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
pub mod paint;
|
||||
pub mod texture;
|
||||
pub mod text;
|
||||
pub mod util;
|
||||
pub mod backend;
|
||||
pub mod presentation;
|
||||
|
||||
use text::FontManager;
|
||||
use texture::TextureAtlas;
|
||||
|
||||
/// Painter instance, stores textures and fonts needed for rendering
|
||||
#[derive(Default)]
|
||||
pub struct PainterInstance {
|
||||
pub(crate) textures: TextureAtlas,
|
||||
pub(crate) fonts: FontManager,
|
||||
}
|
||||
|
||||
impl PainterInstance {
|
||||
pub fn new() -> Self {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
use alloc::vec::Vec;
|
||||
use glam::{Vec2, Vec4};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct Vertex {
|
||||
pub position: Vec2, //Vec3,
|
||||
pub uv: Vec2,
|
||||
|
@ -18,6 +21,11 @@ impl PaintBuffer {
|
|||
indices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.vertices.clear();
|
||||
self.indices.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PaintBuffer {
|
|
@ -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\
|
|
@ -1,4 +1,6 @@
|
|||
use std::{hash::Hasher, ops::RangeFull};
|
||||
use core::{hash::Hasher, ops::RangeFull};
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use hui_shared::rect::Rect;
|
||||
use crate::PainterInstance;
|
||||
use super::PaintCommand;
|
||||
|
@ -23,6 +25,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 {
|
|
@ -1,4 +1,4 @@
|
|||
use std::{hash::Hasher, num::NonZeroU16};
|
||||
use core::{hash::Hasher, num::NonZeroU16};
|
||||
use glam::{vec2, Vec2};
|
||||
use hui_shared::{color, rect::{Corners, FillColor, Rect}};
|
||||
use crate::{
|
||||
|
@ -7,7 +7,7 @@ use crate::{
|
|||
command::PaintCommand,
|
||||
},
|
||||
texture::TextureHandle,
|
||||
util::{hash_vec2, hash_vec3, hash_vec4},
|
||||
util::{hash_vec2, hash_vec4},
|
||||
PainterInstance,
|
||||
};
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -224,7 +224,7 @@ impl PaintCommand for PaintRectangle {
|
|||
|
||||
for i in 0..point_count as u32 {
|
||||
let frac = i as f32 / (point_count - 1) as f32;
|
||||
let angle = frac * std::f32::consts::PI * 0.5;
|
||||
let angle = frac * core::f32::consts::PI * 0.5;
|
||||
let x = angle.sin();
|
||||
let y = angle.cos();
|
||||
into.vertices.extend([
|
|
@ -1,12 +1,16 @@
|
|||
use std::{borrow::Cow, hash::{Hash, Hasher}};
|
||||
use fontdue::layout::{CoordinateSystem, GlyphRasterConfig, Layout};
|
||||
use core::hash::{Hash, Hasher};
|
||||
use alloc::{borrow::Cow, vec::Vec};
|
||||
use fontdue::layout::{CoordinateSystem, Layout};
|
||||
use glam::{vec2, Vec4};
|
||||
use hui_shared::rect::Rect;
|
||||
use crate::{
|
||||
paint::{
|
||||
buffer::{PaintBuffer, Vertex},
|
||||
command::PaintCommand,
|
||||
}, text::FontHandle, PainterInstance
|
||||
},
|
||||
text::FontHandle,
|
||||
util::hash_vec4,
|
||||
PainterInstance,
|
||||
};
|
||||
|
||||
// TODO align, multichunk etc
|
||||
|
@ -66,7 +70,10 @@ 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);
|
||||
if !glyph.char_data.rasterize() {
|
||||
continue;
|
||||
}
|
||||
ctx.fonts.render_glyph(&mut ctx.textures, self.text.font, glyph.key);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,28 +96,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,
|
||||
},
|
||||
|
@ -145,9 +152,23 @@ impl PaintCommand for PaintText {
|
|||
|
||||
fn cache_hash(&self) -> u64 {
|
||||
let mut hasher = rustc_hash::FxHasher::default();
|
||||
|
||||
// cache font/size/color
|
||||
self.text.font.hash(&mut hasher);
|
||||
hasher.write_u32(self.text.size.to_bits());
|
||||
hasher.write(self.text.text.as_bytes());
|
||||
hash_vec4(&mut hasher, self.text.color);
|
||||
|
||||
// cache text content
|
||||
match self.text.text {
|
||||
Cow::Owned(ref s) => hasher.write(s.as_bytes()),
|
||||
Cow::Borrowed(s) => {
|
||||
// since the lifetime is 'static, the str is guaranteed to never change
|
||||
// so we can safely compare the ptr + len instead of the content
|
||||
hasher.write_usize(s.as_ptr() as usize);
|
||||
hasher.write_usize(s.len());
|
||||
}
|
||||
}
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
use std::hash::Hasher;
|
||||
|
||||
use core::hash::Hasher;
|
||||
use glam::vec2;
|
||||
use hui_shared::rect::Rect;
|
||||
|
83
hui-painter/src/presentation.rs
Normal file
83
hui-painter/src/presentation.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
use crate::{
|
||||
PainterInstance,
|
||||
paint::{
|
||||
buffer::PaintBuffer,
|
||||
command::PaintRoot,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Presentatation {
|
||||
current_buffer: PaintBuffer,
|
||||
cur_hash: Option<u64>,
|
||||
prev_hash: Option<u64>,
|
||||
version_counter: u64,
|
||||
}
|
||||
|
||||
impl Presentatation {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current_buffer: PaintBuffer::new(),
|
||||
cur_hash: None,
|
||||
prev_hash: None,
|
||||
version_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// If the paint command has changed since the last draw call, draw it and return true.\
|
||||
/// Otherwise, returns false.
|
||||
pub fn draw(&mut self, painter: &mut PainterInstance, cmd: &impl PaintRoot) -> bool {
|
||||
self.prev_hash = self.cur_hash;
|
||||
self.cur_hash = Some(cmd.cache_hash());
|
||||
|
||||
if self.prev_hash == self.cur_hash {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.current_buffer.clear();
|
||||
cmd.paint_root(painter, &mut self.current_buffer);
|
||||
|
||||
self.version_counter = self.version_counter.wrapping_add(1);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Get the current paint buffer
|
||||
pub fn buffer(&self) -> &PaintBuffer {
|
||||
&self.current_buffer
|
||||
}
|
||||
|
||||
/// Get the complete backend data for the current presentation
|
||||
///
|
||||
/// It contains the current paint buffer and the hash of the presentation\
|
||||
/// Unlike the `TextureAtlasBackendData`, the version is non-incremental
|
||||
pub fn backend_data(&self) -> PresentatationBackendData {
|
||||
PresentatationBackendData {
|
||||
buffer: &self.current_buffer,
|
||||
version: self.version_counter,
|
||||
hash: self.cur_hash.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Presentatation {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Backend data for the Presentation
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PresentatationBackendData<'a> {
|
||||
/// The current paint buffer
|
||||
pub buffer: &'a PaintBuffer,
|
||||
|
||||
/// The version of the presentation
|
||||
///
|
||||
/// This is incremented every time the buffer hash changes
|
||||
pub version: u64,
|
||||
|
||||
/// Unique hash of current paint buffer commands
|
||||
pub hash: u64,
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ use crate::texture::{TextureAtlas, TextureHandle};
|
|||
pub(crate) mod ftm;
|
||||
pub(crate) mod font;
|
||||
|
||||
pub use font::FontHandle;
|
||||
pub use font::{FontHandle, DEFAULT_FONT};
|
||||
|
||||
pub struct FontManager {
|
||||
fonts: font::FontHandleManager,
|
||||
|
@ -12,17 +12,26 @@ pub struct FontManager {
|
|||
}
|
||||
|
||||
impl FontManager {
|
||||
pub fn new() -> Self {
|
||||
pub(crate) fn new_internal() -> Self {
|
||||
Self {
|
||||
fonts: font::FontHandleManager::new(),
|
||||
ftm: ftm::FontTextureManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a font to the manager.
|
||||
pub fn new() -> Self {
|
||||
let mut this = Self::new_internal();
|
||||
#[cfg(feature="default-font")] {
|
||||
this.fonts.idc = 0;
|
||||
this.add(include_bytes!("../assets/font/ProggyTiny.ttf"));
|
||||
}
|
||||
this
|
||||
}
|
||||
|
||||
/// 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);
|
|
@ -2,23 +2,32 @@ use hashbrown::HashMap;
|
|||
use nohash_hasher::BuildNoHashHasher;
|
||||
|
||||
pub(crate) type FontId = u16;
|
||||
|
||||
#[derive(Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub struct FontHandle(pub(crate) FontId);
|
||||
|
||||
#[cfg(feature = "default-font")]
|
||||
pub const DEFAULT_FONT: FontHandle = FontHandle(0);
|
||||
|
||||
#[cfg(feature = "default-font")]
|
||||
impl Default for FontHandle {
|
||||
fn default() -> Self {
|
||||
DEFAULT_FONT
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FontRepr {
|
||||
pub(crate) font: fontdue::Font,
|
||||
}
|
||||
|
||||
pub struct FontHandleManager {
|
||||
idc: FontId,
|
||||
fonts: HashMap<FontId, FontRepr,BuildNoHashHasher<FontId>>,
|
||||
pub(crate) idc: FontId,
|
||||
fonts: HashMap<FontId, FontRepr, BuildNoHashHasher<FontId>>,
|
||||
}
|
||||
|
||||
impl FontHandleManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
idc: 0,
|
||||
idc: 1,
|
||||
fonts: HashMap::default(),
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ impl FontTextureManager {
|
|||
pub(crate) fn drop_font(&mut self, font: FontHandle, atlas: &mut TextureAtlas) {
|
||||
let dump = self.partition.remove(&font.0).expect("Font handle is invalid");
|
||||
for (_, item) in dump {
|
||||
atlas.deallocate(item.handle);
|
||||
atlas.remove(item.handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ impl FontTextureManager {
|
|||
|
||||
// Rasterize the font and copy the texture data
|
||||
let (metrics, data) = font.rasterize_config(config);
|
||||
let handle = atlas.allocate_with_data(SourceTextureFormat::A8, &data, metrics.width);
|
||||
let handle = atlas.add_with_data(SourceTextureFormat::A8, &data, metrics.width);
|
||||
|
||||
// Create a texture item struct and insert it into the partition
|
||||
let itm = RasterizedGlyphInternal { handle, metrics };
|
|
@ -1,3 +1,4 @@
|
|||
use alloc::vec::Vec;
|
||||
use glam::{ivec2, uvec2, vec2, UVec2, Vec2};
|
||||
use hui_shared::rect::Corners;
|
||||
use rect_packer::DensePacker;
|
||||
|
@ -83,6 +84,14 @@ pub struct TextureHandle {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -120,6 +129,13 @@ impl TextureAllocation {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
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
|
||||
|
@ -147,9 +163,10 @@ pub struct TextureAtlas {
|
|||
}
|
||||
|
||||
impl TextureAtlas {
|
||||
/// Create a new texture atlas with the specified size.
|
||||
pub(crate) fn new(size: UVec2) -> Self {
|
||||
/// Internal function, only directly used in tests
|
||||
pub(crate) fn new_internal(size: UVec2) -> Self {
|
||||
assert_size(size);
|
||||
|
||||
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
|
||||
Self {
|
||||
size,
|
||||
|
@ -165,11 +182,45 @@ impl TextureAtlas {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new texture atlas with the specified size.
|
||||
pub(crate) fn new(size: UVec2) -> Self {
|
||||
let mut this = Self::new_internal(size);
|
||||
|
||||
// HACK?: ensure 0,0 is a white pixel
|
||||
let h = this.add_with_data(SourceTextureFormat::A8, &[255], 1);
|
||||
debug_assert!(
|
||||
h.size == uvec2(1, 1) && h.id == 0,
|
||||
"The texture handle was not allocated correctly"
|
||||
);
|
||||
debug_assert!(
|
||||
this.get_uv(h).is_some_and(|x| x.top_left == Vec2::ZERO),
|
||||
"The texture was't allocated in the top-left corner"
|
||||
);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
/// The version of the atlas, incremented every time the atlas is modified.
|
||||
pub fn version(&self) -> u64 {
|
||||
self.version
|
||||
}
|
||||
|
||||
fn mark_modified(&mut self) {
|
||||
/// 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);
|
||||
}
|
||||
|
@ -189,12 +240,52 @@ impl TextureAtlas {
|
|||
handle
|
||||
}
|
||||
|
||||
// TODO resize test
|
||||
|
||||
/// Resize the atlas to the specified size
|
||||
///
|
||||
/// Downscaling is not supported.
|
||||
pub(crate) fn resize(&mut self, new_size: UVec2) {
|
||||
if new_size == self.size {
|
||||
return
|
||||
}
|
||||
assert_size(new_size);
|
||||
assert!(
|
||||
new_size.x >= self.size.x &&
|
||||
new_size.y >= self.size.y,
|
||||
"downscaling is not supported"
|
||||
);
|
||||
|
||||
let old_size = self.size;
|
||||
self.size = new_size;
|
||||
|
||||
self.packer.resize(new_size.x as i32, new_size.y as i32);
|
||||
|
||||
let new_data_len = (new_size.y as usize * new_size.x as usize) * RGBA_BYTES_PER_PIXEL;
|
||||
self.data.resize(new_data_len, 0);
|
||||
|
||||
// Resize the atlas data in-place if needed
|
||||
if new_size.x != old_size.x {
|
||||
for y in (1..old_size.y).rev() { // First source row can be skipped (its alr at idx 0)
|
||||
for x in (0..old_size.x).rev() {
|
||||
let old_idx = (y as usize * old_size.x as usize + x as usize) * RGBA_BYTES_PER_PIXEL;
|
||||
let new_idx = (y as usize * new_size.x as usize + x as usize) * RGBA_BYTES_PER_PIXEL;
|
||||
self.data.copy_within(old_idx..old_idx + RGBA_BYTES_PER_PIXEL, new_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.increment_version();
|
||||
}
|
||||
|
||||
/// Allocate a texture in the atlas, returning a handle to it.\
|
||||
/// The data present in the texture is undefined, and may include garbage data.
|
||||
///
|
||||
/// The texture may be resized if the texture doesn't fit
|
||||
///
|
||||
/// # Panics
|
||||
/// - If any of the dimensions of the texture are zero or exceed `i32::MAX`.
|
||||
pub fn allocate(&mut self, size: UVec2) -> TextureHandle {
|
||||
pub fn add_empty(&mut self, size: UVec2) -> TextureHandle {
|
||||
assert_size(size);
|
||||
|
||||
// Check if any deallocated allocations can be reused
|
||||
|
@ -216,6 +307,19 @@ impl TextureAtlas {
|
|||
}
|
||||
}
|
||||
|
||||
if !self.packer.can_pack(
|
||||
size.x as i32,
|
||||
size.y as i32,
|
||||
false
|
||||
) {
|
||||
let new_size = self.size * if self.size.x >= self.size.y {
|
||||
uvec2(1, 2)
|
||||
} else {
|
||||
uvec2(2, 1)
|
||||
};
|
||||
self.resize(new_size);
|
||||
}
|
||||
|
||||
// Pack the texture
|
||||
let pack = self.packer.pack(
|
||||
size.x as i32,
|
||||
|
@ -241,7 +345,7 @@ impl TextureAtlas {
|
|||
///
|
||||
/// # Panics
|
||||
/// - If the texture handle is invalid for this atlas.
|
||||
pub fn deallocate(&mut self, handle: TextureHandle) {
|
||||
pub fn remove(&mut self, handle: TextureHandle) {
|
||||
// Remove the allocation from the active allocations
|
||||
let allocation = self.allocations
|
||||
.remove(&handle.id)
|
||||
|
@ -276,11 +380,13 @@ impl TextureAtlas {
|
|||
.get(&handle.id)
|
||||
.expect("invalid texture handle");
|
||||
|
||||
debug_assert_eq!(*size, handle.size, "texture size mismatch");
|
||||
|
||||
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.y + y) * self.size.x +
|
||||
(offset.x + x)
|
||||
) as usize * RGBA_BYTES_PER_PIXEL;
|
||||
|
||||
|
@ -289,6 +395,7 @@ impl TextureAtlas {
|
|||
|
||||
match format {
|
||||
SourceTextureFormat::RGBA8 => {
|
||||
// TODO opt: copy entire row in this case
|
||||
dst.copy_from_slice(src);
|
||||
},
|
||||
SourceTextureFormat::ARGB8 => {
|
||||
|
@ -321,7 +428,7 @@ impl TextureAtlas {
|
|||
}
|
||||
}
|
||||
|
||||
self.mark_modified();
|
||||
self.increment_version();
|
||||
}
|
||||
|
||||
/// Allocate a texture in the atlas, returning a handle to it.\
|
||||
|
@ -333,7 +440,7 @@ impl TextureAtlas {
|
|||
/// # 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 {
|
||||
pub fn add_with_data(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle {
|
||||
assert!(
|
||||
!data.is_empty(),
|
||||
"texture data must not be empty"
|
||||
|
@ -355,7 +462,7 @@ impl TextureAtlas {
|
|||
assert_size(size);
|
||||
|
||||
// Allocate the texture
|
||||
let handle = self.allocate(size);
|
||||
let handle = self.add_empty(size);
|
||||
|
||||
// Write the data to the texture
|
||||
self.update(handle, format, data);
|
||||
|
@ -385,3 +492,164 @@ impl Default for TextureAtlas {
|
|||
Self::new(DEFAULT_ATLAS_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_assert_size_valid() {
|
||||
assert_size(uvec2(1, 1));
|
||||
assert_size(uvec2(i32::MAX as u32, i32::MAX as u32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "size must be greater than 0")]
|
||||
fn test_assert_size_zero() {
|
||||
assert_size(uvec2(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "size must be less than i32::MAX")]
|
||||
fn test_assert_size_too_large() {
|
||||
assert_size(uvec2(i32::MAX as u32 + 1, i32::MAX as u32 + 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_handle_new_broken() {
|
||||
let handle = TextureHandle::new_broken();
|
||||
assert_eq!(handle.id, u32::MAX);
|
||||
assert_eq!(handle.size, uvec2(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_allocation_new() {
|
||||
let handle = TextureHandle::new_broken();
|
||||
let allocation = TextureAllocation::new(handle, uvec2(1, 1), uvec2(2, 2));
|
||||
assert_eq!(allocation.handle, handle);
|
||||
assert_eq!(allocation.offset, uvec2(1, 1));
|
||||
assert_eq!(allocation.size, uvec2(2, 2));
|
||||
assert_eq!(allocation.max_size, uvec2(2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_atlas_new() {
|
||||
const SIZE: u32 = 128;
|
||||
|
||||
let atlas = TextureAtlas::new_internal(uvec2(SIZE, SIZE));
|
||||
assert_eq!(atlas.size, uvec2(SIZE, SIZE));
|
||||
assert_eq!(atlas.data.len(), (SIZE as usize) * (SIZE as usize) * RGBA_BYTES_PER_PIXEL);
|
||||
assert_eq!(atlas.next_id, 0);
|
||||
assert_eq!(atlas.allocations.len(), 0);
|
||||
assert_eq!(atlas.reuse_allocations.len(), 0);
|
||||
assert_eq!(atlas.version, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_atlas_add_empty() {
|
||||
let mut atlas = TextureAtlas::new_internal(uvec2(128, 128));
|
||||
let handle = atlas.add_empty(uvec2(32, 32));
|
||||
assert_eq!(handle.size, uvec2(32, 32));
|
||||
assert_eq!(atlas.get_uv(handle).unwrap().bottom_right, vec2(32. / 128., 32. / 128.));
|
||||
assert_eq!(atlas.allocations.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_atlas_add_with_data() {
|
||||
fn make_data(o: u8)-> Vec<u8> {
|
||||
let mut data = vec![o; 32 * 32 * 4];
|
||||
for y in 0..32 {
|
||||
for x in 0..32 {
|
||||
let idx = (y * 32 + x) * 4;
|
||||
data[idx] = x as u8;
|
||||
data[idx + 1] = y as u8;
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
let mut atlas = TextureAtlas::new_internal(uvec2(128, 128));
|
||||
|
||||
let data = make_data(1);
|
||||
let handle = atlas.add_with_data(SourceTextureFormat::RGBA8, &data, 32);
|
||||
assert_eq!(handle.size, uvec2(32, 32));
|
||||
assert_eq!(atlas.allocations.len(), 1);
|
||||
let uv = atlas.get_uv(handle).unwrap();
|
||||
assert_eq!(uv.top_left, vec2(0.0, 0.0));
|
||||
assert_eq!(uv.top_right, vec2(32.0 / 128.0, 0.0));
|
||||
assert_eq!(uv.bottom_left, vec2(0.0, 32.0 / 128.0));
|
||||
assert_eq!(uv.bottom_right, vec2(32.0 / 128.0, 32.0 / 128.0));
|
||||
|
||||
let data = make_data(2);
|
||||
let handle = atlas.add_with_data(SourceTextureFormat::RGBA8, &data, 32);
|
||||
assert_eq!(handle.size, uvec2(32, 32));
|
||||
assert_eq!(atlas.allocations.len(), 2);
|
||||
let uv = atlas.get_uv(handle).unwrap();
|
||||
assert_eq!(uv.top_left, vec2(32.0 / 128.0, 0.0));
|
||||
assert_eq!(uv.top_right, vec2(64.0 / 128.0, 0.0));
|
||||
assert_eq!(uv.bottom_left, vec2(32.0 / 128.0, 32.0 / 128.0));
|
||||
assert_eq!(uv.bottom_right, vec2(64.0 / 128.0, 32.0 / 128.0));
|
||||
|
||||
// now, check the texture data
|
||||
assert_eq!(atlas.version(), 2);
|
||||
let data = atlas.data_rgba();
|
||||
|
||||
// for y in 0..128 {
|
||||
// for x in 0..128 {
|
||||
// let idx = (y * 128 + x) * 4;
|
||||
// print!("{}", data[idx + 2]);
|
||||
// }
|
||||
// println!();
|
||||
// }
|
||||
|
||||
for y in 0..128 {
|
||||
for x in 0..128 {
|
||||
let idx = (y * 128 + x) * 4;
|
||||
if y >= 32 || x >= 64 {
|
||||
continue
|
||||
}
|
||||
assert_eq!(
|
||||
if x < 32 {
|
||||
[x as u8, y as u8, 1, 1]
|
||||
} else if x < 64 {
|
||||
[x as u8 - 32, y as u8, 2, 2]
|
||||
} else {
|
||||
unreachable!()
|
||||
},
|
||||
data[idx..idx + 4],
|
||||
"pixel at ({x}, {y}) idx: {idx} is incorrect",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_texture_atlas_update() {
|
||||
// let mut atlas = TextureAtlas::new(uvec2(128, 128));
|
||||
// let data = vec![255; 32 * 32 * 4];
|
||||
// let handle = atlas.allocate_with_data(SourceTextureFormat::RGBA8, &data, 32);
|
||||
// let new_data = vec![0; 32 * 32 * 4];
|
||||
// atlas.update(handle, SourceTextureFormat::RGBA8, &new_data);
|
||||
// assert_eq!(atlas.data_rgba()[..32 * 32 * 4], new_data[..]);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn test_texture_atlas_remove() {
|
||||
let mut atlas = TextureAtlas::new_internal(uvec2(128, 128));
|
||||
let handle = atlas.add_empty(uvec2(32, 32));
|
||||
atlas.remove(handle);
|
||||
assert_eq!(atlas.allocations.len(), 0);
|
||||
assert_eq!(atlas.reuse_allocations.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_texture_atlas_get_uv() {
|
||||
let mut atlas = TextureAtlas::new_internal(uvec2(128, 128));
|
||||
let handle = atlas.add_empty(uvec2(32, 32));
|
||||
let uv = atlas.get_uv(handle).unwrap();
|
||||
assert_eq!(uv.top_left, vec2(0.0, 0.0));
|
||||
assert_eq!(uv.top_right, vec2(32.0 / 128.0, 0.0));
|
||||
assert_eq!(uv.bottom_left, vec2(0.0, 32.0 / 128.0));
|
||||
assert_eq!(uv.bottom_right, vec2(32.0 / 128.0, 32.0 / 128.0));
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use std::hash::Hasher;
|
||||
use core::hash::Hasher;
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn hash_vec2(hasher: &mut impl Hasher, vec: glam::Vec2) {
|
|
@ -1,10 +1,12 @@
|
|||
[package]
|
||||
name = "hui-shared"
|
||||
description = "internal crate"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["no-std", "gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = true
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
#![no_std]
|
||||
|
||||
pub mod rect;
|
||||
pub mod color;
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::mem::ManuallyDrop;
|
||||
|
||||
/// Represents 4 corners of a rectangular shape.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub struct Corners<T> {
|
||||
|
@ -114,7 +112,7 @@ impl<T> From<(T, T, T, T)> for Corners<T> {
|
|||
|
||||
impl<T> IntoIterator for Corners<T> {
|
||||
type Item = T;
|
||||
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||
type IntoIter = core::array::IntoIter<Self::Item, 4>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.to_array().into_iter()
|
||||
}
|
||||
|
@ -122,7 +120,7 @@ impl<T> IntoIterator for Corners<T> {
|
|||
|
||||
impl<'a, T> IntoIterator for &'a Corners<T> {
|
||||
type Item = &'a T;
|
||||
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||
type IntoIter = core::array::IntoIter<Self::Item, 4>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.as_array().into_iter()
|
||||
|
@ -131,7 +129,7 @@ impl<'a, T> IntoIterator for &'a Corners<T> {
|
|||
|
||||
impl<'a, T> IntoIterator for &'a mut Corners<T> {
|
||||
type Item = &'a mut T;
|
||||
type IntoIter = std::array::IntoIter<Self::Item, 4>;
|
||||
type IntoIter = core::array::IntoIter<Self::Item, 4>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.as_array_mut().into_iter()
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
[package]
|
||||
name = "hui-wgpu"
|
||||
description = "wgpu render backend for `hui`"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
rust-version = "1.85"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
@ -16,7 +18,7 @@ include = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
hui = { version = "=0.1.0-alpha.6", path = "../hui", default-features = false }
|
||||
hui-painter = { version = "=0.1.0-alpha.7", path = "../hui-painter", default-features = false }
|
||||
wgpu = { version = "24", default-features = false, features = ["wgsl"]}
|
||||
bytemuck = "1.15"
|
||||
log = "0.4"
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
use glam::{vec2, Vec2};
|
||||
use hui::{draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance};
|
||||
use hui_painter::{
|
||||
backend::BackendData,
|
||||
paint::buffer::Vertex,
|
||||
presentation::PresentatationBackendData,
|
||||
texture::TextureAtlasBackendData
|
||||
};
|
||||
|
||||
const DEFAULT_BUFFER_SIZE: u64 = 1024;
|
||||
const DEFAULT_TEXTURE_SIZE: u32 = 512;
|
||||
|
@ -27,8 +32,8 @@ impl WgpuVertex {
|
|||
};
|
||||
}
|
||||
|
||||
impl From<UiVertex> for WgpuVertex {
|
||||
fn from(v: UiVertex) -> Self {
|
||||
impl From<Vertex> for WgpuVertex {
|
||||
fn from(v: Vertex) -> Self {
|
||||
Self {
|
||||
position: v.position.to_array(),
|
||||
uv: v.uv.to_array(),
|
||||
|
@ -38,7 +43,9 @@ impl From<UiVertex> for WgpuVertex {
|
|||
}
|
||||
|
||||
pub struct WgpuUiRenderer {
|
||||
pub modified: bool,
|
||||
// pub modified: bool,
|
||||
pub last_buf_hash: u64,
|
||||
pub last_img_version: u64,
|
||||
pub vertex_buffer: wgpu::Buffer,
|
||||
pub index_buffer: wgpu::Buffer,
|
||||
pub vertex_count: usize,
|
||||
|
@ -184,7 +191,8 @@ impl WgpuUiRenderer {
|
|||
});
|
||||
|
||||
Self {
|
||||
modified: true,
|
||||
last_buf_hash: 0,
|
||||
last_img_version: 0,
|
||||
vertex_buffer,
|
||||
index_buffer,
|
||||
vertex_count: 0,
|
||||
|
@ -198,8 +206,8 @@ impl WgpuUiRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_buffers(&mut self, call: &UiDrawCall, queue: &wgpu::Queue, device: &wgpu::Device, resolution: Vec2) {
|
||||
let data_vtx = call.vertices.iter()
|
||||
fn update_buffers(&mut self, present_data: &PresentatationBackendData, queue: &wgpu::Queue, device: &wgpu::Device, resolution: Vec2) {
|
||||
let data_vtx = present_data.buffer.vertices.iter()
|
||||
.copied()
|
||||
.map(|x| {
|
||||
let mut v = x;
|
||||
|
@ -208,13 +216,13 @@ impl WgpuUiRenderer {
|
|||
})
|
||||
.map(WgpuVertex::from)
|
||||
.collect::<Vec<_>>();
|
||||
let data_idx = &call.indices[..];
|
||||
let data_idx = &present_data.buffer.indices[..];
|
||||
|
||||
let data_vtx_view = bytemuck::cast_slice(&data_vtx);
|
||||
let data_idx_view = bytemuck::cast_slice(data_idx);
|
||||
|
||||
self.vertex_count = call.vertices.len();
|
||||
self.index_count = call.indices.len();
|
||||
self.vertex_count = present_data.buffer.vertices.len();
|
||||
self.index_count = present_data.buffer.indices.len();
|
||||
|
||||
if data_vtx.is_empty() || data_idx.is_empty() {
|
||||
return
|
||||
|
@ -240,11 +248,13 @@ impl WgpuUiRenderer {
|
|||
|
||||
queue.write_buffer(&self.vertex_buffer, 0, data_vtx_view);
|
||||
queue.write_buffer(&self.index_buffer, 0, data_idx_view);
|
||||
|
||||
self.last_buf_hash = present_data.hash;
|
||||
}
|
||||
|
||||
fn update_texture(&mut self, meta: TextureAtlasMeta, queue: &wgpu::Queue, device: &wgpu::Device) {
|
||||
fn update_texture(&mut self, atlas: &TextureAtlasBackendData, queue: &wgpu::Queue, device: &wgpu::Device) {
|
||||
//TODO URGENCY:HIGH resize texture if needed
|
||||
if meta.data.len() as u32 > (self.texture.size().width * self.texture.size().height * 4) {
|
||||
if atlas.data.len() as u32 > (self.texture.size().width * self.texture.size().height * 4) {
|
||||
self.texture.destroy();
|
||||
// unimplemented!("texture resize not implemented");
|
||||
self.texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
|
@ -263,44 +273,41 @@ impl WgpuUiRenderer {
|
|||
});
|
||||
}
|
||||
queue.write_texture(
|
||||
wgpu::ImageCopyTexture {
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &self.texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
meta.data,
|
||||
wgpu::ImageDataLayout {
|
||||
atlas.data,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(meta.size.x * 4),
|
||||
rows_per_image: Some(meta.size.y),
|
||||
bytes_per_row: Some(atlas.size.x * 4),
|
||||
rows_per_image: Some(atlas.size.y),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: meta.size.x,
|
||||
height: meta.size.y,
|
||||
width: atlas.size.x,
|
||||
height: atlas.size.y,
|
||||
depth_or_array_layers: 1,
|
||||
}
|
||||
);
|
||||
|
||||
self.last_img_version = atlas.version;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
instance: &UiInstance,
|
||||
data: &BackendData,
|
||||
queue: &wgpu::Queue,
|
||||
device: &wgpu::Device,
|
||||
resolution: Vec2,
|
||||
) {
|
||||
let (modified, call) = instance.draw_call();
|
||||
if self.modified || modified {
|
||||
self.update_buffers(call, queue, device, resolution);
|
||||
if data.presentation.hash != self.last_buf_hash {
|
||||
self.update_buffers(&data.presentation, queue, device, resolution);
|
||||
}
|
||||
|
||||
let meta = instance.atlas();
|
||||
if self.modified || meta.modified {
|
||||
self.update_texture(meta, queue, device);
|
||||
if data.atlas.version != self.last_img_version {
|
||||
self.update_texture(&data.atlas, queue, device);
|
||||
}
|
||||
|
||||
self.modified = false;
|
||||
}
|
||||
|
||||
pub fn draw(
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
[package]
|
||||
name = "hui-winit"
|
||||
description = "winit platform backend for `hui`"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
rust-version = "1.85"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = true
|
||||
|
@ -14,14 +17,11 @@ include = [
|
|||
]
|
||||
|
||||
[dependencies]
|
||||
hui = { version = "=0.1.0-alpha.6", path = "../hui", default-features = false }
|
||||
winit = { version = "0.30", default-features = false, features = ["x11"]}
|
||||
# winit_30 = { package = "winit", version = "0.30", default-features = false, optional = true }
|
||||
# winit_29 = { package = "winit", version = "0.29", default-features = false, optional = true }
|
||||
hui = { version = "=0.1.0-alpha.7", path = "../hui", default-features = false }
|
||||
winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
glam = "0.30"
|
||||
log = "0.4"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# winit_30 = ["dep:winit_30"]
|
||||
# winit_29 = ["dep:winit_29"]
|
||||
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
// #[cfg(all(feature = "winit_30", feature = "winit_29"))]
|
||||
// compile_error!("Only one of the winit_30 and winit_29 features can be enabled at a time");
|
||||
// #[cfg(not(any(feature = "winit_30", feature = "winit_29")))]
|
||||
// compile_error!("One of the winit_30 and winit_29 features must be enabled");
|
||||
// #[cfg(feature = "winit_30")] extern crate winit_30 as winit;
|
||||
// #[cfg(feature = "winit_29")] extern crate winit_29 as winit;
|
||||
|
||||
use glam::vec2;
|
||||
use hui::{event::UiEvent, UiInstance};
|
||||
use winit::event::{Event, WindowEvent, MouseButton, ElementState};
|
||||
|
|
|
@ -1,60 +1,43 @@
|
|||
[package]
|
||||
name = "hui"
|
||||
description = "Simple UI library for games and other interactive applications"
|
||||
keywords = ["ui", "gui", "portable", "gamedev", "hui"]
|
||||
categories = ["no-std", "gui", "game-development"]
|
||||
repository = "https://github.com/griffi-gh/hui"
|
||||
readme = "../README.md"
|
||||
authors = ["griffi-gh <prasol258@gmail.com>"]
|
||||
rust-version = "1.85"
|
||||
version = "0.1.0-alpha.6"
|
||||
version = "0.1.0-alpha.7"
|
||||
edition = "2024"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = true
|
||||
include = [
|
||||
"assets/**/*",
|
||||
"src/**/*.rs",
|
||||
"Cargo.toml",
|
||||
]
|
||||
|
||||
[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-derive = { version = "0.1.0-alpha.7", path = "../hui-derive", optional = true }
|
||||
hui-shared = { version = "0.1.0-alpha.7", path = "../hui-shared" }
|
||||
hui-painter = { version = "0.1.0-alpha.7", path = "../hui-painter", default-features = false }
|
||||
hashbrown = "0.15"
|
||||
nohash-hasher = "0.2"
|
||||
glam = "0.30"
|
||||
fontdue = "0.9"
|
||||
rect_packer = "0.2"
|
||||
log = "0.4"
|
||||
document-features = "0.2"
|
||||
derive_setters = "0.1"
|
||||
derive_more = { version = "2.0", features = [ "full" ] }
|
||||
tinyset = "0.5"
|
||||
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", "derive", "default-font"]
|
||||
|
||||
## Enable the default font (Proggy tiny, \~35kb)
|
||||
default-font = ["hui-painter/default-font"]
|
||||
|
||||
## Enable derive macros
|
||||
derive = ["dep:hui-derive"]
|
||||
|
||||
## Enable image loading support using the `image` crate
|
||||
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
|
||||
|
|
396
hui/src/draw.rs
396
hui/src/draw.rs
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
//! element API and built-in elements like `Container`, `Button`, `Text`, etc.
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use hui_painter::{paint::command::PaintList, text::FontHandle, PainterInstance};
|
||||
use crate::{
|
||||
draw::{atlas::ImageCtx, UiDrawCommandList},
|
||||
input::InputCtx,
|
||||
layout::{LayoutInfo, Size2d},
|
||||
measure::Response,
|
||||
rect::Rect,
|
||||
signal::SignalStore,
|
||||
state::StateRepo,
|
||||
text::{FontHandle, TextMeasure},
|
||||
UiInstance,
|
||||
};
|
||||
|
||||
|
@ -17,24 +17,22 @@ pub use builtin::*;
|
|||
|
||||
/// 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,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! a container element that can hold and layout multiple children elements
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use derive_setters::Setters;
|
||||
use glam::{Vec2, vec2};
|
||||
use crate::{
|
||||
|
@ -28,7 +29,7 @@ struct ContainerUserData {
|
|||
|
||||
/// A container element that can hold and layout multiple children elements
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
pub struct Container {
|
||||
/// Size of the container
|
||||
#[setters(into)]
|
||||
|
@ -187,7 +188,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 +203,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 +375,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 +444,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 +484,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,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Simple element that displays the specified frame
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use derive_setters::Setters;
|
||||
use crate::{
|
||||
element::{MeasureContext, ProcessContext, UiElement},
|
||||
|
@ -11,7 +12,7 @@ use crate::{
|
|||
|
||||
/// Simple rectangle that displays the specified frame
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
pub struct FrameView {
|
||||
/// Size of the rectangle
|
||||
#[setters(into)]
|
||||
|
@ -63,6 +64,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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -9,11 +9,11 @@ use crate::{
|
|||
};
|
||||
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
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()
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
// not sure if this is a good idea...
|
||||
// but having the ability to add a click event to any element would be nice, and this is a naive way to do it
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use crate::{
|
||||
element::{MeasureContext, ProcessContext, UiElement},
|
||||
signal::{trigger::SignalTrigger, Signal},
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use alloc::boxed::Box;
|
||||
use derive_setters::Setters;
|
||||
use glam::vec2;
|
||||
use crate::{
|
||||
|
@ -10,7 +11,7 @@ use crate::{
|
|||
//TODO: Use Frames here instead of FillColor
|
||||
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
pub struct ProgressBar {
|
||||
/// Current progress, should be in the range 0.0..=1.0
|
||||
pub value: f32,
|
||||
|
@ -75,10 +76,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 =
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! a slider element that allows selecting a value in a range
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use derive_setters::Setters;
|
||||
use glam::{Vec2, vec2};
|
||||
|
||||
|
@ -33,7 +34,7 @@ pub enum SliderFollowMode {
|
|||
|
||||
/// A slider element that allows selecting a value in a range
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
pub struct Slider {
|
||||
/// Value of the slider, should be in range 0..1
|
||||
///
|
||||
|
@ -158,7 +159,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 +173,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 +194,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 +
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
//! simple text element, renders a string of text
|
||||
|
||||
use std::borrow::Cow;
|
||||
use alloc::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),
|
||||
|
@ -21,7 +22,7 @@ use crate::{
|
|||
|
||||
/// Simple text element, renders a string of text
|
||||
#[derive(Setters)]
|
||||
#[setters(prefix = "with_")]
|
||||
#[setters(no_std, prefix = "with_")]
|
||||
pub struct Text {
|
||||
/// Text to render
|
||||
#[setters(into)]
|
||||
|
@ -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.,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,9 +64,18 @@ impl Text {
|
|||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn font(&self, f: FontHandle) -> FontHandle {
|
||||
self.font.unwrap_or(f)
|
||||
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,
|
||||
color: self.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,9 +92,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 +110,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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
//! wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling
|
||||
|
||||
use alloc::boxed::Box;
|
||||
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 +49,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! input, window events and event handling
|
||||
|
||||
use alloc::vec::{Vec, Drain};
|
||||
use glam::Vec2;
|
||||
use crate::input::{MouseButton, ButtonState, KeyboardKey};
|
||||
|
||||
|
@ -31,7 +32,7 @@ impl EventQueue {
|
|||
self.events.push(event);
|
||||
}
|
||||
|
||||
pub(crate) fn drain(&mut self) -> std::vec::Drain<UiEvent> {
|
||||
pub(crate) fn drain(&mut self) -> Drain<UiEvent> {
|
||||
self.events.drain(..)
|
||||
}
|
||||
}
|
||||
|
|
40
hui/src/font.rs
Normal file
40
hui/src/font.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
|
||||
use alloc::vec::Vec;
|
||||
use hui_painter::text::{FontHandle, DEFAULT_FONT};
|
||||
|
||||
pub struct FontStack {
|
||||
fonts: Vec<FontHandle>,
|
||||
}
|
||||
|
||||
impl FontStack {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(not(feature = "default-font"))]
|
||||
fonts: Vec::new(),
|
||||
#[cfg(feature = "default-font")]
|
||||
fonts: vec![DEFAULT_FONT],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, font: FontHandle) {
|
||||
self.fonts.push(font);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) {
|
||||
assert!(self.fonts.pop().is_some())
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<FontHandle> {
|
||||
self.fonts.last().copied()
|
||||
}
|
||||
|
||||
// pub fn current_or_default(&self) -> FontHandle {
|
||||
// self.current().unwrap_or_default()
|
||||
// }
|
||||
}
|
||||
|
||||
impl Default for FontStack {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
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 {
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
//! allows stacking two frames on top of each other
|
||||
|
||||
use crate::{draw::UiDrawCommandList, rect::Rect};
|
||||
use alloc::boxed::Box;
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! keyboard, mouse, and touch input handling
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use core::hash::{Hash, Hasher};
|
||||
use alloc::vec::Vec;
|
||||
use glam::Vec2;
|
||||
use hashbrown::HashMap;
|
||||
use nohash_hasher::BuildNoHashHasher;
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
use hui_painter::{
|
||||
backend::BackendData, paint::command::{PaintCommand, PaintList}, presentation::Presentatation, text::{FontHandle, FontManager}, texture::{SourceTextureFormat, TextureAtlas, 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,18 +17,15 @@ 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 {
|
||||
// TODO Do not own Presentation/Painter
|
||||
painter: PainterInstance,
|
||||
presentation: Presentatation,
|
||||
paint_commands: PaintList,
|
||||
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,
|
||||
/// True if in the middle of a laying out a frame
|
||||
state: bool,
|
||||
font_stack: FontStack,
|
||||
}
|
||||
|
||||
impl UiInstance {
|
||||
|
@ -41,86 +34,50 @@ 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(),
|
||||
presentation: Presentatation::new(),
|
||||
paint_commands: PaintList::default(),
|
||||
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(),
|
||||
state: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse and add a font from a raw byte slice to the UI\
|
||||
/// TrueType (`.ttf`/`.ttc`) and OpenType (`.otf`) fonts are supported\
|
||||
///
|
||||
/// Returns a font handle ([`FontHandle`]).
|
||||
///
|
||||
/// ## Panics:
|
||||
/// If the font data is invalid or corrupt
|
||||
pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
|
||||
self.text_renderer.add_font_from_bytes(font)
|
||||
|
||||
/// Returns a reference to the painter instance
|
||||
pub fn painter(&self) -> &PainterInstance {
|
||||
&self.painter
|
||||
}
|
||||
|
||||
/// Add an image to the texture atlas\
|
||||
/// Accepted texture formats are `Rgba` and `Grayscale`
|
||||
///
|
||||
/// 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)
|
||||
/// Returns a mutable reference to the painter instance
|
||||
pub fn painter_mut(&mut self) -> &mut PainterInstance {
|
||||
&mut self.painter
|
||||
}
|
||||
|
||||
//TODO better error handling
|
||||
|
||||
/// Add an image from a file to the texture atlas\
|
||||
/// (experimental, may be removed in the future)
|
||||
/// Returns a reference to the texture atlas
|
||||
///
|
||||
/// Requires the `image` feature
|
||||
/// Shorthand for:
|
||||
/// ```
|
||||
/// # let mut instance = hui::UiInstance::new();
|
||||
/// instance.painter_mut().textures_mut()
|
||||
/// # ;
|
||||
/// ```
|
||||
pub fn textures_mut(&mut self) -> &mut TextureAtlas {
|
||||
self.painter.textures_mut()
|
||||
}
|
||||
|
||||
/// Returns a reference to the font manager
|
||||
///
|
||||
/// # Panics:
|
||||
/// - 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> {
|
||||
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)?);
|
||||
|
||||
//Guess the image format from the magic bytes
|
||||
//Read like 64 bytes, which should be enough for magic byte detection
|
||||
//well this would fail if the image is somehow smaller than 64 bytes, but who the fvck cares...
|
||||
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");
|
||||
let image_rgba = image.as_rgba8().unwrap();
|
||||
|
||||
//Add the image to the atlas
|
||||
let handle = self.add_image(
|
||||
TextureFormat::Rgba,
|
||||
image_rgba,
|
||||
image.width() as usize
|
||||
);
|
||||
|
||||
Ok(handle)
|
||||
/// Shorthand for:
|
||||
/// ```
|
||||
/// # let mut instance = hui::UiInstance::new();
|
||||
/// instance.painter_mut().fonts_mut()
|
||||
/// # ;
|
||||
/// ```
|
||||
pub fn fonts_mut(&mut self) -> &mut FontManager {
|
||||
self.painter.fonts_mut()
|
||||
}
|
||||
|
||||
/// Push a font to the font stack\
|
||||
|
@ -128,21 +85,21 @@ 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);
|
||||
pub fn font_stack_push(&mut self, font: FontHandle) {
|
||||
self.font_stack.push(font);
|
||||
}
|
||||
|
||||
/// Pop a font from the font stack\
|
||||
///
|
||||
/// ## Panics:
|
||||
/// If the font stack is empty
|
||||
pub fn pop_font(&mut self) {
|
||||
self.text_renderer.pop_font();
|
||||
pub fn font_stack_pop(&mut self) {
|
||||
self.font_stack.pop();
|
||||
}
|
||||
|
||||
/// Get the current default font
|
||||
pub fn current_font(&self) -> FontHandle {
|
||||
self.text_renderer.current_font()
|
||||
/// Get the current default font from the font stack
|
||||
pub fn current_font(&self) -> Option<FontHandle> {
|
||||
self.font_stack.current()
|
||||
}
|
||||
|
||||
/// Add an element or an element tree to the UI
|
||||
|
@ -153,7 +110,6 @@ impl UiInstance {
|
|||
/// ## Panics:
|
||||
/// If called while the UI is not active (call [`UiInstance::begin`] first)
|
||||
pub fn add(&mut self, element: impl UiElement, rect: impl Into<Rect>) {
|
||||
assert!(self.state, "must call UiInstance::begin before adding elements");
|
||||
let rect: Rect = rect.into();
|
||||
let layout = LayoutInfo {
|
||||
position: rect.position,
|
||||
|
@ -161,110 +117,50 @@ 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.paint_commands,
|
||||
input: self.input.ctx(),
|
||||
signal: &mut self.signal,
|
||||
current_font,
|
||||
});
|
||||
}
|
||||
|
||||
/// Prepare the UI for layout and processing\
|
||||
/// You must call this function at the beginning of the frame, before adding any elements\
|
||||
/// Reset the state from the previous frame, and prepare the UI for layout and processing
|
||||
///
|
||||
/// ## Panics:
|
||||
/// If called twice in a row (for example, if you forget to call [`UiInstance::end`])\
|
||||
/// This is an indication of a bug in your code and should be fixed.
|
||||
pub fn begin(&mut self) {
|
||||
//check and update current state
|
||||
assert!(!self.state, "must call UiInstance::end before calling UiInstance::begin again");
|
||||
self.state = true;
|
||||
|
||||
/// - You must call this function at the start of the frame, before adding any elements
|
||||
/// - Make sure to provide all of the events that happened since the last frame before calling this function, to avoid a 1-frame delay in event processing
|
||||
pub fn begin_frame(&mut self) {
|
||||
//first, drain and process the event queue
|
||||
self.input.update_state(&mut self.events);
|
||||
|
||||
//then, reset the (remaining) signals
|
||||
self.signal.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;
|
||||
|
||||
//reset atlas modification flag
|
||||
self.atlas.reset_modified();
|
||||
// Clear the draw commands
|
||||
self.paint_commands.clear();
|
||||
}
|
||||
|
||||
/// End the frame and prepare the UI for rendering\
|
||||
/// You must call this function at the end of the frame, before rendering the UI
|
||||
/// End rendering the current frame and present it
|
||||
///
|
||||
/// ## Panics:
|
||||
/// If called without calling [`UiInstance::begin`] first. (or if called twice)\
|
||||
/// This is an indication of a bug in your code and should be fixed.
|
||||
pub fn end(&mut self) {
|
||||
//check and update current state
|
||||
assert!(self.state, "must call UiInstance::begin before calling UiInstance::end");
|
||||
self.state = false;
|
||||
|
||||
//check if the draw commands have been modified
|
||||
if self.draw_commands.commands == self.prev_draw_commands.commands {
|
||||
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;
|
||||
/// You must call this function sometime at the end of the frame, after adding all elements but before rendering, but before running the render backend
|
||||
pub fn end_frame(&mut self) {
|
||||
self.presentation.draw(&mut self.painter, &self.paint_commands);
|
||||
}
|
||||
|
||||
/// Get the draw call information for the current frame
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
pub fn draw_call(&self) -> (bool, &UiDrawCall) {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Get the texture atlas size and data for the current frame
|
||||
///
|
||||
/// This function should only be used by the render backend.\
|
||||
/// You should not call this directly unless you're implementing a custom render backend
|
||||
///
|
||||
/// 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)\
|
||||
/// Using this function in the middle of a frame will return partially modified atlas data that may be outdated or incomplete\
|
||||
/// This will lead to rendering artifacts, 1-frame delays and flashes and is probably not what you want
|
||||
///
|
||||
/// 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 {
|
||||
if self.state {
|
||||
log::warn!("UiInstance::atlas called while in the middle of a frame, this is probably a mistake");
|
||||
}
|
||||
self.atlas.meta()
|
||||
pub fn backend_data(&self) -> BackendData {
|
||||
self.painter.backend_data(&self.presentation)
|
||||
}
|
||||
|
||||
/// Push a platform event to the UI event queue
|
||||
|
@ -277,11 +173,8 @@ impl UiInstance {
|
|||
///
|
||||
/// This function should only be used by the platform backend.\
|
||||
/// You should not call this directly unless you're implementing a custom platform backend
|
||||
/// or have a very specific usecase
|
||||
/// or have a very specific usecase (not using one)
|
||||
pub fn push_event(&mut self, event: UiEvent) {
|
||||
if self.state {
|
||||
log::warn!("UiInstance::push_event called while in the middle of a frame, this is probably a mistake");
|
||||
}
|
||||
self.events.push(event);
|
||||
}
|
||||
|
||||
|
@ -298,6 +191,11 @@ impl UiInstance {
|
|||
pub fn process_signals<T: Signal + 'static>(&mut self, f: impl FnMut(T)) {
|
||||
self.signal.drain::<T>().for_each(f);
|
||||
}
|
||||
|
||||
/// Get the paint commands needed to render the UI
|
||||
pub fn paint_command(&self) -> &impl PaintCommand {
|
||||
&self.paint_commands
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UiInstance {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
#![no_std]
|
||||
#![doc(html_logo_url = "https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg")]
|
||||
//!
|
||||
//! Simple UI library for games and other interactive applications
|
||||
|
@ -7,11 +8,14 @@
|
|||
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
// #![deny(unsafe_code)]
|
||||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||||
#![allow(unused_parens)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
pub use hui_shared::*;
|
||||
pub use hui_painter as painter;
|
||||
|
||||
mod instance;
|
||||
mod macros;
|
||||
|
@ -19,11 +23,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;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
//! element measurement, hints and responses
|
||||
|
||||
use core::any::Any;
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use glam::Vec2;
|
||||
use crate::rect::Rect;
|
||||
|
||||
|
@ -20,7 +22,7 @@ pub struct Response {
|
|||
pub hints: Hints,
|
||||
|
||||
/// Arbitrary user data, can be used to pass data (for example, cache) between measure and process stages
|
||||
pub user_data: Option<Box<dyn std::any::Any>>,
|
||||
pub user_data: Option<Box<dyn Any>>,
|
||||
|
||||
/// If true, the element should always cause the content to wrap to the next line\
|
||||
/// (the element itself gets wrapped to the next line too)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! signal handling for UI events
|
||||
|
||||
use std::any::{Any, TypeId};
|
||||
use core::any::{Any, TypeId};
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use hashbrown::HashMap;
|
||||
use nohash_hasher::BuildNoHashHasher;
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Contains the implementation of signal triggers, which simplify creation of custom elements
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use super::{Signal, SignalStore};
|
||||
//use crate::element::UiElement;
|
||||
|
||||
/// Signal trigger that does not take any arguments
|
||||
#[allow(clippy::complexity)]
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
//! state managment for stateful elements
|
||||
|
||||
use alloc::{boxed::Box, vec::Vec};
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use nohash_hasher::BuildNoHashHasher;
|
||||
use std::{any::Any, hash::{Hash, Hasher}};
|
||||
use core::{any::Any, hash::{Hash, Hasher}};
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
//TODO impl StateRepo functions and automatic cleanup of inactive ids
|
||||
|
@ -78,7 +79,7 @@ impl StateRepo {
|
|||
pub fn acquire_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &T {
|
||||
let id = hash_local(id, &self.id_stack);
|
||||
self.state.entry(id)
|
||||
.or_insert_with(|| Box::new(state))
|
||||
.or_insert_with(|| Box::new(state) as Box<dyn Any>)
|
||||
.downcast_ref::<T>().unwrap()
|
||||
}
|
||||
|
||||
|
@ -86,7 +87,7 @@ impl StateRepo {
|
|||
pub fn acquire_or_default<T: State + Default>(&mut self, id: impl Hash) -> &T {
|
||||
let id = hash_local(id, &self.id_stack);
|
||||
self.state.entry(id)
|
||||
.or_insert_with(|| Box::<T>::default())
|
||||
.or_insert_with(|| Box::<T>::default() as Box<dyn Any>)
|
||||
.downcast_ref::<T>().unwrap()
|
||||
}
|
||||
|
||||
|
@ -101,7 +102,7 @@ impl StateRepo {
|
|||
pub fn acquire_mut_or_insert<T: State>(&mut self, id: impl Hash, state: T) -> &mut T {
|
||||
let id = hash_local(id, &self.id_stack);
|
||||
self.state.entry(id)
|
||||
.or_insert_with(|| Box::new(state))
|
||||
.or_insert_with(|| Box::new(state) as Box<dyn Any>)
|
||||
.downcast_mut::<T>().unwrap()
|
||||
}
|
||||
|
||||
|
@ -109,7 +110,7 @@ impl StateRepo {
|
|||
pub fn acquire_mut_or_default<T: State + Default>(&mut self, id: impl Hash) -> &mut T {
|
||||
let id = hash_local(id, &self.id_stack);
|
||||
self.state.entry(id)
|
||||
.or_insert_with(|| Box::<T>::default())
|
||||
.or_insert_with(|| Box::<T>::default() as Box<dyn Any>)
|
||||
.downcast_mut::<T>().unwrap()
|
||||
}
|
||||
|
||||
|
@ -118,9 +119,9 @@ impl StateRepo {
|
|||
/// Can be useful for state management of non-hierarchical objects, e.g. popups
|
||||
pub fn global<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
|
||||
self.standby.clear();
|
||||
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
core::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
let ret = f(self);
|
||||
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
core::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
ret
|
||||
}
|
||||
|
||||
|
@ -132,7 +133,7 @@ impl StateRepo {
|
|||
self.standby.clear();
|
||||
self.standby.extend(self.id_stack.iter().copied());
|
||||
let ret = f(self);
|
||||
std::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
core::mem::swap(&mut self.id_stack, &mut self.standby);
|
||||
ret
|
||||
//XXX: this is super efficient, but works only for pushes, if anything is popped, it will be lost
|
||||
// let len = self.id_stack.len();
|
||||
|
|
106
hui/src/text.rs
106
hui/src/text.rs
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
use super::FontHandle;
|
||||
|
||||
pub struct FontStack {
|
||||
fonts: Vec<FontHandle>,
|
||||
}
|
||||
|
||||
impl FontStack {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
#[cfg(not(feature = "builtin_font"))]
|
||||
fonts: Vec::new(),
|
||||
#[cfg(feature = "builtin_font")]
|
||||
fonts: vec![super::BUILTIN_FONT],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, font: FontHandle) {
|
||||
self.fonts.push(font);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) {
|
||||
assert!(self.fonts.pop().is_some())
|
||||
}
|
||||
|
||||
pub fn current(&self) -> Option<FontHandle> {
|
||||
self.fonts.last().copied()
|
||||
}
|
||||
|
||||
pub fn current_or_default(&self) -> FontHandle {
|
||||
self.current().unwrap_or_default()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue