Delay hiding git blame tooltip (#22644)

Michael Sloan created

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.

Change summary

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(-)

Detailed changes

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<Pixels>,
 
-    /// Whether the tooltitp can be hovered or not.
-    pub hoverable: bool,
-
-    /// Bounds of the element that triggered the tooltip appearance.
-    pub origin_bounds: Bounds<Pixels>,
+    /// 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<dyn Fn(Bounds<Pixels>, &mut WindowContext) -> bool>,
 }
 
 /// A keystroke event, and potentially the associated action

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<Rc<RefCell<Option<ActiveTooltip>>>>,
 }
 
-/// The current active tooltip
-pub struct ActiveTooltip {
-    pub(crate) tooltip: Option<AnyTooltip>,
-    pub(crate) _task: Option<Task<()>>,
-}
-
 /// 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<RefCell<Option<ActiveTooltip>>>,
+    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<RefCell<Option<ActiveTooltip>>>,
+    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<RefCell<Option<ActiveTooltip>>>,
+    cx: &mut WindowContext,
+) -> Option<TooltipId> {
+    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<RefCell<Option<ActiveTooltip>>>,
+    tooltip_id: Option<TooltipId>,
+    build_tooltip: Rc<dyn Fn(&mut WindowContext) -> Option<(AnyView, bool)>>,
+    check_is_hovered: Rc<dyn Fn(&WindowContext) -> 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<RefCell<Option<ActiveTooltip>>>,
+    build_tooltip: &Rc<dyn Fn(&mut WindowContext) -> Option<(AnyView, bool)>>,
+    check_is_hovered: &Rc<dyn Fn(&WindowContext) -> 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<RefCell<Option<ActiveTooltip>>>,
+    tooltip_is_hoverable: bool,
+    check_is_hovered: &Rc<dyn Fn(&WindowContext) -> bool>,
+    tooltip_bounds: Bounds<Pixels>,
+    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<SharedString, SmallVec<[HitboxId; 1]>>);
 

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<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext)>>,
     hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext)>>,
     tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext) -> Option<AnyView>>>,
+    tooltip_id: Option<TooltipId>,
     clickable_ranges: Vec<Range<usize>>,
 }
 
@@ -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::<InteractiveTextState, _>(
             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);

crates/gpui/src/window.rs 🔗

@@ -1550,62 +1550,71 @@ impl<'a> WindowContext<'a> {
     }
 
     fn prepaint_tooltip(&mut self) -> Option<AnyElement> {
-        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]) {