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 }