signal stuff...

This commit is contained in:
griffi-gh 2024-03-12 00:29:26 +01:00
parent 4b3f15e6ce
commit 01b30c5979
8 changed files with 151 additions and 21 deletions

View file

@ -1,15 +1,21 @@
use std::time::Instant;
use hui::{
color, size,
layout::{Alignment, Direction},
element::{
container::Container,
fill_rect::FillRect,
text::Text,
interactable::ElementInteractableExt,
UiElementExt
},
signal::UiSignal,
};
enum CounterSignal {
Increment,
Decrement,
}
impl UiSignal for CounterSignal {}
#[path = "../boilerplate.rs"]
#[macro_use]
mod boilerplate;
@ -19,16 +25,39 @@ ui_main!(
init: |_| {
0
},
run: |ui, size, n| {
run: |ui, size, counter| {
Container::default()
.with_size(size!(100%))
.with_align(Alignment::Center)
.with_direction(Direction::Horizontal)
.with_gap(5.)
.with_background(color::WHITE)
.with_children(|ui| {
FillRect::default()
.with_size(size!(40))
Container::default()
.with_padding(10.)
.with_corner_radius(8.)
.with_background(color::DARK_RED)
.with_children(|ui| {
Text::new("-")
.add_child(ui);
})
.into_interactable()
.on_click(|| {
println!("clicked");
})
.add_child(ui);
Text::new(counter.to_string())
.with_color(color::BLACK)
.with_text_size(32)
.add_child(ui);
Container::default()
.with_padding(10.)
.with_corner_radius(8.)
.with_background(color::DARK_RED)
.with_children(|ui| {
Text::new("+")
.add_child(ui);
})
.into_interactable()
.on_click(|| {
println!("clicked");
@ -36,5 +65,12 @@ ui_main!(
.add_child(ui);
})
.add_root(ui, size);
ui.process_signals(|sig| {
match sig {
CounterSignal::Increment => *counter += 1,
CounterSignal::Decrement => *counter -= 1,
}
});
}
);

View file

@ -27,6 +27,7 @@ document-features = "0.2"
derive_setters = "0.1"
#smallvec = "1.13"
tinyset = "0.4"
#mopa = "0.2"
[features]
default = ["builtin_elements", "builtin_font", "pixel_perfect_text"]

View file

@ -6,6 +6,7 @@ use crate::{
input::InputCtx,
layout::LayoutInfo,
measure::Response,
signal::SignalCtx,
state::StateRepo,
text::{FontHandle, TextMeasure},
UiInstance,
@ -35,6 +36,7 @@ pub struct ProcessContext<'a> {
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
pub input: InputCtx<'a>,
//pub signal: SignalCtx<'a>,
}
pub trait UiElement {

View file

@ -31,3 +31,4 @@ pub mod interactable;
//TODO add: Image
//TODO add: OverlayContainer (for simply laying multiple elements on top of each other)
//TODO add: Button, Checkbox, Dropdown, Input, Radio, Slider, Textarea, Toggle, etc.
//TODO add: some sort of "flexible" container (like a poor man's flexbox)

View file

@ -3,24 +3,29 @@
// not sure if this is a good idea...
// but having the ability to add a click event to any element would be nice, and this is a naive way to do it
use crate::element::{UiElement, MeasureContext, ProcessContext};
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
signal::{DummySignal, UiSignal},
};
use std::cell::RefCell;
/// Wrapper that allows adding click and hover events to any element
pub struct Interactable {
pub struct Interactable<H: UiSignal + 'static = DummySignal, C: UiSignal + 'static = DummySignal> {
/// The wrapped element that will be interactable
pub element: Box<dyn UiElement>,
/// Function that will be called if the element is hovered in the current frame
/// Signal that will be called if the element is hovered in the current frame
///
/// Will be consumed after the first time it's called
pub hovered: RefCell<Option<Box<dyn FnOnce()>>>,
/// Function that will be called if the element was clicked in the current frame
pub hovered: RefCell<Option<H>>,
/// Signal that will be called if the element was clicked in the current frame
///
/// Will be consumed after the first time it's called
pub clicked: RefCell<Option<Box<dyn FnOnce()>>>,
pub clicked: RefCell<Option<C>>,
}
impl Interactable {
impl<H: UiSignal, C: UiSignal> Interactable<H, C> {
pub fn new(element: Box<dyn UiElement>) -> Self {
Self {
element,
@ -29,16 +34,16 @@ impl Interactable {
}
}
pub fn on_click(self, clicked: impl FnOnce() + 'static) -> Self {
pub fn on_hover(self, hover: H) -> Self {
Self {
clicked: RefCell::new(Some(Box::new(clicked))),
hovered: RefCell::new(Some(hover)),
..self
}
}
pub fn on_hover(self, clicked: impl FnOnce() + 'static) -> Self {
pub fn on_click(self, clicked: C) -> Self {
Self {
clicked: RefCell::new(Some(Box::new(clicked))),
clicked: RefCell::new(Some(clicked)),
..self
}
}
@ -59,9 +64,9 @@ impl UiElement for Interactable {
//XXX: should we do this AFTER normal process call of wrapped element?
//TODO other events...
if ctx.input.check_click(rect) {
//TODO better error message
let clicked = self.clicked.borrow_mut().take().expect("you fucked up");
clicked();
if let Some(sig) = self.clicked.take() {
//ctx.signal.push(sig);
}
}
self.element.process(ctx)

View file

@ -1,8 +1,16 @@
use glam::Vec2;
use crate::{
draw::{
atlas::{TextureAtlasManager, TextureAtlasMeta}, TextureFormat, ImageHandle, UiDrawCall, UiDrawCommandList
}, element::{MeasureContext, ProcessContext, UiElement}, event::{EventQueue, UiEvent}, input::UiInputState, layout::{LayoutInfo, Direction}, state::StateRepo, text::{FontHandle, TextRenderer}
ImageHandle, TextureFormat, UiDrawCall, UiDrawCommandList,
atlas::{TextureAtlasManager, TextureAtlasMeta},
},
element::{MeasureContext, ProcessContext, UiElement},
event::{EventQueue, UiEvent},
input::UiInputState,
layout::{Direction, LayoutInfo},
signal::{SigIntStore, UiSignal},
state::StateRepo,
text::{FontHandle, TextRenderer}
};
/// The main instance of the UI system.
@ -21,6 +29,7 @@ pub struct UiInstance {
atlas: TextureAtlasManager,
events: EventQueue,
input: UiInputState,
signal: SigIntStore,
//True if in the middle of a laying out a frame
state: bool,
}
@ -48,6 +57,7 @@ impl UiInstance {
},
events: EventQueue::new(),
input: UiInputState::new(),
signal: SigIntStore::new(),
state: false,
}
}
@ -228,6 +238,18 @@ impl UiInstance {
}
self.events.push(event);
}
/// Push a "fake" signal to the UI signal queue
pub fn push_signal<T: UiSignal + 'static>(&mut self, signal: T) {
self.signal.add(signal);
}
/// Process all signals of a given type
///
/// This clears the signal queue for the given type and iterates over all signals
pub fn process_signals<T: UiSignal + 'static>(&mut self, f: impl FnMut(T)) {
self.signal.drain::<T>().for_each(f);
}
}
impl Default for UiInstance {

View file

@ -22,5 +22,6 @@ pub mod measure;
pub mod state;
pub mod text;
pub mod color;
pub mod signal;
pub use instance::UiInstance;

62
hui/src/signal.rs Normal file
View file

@ -0,0 +1,62 @@
use std::any::{Any, TypeId};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
/// A marker trait for signals
pub trait UiSignal: Any {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub(crate) struct DummySignal;
impl UiSignal for DummySignal {}
pub(crate) struct SigIntStore {
///XXX: is this truly the most efficient structure?
sig: HashMap<TypeId, Vec<Box<dyn Any>>, BuildNoHashHasher<u64>>
}
impl SigIntStore {
/// Create a new [`SigIntStore`]
pub fn new() -> Self {
Self {
sig: Default::default(),
}
}
/// Ensure that store for given signal type exists and return a mutable reference to it
fn internal_store<T: UiSignal + 'static>(&mut self) -> &mut Vec<Box<dyn Any>> {
let type_id = TypeId::of::<T>();
self.sig.entry(type_id).or_default()
}
/// Add a signal to the store
///
/// Signals are stored in the order they are added
pub fn add<T: UiSignal + 'static>(&mut self, sig: T) {
let type_id = TypeId::of::<T>();
if let Some(v) = self.sig.get_mut(&type_id) {
v.push(Box::new(sig));
} else {
self.sig.insert(type_id, vec![Box::new(sig)]);
}
}
/// Drain all signals of a given type
pub fn drain<T: UiSignal + 'static>(&mut self) -> impl Iterator<Item = T> + '_ {
self.internal_store::<T>()
.drain(..)
.map(|x| *x.downcast::<T>().unwrap()) //unchecked?
}
pub fn ctx(&mut self) -> SignalCtx {
SignalCtx(self)
}
}
pub struct SignalCtx<'a>(&'a mut SigIntStore);
impl<'a> SignalCtx<'a> {
/// Add a signal to the store
pub fn push<T: UiSignal + 'static>(&mut self, sig: T) {
self.0.add(sig);
}
}