diff --git a/egui/src/containers/popup.rs b/egui/src/containers/popup.rs index de2978ea..54b5f25c 100644 --- a/egui/src/containers/popup.rs +++ b/egui/src/containers/popup.rs @@ -8,28 +8,34 @@ use crate::*; #[derive(Clone, Debug, Default)] pub(crate) struct MonoState { last_id: Option, - last_size: Option, + last_size: Vec, } impl MonoState { - fn tooltip_size(&self, id: Id) -> Option { + fn tooltip_size(&self, id: Id, index: usize) -> Option { if self.last_id == Some(id) { - self.last_size + self.last_size.get(index).cloned() } else { None } } - fn set_tooltip_size(&mut self, id: Id, size: Vec2) { + fn set_tooltip_size(&mut self, id: Id, index: usize, size: Vec2) { if self.last_id == Some(id) { - if let Some(stored_size) = &mut self.last_size { - *stored_size = stored_size.max(size); - return; + 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; } self.last_id = Some(id); - self.last_size = Some(size); + self.last_size.clear(); + self.last_size.extend((0..index).map(|_| Vec2::ZERO)); + self.last_size.push(size); } } @@ -84,16 +90,21 @@ pub fn show_tooltip_at_pointer( show_tooltip_at(ctx, id, suggested_pos, add_contents) } +/// Show a tooltip under the given area. +/// +/// If the tooltip does not fit under the area, it tries to place it above it instead. pub fn show_tooltip_under( ctx: &CtxRef, id: Id, rect: &Rect, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - show_tooltip_at( + let expanded_rect = rect.expand2(vec2(2.0, 4.0)); + show_tooltip_at_avoid( ctx, id, - Some(rect.left_bottom() + vec2(-2.0, 4.0)), + Some(expanded_rect.left_bottom()), + expanded_rect, add_contents, ) } @@ -103,16 +114,31 @@ pub fn show_tooltip_under( /// Returns `None` if the tooltip could not be placed. pub fn show_tooltip_at( ctx: &CtxRef, - mut id: Id, + id: Id, suggested_position: Option, add_contents: impl FnOnce(&mut Ui) -> R, ) -> Option { - let mut tooltip_rect = Rect::NOTHING; + show_tooltip_at_avoid(ctx, id, suggested_position, Rect::NOTHING, add_contents) +} - let position = if let Some((stored_id, stored_tooltip_rect)) = ctx.frame_state().tooltip_rect { +fn show_tooltip_at_avoid( + ctx: &CtxRef, + mut id: Id, + suggested_position: Option, + mut avoid_rect: Rect, + add_contents: impl FnOnce(&mut Ui) -> R, +) -> Option { + let mut tooltip_rect = Rect::NOTHING; + let mut count = 0; + + let position = if let Some((stored_id, stored_tooltip_rect, stored_count)) = + 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; + count = stored_count; + avoid_rect = avoid_rect.union(tooltip_rect); tooltip_rect.left_bottom() } else if let Some(position) = suggested_position { position @@ -126,18 +152,31 @@ pub fn show_tooltip_at( .memory() .data_temp .get_or_default::() - .tooltip_size(id); + .tooltip_size(id, count); 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); + // Place the tooltip above the avoid_rect if necessary. + 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. + let position = if avoid_rect.min.x < new_rect.max.x + && new_rect.min.x < avoid_rect.max.x + && avoid_rect.min.y < new_rect.max.y + && new_rect.min.y < avoid_rect.max.y + { + Pos2::new(position.x, avoid_rect.min.y - expected_size.y) + } else { + position + }; + let position = position.max(ctx.input().screen_rect().left_top()); let InnerResponse { inner, response } = show_tooltip_area(ctx, id, position, add_contents); ctx.memory() .data_temp .get_mut_or_default::() - .set_tooltip_size(id, response.rect.size()); + .set_tooltip_size(id, count, response.rect.size()); - ctx.frame_state().tooltip_rect = Some((id, tooltip_rect.union(response.rect))); + ctx.frame_state().tooltip_rect = Some((id, tooltip_rect.union(response.rect), count + 1)); Some(inner) } diff --git a/egui/src/frame_state.rs b/egui/src/frame_state.rs index df932365..579ed16c 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<(Id, Rect)>, + pub(crate) tooltip_rect: Option<(Id, Rect, usize)>, /// Cleared by the first `ScrollArea` that makes use of it. pub(crate) scroll_delta: Vec2,