Only show tooltips when mouse pointer is still (#2263)

* Only show tooltips when mouse pointer is still

Revert to the old behavior by setting
`style.interaction.show_tooltips_only_when_still = false`.

* Area: take `impl Into<Id>`

* refactor tooltips

* Fix was_tooltip_open_last_frame

* Bug fix

* Add some spacing between tooltips
This commit is contained in:
Emil Ernerfeldt 2022-11-09 19:35:08 +01:00 committed by GitHub
parent 51ff32797d
commit b1e71d308f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 68 deletions

View file

@ -21,6 +21,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
### Changed 🔧 ### Changed 🔧
* Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)). * Panels always have a separator line, but no stroke on other sides. Their spacing has also changed slightly ([#2261](https://github.com/emilk/egui/pull/2261)).
* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)).
### Fixed 🐛 ### Fixed 🐛
* ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). * ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)).

View file

@ -2,8 +2,6 @@
//! It has no frame or own size. It is potentially movable. //! It has no frame or own size. It is potentially movable.
//! It is the foundation for windows and popups. //! It is the foundation for windows and popups.
use std::{fmt::Debug, hash::Hash};
use crate::*; use crate::*;
/// State that is persisted between frames. /// State that is persisted between frames.
@ -56,9 +54,9 @@ pub struct Area {
} }
impl Area { impl Area {
pub fn new(id_source: impl Hash) -> Self { pub fn new(id: impl Into<Id>) -> Self {
Self { Self {
id: Id::new(id_source), id: id.into(),
movable: true, movable: true,
interactable: true, interactable: true,
enabled: true, enabled: true,

View file

@ -6,13 +6,13 @@ use crate::*;
/// Same state for all tooltips. /// Same state for all tooltips.
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub(crate) struct MonoState { pub(crate) struct TooltipState {
last_id: Option<Id>, last_common_id: Option<Id>,
last_size: Vec<Vec2>, individual_ids_and_sizes: ahash::HashMap<usize, (Id, Vec2)>,
} }
impl MonoState { impl TooltipState {
fn load(ctx: &Context) -> Option<Self> { pub fn load(ctx: &Context) -> Option<Self> {
ctx.data().get_temp(Id::null()) ctx.data().get_temp(Id::null())
} }
@ -20,30 +20,28 @@ impl MonoState {
ctx.data().insert_temp(Id::null(), self); ctx.data().insert_temp(Id::null(), self);
} }
fn tooltip_size(&self, id: Id, index: usize) -> Option<Vec2> { fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option<Vec2> {
if self.last_id == Some(id) { if self.last_common_id == Some(common_id) {
self.last_size.get(index).cloned() Some(self.individual_ids_and_sizes.get(&index).cloned()?.1)
} else { } else {
None None
} }
} }
fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) { fn set_individual_tooltip(
if self.last_id == Some(id) { &mut self,
if let Some(stored_size) = self.last_size.get_mut(index) { common_id: Id,
*stored_size = size; index: usize,
} else { individual_id: Id,
self.last_size size: Vec2,
.extend((0..index - self.last_size.len()).map(|_| Vec2::ZERO)); ) {
self.last_size.push(size); if self.last_common_id != Some(common_id) {
} self.last_common_id = Some(common_id);
return; self.individual_ids_and_sizes.clear();
} }
self.last_id = Some(id); self.individual_ids_and_sizes
self.last_size.clear(); .insert(index, (individual_id, size));
self.last_size.extend((0..index).map(|_| Vec2::ZERO));
self.last_size.push(size);
} }
} }
@ -151,27 +149,30 @@ pub fn show_tooltip_at<R>(
fn show_tooltip_at_avoid_dyn<'c, R>( fn show_tooltip_at_avoid_dyn<'c, R>(
ctx: &Context, ctx: &Context,
mut id: Id, individual_id: Id,
suggested_position: Option<Pos2>, suggested_position: Option<Pos2>,
above: bool, above: bool,
mut avoid_rect: Rect, mut avoid_rect: Rect,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> Option<R> { ) -> Option<R> {
let mut tooltip_rect = Rect::NOTHING; let spacing = 4.0;
let mut count = 0;
let stored = ctx.frame_state().tooltip_rect; // if there are multiple tooltips open they should use the same common_id for the `tooltip_size` caching to work.
let mut frame_state =
ctx.frame_state()
.tooltip_state
.unwrap_or(crate::frame_state::TooltipFrameState {
common_id: individual_id,
rect: Rect::NOTHING,
count: 0,
});
let mut position = if let Some(stored) = stored { let mut position = if frame_state.rect.is_positive() {
// if there are multiple tooltips open they should use the same id for the `tooltip_size` caching to work. avoid_rect = avoid_rect.union(frame_state.rect);
id = stored.id;
tooltip_rect = stored.rect;
count = stored.count;
avoid_rect = avoid_rect.union(tooltip_rect);
if above { if above {
tooltip_rect.left_top() frame_state.rect.left_top() - spacing * Vec2::Y
} else { } else {
tooltip_rect.left_bottom() frame_state.rect.left_bottom() + spacing * Vec2::Y
} }
} else if let Some(position) = suggested_position { } else if let Some(position) = suggested_position {
position position
@ -181,8 +182,9 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
return None; // No good place for a tooltip :( return None; // No good place for a tooltip :(
}; };
let mut state = MonoState::load(ctx).unwrap_or_default(); let mut long_state = TooltipState::load(ctx).unwrap_or_default();
let expected_size = state.tooltip_size(id, count); let expected_size =
long_state.individual_tooltip_size(frame_state.common_id, frame_state.count);
let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0)); let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0));
if above { if above {
@ -195,31 +197,37 @@ fn show_tooltip_at_avoid_dyn<'c, R>(
{ {
let new_rect = Rect::from_min_size(position, expected_size); let new_rect = Rect::from_min_size(position, expected_size);
// Note: We do not use Rect::intersects() since it returns true even if the rects only touch. // Note: We use shrink so that we don't get false positives when the rects just touch
if new_rect.shrink(1.0).intersects(avoid_rect) { if new_rect.shrink(1.0).intersects(avoid_rect) {
if above { if above {
// place below instead: // place below instead:
position = avoid_rect.left_bottom(); position = avoid_rect.left_bottom() + spacing * Vec2::Y;
} else { } else {
// place above instead: // place above instead:
position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y); position = Pos2::new(position.x, avoid_rect.min.y - expected_size.y - spacing);
} }
} }
} }
let position = position.at_least(ctx.input().screen_rect().min); let position = position.at_least(ctx.input().screen_rect().min);
let area_id = frame_state.common_id.with(frame_state.count);
let InnerResponse { inner, response } = let InnerResponse { inner, response } =
show_tooltip_area_dyn(ctx, id.with(count), position, add_contents); show_tooltip_area_dyn(ctx, area_id, position, add_contents);
state.set_tooltip_size(id, count, response.rect.size()); long_state.set_individual_tooltip(
state.store(ctx); frame_state.common_id,
frame_state.count,
individual_id,
response.rect.size(),
);
long_state.store(ctx);
frame_state.count += 1;
frame_state.rect = frame_state.rect.union(response.rect);
ctx.frame_state().tooltip_state = Some(frame_state);
ctx.frame_state().tooltip_rect = Some(crate::frame_state::TooltipRect {
id,
rect: tooltip_rect.union(response.rect),
count: count + 1,
});
Some(inner) Some(inner)
} }
@ -247,12 +255,12 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into<WidgetText>) ->
/// Show a pop-over window. /// Show a pop-over window.
fn show_tooltip_area_dyn<'c, R>( fn show_tooltip_area_dyn<'c, R>(
ctx: &Context, ctx: &Context,
id: Id, area_id: Id,
window_pos: Pos2, window_pos: Pos2,
add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>, add_contents: Box<dyn FnOnce(&mut Ui) -> R + 'c>,
) -> InnerResponse<R> { ) -> InnerResponse<R> {
use containers::*; use containers::*;
Area::new(id) Area::new(area_id)
.order(Order::Tooltip) .order(Order::Tooltip)
.fixed_pos(window_pos) .fixed_pos(window_pos)
.interactable(false) .interactable(false)
@ -267,6 +275,25 @@ fn show_tooltip_area_dyn<'c, R>(
}) })
} }
/// Was this popup visible last frame?
pub fn was_tooltip_open_last_frame(ctx: &Context, tooltip_id: Id) -> bool {
if let Some(state) = TooltipState::load(ctx) {
if let Some(common_id) = state.last_common_id {
for (count, (individual_id, _size)) in &state.individual_ids_and_sizes {
if *individual_id == tooltip_id {
let area_id = common_id.with(count);
let layer_id = LayerId::new(Order::Tooltip, area_id);
if ctx.memory().areas.visible_last_frame(&layer_id) {
return true;
}
}
}
}
}
false
}
/// Shows a popup below another widget. /// Shows a popup below another widget.
/// ///
/// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. /// Useful for drop-down menus (combo boxes) or suggestion menus under text fields.

View file

@ -39,7 +39,7 @@ impl<'open> Window<'open> {
/// If you need a changing title, you must call `window.id(…)` with a fixed id. /// If you need a changing title, you must call `window.id(…)` with a fixed id.
pub fn new(title: impl Into<WidgetText>) -> Self { pub fn new(title: impl Into<WidgetText>) -> Self {
let title = title.into().fallback_text_style(TextStyle::Heading); let title = title.into().fallback_text_style(TextStyle::Heading);
let area = Area::new(title.text()); let area = Area::new(Id::new(title.text()));
Self { Self {
title, title,
open: None, open: None,

View file

@ -3,8 +3,8 @@ use std::ops::RangeInclusive;
use crate::*; use crate::*;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub(crate) struct TooltipRect { pub(crate) struct TooltipFrameState {
pub id: Id, pub common_id: Id,
pub rect: Rect, pub rect: Rect,
pub count: usize, pub count: usize,
} }
@ -32,7 +32,7 @@ pub(crate) struct FrameState {
/// If a tooltip has been shown this frame, where was it? /// If a tooltip has been shown this frame, where was it?
/// This is used to prevent multiple tooltips to cover each other. /// This is used to prevent multiple tooltips to cover each other.
/// Initialized to `None` at the start of each frame. /// Initialized to `None` at the start of each frame.
pub(crate) tooltip_rect: Option<TooltipRect>, pub(crate) tooltip_state: Option<TooltipFrameState>,
/// Set to [`InputState::scroll_delta`] on the start of each frame. /// Set to [`InputState::scroll_delta`] on the start of each frame.
/// ///
@ -50,7 +50,7 @@ impl Default for FrameState {
available_rect: Rect::NAN, available_rect: Rect::NAN,
unused_rect: Rect::NAN, unused_rect: Rect::NAN,
used_by_panels: Rect::NAN, used_by_panels: Rect::NAN,
tooltip_rect: None, tooltip_state: None,
scroll_delta: Vec2::ZERO, scroll_delta: Vec2::ZERO,
scroll_target: [None, None], scroll_target: [None, None],
} }
@ -64,7 +64,7 @@ impl FrameState {
available_rect, available_rect,
unused_rect, unused_rect,
used_by_panels, used_by_panels,
tooltip_rect, tooltip_state,
scroll_delta, scroll_delta,
scroll_target, scroll_target,
} = self; } = self;
@ -73,7 +73,7 @@ impl FrameState {
*available_rect = input.screen_rect(); *available_rect = input.screen_rect();
*unused_rect = input.screen_rect(); *unused_rect = input.screen_rect();
*used_by_panels = Rect::NOTHING; *used_by_panels = Rect::NOTHING;
*tooltip_rect = None; *tooltip_state = None;
*scroll_delta = input.scroll_delta; *scroll_delta = input.scroll_delta;
*scroll_target = [None, None]; *scroll_target = [None, None];
} }

View file

@ -117,7 +117,7 @@ pub(crate) fn submenu_button<R>(
/// wrapper for the contents of every menu. /// wrapper for the contents of every menu.
pub(crate) fn menu_ui<'c, R>( pub(crate) fn menu_ui<'c, R>(
ctx: &Context, ctx: &Context,
menu_id: impl std::hash::Hash, menu_id: impl Into<Id>,
menu_state_arc: &Arc<RwLock<MenuState>>, menu_state_arc: &Arc<RwLock<MenuState>>,
add_contents: impl FnOnce(&mut Ui) -> R + 'c, add_contents: impl FnOnce(&mut Ui) -> R + 'c,
) -> InnerResponse<R> { ) -> InnerResponse<R> {

View file

@ -386,6 +386,11 @@ impl Response {
self self
} }
/// Was the tooltip open last frame?
pub fn is_tooltip_open(&self) -> bool {
crate::popup::was_tooltip_open_last_frame(&self.ctx, self.id.with("__tooltip"))
}
fn should_show_hover_ui(&self) -> bool { fn should_show_hover_ui(&self) -> bool {
if self.ctx.memory().everything_is_visible() { if self.ctx.memory().everything_is_visible() {
return true; return true;
@ -395,12 +400,16 @@ impl Response {
return false; return false;
} }
if self.ctx.style().interaction.show_tooltips_only_when_still if self.ctx.style().interaction.show_tooltips_only_when_still {
&& !self.ctx.input().pointer.is_still() // We only show the tooltip when the mouse pointer is still,
{ // but once shown we keep showing it until the mouse leaves the parent.
// wait for mouse to stop
self.ctx.request_repaint(); let is_pointer_still = self.ctx.input().pointer.is_still();
return false; if !is_pointer_still && !self.is_tooltip_open() {
// wait for mouse to stop
self.ctx.request_repaint();
return false;
}
} }
// We don't want tooltips of things while we are dragging them, // We don't want tooltips of things while we are dragging them,

View file

@ -668,7 +668,7 @@ impl Default for Interaction {
Self { Self {
resize_grab_radius_side: 5.0, resize_grab_radius_side: 5.0,
resize_grab_radius_corner: 10.0, resize_grab_radius_corner: 10.0,
show_tooltips_only_when_still: false, show_tooltips_only_when_still: true,
} }
} }
} }