Skip to content

Commit

Permalink
On touch screens, press-and-hold equals a secondary click (#4195)
Browse files Browse the repository at this point in the history
* Closes #3444
* Closes #865

On a touch screen, if you press down on a widget and hold for 0.6
seconds (`MAX_CLICK_DURATION`), it will now trigger a secondary click,
i.e. `Response::secondary_clicked` will be `true`. This means you can
now open context menus on touch screens.
  • Loading branch information
emilk authored Mar 20, 2024
1 parent cd1ed73 commit d449cb1
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 92 deletions.
48 changes: 25 additions & 23 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

use std::{borrow::Cow, cell::RefCell, panic::Location, sync::Arc, time::Duration};

use ahash::HashMap;
use epaint::{
emath::TSTransform, mutex::*, stats::*, text::Fonts, util::OrderedFloat, TessellationOptions, *,
};
Expand Down Expand Up @@ -421,36 +420,30 @@ impl ContextImpl {
// but the `screen_rect` is the most important part.
}
}
let pixels_per_point = self.memory.options.zoom_factor
* new_raw_input
.viewport()
.native_pixels_per_point
.unwrap_or(1.0);
let native_pixels_per_point = new_raw_input
.viewport()
.native_pixels_per_point
.unwrap_or(1.0);
let pixels_per_point = self.memory.options.zoom_factor * native_pixels_per_point;

let all_viewport_ids: ViewportIdSet = self.all_viewport_ids();

let viewport = self.viewports.entry(self.viewport_id()).or_default();

self.memory
.begin_frame(&viewport.input, &new_raw_input, &all_viewport_ids);
self.memory.begin_frame(&new_raw_input, &all_viewport_ids);

viewport.input = std::mem::take(&mut viewport.input).begin_frame(
new_raw_input,
viewport.repaint.requested_immediate_repaint_prev_frame(),
pixels_per_point,
);

viewport.frame_state.begin_frame(&viewport.input);
let screen_rect = viewport.input.screen_rect;

viewport.frame_state.begin_frame(screen_rect);

{
let area_order: HashMap<LayerId, usize> = self
.memory
.areas()
.order()
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect();
let area_order = self.memory.areas().order_map();

let mut layers: Vec<LayerId> = viewport.widgets_prev_frame.layer_ids().collect();

Expand Down Expand Up @@ -488,7 +481,6 @@ impl ContextImpl {
}

// Ensure we register the background area so panels and background ui can catch clicks:
let screen_rect = viewport.input.screen_rect();
self.memory.areas_mut().set_state(
LayerId::background(),
containers::area::State {
Expand Down Expand Up @@ -1106,6 +1098,7 @@ impl Context {
highlighted,
clicked: false,
fake_primary_click: false,
long_touched: false,
drag_started: false,
dragged: false,
drag_stopped: false,
Expand Down Expand Up @@ -1141,6 +1134,10 @@ impl Context {
res.fake_primary_click = true;
}

if enabled && sense.click && Some(id) == viewport.interact_widgets.long_touched {
res.long_touched = true;
}

let interaction = memory.interaction();

res.is_pointer_button_down_on = interaction.potential_click_id == Some(id)
Expand Down Expand Up @@ -1168,7 +1165,8 @@ impl Context {

// is_pointer_button_down_on is false when released, but we want interact_pointer_pos
// to still work.
let is_interacted_with = res.is_pointer_button_down_on || clicked || res.drag_stopped;
let is_interacted_with =
res.is_pointer_button_down_on || res.long_touched || clicked || res.drag_stopped;
if is_interacted_with {
res.interact_pointer_pos = input.pointer.interact_pos();
if let (Some(transform), Some(pos)) = (
Expand All @@ -1179,7 +1177,7 @@ impl Context {
}
}

if input.pointer.any_down() && !res.is_pointer_button_down_on {
if input.pointer.any_down() && !is_interacted_with {
// We don't hover widgets while interacting with *other* widgets:
res.hovered = false;
}
Expand Down Expand Up @@ -1847,6 +1845,7 @@ impl Context {
let interact_widgets = self.write(|ctx| ctx.viewport().interact_widgets.clone());
let InteractionSnapshot {
clicked,
long_touched: _,
drag_started: _,
dragged,
drag_stopped: _,
Expand Down Expand Up @@ -1956,7 +1955,10 @@ impl ContextImpl {
})
.collect()
};
let focus_id = self.memory.focus().map_or(root_id, |id| id.accesskit_id());
let focus_id = self
.memory
.focused()
.map_or(root_id, |id| id.accesskit_id());
platform_output.accesskit_update = Some(accesskit::TreeUpdate {
nodes,
tree: Some(accesskit::Tree::new(root_id)),
Expand Down Expand Up @@ -2221,7 +2223,7 @@ impl Context {

/// If `true`, egui is currently listening on text input (e.g. typing text in a [`TextEdit`]).
pub fn wants_keyboard_input(&self) -> bool {
self.memory(|m| m.interaction().focus.focused().is_some())
self.memory(|m| m.focused().is_some())
}

/// Highlight this widget, to make it look like it is hovered, even if it isn't.
Expand Down Expand Up @@ -2481,7 +2483,7 @@ impl Context {
.on_hover_text("Is egui currently listening for text input?");
ui.label(format!(
"Keyboard focus widget: {}",
self.memory(|m| m.interaction().focus.focused())
self.memory(|m| m.focused())
.as_ref()
.map(Id::short_debug_format)
.unwrap_or_default()
Expand Down
6 changes: 3 additions & 3 deletions crates/egui/src/frame_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ impl Default for FrameState {
}

impl FrameState {
pub(crate) fn begin_frame(&mut self, input: &InputState) {
pub(crate) fn begin_frame(&mut self, screen_rect: Rect) {
crate::profile_function!();
let Self {
used_ids,
Expand All @@ -94,8 +94,8 @@ impl FrameState {
} = self;

used_ids.clear();
*available_rect = input.screen_rect();
*unused_rect = input.screen_rect();
*available_rect = screen_rect;
*unused_rect = screen_rect;
*used_by_panels = Rect::NOTHING;
*tooltip_state = None;
*scroll_target = [None, None];
Expand Down
57 changes: 43 additions & 14 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ use touch_state::TouchState;
/// If the pointer moves more than this, it won't become a click (but it is still a drag)
const MAX_CLICK_DIST: f32 = 6.0; // TODO(emilk): move to settings

/// If the pointer is down for longer than this, it won't become a click (but it is still a drag)
const MAX_CLICK_DURATION: f64 = 0.6; // TODO(emilk): move to settings
/// If the pointer is down for longer than this it will no longer register as a click.
///
/// If a touch is held for this many seconds while still,
/// then it will register as a "long-touch" which is equivalent to a secondary click.
///
/// This is to support "press and hold for context menu" on touch screens.
const MAX_CLICK_DURATION: f64 = 0.8; // TODO(emilk): move to settings

/// The new pointer press must come within this many seconds from previous pointer release
const MAX_DOUBLE_CLICK_DELAY: f64 = 0.3; // TODO(emilk): move to settings
Expand Down Expand Up @@ -544,6 +549,14 @@ impl InputState {
.cloned()
.collect()
}

/// A long press is something we detect on touch screens
/// to trigger a secondary click (context menu).
///
/// Returns `true` only on one frame.
pub(crate) fn is_long_touch(&self) -> bool {
self.any_touches() && self.pointer.is_long_press()
}
}

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -651,6 +664,8 @@ pub struct PointerState {
pub(crate) has_moved_too_much_for_a_click: bool,

/// Did [`Self::is_decidedly_dragging`] go from `false` to `true` this frame?
///
/// This could also be the trigger point for a long-touch.
pub(crate) started_decidedly_dragging: bool,

/// When did the pointer get click last?
Expand Down Expand Up @@ -751,6 +766,7 @@ impl PointerState {
button,
});
} else {
// Released
let clicked = self.could_any_button_be_click();

let click = if clicked {
Expand Down Expand Up @@ -1027,21 +1043,21 @@ impl PointerState {
///
/// See also [`Self::is_decidedly_dragging`].
pub fn could_any_button_be_click(&self) -> bool {
if !self.any_down() {
return false;
}

if self.has_moved_too_much_for_a_click {
return false;
}

if let Some(press_start_time) = self.press_start_time {
if self.time - press_start_time > MAX_CLICK_DURATION {
if self.any_down() || self.any_released() {
if self.has_moved_too_much_for_a_click {
return false;
}
}

true
if let Some(press_start_time) = self.press_start_time {
if self.time - press_start_time > MAX_CLICK_DURATION {
return false;
}
}

true
} else {
false
}
}

/// Just because the mouse is down doesn't mean we are dragging.
Expand All @@ -1060,6 +1076,19 @@ impl PointerState {
&& !self.any_click()
}

/// A long press is something we detect on touch screens
/// to trigger a secondary click (context menu).
///
/// Returns `true` only on one frame.
pub(crate) fn is_long_press(&self) -> bool {
self.started_decidedly_dragging
&& !self.has_moved_too_much_for_a_click
&& self.button_down(PointerButton::Primary)
&& self.press_start_time.map_or(false, |press_start_time| {
self.time - press_start_time > MAX_CLICK_DURATION
})
}

/// Is the primary button currently down?
#[inline(always)]
pub fn primary_down(&self) -> bool {
Expand Down
47 changes: 43 additions & 4 deletions crates/egui/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ 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.
Expand Down Expand Up @@ -56,6 +60,7 @@ impl InteractionSnapshot {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,
Expand All @@ -74,6 +79,10 @@ impl InteractionSnapshot {
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();
Expand Down Expand Up @@ -123,6 +132,21 @@ pub(crate) fn interact(

let mut clicked = None;
let mut dragged = prev_snapshot.dragged;
let mut long_touched = 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 {
Expand All @@ -142,7 +166,7 @@ pub(crate) fn interact(
}

PointerEvent::Released { click, button: _ } => {
if click.is_some() {
if click.is_some() && !input.pointer.is_decidedly_dragging() {
if let Some(widget) = interaction
.potential_click_id
.and_then(|id| widgets.get(id))
Expand Down Expand Up @@ -179,6 +203,15 @@ pub(crate) fn interact(
}
}

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;
Expand All @@ -201,9 +234,14 @@ pub(crate) fn interact(
.map(|w| w.id)
.collect();

let hovered = if clicked.is_some() || dragged.is_some() {
// If currently clicking or dragging, nothing else is hovered.
clicked.iter().chain(&dragged).copied().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 if hits.click.is_some() || hits.drag.is_some() {
// We are hovering over an interactive widget or two.
hits.click.iter().chain(&hits.drag).map(|w| w.id).collect()
Expand All @@ -220,6 +258,7 @@ pub(crate) fn interact(

InteractionSnapshot {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,
Expand Down
1 change: 0 additions & 1 deletion crates/egui/src/introspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ impl Widget for &memory::InteractionState {
let memory::InteractionState {
potential_click_id,
potential_drag_id,
focus: _,
} = self;

ui.vertical(|ui| {
Expand Down
Loading

0 comments on commit d449cb1

Please sign in to comment.