From a7dfacb83e05a8e9e184d1099191a37a8c78fe53 Mon Sep 17 00:00:00 2001 From: Shadow Cat Date: Fri, 15 Aug 2025 21:42:35 -0400 Subject: [PATCH] REAL SENSORS --- src/core/sense.rs | 84 ++++++++++++++++++++----------------------- src/layout/color.rs | 55 ++++++++++++++++++++++++++-- src/layout/mod.rs | 2 ++ src/layout/painter.rs | 35 +++++++++++++----- src/layout/region.rs | 1 + src/layout/sense.rs | 44 +++++++++++++++++++++++ src/layout/ui.rs | 64 ++++++++++++++++----------------- src/layout/widget.rs | 50 +++++++++++++++++--------- src/testing/input.rs | 27 +++++++------- src/testing/mod.rs | 18 +++++++--- 10 files changed, 257 insertions(+), 123 deletions(-) create mode 100644 src/layout/sense.rs diff --git a/src/core/sense.rs b/src/core/sense.rs index 09379da..f47162d 100644 --- a/src/core/sense.rs +++ b/src/core/sense.rs @@ -1,58 +1,52 @@ -use crate::{ - Painter, Sense, SenseFn, SenseShape, SenseTrigger, Ui, Widget, WidgetFn, WidgetId, WidgetLike, -}; - -pub struct Sensor { - inner: WidgetId, - sense: Sense, - f: Box>, -} - -impl Widget for Sensor { - fn draw(&self, painter: &mut Painter) { - painter.sense( - SenseTrigger { - shape: painter.region, - sense: self.sense, - }, - self.f.box_clone(), - ); - painter.draw(self.inner.as_any()); - } -} +use crate::{Sense, SenseFn, SenseTrigger, Sensor, Ui, Widget, WidgetIdFn, WidgetLike}; pub trait SenseCtx: 'static { fn active(&mut self, trigger: &SenseTrigger) -> bool; } -pub trait WidgetSenseFn, Ctx> = Fn(&WidgetId, &mut Ui, &mut Ctx) + 'static; - -pub trait Sensable, Ctx: 'static, Tag> { - // copied here so LSP can at least get the UI and id - fn sense + Clone>( +pub trait Sensable { + fn sense( self, sense: Sense, - f: F, - ) -> impl WidgetFn, Ctx>; + // trait copied here bc rust analyzer skill issue + f: impl Fn(&mut Ui, &mut Ctx) + 'static + Clone, + ) -> impl WidgetIdFn; + fn sense_and_edit( + self, + sense: Sense, + // trait copied here bc rust analyzer skill issue + f: impl Fn(&mut W, &mut Ctx) + 'static + Clone, + ) -> impl WidgetIdFn + where + W: Widget; } -impl, Ctx: SenseCtx, Tag> Sensable for W -where - W::Widget: Widget, -{ - fn sense + Clone>( - self, - sense: Sense, - f: F, - ) -> impl WidgetFn, Ctx> { +impl, Ctx: SenseCtx, Tag> Sensable for W { + fn sense(self, sense: Sense, f: impl SenseFn + Clone) -> impl WidgetIdFn { move |ui| { - let inner_arg = self.add(ui); - let inner = inner_arg.clone().erase_type(); - Sensor { - inner, - sense, - f: Box::new(move |ui, ctx| (f)(&inner_arg, ui, ctx)), - } + let id = self.add(ui); + ui.add_sensor( + &id, + Sensor { + sense, + f: Box::new(f), + }, + ); + id } } + fn sense_and_edit( + self, + sense: Sense, + // trait copied here bc rust analyzer skill issue + f: impl Fn(&mut W::Widget, &mut Ctx) + 'static + Clone, + ) -> impl WidgetIdFn + where + W::Widget: Widget, + { + self.with_id(move |ui, id| { + let id2 = id.clone(); + ui.add(id.sense(sense, move |ui, ctx| f(&mut ui[&id2], ctx))) + }) + } } diff --git a/src/layout/color.rs b/src/layout/color.rs index 674892c..5909711 100644 --- a/src/layout/color.rs +++ b/src/layout/color.rs @@ -1,7 +1,7 @@ #![allow(clippy::multiple_bound_locations)] #[repr(C)] -#[derive(Clone, Copy, bytemuck::Zeroable)] +#[derive(Clone, Copy, bytemuck::Zeroable, Debug)] pub struct Color { pub r: T, pub g: T, @@ -20,10 +20,9 @@ impl Color { pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN); pub const CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX); pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, T::MAX); + pub const INDIGO: Self = Self::rgb(T::MIN, T::MID, T::MAX); pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX); -} -impl Color { pub const fn new(r: T, g: T, b: T, a: T) -> Self { Self { r, g, b, a } } @@ -48,4 +47,54 @@ impl ColorNum for u8 { const MAX: Self = u8::MAX; } +impl ColorNum for f32 { + const MIN: Self = 0.0; + const MID: Self = 0.5; + const MAX: Self = 1.0; +} + unsafe impl bytemuck::Pod for Color {} + +pub trait F32Conversion { + fn to(self) -> f32; + fn from(x: f32) -> Self; +} + +impl Color { + pub fn mul_rgb(self, x: impl F32Conversion) -> Self { + let x = x.to(); + Self { + r: T::from(self.r.to() * x), + g: T::from(self.g.to() * x), + b: T::from(self.b.to() * x), + a: self.a, + } + } + pub fn add_rgb(self, x: impl F32Conversion) -> Self { + let x = x.to(); + Self { + r: T::from(self.r.to() + x), + g: T::from(self.g.to() + x), + b: T::from(self.b.to() + x), + a: self.a, + } + } +} + +impl F32Conversion for f32 { + fn to(self) -> f32 { + self + } + fn from(x: f32) -> Self { + x + } +} + +impl F32Conversion for u8 { + fn to(self) -> f32 { + self as f32 / 255.0 + } + fn from(x: f32) -> Self { + (x * 255.0).clamp(0.0, 255.0) as Self + } +} diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 6891c09..0d1f9da 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -1,6 +1,7 @@ mod color; mod painter; mod region; +mod sense; mod ui; mod vec2; mod widget; @@ -8,6 +9,7 @@ mod widget; pub use color::*; pub use painter::*; pub use region::*; +pub use sense::*; pub use ui::*; pub use vec2::*; pub use widget::*; diff --git a/src/layout/painter.rs b/src/layout/painter.rs index e92e05f..a5ed1ba 100644 --- a/src/layout/painter.rs +++ b/src/layout/painter.rs @@ -1,22 +1,29 @@ use crate::{ - SenseFn, SenseTrigger, Sensors, UiRegion, WidgetId, Widgets, + ActiveSensor, ActiveSensors, SenseTrigger, SensorMap, UiRegion, WidgetId, Widgets, primitive::{PrimitiveData, PrimitiveInstance, Primitives}, }; -pub struct Painter<'a, Ctx> { +pub struct Painter<'a, Ctx: 'static> { nodes: &'a Widgets, ctx: &'a mut Ctx, - sensors: &'a mut Sensors, + sensors_map: &'a mut SensorMap, + active_sensors: &'a mut ActiveSensors, primitives: Primitives, pub region: UiRegion, } impl<'a, Ctx> Painter<'a, Ctx> { - pub fn new(nodes: &'a Widgets, ctx: &'a mut Ctx, sensors: &'a mut Sensors) -> Self { + pub fn new( + nodes: &'a Widgets, + ctx: &'a mut Ctx, + sensors_map: &'a mut SensorMap, + active_sensors: &'a mut ActiveSensors, + ) -> Self { Self { nodes, ctx, - sensors, + active_sensors, + sensors_map, primitives: Primitives::default(), region: UiRegion::full(), } @@ -37,6 +44,20 @@ impl<'a, Ctx> Painter<'a, Ctx> { where Ctx: 'static, { + if let Some(sensors) = self.sensors_map.get(&id.id) { + self.active_sensors.push( + sensors + .iter() + .map(|sensor| ActiveSensor { + trigger: SenseTrigger { + shape: self.region, + sense: sensor.sense, + }, + f: sensor.f.box_clone(), + }) + .collect(), + ); + } self.nodes.get_dyn(id).draw(self); } @@ -50,10 +71,6 @@ impl<'a, Ctx> Painter<'a, Ctx> { self.region = old; } - pub fn sense(&mut self, trigger: SenseTrigger, f: Box>) { - self.sensors.push((trigger, f)); - } - pub fn finish(self) -> Primitives { self.primitives } diff --git a/src/layout/region.rs b/src/layout/region.rs index bc0e275..b7431b5 100644 --- a/src/layout/region.rs +++ b/src/layout/region.rs @@ -158,6 +158,7 @@ impl UiRegion { } } +#[derive(Debug)] pub struct ScreenRect { top_left: Vec2, bot_right: Vec2, diff --git a/src/layout/sense.rs b/src/layout/sense.rs new file mode 100644 index 0000000..2ac26ab --- /dev/null +++ b/src/layout/sense.rs @@ -0,0 +1,44 @@ +use crate::{HashMap, Ui, UiRegion, util::Id}; + +#[derive(Clone, Copy, PartialEq)] +pub enum Sense { + Press, + Held, + Hover, + NoHover, +} + +pub struct Sensor { + pub sense: Sense, + pub f: Box>, +} + +pub struct ActiveSensor { + pub trigger: SenseTrigger, + pub f: Box>, +} +impl Clone for ActiveSensor { + fn clone(&self) -> Self { + Self { + trigger: self.trigger.clone(), + f: self.f.box_clone(), + } + } +} +pub type SensorMap = HashMap>>; +pub type ActiveSensors = Vec>>; +pub trait SenseFn_ = FnMut(&mut Ui, &mut Ctx) + 'static; +pub type SenseShape = UiRegion; +#[derive(Clone)] +pub struct SenseTrigger { + pub shape: SenseShape, + pub sense: Sense, +} +pub trait SenseFn: SenseFn_ { + fn box_clone(&self) -> Box>; +} +impl + Clone, Ctx> SenseFn for F { + fn box_clone(&self) -> Box> { + Box::new(self.clone()) + } +} diff --git a/src/layout/ui.rs b/src/layout/ui.rs index 250e7d7..0e60e8f 100644 --- a/src/layout/ui.rs +++ b/src/layout/ui.rs @@ -1,5 +1,5 @@ use crate::{ - HashMap, Painter, SenseCtx, UiRegion, Widget, WidgetId, WidgetLike, + ActiveSensors, HashMap, Painter, SenseCtx, Sensor, SensorMap, Widget, WidgetId, WidgetLike, primitive::Primitives, util::{IDTracker, Id}, }; @@ -13,7 +13,8 @@ pub struct Ui { base: Option, widgets: Widgets, updates: Vec, - sensors: Sensors, + active_sensors: ActiveSensors, + sensor_map: SensorMap, primitives: Primitives, full_redraw: bool, } @@ -21,27 +22,6 @@ pub struct Ui { #[derive(Default)] pub struct Widgets(HashMap>>); -#[derive(Clone, Copy)] -pub enum Sense { - Click, - Hover, -} -pub type Sensors = Vec<(SenseTrigger, Box>)>; -pub trait SenseFn_ = Fn(&mut Ui, &mut Ctx) + 'static; -pub type SenseShape = UiRegion; -pub struct SenseTrigger { - pub shape: SenseShape, - pub sense: Sense, -} -pub trait SenseFn: SenseFn_ { - fn box_clone(&self) -> Box>; -} -impl + Clone, Ctx> SenseFn for F { - fn box_clone(&self) -> Box> { - Box::new(self.clone()) - } -} - impl Ui { pub fn add, Tag>( &mut self, @@ -69,7 +49,10 @@ impl Ui { self.full_redraw = true; } - pub fn new() -> Self { + pub fn new() -> Self + where + Ctx: 'static, + { Self::default() } @@ -89,23 +72,35 @@ impl Ui { where Ctx: 'static, { - self.sensors.clear(); - let mut painter = Painter::new(&self.widgets, ctx, &mut self.sensors); + self.active_sensors.clear(); + let mut painter = Painter::new( + &self.widgets, + ctx, + &mut self.sensor_map, + &mut self.active_sensors, + ); if let Some(base) = &self.base { painter.draw(base); } self.primitives = painter.finish(); } + pub fn add_sensor(&mut self, id: &WidgetId, f: Sensor) { + self.sensor_map + .entry(id.id.duplicate()) + .or_default() + .push(f); + } + pub fn run_sensors(&mut self, ctx: &mut Ctx) where - Ctx: SenseCtx, + Ctx: SenseCtx + 'static, { - for (t, f) in self.sensors.iter().rev() { - if ctx.active(t) { - let f = f.as_ref().box_clone(); - f(self, ctx); - return; + for sensors in self.active_sensors.clone().iter().rev() { + for sensor in sensors { + if ctx.active(&sensor.trigger) { + (sensor.f.box_clone())(self, ctx); + } } } } @@ -183,7 +178,7 @@ impl dyn Widget { } } -impl Default for Ui { +impl Default for Ui { fn default() -> Self { Self { ids: Default::default(), @@ -192,7 +187,8 @@ impl Default for Ui { updates: Default::default(), primitives: Default::default(), full_redraw: false, - sensors: Default::default(), + active_sensors: Default::default(), + sensor_map: Default::default(), } } } diff --git a/src/layout/widget.rs b/src/layout/widget.rs index 81ccc90..32abdd5 100644 --- a/src/layout/widget.rs +++ b/src/layout/widget.rs @@ -17,13 +17,19 @@ pub struct AnyWidget; /// W does not need to implement widget so that AnyWidget is valid; /// Instead, add generic bounds on methods that take an ID if they need specific data. #[repr(C)] -#[derive(Eq, Hash, PartialEq, Debug)] +#[derive(Eq, Hash, PartialEq)] pub struct WidgetId { pub(super) ty: TypeId, pub(super) id: Id, _pd: PhantomData, } +impl std::fmt::Debug for WidgetId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.id.fmt(f) + } +} + // TODO: temp impl Clone for WidgetId { fn clone(&self) -> Self { @@ -63,30 +69,42 @@ pub struct FnTag; pub struct IdTag; pub trait WidgetLike { - type Widget; + type Widget: 'static; fn add(self, ui: &mut Ui) -> WidgetId; + fn with_id( + self, + f: impl FnOnce(&mut Ui, WidgetId) -> WidgetId, + ) -> impl WidgetIdFn + where + Self: Sized, + { + move |ui| { + let id = self.add(ui); + f(ui, id) + } + } } /// A function that returns a widget given a UI. /// Useful for defining trait functions on widgets that create a parent widget so that the children /// don't need to be IDs yet pub trait WidgetFn, Ctx> = FnOnce(&mut Ui) -> W; -pub trait WidgetIdFn, Ctx> = FnOnce(&mut Ui) -> WidgetId; +pub trait WidgetIdFn = FnOnce(&mut Ui) -> WidgetId; pub trait Idable { type Widget: Widget; fn set(self, ui: &mut Ui, id: &WidgetId); -} - -pub trait WidgetFns, Ctx, Tag> { - fn id(self, id: &WidgetId) -> impl WidgetIdFn; -} - -impl, Ctx, Tag> WidgetFns for I { - fn id(self, id: &WidgetId) -> impl WidgetIdFn { - |ui| { - self.set(ui, id); - id.clone() + fn id<'a>( + self, + id: &WidgetId, + ) -> impl WidgetIdFn + use<'a, Self, Ctx, Tag> + where + Self: Sized, + { + let id = id.clone(); + move |ui| { + self.set(ui, &id); + id } } } @@ -116,7 +134,7 @@ impl, Ctx, F: FnOnce(&mut Ui) -> W> WidgetLike f } } -impl, F: FnOnce(&mut Ui) -> WidgetId, Ctx> WidgetLike for F { +impl) -> WidgetId, Ctx> WidgetLike for F { type Widget = W; fn add(self, ui: &mut Ui) -> WidgetId { self(ui) @@ -130,7 +148,7 @@ impl, Ctx> WidgetLike for W { } } -impl, Ctx> WidgetLike for WidgetId { +impl WidgetLike for WidgetId { type Widget = W; fn add(self, _: &mut Ui) -> WidgetId { self diff --git a/src/testing/input.rs b/src/testing/input.rs index da40e31..5d80c04 100644 --- a/src/testing/input.rs +++ b/src/testing/input.rs @@ -1,4 +1,4 @@ -use gui::{SenseCtx, SenseTrigger, Vec2}; +use gui::{Sense, SenseCtx, SenseTrigger, Vec2}; use winit::event::WindowEvent; use crate::testing::Client; @@ -9,28 +9,29 @@ pub struct Input { mouse_pos: Vec2, mouse_pressed: bool, mouse_just_pressed: bool, + mouse_just_released: bool, + mouse_in: bool, } impl Input { pub fn event(&mut self, event: &WindowEvent) { self.mouse_just_pressed = false; + self.mouse_just_released = false; match event { WindowEvent::Resized(size) => { - self.size = Vec2::new(size.width as f32, size.height as f32) + self.size = Vec2::new(size.width as f32, size.height as f32); } WindowEvent::CursorMoved { position, .. } => { - self.mouse_pos = Vec2::new(position.x as f32, position.y as f32) + self.mouse_pos = Vec2::new(position.x as f32, position.y as f32); + self.mouse_in = true; } WindowEvent::MouseInput { state, button, .. } => match button { winit::event::MouseButton::Left => { if state.is_pressed() { - if !self.mouse_pressed { - self.mouse_just_pressed = true; - } else { - self.mouse_just_pressed = false; - } + self.mouse_just_pressed = !self.mouse_pressed; self.mouse_pressed = true; } else { + self.mouse_just_released = self.mouse_pressed; self.mouse_pressed = false; } } @@ -41,12 +42,14 @@ impl Input { } fn active(&mut self, trigger: &SenseTrigger) -> bool { let region = trigger.shape.to_screen(self.size); - if !region.contains(self.mouse_pos) { - return false; + if !self.mouse_in || !region.contains(self.mouse_pos) { + return trigger.sense == Sense::NoHover; } match trigger.sense { - gui::Sense::Click => self.mouse_just_pressed, - gui::Sense::Hover => true, + Sense::Press => self.mouse_just_pressed, + Sense::Held => self.mouse_pressed, + Sense::Hover => true, + Sense::NoHover => false, } } } diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 62fcb1c..14560c6 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -80,12 +80,22 @@ impl Client { color: UiColor, main: &WidgetId, to: &WidgetId, - ) -> impl WidgetLike { + ) -> impl WidgetLike { let main = main.clone(); let to = to.clone().erase_type(); - Rect::new(color).sense(Sense::Click, move |_, ui: &mut Ui, _| { - ui[&main].inner = to.clone(); - }) + Rect::new(color) + .sense(Sense::Press, move |ui, _| { + ui[&main].inner = to.clone(); + }) + .sense_and_edit(Sense::Hover, move |r, _| { + r.color = color.add_rgb(0.1); + }) + .sense_and_edit(Sense::NoHover, move |r, _| { + r.color = color; + }) + .sense_and_edit(Sense::Held, move |r, _| { + r.color = color.add_rgb(-0.1); + }) } let buttons = ui.add(