From 7b21b0714d8c3f710e759976b58ea5854746dbc1 Mon Sep 17 00:00:00 2001 From: shadow cat Date: Mon, 25 Aug 2025 16:12:49 -0400 Subject: [PATCH] static ids --- src/layout/id.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++-- src/layout/ui.rs | 59 +++++++++++++++++++++++++---- src/testing/mod.rs | 36 +++++++----------- src/util/id.rs | 16 ++++++++ 4 files changed, 172 insertions(+), 33 deletions(-) diff --git a/src/layout/id.rs b/src/layout/id.rs index 0d6006e..6d01c7d 100644 --- a/src/layout/id.rs +++ b/src/layout/id.rs @@ -2,7 +2,7 @@ use std::{any::TypeId, marker::PhantomData, sync::mpsc::Sender}; use crate::{ layout::{FnTag, Ui, Widget, WidgetLike, WidgetTag}, - util::{Id, RefCounter}, + util::{CopyId, Id, RefCounter}, }; pub struct AnyWidget; @@ -13,12 +13,29 @@ 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. +/// +/// TODO: ergonomic clones when they get put in rust-analyzer & don't cause ICEs? #[repr(C)] pub struct WidgetId { pub(super) ty: TypeId, pub(super) id: Id, counter: RefCounter, send: Sender, + is_static: bool, + _pd: PhantomData, +} + +/// A WidgetId for a static widget that cannot be removed from a Ui. +/// Useful because ergonomic clones don't exist yet so you can easily use these in closures. +/// Do not use this if you want the widget to be freeable. +/// +/// This is currently not perfectly efficient and just creates new WidgetIds every time it's used, +/// but they don't send drop messages to Ui. +/// Ideally I'd have an enum or something that lets you use either, but that doesn't seem worth it +/// right now; it's good enough and relatively cheap. +pub struct StaticWidgetId { + pub(super) ty: TypeId, + pub(super) id: CopyId, _pd: PhantomData, } @@ -35,18 +52,20 @@ impl Clone for WidgetId { ty: self.ty, counter: self.counter.clone(), send: self.send.clone(), + is_static: self.is_static, _pd: PhantomData, } } } impl WidgetId { - pub(super) fn new(id: Id, ty: TypeId, send: Sender) -> Self { + pub(super) fn new(id: Id, ty: TypeId, send: Sender, is_static: bool) -> Self { Self { ty, id, counter: RefCounter::new(), send, + is_static, _pd: PhantomData, } } @@ -68,11 +87,30 @@ impl WidgetId { pub fn refs(&self) -> u32 { self.counter.refs() } + + pub(super) fn into_static(self) -> StaticWidgetId { + let s = std::mem::ManuallyDrop::new(self); + StaticWidgetId { + ty: s.ty, + id: s.id.copyable(), + _pd: PhantomData, + } + } +} + +impl WidgetId { + pub fn set_static(&mut self, other: StaticWidgetId) { + let send = self.send.clone(); + drop(std::mem::replace( + self, + Self::new(other.id.id(), self.ty, send, true), + )); + } } impl Drop for WidgetId { fn drop(&mut self) { - if self.counter.drop() { + if self.counter.drop() && !self.is_static { let _ = self.send.send(self.id.duplicate()); } } @@ -108,6 +146,19 @@ pub trait Idable { id } } + fn id_static<'a>( + self, + id: StaticWidgetId, + ) -> WidgetIdFnRet!(Self::Widget, Ctx, 'a, Self, Ctx, Tag) + where + Self: Sized, + { + move |ui| { + let id = id.id(&ui.send); + self.set(ui, &id); + id + } + } } impl, Ctx> Idable for W { @@ -140,3 +191,40 @@ impl) -> WidgetId, Ctx> WidgetLike StaticWidgetId { + pub fn to_id(&self, send: &Sender) -> WidgetId { + WidgetId::new(self.id.id(), self.ty, send.clone(), true) + } +} + +impl WidgetLike for StaticWidgetId { + type Widget = W; + fn add(self, ui: &mut Ui) -> WidgetId { + self.id(&ui.send) + } +} + +impl Clone for StaticWidgetId { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StaticWidgetId {} + +pub trait IdLike { + fn id(self, send: &Sender) -> WidgetId; +} + +impl IdLike for &WidgetId { + fn id(self, _: &Sender) -> WidgetId { + self.clone() + } +} + +impl IdLike for StaticWidgetId { + fn id(self, send: &Sender) -> WidgetId { + self.to_id(send) + } +} diff --git a/src/layout/ui.rs b/src/layout/ui.rs index 57d44e4..840a4e5 100644 --- a/src/layout/ui.rs +++ b/src/layout/ui.rs @@ -2,8 +2,8 @@ use image::DynamicImage; use crate::{ layout::{ - ActiveSensors, Painter, SensorMap, TextData, TextureHandle, Textures, UiRegion, Vec2, - Widget, WidgetId, WidgetLike, + ActiveSensors, Painter, SensorMap, StaticWidgetId, TextData, TextureHandle, Textures, + UiRegion, Vec2, Widget, WidgetId, WidgetLike, }, render::Primitives, util::{HashMap, Id, IdTracker}, @@ -18,9 +18,9 @@ pub struct Ui { base: Option, widgets: Widgets, labels: HashMap, - updates: Vec, + updates: Vec, recv: Receiver, - send: Sender, + pub(super) send: Sender, size: Vec2, // TODO: make these non pub(crate) pub(crate) primitives: Primitives, @@ -46,6 +46,14 @@ impl Ui { w.add(self) } + pub fn add_static, Tag>( + &mut self, + w: impl WidgetLike, + ) -> StaticWidgetId { + let id = w.add(self); + id.into_static() + } + /// useful for debugging pub fn set_label(&mut self, id: &WidgetId, label: String) { self.labels.insert(id.id.duplicate(), label); @@ -86,7 +94,17 @@ impl Ui { } pub fn id>(&mut self) -> WidgetId { - WidgetId::new(self.widgets.reserve(), TypeId::of::(), self.send.clone()) + WidgetId::new( + self.widgets.reserve(), + TypeId::of::(), + self.send.clone(), + false, + ) + } + + pub fn id_static>(&mut self) -> StaticWidgetId { + let id = self.id(); + id.into_static() } pub fn add_texture(&mut self, image: DynamicImage) -> TextureHandle { @@ -148,7 +166,7 @@ impl Ui { self.size, ); for id in self.updates.drain(..) { - painter.redraw(&id.id); + painter.redraw(&id); } self.free(); } @@ -181,11 +199,26 @@ impl, Ctx> Index<&WidgetId> for Ui { impl, Ctx> IndexMut<&WidgetId> for Ui { fn index_mut(&mut self, id: &WidgetId) -> &mut Self::Output { - self.updates.push(id.clone().erase_type()); + self.updates.push(id.id.duplicate()); self.get_mut(id).unwrap() } } +impl, Ctx> Index> for Ui { + type Output = W; + + fn index(&self, id: StaticWidgetId) -> &Self::Output { + self.widgets.get_static(&id).unwrap() + } +} + +impl, Ctx> IndexMut> for Ui { + fn index_mut(&mut self, id: StaticWidgetId) -> &mut Self::Output { + self.updates.push(id.id.id()); + self.widgets.get_static_mut(&id).unwrap() + } +} + impl Widgets { pub fn new() -> Self { Self { @@ -198,6 +231,18 @@ impl Widgets { self.map.get(id).unwrap().as_ref() } + pub fn get_static>(&self, id: &StaticWidgetId) -> Option<&W> { + self.map.get(&id.id.id()).unwrap().as_any().downcast_ref() + } + + pub fn get_static_mut>(&mut self, id: &StaticWidgetId) -> Option<&mut W> { + self.map + .get_mut(&id.id.id()) + .unwrap() + .as_any_mut() + .downcast_mut() + } + pub fn get>(&self, id: &WidgetId) -> Option<&W> { self.map.get(&id.id).unwrap().as_any().downcast_ref() } diff --git a/src/testing/mod.rs b/src/testing/mod.rs index 3769173..18c2960 100644 --- a/src/testing/mod.rs +++ b/src/testing/mod.rs @@ -29,14 +29,13 @@ pub struct ClientUi { impl Client { pub fn create_ui() -> (Ui, ClientUi) { let mut ui = Ui::new(); - let span_add = ui.id(); let rect = Rect { color: UiColor::WHITE, radius: 20.0, thickness: 0.0, inner_radius: 0.0, }; - let pad_test = ui.add( + let pad_test = ui.add_static( ( rect.color(UiColor::BLUE), ( @@ -53,7 +52,7 @@ impl Client { ) .span(Dir::RIGHT, [1, 3]), ); - let span_test = ui.add( + let span_test = ui.add_static( ( rect.color(UiColor::GREEN), rect.color(UiColor::ORANGE), @@ -74,20 +73,13 @@ impl Client { ], ), ); - let span_add_test = ui.add(Span::empty(Dir::RIGHT).id(&span_add)); - let main: WidgetId = ui.id(); + let span_add = ui.add_static(Span::empty(Dir::RIGHT)); + let main: StaticWidgetId = ui.id_static(); - fn switch_button( - color: UiColor, - main: &WidgetId, - to: &WidgetId, - label: impl Into, - ) -> impl WidgetLike { - let main = main.clone(); - let to = to.clone().erase_type(); + let switch_button = |color, to, label| { let rect = Rect::new(color) .id_on(Sense::PressStart, move |id, ctx| { - ctx.ui[&main].inner = to.clone(); + ctx.ui[main].inner.set_static(to); ctx.ui[id].color = color.add_rgb(-0.2); }) .edit_on(Sense::HoverStart, move |r, _| { @@ -100,17 +92,16 @@ impl Client { r.color = color; }); (rect, text(label).size(30)).stack() - } + }; let tabs = ui.add( ( - switch_button(UiColor::RED, &main, &pad_test, "pad test"), - switch_button(UiColor::GREEN, &main, &span_test, "span test"), - switch_button(UiColor::BLUE, &main, &span_add_test, "span add test"), + switch_button(UiColor::RED, pad_test, "pad test"), + switch_button(UiColor::GREEN, span_test, "span test"), + switch_button(UiColor::BLUE, span_add, "span add test"), ) .span(Dir::RIGHT, [1, 1, 1]), ); - let s = span_add.clone(); let add_button = Rect::new(Color::LIME) .radius(30) .on(Sense::PressStart, move |ctx| { @@ -118,7 +109,7 @@ impl Client { .ui .add(image(include_bytes!("assets/sungals.png"))) .erase_type(); - ctx.ui[&s].children.push((child, ratio(1))); + ctx.ui[span_add].children.push((child, ratio(1))); }) .region( UiPos::corner(Corner::BotRight) @@ -126,11 +117,10 @@ impl Client { .shifted((-75, -75)), ); - let s = span_add.clone(); let del_button = Rect::new(Color::RED) .radius(30) .on(Sense::PressStart, move |ctx| { - ctx.ui[&s].children.pop(); + ctx.ui[span_add].children.pop(); }) .region( UiPos::corner(Corner::BotLeft) @@ -148,7 +138,7 @@ impl Client { ( tabs.label("tabs"), ( - pad_test.pad(10).id(&main), + pad_test.pad(10).id_static(main), add_button.label("add button"), del_button.label("del button"), info_sect.label("info sect"), diff --git a/src/util/id.rs b/src/util/id.rs index 9b92cac..e07745f 100644 --- a/src/util/id.rs +++ b/src/util/id.rs @@ -37,4 +37,20 @@ impl Id { pub fn duplicate(&self) -> Self { Self(self.0) } + + /// this must be used carefully to make sure + /// all IDs are still valid references. + /// generally should not be used in "user" code + pub fn copyable(&self) -> CopyId { + CopyId(self.0) + } +} + +#[derive(Eq, Hash, PartialEq, Debug, Clone, Copy)] +pub struct CopyId(u64); + +impl CopyId { + pub fn id(&self) -> Id { + Id(self.0) + } }