Move more tooltip logic into gpui2 & fix tooltip moving on paint

Julia and Conrad Irwin created

Co-Authored-By: Conrad Irwin <conrad@zed.dev>

Change summary

crates/gpui2/src/app.rs              |  7 +++
crates/gpui2/src/geometry.rs         |  2 
crates/gpui2/src/interactive.rs      | 58 +++++++++++++++++++++++------
crates/gpui2/src/window.rs           | 13 ++----
crates/ui2/src/components/tooltip.rs | 33 +----------------
crates/workspace2/src/pane.rs        |  2 
6 files changed, 61 insertions(+), 54 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -157,7 +157,7 @@ pub struct AppContext {
     flushing_effects: bool,
     pending_updates: usize,
     pub(crate) active_drag: Option<AnyDrag>,
-    pub(crate) active_tooltip: Option<AnyView>,
+    pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>,
     pub(crate) background_executor: BackgroundExecutor,
@@ -898,3 +898,8 @@ pub(crate) struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }
+
+pub(crate) struct AnyTooltip {
+    pub view: AnyView,
+    pub cursor_offset: Point<Pixels>,
+}

crates/gpui2/src/geometry.rs 🔗

@@ -21,7 +21,7 @@ pub fn point<T: Clone + Debug + Default>(x: T, y: T) -> Point<T> {
 }
 
 impl<T: Clone + Debug + Default> Point<T> {
-    pub fn new(x: T, y: T) -> Self {
+    pub const fn new(x: T, y: T) -> Self {
         Self { x, y }
     }
 

crates/gpui2/src/interactive.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
-    div, point, px, Action, AnyDrag, AnyView, AppContext, BorrowWindow, Bounds, Component,
-    DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, Keystroke,
-    Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, View,
-    ViewContext,
+    div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds,
+    Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch,
+    Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style,
+    StyleRefinement, View, ViewContext,
 };
 use collections::HashMap;
 use derive_more::{Deref, DerefMut};
@@ -17,9 +17,12 @@ use std::{
     ops::Deref,
     path::PathBuf,
     sync::Arc,
+    time::Duration,
 };
 
 const DRAG_THRESHOLD: f64 = 2.;
+const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
+const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
 
 pub trait StatelessInteractive<V: 'static>: Element<V> {
     fn stateless_interaction(&mut self) -> &mut StatelessInteraction<V>;
@@ -621,7 +624,7 @@ pub trait ElementInteraction<V: 'static>: 'static {
             }
 
             if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
-                let tooltip_view = element_state.tooltip_view.clone();
+                let active_tooltip = element_state.active_tooltip.clone();
                 let pending_mouse_down = element_state.pending_mouse_down.clone();
 
                 cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
@@ -631,19 +634,44 @@ pub trait ElementInteraction<V: 'static>: 'static {
 
                     let is_hovered = bounds.contains_point(&event.position)
                         && pending_mouse_down.lock().is_none();
-                    let mut tooltip_view = tooltip_view.lock();
+                    let mut tooltip_lock = active_tooltip.lock();
 
                     if is_hovered {
-                        if tooltip_view.is_none() {
-                            *tooltip_view = Some(tooltip_builder(view_state, cx));
+                        if tooltip_lock.is_none() {
+                            *tooltip_lock = Some(ActiveTooltip {
+                                view: tooltip_builder(view_state, cx),
+                                visible: false,
+                                coordinates: event.position,
+                            });
+
+                            let active_tooltip = active_tooltip.clone();
+                            cx.spawn(move |view, mut cx| async move {
+                                cx.background_executor().timer(TOOLTIP_DELAY).await;
+
+                                view.update(&mut cx, |_, cx| {
+                                    if let Some(active_tooltip) = active_tooltip.lock().as_mut() {
+                                        active_tooltip.visible = true;
+                                        active_tooltip.coordinates =
+                                            cx.mouse_position() + TOOLTIP_OFFSET;
+                                    }
+                                    cx.notify();
+                                })
+                                .ok()
+                            })
+                            .detach();
                         }
                     } else {
-                        tooltip_view.take();
+                        tooltip_lock.take();
                     }
                 });
 
-                if let Some(active_tooltip) = element_state.tooltip_view.lock().as_ref() {
-                    cx.active_tooltip = Some(active_tooltip.clone());
+                if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+                    if active_tooltip.visible {
+                        cx.active_tooltip = Some(AnyTooltip {
+                            view: active_tooltip.view.clone(),
+                            cursor_offset: active_tooltip.coordinates,
+                        });
+                    }
                 }
             }
 
@@ -834,7 +862,13 @@ pub struct InteractiveElementState {
     hover_state: Arc<Mutex<bool>>,
     pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
     scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
-    tooltip_view: Arc<Mutex<Option<AnyView>>>,
+    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+struct ActiveTooltip {
+    view: AnyView,
+    visible: bool,
+    coordinates: Point<Pixels>,
 }
 
 impl InteractiveElementState {

crates/gpui2/src/window.rs 🔗

@@ -989,14 +989,11 @@ impl<'a> WindowContext<'a> {
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
             self.stack(1, |cx| {
-                cx.with_element_offset(
-                    Some(cx.mouse_position() + Point::new(px(8.0), px(8.0))),
-                    |cx| {
-                        let available_space =
-                            size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                        active_tooltip.draw(available_space, cx);
-                    },
-                );
+                cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
+                    let available_space =
+                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+                    active_tooltip.view.draw(available_space, cx);
+                });
             });
         }
 

crates/ui2/src/components/tooltip.rs 🔗

@@ -1,44 +1,16 @@
 use std::time::Duration;
 
-use gpui2::{
-    div, px, Component, Div, ParentElement, Render, SharedString, Styled, View, ViewContext,
-    VisualContext, WindowContext,
-};
+use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext};
 use theme2::ActiveTheme;
 
-const DELAY: Duration = Duration::from_millis(500);
-
 #[derive(Clone, Debug)]
 pub struct TextTooltip {
     title: SharedString,
-    visible: bool,
 }
 
 impl TextTooltip {
     pub fn new(str: SharedString) -> Self {
-        Self {
-            title: str,
-            visible: false,
-        }
-    }
-
-    pub fn build_view(str: SharedString, cx: &mut WindowContext) -> View<Self> {
-        let view = cx.build_view(|cx| TextTooltip::new(str));
-
-        let handle = view.downgrade();
-        cx.spawn(|mut cx| async move {
-            cx.background_executor().timer(DELAY).await;
-
-            handle
-                .update(&mut cx, |this, cx| {
-                    this.visible = true;
-                    cx.notify();
-                })
-                .ok();
-        })
-        .detach();
-
-        view
+        Self { title: str }
     }
 }
 
@@ -48,7 +20,6 @@ impl Render for TextTooltip {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();
         div()
-            .when(!self.visible, |this| this.invisible())
             .bg(theme.colors().background)
             .rounded(px(8.))
             .border()

crates/workspace2/src/pane.rs 🔗

@@ -1399,7 +1399,7 @@ impl Pane {
             .id(item.id())
             .cursor_pointer()
             .when_some(item.tab_tooltip_text(cx), |div, text| {
-                div.tooltip(move |_, cx| TextTooltip::build_view(text.clone(), cx))
+                div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
             })
             // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
             // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))