Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add max click timings & distance to Options #4986

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,11 @@ impl ContextImpl {
&viewport.prev_frame.widgets,
&viewport.hits,
&viewport.input,
self.memory.interaction_mut(),
self.memory
.interactions
.entry(self.memory.viewport_id)
.or_default(),
&self.memory.options,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't super pretty, but otherwise it conflicts with the mut access of memory above

);
}

Expand Down Expand Up @@ -2046,7 +2050,7 @@ impl ContextImpl {

if repaint_needed {
self.request_repaint(ended_viewport_id, RepaintCause::new());
} else if let Some(delay) = viewport.input.wants_repaint_after() {
} else if let Some(delay) = viewport.input.wants_repaint_after(&self.memory.options) {
self.request_repaint_after(delay, ended_viewport_id, RepaintCause::new());
}

Expand Down
58 changes: 23 additions & 35 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod touch_state;

use crate::data::input::*;
use crate::Options;
use crate::{emath::*, util::History};
use std::{
collections::{BTreeMap, HashSet},
Expand All @@ -11,20 +12,6 @@ pub use crate::Key;
pub use touch_state::MultiTouchInfo;
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 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

/// Input state that egui updates each frame.
///
/// You can access this with [`crate::Context::input`].
Expand Down Expand Up @@ -218,7 +205,7 @@ impl InputState {
for touch_state in self.touch_states.values_mut() {
touch_state.begin_frame(time, &new, self.pointer.interact_pos);
}
let pointer = self.pointer.begin_frame(time, &new);
let pointer = self.pointer.begin_frame(time, &new, options);

let mut keys_down = self.keys_down;
let mut zoom_factor_delta = 1.0; // TODO(emilk): smoothing for zoom factor
Expand Down Expand Up @@ -422,7 +409,7 @@ impl InputState {
/// The [`crate::Context`] will call this at the end of each frame to see if we need a repaint.
///
/// Returns how long to wait for a repaint.
pub fn wants_repaint_after(&self) -> Option<Duration> {
pub fn wants_repaint_after(&self, options: &Options) -> Option<Duration> {
if self.pointer.wants_repaint()
|| self.unprocessed_scroll_delta.abs().max_elem() > 0.2
|| self.unprocessed_scroll_delta_for_zoom.abs() > 0.2
Expand All @@ -432,12 +419,12 @@ impl InputState {
return Some(Duration::ZERO);
}

if self.any_touches() && !self.pointer.is_decidedly_dragging() {
if self.any_touches() && !self.pointer.is_decidedly_dragging(options) {
// We need to wake up and check for press-and-hold for the context menu.
if let Some(press_start_time) = self.pointer.press_start_time {
let press_duration = self.time - press_start_time;
if press_duration < MAX_CLICK_DURATION {
let secs_until_menu = MAX_CLICK_DURATION - press_duration;
if press_duration < options.max_click_duration {
let secs_until_menu = options.max_click_duration - press_duration;
return Some(Duration::from_secs_f64(secs_until_menu));
}
}
Expand Down Expand Up @@ -663,8 +650,8 @@ impl InputState {
/// 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()
pub(crate) fn is_long_touch(&self, options: &Options) -> bool {
self.any_touches() && self.pointer.is_long_press(options)
}
}

Expand Down Expand Up @@ -822,8 +809,8 @@ impl Default for PointerState {

impl PointerState {
#[must_use]
pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput) -> Self {
let was_decidedly_dragging = self.is_decidedly_dragging();
pub(crate) fn begin_frame(mut self, time: f64, new: &RawInput, options: &Options) -> Self {
let was_decidedly_dragging = self.is_decidedly_dragging(options);

self.time = time;

Expand All @@ -845,7 +832,7 @@ impl PointerState {

if let Some(press_origin) = self.press_origin {
self.has_moved_too_much_for_a_click |=
press_origin.distance(pos) > MAX_CLICK_DIST;
press_origin.distance(pos) > options.max_click_dist;
}

self.pointer_events.push(PointerEvent::Moved(pos));
Expand Down Expand Up @@ -880,13 +867,13 @@ impl PointerState {
});
} else {
// Released
let clicked = self.could_any_button_be_click();
let clicked = self.could_any_button_be_click(options);

let click = if clicked {
let double_click =
(time - self.last_click_time) < MAX_DOUBLE_CLICK_DELAY;
let triple_click =
(time - self.last_last_click_time) < (MAX_DOUBLE_CLICK_DELAY * 2.0);
(time - self.last_click_time) < options.max_double_click_delay;
let triple_click = (time - self.last_last_click_time)
< (options.max_double_click_delay * 2.0);
let count = if triple_click {
3
} else if double_click {
Expand Down Expand Up @@ -955,7 +942,8 @@ impl PointerState {

self.direction = self.pos_history.velocity().unwrap_or_default().normalized();

self.started_decidedly_dragging = self.is_decidedly_dragging() && !was_decidedly_dragging;
self.started_decidedly_dragging =
self.is_decidedly_dragging(options) && !was_decidedly_dragging;

self
}
Expand Down Expand Up @@ -1177,14 +1165,14 @@ impl PointerState {
/// If the pointer button is down, will it register as a click when released?
///
/// See also [`Self::is_decidedly_dragging`].
pub fn could_any_button_be_click(&self) -> bool {
pub fn could_any_button_be_click(&self, options: &Options) -> bool {
if self.any_down() || self.any_released() {
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.time - press_start_time > options.max_click_duration {
return false;
}
}
Expand All @@ -1204,23 +1192,23 @@ impl PointerState {
/// but NOT on the first frame it was started.
///
/// See also [`Self::could_any_button_be_click`].
pub fn is_decidedly_dragging(&self) -> bool {
pub fn is_decidedly_dragging(&self, options: &Options) -> bool {
(self.any_down() || self.any_released())
&& !self.any_pressed()
&& !self.could_any_button_be_click()
&& !self.could_any_button_be_click(options)
&& !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 {
pub(crate) fn is_long_press(&self, options: &Options) -> 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
self.time - press_start_time > options.max_click_duration
})
}

Expand Down
9 changes: 5 additions & 4 deletions crates/egui/src/interaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ pub(crate) fn interact(
hits: &WidgetHits,
input: &InputState,
interaction: &mut InteractionState,
options: &Options,
) -> InteractionSnapshot {
crate::profile_function!();

Expand Down Expand Up @@ -140,7 +141,7 @@ pub(crate) fn interact(
interaction.potential_drag_id = None;
}

if input.is_long_touch() {
if input.is_long_touch(options) {
// We implement "press-and-hold for context menu" on touch screens here
if let Some(widget) = interaction
.potential_click_id
Expand Down Expand Up @@ -172,7 +173,7 @@ pub(crate) fn interact(
}

PointerEvent::Released { click, button: _ } => {
if click.is_some() && !input.pointer.is_decidedly_dragging() {
if click.is_some() && !input.pointer.is_decidedly_dragging(options) {
if let Some(widget) = interaction
.potential_click_id
.and_then(|id| widgets.get(id))
Expand All @@ -196,7 +197,7 @@ pub(crate) fn interact(
// 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()
input.pointer.is_decidedly_dragging(options)
} else {
// This widget is just sensitive to drags, so we can mark it as dragged right away:
widget.sense.drag
Expand All @@ -209,7 +210,7 @@ pub(crate) fn interact(
}
}

if !input.pointer.could_any_button_be_click() {
if !input.pointer.could_any_button_be_click(options) {
interaction.potential_click_id = None;
}

Expand Down
51 changes: 51 additions & 0 deletions crates/egui/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,20 @@ pub struct Options {
///
/// Default is `false`.
pub reduce_texture_memory: bool,

/// If the pointer moves more than this, it won't become a click (but it is still a drag)
pub max_click_dist: f32,

/// 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.
pub max_click_duration: f64,

/// The new pointer press must come within this many seconds from previous pointer release
pub max_double_click_delay: f64,
}

impl Default for Options {
Expand Down Expand Up @@ -295,6 +309,10 @@ impl Default for Options {
line_scroll_speed,
scroll_zoom_speed: 1.0 / 200.0,
reduce_texture_memory: false,

max_click_dist: 6.0,
max_click_duration: 0.8,
max_double_click_delay: 0.3,
}
}
}
Expand Down Expand Up @@ -339,6 +357,10 @@ impl Options {
line_scroll_speed,
scroll_zoom_speed,
reduce_texture_memory,

max_click_dist,
max_click_duration,
max_double_click_delay,
} = self;

use crate::Widget as _;
Expand Down Expand Up @@ -396,6 +418,35 @@ impl Options {
)
.on_hover_text("How fast to zoom with ctrl/cmd + scroll");
});

ui.horizontal(|ui| {
ui.label("Max click distance");
ui.add(
crate::DragValue::new(max_click_dist)
.range(0.0..=f32::INFINITY)
.speed(1.0),
)
.on_hover_text("If the pointer moves more than this, it won't become a click (but it is still a drag)");
});
ui.horizontal(|ui| {
ui.label("Max click duration");
ui.add(
crate::DragValue::new(max_click_duration)
.range(0.0..=f32::INFINITY)
.speed(0.1),
)
.on_hover_text("If the pointer is down for longer than this it will no longer register as a click");
});
ui.horizontal(|ui| {
ui.label("Max double click delay");
ui.add(
crate::DragValue::new(max_double_click_delay)
.range(0.0..=f32::INFINITY)
.speed(0.1),
)
.on_hover_text("The new pointer press must come within this many seconds from previous pointer release");
});

});

ui.vertical_centered(|ui| crate::reset_button(ui, self, "Reset all"));
Expand Down
2 changes: 1 addition & 1 deletion crates/egui/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2721,7 +2721,7 @@ fn register_rect(ui: &Ui, rect: Rect) {
return;
}

let is_clicking = ui.input(|i| i.pointer.could_any_button_be_click());
let is_clicking = ui.memory(|m| ui.input(|i| i.pointer.could_any_button_be_click(&m.options)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deadlocks (try holding down all modifier keys in the demo app)


#[cfg(feature = "callstack")]
let callstack = crate::callstack::capture();
Expand Down