diff --git a/hui-examples/examples/ui_test_5_input.rs b/hui-examples/examples/ui_test_5_input.rs index c1f24eb..1ae6d37 100644 --- a/hui-examples/examples/ui_test_5_input.rs +++ b/hui-examples/examples/ui_test_5_input.rs @@ -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, + } + }); } ); diff --git a/hui/Cargo.toml b/hui/Cargo.toml index 886a3ae..f656c57 100644 --- a/hui/Cargo.toml +++ b/hui/Cargo.toml @@ -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"] diff --git a/hui/src/element.rs b/hui/src/element.rs index dbcba71..f3f2164 100644 --- a/hui/src/element.rs +++ b/hui/src/element.rs @@ -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 { diff --git a/hui/src/element/builtin.rs b/hui/src/element/builtin.rs index 1724c2c..f650d65 100644 --- a/hui/src/element/builtin.rs +++ b/hui/src/element/builtin.rs @@ -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) diff --git a/hui/src/element/builtin/interactable.rs b/hui/src/element/builtin/interactable.rs index d59a3b9..0f681a5 100644 --- a/hui/src/element/builtin/interactable.rs +++ b/hui/src/element/builtin/interactable.rs @@ -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 { /// The wrapped element that will be interactable pub element: Box, - /// 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>>, - /// Function that will be called if the element was clicked in the current frame + pub hovered: RefCell>, + + /// 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>>, + pub clicked: RefCell>, } -impl Interactable { +impl Interactable { pub fn new(element: Box) -> 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) diff --git a/hui/src/instance.rs b/hui/src/instance.rs index fc6f981..f70491e 100644 --- a/hui/src/instance.rs +++ b/hui/src/instance.rs @@ -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(&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(&mut self, f: impl FnMut(T)) { + self.signal.drain::().for_each(f); + } } impl Default for UiInstance { diff --git a/hui/src/lib.rs b/hui/src/lib.rs index b3b6f12..5f0506a 100644 --- a/hui/src/lib.rs +++ b/hui/src/lib.rs @@ -22,5 +22,6 @@ pub mod measure; pub mod state; pub mod text; pub mod color; +pub mod signal; pub use instance::UiInstance; diff --git a/hui/src/signal.rs b/hui/src/signal.rs new file mode 100644 index 0000000..f6768ec --- /dev/null +++ b/hui/src/signal.rs @@ -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>, BuildNoHashHasher> +} + +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(&mut self) -> &mut Vec> { + let type_id = TypeId::of::(); + 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(&mut self, sig: T) { + let type_id = TypeId::of::(); + 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(&mut self) -> impl Iterator + '_ { + self.internal_store::() + .drain(..) + .map(|x| *x.downcast::().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(&mut self, sig: T) { + self.0.add(sig); + } +}