Browse Source

On touch screens, press-and-hold equals a secondary click (#4195)

* Closes https://github.com/emilk/egui/issues/3444
* Closes https://github.com/emilk/egui/issues/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.
pull/4200/head
Emil Ernerfeldt 8 months ago
committed by GitHub
parent
commit
d449cb1d48
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 48
      crates/egui/src/context.rs
  2. 6
      crates/egui/src/frame_state.rs
  3. 57
      crates/egui/src/input_state.rs
  4. 47
      crates/egui/src/interaction.rs
  5. 1
      crates/egui/src/introspection.rs
  6. 88
      crates/egui/src/memory.rs
  7. 20
      crates/egui/src/response.rs
  8. 6
      crates/egui_demo_lib/src/demo/context_menu.rs
  9. 4
      crates/egui_demo_lib/src/demo/tests.rs
  10. 2
      examples/custom_keypad/src/keypad.rs

48
crates/egui/src/context.rs

@ -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, *,
};
@ -421,18 +420,17 @@ 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,
@ -440,17 +438,12 @@ impl ContextImpl {
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();
@ -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 {
@ -1106,6 +1098,7 @@ impl Context {
highlighted,
clicked: false,
fake_primary_click: false,
long_touched: false,
drag_started: false,
dragged: false,
drag_stopped: false,
@ -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)
@ -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)) = (
@ -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;
}
@ -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: _,
@ -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)),
@ -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.
@ -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()

6
crates/egui/src/frame_state.rs

@ -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,
@ -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];

57
crates/egui/src/input_state.rs

@ -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
@ -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()
}
}
// ----------------------------------------------------------------------------
@ -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?
@ -751,6 +766,7 @@ impl PointerState {
button,
});
} else {
// Released
let clicked = self.could_any_button_be_click();
let click = if clicked {
@ -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.
@ -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 {

47
crates/egui/src/interaction.rs

@ -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.
@ -56,6 +60,7 @@ impl InteractionSnapshot {
pub fn ui(&self, ui: &mut crate::Ui) {
let Self {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,
@ -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();
@ -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 {
@ -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))
@ -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;
@ -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()
@ -220,6 +258,7 @@ pub(crate) fn interact(
InteractionSnapshot {
clicked,
long_touched,
drag_started,
dragged,
drag_stopped,

1
crates/egui/src/introspection.rs

@ -194,7 +194,6 @@ impl Widget for &memory::InteractionState {
let memory::InteractionState {
potential_click_id,
potential_drag_id,
focus: _,
} = self;
ui.vertical(|ui| {

88
crates/egui/src/memory.rs

@ -4,7 +4,7 @@ use ahash::HashMap;
use epaint::emath::TSTransform;
use crate::{
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, Rect, Style, Vec2,
area, vec2, EventFilter, Id, IdMap, LayerId, Order, Pos2, Rangef, RawInput, Rect, Style, Vec2,
ViewportId, ViewportIdMap, ViewportIdSet,
};
@ -95,6 +95,9 @@ pub struct Memory {
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) interactions: ViewportIdMap<InteractionState>,
#[cfg_attr(feature = "persistence", serde(skip))]
pub(crate) focus: ViewportIdMap<Focus>,
}
impl Default for Memory {
@ -105,6 +108,7 @@ impl Default for Memory {
caches: Default::default(),
new_font_definitions: Default::default(),
interactions: Default::default(),
focus: Default::default(),
viewport_id: Default::default(),
areas: Default::default(),
layer_transforms: Default::default(),
@ -308,8 +312,6 @@ pub(crate) struct InteractionState {
/// as that can only happen after the mouse has moved a bit
/// (at least if the widget is interesated in both clicks and drags).
pub potential_drag_id: Option<Id>,
pub focus: Focus,
}
/// Keeps tracks of what widget has keyboard focus
@ -362,24 +364,6 @@ impl InteractionState {
pub fn is_using_pointer(&self) -> bool {
self.potential_click_id.is_some() || self.potential_drag_id.is_some()
}
fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
) {
if !prev_input.pointer.could_any_button_be_click() {
self.potential_click_id = None;
}
if !prev_input.pointer.any_down() || prev_input.pointer.latest_pos().is_none() {
// pointer button was not down last frame
self.potential_click_id = None;
self.potential_drag_id = None;
}
self.focus.begin_frame(new_input);
}
}
impl Focus {
@ -603,30 +587,29 @@ impl Focus {
}
impl Memory {
pub(crate) fn begin_frame(
&mut self,
prev_input: &crate::input_state::InputState,
new_input: &crate::data::input::RawInput,
viewports: &ViewportIdSet,
) {
pub(crate) fn begin_frame(&mut self, new_raw_input: &RawInput, viewports: &ViewportIdSet) {
crate::profile_function!();
self.viewport_id = new_raw_input.viewport_id;
// Cleanup
self.interactions.retain(|id, _| viewports.contains(id));
self.areas.retain(|id, _| viewports.contains(id));
self.viewport_id = new_input.viewport_id;
self.interactions
self.areas.entry(self.viewport_id).or_default();
// self.interactions is handled elsewhere
self.focus
.entry(self.viewport_id)
.or_default()
.begin_frame(prev_input, new_input);
self.areas.entry(self.viewport_id).or_default();
.begin_frame(new_raw_input);
}
pub(crate) fn end_frame(&mut self, used_ids: &IdMap<Rect>) {
self.caches.update();
self.areas_mut().end_frame();
self.interaction_mut().focus.end_frame(used_ids);
self.focus_mut().end_frame(used_ids);
}
pub(crate) fn set_viewport_id(&mut self, viewport_id: ViewportId) {
@ -656,7 +639,7 @@ impl Memory {
}
pub(crate) fn had_focus_last_frame(&self, id: Id) -> bool {
self.interaction().focus.id_previous_frame == Some(id)
self.focus().id_previous_frame == Some(id)
}
/// True if the given widget had keyboard focus last frame, but not this one.
@ -677,12 +660,12 @@ impl Memory {
/// from the window and back.
#[inline(always)]
pub fn has_focus(&self, id: Id) -> bool {
self.interaction().focus.focused() == Some(id)
self.focused() == Some(id)
}
/// Which widget has keyboard focus?
pub fn focus(&self) -> Option<Id> {
self.interaction().focus.focused()
pub fn focused(&self) -> Option<Id> {
self.focus().focused()
}
/// Set an event filter for a widget.
@ -693,7 +676,7 @@ impl Memory {
/// You must first give focus to the widget before calling this.
pub fn set_focus_lock_filter(&mut self, id: Id, event_filter: EventFilter) {
if self.had_focus_last_frame(id) && self.has_focus(id) {
if let Some(focused) = &mut self.interaction_mut().focus.focused_widget {
if let Some(focused) = &mut self.focus_mut().focused_widget {
if focused.id == id {
focused.filter = event_filter;
}
@ -705,16 +688,16 @@ impl Memory {
/// See also [`crate::Response::request_focus`].
#[inline(always)]
pub fn request_focus(&mut self, id: Id) {
self.interaction_mut().focus.focused_widget = Some(FocusWidget::new(id));
self.focus_mut().focused_widget = Some(FocusWidget::new(id));
}
/// Surrender keyboard focus for a specific widget.
/// See also [`crate::Response::surrender_focus`].
#[inline(always)]
pub fn surrender_focus(&mut self, id: Id) {
let interaction = self.interaction_mut();
if interaction.focus.focused() == Some(id) {
interaction.focus.focused_widget = None;
let focus = self.focus_mut();
if focus.focused() == Some(id) {
focus.focused_widget = None;
}
}
@ -727,13 +710,13 @@ impl Memory {
/// and rendered correctly in a single frame.
#[inline(always)]
pub fn interested_in_focus(&mut self, id: Id) {
self.interaction_mut().focus.interested_in_focus(id);
self.focus_mut().interested_in_focus(id);
}
/// Stop editing of active [`TextEdit`](crate::TextEdit) (if any).
#[inline(always)]
pub fn stop_text_input(&mut self) {
self.interaction_mut().focus.focused_widget = None;
self.focus_mut().focused_widget = None;
}
/// Is any widget being dragged?
@ -813,6 +796,16 @@ impl Memory {
pub(crate) fn interaction_mut(&mut self) -> &mut InteractionState {
self.interactions.entry(self.viewport_id).or_default()
}
pub(crate) fn focus(&self) -> &Focus {
self.focus
.get(&self.viewport_id)
.expect("Failed to get focus")
}
pub(crate) fn focus_mut(&mut self) -> &mut Focus {
self.focus.entry(self.viewport_id).or_default()
}
}
/// ## Popups
@ -908,6 +901,15 @@ impl Areas {
&self.order
}
/// For each layer, which order is it in [`Self::order`]?
pub(crate) fn order_map(&self) -> HashMap<LayerId, usize> {
self.order
.iter()
.enumerate()
.map(|(i, id)| (*id, i))
.collect()
}
pub(crate) fn set_state(&mut self, layer_id: LayerId, state: area::State) {
self.visible_current_frame.insert(layer_id);
self.areas.insert(layer_id.id, state);

20
crates/egui/src/response.rs

@ -89,6 +89,10 @@ pub struct Response {
#[doc(hidden)]
pub fake_primary_click: bool,
/// This widget was long-pressed on a touch screen to simulate a secondary click.
#[doc(hidden)]
pub long_touched: bool,
/// The widget started being dragged this frame.
#[doc(hidden)]
pub drag_started: bool,
@ -142,15 +146,28 @@ impl Response {
/// This will NOT return true if the widget was "clicked" via
/// some accessibility integration, or if the widget had keyboard focus and the
/// user pressed Space/Enter. For that, use [`Self::clicked`] instead.
///
/// This will likewise ignore the press-and-hold action on touch screens.
/// Use [`Self::secondary_clicked`] instead to also detect that.
#[inline]
pub fn clicked_by(&self, button: PointerButton) -> bool {
self.clicked && self.ctx.input(|i| i.pointer.button_clicked(button))
}
/// Returns true if this widget was clicked this frame by the secondary mouse button (e.g. the right mouse button).
///
/// This also returns true if the widget was pressed-and-held on a touch screen.
#[inline]
pub fn secondary_clicked(&self) -> bool {
self.clicked_by(PointerButton::Secondary)
self.long_touched || self.clicked_by(PointerButton::Secondary)
}
/// Was this long-pressed on a touch screen?
///
/// Usually you want to check [`Self::secondary_clicked`] instead.
#[inline]
pub fn long_touched(&self) -> bool {
self.long_touched
}
/// Returns true if this widget was clicked this frame by the middle mouse button.
@ -933,6 +950,7 @@ impl Response {
highlighted: self.highlighted || other.highlighted,
clicked: self.clicked || other.clicked,
fake_primary_click: self.fake_primary_click || other.fake_primary_click,
long_touched: self.long_touched || other.long_touched,
drag_started: self.drag_started || other.drag_started,
dragged: self.dragged || other.dragged,
drag_stopped: self.drag_stopped || other.drag_stopped,

6
crates/egui_demo_lib/src/demo/context_menu.rs

@ -80,6 +80,8 @@ impl super::View for ContextMenus {
ui.label("Right-click plot to edit it!");
ui.horizontal(|ui| {
self.example_plot(ui).context_menu(|ui| {
ui.set_min_width(220.0);
ui.menu_button("Plot", |ui| {
if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked()
|| ui
@ -96,12 +98,12 @@ impl super::View for ContextMenus {
ui.add(
egui::DragValue::new(&mut self.width)
.speed(1.0)
.prefix("Width:"),
.prefix("Width: "),
);
ui.add(
egui::DragValue::new(&mut self.height)
.speed(1.0)
.prefix("Height:"),
.prefix("Height: "),
);
ui.end_row();
ui.checkbox(&mut self.show_axes[0], "x-Axis");

4
crates/egui_demo_lib/src/demo/tests.rs

@ -486,6 +486,10 @@ fn response_summary(response: &egui::Response, show_hovers: bool) -> String {
}
}
if response.long_touched() {
writeln!(new_info, "Clicked with long-press").ok();
}
new_info
}

2
examples/custom_keypad/src/keypad.rs

@ -174,7 +174,7 @@ impl Keypad {
pub fn show(&self, ctx: &egui::Context) {
let (focus, mut state) = ctx.memory(|m| {
(
m.focus(),
m.focused(),
m.data.get_temp::<State>(self.id).unwrap_or_default(),
)
});

Loading…
Cancel
Save