diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b3cf0b8daa..57a3bef2bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### 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)). +* Tooltips are only shown when mouse pointer is still ([#2263](https://github.com/emilk/egui/pull/2263)). ### Fixed 🐛 * ⚠️ BREAKING: Fix text being too small ([#2069](https://github.com/emilk/egui/pull/2069)). diff --git a/crates/egui/src/containers/area.rs b/crates/egui/src/containers/area.rs index 17ecea5ba54..befa0a7f375 100644 --- a/crates/egui/src/containers/area.rs +++ b/crates/egui/src/containers/area.rs @@ -2,8 +2,6 @@ //! It has no frame or own size. It is potentially movable. //! It is the foundation for windows and popups. -use std::{fmt::Debug, hash::Hash}; - use crate::*; /// State that is persisted between frames. @@ -56,9 +54,9 @@ pub struct Area { } impl Area { - pub fn new(id_source: impl Hash) -> Self { + pub fn new(id: impl Into) -> Self { Self { - id: Id::new(id_source), + id: id.into(), movable: true, interactable: true, enabled: true, diff --git a/crates/egui/src/containers/popup.rs b/crates/egui/src/containers/popup.rs index 9742ac43542..51381de696f 100644 --- a/crates/egui/src/containers/popup.rs +++ b/crates/egui/src/containers/popup.rs @@ -6,13 +6,13 @@ use crate::*; /// Same state for all tooltips. #[derive(Clone, Debug, Default)] -pub(crate) struct MonoState { - last_id: Option, - last_size: Vec, +pub(crate) struct TooltipState { + last_common_id: Option, + individual_ids_and_sizes: ahash::HashMap, } -impl MonoState { - fn load(ctx: &Context) -> Option { +impl TooltipState { + pub fn load(ctx: &Context) -> Option { ctx.data().get_temp(Id::null()) } @@ -20,30 +20,28 @@ impl MonoState { ctx.data().insert_temp(Id::null(), self); } - fn tooltip_size(&self, id: Id, index: usize) -> Option { - if self.last_id == Some(id) { - self.last_size.get(index).cloned() + fn individual_tooltip_size(&self, common_id: Id, index: usize) -> Option { + if self.last_common_id == Some(common_id) { + Some(self.individual_ids_and_sizes.get(&index).cloned()?.1) } else { None } } - fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) { - if self.last_id == Some(id) { - if let Some(stored_size) = self.last_size.get_mut(index) { - *stored_size = size; - } else { - self.last_size - .extend((0..index - self.last_size.len()).map(|_| Vec2::ZERO)); - self.last_size.push(size); - } - return; + fn set_individual_tooltip( + &mut self, + common_id: Id, + index: usize, + individual_id: Id, + size: Vec2, + ) { + if self.last_common_id != Some(common_id) { + self.last_common_id = Some(common_id); + self.individual_ids_and_sizes.clear(); } - self.last_id = Some(id); - self.last_size.clear(); - self.last_size.extend((0..index).map(|_| Vec2::ZERO)); - self.last_size.push(size); + self.individual_ids_and_sizes + .insert(index, (individual_id, size)); } } @@ -151,27 +149,30 @@ pub fn show_tooltip_at( fn show_tooltip_at_avoid_dyn<'c, R>( ctx: &Context, - mut id: Id, + individual_id: Id, suggested_position: Option, above: bool, mut avoid_rect: Rect, add_contents: Box R + 'c>, ) -> Option { - let mut tooltip_rect = Rect::NOTHING; - let mut count = 0; + let spacing = 4.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 { - // if there are multiple tooltips open they should use the same id for the `tooltip_size` caching to work. - id = stored.id; - tooltip_rect = stored.rect; - count = stored.count; - avoid_rect = avoid_rect.union(tooltip_rect); + let mut position = if frame_state.rect.is_positive() { + avoid_rect = avoid_rect.union(frame_state.rect); if above { - tooltip_rect.left_top() + frame_state.rect.left_top() - spacing * Vec2::Y } else { - tooltip_rect.left_bottom() + frame_state.rect.left_bottom() + spacing * Vec2::Y } } else if let Some(position) = suggested_position { position @@ -181,8 +182,9 @@ fn show_tooltip_at_avoid_dyn<'c, R>( return None; // No good place for a tooltip :( }; - let mut state = MonoState::load(ctx).unwrap_or_default(); - let expected_size = state.tooltip_size(id, count); + let mut long_state = TooltipState::load(ctx).unwrap_or_default(); + 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)); if above { @@ -195,31 +197,37 @@ fn show_tooltip_at_avoid_dyn<'c, R>( { 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 above { // place below instead: - position = avoid_rect.left_bottom(); + position = avoid_rect.left_bottom() + spacing * Vec2::Y; } else { // 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 area_id = frame_state.common_id.with(frame_state.count); + 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()); - state.store(ctx); + long_state.set_individual_tooltip( + 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) } @@ -247,12 +255,12 @@ pub fn show_tooltip_text(ctx: &Context, id: Id, text: impl Into) -> /// Show a pop-over window. fn show_tooltip_area_dyn<'c, R>( ctx: &Context, - id: Id, + area_id: Id, window_pos: Pos2, add_contents: Box R + 'c>, ) -> InnerResponse { use containers::*; - Area::new(id) + Area::new(area_id) .order(Order::Tooltip) .fixed_pos(window_pos) .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. /// /// Useful for drop-down menus (combo boxes) or suggestion menus under text fields. diff --git a/crates/egui/src/containers/window.rs b/crates/egui/src/containers/window.rs index ed2dc9a55e3..6182b9f85fa 100644 --- a/crates/egui/src/containers/window.rs +++ b/crates/egui/src/containers/window.rs @@ -39,7 +39,7 @@ impl<'open> Window<'open> { /// If you need a changing title, you must call `window.id(…)` with a fixed id. pub fn new(title: impl Into) -> Self { let title = title.into().fallback_text_style(TextStyle::Heading); - let area = Area::new(title.text()); + let area = Area::new(Id::new(title.text())); Self { title, open: None, diff --git a/crates/egui/src/frame_state.rs b/crates/egui/src/frame_state.rs index baa6ebb07bd..3b9589b1d02 100644 --- a/crates/egui/src/frame_state.rs +++ b/crates/egui/src/frame_state.rs @@ -3,8 +3,8 @@ use std::ops::RangeInclusive; use crate::*; #[derive(Clone, Copy, Debug)] -pub(crate) struct TooltipRect { - pub id: Id, +pub(crate) struct TooltipFrameState { + pub common_id: Id, pub rect: Rect, pub count: usize, } @@ -32,7 +32,7 @@ pub(crate) struct FrameState { /// If a tooltip has been shown this frame, where was it? /// This is used to prevent multiple tooltips to cover each other. /// Initialized to `None` at the start of each frame. - pub(crate) tooltip_rect: Option, + pub(crate) tooltip_state: Option, /// Set to [`InputState::scroll_delta`] on the start of each frame. /// @@ -50,7 +50,7 @@ impl Default for FrameState { available_rect: Rect::NAN, unused_rect: Rect::NAN, used_by_panels: Rect::NAN, - tooltip_rect: None, + tooltip_state: None, scroll_delta: Vec2::ZERO, scroll_target: [None, None], } @@ -64,7 +64,7 @@ impl FrameState { available_rect, unused_rect, used_by_panels, - tooltip_rect, + tooltip_state, scroll_delta, scroll_target, } = self; @@ -73,7 +73,7 @@ impl FrameState { *available_rect = input.screen_rect(); *unused_rect = input.screen_rect(); *used_by_panels = Rect::NOTHING; - *tooltip_rect = None; + *tooltip_state = None; *scroll_delta = input.scroll_delta; *scroll_target = [None, None]; } diff --git a/crates/egui/src/menu.rs b/crates/egui/src/menu.rs index a697e2dac8c..2c50c1926db 100644 --- a/crates/egui/src/menu.rs +++ b/crates/egui/src/menu.rs @@ -117,7 +117,7 @@ pub(crate) fn submenu_button( /// wrapper for the contents of every menu. pub(crate) fn menu_ui<'c, R>( ctx: &Context, - menu_id: impl std::hash::Hash, + menu_id: impl Into, menu_state_arc: &Arc>, add_contents: impl FnOnce(&mut Ui) -> R + 'c, ) -> InnerResponse { diff --git a/crates/egui/src/response.rs b/crates/egui/src/response.rs index 5b8f067c401..8ebf738ab6b 100644 --- a/crates/egui/src/response.rs +++ b/crates/egui/src/response.rs @@ -386,6 +386,11 @@ impl Response { 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 { if self.ctx.memory().everything_is_visible() { return true; @@ -395,12 +400,16 @@ impl Response { return false; } - if self.ctx.style().interaction.show_tooltips_only_when_still - && !self.ctx.input().pointer.is_still() - { - // wait for mouse to stop - self.ctx.request_repaint(); - return false; + if self.ctx.style().interaction.show_tooltips_only_when_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. + + let is_pointer_still = self.ctx.input().pointer.is_still(); + 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, diff --git a/crates/egui/src/style.rs b/crates/egui/src/style.rs index a92e723a737..195577fa66c 100644 --- a/crates/egui/src/style.rs +++ b/crates/egui/src/style.rs @@ -668,7 +668,7 @@ impl Default for Interaction { Self { resize_grab_radius_side: 5.0, resize_grab_radius_corner: 10.0, - show_tooltips_only_when_still: false, + show_tooltips_only_when_still: true, } } }