REAL SENSORS

This commit is contained in:
2025-08-15 21:42:35 -04:00
parent 9f1802f497
commit a7dfacb83e
10 changed files with 257 additions and 123 deletions

View File

@@ -1,58 +1,52 @@
use crate::{ use crate::{Sense, SenseFn, SenseTrigger, Sensor, Ui, Widget, WidgetIdFn, WidgetLike};
Painter, Sense, SenseFn, SenseShape, SenseTrigger, Ui, Widget, WidgetFn, WidgetId, WidgetLike,
};
pub struct Sensor<Ctx> {
inner: WidgetId,
sense: Sense,
f: Box<dyn SenseFn<Ctx>>,
}
impl<Ctx: 'static> Widget<Ctx> for Sensor<Ctx> {
fn draw(&self, painter: &mut Painter<Ctx>) {
painter.sense(
SenseTrigger {
shape: painter.region,
sense: self.sense,
},
self.f.box_clone(),
);
painter.draw(self.inner.as_any());
}
}
pub trait SenseCtx: 'static { pub trait SenseCtx: 'static {
fn active(&mut self, trigger: &SenseTrigger) -> bool; fn active(&mut self, trigger: &SenseTrigger) -> bool;
} }
pub trait WidgetSenseFn<W: Widget<Ctx>, Ctx> = Fn(&WidgetId<W>, &mut Ui<Ctx>, &mut Ctx) + 'static; pub trait Sensable<W, Ctx: SenseCtx, Tag> {
fn sense(
pub trait Sensable<W: Widget<Ctx>, Ctx: 'static, Tag> {
// copied here so LSP can at least get the UI and id
fn sense<F: WidgetSenseFn<W, Ctx> + Clone>(
self, self,
sense: Sense, sense: Sense,
f: F, // trait copied here bc rust analyzer skill issue
) -> impl WidgetFn<Sensor<Ctx>, Ctx>; f: impl Fn(&mut Ui<Ctx>, &mut Ctx) + 'static + Clone,
) -> impl WidgetIdFn<W, Ctx>;
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<W, Ctx>
where
W: Widget<Ctx>;
} }
impl<W: WidgetLike<Ctx, Tag>, Ctx: SenseCtx, Tag> Sensable<W::Widget, Ctx, Tag> for W impl<W: WidgetLike<Ctx, Tag>, Ctx: SenseCtx, Tag> Sensable<W::Widget, Ctx, Tag> for W {
fn sense(self, sense: Sense, f: impl SenseFn<Ctx> + Clone) -> impl WidgetIdFn<W::Widget, Ctx> {
move |ui| {
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<W::Widget, Ctx>
where where
W::Widget: Widget<Ctx>, W::Widget: Widget<Ctx>,
{ {
fn sense<F: WidgetSenseFn<W::Widget, Ctx> + Clone>( self.with_id(move |ui, id| {
self, let id2 = id.clone();
sense: Sense, ui.add(id.sense(sense, move |ui, ctx| f(&mut ui[&id2], ctx)))
f: F, })
) -> impl WidgetFn<Sensor<Ctx>, Ctx> {
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)),
}
}
} }
} }

View File

@@ -1,7 +1,7 @@
#![allow(clippy::multiple_bound_locations)] #![allow(clippy::multiple_bound_locations)]
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable)] #[derive(Clone, Copy, bytemuck::Zeroable, Debug)]
pub struct Color<T: ColorNum> { pub struct Color<T: ColorNum> {
pub r: T, pub r: T,
pub g: T, pub g: T,
@@ -20,10 +20,9 @@ impl<T: ColorNum> Color<T> {
pub const GREEN: Self = Self::rgb(T::MIN, T::MAX, T::MIN); 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 CYAN: Self = Self::rgb(T::MIN, T::MAX, T::MAX);
pub const BLUE: Self = Self::rgb(T::MIN, T::MIN, 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); pub const MAGENTA: Self = Self::rgb(T::MAX, T::MIN, T::MAX);
}
impl<T: ColorNum> Color<T> {
pub const fn new(r: T, g: T, b: T, a: T) -> Self { pub const fn new(r: T, g: T, b: T, a: T) -> Self {
Self { r, g, b, a } Self { r, g, b, a }
} }
@@ -48,4 +47,54 @@ impl ColorNum for u8 {
const MAX: Self = u8::MAX; 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<u8> {} unsafe impl bytemuck::Pod for Color<u8> {}
pub trait F32Conversion {
fn to(self) -> f32;
fn from(x: f32) -> Self;
}
impl<T: ColorNum + F32Conversion> Color<T> {
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
}
}

View File

@@ -1,6 +1,7 @@
mod color; mod color;
mod painter; mod painter;
mod region; mod region;
mod sense;
mod ui; mod ui;
mod vec2; mod vec2;
mod widget; mod widget;
@@ -8,6 +9,7 @@ mod widget;
pub use color::*; pub use color::*;
pub use painter::*; pub use painter::*;
pub use region::*; pub use region::*;
pub use sense::*;
pub use ui::*; pub use ui::*;
pub use vec2::*; pub use vec2::*;
pub use widget::*; pub use widget::*;

View File

@@ -1,22 +1,29 @@
use crate::{ use crate::{
SenseFn, SenseTrigger, Sensors, UiRegion, WidgetId, Widgets, ActiveSensor, ActiveSensors, SenseTrigger, SensorMap, UiRegion, WidgetId, Widgets,
primitive::{PrimitiveData, PrimitiveInstance, Primitives}, primitive::{PrimitiveData, PrimitiveInstance, Primitives},
}; };
pub struct Painter<'a, Ctx> { pub struct Painter<'a, Ctx: 'static> {
nodes: &'a Widgets<Ctx>, nodes: &'a Widgets<Ctx>,
ctx: &'a mut Ctx, ctx: &'a mut Ctx,
sensors: &'a mut Sensors<Ctx>, sensors_map: &'a mut SensorMap<Ctx>,
active_sensors: &'a mut ActiveSensors<Ctx>,
primitives: Primitives, primitives: Primitives,
pub region: UiRegion, pub region: UiRegion,
} }
impl<'a, Ctx> Painter<'a, Ctx> { impl<'a, Ctx> Painter<'a, Ctx> {
pub fn new(nodes: &'a Widgets<Ctx>, ctx: &'a mut Ctx, sensors: &'a mut Sensors<Ctx>) -> Self { pub fn new(
nodes: &'a Widgets<Ctx>,
ctx: &'a mut Ctx,
sensors_map: &'a mut SensorMap<Ctx>,
active_sensors: &'a mut ActiveSensors<Ctx>,
) -> Self {
Self { Self {
nodes, nodes,
ctx, ctx,
sensors, active_sensors,
sensors_map,
primitives: Primitives::default(), primitives: Primitives::default(),
region: UiRegion::full(), region: UiRegion::full(),
} }
@@ -37,6 +44,20 @@ impl<'a, Ctx> Painter<'a, Ctx> {
where where
Ctx: 'static, 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); self.nodes.get_dyn(id).draw(self);
} }
@@ -50,10 +71,6 @@ impl<'a, Ctx> Painter<'a, Ctx> {
self.region = old; self.region = old;
} }
pub fn sense(&mut self, trigger: SenseTrigger, f: Box<dyn SenseFn<Ctx>>) {
self.sensors.push((trigger, f));
}
pub fn finish(self) -> Primitives { pub fn finish(self) -> Primitives {
self.primitives self.primitives
} }

View File

@@ -158,6 +158,7 @@ impl UiRegion {
} }
} }
#[derive(Debug)]
pub struct ScreenRect { pub struct ScreenRect {
top_left: Vec2, top_left: Vec2,
bot_right: Vec2, bot_right: Vec2,

44
src/layout/sense.rs Normal file
View File

@@ -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<Ctx> {
pub sense: Sense,
pub f: Box<dyn SenseFn<Ctx>>,
}
pub struct ActiveSensor<Ctx> {
pub trigger: SenseTrigger,
pub f: Box<dyn SenseFn<Ctx>>,
}
impl<Ctx: 'static> Clone for ActiveSensor<Ctx> {
fn clone(&self) -> Self {
Self {
trigger: self.trigger.clone(),
f: self.f.box_clone(),
}
}
}
pub type SensorMap<Ctx> = HashMap<Id, Vec<Sensor<Ctx>>>;
pub type ActiveSensors<Ctx> = Vec<Vec<ActiveSensor<Ctx>>>;
pub trait SenseFn_<Ctx> = FnMut(&mut Ui<Ctx>, &mut Ctx) + 'static;
pub type SenseShape = UiRegion;
#[derive(Clone)]
pub struct SenseTrigger {
pub shape: SenseShape,
pub sense: Sense,
}
pub trait SenseFn<Ctx>: SenseFn_<Ctx> {
fn box_clone(&self) -> Box<dyn SenseFn<Ctx>>;
}
impl<F: SenseFn_<Ctx> + Clone, Ctx> SenseFn<Ctx> for F {
fn box_clone(&self) -> Box<dyn SenseFn<Ctx>> {
Box::new(self.clone())
}
}

View File

@@ -1,5 +1,5 @@
use crate::{ use crate::{
HashMap, Painter, SenseCtx, UiRegion, Widget, WidgetId, WidgetLike, ActiveSensors, HashMap, Painter, SenseCtx, Sensor, SensorMap, Widget, WidgetId, WidgetLike,
primitive::Primitives, primitive::Primitives,
util::{IDTracker, Id}, util::{IDTracker, Id},
}; };
@@ -13,7 +13,8 @@ pub struct Ui<Ctx> {
base: Option<WidgetId>, base: Option<WidgetId>,
widgets: Widgets<Ctx>, widgets: Widgets<Ctx>,
updates: Vec<WidgetId>, updates: Vec<WidgetId>,
sensors: Sensors<Ctx>, active_sensors: ActiveSensors<Ctx>,
sensor_map: SensorMap<Ctx>,
primitives: Primitives, primitives: Primitives,
full_redraw: bool, full_redraw: bool,
} }
@@ -21,27 +22,6 @@ pub struct Ui<Ctx> {
#[derive(Default)] #[derive(Default)]
pub struct Widgets<Ctx>(HashMap<Id, Box<dyn Widget<Ctx>>>); pub struct Widgets<Ctx>(HashMap<Id, Box<dyn Widget<Ctx>>>);
#[derive(Clone, Copy)]
pub enum Sense {
Click,
Hover,
}
pub type Sensors<Ctx> = Vec<(SenseTrigger, Box<dyn SenseFn<Ctx>>)>;
pub trait SenseFn_<Ctx> = Fn(&mut Ui<Ctx>, &mut Ctx) + 'static;
pub type SenseShape = UiRegion;
pub struct SenseTrigger {
pub shape: SenseShape,
pub sense: Sense,
}
pub trait SenseFn<Ctx>: SenseFn_<Ctx> {
fn box_clone(&self) -> Box<dyn SenseFn<Ctx>>;
}
impl<F: SenseFn_<Ctx> + Clone, Ctx> SenseFn<Ctx> for F {
fn box_clone(&self) -> Box<dyn SenseFn<Ctx>> {
Box::new(self.clone())
}
}
impl<Ctx> Ui<Ctx> { impl<Ctx> Ui<Ctx> {
pub fn add<W: Widget<Ctx>, Tag>( pub fn add<W: Widget<Ctx>, Tag>(
&mut self, &mut self,
@@ -69,7 +49,10 @@ impl<Ctx> Ui<Ctx> {
self.full_redraw = true; self.full_redraw = true;
} }
pub fn new() -> Self { pub fn new() -> Self
where
Ctx: 'static,
{
Self::default() Self::default()
} }
@@ -89,23 +72,35 @@ impl<Ctx> Ui<Ctx> {
where where
Ctx: 'static, Ctx: 'static,
{ {
self.sensors.clear(); self.active_sensors.clear();
let mut painter = Painter::new(&self.widgets, ctx, &mut self.sensors); let mut painter = Painter::new(
&self.widgets,
ctx,
&mut self.sensor_map,
&mut self.active_sensors,
);
if let Some(base) = &self.base { if let Some(base) = &self.base {
painter.draw(base); painter.draw(base);
} }
self.primitives = painter.finish(); self.primitives = painter.finish();
} }
pub fn add_sensor<W>(&mut self, id: &WidgetId<W>, f: Sensor<Ctx>) {
self.sensor_map
.entry(id.id.duplicate())
.or_default()
.push(f);
}
pub fn run_sensors(&mut self, ctx: &mut Ctx) pub fn run_sensors(&mut self, ctx: &mut Ctx)
where where
Ctx: SenseCtx, Ctx: SenseCtx + 'static,
{ {
for (t, f) in self.sensors.iter().rev() { for sensors in self.active_sensors.clone().iter().rev() {
if ctx.active(t) { for sensor in sensors {
let f = f.as_ref().box_clone(); if ctx.active(&sensor.trigger) {
f(self, ctx); (sensor.f.box_clone())(self, ctx);
return; }
} }
} }
} }
@@ -183,7 +178,7 @@ impl<Ctx> dyn Widget<Ctx> {
} }
} }
impl<Ctx> Default for Ui<Ctx> { impl<Ctx: 'static> Default for Ui<Ctx> {
fn default() -> Self { fn default() -> Self {
Self { Self {
ids: Default::default(), ids: Default::default(),
@@ -192,7 +187,8 @@ impl<Ctx> Default for Ui<Ctx> {
updates: Default::default(), updates: Default::default(),
primitives: Default::default(), primitives: Default::default(),
full_redraw: false, full_redraw: false,
sensors: Default::default(), active_sensors: Default::default(),
sensor_map: Default::default(),
} }
} }
} }

View File

@@ -17,13 +17,19 @@ pub struct AnyWidget;
/// W does not need to implement widget so that AnyWidget is valid; /// 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. /// Instead, add generic bounds on methods that take an ID if they need specific data.
#[repr(C)] #[repr(C)]
#[derive(Eq, Hash, PartialEq, Debug)] #[derive(Eq, Hash, PartialEq)]
pub struct WidgetId<W = AnyWidget> { pub struct WidgetId<W = AnyWidget> {
pub(super) ty: TypeId, pub(super) ty: TypeId,
pub(super) id: Id, pub(super) id: Id,
_pd: PhantomData<W>, _pd: PhantomData<W>,
} }
impl<W> std::fmt::Debug for WidgetId<W> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.id.fmt(f)
}
}
// TODO: temp // TODO: temp
impl<W> Clone for WidgetId<W> { impl<W> Clone for WidgetId<W> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
@@ -63,30 +69,42 @@ pub struct FnTag;
pub struct IdTag; pub struct IdTag;
pub trait WidgetLike<Ctx, Tag> { pub trait WidgetLike<Ctx, Tag> {
type Widget; type Widget: 'static;
fn add(self, ui: &mut Ui<Ctx>) -> WidgetId<Self::Widget>; fn add(self, ui: &mut Ui<Ctx>) -> WidgetId<Self::Widget>;
fn with_id<W2>(
self,
f: impl FnOnce(&mut Ui<Ctx>, WidgetId<Self::Widget>) -> WidgetId<W2>,
) -> impl WidgetIdFn<W2, Ctx>
where
Self: Sized,
{
move |ui| {
let id = self.add(ui);
f(ui, id)
}
}
} }
/// A function that returns a widget given a UI. /// 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 /// Useful for defining trait functions on widgets that create a parent widget so that the children
/// don't need to be IDs yet /// don't need to be IDs yet
pub trait WidgetFn<W: Widget<Ctx>, Ctx> = FnOnce(&mut Ui<Ctx>) -> W; pub trait WidgetFn<W: Widget<Ctx>, Ctx> = FnOnce(&mut Ui<Ctx>) -> W;
pub trait WidgetIdFn<W: Widget<Ctx>, Ctx> = FnOnce(&mut Ui<Ctx>) -> WidgetId<W>; pub trait WidgetIdFn<W, Ctx> = FnOnce(&mut Ui<Ctx>) -> WidgetId<W>;
pub trait Idable<Ctx, Tag> { pub trait Idable<Ctx, Tag> {
type Widget: Widget<Ctx>; type Widget: Widget<Ctx>;
fn set(self, ui: &mut Ui<Ctx>, id: &WidgetId<Self::Widget>); fn set(self, ui: &mut Ui<Ctx>, id: &WidgetId<Self::Widget>);
} fn id<'a>(
self,
pub trait WidgetFns<W: Widget<Ctx>, Ctx, Tag> { id: &WidgetId<Self::Widget>,
fn id(self, id: &WidgetId<W>) -> impl WidgetIdFn<W, Ctx>; ) -> impl WidgetIdFn<Self::Widget, Ctx> + use<'a, Self, Ctx, Tag>
} where
Self: Sized,
impl<I: Idable<Ctx, Tag>, Ctx, Tag> WidgetFns<I::Widget, Ctx, Tag> for I { {
fn id(self, id: &WidgetId<I::Widget>) -> impl WidgetIdFn<I::Widget, Ctx> { let id = id.clone();
|ui| { move |ui| {
self.set(ui, id); self.set(ui, &id);
id.clone() id
} }
} }
} }
@@ -116,7 +134,7 @@ impl<W: Widget<Ctx>, Ctx, F: FnOnce(&mut Ui<Ctx>) -> W> WidgetLike<Ctx, FnTag> f
} }
} }
impl<W: Widget<Ctx>, F: FnOnce(&mut Ui<Ctx>) -> WidgetId<W>, Ctx> WidgetLike<Ctx, IdTag> for F { impl<W: 'static, F: FnOnce(&mut Ui<Ctx>) -> WidgetId<W>, Ctx> WidgetLike<Ctx, IdTag> for F {
type Widget = W; type Widget = W;
fn add(self, ui: &mut Ui<Ctx>) -> WidgetId<W> { fn add(self, ui: &mut Ui<Ctx>) -> WidgetId<W> {
self(ui) self(ui)
@@ -130,7 +148,7 @@ impl<W: Widget<Ctx>, Ctx> WidgetLike<Ctx, WidgetTag> for W {
} }
} }
impl<W: Widget<Ctx>, Ctx> WidgetLike<Ctx, FnTag> for WidgetId<W> { impl<W: 'static, Ctx> WidgetLike<Ctx, FnTag> for WidgetId<W> {
type Widget = W; type Widget = W;
fn add(self, _: &mut Ui<Ctx>) -> WidgetId<W> { fn add(self, _: &mut Ui<Ctx>) -> WidgetId<W> {
self self

View File

@@ -1,4 +1,4 @@
use gui::{SenseCtx, SenseTrigger, Vec2}; use gui::{Sense, SenseCtx, SenseTrigger, Vec2};
use winit::event::WindowEvent; use winit::event::WindowEvent;
use crate::testing::Client; use crate::testing::Client;
@@ -9,28 +9,29 @@ pub struct Input {
mouse_pos: Vec2, mouse_pos: Vec2,
mouse_pressed: bool, mouse_pressed: bool,
mouse_just_pressed: bool, mouse_just_pressed: bool,
mouse_just_released: bool,
mouse_in: bool,
} }
impl Input { impl Input {
pub fn event(&mut self, event: &WindowEvent) { pub fn event(&mut self, event: &WindowEvent) {
self.mouse_just_pressed = false; self.mouse_just_pressed = false;
self.mouse_just_released = false;
match event { match event {
WindowEvent::Resized(size) => { 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, .. } => { 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 { WindowEvent::MouseInput { state, button, .. } => match button {
winit::event::MouseButton::Left => { winit::event::MouseButton::Left => {
if state.is_pressed() { if state.is_pressed() {
if !self.mouse_pressed { self.mouse_just_pressed = !self.mouse_pressed;
self.mouse_just_pressed = true;
} else {
self.mouse_just_pressed = false;
}
self.mouse_pressed = true; self.mouse_pressed = true;
} else { } else {
self.mouse_just_released = self.mouse_pressed;
self.mouse_pressed = false; self.mouse_pressed = false;
} }
} }
@@ -41,12 +42,14 @@ impl Input {
} }
fn active(&mut self, trigger: &SenseTrigger) -> bool { fn active(&mut self, trigger: &SenseTrigger) -> bool {
let region = trigger.shape.to_screen(self.size); let region = trigger.shape.to_screen(self.size);
if !region.contains(self.mouse_pos) { if !self.mouse_in || !region.contains(self.mouse_pos) {
return false; return trigger.sense == Sense::NoHover;
} }
match trigger.sense { match trigger.sense {
gui::Sense::Click => self.mouse_just_pressed, Sense::Press => self.mouse_just_pressed,
gui::Sense::Hover => true, Sense::Held => self.mouse_pressed,
Sense::Hover => true,
Sense::NoHover => false,
} }
} }
} }

View File

@@ -80,12 +80,22 @@ impl Client {
color: UiColor, color: UiColor,
main: &WidgetId<Regioned>, main: &WidgetId<Regioned>,
to: &WidgetId<To>, to: &WidgetId<To>,
) -> impl WidgetLike<Client, FnTag> { ) -> impl WidgetLike<Client, IdTag> {
let main = main.clone(); let main = main.clone();
let to = to.clone().erase_type(); let to = to.clone().erase_type();
Rect::new(color).sense(Sense::Click, move |_, ui: &mut Ui<Client>, _| { Rect::new(color)
.sense(Sense::Press, move |ui, _| {
ui[&main].inner = to.clone(); 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( let buttons = ui.add(