diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index cc4f586a3dce937c310e177eefaff1c81c6a4b89..bdda213dfd0f45c8d57b94bd830f966beb1c0050 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -3067,21 +3067,29 @@ fn handle_tooltip_mouse_move( } Action::ScheduleShow => { let delayed_show_task = window.spawn(cx, { - let active_tooltip = active_tooltip.clone(); + let weak_active_tooltip = Rc::downgrade(active_tooltip); let build_tooltip = build_tooltip.clone(); let check_is_hovered_during_prepaint = check_is_hovered_during_prepaint.clone(); async move |cx| { cx.background_executor().timer(TOOLTIP_SHOW_DELAY).await; + let Some(active_tooltip) = weak_active_tooltip.upgrade() else { + return; + }; cx.update(|window, cx| { let new_tooltip = build_tooltip(window, cx).map(|(view, tooltip_is_hoverable)| { - let active_tooltip = active_tooltip.clone(); + let weak_active_tooltip = Rc::downgrade(&active_tooltip); ActiveTooltip::Visible { tooltip: AnyTooltip { view, mouse_position: window.mouse_position(), check_visible_and_update: Rc::new( move |tooltip_bounds, window, cx| { + let Some(active_tooltip) = + weak_active_tooltip.upgrade() + else { + return false; + }; handle_tooltip_check_visible_and_update( &active_tooltip, tooltip_is_hoverable, @@ -3160,11 +3168,14 @@ 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 active_tooltip = active_tooltip.clone(); + let weak_active_tooltip = Rc::downgrade(active_tooltip); async move |cx| { cx.background_executor() .timer(HOVERABLE_TOOLTIP_HIDE_DELAY) .await; + let Some(active_tooltip) = weak_active_tooltip.upgrade() else { + return; + }; if active_tooltip.borrow_mut().take().is_some() { cx.update(|window, _cx| window.refresh()).ok(); } @@ -3577,6 +3588,112 @@ impl ScrollHandle { #[cfg(test)] mod tests { use super::*; + use crate::{AppContext as _, Context, InputEvent, MouseMoveEvent, TestAppContext}; + use std::rc::Weak; + + struct TestTooltipView; + + impl Render for TestTooltipView { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().w(px(20.)).h(px(20.)).child("tooltip") + } + } + + type CapturedActiveTooltip = Rc>>>>>; + + struct TooltipCaptureElement { + child: AnyElement, + captured_active_tooltip: CapturedActiveTooltip, + } + + impl IntoElement for TooltipCaptureElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl Element for TooltipCaptureElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (self.child.request_layout(window, cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + window: &mut Window, + cx: &mut App, + ) -> Self::PrepaintState { + self.child.prepaint(window, cx); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + self.child.paint(window, cx); + window.with_global_id("target".into(), |global_id, window| { + window.with_element_state::( + global_id, + |state, _window| { + let state = state.unwrap(); + *self.captured_active_tooltip.borrow_mut() = + state.active_tooltip.as_ref().map(Rc::downgrade); + ((), state) + }, + ) + }); + } + } + + struct TooltipOwner { + captured_active_tooltip: CapturedActiveTooltip, + } + + impl Render for TooltipOwner { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + TooltipCaptureElement { + child: div() + .size_full() + .child( + div() + .id("target") + .w(px(50.)) + .h(px(50.)) + .tooltip(|_, cx| cx.new(|_| TestTooltipView).into()), + ) + .into_any_element(), + captured_active_tooltip: self.captured_active_tooltip.clone(), + } + } + } #[test] fn scroll_handle_aligns_wide_children_to_left_edge() { @@ -3615,4 +3732,96 @@ mod tests { assert_eq!(handle.offset().y, px(-25.)); } + + fn setup_tooltip_owner_test() -> ( + TestAppContext, + crate::AnyWindowHandle, + CapturedActiveTooltip, + ) { + let mut test_app = TestAppContext::single(); + let captured_active_tooltip: CapturedActiveTooltip = Rc::new(RefCell::new(None)); + let window = test_app.add_window({ + let captured_active_tooltip = captured_active_tooltip.clone(); + move |_, _| TooltipOwner { + captured_active_tooltip, + } + }); + let any_window = window.into(); + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(); + }) + .unwrap(); + + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseMoveEvent { + position: point(px(10.), px(10.)), + modifiers: Default::default(), + pressed_button: None, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(); + }) + .unwrap(); + + (test_app, any_window, captured_active_tooltip) + } + + #[test] + fn tooltip_waiting_for_show_is_released_when_its_owner_disappears() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::WaitingForShow { .. }) + )); + + test_app + .update_window(any_window, |_, window, _| { + window.remove_window(); + }) + .unwrap(); + test_app.run_until_parked(); + drop(active_tooltip); + + assert!(weak_active_tooltip.upgrade().is_none()); + } + + #[test] + fn tooltip_is_released_when_its_owner_disappears() { + let (mut test_app, any_window, captured_active_tooltip) = setup_tooltip_owner_test(); + + let weak_active_tooltip = captured_active_tooltip.borrow().clone().unwrap(); + let active_tooltip = weak_active_tooltip.upgrade().unwrap(); + + test_app.dispatcher.advance_clock(TOOLTIP_SHOW_DELAY); + test_app.run_until_parked(); + + assert!(matches!( + active_tooltip.borrow().as_ref(), + Some(ActiveTooltip::Visible { .. }) + )); + + test_app + .update_window(any_window, |_, window, _| { + window.remove_window(); + }) + .unwrap(); + test_app.run_until_parked(); + drop(active_tooltip); + + assert!(weak_active_tooltip.upgrade().is_none()); + } }