Tooltip on tabs

Conrad Irwin and Julia created

Co-Authored-By: Julia <julia@zed.dev>

Change summary

crates/gpui2/src/app.rs              |  2 +
crates/gpui2/src/interactive.rs      | 24 ++++++++++++++++++-
crates/gpui2/src/window.rs           |  8 ++++++
crates/ui2/src/components.rs         |  2 +
crates/ui2/src/components/tooltip.rs | 36 ++++++++++++++++++++++++++++++
crates/workspace2/src/pane.rs        | 12 +++++----
6 files changed, 77 insertions(+), 7 deletions(-)

Detailed changes

crates/gpui2/src/app.rs 🔗

@@ -157,6 +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) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) frame_consumers: HashMap<DisplayId, Task<()>>,
     pub(crate) background_executor: BackgroundExecutor,
@@ -215,6 +216,7 @@ impl AppContext {
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
+                active_tooltip: None,
                 next_frame_callbacks: HashMap::default(),
                 frame_consumers: HashMap::default(),
                 background_executor: executor,

crates/gpui2/src/interactive.rs 🔗

@@ -358,7 +358,7 @@ pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
             self.stateful_interaction().tooltip_builder.is_none(),
             "calling tooltip more than once on the same element is not supported"
         );
-        self.stateful_interaction().tooltip_builder = Some(Box::new(move |view_state, cx| {
+        self.stateful_interaction().tooltip_builder = Some(Arc::new(move |view_state, cx| {
             build_tooltip(view_state, cx).into()
         }));
 
@@ -602,6 +602,10 @@ 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();
+
                 cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
                     if phase != DispatchPhase::Bubble {
                         return;
@@ -612,11 +616,26 @@ pub trait ElementInteraction<V: 'static>: 'static {
                     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());
+                }
+            }
+
             let active_state = element_state.active_state.clone();
             if active_state.lock().is_none() {
                 let active_group_bounds = stateful
@@ -804,6 +823,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<AnyView>>>,
 }
 
 impl InteractiveElementState {
@@ -1155,7 +1175,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> = Box<dyn Fn(&mut V, &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/window.rs 🔗

@@ -987,6 +987,14 @@ impl<'a> WindowContext<'a> {
                     cx.active_drag = Some(active_drag);
                 });
             });
+        } 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);
+                });
+            });
         }
 
         self.window.root_view = Some(root_view);

crates/ui2/src/components.rs 🔗

@@ -31,6 +31,7 @@ mod theme_selector;
 mod title_bar;
 mod toast;
 mod toolbar;
+mod tooltip;
 mod traffic_lights;
 mod workspace;
 
@@ -67,5 +68,6 @@ pub use theme_selector::*;
 pub use title_bar::*;
 pub use toast::*;
 pub use toolbar::*;
+pub use tooltip::*;
 pub use traffic_lights::*;
 pub use workspace::*;

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

@@ -0,0 +1,36 @@
+use gpui2::{
+    div, px, Div, ParentElement, Render, SharedString, Styled, View, ViewContext, VisualContext,
+};
+use theme2::ActiveTheme;
+
+#[derive(Clone, Debug)]
+pub struct TextTooltip {
+    title: SharedString,
+}
+
+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 {
+    type Element = Div<Self>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let theme = cx.theme();
+        div()
+            .bg(theme.colors().background)
+            .rounded(px(8.))
+            .border()
+            .border_color(theme.colors().border)
+            .text_color(theme.colors().text)
+            .pl_2()
+            .pr_2()
+            .child(self.title.clone())
+    }
+}

crates/workspace2/src/pane.rs 🔗

@@ -9,9 +9,8 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    AppContext, AsyncWindowContext, Component, CursorStyle, Div, EntityId, EventEmitter,
-    FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    AnyView, 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};
@@ -27,7 +26,7 @@ use std::{
     },
 };
 use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconColor, IconElement};
+use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -1402,7 +1401,10 @@ impl Pane {
             .on_hover(|_, hovered, _| {
                 dbg!(hovered);
             })
-            // .tooltip(|pane, cx| cx.create_view( tooltip.child("Hovering the tab"))
+            .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| {