diff --git a/CHANGELOG.md b/CHANGELOG.md index b50edd81..67baa18d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Add `egui::plot::Plot` to plot some 2D data * Add `Ui::hyperlink_to(label, url)`. +### Changed 🔧 + +* Improve the positioning of tooltips. + + ## 0.9.0 - 2021-02-07 - Light Mode and much more diff --git a/egui/src/containers/popup.rs b/egui/src/containers/popup.rs index e0beffd4..70ca2319 100644 --- a/egui/src/containers/popup.rs +++ b/egui/src/containers/popup.rs @@ -11,21 +11,47 @@ use crate::*; /// ``` /// # let mut ui = egui::Ui::__test(); /// if ui.ui_contains_pointer() { -/// egui::show_tooltip(ui.ctx(), |ui| { +/// egui::show_tooltip(ui.ctx(), egui::Id::new("my_tooltip"), |ui| { /// ui.label("Helpful text"); /// }); /// } /// ``` -pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { - let tooltip_rect = ctx.frame_state().tooltip_rect; +pub fn show_tooltip(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui)) { + show_tooltip_at_pointer(ctx, id, add_contents) +} - let window_pos = if let Some(tooltip_rect) = tooltip_rect { +pub fn show_tooltip_at_pointer(ctx: &CtxRef, id: Id, add_contents: impl FnOnce(&mut Ui)) { + let suggested_pos = ctx + .input() + .pointer + .tooltip_pos() + .map(|pointer_pos| pointer_pos + vec2(16.0, 16.0)); + show_tooltip_at(ctx, id, suggested_pos, add_contents) +} + +pub fn show_tooltip_under(ctx: &CtxRef, id: Id, rect: &Rect, add_contents: impl FnOnce(&mut Ui)) { + show_tooltip_at( + ctx, + id, + Some(rect.left_bottom() + vec2(-2.0, 4.0)), + add_contents, + ) +} + +pub fn show_tooltip_at( + ctx: &CtxRef, + mut id: Id, + suggested_position: Option, + add_contents: impl FnOnce(&mut Ui), +) { + let mut tooltip_rect = Rect::NOTHING; + + let position = if let Some((stored_id, stored_tooltip_rect)) = ctx.frame_state().tooltip_rect { + // 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_tooltip_rect; tooltip_rect.left_bottom() - } else if let Some(pointer_pos) = ctx.input().pointer.tooltip_pos() { - let expected_size = vec2(ctx.style().spacing.tooltip_width, 32.0); - let position = pointer_pos + vec2(16.0, 16.0); - let position = position.min(ctx.input().screen_rect().right_bottom() - expected_size); - let position = position.max(ctx.input().screen_rect().left_top()); + } else if let Some(position) = suggested_position { position } else if ctx.memory().everything_is_visible() { Pos2::default() @@ -33,12 +59,15 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { return; // No good place for a tooltip :( }; - // TODO: default size - let id = Id::tooltip(); - let response = show_tooltip_area(ctx, id, window_pos, add_contents); + let expected_size = ctx.memory().tooltip_size(id); + let expected_size = expected_size.unwrap_or_else(|| vec2(64.0, 32.0)); + let position = position.min(ctx.input().screen_rect().right_bottom() - expected_size); + let position = position.max(ctx.input().screen_rect().left_top()); - let tooltip_rect = tooltip_rect.unwrap_or(Rect::NOTHING); - ctx.frame_state().tooltip_rect = Some(tooltip_rect.union(response.rect)); + let response = show_tooltip_area(ctx, id, position, add_contents); + ctx.memory().set_tooltip_size(id, response.rect.size()); + + ctx.frame_state().tooltip_rect = Some((id, tooltip_rect.union(response.rect))); } /// Show some text at the current pointer position (if any). @@ -50,11 +79,11 @@ pub fn show_tooltip(ctx: &CtxRef, add_contents: impl FnOnce(&mut Ui)) { /// ``` /// # let mut ui = egui::Ui::__test(); /// if ui.ui_contains_pointer() { -/// egui::show_tooltip_text(ui.ctx(), "Helpful text"); +/// egui::show_tooltip_text(ui.ctx(), egui::Id::new("my_tooltip"), "Helpful text"); /// } /// ``` -pub fn show_tooltip_text(ctx: &CtxRef, text: impl Into) { - show_tooltip(ctx, |ui| { +pub fn show_tooltip_text(ctx: &CtxRef, id: Id, text: impl Into) { + show_tooltip(ctx, id, |ui| { ui.add(crate::widgets::Label::new(text)); }) } diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index 3d7c0b91..84c4bf52 100644 --- a/egui/src/frame_state.rs +++ b/egui/src/frame_state.rs @@ -24,7 +24,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_rect: Option<(Id, Rect)>, /// Cleared by the first `ScrollArea` that makes use of it. pub(crate) scroll_delta: Vec2, diff --git a/egui/src/id.rs b/egui/src/id.rs index e077431f..7915954a 100644 --- a/egui/src/id.rs +++ b/egui/src/id.rs @@ -32,14 +32,11 @@ use std::hash::Hash; pub struct Id(u64); impl Id { - pub fn background() -> Self { + pub(crate) fn background() -> Self { Self(0) } - pub fn tooltip() -> Self { - Self(1) - } - + /// Generate a new `Id` by hashing some source (e.g. a string or integer). pub fn new(source: impl Hash) -> Id { // NOTE: AHasher is NOT suitable for this! use std::collections::hash_map::DefaultHasher; @@ -49,6 +46,7 @@ impl Id { Id(hasher.finish()) } + /// Generate a new `Id` by hashing the parent `Id` and the given argument. pub fn with(self, child: impl Hash) -> Id { // NOTE: AHasher is NOT suitable for this! use std::collections::hash_map::DefaultHasher; diff --git a/egui/src/memory.rs b/egui/src/memory.rs index 5789b32c..650f88c7 100644 --- a/egui/src/memory.rs +++ b/egui/src/memory.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ area, collapsing_header, menu, resize, scroll_area, util::Cache, widgets::text_edit, window, - Id, LayerId, Pos2, Rect, Style, + Id, LayerId, Pos2, Rect, Style, Vec2, }; use epaint::color::{Color32, Hsva}; @@ -51,6 +51,10 @@ pub struct Memory { #[cfg_attr(feature = "persistence", serde(skip))] popup: Option, + /// Used to clamp the tooltip to the screen. + #[cfg_attr(feature = "persistence", serde(skip))] + tooltip_size: Option<(Id, Vec2)>, + #[cfg_attr(feature = "persistence", serde(skip))] everything_is_visible: bool, } @@ -254,6 +258,29 @@ impl Memory { pub fn reset_areas(&mut self) { self.areas = Default::default(); } + + pub(crate) fn tooltip_size(&self, id: Id) -> Option { + if let Some((stored_id, stored_size)) = self.tooltip_size { + if stored_id == id { + Some(stored_size) + } else { + None + } + } else { + None + } + } + + pub(crate) fn set_tooltip_size(&mut self, id: Id, size: Vec2) { + if let Some((stored_id, stored_size)) = &mut self.tooltip_size { + if *stored_id == id { + *stored_size = stored_size.max(size); + return; + } + } + + self.tooltip_size = Some((id, size)); + } } /// ## Popups diff --git a/egui/src/response.rs b/egui/src/response.rs index a1765b47..4fa00520 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -193,7 +193,12 @@ impl Response { if (self.hovered() && self.ctx.input().pointer.tooltip_pos().is_some()) || self.ctx.memory().everything_is_visible() { - crate::containers::show_tooltip(&self.ctx, add_contents); + crate::containers::show_tooltip_under( + &self.ctx, + self.id.with("__tooltip"), + &self.rect, + add_contents, + ); } self } diff --git a/egui/src/style.rs b/egui/src/style.rs index 8e25a32e..3b80e719 100644 --- a/egui/src/style.rs +++ b/egui/src/style.rs @@ -293,7 +293,7 @@ impl Default for Spacing { text_edit_width: 280.0, icon_width: 16.0, icon_spacing: 0.0, - tooltip_width: 400.0, + tooltip_width: 600.0, } } } @@ -505,7 +505,7 @@ impl Spacing { ui.add(Slider::f32(text_edit_width, 0.0..=1000.0).text("text_edit_width")); ui.add(Slider::f32(icon_width, 0.0..=60.0).text("icon_width")); ui.add(Slider::f32(icon_spacing, 0.0..=10.0).text("icon_spacing")); - ui.add(Slider::f32(tooltip_width, 0.0..=10.0).text("tooltip_width")); + ui.add(Slider::f32(tooltip_width, 0.0..=1000.0).text("tooltip_width")); ui.vertical_centered(|ui| reset_button(ui, self)); }