Compare commits

..

15 commits

Author SHA1 Message Date
griffi-gh 2db8d2f056 aaa 2024-03-24 22:55:36 +01:00
griffi-gh 95b4c47fbd prepare test 2024-03-24 22:55:15 +01:00
griffi-gh 91c9af9fd5 add image loading 2024-03-24 22:55:05 +01:00
griffi-gh 3b7059d49f allow customizing handle size 2024-03-24 22:27:59 +01:00
griffi-gh dac0c7ac6d upd api to frame 2024-03-24 22:18:15 +01:00
griffi-gh 6ec12187ac disable covers_opaque optimization for images 2024-03-24 22:01:36 +01:00
griffi-gh fc4bc83ba9 add covers_opaque 2024-03-24 21:35:44 +01:00
griffi-gh deec244547 slider: use frames (less efficient :<) 2024-03-24 21:14:24 +01:00
griffi-gh 8e7e32671c :3 2024-03-24 21:01:30 +01:00
griffi-gh 4ea98db39a update glam to 0.27 2024-03-24 21:00:46 +01:00
griffi-gh 7bd93af63f format the thingy 2024-03-24 20:58:14 +01:00
griffi-gh d4402756a3 update built-in element features 2024-03-24 20:53:03 +01:00
griffi-gh 0428af0f63 uwu 2024-03-24 19:08:24 +01:00
griffi-gh 290157e8d9 improve frame_rect macro docs 2024-03-24 19:07:51 +01:00
griffi-gh b8f9ace3da use the new macro in examples 2024-03-24 18:32:50 +01:00
21 changed files with 397 additions and 147 deletions

View file

@ -12,7 +12,7 @@ hui-winit = { path = "../hui-winit" }
kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "c162893fd" } kubi-logging = { git = "https://github.com/griffi-gh/kubi", rev = "c162893fd" }
glium = "0.34" glium = "0.34"
winit = "0.29" winit = "0.29"
glam = "0.25" glam = "0.27"
log = "0.4" log = "0.4"
#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)

View file

@ -73,24 +73,21 @@ fn main() {
children: ElementList(vec![ children: ElementList(vec![
Box::new(FillRect { Box::new(FillRect {
size: (Size::Relative(0.5), Size::Absolute(30.)).into(), size: (Size::Relative(0.5), Size::Absolute(30.)).into(),
background: vec4(0.75, 0., 0., 1.).into(), frame: Box::new(vec4(0.75, 0., 0., 1.)),
..Default::default()
}), }),
Box::new(FillRect { Box::new(FillRect {
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(), size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
background: Corners::left_right( frame: Box::new(Corners::left_right(
vec4(1., 0., 0., 1.), vec4(1., 0., 0., 1.),
vec4(0., 1., 0., 1.) vec4(0., 1., 0., 1.)
).into(), )),
..Default::default()
}), }),
]), ]),
..Default::default() ..Default::default()
}), }),
Box::new(FillRect { Box::new(FillRect {
size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(), size: (Size::Relative(z / 2. + 0.5), Size::Absolute(30.)).into(),
background: vec4(0., 0.75, 0., 1.).into(), frame: Box::new(vec4(0., 0.75, 0., 1.)),
..Default::default()
}), }),
Box::new(Container { Box::new(Container {
gap: 5., gap: 5.,
@ -102,12 +99,11 @@ fn main() {
for i in 0..10 { for i in 0..10 {
x.push(Box::new(FillRect { x.push(Box::new(FillRect {
size: (Size::Absolute(50.), Size::Absolute(50.)).into(), size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
background: if i == 1 { frame: Box::new(if i == 1 {
vec4(0.75, 0.75, 0.75, 0.75).into() vec4(0.75, 0.75, 0.75, 0.75)
} else { } else {
vec4(0.5, 0.5, 0.5, 0.75).into() vec4(0.5, 0.5, 0.5, 0.75)
}, }),
..Default::default()
})); }));
} }
ElementList(x) ElementList(x)
@ -130,8 +126,7 @@ fn main() {
children: ElementList(vec![ children: ElementList(vec![
Box::new(FillRect { Box::new(FillRect {
size: (Size::Absolute(50.), Size::Absolute(50.)).into(), size: (Size::Absolute(50.), Size::Absolute(50.)).into(),
background: vec4(1., 1., 1., 0.75).into(), frame: Box::new(vec4(1., 1., 1., 0.75)),
..Default::default()
}), }),
]), ]),
..Default::default() ..Default::default()

View file

@ -73,13 +73,11 @@ fn main() {
} }
elem.push(Box::new(FillRect { elem.push(Box::new(FillRect {
size: (Size::Relative(1.), Size::Absolute(10.)).into(), size: (Size::Relative(1.), Size::Absolute(10.)).into(),
background: vec4(0., 0., 1., 1.).into(), frame: Box::new(vec4(0., 0., 1., 1.)),
..Default::default()
})); }));
elem.push(Box::new(FillRect { elem.push(Box::new(FillRect {
size: (Size::Relative(1.), Size::Absolute(10.)).into(), size: (Size::Relative(1.), Size::Absolute(10.)).into(),
background: vec4(1., 1., 0., 1.).into(), frame: Box::new(vec4(1., 1., 0., 1.)),
..Default::default()
})); }));
elem.push(Box::new(Text { elem.push(Box::new(Text {
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(), text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
@ -90,13 +88,11 @@ fn main() {
if instant.elapsed().as_secs() & 1 != 0 { if instant.elapsed().as_secs() & 1 != 0 {
elem.push(Box::new(FillRect { elem.push(Box::new(FillRect {
size: (Size::Relative(1.), Size::Absolute(10.)).into(), size: (Size::Relative(1.), Size::Absolute(10.)).into(),
background: vec4(1., 0., 0., 1.).into(), frame: Box::new(vec4(1., 0., 0., 1.)),
..Default::default()
})); }));
elem.push(Box::new(FillRect { elem.push(Box::new(FillRect {
size: (Size::Relative(1.), Size::Absolute(10.)).into(), size: (Size::Relative(1.), Size::Absolute(10.)).into(),
background: vec4(0., 0., 0., 1.).into(), frame: Box::new(vec4(0., 0., 0., 1.)),
..Default::default()
})); }));
elem.push(Box::new(Spacer(100.))); elem.push(Box::new(Spacer(100.)));
elem.push(Box::new(Text { elem.push(Box::new(Text {

View file

@ -1,5 +1,8 @@
use hui::{ use hui::{
color, element::{container::Container, text::Text, UiElementExt}, frame::FrameRect, layout::Alignment, size color, size, frame_rect,
element::{container::Container, text::Text, UiElementExt},
frame::FrameRect,
layout::Alignment,
}; };
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
@ -12,10 +15,10 @@ ui_main!(|ui, size, _| {
.with_align(Alignment::Center) .with_align(Alignment::Center)
.with_padding(5.) .with_padding(5.)
.with_gap(10.) .with_gap(10.)
.with_background( .with_background(frame_rect! {
FrameRect::color(color::WHITE) color: (0.5, 0.5, 0.5, 1.),
.with_corner_radius(10.) corner_radius: 10.,
) })
.with_children(|ui| { .with_children(|ui| {
Text::default() Text::default()
.with_text("Hello, world") .with_text("Hello, world")
@ -24,10 +27,10 @@ ui_main!(|ui, size, _| {
.add_child(ui); .add_child(ui);
Container::default() Container::default()
.with_padding((10., 20.)) .with_padding((10., 20.))
.with_background( .with_background(frame_rect! {
FrameRect::color(color::DARK_RED) color: color::DARK_RED,
.with_corner_radius((2.5, 30., 2.5, 2.5)) 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.")

View file

@ -45,10 +45,10 @@ ui_main!(
.with_children(|ui| { .with_children(|ui| {
Container::default() Container::default()
.with_padding((10., 15.)) .with_padding((10., 15.))
.with_background( .with_background(frame_rect! {
FrameRect::color((0., 0., 0., 0.5)) color: (0., 0., 0., 0.5),
.with_corner_radius(8.) corner_radius: 8.,
) })
.with_children(|ui| { .with_children(|ui| {
let flash = 1. - 0.5 * (4. * instant.elapsed().as_secs_f32()).sin().powi(2); let flash = 1. - 0.5 * (4. * instant.elapsed().as_secs_f32()).sin().powi(2);
Text::default() Text::default()
@ -111,10 +111,10 @@ ui_main!(
.with_children(|ui| { .with_children(|ui| {
Container::default() Container::default()
.with_padding(10.) .with_padding(10.)
.with_background( .with_background(frame_rect!{
FrameRect::color((0., 0., 0., 0.5)) color: (0., 0., 0., 0.5),
.with_corner_radius(8.) corner_radius: 8.,
) })
.with_children(|ui| { .with_children(|ui| {
Text::default() Text::default()
.with_text("Level 5") .with_text("Level 5")

View file

@ -1,12 +1,10 @@
use std::time::Instant; use std::time::Instant;
use hui::{ use hui::{
color, size, color, element::{
layout::{Alignment, Direction},
element::{
container::Container, container::Container,
fill_rect::FillRect, fill_rect::FillRect,
UiElementExt UiElementExt
}, }, frame_rect, layout::{Alignment, Direction}, size
}; };
#[path = "../boilerplate.rs"] #[path = "../boilerplate.rs"]
@ -32,8 +30,10 @@ ui_main!(
for i in 0..10 { for i in 0..10 {
FillRect::default() FillRect::default()
.with_size(size!((40 + i * 10))) .with_size(size!((40 + i * 10)))
.with_corner_radius(8.) .with_frame(frame_rect! {
.with_background(color::DARK_RED) color: color::DARK_RED,
corner_radius: 8.
})
.add_child(ui); .add_child(ui);
} }
}) })

View file

@ -8,7 +8,6 @@ use hui::{
text::Text, text::Text,
UiElementExt, UiElementExt,
}, },
frame::FrameRect,
layout::{Alignment, Direction}, layout::{Alignment, Direction},
signal::Signal, signal::Signal,
size, size,

View file

@ -0,0 +1,34 @@
use std::time::Instant;
use hui::{
color, element::{
container::Container,
fill_rect::FillRect,
UiElementExt
}, frame_rect, layout::{Alignment, Direction}, size
};
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
ui_main!(
"hUI: 9-Patch demo",
init: |_| {
},
run: |ui, size, _| {
Container::default()
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_background(color::WHITE)
.with_children(|ui| {
FillRect::default()
.with_size(size!(300, 100))
.with_frame(frame_rect! {
color: color::RED
})
.add_child(ui);
})
.add_root(ui, size);
}
);

View file

@ -55,7 +55,7 @@ ui_main!(
.add_child(ui); .add_child(ui);
FillRect::default() FillRect::default()
.with_size(size!(100%, 1)) .with_size(size!(100%, 1))
.with_background(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%))
@ -67,7 +67,7 @@ ui_main!(
.add_child(ui); .add_child(ui);
FillRect::default() FillRect::default()
.with_size(size!(1, 100%)) .with_size(size!(1, 100%))
.with_background(color::rgb_hex(0x2d2d30)) .with_frame(color::rgb_hex(0x2d2d30))
.add_child(ui); .add_child(ui);
Container::default() Container::default()
.with_size(size!(200, 100%)) .with_size(size!(200, 100%))

View file

@ -17,5 +17,5 @@ include = [
[dependencies] [dependencies]
hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false } hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false }
glium = { version = "0.34", default-features = false } glium = { version = "0.34", default-features = false }
glam = "0.25" glam = "0.27"
log = "0.4" log = "0.4"

View file

@ -16,5 +16,5 @@ include = [
[dependencies] [dependencies]
hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false } hui = { version = "=0.1.0-alpha.4", path = "../hui", default-features = false }
winit = { version = "0.29", default-features = false } winit = { version = "0.29", default-features = false }
glam = "0.25" glam = "0.27"
log = "0.4" log = "0.4"

View file

@ -18,7 +18,7 @@ include = [
[dependencies] [dependencies]
hashbrown = "0.14" hashbrown = "0.14"
nohash-hasher = "0.2" nohash-hasher = "0.2"
glam = "0.25" glam = "0.27"
fontdue = "0.8" fontdue = "0.8"
rect_packer = "0.2" rect_packer = "0.2"
log = "0.4" log = "0.4"
@ -26,21 +26,77 @@ document-features = "0.2"
derive_setters = "0.1" derive_setters = "0.1"
derive_more = "0.99" derive_more = "0.99"
tinyset = "0.4" tinyset = "0.4"
#enum_dispatch = "0.3" image = { version = "0.25", default-features = false, optional = true }
[features] [features]
default = ["builtin_elements", "builtin_font", "pixel_perfect_text"] default = ["el_all", "image", "builtin_font", "pixel_perfect_text"]
## Enable the built-in font (ProggyTiny, adds 35kb to the executable)
#! Image loading support:
## Enable image loading support using the `image` crate
image = ["dep:image"]
#! #### Built-in font:
## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
builtin_font = [] builtin_font = []
## Enable the built-in elements (`Container`, `ProgressBar`, etc.)\
builtin_elements = ["builtin_container"] #! #### Pixel-perfect rendering:
## Enable only the `Container` component (which is essential for laying out other components)
builtin_container = []
## Round all vertex positions to nearest integer coordinates (not recommended) ## Round all vertex positions to nearest integer coordinates (not recommended)
pixel_perfect = ["pixel_perfect_text"] pixel_perfect = ["pixel_perfect_text"]
## Apply pixel-perfect rendering hack to text (fixes blurry text rendering) ## Apply pixel-perfect rendering hack to text (fixes blurry text rendering)
pixel_perfect_text = [] pixel_perfect_text = []
#! Make sure to disable the `pixel_perfect` feature if you are rendering UI in 3D space\
#! or using DPI (or any other form of) scaling while passing the virtual resolution to the ui #! 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
el_all = [
"el_container",
"el_fill_rect",
"el_spacer",
"el_br",
"el_text",
"el_image",
"el_progress_bar",
"el_slider",
"el_transformer",
"el_interactable",
]
## Enable the built-in `Container` element
el_container = []
## Enable the built-in `FillRect` element
el_fill_rect = []
## Enable the built-in `Spacer` element
el_spacer = []
## Enable the built-in `Break` element
el_br = []
## Enable the built-in `Text` element
el_text = []
## Enable the built-in `Image` element
el_image = []
## Enable the built-in `ProgressBar` element
el_progress_bar = []
## Enable the built-in `Slider` element
el_slider = []
## Enable the built-in `Transformer` element
el_transformer = []
## Enable the built-in `Interactable` element
el_interactable = []
# ## Enable multi-threading support (currently only affects some 3rd-party libraries) # ## Enable multi-threading support (currently only affects some 3rd-party libraries)
# parallel = ["fontdue/parallel"] # parallel = ["fontdue/parallel"]

View file

@ -1,40 +1,40 @@
// Layout stuff: // Layout stuff:
#[cfg(feature = "builtin_container")] #[cfg(feature = "el_container")]
pub mod container; pub mod container;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_fill_rect")]
pub mod fill_rect; pub mod fill_rect;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_spacer")]
pub mod spacer; pub mod spacer;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_br")]
pub mod br; pub mod br;
// Basic elements: // Basic elements:
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_text")]
pub mod text; pub mod text;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_image")]
pub mod image; pub mod image;
// "Extras": // "Extras":
// (meant to be replaced if needed) // (meant to be replaced if needed)
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_progress_bar")]
pub mod progress_bar; pub mod progress_bar;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_slider")]
pub mod slider; pub mod slider;
// Wrappers: // Wrappers:
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_transformer")]
pub mod transformer; pub mod transformer;
#[cfg(feature = "builtin_elements")] #[cfg(feature = "el_interactable")]
pub mod interactable; pub mod interactable;
//TODO add: Image //TODO add: Image

View file

@ -1,39 +1,41 @@
//! Simple filled rectangle with the specified size, background and corner radius //! Simple filled rectangle with the specified size, background and corner radius
use derive_setters::Setters; use derive_setters::Setters;
use glam::{vec2, Vec4}; use glam::vec2;
use crate::{ use crate::{
draw::{RoundedCorners, UiDrawCommand}, draw::{RoundedCorners, UiDrawCommand},
element::{MeasureContext, ProcessContext, UiElement}, element::{MeasureContext, ProcessContext, UiElement},
frame::{Frame, FrameRect},
layout::{Size, Size2d}, layout::{Size, Size2d},
measure::Response, measure::Response,
rect::{Corners, FillColor}, size
size,
}; };
/// Simple filled rectangle with the specified size, background, and corner radius /// Simple filled rectangle with the specified size, background, and corner radius
#[derive(Debug, Clone, Copy, Setters)] #[derive(Setters)]
#[setters(prefix = "with_")] #[setters(prefix = "with_")]
pub struct FillRect { pub struct FillRect {
/// Size of the rectangle /// Size of the rectangle
#[setters(into)] #[setters(into)]
pub size: Size2d, pub size: Size2d,
/// Background color of the rectangle /// Frame
#[setters(into)] #[setters(skip)]
pub background: FillColor, pub frame: Box<dyn Frame>,
}
/// Corner radius of the rectangle impl FillRect {
#[setters(into)] pub fn with_frame(mut self, frame: impl Frame + 'static) -> Self {
pub corner_radius: Corners<f32>, self.frame = Box::new(frame);
self
}
} }
impl Default for FillRect { impl Default for FillRect {
fn default() -> Self { fn default() -> Self {
Self { Self {
size: size!(10, 10), size: size!(10, 10),
background: Vec4::new(0., 0., 0., 0.5).into(), frame: Box::new(FrameRect::color((0., 0., 0., 0.5))),
corner_radius: Corners::all(0.),
} }
} }
} }
@ -62,16 +64,17 @@ impl UiElement for FillRect {
} }
fn process(&self, ctx: ProcessContext) { fn process(&self, ctx: ProcessContext) {
if !self.background.is_transparent() { // if !self.background.is_transparent() {
ctx.draw.add(UiDrawCommand::Rectangle { // ctx.draw.add(UiDrawCommand::Rectangle {
position: ctx.layout.position, // position: ctx.layout.position,
size: ctx.measure.size, // size: ctx.measure.size,
color: self.background.corners(), // color: self.background.corners(),
texture: None, // texture: 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)
}), // }),
}); // });
} // }
self.frame.draw(ctx.draw, ctx.layout.position, ctx.measure.size);
} }
} }

View file

@ -4,12 +4,7 @@ use derive_setters::Setters;
use glam::{Vec2, vec2}; use glam::{Vec2, vec2};
use crate::{ use crate::{
draw::UiDrawCommand, draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, frame::{Frame, FrameRect}, layout::{compute_size, Size2d}, measure::Response, rect::FillColor, signal::{trigger::SignalTriggerArg, Signal}
element::{MeasureContext, ProcessContext, UiElement},
layout::{Size2d, compute_size},
measure::Response,
rect::FillColor,
signal::{trigger::SignalTriggerArg, Signal},
}; };
@ -46,22 +41,31 @@ pub struct Slider {
#[setters(into)] #[setters(into)]
pub size: Size2d, pub size: Size2d,
/// Color of the slider handle /// Track frame
#[setters(into)] #[setters(skip)]
pub handle_color: FillColor, pub track: Box<dyn Frame>,
/// Color of the slider track /// Track active frame
#[setters(into)] #[setters(skip)]
pub track_color: FillColor, pub track_active: Box<dyn Frame>,
/// Color of the "active" part of the slider /// Handle frame
#[setters(into)] #[setters(skip)]
pub track_active_color: FillColor, pub handle: Box<dyn Frame>,
/// Track height relative to the slider height\ /// Track height *relative to the slider height*\
/// ///
/// Range: 0.0..=1.0 /// Range: 0.0..=1.0
pub track_height_ratio: f32, pub track_height: f32,
/// Handle size
///
/// Please be aware that:
///
/// - Width is *static* and specified in *pixels* (e.g. `15.0`)
/// - Height is *relative* to the slider height,\
/// ...and is specified as a *ratio* in range `0.0..=1.0`
pub handle_size: (f32, f32),
/// Follow mode /// Follow mode
pub follow_mode: SliderFollowMode, pub follow_mode: SliderFollowMode,
@ -75,10 +79,11 @@ impl Default for Slider {
Self { Self {
value: 0.0, value: 0.0,
size: Size2d::default(), size: Size2d::default(),
handle_color: (0.0, 0.0, 1.).into(), handle: Box::new(FrameRect::color((0.0, 0.0, 1.))),
track_color: (0.5, 0.5, 0.5).into(), track: Box::new(FrameRect::color((0.5, 0.5, 0.5))),
track_active_color: (0.0, 0.0, 0.75).into(), track_active: Box::new(FrameRect::color((0.0, 0.0, 0.75))),
track_height_ratio: 0.25, track_height: 0.25,
handle_size: (15.0, 1.),
follow_mode: SliderFollowMode::default(), follow_mode: SliderFollowMode::default(),
on_change: None on_change: None
} }
@ -101,6 +106,21 @@ impl Slider {
..self ..self
} }
} }
pub fn with_track(mut self, track: impl Frame + 'static) -> Self {
self.track = Box::new(track);
self
}
pub fn with_track_active(mut self, track_active: impl Frame + 'static) -> Self {
self.track_active = Box::new(track_active);
self
}
pub fn with_handle(mut self, handle: impl Frame + 'static) -> Self {
self.handle = Box::new(handle);
self
}
} }
impl UiElement for Slider { impl UiElement for Slider {
@ -121,55 +141,65 @@ impl UiElement for Slider {
//Compute handle size: //Compute handle size:
// This is kinda counter-intuitive, but if the handle is transparent, we treat it as completely disabled // This is kinda counter-intuitive, but if the handle is transparent, we treat it as completely disabled
// To prevent confusing offset from the edge of the slider, we set the handle size to 0 // To prevent confusing offset from the edge of the slider, we set the handle size to 0
let handle_size = if self.handle_color.is_transparent() { // let handle_size = if self.handle_color.is_transparent() {
Vec2::ZERO // Vec2::ZERO
} else { // } else {
vec2(15., ctx.measure.size.y) // vec2(15., ctx.measure.size.y)
}; // };
let handle_size = vec2(self.handle_size.0, self.handle_size.1 * ctx.measure.size.y);
//Draw the track //Draw the track
//If the active part is opaque and value >= 1., we don't need to draw the background as the active part will cover it //If the active part is opaque and value >= 1., we don't need to draw the background as the active part will cover it
//However, if the handle is not opaque, we need to draw the background as the active part won't quite reach the end //However, if the handle is not opaque, we need to draw the background as the active part won't quite reach the end
//Of corse, if it's fully transparent, we don't need to draw it either //Of corse, if it's fully transparent, we don't need to draw it either
if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) { // if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) {
ctx.draw.add(UiDrawCommand::Rectangle { if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) {
position: ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height_ratio / 2.), self.track.draw(
size: ctx.measure.size * vec2(1., self.track_height_ratio), ctx.draw,
color: self.track_color.into(), ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
texture: None, ctx.measure.size * vec2(1., self.track_height),
rounded_corners: None, );
});
} }
//"Active" part of the track //"Active" part of the track
//We can skip drawing it if it's fully transparent or value <= 0. //We can skip drawing it if it's fully transparent or value <= 0.
//But if the handle is not opaque, it should be visible even if value is zero //But if the handle is not opaque, it should be visible even if value is zero
if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) { // if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) {
ctx.draw.add(UiDrawCommand::Rectangle { if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) {
position: ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height_ratio / 2.), self.track_active.draw(
size: (ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height_ratio) + handle_size * Vec2::X / 2., ctx.draw,
color: self.track_active_color.into(), ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
texture: None, (ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2.,
rounded_corners: None, );
});
} }
// The handle // The handle
if handle_size.x != 0. && !self.handle_color.is_transparent() { // if handle_size.x != 0. && !self.handle_color.is_transparent() {
let value = self.value.clamp(0., 1.); // let value = self.value.clamp(0., 1.);
ctx.draw.add(UiDrawCommand::Rectangle { // ctx.draw.add(UiDrawCommand::Rectangle {
position: ctx.layout.position + ((ctx.measure.size.x - handle_size.x) * value) * Vec2::X, // position: ctx.layout.position + ((ctx.measure.size.x - handle_size.x) * value) * Vec2::X,
size: handle_size, // size: handle_size,
color: self.handle_color.into(), // color: self.handle_color.into(),
texture: None, // texture: None,
rounded_corners: None, // rounded_corners: None,
}); // });
// }
if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) {
self.handle.draw(
ctx.draw,
ctx.layout.position +
((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X +
ctx.measure.size.y * ((1. - self.handle_size.1) * 0.5) * Vec2::Y,
handle_size,
);
} }
//handle events //handle events
if let Some(res) = ctx.input.check_active(ctx.measure.rect(ctx.layout.position)) { if let Some(res) = ctx.input.check_active(ctx.measure.rect(ctx.layout.position)) {
let new_value = match self.follow_mode { let new_value = match self.follow_mode {
SliderFollowMode::Absolute => ((res.position_in_rect.x - handle_size.x / 2.) / (ctx.measure.size.x - handle_size.x)).clamp(0., 1.), SliderFollowMode::Absolute => {
((res.position_in_rect.x - handle_size.x / 2.) / (ctx.measure.size.x - handle_size.x)).clamp(0., 1.)
},
SliderFollowMode::Relative => { SliderFollowMode::Relative => {
let delta = res.position_in_rect.x - res.last_position_in_rect.x; let delta = res.position_in_rect.x - res.last_position_in_rect.x;
let delta_ratio = delta / (ctx.measure.size.x - handle_size.x); let delta_ratio = delta / (ctx.measure.size.x - handle_size.x);

View file

@ -9,5 +9,16 @@ mod impls;
pub use rect::FrameRect; pub use rect::FrameRect;
pub trait Frame { pub trait Frame {
/// 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);
/// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size
///
/// Returns true if the frame:
/// - Is fully opaque (i.e. `alpha >= 1.0`)
/// - Completely covers (or exceeds the size of) the frame
///
/// False negatives are acceptable, but false positives ***are not***.\
/// May be used for optimization purposes
fn covers_opaque(&self) -> bool { false }
} }

View file

@ -16,6 +16,10 @@ impl Frame for ImageHandle {
rounded_corners: None, rounded_corners: None,
}) })
} }
fn covers_opaque(&self) -> bool {
false
}
} }
impl Frame for FillColor { impl Frame for FillColor {
@ -28,6 +32,10 @@ impl Frame for FillColor {
rounded_corners: None, rounded_corners: None,
}) })
} }
fn covers_opaque(&self) -> bool {
self.is_opaque()
}
} }
// impl for various types resembling colors // impl for various types resembling colors
@ -38,24 +46,36 @@ impl Frame for Corners<Vec4> {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for (Vec4, Vec4, Vec4, Vec4) { impl Frame for (Vec4, Vec4, Vec4, Vec4) {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)) { 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, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for [[f32; 4]; 4] { impl Frame for [[f32; 4]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
// Corners (RGB): // Corners (RGB):
@ -64,24 +84,36 @@ impl Frame for Corners<Vec3> {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for (Vec3, Vec3, Vec3, Vec3) { impl Frame for (Vec3, Vec3, Vec3, Vec3) {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) { impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for [[f32; 3]; 4] { impl Frame for [[f32; 3]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
// RGBA: // RGBA:
@ -90,18 +122,27 @@ impl Frame for Vec4 {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for (f32, f32, f32, f32) { impl Frame for (f32, f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for [f32; 4] { impl Frame for [f32; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
// RGB: // RGB:
@ -110,16 +151,25 @@ impl Frame for Vec3 {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for (f32, f32, f32) { impl Frame for (f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }
impl Frame for [f32; 3] { impl Frame for [f32; 3] {
fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) { fn draw(&self, draw: &mut UiDrawCommandList, position: Vec2, parent_size: Vec2) {
FillColor::from(*self).draw(draw, position, parent_size) FillColor::from(*self).draw(draw, position, parent_size)
} }
fn covers_opaque(&self) -> bool {
FillColor::from(*self).is_opaque()
}
} }

View file

@ -6,6 +6,9 @@ use crate::{
}; };
use super::{Frame, point::FramePoint2d}; use super::{Frame, point::FramePoint2d};
/// A rectangular frame
///
/// Can optionally be tinted, textured, and have rounded corners
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct FrameRect { pub struct FrameRect {
/// Background color of the frame\ /// Background color of the frame\
@ -59,7 +62,9 @@ impl FrameRect {
} }
} }
/// Create a new [`FrameRect`] with the given image /// Create a new [`FrameRect`] 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: ImageHandle) -> Self {
Self { Self {
color: color::WHITE.into(), color: color::WHITE.into(),
@ -124,4 +129,17 @@ impl Frame for FrameRect {
), ),
}); });
} }
fn covers_opaque(&self) -> bool {
self.top_left.x.absolute <= 0. &&
self.top_left.x.relative <= 0. &&
self.top_left.y.absolute <= 0. &&
self.top_left.y.relative <= 0. &&
self.bottom_right.x.absolute >= 0. &&
self.bottom_right.x.relative >= 1. &&
self.bottom_right.y.absolute >= 0. &&
self.bottom_right.y.relative >= 1. &&
self.color.is_opaque() &&
self.image.is_none()
}
} }

View file

@ -9,6 +9,11 @@ impl Frame for FrameStack {
self.0.draw(draw, position, parent_size); self.0.draw(draw, position, parent_size);
self.1.draw(draw, position, parent_size); self.1.draw(draw, position, parent_size);
} }
fn covers_opaque(&self) -> bool {
self.0.covers_opaque() ||
self.1.covers_opaque()
}
} }
pub trait FrameStackExt: Frame { pub trait FrameStackExt: Frame {

View file

@ -83,6 +83,44 @@ impl UiInstance {
self.atlas.add(width, data, format) self.atlas.add(width, data, format)
} }
//TODO better error handling
/// Add an image from a file to the texture atlas\
/// (experimental, may be removed in the future)
///
/// Requires the `image` feature
///
/// # 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;
// 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)");
//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)
}
/// Push a font to the font stack\ /// Push a font to the font stack\
/// The font will be used for all text rendering until it is popped /// The font will be used for all text rendering until it is popped
/// ///

View file

@ -62,6 +62,18 @@ macro_rules! size {
} }
/// Helper macro for constructing a `FrameRect` /// Helper macro for constructing a `FrameRect`
///
/// # Example:
/// ```
/// frame_rect! {
/// color: (0.2, 0.2, 0.3, 1.),
/// corner_radius: 5.,
/// };
/// ```
///
/// # Note:
/// - If the `image` field is set, but not `color`, the `color` field will default to [`WHITE`](crate::color::WHITE) (to ensure visibility)
/// - If both `color` and `image` are not set, the `color` field will default to [`TRANSPARENT`](crate::color::TRANSPARENT)
#[macro_export] #[macro_export]
macro_rules! frame_rect { macro_rules! frame_rect {
{} => { {} => {