diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 9728281e9b7..d0d6b9ca266 100644 --- a/crates/egui/src/context.rs +++ b/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 = self - .memory - .areas() - .order() - .iter() - .enumerate() - .map(|(i, id)| (*id, i)) - .collect(); + let area_order = self.memory.areas().order_map(); let mut layers: Vec = 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() diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index d94f1222aa6..87074c2a725 100644 --- a/crates/egui/src/frame_state.rs +++ b/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]; diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index 7fc29518ad1..8809cdb8d1f 100644 --- a/crates/egui/src/input_state.rs +++ b/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 { diff --git a/crates/egui/src/interaction.rs b/crates/egui/src/interaction.rs index 20d54887018..d5812d4d7c2 100644 --- a/crates/egui/src/interaction.rs +++ b/crates/egui/src/interaction.rs @@ -14,6 +14,10 @@ pub struct InteractionSnapshot { /// The widget that got clicked this frame. pub clicked: Option, + /// This widget was long-pressed on a touch screen, + /// so trigger a secondary click on it (context menu). + pub long_touched: Option, + /// 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, diff --git a/crates/egui/src/introspection.rs b/crates/egui/src/introspection.rs index 240ff6974a5..9c738153873 100644 --- a/crates/egui/src/introspection.rs +++ b/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| { diff --git a/crates/egui/src/memory.rs b/crates/egui/src/memory.rs index fb18258eb12..3ea3a19fe28 100644 --- a/crates/egui/src/memory.rs +++ b/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, + + #[cfg_attr(feature = "persistence", serde(skip))] + pub(crate) focus: ViewportIdMap, } 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, - - 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) { 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 { - self.interaction().focus.focused() + pub fn focused(&self) -> Option { + 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 { + 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); diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index be65b8445d6..32c07ed7b1a 100644 --- a/crates/egui/src/response.rs +++ b/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, diff --git a/crates/egui_demo_lib/src/demo/context_menu.rs b/crates/egui_demo_lib/src/demo/context_menu.rs index ffcc73379d8..1b71eb4f787 100644 --- a/crates/egui_demo_lib/src/demo/context_menu.rs +++ b/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"); diff --git a/crates/egui_demo_lib/src/demo/tests.rs b/crates/egui_demo_lib/src/demo/tests.rs index 2dd6ea64a86..8d45de1e15b 100644 --- a/crates/egui_demo_lib/src/demo/tests.rs +++ b/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 } diff --git a/examples/custom_keypad/src/keypad.rs b/examples/custom_keypad/src/keypad.rs index 81800f47d84..d4d0cb0d855 100644 --- a/examples/custom_keypad/src/keypad.rs +++ b/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::(self.id).unwrap_or_default(), ) });