Skip to content

Commit

Permalink
Hide tooltips when scrolling (#4784)
Browse files Browse the repository at this point in the history
* Closes #4781
  • Loading branch information
emilk authored Jul 5, 2024
1 parent 1431199 commit 977d83a
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 13 deletions.
10 changes: 10 additions & 0 deletions crates/egui/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions crates/egui/src/input_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ pub struct InputState {
/// (We keep a separate [`TouchState`] for each encountered touch device.)
touch_states: BTreeMap<TouchDeviceId, TouchState>,

// ----------------------------------------------
// Scrolling:
//
/// Time of the last scroll event.
last_scroll_time: f64,

/// Used for smoothing the scroll delta.
unprocessed_scroll_delta: Vec2,

Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1218,6 +1244,7 @@ impl InputState {
pointer,
touch_states,

last_scroll_time,
unprocessed_scroll_delta,
unprocessed_scroll_delta_for_zoom,
raw_scroll_delta,
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down
36 changes: 26 additions & 10 deletions crates/egui/src/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -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;
}
}
Expand Down
3 changes: 1 addition & 2 deletions crates/egui/src/text_selection/visuals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
31 changes: 30 additions & 1 deletion crates/egui_demo_lib/src/demo/tooltips.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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.");

Expand Down

0 comments on commit 977d83a

Please sign in to comment.