From ac214c52c916ee4d130d170726462055792229df Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Sat, 18 Jan 2025 13:52:14 -0700 Subject: [PATCH] Delay hiding git blame tooltip (#22644) It's easy to overshoot the bottom of the tooltip when cursoring to a button, such as opening the commit from a blame tooltip. Before this change the tooltip would immediately disappear, and now it sticks around for a bit. Also: * Shares the implementation with `elements/text.rs`. This will particularly be handy when it makes use of hoverable tooltips. * Improves the fix to #21657. - Now the element will no longer think it has an active tooltip that it registers with the window. - It will instead display the next available tooltip, whereas I believe before the next available tooltip would be suppressed. * Fixes bug where `cx.refresh()` wasn't called when text tooltip is hidden due to a mouse down event. * Ports over fix in https://github.com/zed-industries/zed/pull/14832 to `elements/text.rs` Release Notes: - The tooltip for inline git blame now waits a bit before disappearing when the mouse leaves it. --- crates/gpui/src/app.rs | 9 +- crates/gpui/src/elements/div.rs | 383 +++++++++++++++++++++++-------- crates/gpui/src/elements/text.rs | 105 ++++----- crates/gpui/src/window.rs | 107 +++++---- 4 files changed, 390 insertions(+), 214 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index abc5bea372770bb99e84390f00123dcb799e7d35..772447eb6f80429d20a274bbc80bd6c6d095dee1 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1635,11 +1635,10 @@ pub struct AnyTooltip { /// The absolute position of the mouse when the tooltip was deployed. pub mouse_position: Point, - /// Whether the tooltitp can be hovered or not. - pub hoverable: bool, - - /// Bounds of the element that triggered the tooltip appearance. - pub origin_bounds: Bounds, + /// Given the bounds of the tooltip, checks whether the tooltip should still be visible and + /// updates its state accordingly. This is needed atop the hovered element's mouse move handler + /// to handle the case where the element is not painted (e.g. via use of `visible_on_hover`). + pub check_visible_and_update: Rc, &mut WindowContext) -> bool>, } /// A keystroke event, and potentially the associated action diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 30222b64e10eaa6af79d643e5ed037b2528b270a..cf33668ba36c827d47d019db6699e7098d04dbd8 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -42,7 +42,8 @@ use taffy::style::Overflow; use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; -pub(crate) const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +const TOOLTIP_SHOW_DELAY: Duration = Duration::from_millis(500); +const HOVERABLE_TOOLTIP_HIDE_DELAY: Duration = Duration::from_millis(500); /// The styling information for a given group. pub struct GroupStyle { @@ -1425,17 +1426,17 @@ impl Interactivity { element_state.map(|element_state| element_state.unwrap_or_default()); let style = self.compute_style_internal(None, element_state.as_mut(), cx); - if let Some(element_state) = element_state.as_ref() { + if let Some(element_state) = element_state.as_mut() { if let Some(clicked_state) = element_state.clicked_state.as_ref() { let clicked_state = clicked_state.borrow(); self.active = Some(clicked_state.element); } - if let Some(active_tooltip) = element_state.active_tooltip.as_ref() { - if let Some(active_tooltip) = active_tooltip.borrow().as_ref() { - if let Some(tooltip) = active_tooltip.tooltip.clone() { - self.tooltip_id = Some(cx.set_tooltip(tooltip)); - } + if self.tooltip_builder.is_some() { + self.tooltip_id = set_tooltip_on_window(active_tooltip, cx); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + element_state.active_tooltip.take(); } } } @@ -1935,13 +1936,7 @@ impl Interactivity { }); } - // Ensure to remove active tooltip if tooltip builder is none - if self.tooltip_builder.is_none() { - element_state.active_tooltip.take(); - } - if let Some(tooltip_builder) = self.tooltip_builder.take() { - let tooltip_is_hoverable = tooltip_builder.hoverable; let active_tooltip = element_state .active_tooltip .get_or_insert_with(Default::default) @@ -1951,85 +1946,24 @@ impl Interactivity { .get_or_insert_with(Default::default) .clone(); - cx.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - let hitbox = hitbox.clone(); - let source_bounds = hitbox.bounds; - let tooltip_id = self.tooltip_id; - move |_: &MouseMoveEvent, phase, cx| { - let is_hovered = - pending_mouse_down.borrow().is_none() && hitbox.is_hovered(cx); - let tooltip_is_hovered = - tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); - if !is_hovered && (!tooltip_is_hoverable || !tooltip_is_hovered) { - if active_tooltip.borrow_mut().take().is_some() { - cx.refresh(); - } - - return; - } - - if phase != DispatchPhase::Bubble { - return; - } - - if active_tooltip.borrow().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let build_tooltip = tooltip_builder.build.clone(); - move |mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - cx.update(|cx| { - active_tooltip.borrow_mut().replace(ActiveTooltip { - tooltip: Some(AnyTooltip { - view: build_tooltip(cx), - mouse_position: cx.mouse_position(), - hoverable: tooltip_is_hoverable, - origin_bounds: source_bounds, - }), - _task: None, - }); - cx.refresh(); - }) - .ok(); - } - }); - active_tooltip.borrow_mut().replace(ActiveTooltip { - tooltip: None, - _task: Some(task), - }); - } - } + let tooltip_is_hoverable = tooltip_builder.hoverable; + let build_tooltip = Rc::new(move |cx: &mut WindowContext| { + Some(((tooltip_builder.build)(cx), tooltip_is_hoverable)) }); - - cx.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - let tooltip_id = self.tooltip_id; - move |_: &MouseDownEvent, _, cx| { - let tooltip_is_hovered = - tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); - - if (!tooltip_is_hoverable || !tooltip_is_hovered) - && active_tooltip.borrow_mut().take().is_some() - { - cx.refresh(); - } - } + // Use bounds instead of testing hitbox since check_is_hovered is also called + // during prepaint. + let source_bounds = hitbox.bounds; + let check_is_hovered = Rc::new(move |cx: &WindowContext| { + pending_mouse_down.borrow().is_none() + && source_bounds.contains(&cx.mouse_position()) }); - - cx.on_mouse_event({ - let active_tooltip = active_tooltip.clone(); - let tooltip_id = self.tooltip_id; - move |_: &ScrollWheelEvent, _, cx| { - let tooltip_is_hovered = - tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)); - if (!tooltip_is_hoverable || !tooltip_is_hovered) - && active_tooltip.borrow_mut().take().is_some() - { - cx.refresh(); - } - } - }) + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + cx, + ); } let active_state = element_state @@ -2284,12 +2218,6 @@ pub struct InteractiveElementState { pub(crate) active_tooltip: Option>>>, } -/// The current active tooltip -pub struct ActiveTooltip { - pub(crate) tooltip: Option, - pub(crate) _task: Option>, -} - /// Whether or not the element or a group that contains it is clicked by the mouse. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct ElementClickedState { @@ -2306,6 +2234,269 @@ impl ElementClickedState { } } +pub(crate) enum ActiveTooltip { + /// Currently delaying before showing the tooltip. + WaitingForShow { _task: Task<()> }, + /// Tooltip is visible, element was hovered or for hoverable tooltips, the tooltip was hovered. + Visible { + tooltip: AnyTooltip, + is_hoverable: bool, + }, + /// Tooltip is visible and hoverable, but the mouse is no longer hovering. Currently delaying + /// before hiding it. + WaitingForHide { + tooltip: AnyTooltip, + _task: Task<()>, + }, +} + +pub(crate) fn clear_active_tooltip( + active_tooltip: &Rc>>, + cx: &mut WindowContext, +) { + match active_tooltip.borrow_mut().take() { + None => {} + Some(ActiveTooltip::WaitingForShow { .. }) => {} + Some(ActiveTooltip::Visible { .. }) => cx.refresh(), + Some(ActiveTooltip::WaitingForHide { .. }) => cx.refresh(), + } +} + +pub(crate) fn clear_active_tooltip_if_not_hoverable( + active_tooltip: &Rc>>, + cx: &mut WindowContext, +) { + let should_clear = match active_tooltip.borrow().as_ref() { + None => false, + Some(ActiveTooltip::WaitingForShow { .. }) => false, + Some(ActiveTooltip::Visible { is_hoverable, .. }) => !is_hoverable, + Some(ActiveTooltip::WaitingForHide { .. }) => false, + }; + if should_clear { + active_tooltip.borrow_mut().take(); + cx.refresh(); + } +} + +pub(crate) fn set_tooltip_on_window( + active_tooltip: &Rc>>, + cx: &mut WindowContext, +) -> Option { + let tooltip = match active_tooltip.borrow().as_ref() { + None => return None, + Some(ActiveTooltip::WaitingForShow { .. }) => return None, + Some(ActiveTooltip::Visible { tooltip, .. }) => tooltip.clone(), + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => tooltip.clone(), + }; + Some(cx.set_tooltip(tooltip)) +} + +pub(crate) fn register_tooltip_mouse_handlers( + active_tooltip: &Rc>>, + tooltip_id: Option, + build_tooltip: Rc Option<(AnyView, bool)>>, + check_is_hovered: Rc bool>, + cx: &mut WindowContext, +) { + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered = check_is_hovered.clone(); + move |_: &MouseMoveEvent, phase, cx| { + handle_tooltip_mouse_move( + &active_tooltip, + &build_tooltip, + &check_is_hovered, + phase, + cx, + ) + } + }); + + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &MouseDownEvent, _, cx| { + if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, cx); + } + } + }); + + cx.on_mouse_event({ + let active_tooltip = active_tooltip.clone(); + move |_: &ScrollWheelEvent, _, cx| { + if !tooltip_id.map_or(false, |tooltip_id| tooltip_id.is_hovered(cx)) { + clear_active_tooltip_if_not_hoverable(&active_tooltip, cx); + } + } + }); +} + +fn handle_tooltip_mouse_move( + active_tooltip: &Rc>>, + build_tooltip: &Rc Option<(AnyView, bool)>>, + check_is_hovered: &Rc bool>, + phase: DispatchPhase, + cx: &mut WindowContext, +) { + // Separates logic for what mutation should occur from applying it, to avoid overlapping + // RefCell borrows. + enum Action { + None, + CancelShow, + ScheduleShow, + } + + let action = match active_tooltip.borrow().as_ref() { + None => { + let is_hovered = check_is_hovered(cx); + if is_hovered && phase.bubble() { + Action::ScheduleShow + } else { + Action::None + } + } + Some(ActiveTooltip::WaitingForShow { .. }) => { + let is_hovered = check_is_hovered(cx); + if is_hovered { + Action::None + } else { + Action::CancelShow + } + } + // These are handled in check_visible_and_update. + Some(ActiveTooltip::Visible { .. }) | Some(ActiveTooltip::WaitingForHide { .. }) => { + Action::None + } + }; + + match action { + Action::None => {} + Action::CancelShow => { + // Cancel waiting to show tooltip when it is no longer hovered. + active_tooltip.borrow_mut().take(); + } + Action::ScheduleShow => { + let delayed_show_task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let build_tooltip = build_tooltip.clone(); + let check_is_hovered = check_is_hovered.clone(); + move |mut cx| async move { + cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await; + cx.update(|cx| { + let new_tooltip = build_tooltip(cx).map(|(view, tooltip_is_hoverable)| { + let active_tooltip = active_tooltip.clone(); + ActiveTooltip::Visible { + tooltip: AnyTooltip { + view, + mouse_position: cx.mouse_position(), + check_visible_and_update: Rc::new(move |tooltip_bounds, cx| { + handle_tooltip_check_visible_and_update( + &active_tooltip, + tooltip_is_hoverable, + &check_is_hovered, + tooltip_bounds, + cx, + ) + }), + }, + is_hoverable: tooltip_is_hoverable, + } + }); + *active_tooltip.borrow_mut() = new_tooltip; + cx.refresh(); + }) + .ok(); + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForShow { + _task: delayed_show_task, + }); + } + } +} + +/// Returns a callback which will be called by window prepaint to update tooltip visibility. The +/// purpose of doing this logic here instead of the mouse move handler is that the mouse move +/// handler won't get called when the element is not painted (e.g. via use of `visible_on_hover`). +fn handle_tooltip_check_visible_and_update( + active_tooltip: &Rc>>, + tooltip_is_hoverable: bool, + check_is_hovered: &Rc bool>, + tooltip_bounds: Bounds, + cx: &mut WindowContext, +) -> bool { + // Separates logic for what mutation should occur from applying it, to avoid overlapping RefCell + // borrows. + enum Action { + None, + Hide, + ScheduleHide(AnyTooltip), + CancelHide(AnyTooltip), + } + + let is_hovered = check_is_hovered(cx) + || (tooltip_is_hoverable && tooltip_bounds.contains(&cx.mouse_position())); + let action = match active_tooltip.borrow().as_ref() { + Some(ActiveTooltip::Visible { tooltip, .. }) => { + if is_hovered { + Action::None + } else { + if tooltip_is_hoverable { + Action::ScheduleHide(tooltip.clone()) + } else { + Action::Hide + } + } + } + Some(ActiveTooltip::WaitingForHide { tooltip, .. }) => { + if is_hovered { + Action::CancelHide(tooltip.clone()) + } else { + Action::None + } + } + None | Some(ActiveTooltip::WaitingForShow { .. }) => Action::None, + }; + + match action { + Action::None => {} + Action::Hide => { + clear_active_tooltip(&active_tooltip, cx); + } + Action::ScheduleHide(tooltip) => { + let delayed_hide_task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + move |mut cx| async move { + cx.background_executor() + .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) + .await; + if active_tooltip.borrow_mut().take().is_some() { + cx.update(|cx| cx.refresh()).ok(); + } + } + }); + active_tooltip + .borrow_mut() + .replace(ActiveTooltip::WaitingForHide { + tooltip, + _task: delayed_hide_task, + }); + } + Action::CancelHide(tooltip) => { + // Cancel waiting to hide tooltip when it becomes hovered. + active_tooltip.borrow_mut().replace(ActiveTooltip::Visible { + tooltip, + is_hoverable: true, + }); + } + } + + active_tooltip.borrow().is_some() +} + #[derive(Default)] pub(crate) struct GroupHitboxes(HashMap>); diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 07f51be4bf646426c5b2ec2f779b3180f7a24c11..8ebf8454103899653efc3acca639c9917f431d9f 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -1,8 +1,9 @@ use crate::{ - ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId, - HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext, - WrappedLine, WrappedLineLayout, TOOLTIP_DELAY, + register_tooltip_mouse_handlers, set_tooltip_on_window, ActiveTooltip, AnyView, Bounds, + DispatchPhase, Element, ElementId, GlobalElementId, HighlightStyle, Hitbox, IntoElement, + LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, + TextRun, TextStyle, TooltipId, Truncate, WhiteSpace, WindowContext, WrappedLine, + WrappedLineLayout, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; @@ -505,6 +506,7 @@ pub struct InteractiveText { Option], InteractiveTextClickEvent, &mut WindowContext)>>, hover_listener: Option, MouseMoveEvent, &mut WindowContext)>>, tooltip_builder: Option Option>>, + tooltip_id: Option, clickable_ranges: Vec>, } @@ -531,6 +533,7 @@ impl InteractiveText { click_listener: None, hover_listener: None, tooltip_builder: None, + tooltip_id: None, clickable_ranges: Vec::new(), } } @@ -600,15 +603,16 @@ impl Element for InteractiveText { cx.with_optional_element_state::( global_id, |interactive_state, cx| { - let interactive_state = interactive_state + let mut interactive_state = interactive_state .map(|interactive_state| interactive_state.unwrap_or_default()); - if let Some(interactive_state) = interactive_state.as_ref() { - if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref() - { - if let Some(tooltip) = active_tooltip.tooltip.clone() { - cx.set_tooltip(tooltip); - } + if let Some(interactive_state) = interactive_state.as_mut() { + if self.tooltip_builder.is_some() { + self.tooltip_id = + set_tooltip_on_window(&interactive_state.active_tooltip, cx); + } else { + // If there is no longer a tooltip builder, remove the active tooltip. + interactive_state.active_tooltip.take(); } } @@ -704,64 +708,37 @@ impl Element for InteractiveText { }); if let Some(tooltip_builder) = self.tooltip_builder.clone() { - let hitbox = hitbox.clone(); - let source_bounds = hitbox.bounds; let active_tooltip = interactive_state.active_tooltip.clone(); let pending_mouse_down = interactive_state.mouse_down_index.clone(); - let text_layout = text_layout.clone(); - - cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { - let position = text_layout.index_for_position(event.position).ok(); - let is_hovered = position.is_some() - && hitbox.is_hovered(cx) - && pending_mouse_down.get().is_none(); - if !is_hovered { - active_tooltip.take(); - return; - } - let position = position.unwrap(); - - if phase != DispatchPhase::Bubble { - return; - } - - if active_tooltip.borrow().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let tooltip_builder = tooltip_builder.clone(); - - move |mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - cx.update(|cx| { - let new_tooltip = - tooltip_builder(position, cx).map(|tooltip| { - ActiveTooltip { - tooltip: Some(AnyTooltip { - view: tooltip, - mouse_position: cx.mouse_position(), - hoverable: true, - origin_bounds: source_bounds, - }), - _task: None, - } - }); - *active_tooltip.borrow_mut() = new_tooltip; - cx.refresh(); - }) - .ok(); - } - }); - *active_tooltip.borrow_mut() = Some(ActiveTooltip { - tooltip: None, - _task: Some(task), - }); + let build_tooltip = Rc::new({ + let tooltip_is_hoverable = false; + let text_layout = text_layout.clone(); + move |cx: &mut WindowContext| { + text_layout + .index_for_position(cx.mouse_position()) + .ok() + .and_then(|position| tooltip_builder(position, cx)) + .map(|view| (view, tooltip_is_hoverable)) } }); - - let active_tooltip = interactive_state.active_tooltip.clone(); - cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { - active_tooltip.take(); + // Use bounds instead of testing hitbox since check_is_hovered is also + // called during prepaint. + let source_bounds = hitbox.bounds; + let check_is_hovered = Rc::new({ + let text_layout = text_layout.clone(); + move |cx: &WindowContext| { + text_layout.index_for_position(cx.mouse_position()).is_ok() + && source_bounds.contains(&cx.mouse_position()) + && pending_mouse_down.get().is_none() + } }); + register_tooltip_mouse_handlers( + &active_tooltip, + self.tooltip_id, + build_tooltip, + check_is_hovered, + cx, + ); } self.text.paint(None, bounds, &mut (), &mut (), cx); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e8f1740729f9936aedaf16b9881dda288b3da29f..e1a32da3165bd4855a2ebb97228b20e4bda11235 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1550,62 +1550,71 @@ impl<'a> WindowContext<'a> { } fn prepaint_tooltip(&mut self) -> Option { - let tooltip_request = self.window.next_frame.tooltip_requests.last().cloned()?; - let tooltip_request = tooltip_request.unwrap(); - let mut element = tooltip_request.tooltip.view.clone().into_any(); - let mouse_position = tooltip_request.tooltip.mouse_position; - let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); - - let mut tooltip_bounds = Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); - let window_bounds = Bounds { - origin: Point::default(), - size: self.viewport_size(), - }; + // Use indexing instead of iteration to avoid borrowing self for the duration of the loop. + for tooltip_request_index in (0..self.window.next_frame.tooltip_requests.len()).rev() { + let Some(Some(tooltip_request)) = self + .window + .next_frame + .tooltip_requests + .get(tooltip_request_index) + .cloned() + else { + log::error!("Unexpectedly absent TooltipRequest"); + continue; + }; + let mut element = tooltip_request.tooltip.view.clone().into_any(); + let mouse_position = tooltip_request.tooltip.mouse_position; + let tooltip_size = element.layout_as_root(AvailableSpace::min_size(), self); + + let mut tooltip_bounds = + Bounds::new(mouse_position + point(px(1.), px(1.)), tooltip_size); + let window_bounds = Bounds { + origin: Point::default(), + size: self.viewport_size(), + }; - if tooltip_bounds.right() > window_bounds.right() { - let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); - if new_x >= Pixels::ZERO { - tooltip_bounds.origin.x = new_x; - } else { - tooltip_bounds.origin.x = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), - ); + if tooltip_bounds.right() > window_bounds.right() { + let new_x = mouse_position.x - tooltip_bounds.size.width - px(1.); + if new_x >= Pixels::ZERO { + tooltip_bounds.origin.x = new_x; + } else { + tooltip_bounds.origin.x = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.x - tooltip_bounds.right() - window_bounds.right(), + ); + } } - } - if tooltip_bounds.bottom() > window_bounds.bottom() { - let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); - if new_y >= Pixels::ZERO { - tooltip_bounds.origin.y = new_y; - } else { - tooltip_bounds.origin.y = cmp::max( - Pixels::ZERO, - tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), - ); + if tooltip_bounds.bottom() > window_bounds.bottom() { + let new_y = mouse_position.y - tooltip_bounds.size.height - px(1.); + if new_y >= Pixels::ZERO { + tooltip_bounds.origin.y = new_y; + } else { + tooltip_bounds.origin.y = cmp::max( + Pixels::ZERO, + tooltip_bounds.origin.y - tooltip_bounds.bottom() - window_bounds.bottom(), + ); + } } - } - // Element's parent can get hidden (e.g. via the `visible_on_hover` method), - // and element's `paint` won't be called (ergo, mouse listeners also won't be active) to detect that the tooltip has to be removed. - // Ensure it's not stuck around in such cases. - let invalidate_tooltip = !tooltip_request - .tooltip - .origin_bounds - .contains(&self.mouse_position()) - && (!tooltip_request.tooltip.hoverable - || !tooltip_bounds.contains(&self.mouse_position())); - if invalidate_tooltip { - return None; - } + // It's possible for an element to have an active tooltip while not being painted (e.g. + // via the `visible_on_hover` method). Since mouse listeners are not active in this + // case, instead update the tooltip's visibility here. + let is_visible = + (tooltip_request.tooltip.check_visible_and_update)(tooltip_bounds, self); + if !is_visible { + continue; + } - self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); + self.with_absolute_element_offset(tooltip_bounds.origin, |cx| element.prepaint(cx)); - self.window.tooltip_bounds = Some(TooltipBounds { - id: tooltip_request.id, - bounds: tooltip_bounds, - }); - Some(element) + self.window.tooltip_bounds = Some(TooltipBounds { + id: tooltip_request.id, + bounds: tooltip_bounds, + }); + return Some(element); + } + None } fn prepaint_deferred_draws(&mut self, deferred_draw_indices: &[usize]) {