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}