Tooltips in mouse event handler & fix executor timer

Julia and Conrad Irwin created

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

Change summary

crates/gpui2/build.rs                       |  1 
crates/gpui2/src/interactive.rs             | 56 +++++++++-------------
crates/gpui2/src/platform/mac/dispatcher.rs | 14 ----
crates/gpui2/src/window.rs                  | 13 +++--
crates/ui2/src/components/tooltip.rs        | 34 ++++++++++++-
crates/workspace2/src/pane.rs               |  8 --
6 files changed, 67 insertions(+), 59 deletions(-)

Detailed changes

crates/gpui2/build.rs 🔗

@@ -20,6 +20,7 @@ fn generate_dispatch_bindings() {
         .header("src/platform/mac/dispatch.h")
         .allowlist_var("_dispatch_main_q")
         .allowlist_var("DISPATCH_QUEUE_PRIORITY_DEFAULT")
+        .allowlist_var("DISPATCH_TIME_NOW")
         .allowlist_function("dispatch_get_global_queue")
         .allowlist_function("dispatch_async_f")
         .allowlist_function("dispatch_after_f")

crates/gpui2/src/interactive.rs 🔗

@@ -17,7 +17,6 @@ use std::{
     ops::Deref,
     path::PathBuf,
     sync::Arc,
-    time::{Duration, Instant},
 };
 
 const DRAG_THRESHOLD: f64 = 2.;
@@ -602,13 +601,14 @@ pub trait ElementInteraction<V: 'static>: 'static {
 
             if let Some(hover_listener) = stateful.hover_listener.take() {
                 let was_hovered = element_state.hover_state.clone();
-                let has_mouse_down = element_state.pending_mouse_down.lock().is_some();
+                let has_mouse_down = element_state.pending_mouse_down.clone();
 
                 cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
                     if phase != DispatchPhase::Bubble {
                         return;
                     }
-                    let is_hovered = bounds.contains_point(&event.position) && !has_mouse_down;
+                    let is_hovered =
+                        bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
                     let mut was_hovered = was_hovered.lock();
 
                     if is_hovered != was_hovered.clone() {
@@ -620,33 +620,30 @@ pub trait ElementInteraction<V: 'static>: 'static {
                 });
             }
 
-            // if we're hovered:
-            //   if no timer, start timer
-            //   if timer hits 1s, call tooltip_builder()
-            //
+            if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
+                let tooltip_view = element_state.tooltip_view.clone();
+                let pending_mouse_down = element_state.pending_mouse_down.clone();
 
-            if let Some(tooltip_builder) = &stateful.tooltip_builder {
-                let mut active_tooltip = element_state.active_tooltip.lock();
-                let is_hovered = bounds.contains_point(&cx.mouse_position())
-                    && !element_state.pending_mouse_down.lock().is_some();
+                cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                    if phase != DispatchPhase::Bubble {
+                        return;
+                    }
+
+                    let is_hovered = bounds.contains_point(&event.position)
+                        && pending_mouse_down.lock().is_none();
+                    let mut tooltip_view = tooltip_view.lock();
 
-                if is_hovered {
-                    if let Some(active_tooltip) = active_tooltip {
-                        active_tooltip.view = Some(tooltip_builder(cx))
+                    if is_hovered {
+                        if tooltip_view.is_none() {
+                            *tooltip_view = Some(tooltip_builder(view_state, cx));
+                        }
                     } else {
-                        *active_tooltip = Some(ActiveTooltip {
-                            hover_start: Instant::now(),
-                            view: None,
-                        });
+                        tooltip_view.take();
                     }
-                } else {
-                    active_tooltip.take();
-                }
+                });
 
-                if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
-                    if *element_state.hover_state.lock() {
-                        cx.active_tooltip = Some(active_tooltip.clone());
-                    }
+                if let Some(active_tooltip) = element_state.tooltip_view.lock().as_ref() {
+                    cx.active_tooltip = Some(active_tooltip.clone());
                 }
             }
 
@@ -837,12 +834,7 @@ pub struct InteractiveElementState {
     hover_state: Arc<Mutex<bool>>,
     pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
     scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
-    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
-}
-
-pub struct ActiveTooltip {
-    hover_start: Instant,
-    view: Option<AnyView>,
+    tooltip_view: Arc<Mutex<Option<AnyView>>>,
 }
 
 impl InteractiveElementState {
@@ -1194,7 +1186,7 @@ pub(crate) type DragListener<V> =
 
 pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
 
-pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut ViewContext<V>) -> AnyView + 'static>;
+pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
 
 pub type KeyListener<V> = Box<
     dyn Fn(

crates/gpui2/src/platform/mac/dispatcher.rs 🔗

@@ -11,11 +11,7 @@ use objc::{
 };
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
-use std::{
-    ffi::c_void,
-    sync::Arc,
-    time::{Duration, SystemTime},
-};
+use std::{ffi::c_void, sync::Arc, time::Duration};
 
 include!(concat!(env!("OUT_DIR"), "/dispatch_sys.rs"));
 
@@ -62,16 +58,10 @@ impl PlatformDispatcher for MacDispatcher {
     }
 
     fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
-        let now = SystemTime::now();
-        let after_duration = now
-            .duration_since(SystemTime::UNIX_EPOCH)
-            .unwrap()
-            .as_nanos() as u64
-            + duration.as_nanos() as u64;
         unsafe {
             let queue =
                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0);
-            let when = dispatch_time(0, after_duration as i64);
+            let when = dispatch_time(DISPATCH_TIME_NOW as u64, duration.as_nanos() as i64);
             dispatch_after_f(
                 when,
                 queue,

crates/gpui2/src/window.rs 🔗

@@ -989,11 +989,14 @@ 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()), |cx| {
-                    let available_space =
-                        size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                    active_tooltip.draw(available_space, 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);
+                    },
+                );
             });
         }
 

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

@@ -1,20 +1,44 @@
+use std::time::Duration;
+
 use gpui2::{
-    div, px, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext,
+    div, px, Component, Div, ParentElement, Render, SharedString, Styled, View, ViewContext,
+    VisualContext, WindowContext,
 };
 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 }
+        Self {
+            title: str,
+            visible: false,
+        }
     }
 
-    pub fn build_view<C: VisualContext>(str: SharedString, cx: &mut C) -> C::Result<View<Self>> {
-        cx.build_view(|cx| TextTooltip::new(str))
+    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
     }
 }
 
@@ -24,9 +48,11 @@ 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()
+            .font("Zed Sans")
             .border_color(theme.colors().border)
             .text_color(theme.colors().text)
             .pl_2()

crates/workspace2/src/pane.rs 🔗

@@ -9,8 +9,8 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    AnyView, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle,
-    Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+    AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model,
+    PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project2::{Project, ProjectEntryId, ProjectPath};
@@ -1398,13 +1398,9 @@ impl Pane {
             .group("")
             .id(item.id())
             .cursor_pointer()
-            .on_hover(|_, hovered, _| {
-                dbg!(hovered);
-            })
             .when_some(item.tab_tooltip_text(cx), |div, text| {
                 div.tooltip(move |_, cx| TextTooltip::build_view(text.clone(), cx))
             })
-            // .tooltip(|pane, cx| cx.build_view(|cx| div().child(title)))
             // .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))
             // .on_drop(|_view, state: View<DraggedTab>, cx| {