egui/interaction.rs
1//! How mouse and touch interzcts with widgets.
2
3use crate::{hit_test, id, input_state, memory, Id, InputState, Key, WidgetRects};
4
5use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState};
6
7/// Calculated at the start of each frame
8/// based on:
9/// * Widget rects from precious frame
10/// * Mouse/touch input
11/// * Current [`InteractionState`].
12#[derive(Clone, Default)]
13pub struct InteractionSnapshot {
14 /// The widget that got clicked this frame.
15 pub clicked: Option<Id>,
16
17 /// This widget was long-pressed on a touch screen,
18 /// so trigger a secondary click on it (context menu).
19 pub long_touched: Option<Id>,
20
21 /// Drag started on this widget this frame.
22 ///
23 /// This will also be found in `dragged` this frame.
24 pub drag_started: Option<Id>,
25
26 /// This widget is being dragged this frame.
27 ///
28 /// Set the same frame a drag starts,
29 /// but unset the frame a drag ends.
30 ///
31 /// NOTE: this may not have a corresponding [`crate::WidgetRect`],
32 /// if this for instance is a drag-and-drop widget which
33 /// isn't painted whilst being dragged
34 pub dragged: Option<Id>,
35
36 /// This widget was let go this frame,
37 /// after having been dragged.
38 ///
39 /// The widget will not be found in [`Self::dragged`] this frame.
40 pub drag_stopped: Option<Id>,
41
42 /// A small set of widgets (usually 0-1) that the pointer is hovering over.
43 ///
44 /// Show these widgets as highlighted, if they are interactive.
45 ///
46 /// While dragging or clicking something, nothing else is hovered.
47 ///
48 /// Use [`Self::contains_pointer`] to find a drop-zone for drag-and-drop.
49 pub hovered: IdSet,
50
51 /// All widgets that contain the pointer this frame,
52 /// regardless if the user is currently clicking or dragging.
53 ///
54 /// This is usually a larger set than [`Self::hovered`],
55 /// and can be used for e.g. drag-and-drop zones.
56 pub contains_pointer: IdSet,
57}
58
59impl InteractionSnapshot {
60 pub fn ui(&self, ui: &mut crate::Ui) {
61 let Self {
62 clicked,
63 long_touched,
64 drag_started,
65 dragged,
66 drag_stopped,
67 hovered,
68 contains_pointer,
69 } = self;
70
71 fn id_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator<Item = &'a Id>) {
72 for id in widgets {
73 ui.label(id.short_debug_format());
74 }
75 }
76
77 crate::Grid::new("interaction").show(ui, |ui| {
78 ui.label("clicked");
79 id_ui(ui, clicked);
80 ui.end_row();
81
82 ui.label("long_touched");
83 id_ui(ui, long_touched);
84 ui.end_row();
85
86 ui.label("drag_started");
87 id_ui(ui, drag_started);
88 ui.end_row();
89
90 ui.label("dragged");
91 id_ui(ui, dragged);
92 ui.end_row();
93
94 ui.label("drag_stopped");
95 id_ui(ui, drag_stopped);
96 ui.end_row();
97
98 ui.label("hovered");
99 id_ui(ui, hovered);
100 ui.end_row();
101
102 ui.label("contains_pointer");
103 id_ui(ui, contains_pointer);
104 ui.end_row();
105 });
106 }
107}
108
109pub(crate) fn interact(
110 prev_snapshot: &InteractionSnapshot,
111 widgets: &WidgetRects,
112 hits: &WidgetHits,
113 input: &InputState,
114 interaction: &mut InteractionState,
115) -> InteractionSnapshot {
116 profiling::function_scope!();
117
118 if let Some(id) = interaction.potential_click_id {
119 if !widgets.contains(id) {
120 // The widget we were interested in clicking is gone.
121 interaction.potential_click_id = None;
122 }
123 }
124 if let Some(id) = interaction.potential_drag_id {
125 if !widgets.contains(id) {
126 // The widget we were interested in dragging is gone.
127 // This is fine! This could be drag-and-drop,
128 // and the widget being dragged is now "in the air" and thus
129 // not registered in the new frame.
130 }
131 }
132
133 let mut clicked = None;
134 let mut dragged = prev_snapshot.dragged;
135 let mut long_touched = None;
136
137 if input.key_pressed(Key::Escape) {
138 // Abort dragging on escape
139 dragged = None;
140 interaction.potential_drag_id = None;
141 }
142
143 if input.is_long_touch() {
144 // We implement "press-and-hold for context menu" on touch screens here
145 if let Some(widget) = interaction
146 .potential_click_id
147 .and_then(|id| widgets.get(id))
148 {
149 dragged = None;
150 clicked = Some(widget.id);
151 long_touched = Some(widget.id);
152 interaction.potential_click_id = None;
153 interaction.potential_drag_id = None;
154 }
155 }
156
157 // Note: in the current code a press-release in the same frame is NOT considered a drag.
158 for pointer_event in &input.pointer.pointer_events {
159 match pointer_event {
160 PointerEvent::Moved(_) => {}
161
162 PointerEvent::Pressed { .. } => {
163 // Maybe new click?
164 if interaction.potential_click_id.is_none() {
165 interaction.potential_click_id = hits.click.map(|w| w.id);
166 }
167
168 // Maybe new drag?
169 if interaction.potential_drag_id.is_none() {
170 interaction.potential_drag_id = hits.drag.map(|w| w.id);
171 }
172 }
173
174 PointerEvent::Released { click, button: _ } => {
175 if click.is_some() && !input.pointer.is_decidedly_dragging() {
176 if let Some(widget) = interaction
177 .potential_click_id
178 .and_then(|id| widgets.get(id))
179 {
180 clicked = Some(widget.id);
181 }
182 }
183
184 interaction.potential_drag_id = None;
185 interaction.potential_click_id = None;
186 dragged = None;
187 }
188 }
189 }
190
191 if dragged.is_none() {
192 // Check if we started dragging something new:
193 if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
194 if widget.enabled {
195 let is_dragged = if widget.sense.click && widget.sense.drag {
196 // This widget is sensitive to both clicks and drags.
197 // When the mouse first is pressed, it could be either,
198 // so we postpone the decision until we know.
199 input.pointer.is_decidedly_dragging()
200 } else {
201 // This widget is just sensitive to drags, so we can mark it as dragged right away:
202 widget.sense.drag
203 };
204
205 if is_dragged {
206 dragged = Some(widget.id);
207 }
208 }
209 }
210 }
211
212 if !input.pointer.could_any_button_be_click() {
213 interaction.potential_click_id = None;
214 }
215
216 if !input.pointer.any_down() || input.pointer.latest_pos().is_none() {
217 interaction.potential_click_id = None;
218 interaction.potential_drag_id = None;
219 }
220
221 // ------------------------------------------------------------------------
222
223 let drag_changed = dragged != prev_snapshot.dragged;
224 let drag_stopped = drag_changed.then_some(prev_snapshot.dragged).flatten();
225 let drag_started = drag_changed.then_some(dragged).flatten();
226
227 // if let Some(drag_started) = drag_started {
228 // eprintln!(
229 // "Started dragging {} {:?}",
230 // drag_started.id.short_debug_format(),
231 // drag_started.rect
232 // );
233 // }
234
235 let contains_pointer: IdSet = hits
236 .contains_pointer
237 .iter()
238 .chain(&hits.click)
239 .chain(&hits.drag)
240 .map(|w| w.id)
241 .collect();
242
243 let hovered = if clicked.is_some() || dragged.is_some() || long_touched.is_some() {
244 // If currently clicking or dragging, only that and nothing else is hovered.
245 clicked
246 .iter()
247 .chain(&dragged)
248 .chain(&long_touched)
249 .copied()
250 .collect()
251 } else {
252 // We may be hovering an interactive widget or two.
253 // We must also consider the case where non-interactive widgets
254 // are _on top_ of an interactive widget.
255 // For instance: a label in a draggable window.
256 // In that case we want to hover _both_ widgets,
257 // otherwise we won't see tooltips for the label.
258 //
259 // Because of how `Ui` work, we will often allocate the `Ui` rect
260 // _after_ adding the children in it (once we know the size it will occopy)
261 // so we will also have a lot of such `Ui` widgets rects covering almost any widget.
262 //
263 // So: we want to hover _all_ widgets above the interactive widget (if any),
264 // but none below it (an interactive widget stops the hover search).
265 //
266 // To know when to stop we need to first know the order of the widgets,
267 // which luckily we already have in `hits.close`.
268
269 let order = |id| hits.close.iter().position(|w| w.id == id);
270
271 let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0);
272 let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
273 let top_interactive_order = click_order.max(drag_order);
274
275 let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
276
277 for w in &hits.contains_pointer {
278 if top_interactive_order <= order(w.id).unwrap_or(0) {
279 hovered.insert(w.id);
280 }
281 }
282
283 hovered
284 };
285
286 InteractionSnapshot {
287 clicked,
288 long_touched,
289 drag_started,
290 dragged,
291 drag_stopped,
292 contains_pointer,
293 hovered,
294 }
295}