git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/handle.rs (8.4K)

  1 //! `Handle<W>` — how an app names a widget the [`UiContext`] owns.
  2 //!
  3 //! A handle is a `WidgetId` and the widget's type: `Copy`, cheap, and good for the widget's
  4 //! life. It is not a reference; the widget is reached through the context
  5 //! (`ctx.get_mut(h)`, `ctx[h]`), which is what lets the compiler keep an app's access and the
  6 //! context's own from overlapping. A removed widget's handle resolves to `None`. See
  7 //! `docs/rfc-owning-registry.md`.
  8 //!
  9 //! [`UiContext`]: crate::context::UiContext
 10 
 11 use std::marker::PhantomData;
 12 
 13 use super::WidgetId;
 14 
 15 pub struct Handle<W: ?Sized> {
 16     id: WidgetId,
 17     _w: PhantomData<fn() -> W>,
 18 }
 19 
 20 impl<W: ?Sized> Handle<W> {
 21     /// The handle for the widget registered under `id` — for the context, which made it.
 22     pub(crate) fn from_id(id: WidgetId) -> Self {
 23         Handle { id, _w: PhantomData }
 24     }
 25 
 26     /// A handle that names no widget: it resolves to nothing (`get`, `lend_h`), and
 27     /// indexing the context with it panics. It stands in a handle field of a value built
 28     /// where there is no context — a page state a worker thread fetches, whose widgets the
 29     /// app's own copy keeps — and that field is never read. Ids start at 1, so 0 is no
 30     /// widget's.
 31     pub const fn none() -> Self {
 32         Handle { id: WidgetId(0), _w: PhantomData }
 33     }
 34 
 35     /// The widget's id: what focus, links, popovers and dispatch roots are keyed by.
 36     pub fn id(&self) -> WidgetId {
 37         self.id
 38     }
 39 }
 40 
 41 /// [`Handle::none`], so a state that holds handles can derive `Default` for the copy a
 42 /// worker builds.
 43 impl<W: ?Sized> Default for Handle<W> {
 44     fn default() -> Self {
 45         Self::none()
 46     }
 47 }
 48 
 49 impl<W: ?Sized> Clone for Handle<W> {
 50     fn clone(&self) -> Self {
 51         *self
 52     }
 53 }
 54 impl<W: ?Sized> Copy for Handle<W> {}
 55 
 56 impl<W: ?Sized> PartialEq for Handle<W> {
 57     fn eq(&self, other: &Self) -> bool {
 58         self.id == other.id
 59     }
 60 }
 61 impl<W: ?Sized> Eq for Handle<W> {}
 62 
 63 impl<W: ?Sized> std::hash::Hash for Handle<W> {
 64     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
 65         self.id.hash(state);
 66     }
 67 }
 68 
 69 impl<W: ?Sized> std::fmt::Debug for Handle<W> {
 70     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 71         write!(f, "Handle<{}>({:?})", std::any::type_name::<W>(), self.id)
 72     }
 73 }
 74 
 75 #[cfg(test)]
 76 mod tests {
 77     use std::cell::Cell;
 78     use std::rc::Rc;
 79 
 80     use crate::context::UiContext;
 81     use crate::widget::{ElementState, Event, MouseButton, Slider, Widget, WidgetHost};
 82 
 83     /// A widget that records, each time it handles an event, whether the context would hand
 84     /// it out again from inside that call; and counts its drops.
 85     struct Reacher {
 86         base: Widget,
 87         saw_itself: Option<bool>,
 88         drops: Rc<Cell<u32>>,
 89     }
 90     impl Reacher {
 91         fn new(drops: &Rc<Cell<u32>>) -> Self {
 92             Reacher { base: Widget::new(), saw_itself: None, drops: drops.clone() }
 93         }
 94     }
 95     impl Drop for Reacher {
 96         fn drop(&mut self) {
 97             self.drops.set(self.drops.get() + 1);
 98         }
 99     }
100     impl WidgetHost for Reacher {
101         crate::impl_widget_base!(Reacher);
102         fn handle_event(&mut self, _event: &Event, ctx: &mut UiContext) -> bool {
103             let id = self.base.id();
104             self.saw_itself = Some(ctx.get_widget_mut(id).is_some() || ctx.lend(id, |_, _| ()).is_some());
105             true
106         }
107     }
108 
109     /// A widget the context owns is reached by its handle, changed through it, given back by
110     /// value, and its handle then resolves to nothing; dropping the context drops the rest.
111     #[test]
112     fn an_owned_widget_is_reached_by_handle_and_given_back() {
113         let drops = Rc::new(Cell::new(0));
114         let mut ctx = UiContext::new();
115         let a = ctx.insert(Reacher::new(&drops));
116         let b = ctx.insert(Reacher::new(&drops));
117         assert!(ctx.tree.is_registered(a.id()), "registered under its own id");
118         ctx[a].saw_itself = Some(false);
119         assert_eq!(ctx.get(a).and_then(|w| w.saw_itself), Some(false));
120         let back = ctx.remove(a).expect("given back");
121         assert_eq!(back.saw_itself, Some(false));
122         assert!(ctx.get(a).is_none() && !ctx.tree.is_registered(a.id()), "a removed handle names nothing");
123         drop(back);
124         assert_eq!(drops.get(), 1);
125         drop(ctx);
126         assert_eq!(drops.get(), 2, "the context drops what it owns");
127         let _ = b;
128     }
129 
130     /// Routed dispatch reaches an owned widget exactly as a registered one: a press arms the
131     /// drag, a move past the slop drags the slider.
132     #[test]
133     fn routed_dispatch_reaches_an_owned_widget() {
134         let mut ctx = UiContext::new();
135         let h = ctx.insert(Slider::new());
136         WidgetHost::set_rect(&mut ctx[h], 0.0, 0.0, 200.0, 30.0);
137         let press = Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, x: 100.0, y: 15.0, local_x: 100.0, local_y: 15.0 };
138         assert!(ctx.propagate_event(&press, h.id()));
139         assert!(ctx[h].is_dragging());
140         let release = Event::MouseButton { button: MouseButton::Left, state: ElementState::Released, x: 150.0, y: 15.0, local_x: 150.0, local_y: 15.0 };
141         ctx.propagate_event(&release, h.id());
142         assert!(!ctx[h].is_dragging());
143     }
144 
145     /// While the context is inside a widget it has that widget out on loan: the widget
146     /// reaching for itself through the context finds nothing — a miss, not a second `&mut`.
147     /// Afterwards it is back.
148     #[test]
149     fn a_lent_widget_is_not_reachable_until_it_is_back() {
150         let drops = Rc::new(Cell::new(0));
151         let mut ctx = UiContext::new();
152         let h = ctx.insert(Reacher::new(&drops));
153         let ev = Event::FocusIn;
154         assert!(ctx.propagate_event(&ev, h.id()));
155         assert_eq!(ctx[h].saw_itself, Some(false), "out on loan while handling");
156         assert!(ctx.get(h).is_some() && ctx.lend(h.id(), |_, _| ()).is_some(), "back afterwards");
157     }
158 
159     /// A handle that names no widget resolves to nothing, and never to a widget the
160     /// context owns.
161     #[test]
162     fn a_none_handle_names_nothing() {
163         let mut ctx = UiContext::new();
164         let h = ctx.insert(Slider::new());
165         let none: super::Handle<crate::widget::Adapted<Slider>> = Default::default();
166         assert_ne!(none, h);
167         assert!(ctx.get(none).is_none() && ctx.lend_h(none, |_, _| ()).is_none());
168         assert!(ctx.get(h).is_some());
169     }
170 
171     /// A widget that animates is ticked by the context once it is inserted, and leaves the
172     /// tick list when it is removed. Without it an inserted tree list never applied its
173     /// search (it does so in its tick).
174     #[test]
175     fn an_inserted_widget_that_ticks_is_ticked() {
176         let mut ctx = UiContext::new();
177         let h = ctx.insert(crate::widget::TextBox::new(String::new()));
178         assert!(ctx.tick_receivers.contains(&h.id()), "a text box ticks");
179         let still = ctx.insert(Slider::new());
180         assert!(!ctx.tick_receivers.contains(&still.id()), "a slider does not");
181         ctx.remove(h);
182         assert!(!ctx.tick_receivers.contains(&h.id()), "removed, it leaves the tick list");
183     }
184 
185     /// An app that rebuilds its links every frame (`clear_hierarchy`) does not hand back the
186     /// widgets the context owns by doing so.
187     #[test]
188     fn clearing_the_hierarchy_keeps_owned_widgets() {
189         let drops = Rc::new(Cell::new(0));
190         let mut ctx = UiContext::new();
191         let parent = ctx.insert(Reacher::new(&drops));
192         let child = ctx.insert(Reacher::new(&drops));
193         ctx.link_ids(parent.id(), child.id());
194         ctx.clear_hierarchy();
195         assert!(ctx.get(parent).is_some() && ctx.get(child).is_some());
196         assert!(ctx.tree.child_ids(parent.id()).is_empty(), "the links go");
197         assert_eq!(drops.get(), 0);
198     }
199 
200     /// The app's access through its handle and the context's dispatch take turns on one
201     /// widget, under Miri's aliasing models too (CI runs this module under Miri).
202     #[test]
203     fn an_app_and_the_context_take_turns_through_a_handle() {
204         let drops = Rc::new(Cell::new(0));
205         let mut ctx = UiContext::new();
206         let h = ctx.insert(Reacher::new(&drops));
207         for _ in 0..3 {
208             ctx[h].saw_itself = None;
209             ctx.propagate_event(&Event::FocusIn, h.id());
210             let w = ctx.get_mut(h).unwrap();
211             assert_eq!(w.saw_itself, Some(false));
212             w.saw_itself = None;
213             ctx.set_focused_id(h.id());
214             ctx.clear_focus();
215         }
216     }
217 }