tooltip2 (#3237)

Conrad Irwin created

- Fix executor.timer() in gpui2
- Add support for tooltips 

[[PR Description]]

Release Notes:

- N/A

Change summary

crates/gpui2/build.rs                       |  1 
crates/gpui2/src/app.rs                     |  8 +
crates/gpui2/src/geometry.rs                |  2 
crates/gpui2/src/interactive.rs             | 85 +++++++++++++++++-----
crates/gpui2/src/platform/mac/dispatcher.rs | 14 ---
crates/gpui2/src/window.rs                  |  4 
crates/ui2/src/components/tooltip.rs        |  9 -
crates/workspace2/src/pane.rs               |  6 -
8 files changed, 80 insertions(+), 49 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/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,9 @@ pub(crate) struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }
+
+#[derive(Clone)]
+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, Task, 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>;
@@ -601,38 +604,72 @@ 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 active_tooltip = element_state.active_tooltip.clone();
-                let tooltip_builder = stateful.tooltip_builder.clone();
+                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() {
                         *was_hovered = is_hovered;
                         drop(was_hovered);
-                        if let Some(tooltip_builder) = &tooltip_builder {
-                            let mut active_tooltip = active_tooltip.lock();
-                            if is_hovered && active_tooltip.is_none() {
-                                *active_tooltip = Some(tooltip_builder(view_state, cx));
-                            } else if !is_hovered {
-                                active_tooltip.take();
-                            }
-                        }
 
                         hover_listener(view_state, is_hovered, cx);
                     }
                 });
             }
 
-            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(tooltip_builder) = stateful.tooltip_builder.take() {
+                let active_tooltip = element_state.active_tooltip.clone();
+                let pending_mouse_down = element_state.pending_mouse_down.clone();
+
+                cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                    if phase != DispatchPhase::Bubble {
+                        return;
+                    }
+
+                    let is_hovered = bounds.contains_point(&event.position)
+                        && pending_mouse_down.lock().is_none();
+                    if !is_hovered {
+                        active_tooltip.lock().take();
+                        return;
+                    }
+
+                    if active_tooltip.lock().is_none() {
+                        let task = cx.spawn({
+                            let active_tooltip = active_tooltip.clone();
+                            let tooltip_builder = tooltip_builder.clone();
+
+                            move |view, mut cx| async move {
+                                cx.background_executor().timer(TOOLTIP_DELAY).await;
+                                view.update(&mut cx, move |view_state, cx| {
+                                    active_tooltip.lock().replace(ActiveTooltip {
+                                        waiting: None,
+                                        tooltip: Some(AnyTooltip {
+                                            view: tooltip_builder(view_state, cx),
+                                            cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
+                                        }),
+                                    });
+                                    cx.notify();
+                                })
+                                .ok();
+                            }
+                        });
+                        active_tooltip.lock().replace(ActiveTooltip {
+                            waiting: Some(task),
+                            tooltip: None,
+                        });
+                    }
+                });
+
+                if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+                    if active_tooltip.tooltip.is_some() {
+                        cx.active_tooltip = active_tooltip.tooltip.clone()
+                    }
                 }
             }
 
@@ -823,7 +860,13 @@ 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<AnyView>>>,
+    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+struct ActiveTooltip {
+    #[allow(unused)] // used to drop the task
+    waiting: Option<Task<()>>,
+    tooltip: Option<AnyTooltip>,
 }
 
 impl InteractiveElementState {

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

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

@@ -1,6 +1,4 @@
-use gpui2::{
-    div, px, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext,
-};
+use gpui2::{div, px, Div, ParentElement, Render, SharedString, Styled, ViewContext};
 use theme2::ActiveTheme;
 
 #[derive(Clone, Debug)]
@@ -12,10 +10,6 @@ impl TextTooltip {
     pub fn new(str: SharedString) -> Self {
         Self { title: str }
     }
-
-    pub fn build_view<C: VisualContext>(str: SharedString, cx: &mut C) -> C::Result<View<Self>> {
-        cx.build_view(|cx| TextTooltip::new(str))
-    }
 }
 
 impl Render for TextTooltip {
@@ -27,6 +21,7 @@ impl Render for TextTooltip {
             .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 🔗

@@ -1395,13 +1395,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))
+                div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone())))
             })
-            // .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| {