egui/crates/egui/src/interaction.rs

296 lines
9.7 KiB
Rust

//! How mouse and touch interzcts with widgets.
use crate::{hit_test, id, input_state, memory, Id, InputState, Key, WidgetRects};
use self::{hit_test::WidgetHits, id::IdSet, input_state::PointerEvent, memory::InteractionState};
/// Calculated at the start of each frame
/// based on:
/// * Widget rects from precious frame
/// * Mouse/touch input
/// * Current [`InteractionState`].
#[derive(Clone, Default)]
pub struct InteractionSnapshot {
/// The widget that got clicked this frame.
pub clicked: Option<Id>,
/// This widget was long-pressed on a touch screen,
/// so trigger a secondary click on it (context menu).
pub long_touched: Option<Id>,
/// Drag started on this widget this frame.
///
/// This will also be found in `dragged` this frame.
pub drag_started: Option<Id>,
/// This widget is being dragged this frame.
///
/// Set the same frame a drag starts,
/// but unset the frame a drag ends.
///
/// NOTE: this may not have a corresponding [`crate::WidgetRect`],
/// if this for instance is a drag-and-drop widget which
/// isn't painted whilst being dragged
pub dragged: Option<Id>,
/// This widget was let go this frame,
/// after having been dragged.
///
/// The widget will not be found in [`Self::dragged`] this frame.
pub drag_stopped: Option<Id>,
/// A small set of widgets (usually 0-1) that the pointer is hovering over.
///
/// Show these widgets as highlighted, if they are interactive.
///
/// While dragging or clicking something, nothing else is hovered.
///
/// Use [`Self::contains_pointer`] to find a drop-zone for drag-and-drop.
pub hovered: IdSet,
/// All widgets that contain the pointer this frame,
/// regardless if the user is currently clicking or dragging.
///
/// This is usually a larger set than [`Self::hovered`],
/// and can be used for e.g. drag-and-drop zones.
pub contains_pointer: IdSet,
}
impl InteractionSnapshot {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,
hovered,
contains_pointer,
} = self;
fn id_ui<'a>(ui: &mut crate::Ui, widgets: impl IntoIterator<Item = &'a Id>) {
for id in widgets {
ui.label(id.short_debug_format());
}
}
crate::Grid::new("interaction").show(ui, |ui| {
ui.label("clicked");
id_ui(ui, clicked);
ui.end_row();
ui.label("long_touched");
id_ui(ui, long_touched);
ui.end_row();
ui.label("drag_started");
id_ui(ui, drag_started);
ui.end_row();
ui.label("dragged");
id_ui(ui, dragged);
ui.end_row();
ui.label("drag_stopped");
id_ui(ui, drag_stopped);
ui.end_row();
ui.label("hovered");
id_ui(ui, hovered);
ui.end_row();
ui.label("contains_pointer");
id_ui(ui, contains_pointer);
ui.end_row();
});
}
}
pub(crate) fn interact(
prev_snapshot: &InteractionSnapshot,
widgets: &WidgetRects,
hits: &WidgetHits,
input: &InputState,
interaction: &mut InteractionState,
) -> InteractionSnapshot {
profiling::function_scope!();
if let Some(id) = interaction.potential_click_id {
if !widgets.contains(id) {
// The widget we were interested in clicking is gone.
interaction.potential_click_id = None;
}
}
if let Some(id) = interaction.potential_drag_id {
if !widgets.contains(id) {
// The widget we were interested in dragging is gone.
// This is fine! This could be drag-and-drop,
// and the widget being dragged is now "in the air" and thus
// not registered in the new frame.
}
}
let mut clicked = None;
let mut dragged = prev_snapshot.dragged;
let mut long_touched = None;
if input.key_pressed(Key::Escape) {
// Abort dragging on escape
dragged = None;
interaction.potential_drag_id = None;
}
if input.is_long_touch() {
// We implement "press-and-hold for context menu" on touch screens here
if let Some(widget) = interaction
.potential_click_id
.and_then(|id| widgets.get(id))
{
dragged = None;
clicked = Some(widget.id);
long_touched = Some(widget.id);
interaction.potential_click_id = None;
interaction.potential_drag_id = None;
}
}
// Note: in the current code a press-release in the same frame is NOT considered a drag.
for pointer_event in &input.pointer.pointer_events {
match pointer_event {
PointerEvent::Moved(_) => {}
PointerEvent::Pressed { .. } => {
// Maybe new click?
if interaction.potential_click_id.is_none() {
interaction.potential_click_id = hits.click.map(|w| w.id);
}
// Maybe new drag?
if interaction.potential_drag_id.is_none() {
interaction.potential_drag_id = hits.drag.map(|w| w.id);
}
}
PointerEvent::Released { click, button: _ } => {
if click.is_some() && !input.pointer.is_decidedly_dragging() {
if let Some(widget) = interaction
.potential_click_id
.and_then(|id| widgets.get(id))
{
clicked = Some(widget.id);
}
}
interaction.potential_drag_id = None;
interaction.potential_click_id = None;
dragged = None;
}
}
}
if dragged.is_none() {
// Check if we started dragging something new:
if let Some(widget) = interaction.potential_drag_id.and_then(|id| widgets.get(id)) {
if widget.enabled {
let is_dragged = if widget.sense.click && widget.sense.drag {
// This widget is sensitive to both clicks and drags.
// When the mouse first is pressed, it could be either,
// so we postpone the decision until we know.
input.pointer.is_decidedly_dragging()
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.drag
};
if is_dragged {
dragged = Some(widget.id);
}
}
}
}
if !input.pointer.could_any_button_be_click() {
interaction.potential_click_id = None;
}
if !input.pointer.any_down() || input.pointer.latest_pos().is_none() {
interaction.potential_click_id = None;
interaction.potential_drag_id = None;
}
// ------------------------------------------------------------------------
let drag_changed = dragged != prev_snapshot.dragged;
let drag_stopped = drag_changed.then_some(prev_snapshot.dragged).flatten();
let drag_started = drag_changed.then_some(dragged).flatten();
// if let Some(drag_started) = drag_started {
// eprintln!(
// "Started dragging {} {:?}",
// drag_started.id.short_debug_format(),
// drag_started.rect
// );
// }
let contains_pointer: IdSet = hits
.contains_pointer
.iter()
.chain(&hits.click)
.chain(&hits.drag)
.map(|w| w.id)
.collect();
let hovered = if clicked.is_some() || dragged.is_some() || long_touched.is_some() {
// If currently clicking or dragging, only that and nothing else is hovered.
clicked
.iter()
.chain(&dragged)
.chain(&long_touched)
.copied()
.collect()
} else {
// We may be hovering a an interactive widget or two.
// We must also consider the case where non-interactive widgets
// are _on top_ of an interactive widget.
// For instance: a label in a draggable window.
// In that case we want to hover _both_ widgets,
// otherwise we won't see tooltips for the label.
//
// Because of how `Ui` work, we will often allocate the `Ui` rect
// _after_ adding the children in it (once we know the size it will occopy)
// so we will also have a lot of such `Ui` widgets rects covering almost any widget.
//
// So: we want to hover _all_ widgets above the interactive widget (if any),
// but none below it (an interactive widget stops the hover search).
//
// To know when to stop we need to first know the order of the widgets,
// which luckily we have in the `WidgetRects`.
let order = |id| widgets.order(id).map(|(_layer, order)| order); // we ignore the layer, since all widgets at this point is in the same layer
let click_order = hits.click.and_then(|w| order(w.id)).unwrap_or(0);
let drag_order = hits.drag.and_then(|w| order(w.id)).unwrap_or(0);
let top_interactive_order = click_order.max(drag_order);
let mut hovered: IdSet = hits.click.iter().chain(&hits.drag).map(|w| w.id).collect();
for w in &hits.contains_pointer {
if top_interactive_order <= order(w.id).unwrap_or(0) {
hovered.insert(w.id);
}
}
hovered
};
InteractionSnapshot {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,
contains_pointer,
hovered,
}
}