mirror of
https://github.com/griffi-gh/hUI.git
synced 2024-11-25 08:28:42 -06:00
signal stuff...
This commit is contained in:
parent
4b3f15e6ce
commit
01b30c5979
|
@ -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,
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
62
hui/src/signal.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue