From 977d83a08f382f5a6d08a7f90dc78e5c298a4060 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 5 Jul 2024 09:39:12 +0200 Subject: [PATCH] Hide tooltips when scrolling (#4784) * Closes #4781 --- crates/egui/src/context.rs | 10 +++++++ crates/egui/src/input_state.rs | 32 ++++++++++++++++++++ crates/egui/src/response.rs | 36 ++++++++++++++++------- crates/egui/src/text_selection/visuals.rs | 3 +- crates/egui_demo_lib/src/demo/tooltips.rs | 31 ++++++++++++++++++- 5 files changed, 99 insertions(+), 13 deletions(-) diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index f6d6ba836e5..7e1813b9ea9 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1442,6 +1442,16 @@ impl Context { self.request_repaint_after_for(duration, self.viewport_id()); } + /// Repaint after this many seconds. + /// + /// See [`Self::request_repaint_after`] for details. + #[track_caller] + pub fn request_repaint_after_secs(&self, seconds: f32) { + if let Ok(duration) = std::time::Duration::try_from_secs_f32(seconds) { + self.request_repaint_after(duration); + } + } + /// Request repaint after at most the specified duration elapses. /// /// The backend can chose to repaint sooner, for instance if some other code called diff --git a/crates/egui/src/input_state.rs b/crates/egui/src/input_state.rs index ad0e7b41f2a..cac8efe62d5 100644 --- a/crates/egui/src/input_state.rs +++ b/crates/egui/src/input_state.rs @@ -44,6 +44,12 @@ pub struct InputState { /// (We keep a separate [`TouchState`] for each encountered touch device.) touch_states: BTreeMap, + // ---------------------------------------------- + // Scrolling: + // + /// Time of the last scroll event. + last_scroll_time: f64, + /// Used for smoothing the scroll delta. unprocessed_scroll_delta: Vec2, @@ -87,6 +93,7 @@ pub struct InputState { /// * `zoom > 1`: pinch spread zoom_factor_delta: f32, + // ---------------------------------------------- /// Position and size of the egui area. pub screen_rect: Rect, @@ -161,11 +168,14 @@ impl Default for InputState { raw: Default::default(), pointer: Default::default(), touch_states: Default::default(), + + last_scroll_time: f64::NEG_INFINITY, unprocessed_scroll_delta: Vec2::ZERO, unprocessed_scroll_delta_for_zoom: 0.0, raw_scroll_delta: Vec2::ZERO, smooth_scroll_delta: Vec2::ZERO, zoom_factor_delta: 1.0, + screen_rect: Rect::from_min_size(Default::default(), vec2(10_000.0, 10_000.0)), pixels_per_point: 1.0, max_texture_side: 2048, @@ -320,14 +330,24 @@ impl InputState { } } + let is_scrolling = raw_scroll_delta != Vec2::ZERO || smooth_scroll_delta != Vec2::ZERO; + let last_scroll_time = if is_scrolling { + time + } else { + self.last_scroll_time + }; + Self { pointer, touch_states: self.touch_states, + + last_scroll_time, unprocessed_scroll_delta, unprocessed_scroll_delta_for_zoom, raw_scroll_delta, smooth_scroll_delta, zoom_factor_delta, + screen_rect, pixels_per_point, max_texture_side: new.max_texture_side.unwrap_or(self.max_texture_side), @@ -393,6 +413,12 @@ impl InputState { ) } + /// How long has it been (in seconds) since the use last scrolled? + #[inline(always)] + pub fn time_since_last_scroll(&self) -> f32 { + (self.time - self.last_scroll_time) as f32 + } + /// 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. @@ -1218,6 +1244,7 @@ impl InputState { pointer, touch_states, + last_scroll_time, unprocessed_scroll_delta, unprocessed_scroll_delta_for_zoom, raw_scroll_delta, @@ -1257,6 +1284,10 @@ impl InputState { }); } + ui.label(format!( + "Time since last scroll: {:.1} s", + time - last_scroll_time + )); if cfg!(debug_assertions) { ui.label(format!( "unprocessed_scroll_delta: {unprocessed_scroll_delta:?} points" @@ -1270,6 +1301,7 @@ impl InputState { "smooth_scroll_delta: {smooth_scroll_delta:?} points" )); ui.label(format!("zoom_factor_delta: {zoom_factor_delta:4.2}x")); + ui.label(format!("screen_rect: {screen_rect:?} points")); ui.label(format!( "{pixels_per_point} physical pixels for each logical point" diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 04b11fde280..3532bf342c3 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -611,6 +611,21 @@ impl Response { return false; } + let style = self.ctx.style(); + + let tooltip_delay = style.interaction.tooltip_delay; + let tooltip_grace_time = style.interaction.tooltip_grace_time; + + let time_since_last_scroll = self.ctx.input(|i| i.time_since_last_scroll()); + + if time_since_last_scroll < tooltip_delay { + // See https://github.com/emilk/egui/issues/4781 + // Note that this means we cannot have `ScrollArea`s in a tooltip. + self.ctx + .request_repaint_after_secs(tooltip_delay - time_since_last_scroll); + return false; + } + let is_our_tooltip_open = self.is_tooltip_open(); if is_our_tooltip_open { @@ -680,9 +695,6 @@ impl Response { return false; } - let tooltip_delay = self.ctx.style().interaction.tooltip_delay; - let tooltip_grace_time = self.ctx.style().interaction.tooltip_grace_time; - // There is a tooltip_delay before showing the first tooltip, // but once one tooltips is show, moving the mouse cursor to // another widget should show the tooltip for that widget right away. @@ -692,23 +704,27 @@ impl Response { crate::popup::seconds_since_last_tooltip(&self.ctx) < tooltip_grace_time; if !tooltip_was_recently_shown && !is_our_tooltip_open { - if self.ctx.style().interaction.show_tooltips_only_when_still { + if style.interaction.show_tooltips_only_when_still { // We only show the tooltip when the mouse pointer is still. - if !self.ctx.input(|i| i.pointer.is_still()) { + if !self + .ctx + .input(|i| i.pointer.is_still() && i.smooth_scroll_delta == Vec2::ZERO) + { // wait for mouse to stop self.ctx.request_repaint(); return false; } } - let time_til_tooltip = - tooltip_delay - self.ctx.input(|i| i.pointer.time_since_last_movement()); + let time_since_last_interaction = self.ctx.input(|i| { + i.time_since_last_scroll() + .max(i.pointer.time_since_last_movement()) + }); + let time_til_tooltip = tooltip_delay - time_since_last_interaction; if 0.0 < time_til_tooltip { // Wait until the mouse has been still for a while - if let Ok(duration) = std::time::Duration::try_from_secs_f32(time_til_tooltip) { - self.ctx.request_repaint_after(duration); - } + self.ctx.request_repaint_after_secs(time_til_tooltip); return false; } } diff --git a/crates/egui/src/text_selection/visuals.rs b/crates/egui/src/text_selection/visuals.rs index d31f1756edd..252f727a650 100644 --- a/crates/egui/src/text_selection/visuals.rs +++ b/crates/egui/src/text_selection/visuals.rs @@ -99,8 +99,7 @@ pub fn paint_text_cursor( total_duration - time_in_cycle }; - ui.ctx() - .request_repaint_after(std::time::Duration::from_secs_f32(wake_in)); + ui.ctx().request_repaint_after_secs(wake_in); } else { paint_cursor_end(painter, ui.visuals(), primary_cursor_rect); } diff --git a/crates/egui_demo_lib/src/demo/tooltips.rs b/crates/egui_demo_lib/src/demo/tooltips.rs index c0960a70e3a..81aa8278efe 100644 --- a/crates/egui_demo_lib/src/demo/tooltips.rs +++ b/crates/egui_demo_lib/src/demo/tooltips.rs @@ -19,7 +19,8 @@ impl crate::Demo for Tooltips { use crate::View as _; let window = egui::Window::new("Tooltips") .constrain(false) // So we can test how tooltips behave close to the screen edge - .resizable(false) + .resizable(true) + .default_size([450.0, 300.0]) .scroll(false) .open(open); window.show(ctx, |ui| self.ui(ui)); @@ -34,6 +35,34 @@ impl crate::View for Tooltips { ui.add(crate::egui_github_link_file_line!()); }); + egui::SidePanel::right("scroll_test").show_inside(ui, |ui| { + ui.label( + "The scroll area below has many labels with interactive tooltips. \ + The purpose is to test that the tooltips close when you scroll.", + ) + .on_hover_text("Try hovering a label below, then scroll!"); + egui::ScrollArea::vertical() + .auto_shrink(false) + .show(ui, |ui| { + for i in 0..1000 { + ui.label(format!("This is line {i}")).on_hover_ui(|ui| { + ui.style_mut().interaction.selectable_labels = true; + ui.label( + "This tooltip is interactive, because the text in it is selectable.", + ); + }); + } + }); + }); + + egui::CentralPanel::default().show_inside(ui, |ui| { + self.misc_tests(ui); + }); + } +} + +impl Tooltips { + fn misc_tests(&mut self, ui: &mut egui::Ui) { ui.label("All labels in this demo have tooltips.") .on_hover_text("Yes, even this one.");