diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index f453e1c52be1b45252b37dbe4881ae95e898c975..4a3fc3b82e472807520e34d5d423f38a0ba7c7ea 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -38,7 +38,7 @@ use std::{ fmt::Debug, marker::PhantomData, mem, - rc::{Rc, Weak}, + rc::Rc, sync::Arc, time::Duration, }; @@ -2878,7 +2878,7 @@ pub struct InteractiveElementState { pub(crate) hover_listener_state: Option>>, pub(crate) pending_mouse_down: Option>>>, pub(crate) scroll_offset: Option>>>, - pub(crate) active_tooltip: Option, + pub(crate) active_tooltip: Option>>>, } /// Whether or not the element or a group that contains it is clicked by the mouse. @@ -2907,9 +2907,6 @@ pub struct ElementHoverState { pub element: bool, } -type ActiveTooltipState = Rc>>; -type WeakActiveTooltipState = Weak>>; - pub(crate) enum ActiveTooltip { /// Currently delaying before showing the tooltip. WaitingForShow { _task: Task<()> }, @@ -2926,7 +2923,10 @@ pub(crate) enum ActiveTooltip { }, } -pub(crate) fn clear_active_tooltip(active_tooltip: &ActiveTooltipState, window: &mut Window) { +pub(crate) fn clear_active_tooltip( + active_tooltip: &Rc>>, + window: &mut Window, +) { match active_tooltip.borrow_mut().take() { None => {} Some(ActiveTooltip::WaitingForShow { .. }) => {} @@ -2936,7 +2936,7 @@ pub(crate) fn clear_active_tooltip(active_tooltip: &ActiveTooltipState, window: } pub(crate) fn clear_active_tooltip_if_not_hoverable( - active_tooltip: &ActiveTooltipState, + active_tooltip: &Rc>>, window: &mut Window, ) { let should_clear = match active_tooltip.borrow().as_ref() { @@ -2952,7 +2952,7 @@ pub(crate) fn clear_active_tooltip_if_not_hoverable( } pub(crate) fn set_tooltip_on_window( - active_tooltip: &ActiveTooltipState, + active_tooltip: &Rc>>, window: &mut Window, ) -> Option { let tooltip = match active_tooltip.borrow().as_ref() { @@ -2965,7 +2965,7 @@ pub(crate) fn set_tooltip_on_window( } pub(crate) fn register_tooltip_mouse_handlers( - active_tooltip: &ActiveTooltipState, + active_tooltip: &Rc>>, tooltip_id: Option, build_tooltip: Rc Option<(AnyView, bool)>>, check_is_hovered: Rc bool>, @@ -3020,7 +3020,7 @@ pub(crate) fn register_tooltip_mouse_handlers( /// does not know if the hitbox is occluded. In the case where a tooltip gets displayed and then /// gets occluded after display, it will stick around until the mouse exits the hover bounds. fn handle_tooltip_mouse_move( - active_tooltip: &ActiveTooltipState, + active_tooltip: &Rc>>, build_tooltip: &Rc Option<(AnyView, bool)>>, check_is_hovered: &Rc bool>, check_is_hovered_during_prepaint: &Rc bool>, @@ -3123,7 +3123,7 @@ fn handle_tooltip_mouse_move( /// 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: &ActiveTooltipState, + active_tooltip: &Rc>>, tooltip_is_hoverable: bool, check_is_hovered: &Rc bool>, tooltip_bounds: Bounds, @@ -3168,7 +3168,7 @@ fn handle_tooltip_check_visible_and_update( Action::Hide => clear_active_tooltip(active_tooltip, window), Action::ScheduleHide(tooltip) => { let delayed_hide_task = window.spawn(cx, { - let weak_active_tooltip: WeakActiveTooltipState = Rc::downgrade(active_tooltip); + let weak_active_tooltip = Rc::downgrade(active_tooltip); async move |cx| { cx.background_executor() .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) @@ -3588,6 +3588,62 @@ impl ScrollHandle { #[cfg(test)] mod tests { use super::*; + use crate::{AppContext as _, Context, Modifiers, TestAppContext, size}; + use std::sync::{ + Arc, + atomic::{AtomicUsize, Ordering::SeqCst}, + }; + + struct TooltipLifecycleCounts { + created: AtomicUsize, + dropped: AtomicUsize, + } + + struct DropTrackingTooltip { + tooltip_lifecycle_counts: Arc, + } + + impl Drop for DropTrackingTooltip { + fn drop(&mut self) { + self.tooltip_lifecycle_counts.dropped.fetch_add(1, SeqCst); + } + } + + impl Render for DropTrackingTooltip { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(20.)).h(px(20.)).child("tooltip") + } + } + + struct TooltipOwner { + show_target: bool, + tooltip_lifecycle_counts: Arc, + } + + impl Render for TooltipOwner { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let root = div().size_full(); + if self.show_target { + let tooltip_lifecycle_counts = self.tooltip_lifecycle_counts.clone(); + root.child( + div() + .id("target") + .w(px(50.)) + .h(px(50.)) + .tooltip(move |_window, cx| { + tooltip_lifecycle_counts.created.fetch_add(1, SeqCst); + let tooltip_lifecycle_counts = tooltip_lifecycle_counts.clone(); + cx.new(|_| DropTrackingTooltip { + tooltip_lifecycle_counts, + }) + .into() + }), + ) + } else { + root + } + } + } #[test] fn scroll_handle_aligns_wide_children_to_left_edge() { @@ -3626,4 +3682,49 @@ mod tests { assert_eq!(handle.offset().y, px(-25.)); } + + #[test] + fn tooltip_is_released_when_its_owner_disappears() { + let mut test_app = TestAppContext::single(); + let tooltip_lifecycle_counts = Arc::new(TooltipLifecycleCounts { + created: AtomicUsize::new(0), + dropped: AtomicUsize::new(0), + }); + + let (view, cx) = test_app.add_window_view({ + let tooltip_lifecycle_counts = tooltip_lifecycle_counts.clone(); + move |_window, _cx| TooltipOwner { + show_target: true, + tooltip_lifecycle_counts, + } + }); + + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + view.clone().into_any_element() + }); + cx.simulate_mouse_move(point(px(10.), px(10.)), None, Modifiers::default()); + cx.run_until_parked(); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + view.clone().into_any_element() + }); + + assert_eq!(tooltip_lifecycle_counts.created.load(SeqCst), 1); + assert_eq!(tooltip_lifecycle_counts.dropped.load(SeqCst), 0); + + cx.update(|_window, app| { + view.update(app, |tooltip_owner, cx| { + tooltip_owner.show_target = false; + cx.notify(); + }); + }); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + view.clone().into_any_element() + }); + cx.run_until_parked(); + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, _| { + view.clone().into_any_element() + }); + + assert_eq!(tooltip_lifecycle_counts.dropped.load(SeqCst), 1); + } }