Compare commits

...

33 commits

Author SHA1 Message Date
griffi-gh ec4404b26c
Update devcontainer.json 2024-03-26 00:40:54 +01:00
griffi-gh 9e9cf7d1c2
Update devcontainer.json 2024-03-26 00:36:04 +01:00
griffi-gh bf7244ea33
Create devcontainer.json 2024-03-26 00:25:28 +01:00
griffi-gh 787b20b3db bump master version to 0.1.0-alpha.5 2024-03-25 18:41:44 +01:00
griffi-gh dd5af8b9e2 Prepare for publish 2024-03-25 18:35:54 +01:00
griffi-gh cf106bb893 add note about MSRV 2024-03-25 18:32:31 +01:00
griffi-gh adbc81e704 add doc_auto_cfg 2024-03-25 18:29:44 +01:00
griffi-gh 964cf22372 re-export Signal derive from the signal module 2024-03-25 18:14:36 +01:00
griffi-gh d14f27a428 document some stuff 2024-03-25 18:13:10 +01:00
griffi-gh 405963460d fix texture uv mess 2024-03-25 18:00:15 +01:00
griffi-gh dca0c0d2a4 make last rect shorter in example 7 2024-03-25 17:52:24 +01:00
griffi-gh 4e4c16ce76 use derive macro in examples 2024-03-25 17:51:34 +01:00
griffi-gh 72ff23ac0b add derive macro for Signal 2024-03-25 17:49:32 +01:00
griffi-gh 66ef58a131 use frames for progressbar 2024-03-25 16:16:56 +01:00
griffi-gh efe7326b4d load shader based on api 2024-03-25 15:37:32 +01:00
griffi-gh 55c908a3b9 disable word wrap 2024-03-25 14:26:30 +01:00
griffi-gh 6f7f3bc8b0 update readme 2024-03-25 14:26:20 +01:00
griffi-gh d8b470d805 this should fix the gap thing? 2024-03-25 14:18:14 +01:00
griffi-gh c1be9bf22b fix gap/padding layout (kinda) for remaining size 2024-03-25 14:14:12 +01:00
griffi-gh 3c6e6be754 wip Remaing size 2024-03-25 14:08:04 +01:00
griffi-gh 6da1cc5d88 more granular control over wrapping 2024-03-25 12:57:45 +01:00
griffi-gh 535a56a257 rearrange stuff 2024-03-25 12:40:06 +01:00
griffi-gh bf0b4dcdf2 add todo note :3 2024-03-25 02:46:30 +01:00
griffi-gh 807c9b087d remove fixme comment, fixed by ogl backend change 2024-03-25 02:45:43 +01:00
griffi-gh bd9c3aec81 upd slider 2024-03-25 02:31:57 +01:00
griffi-gh 579b7c5484 clean up example, make slider work 2024-03-25 02:30:51 +01:00
griffi-gh af0bf04ffc use linear minify filter 2024-03-25 02:25:46 +01:00
griffi-gh 36e6fc50ec add fixme note for tomorrow 2024-03-25 02:13:50 +01:00
griffi-gh 7daf4c44fa add note about slider 2024-03-25 02:10:15 +01:00
griffi-gh edb7305d7e aaa 2024-03-25 02:06:23 +01:00
griffi-gh 7b4772ca94 add custom slider as an example 2024-03-25 02:05:26 +01:00
griffi-gh 19ca54b1f3 add 9 patch rendering 2024-03-25 01:59:13 +01:00
griffi-gh c0af88fee8 disallow opts for rounded rects 2024-03-24 23:59:27 +01:00
44 changed files with 807 additions and 180 deletions

View file

@ -0,0 +1,16 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/desktop-lite:1": {}
},
"forwardPorts": [6080, 5901],
"portsAttributes": {
"6080": {
"label": "desktop"
},
"5901": {
"label": "desktop"
}
}
}

5
.markdownlint.jsonc Normal file
View file

@ -0,0 +1,5 @@
{
"MD041": false, //first-line-heading
"MD013": false, //line-length
"MD033": false //inline-html
}

View file

@ -1,5 +1,6 @@
{ {
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.insertSpaces": true "editor.insertSpaces": true,
"editor.wordWrap": "off"
} }

View file

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["hui", "hui-examples", "hui-glium", "hui-winit"] members = ["hui", "hui-derive", "hui-examples", "hui-glium", "hui-winit"]

View file

@ -1,5 +1,5 @@
<p></p><p></p> <p></p><p></p>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg" width="120" align="left"> <img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/hui.svg" width="120" align="left" alt="logo">
<h1>hUI</h1> <h1>hUI</h1>
<div> <div>
<span> <span>
@ -17,22 +17,25 @@
<table align="center"> <table align="center">
<td> <td>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo0.gif" width="300"> <img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo0.gif" width="300" alt="example: mom_downloader">
</td> </td>
<td> <td>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo1.gif" width="300"> <img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/demo1.gif" width="300" alt="example: align_test">
</td> </td>
</table> </table>
<h2>Example</h2> <h2>Example</h2>
<img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/exemplaris.png" height="175" align="right" float="right"> <img src="https://raw.githubusercontent.com/griffi-gh/hui/master/.assets/exemplaris.png"
height="175" align="right" float="right" alt="code result">
<pre lang="rust">Container::default() <pre lang="rust">Container::default()
.with_size(size!(100%, 50%)) .with_size(size!(100%, 50%))
.with_align(Alignment::Center) .with_align(Alignment::Center)
.with_padding(5.) .with_padding(5.)
.with_gap(10.) .with_gap(10.)
.with_corner_radius(10.) .with_background(frame_rect! {
.with_background(color::WHITE) color: (0.5, 0.5, 0.5, 1.),
corner_radius: 10.,
})
.with_children(|ui| { .with_children(|ui| {
Text::default() Text::default()
.with_text("Hello, world") .with_text("Hello, world")
@ -41,8 +44,10 @@
.add_child(ui); .add_child(ui);
Container::default() Container::default()
.with_padding((10., 20.)) .with_padding((10., 20.))
.with_corner_radius((2.5, 30., 2.5, 2.5)) .with_background(frame_rect! {
.with_background(color::DARK_RED) color: color::DARK_RED,
corner_radius: (2.5, 30., 2.5, 2.5),
})
.with_children(|ui| { .with_children(|ui| {
Text::default() Text::default()
.with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") .with_text("Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
@ -51,7 +56,7 @@
}) })
.add_child(ui); .add_child(ui);
}) })
.add_root(&mut hui, resolution);</pre> .add_root(ui, size);</pre>
<h2>Backends</h2> <h2>Backends</h2>
<p> <p>
@ -89,6 +94,20 @@
</td> </td>
<td align="center">(support planned)</td> <td align="center">(support planned)</td>
</tr> </tr>
<tr>
<td align="center">
<code>0.1.0-alpha.4</code>
</th>
<td>
<code>hui-glium = "0.1.0-alpha.4"</code><br>
<code>glium = "0.34"</code>
</td>
<td>
<code>hui-winit = "0.1.0-alpha.4"</code><br>
<code>winit = "0.29"</code>
</td>
<td align="center">N/A</td>
</tr>
<tr> <tr>
<td align="center"> <td align="center">
<code>0.1.0-alpha.3</code> <code>0.1.0-alpha.3</code>
@ -140,3 +159,6 @@
<td align="center">-</td> <td align="center">-</td>
</tr> --> </tr> -->
</table> </table>
<h2>MSRV</h2>
1.75

23
hui-derive/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "hui-derive"
description = "Derive macros for hUI"
repository = "https://github.com/griffi-gh/hui"
readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75"
version = "0.1.0-alpha.5"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true
include = [
"assets/**/*",
"src/**/*.rs",
"Cargo.toml",
]
[lib]
proc-macro = true
[dependencies]
quote = "1.0"
syn = "2.0"

13
hui-derive/src/lib.rs Normal file
View file

@ -0,0 +1,13 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
/// Implements `Signal` trait for the given type
#[proc_macro_derive(Signal)]
pub fn signal(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
quote!(impl ::hui::signal::Signal for #name {}).into()
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 638 B

View file

@ -62,7 +62,7 @@ fn main() {
children: ElementList(vec![ children: ElementList(vec![
Box::new(ProgressBar { Box::new(ProgressBar {
value: z, value: z,
corner_radius: Corners::all(0.25 * ProgressBar::DEFAULT_HEIGHT), // corner_radius: Corners::all(0.25 * ProgressBar::DEFAULT_HEIGHT),
..Default::default() ..Default::default()
}), }),
Box::new(Container { Box::new(Container {

View file

@ -1,6 +1,6 @@
use std::time::Instant; use std::time::Instant;
use hui::{ use hui::{
element::{ color, element::{
container::Container, container::Container,
progress_bar::ProgressBar, progress_bar::ProgressBar,
text::Text, text::Text,
@ -43,7 +43,14 @@ ui_main!{
.add_child(ui); .add_child(ui);
ProgressBar::default() ProgressBar::default()
.with_value(mom_ratio) .with_value(mom_ratio)
.with_corner_radius(0.125 * ProgressBar::DEFAULT_HEIGHT) .with_background(frame_rect! {
color: color::BLACK,
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
})
.with_foreground(frame_rect! {
color: color::BLUE,
corner_radius: 0.125 * ProgressBar::DEFAULT_HEIGHT
})
.add_child(ui); .add_child(ui);
Container::default() Container::default()
.with_direction(Direction::Horizontal) .with_direction(Direction::Horizontal)

View file

@ -1,7 +1,7 @@
use hui::{ use hui::{
color, size, color, size,
draw::TextureFormat,
signal::Signal, signal::Signal,
draw::TextureFormat,
layout::{Alignment, Direction}, layout::{Alignment, Direction},
element::{ element::{
container::Container, container::Container,
@ -13,11 +13,11 @@ use hui::{
}, },
}; };
#[derive(Signal)]
enum CounterSignal { enum CounterSignal {
Increment, Increment,
Decrement, Decrement,
} }
impl Signal for CounterSignal {}
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
#[macro_use] #[macro_use]

View file

@ -8,15 +8,15 @@ use hui::{
text::Text, text::Text,
UiElementExt, UiElementExt,
}, },
layout::{Alignment, Direction},
signal::Signal, signal::Signal,
layout::{Alignment, Direction},
size, size,
}; };
#[derive(Signal)]
enum CounterSignal { enum CounterSignal {
ChangeValue(u32) ChangeValue(u32)
} }
impl Signal for CounterSignal {}
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
#[macro_use] #[macro_use]

View file

@ -1,34 +1,92 @@
use std::time::Instant; use glam::vec2;
use hui::{ use hui::{
color, element::{ color,
element::{
container::Container, container::Container,
fill_rect::FillRect, fill_rect::FillRect,
slider::Slider,
text::Text,
UiElementExt UiElementExt
}, frame_rect, layout::{Alignment, Direction}, size },
frame::nine_patch::{NinePatchAsset, NinePatchFrame},
layout::Alignment,
rect::Rect,
signal::Signal,
size,
}; };
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
#[macro_use] #[macro_use]
mod boilerplate; mod boilerplate;
#[derive(Signal)]
struct SetValue(f32);
ui_main!( ui_main!(
"hUI: 9-Patch demo", "hUI: 9-Patch demo",
init: |_| { init: |ui| {
(
NinePatchAsset {
image: ui.add_image_file_path("./hui-examples/assets/ninepatch_button.png").unwrap(),
size: (190, 49),
scalable_region: Rect {
position: vec2(8. / 190., 8. / 49.),
size: vec2(1. - 16. / 190., 1. - 18. / 49.),
},
},
0.33,
)
}, },
run: |ui, size, _| { run: |ui, size, (asset, value)| {
Container::default() Container::default()
.with_size(size!(100%)) .with_size(size!(100%))
.with_align(Alignment::Center) .with_align(Alignment::Center)
.with_gap(5.)
.with_background(color::WHITE) .with_background(color::WHITE)
.with_children(|ui| { .with_children(|ui| {
FillRect::default() Container::default()
.with_size(size!(300, 100)) .with_size(size!(300, 100))
.with_frame(frame_rect! { .with_background(NinePatchFrame::from_asset(*asset).with_color(color::RED))
color: color::RED .with_padding(10.)
.with_children(|ui| {
Text::new("Hello, world!\nThis is a 9-patch frame used as a background \nfor Container with a Text element.\nIt's scalable and looks great!\nBelow, there are two FillRects with the same \n9-patch frame used as the background.")
.with_text_size(16)
.add_child(ui);
}) })
.add_child(ui); .add_child(ui);
FillRect::default()
.with_size(size!(600, 75))
.with_frame(NinePatchFrame::from_asset(*asset).with_color(color::GREEN))
.add_child(ui);
Text::new("This one's fancy:")
.with_color(color::BLACK)
.with_text_size(32)
.add_child(ui);
FillRect::default()
.with_size(size!(700, 50))
.with_frame(NinePatchFrame::from_asset(*asset).with_color((
(1., 0., 1.),
(0., 1., 1.),
(1., 1., 0.),
(0., 0., 1.),
)))
.add_child(ui);
Text::new("Slider customized with `NinePatchFrame`s:")
.with_color(color::BLACK)
.with_text_size(32)
.add_child(ui);
Slider::new(*value)
.with_size(size!(50%, 30))
.with_track_height(1.)
.with_handle_size((20., 1.))
.with_handle(NinePatchFrame::from_asset(*asset).with_color(color::CYAN))
.with_track(NinePatchFrame::from_asset(*asset))
.with_track_active(NinePatchFrame::from_asset(*asset).with_color(color::SKY_BLUE))
.on_change(SetValue)
.add_child(ui);
}) })
.add_root(ui, size); .add_root(ui, size);
ui.process_signals::<SetValue>(|signal| *value = signal.0);
} }
); );

View file

@ -38,7 +38,7 @@ ui_main!(
.with_size(size!(100%, auto)) .with_size(size!(100%, auto))
.with_direction(Direction::Horizontal) .with_direction(Direction::Horizontal)
.with_align((Alignment::Begin, Alignment::Center)) .with_align((Alignment::Begin, Alignment::Center))
.with_padding(8.) .with_padding(5.)
.with_gap(15.) .with_gap(15.)
.with_background(color::rgb_hex(0x3d3c3e)) .with_background(color::rgb_hex(0x3d3c3e))
.with_wrap(true) //XXX: not authentic but great for demostration .with_wrap(true) //XXX: not authentic but great for demostration
@ -51,6 +51,15 @@ ui_main!(
.with_text_size(15) .with_text_size(15)
.add_child(ui); .add_child(ui);
} }
Container::default()
.with_size(size!(100%=, 100%))
.with_align((Alignment::End, Alignment::Center))
.with_children(|ui| {
Text::new("- ×")
.with_text_size(32)
.add_child(ui);
})
.add_child(ui);
}) })
.add_child(ui); .add_child(ui);
FillRect::default() FillRect::default()
@ -58,9 +67,10 @@ ui_main!(
.with_frame(color::rgb_hex(0x2d2d30)) .with_frame(color::rgb_hex(0x2d2d30))
.add_child(ui); .add_child(ui);
Container::default() Container::default()
.with_size(size!(100%, 100%)) .with_size(size!(100%, 100%=))
.with_direction(Direction::Horizontal) .with_direction(Direction::Horizontal)
.with_children(|ui| { .with_children(|ui| {
// Sidebar:
Container::default() Container::default()
.with_size(size!(54, 100%)) .with_size(size!(54, 100%))
.with_background(color::rgb_hex(0x343334)) .with_background(color::rgb_hex(0x343334))
@ -69,6 +79,8 @@ ui_main!(
.with_size(size!(1, 100%)) .with_size(size!(1, 100%))
.with_frame(color::rgb_hex(0x2d2d30)) .with_frame(color::rgb_hex(0x2d2d30))
.add_child(ui); .add_child(ui);
// Explorer pane:
Container::default() Container::default()
.with_size(size!(200, 100%)) .with_size(size!(200, 100%))
.with_padding((15., 8.)) .with_padding((15., 8.))
@ -78,20 +90,16 @@ ui_main!(
.add_child(ui); .add_child(ui);
}) })
.add_child(ui); .add_child(ui);
// "Code" pane
Container::default() Container::default()
.with_size(size!(100%, 100%)) .with_size(size!(100%=, 100%))
.with_background(color::rgb_hex(0x1f1e1f)) .with_background(color::rgb_hex(0x1f1e1f))
.add_child(ui); .add_child(ui);
}) })
.add_child(ui); .add_child(ui);
})
.add_root(ui, size);
//Bottom bar (yeah, it's basically fake/overlay) //Status bar
Container::default()
.with_size(size!(100%))
.with_align((Alignment::Begin, Alignment::End))
.with_children(|ui| {
Container::default() Container::default()
.with_size(size!(100%, auto)) .with_size(size!(100%, auto))
.with_background(color::rgb_hex(0x0079cc)) .with_background(color::rgb_hex(0x0079cc))
@ -111,8 +119,8 @@ ui_main!(
.with_text_size(15) .with_text_size(15)
.add_child(ui); .add_child(ui);
}) })
.add_child(ui); .add_child(ui);
}) })
.add_root(ui, size); .add_root(ui, size);
} }
); );

View file

@ -4,7 +4,8 @@ description = "glium render backend for `hui`"
repository = "https://github.com/griffi-gh/hui" repository = "https://github.com/griffi-gh/hui"
readme = "../README.md" readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"] authors = ["griffi-gh <prasol258@gmail.com>"]
version = "0.1.0-alpha.4" version = "0.1.0-alpha.5"
rust-version = "1.75"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
publish = true publish = true
@ -15,7 +16,7 @@ include = [
] ]
[dependencies] [dependencies]
hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false } hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
glium = { version = "0.34", default-features = false } glium = { version = "0.34", default-features = false }
glam = "0.27" glam = "0.27"
log = "0.4" log = "0.4"

View file

@ -0,0 +1,13 @@
#version 150 core
precision highp float;
precision highp sampler2D;
out vec4 out_color;
in vec4 vtx_color;
in vec2 vtx_uv;
uniform sampler2D tex;
void main() {
out_color = texture(tex, vtx_uv) * vtx_color;
}

View file

@ -0,0 +1,17 @@
#version 150 core
precision highp float;
uniform vec2 resolution;
in vec2 uv;
in vec4 color;
in vec2 position;
out vec4 vtx_color;
out vec2 vtx_uv;
void main() {
vtx_color = color;
vtx_uv = uv;
vec2 pos2d = (vec2(2., -2.) * (position / resolution)) + vec2(-1, 1);
gl_Position = vec4(pos2d, 0., 1.);
}

View file

@ -1,19 +1,17 @@
use std::rc::Rc; use std::rc::Rc;
use glam::Vec2; use glam::Vec2;
use glium::{ use glium::{
Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer, backend::{Context, Facade}, implement_vertex, index::PrimitiveType, texture::{RawImage2d, Texture2d}, uniform, uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction}, Api, Blend, DrawParameters, IndexBuffer, Program, Surface, VertexBuffer
implement_vertex, uniform,
backend::{Context, Facade},
index::PrimitiveType,
texture::{RawImage2d, Texture2d},
uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler, SamplerBehavior, SamplerWrapFunction},
}; };
use hui::{ use hui::{
draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
}; };
const VERTEX_SHADER: &str = include_str!("../shaders/vertex.vert"); const VERTEX_SHADER_GLES3: &str = include_str!("../shaders/vertex.es.vert");
const FRAGMENT_SHADER: &str = include_str!("../shaders/fragment.frag"); const FRAGMENT_SHADER_GLES3: &str = include_str!("../shaders/fragment.es.frag");
const VERTEX_SHADER_150: &str = include_str!("../shaders/vertex.150.vert");
const FRAGMENT_SHADER_150: &str = include_str!("../shaders/fragment.150.frag");
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[repr(C)] #[repr(C)]
@ -122,7 +120,10 @@ impl GliumUiRenderer {
pub fn new<F: Facade>(facade: &F) -> Self { pub fn new<F: Facade>(facade: &F) -> Self {
log::info!("initializing hui-glium"); log::info!("initializing hui-glium");
Self { Self {
program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(), program: match facade.get_context().get_supported_glsl_version().0 {
Api::Gl => Program::from_source(facade, VERTEX_SHADER_150, FRAGMENT_SHADER_150, None).unwrap(),
Api::GlEs => Program::from_source(facade, VERTEX_SHADER_GLES3, FRAGMENT_SHADER_GLES3, None).unwrap(),
},
context: Rc::clone(facade.get_context()), context: Rc::clone(facade.get_context()),
ui_texture: None, ui_texture: None,
buffer_pair: None, buffer_pair: None,
@ -183,7 +184,7 @@ impl GliumUiRenderer {
tex: Sampler(self.ui_texture.as_ref().unwrap(), SamplerBehavior { tex: Sampler(self.ui_texture.as_ref().unwrap(), SamplerBehavior {
max_anisotropy: 1, max_anisotropy: 1,
magnify_filter: MagnifySamplerFilter::Nearest, magnify_filter: MagnifySamplerFilter::Nearest,
minify_filter: MinifySamplerFilter::NearestMipmapNearest, minify_filter: MinifySamplerFilter::Linear,
wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp), wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp),
..Default::default() ..Default::default()
}), }),

View file

@ -4,7 +4,7 @@ description = "winit platform backend for `hui`"
repository = "https://github.com/griffi-gh/hui" repository = "https://github.com/griffi-gh/hui"
readme = "../README.md" readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"] authors = ["griffi-gh <prasol258@gmail.com>"]
version = "0.1.0-alpha.4" version = "0.1.0-alpha.5"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
publish = true publish = true
@ -14,7 +14,7 @@ include = [
] ]
[dependencies] [dependencies]
hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false } hui = { version = "=0.1.0-alpha.5", path = "../hui", default-features = false }
winit = { version = "0.29", default-features = false } winit = { version = "0.29", default-features = false }
glam = "0.27" glam = "0.27"
log = "0.4" log = "0.4"

View file

@ -5,7 +5,7 @@ repository = "https://github.com/griffi-gh/hui"
readme = "../README.md" readme = "../README.md"
authors = ["griffi-gh <prasol258@gmail.com>"] authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75" rust-version = "1.75"
version = "0.1.0-alpha.4" version = "0.1.0-alpha.5"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
publish = true publish = true
@ -16,6 +16,7 @@ include = [
] ]
[dependencies] [dependencies]
hui-derive = { version = "0.1.0-alpha.5", path = "../hui-derive", optional = true }
hashbrown = "0.14" hashbrown = "0.14"
nohash-hasher = "0.2" nohash-hasher = "0.2"
glam = "0.27" glam = "0.27"
@ -29,15 +30,14 @@ tinyset = "0.4"
image = { version = "0.25", default-features = false, optional = true } image = { version = "0.25", default-features = false, optional = true }
[features] [features]
default = ["el_all", "image", "builtin_font", "pixel_perfect_text"] default = ["el_all", "image", "builtin_font", "pixel_perfect_text", "derive"]
#! Image loading support: ## Enable derive macros
derive = ["dep:hui-derive"]
## Enable image loading support using the `image` crate ## Enable image loading support using the `image` crate
image = ["dep:image"] image = ["dep:image"]
#! #### Built-in font:
## Enable the built-in font (ProggyTiny, adds *35kb* to the executable) ## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
builtin_font = [] builtin_font = []

View file

@ -1,7 +1,5 @@
//! draw commands, tesselation and UI rendering. //! draw commands, tesselation and UI rendering.
//TODO: 9-slice draw command
use crate::{ use crate::{
rect::Corners, rect::Corners,
text::{FontHandle, TextRenderer} text::{FontHandle, TextRenderer}
@ -35,6 +33,8 @@ pub enum UiDrawCommand {
color: Corners<Vec4>, color: Corners<Vec4>,
///Texture ///Texture
texture: Option<ImageHandle>, texture: Option<ImageHandle>,
///Sub-UV coordinates for the texture
texture_uv: Option<Corners<Vec2>>,
///Rounded corners ///Rounded corners
rounded_corners: Option<RoundedCorners>, rounded_corners: Option<RoundedCorners>,
}, },
@ -161,10 +161,36 @@ impl UiDrawCall {
v.position += center; v.position += center;
} }
}, },
UiDrawCommand::Rectangle { position, size, color, texture, rounded_corners } => { UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
let uvs = texture let uvs = texture
.map(|x| atlas.get_uv(x)) .map(|x| atlas.get_uv(x))
.flatten() .flatten()
.map(|guv| {
if let Some(texture_uv) = texture_uv {
//XXX: this 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)); .unwrap_or(Corners::all(Vec2::ZERO));
let vidx = draw_call.vertices.len() as u32; let vidx = draw_call.vertices.len() as u32;

View file

@ -4,7 +4,7 @@ use std::any::Any;
use crate::{ use crate::{
draw::{atlas::ImageCtx, UiDrawCommandList}, draw::{atlas::ImageCtx, UiDrawCommandList},
input::InputCtx, input::InputCtx,
layout::LayoutInfo, layout::{LayoutInfo, Size2d},
measure::Response, measure::Response,
signal::SignalStore, signal::SignalStore,
state::StateRepo, state::StateRepo,
@ -45,6 +45,11 @@ pub trait UiElement {
/// For example, "button" or "progress_bar" /// For example, "button" or "progress_bar"
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
/// Get the requested UiElement size
///
/// You should implement this function whenever possible, otherwise some features may not work at all, such as the `Remaining` size
fn size(&self) -> Option<Size2d> { None }
/// Get the unique id used for internal state management\ /// Get the unique id used for internal state management\
/// This value must be unique for each instance of the element /// This value must be unique for each instance of the element
/// ///

View file

@ -4,10 +4,10 @@ use derive_setters::Setters;
use glam::{Vec2, vec2}; use glam::{Vec2, vec2};
use crate::{ use crate::{
element::{ElementList, MeasureContext, ProcessContext, UiElement}, element::{ElementList, MeasureContext, ProcessContext, UiElement},
layout::{Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d},
frame::{Frame, FrameRect}, frame::{Frame, FrameRect},
layout::{compute_size, Alignment, Alignment2d, Direction, LayoutInfo, Size, Size2d, WrapBehavior},
measure::{Hints, Response}, measure::{Hints, Response},
rect::{Sides, FillColor}, rect::Sides,
}; };
//XXX: add Order/Direction::Forward/Reverse or sth? //XXX: add Order/Direction::Forward/Reverse or sth?
@ -19,6 +19,7 @@ use crate::{
struct CudLine { struct CudLine {
start_idx: usize, start_idx: usize,
content_size: Vec2, content_size: Vec2,
remaining_space: f32,
} }
struct ContainerUserData { struct ContainerUserData {
@ -52,13 +53,9 @@ pub struct Container {
#[setters(skip)] #[setters(skip)]
pub background_frame: Box<dyn Frame>, pub background_frame: Box<dyn Frame>,
/// Set this to `true` to allow the elements wrap automatically /// Controls if wrapping is enabled
/// #[setters(into)]
/// Disabling/enabling this does not affect explicit wrapping\ pub wrap: WrapBehavior,
/// (for example, `Br`, or any other element with `should_wrap` set to `true`)
///
/// This is an experimental feature and may not work as expected
pub wrap: bool,
/// List of children elements /// List of children elements
#[setters(skip)] #[setters(skip)]
@ -86,7 +83,7 @@ impl Default for Container {
padding: Sides::all(0.), padding: Sides::all(0.),
align: Alignment2d::default(), align: Alignment2d::default(),
background_frame: Box::<FrameRect>::default(), background_frame: Box::<FrameRect>::default(),
wrap: false, wrap: WrapBehavior::Allow,
children: ElementList(Vec::new()), children: ElementList(Vec::new()),
} }
} }
@ -94,19 +91,24 @@ impl Default for Container {
impl Container { impl Container {
pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 { pub fn measure_max_inner_size(&self, layout: &LayoutInfo) -> Vec2 {
let outer_size_x = match self.size.width { // let outer_size_x = match self.size.width {
Size::Auto => layout.max_size.x, // Size::Auto => layout.max_size.x,
Size::Relative(p) => layout.max_size.x * p, // Size::Relative(p) => layout.max_size.x * p,
Size::Absolute(p) => p, // Size::Absolute(p) => p,
}; // Size::Remaining(p) => match layout.direction {
let outer_size_y = match self.size.height { // Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * p,
Size::Auto => layout.max_size.y, // Direction::Vertical => layout.max_size.x,
Size::Relative(p) => layout.max_size.y * p, // }
Size::Absolute(p) => p, // };
}; // let outer_size_y = match self.size.height {
// Size::Auto => layout.max_size.y,
// Size::Relative(p) => layout.max_size.y * p,
// Size::Absolute(p) => p,
// };
let outer_size = compute_size(layout, self.size, layout.max_size);
vec2( vec2(
outer_size_x - (self.padding.left + self.padding.right), outer_size.x - (self.padding.left + self.padding.right),
outer_size_y - (self.padding.top + self.padding.bottom), outer_size.y - (self.padding.top + self.padding.bottom),
) )
} }
} }
@ -116,6 +118,10 @@ impl UiElement for Container {
"container" "container"
} }
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
// XXX: If both axes are NOT set to auto, we should be able quickly return the size // XXX: If both axes are NOT set to auto, we should be able quickly return the size
// ... but we can't, because we need to measure the children to get the inner_content_size and user_data values // ... but we can't, because we need to measure the children to get the inner_content_size and user_data values
@ -130,11 +136,13 @@ impl UiElement for Container {
Size::Auto => ctx.layout.max_size.x, Size::Auto => ctx.layout.max_size.x,
Size::Relative(p) => ctx.layout.max_size.x * p, Size::Relative(p) => ctx.layout.max_size.x * p,
Size::Absolute(p) => p, Size::Absolute(p) => p,
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.x) * p,
}, },
Direction::Vertical => match self.size.height { Direction::Vertical => match self.size.height {
Size::Auto => ctx.layout.max_size.y, Size::Auto => ctx.layout.max_size.y,
Size::Relative(p) => ctx.layout.max_size.y * p, Size::Relative(p) => ctx.layout.max_size.y * p,
Size::Absolute(p) => p, Size::Absolute(p) => p,
Size::Remaining(p) => ctx.layout.remaining_space.unwrap_or(ctx.layout.max_size.y) * p,
} }
}; };
@ -160,10 +168,25 @@ impl UiElement for Container {
CudLine { CudLine {
start_idx: 0, start_idx: 0,
content_size: Vec2::ZERO, content_size: Vec2::ZERO,
remaining_space: 0.,
} }
]; ];
//set to true if in the current line there is an element with Remaining size (line will have to be wrapped)
// let mut has_remaining = false;
for (idx, element) in self.children.0.iter().enumerate() { for (idx, element) in self.children.0.iter().enumerate() {
if let Some(esize) = element.size() {
let pri_size = match self.direction {
Direction::Horizontal => esize.width,
Direction::Vertical => esize.height,
};
if matches!(pri_size, Size::Remaining(_)) {
//XXX: kinda a hack?
continue;
}
}
let measure = element.measure(MeasureContext{ let measure = element.measure(MeasureContext{
state: ctx.state, state: ctx.state,
layout: &LayoutInfo { layout: &LayoutInfo {
@ -176,6 +199,7 @@ impl UiElement for Container {
//TODO: subtract size already taken by previous children //TODO: subtract size already taken by previous children
max_size: self.measure_max_inner_size(ctx.layout), max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction, direction: self.direction,
remaining_space: None,
}, },
text_measure: ctx.text_measure, text_measure: ctx.text_measure,
current_font: ctx.current_font, current_font: ctx.current_font,
@ -189,19 +213,30 @@ impl UiElement for Container {
}; };
//Wrap the element if it exceeds container's size and is not the first element in the line //Wrap the element if it exceeds container's size and is not the first element in the line
if ((self.wrap && (end_pos_pri > max_line_pri)) || measure.should_wrap) && (line_element_count > 0) { let should_wrap_overflow = self.wrap.is_enabled() && (end_pos_pri > max_line_pri);
if self.wrap.is_allowed() && line_element_count > 0 && (measure.should_wrap || should_wrap_overflow) {
// >>>>>>> WRAP THAT B*TCH! // >>>>>>> WRAP THAT B*TCH!
//Negate the leftover gap from the previous element //Negate the leftover gap from the previous element
line_size -= leftover_gap; line_size -= leftover_gap;
//update the previous line metadata //update the previous line metadata
lines.last_mut().unwrap().content_size = line_size; {
let last_line = lines.last_mut().unwrap();
last_line.content_size = line_size;
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
last_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
};
}
//push the line metadata //push the line metadata
lines.push(CudLine { lines.push(CudLine {
start_idx: idx, start_idx: idx,
content_size: Vec2::ZERO, content_size: Vec2::ZERO,
remaining_space: 0.,
}); });
//Update the total size accordingly //Update the total size accordingly
@ -252,7 +287,16 @@ impl UiElement for Container {
line_size -= leftover_gap; line_size -= leftover_gap;
//Update the content size of the last line //Update the content size of the last line
lines.last_mut().unwrap().content_size = line_size; {
//HACK: why? - self.gap, may be different for the last element or if it's the only element in the line
let cur_line = lines.last_mut().unwrap();
cur_line.content_size = line_size;
let will_produce_gap = if line_element_count > 1 { self.gap } else { 0. };
cur_line.remaining_space = max_line_pri - will_produce_gap - match self.direction {
Direction::Horizontal => line_size.x + self.padding.left + self.padding.right,
Direction::Vertical => line_size.y + self.padding.top + self.padding.bottom,
};
}
//Update the total size according to the size of the last line //Update the total size according to the size of the last line
match self.direction { match self.direction {
@ -278,17 +322,27 @@ impl UiElement for Container {
self.padding.top + self.padding.bottom, self.padding.top + self.padding.bottom,
); );
let computed_size = compute_size(ctx.layout, self.size, total_size);
match self.size.width { match self.size.width {
Size::Auto => (), Size::Auto => (),
Size::Relative(percentage) => total_size.x = ctx.layout.max_size.x * percentage, _ => total_size.x = computed_size.x,
Size::Absolute(pixels) => total_size.x = pixels,
} }
match self.size.height { match self.size.height {
Size::Auto => (), Size::Auto => (),
Size::Relative(percentage) => total_size.y = ctx.layout.max_size.y * percentage, _ => total_size.y = computed_size.y,
Size::Absolute(pixels) => total_size.y = pixels,
} }
// match self.size.width {
// Size::Auto => (),
// Size::Relative(percentage) => total_size.x = ctx.layout.max_size.x * percentage,
// Size::Absolute(pixels) => total_size.x = pixels,
// }
// match self.size.height {
// Size::Auto => (),
// Size::Relative(percentage) => total_size.y = ctx.layout.max_size.y * percentage,
// Size::Absolute(pixels) => total_size.y = pixels,
// }
Response { Response {
size: total_size, size: total_size,
hints: Hints { hints: Hints {
@ -385,6 +439,7 @@ impl UiElement for Container {
position: local_position, position: local_position,
max_size: self.measure_max_inner_size(ctx.layout), max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction, direction: self.direction,
remaining_space: Some(cur_line.remaining_space),
}; };
//measure //measure

View file

@ -6,7 +6,7 @@ use crate::{
draw::{RoundedCorners, UiDrawCommand}, draw::{RoundedCorners, UiDrawCommand},
element::{MeasureContext, ProcessContext, UiElement}, element::{MeasureContext, ProcessContext, UiElement},
frame::{Frame, FrameRect}, frame::{Frame, FrameRect},
layout::{Size, Size2d}, layout::{compute_size, Size, Size2d},
measure::Response, measure::Response,
size size
}; };
@ -45,20 +45,13 @@ impl UiElement for FillRect {
"fill_rect" "fill_rect"
} }
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
Response { Response {
size: vec2( size: compute_size(ctx.layout, self.size, ctx.layout.max_size),
match self.size.width {
Size::Auto => ctx.layout.max_size.x,
Size::Relative(percentage) => ctx.layout.max_size.x * percentage,
Size::Absolute(pixels) => pixels,
},
match self.size.height {
Size::Auto => ctx.layout.max_size.y,
Size::Relative(percentage) => ctx.layout.max_size.y * percentage,
Size::Absolute(pixels) => pixels,
},
),
..Default::default() ..Default::default()
} }
} }

View file

@ -54,6 +54,10 @@ impl UiElement for Image {
"image" "image"
} }
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
let dim = ctx.images.get_size(self.image).expect("invalid image handle"); let dim = ctx.images.get_size(self.image).expect("invalid image handle");
let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2()); let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2());
@ -79,6 +83,7 @@ impl UiElement for Image {
size: ctx.measure.size, size: ctx.measure.size,
color: self.color.corners(), color: self.color.corners(),
texture: Some(self.image), texture: Some(self.image),
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({ rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
RoundedCorners::from_radius(self.corner_radius) RoundedCorners::from_radius(self.corner_radius)
}), }),

View file

@ -48,6 +48,10 @@ impl UiElement for Interactable {
"interactable" "interactable"
} }
fn size(&self) -> Option<crate::layout::Size2d> {
self.element.size()
}
fn measure(&self, ctx: MeasureContext) -> crate::measure::Response { fn measure(&self, ctx: MeasureContext) -> crate::measure::Response {
self.element.measure(ctx) self.element.measure(ctx)
} }

View file

@ -1,14 +1,15 @@
use derive_setters::Setters; use derive_setters::Setters;
use glam::{vec2, vec4}; use glam::vec2;
use crate::{ use crate::{
draw::{RoundedCorners, UiDrawCommand},
element::{MeasureContext, ProcessContext, UiElement}, element::{MeasureContext, ProcessContext, UiElement},
frame::{Frame, FrameRect},
layout::{compute_size, Size, Size2d}, layout::{compute_size, Size, Size2d},
measure::Response, measure::Response,
rect::{Corners, FillColor}
}; };
#[derive(Debug, Clone, Copy, Setters)] //TODO: Use Frames here instead of FillColor
#[derive(Setters)]
#[setters(prefix = "with_")] #[setters(prefix = "with_")]
pub struct ProgressBar { pub struct ProgressBar {
/// Current progress, should be in the range 0.0..=1.0 /// Current progress, should be in the range 0.0..=1.0
@ -19,20 +20,26 @@ pub struct ProgressBar {
pub size: Size2d, pub size: Size2d,
/// Foreground (bar) color /// Foreground (bar) color
#[setters(into)] #[setters(skip)]
pub foreground: FillColor, pub foreground: Box<dyn Frame>,
/// Background color /// Background color
#[setters(into)] #[setters(skip)]
pub background: FillColor, pub background: Box<dyn Frame>,
/// Corner radius of the progress bar
#[setters(into)]
pub corner_radius: Corners<f32>,
} }
impl ProgressBar { impl ProgressBar {
pub const DEFAULT_HEIGHT: f32 = 20.0; pub const DEFAULT_HEIGHT: f32 = 20.0;
pub fn with_background(mut self, frame: impl Frame + 'static) -> Self {
self.background = Box::new(frame);
self
}
pub fn with_foreground(mut self, frame: impl Frame + 'static) -> Self {
self.foreground = Box::new(frame);
self
}
} }
impl Default for ProgressBar { impl Default for ProgressBar {
@ -40,9 +47,8 @@ impl Default for ProgressBar {
Self { Self {
value: 0., value: 0.,
size: Size::Auto.into(), size: Size::Auto.into(),
foreground: vec4(0.0, 0.0, 1.0, 1.0).into(), foreground: Box::new(FrameRect::color((0.0, 0.0, 1.0, 1.0))),
background: vec4(0.0, 0.0, 0.0, 1.0).into(), background: Box::new(FrameRect::color((0.0, 0.0, 0.0, 1.0))),
corner_radius: Corners::all(0.),
} }
} }
} }
@ -55,7 +61,7 @@ impl UiElement for ProgressBar {
fn measure(&self, ctx: MeasureContext) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
Response { Response {
size: compute_size(ctx.layout, self.size, vec2( size: compute_size(ctx.layout, self.size, vec2(
ctx.layout.max_size.x.max(300.), ctx.layout.max_size.x.max(300.), //XXX: remove .max(300)?
Self::DEFAULT_HEIGHT, Self::DEFAULT_HEIGHT,
)), )),
hints: Default::default(), hints: Default::default(),
@ -66,38 +72,49 @@ impl UiElement for ProgressBar {
fn process(&self, ctx: ProcessContext) { fn process(&self, ctx: ProcessContext) {
let value = self.value.clamp(0., 1.); let value = self.value.clamp(0., 1.);
let rounded_corners =
(self.corner_radius.max_f32() > 0.).then_some({ //FIXME: these optimizations may not be valid
//HACK: fix clipping issues; //todo: get rid of this if value < 1. || !self.foreground.covers_opaque() {
let mut radii = self.corner_radius; self.background.draw(ctx.draw, ctx.layout.position, ctx.measure.size);
let width = ctx.measure.size.x * value;
if width <= radii.max_f32() * 2. {
radii.bottom_right = 0.;
radii.top_right = 0.;
}
if width <= radii.max_f32() {
radii.bottom_left = 0.;
radii.top_left = 0.;
}
RoundedCorners::from_radius(radii)
});
if value < 1. {
ctx.draw.add(UiDrawCommand::Rectangle {
position: ctx.layout.position,
size: ctx.measure.size,
color: self.background.corners(),
texture: None,
rounded_corners
});
} }
if value > 0. { if value > 0. {
ctx.draw.add(UiDrawCommand::Rectangle { self.foreground.draw(ctx.draw, ctx.layout.position, ctx.measure.size * vec2(value, 1.));
position: ctx.layout.position,
size: ctx.measure.size * vec2(value, 1.0),
color: self.foreground.corners(),
texture: None,
rounded_corners,
});
} }
// let rounded_corners =
// (self.corner_radius.max_f32() > 0.).then_some({
// //HACK: fix clipping issues; //todo: get rid of this
// let mut radii = self.corner_radius;
// let width = ctx.measure.size.x * value;
// if width <= radii.max_f32() * 2. {
// radii.bottom_right = 0.;
// radii.top_right = 0.;
// }
// if width <= radii.max_f32() {
// radii.bottom_left = 0.;
// radii.top_left = 0.;
// }
// RoundedCorners::from_radius(radii)
// });
// if value < 1. {
// ctx.draw.add(UiDrawCommand::Rectangle {
// position: ctx.layout.position,
// size: ctx.measure.size,
// color: self.background.corners(),
// texture: None,
// texture_uv: None,
// rounded_corners
// });
// }
// if value > 0. {
// ctx.draw.add(UiDrawCommand::Rectangle {
// position: ctx.layout.position,
// size: ctx.measure.size * vec2(value, 1.0),
// color: self.foreground.corners(),
// texture: None,
// texture_uv: None,
// rounded_corners,
// });
// }
} }
} }

View file

@ -6,7 +6,7 @@ use glam::{vec2, Vec4};
use crate::{ use crate::{
draw::UiDrawCommand, draw::UiDrawCommand,
element::{MeasureContext, ProcessContext, UiElement}, element::{MeasureContext, ProcessContext, UiElement},
layout::{Size, Size2d}, layout::{compute_size, Size, Size2d},
measure::Response, measure::Response,
text::FontHandle, text::FontHandle,
}; };
@ -74,6 +74,10 @@ impl UiElement for Text {
"text" "text"
} }
fn size(&self) -> Option<Size2d> {
Some(self.size)
}
fn measure(&self, ctx: MeasureContext) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
let mut size = (0., 0.); let mut size = (0., 0.);
if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) { if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) {
@ -83,18 +87,7 @@ impl UiElement for Text {
size.1 = res.height; size.1 = res.height;
} }
Response { Response {
size: vec2( size: compute_size(ctx.layout, self.size, size.into()),
match self.size.width {
Size::Auto => size.0,
Size::Relative(percentage) => ctx.layout.max_size.x * percentage,
Size::Absolute(pixels) => pixels,
},
match self.size.height {
Size::Auto => size.1,
Size::Relative(percentage) => ctx.layout.max_size.y * percentage,
Size::Absolute(pixels) => pixels,
},
),
..Default::default() ..Default::default()
} }
} }

View file

@ -1,13 +1,17 @@
//! modular procedural background system
use glam::Vec2; use glam::Vec2;
use crate::draw::UiDrawCommandList; use crate::draw::UiDrawCommandList;
pub mod point; pub mod point;
mod rect; mod rect;
pub mod stack; pub mod stack;
pub mod nine_patch;
mod impls; mod impls;
pub use rect::FrameRect; pub use rect::FrameRect;
/// Trait for a drawable frame
pub trait Frame { pub trait Frame {
/// Draw the frame at the given position and size /// Draw the frame at the given position and size
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2); fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2);

View file

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

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

@ -0,0 +1,234 @@
//! nine-patch frame implementation
//!
//! 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 crate::{color, draw::{ImageHandle, UiDrawCommand}, rect::{Corners, FillColor, Rect}};
use super::Frame;
/// Represents a 9-patch image asset
#[derive(Clone, Copy, Debug)]
pub struct NinePatchAsset {
pub image: ImageHandle,
//TODO: remove this:
pub size: (u32, u32),
pub scalable_region: Rect,
}
//TODO allow scaling/moving corners
/// A 9-patch frame
///
/// Can optionally be tinted with a color (works well with grayscale assets)
#[derive(Clone, Copy, Debug)]
pub struct NinePatchFrame {
pub asset: NinePatchAsset,
pub color: FillColor,
}
impl NinePatchFrame {
pub fn from_asset(asset: NinePatchAsset) -> Self {
Self { asset, ..Default::default() }
}
pub fn with_color(mut self, color: impl Into<FillColor>) -> Self {
self.color = color.into();
self
}
}
impl Default for NinePatchFrame {
fn default() -> Self {
Self {
//This is not supposed to be left out as the default, so just set it to whatever :p
asset: NinePatchAsset { image: ImageHandle::default(), size: (0, 0), scalable_region: Rect::default() },
color: color::WHITE.into(),
}
}
}
impl Frame for NinePatchFrame {
fn draw(&self, draw: &mut crate::draw::UiDrawCommandList, position: glam::Vec2, parent_size: glam::Vec2) {
// without this, shїt gets messed up when the position is not a whole number
//XXX: should we round the size as well?
let position = position.round();
let img_sz = UVec2::from(self.asset.size).as_vec2();
//Color stuff
let interpolate_color_rect = |uvs: Corners<Vec2>| {
Corners {
top_left: self.color.interpolate(uvs.top_left),
top_right: self.color.interpolate(uvs.top_right),
bottom_left: self.color.interpolate(uvs.bottom_left),
bottom_right: self.color.interpolate(uvs.bottom_right),
}
};
// Inset coords, in UV space
let region_uv = self.asset.scalable_region.corners();
// Inset coords, in image (px) space
let corners_image_px = Corners {
top_left: img_sz * region_uv.top_left,
top_right: img_sz * region_uv.top_right,
bottom_left: img_sz * region_uv.bottom_left,
bottom_right: img_sz * region_uv.bottom_right,
};
let size_h = (
corners_image_px.top_left.x,
parent_size.x - corners_image_px.top_left.x - (img_sz.x - corners_image_px.top_right.x),
img_sz.x - corners_image_px.top_right.x,
);
let size_v = (
corners_image_px.top_left.y,
parent_size.y - corners_image_px.top_left.y - (img_sz.y - corners_image_px.bottom_left.y),
img_sz.y - corners_image_px.bottom_left.y,
);
//Top-left patch
let top_left_patch_uv = Corners {
top_left: vec2(0., 0.),
top_right: vec2(region_uv.top_left.x, 0.),
bottom_left: vec2(0., region_uv.top_left.y),
bottom_right: region_uv.top_left,
};
draw.add(UiDrawCommand::Rectangle {
position,
size: vec2(size_h.0, size_v.0),
color: interpolate_color_rect(top_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_left_patch_uv),
rounded_corners: None
});
//Top patch
let top_patch_uv = Corners {
top_left: vec2(region_uv.top_left.x, 0.),
top_right: vec2(region_uv.top_right.x, 0.),
bottom_left: region_uv.top_left,
bottom_right: region_uv.top_right,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, 0.),
size: vec2(size_h.1, size_v.0),
color: interpolate_color_rect(top_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_patch_uv),
rounded_corners: None
});
//Top-right patch
let top_right_patch_uv = Corners {
top_left: vec2(region_uv.top_right.x, 0.),
top_right: vec2(1., 0.),
bottom_left: region_uv.top_right,
bottom_right: vec2(1., region_uv.top_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, 0.),
size: vec2(size_h.2, size_v.0),
color: interpolate_color_rect(top_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_right_patch_uv),
rounded_corners: None
});
//Left patch
let left_patch_uv = Corners {
top_left: vec2(0., region_uv.top_left.y),
top_right: region_uv.top_left,
bottom_left: vec2(0., region_uv.bottom_left.y),
bottom_right: region_uv.bottom_left,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0),
size: vec2(size_h.0, size_v.1),
color: interpolate_color_rect(left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(left_patch_uv),
rounded_corners: None
});
// Center patch
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0),
size: vec2(size_h.1, size_v.1),
color: interpolate_color_rect(region_uv),
texture: Some(self.asset.image),
texture_uv: Some(region_uv),
rounded_corners: None
});
//Right patch
let right_patch_uv = Corners {
top_left: region_uv.top_right,
top_right: vec2(1., region_uv.top_right.y),
bottom_left: region_uv.bottom_right,
bottom_right: vec2(1., region_uv.bottom_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0),
size: vec2(size_h.2, size_v.1),
color: interpolate_color_rect(right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(right_patch_uv),
rounded_corners: None
});
//Bottom-left patch
let bottom_left_patch_uv = Corners {
top_left: vec2(0., region_uv.bottom_left.y),
top_right: region_uv.bottom_left,
bottom_left: vec2(0., 1.),
bottom_right: vec2(region_uv.bottom_left.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0 + size_v.1),
size: vec2(size_h.0, size_v.2),
color: interpolate_color_rect(bottom_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_left_patch_uv),
rounded_corners: None
});
//Bottom patch
let bottom_patch_uv = Corners {
top_left: region_uv.bottom_left,
top_right: region_uv.bottom_right,
bottom_left: vec2(region_uv.bottom_left.x, 1.),
bottom_right: vec2(region_uv.bottom_right.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0 + size_v.1),
size: vec2(size_h.1, size_v.2),
color: interpolate_color_rect(bottom_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_patch_uv),
rounded_corners: None
});
//Bottom-right patch
let bottom_right_patch_uv = Corners {
top_left: region_uv.bottom_right,
top_right: vec2(1., region_uv.bottom_right.y),
bottom_left: vec2(region_uv.bottom_right.x, 1.),
bottom_right: vec2(1., 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1),
size: vec2(size_h.2, size_v.2),
color: interpolate_color_rect(bottom_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_right_patch_uv),
rounded_corners: None
});
}
fn covers_opaque(&self) -> bool {
false
}
}

View file

@ -1,3 +1,5 @@
//! frame-relative positioning/size
use glam::{Vec2, vec2}; use glam::{Vec2, vec2};
use derive_more::{Add, AddAssign, Sub, SubAssign}; use derive_more::{Add, AddAssign, Sub, SubAssign};
use crate::layout::{Size, Size2d}; use crate::layout::{Size, Size2d};
@ -24,11 +26,13 @@ impl From<f32> for FramePoint {
impl From<Size> for FramePoint { impl From<Size> for FramePoint {
/// Convert a `Size` into a `FramePoint` /// Convert a `Size` into a `FramePoint`
/// ///
/// This function behaves just as you would expect, but `Auto` is always treated as `BEGIN` /// This function behaves just as you would expect, but:
/// - `Auto` is always treated as `BEGIN`
/// - `Remaining` is treated as `Relative`
fn from(size: Size) -> Self { fn from(size: Size) -> Self {
match size { match size {
Size::Auto => Self::BEGIN, Size::Auto => Self::BEGIN,
Size::Relative(value) => Self::relative(value), Size::Relative(value) | Size::Remaining(value) => Self::relative(value),
Size::Absolute(value) => Self::absolute(value), Size::Absolute(value) => Self::absolute(value),
} }
} }

View file

@ -124,6 +124,7 @@ impl Frame for FrameRect {
size: bottom_right - top_left, size: bottom_right - top_left,
color: self.color.corners(), color: self.color.corners(),
texture: self.image, texture: self.image,
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some( rounded_corners: (self.corner_radius.max_f32() > 0.).then_some(
RoundedCorners::from_radius(self.corner_radius) RoundedCorners::from_radius(self.corner_radius)
), ),
@ -140,6 +141,7 @@ impl Frame for FrameRect {
self.bottom_right.y.absolute >= 0. && self.bottom_right.y.absolute >= 0. &&
self.bottom_right.y.relative >= 1. && self.bottom_right.y.relative >= 1. &&
self.color.is_opaque() && self.color.is_opaque() &&
self.image.is_none() self.image.is_none() &&
self.corner_radius.max_f32() == 0.
} }
} }

View file

@ -1,7 +1,10 @@
//! allows stacking two frames on top of each other
use glam::Vec2; use glam::Vec2;
use crate::draw::UiDrawCommandList; use crate::draw::UiDrawCommandList;
use super::Frame; 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>); pub struct FrameStack(pub Box<dyn Frame>, pub Box<dyn Frame>);
impl Frame for FrameStack { impl Frame for FrameStack {
@ -17,6 +20,7 @@ impl Frame for FrameStack {
} }
pub trait FrameStackExt: Frame { pub trait FrameStackExt: Frame {
/// Stack another frame on top of this one
fn stack(self, other: impl Frame + 'static) -> FrameStack; fn stack(self, other: impl Frame + 'static) -> FrameStack;
} }

View file

@ -101,6 +101,8 @@ pub enum KeyboardKey {
macro_rules! impl_fits64_for_keyboard_key { macro_rules! impl_fits64_for_keyboard_key {
($($i:ident = $v:literal),*) => { ($($i:ident = $v:literal),*) => {
impl Fits64 for KeyboardKey { impl Fits64 for KeyboardKey {
// SAFETY: not actually doing anything unsafe
#[allow(unsafe_code)]
unsafe fn from_u64(x: u64) -> Self { unsafe fn from_u64(x: u64) -> Self {
match x { match x {
$( $v => KeyboardKey::$i, )* $( $v => KeyboardKey::$i, )*

View file

@ -95,7 +95,7 @@ impl UiInstance {
/// (this will change to a soft error in the future) /// (this will change to a soft error in the future)
#[cfg(feature = "image")] #[cfg(feature = "image")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> { pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
use std::io::Read; use std::io::{Read, Seek};
// Open the file (and wrap it in a bufreader) // Open the file (and wrap it in a bufreader)
let mut file = std::io::BufReader::new(std::fs::File::open(path)?); let mut file = std::io::BufReader::new(std::fs::File::open(path)?);
@ -106,6 +106,7 @@ impl UiInstance {
let mut magic = [0; 64]; let mut magic = [0; 64];
file.read_exact(&mut magic)?; file.read_exact(&mut magic)?;
let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)"); let format = image::guess_format(&magic).expect("Invalid image data (FORMAT)");
file.seek(std::io::SeekFrom::Start(0))?;
//Parse the image and read the raw uncompressed rgba data //Parse the image and read the raw uncompressed rgba data
let image = image::load(file, format).expect("Invalid image data"); let image = image::load(file, format).expect("Invalid image data");
@ -156,6 +157,7 @@ impl UiInstance {
position: Vec2::ZERO, position: Vec2::ZERO,
max_size, max_size,
direction: Direction::Vertical, direction: Direction::Vertical,
remaining_space: None,
}; };
let measure = element.measure(MeasureContext { let measure = element.measure(MeasureContext {
state: &self.stateful_state, state: &self.stateful_state,

View file

@ -2,6 +2,46 @@
use glam::{vec2, Vec2}; use glam::{vec2, Vec2};
/// Controls wrapping behavior of elements
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord, Default)]
pub enum WrapBehavior {
/// No wrapping is allowed, even if explicit line breaks is requested by the element
Disable = 0,
/// Allow wrapping if the element explicitly requests it (default behavior)
#[default]
Allow = 1,
/// Elements will be wrapped automatically when they reach the maximum width/height of the container
Enable = 2,
}
impl From<bool> for WrapBehavior {
#[inline]
fn from(value: bool) -> Self {
match value {
true => Self::Enable,
false => Self::Disable,
}
}
}
impl WrapBehavior {
/// Check if wrapping is allowed for the element
#[inline]
pub fn is_allowed(&self) -> bool {
*self != Self::Disable
}
/// Check if wrapping is enabled for the element
///
/// (Wrapping will be done automatically when the element reaches the maximum width/height)
#[inline]
pub fn is_enabled(&self) -> bool {
*self == Self::Enable
}
}
/// Alignment along a single axis /// Alignment along a single axis
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, PartialOrd, Ord)]
pub enum Alignment { pub enum Alignment {
@ -86,17 +126,24 @@ pub enum Size {
#[default] #[default]
Auto, Auto,
/// Size as a ratio of parent size\
/// Valid range: 0.0-1.0 (0-100%)
///
/// Out of range values are allowed, but are not guaranteed to work as expected\
/// (especially with negative values)
Relative(f32),
//TODO FractionRemaining(f32),
/// Static size in pixels /// Static size in pixels
Absolute(f32), Absolute(f32),
/// Size as a ratio of parent element size
///
/// Expected range: `0.0..=1.0`
Relative(f32),
/// Size as a ratio of remaining space after all other elements have been laid out
///
/// Expected range: `0.0..=1.0`
///
/// - This feature is experimental and may not work as expected;\
/// Current `Container` implementation:
/// - Assumes that he line is fully filled if any element uses `Remaining` size, even if sum of remaining sizes is less than 1.0
/// - Does not support `Remaining` size in the secondary axis, it will be treated as `Relative`
/// - In cases where it's not applicable or not supported, it's defined to behave as `Relative`
Remaining(f32),
} }
impl From<f32> for Size { impl From<f32> for Size {
@ -159,6 +206,13 @@ pub struct LayoutInfo {
/// Current direction of the layout\ /// Current direction of the layout\
/// (Usually matches direction of the parent container) /// (Usually matches direction of the parent container)
pub direction: Direction, pub direction: Direction,
/// Remaining space in the primary axis\
///
/// This value is only available during the layout step and is only likely to be present if the element uses `Size::Remaining`
///
/// (Make sure that LayoutInfo::direction is set to the correct direction!)
pub remaining_space: Option<f32>,
} }
/// Helper function to calculate the size of an element based on its layout and size information\ /// Helper function to calculate the size of an element based on its layout and size information\
@ -168,11 +222,19 @@ pub fn compute_size(layout: &LayoutInfo, size: Size2d, comfy_size: Vec2) -> Vec2
Size::Auto => comfy_size.x, Size::Auto => comfy_size.x,
Size::Relative(fraction) => layout.max_size.x * fraction, Size::Relative(fraction) => layout.max_size.x * fraction,
Size::Absolute(size) => size, Size::Absolute(size) => size,
Size::Remaining(fraction) => match layout.direction {
Direction::Horizontal => layout.remaining_space.unwrap_or(layout.max_size.x) * fraction,
Direction::Vertical => layout.max_size.x * fraction,
}
}; };
let height = match size.height { let height = match size.height {
Size::Auto => comfy_size.y, Size::Auto => comfy_size.y,
Size::Relative(fraction) => layout.max_size.y * fraction, Size::Relative(fraction) => layout.max_size.y * fraction,
Size::Absolute(size) => size, Size::Absolute(size) => size,
Size::Remaining(fraction) => match layout.direction {
Direction::Horizontal => layout.max_size.y * fraction,
Direction::Vertical => layout.remaining_space.unwrap_or(layout.max_size.y) * fraction,
}
}; };
vec2(width, height) vec2(width, height)
} }

View file

@ -5,9 +5,11 @@
//! # Features //! # Features
#![doc = document_features::document_features!()] #![doc = document_features::document_features!()]
#![allow(unused_parens)] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
//#![forbid(unsafe_code)]
#![deny(unsafe_code)]
#![forbid(unsafe_op_in_unsafe_fn)] #![forbid(unsafe_op_in_unsafe_fn)]
#![allow(unused_parens)]
mod instance; mod instance;
mod macros; mod macros;

View file

@ -6,6 +6,8 @@
/// - `x` - `Size::Absolute(x)` /// - `x` - `Size::Absolute(x)`
/// - `x%` - `Size::Relative(x / 100.)` *(literal only)* /// - `x%` - `Size::Relative(x / 100.)` *(literal only)*
/// - `x/` - `Size::Relative(x)` /// - `x/` - `Size::Relative(x)`
/// - `x%=` - `Size::Remaining(x / 100.)` *(literal only)*
/// - `x/=` - `Size::Remaining(x)`
/// ///
/// ...where `x` is a literal, identifier or an expression wrapped in parentheses /// ...where `x` is a literal, identifier or an expression wrapped in parentheses
/// ///
@ -32,6 +34,12 @@ macro_rules! size {
($x:literal /) => { ($x:literal /) => {
$crate::layout::Size::Relative($x as f32) $crate::layout::Size::Relative($x as f32)
}; };
($x:literal %=) => {
$crate::layout::Size::Remaining($x as f32 / 100.)
};
($x:literal /=) => {
$crate::layout::Size::Remaining($x as f32)
};
($x:ident) => { ($x:ident) => {
$crate::layout::Size::Absolute($x as f32) $crate::layout::Size::Absolute($x as f32)
@ -39,6 +47,9 @@ macro_rules! size {
($x:ident /) => { ($x:ident /) => {
$crate::layout::Size::Relative($x as f32) $crate::layout::Size::Relative($x as f32)
}; };
($x:ident /=) => {
$crate::layout::Size::Remaining($x as f32)
};
(($x:expr)) => { (($x:expr)) => {
$crate::layout::Size::Absolute(($x) as f32) $crate::layout::Size::Absolute(($x) as f32)
@ -46,6 +57,9 @@ macro_rules! size {
(($x:expr) /) => { (($x:expr) /) => {
$crate::layout::Size::Relative(($x) as f32) $crate::layout::Size::Relative(($x) as f32)
}; };
(($x:expr) /=) => {
$crate::layout::Size::Remaining(($x) as f32)
};
($x:tt , $y:tt $($ys:tt)?) => { ($x:tt , $y:tt $($ys:tt)?) => {
$crate::layout::Size2d { $crate::layout::Size2d {

View file

@ -1,5 +1,5 @@
use super::Corners; use super::Corners;
use glam::{Vec3, Vec4, vec4}; use glam::{Vec2, Vec3, Vec4, vec4};
/// Represents the fill color of a rectangle /// Represents the fill color of a rectangle
/// ///
@ -69,6 +69,14 @@ impl FillColor {
pub const fn corners(&self) -> Corners<Vec4> { pub const fn corners(&self) -> Corners<Vec4> {
self.0 self.0
} }
/// Interpolate color on position, assuming a linear gradient
pub fn interpolate(&self, uv: Vec2) -> Vec4 {
let c = self.corners();
let top = c.top_left.lerp(c.top_right, uv.x);
let bottom = c.bottom_left.lerp(c.bottom_right, uv.x);
top.lerp(bottom, uv.y)
}
} }
impl Default for FillColor { impl Default for FillColor {

View file

@ -6,6 +6,9 @@ use nohash_hasher::BuildNoHashHasher;
pub mod trigger; pub mod trigger;
#[cfg(feature = "derive")]
pub use hui_derive::Signal;
/// A marker trait for UI Signals /// A marker trait for UI Signals
pub trait Signal: Any {} pub trait Signal: Any {}