Preserve tooltips requested by cached views

Antonio Scandurra , Nathan , and Max created

Co-Authored-By: Nathan <nathan@zed.dev>
Co-Authored-By: Max <max@zed.dev>

Change summary

crates/gpui/src/app.rs          |  6 +-
crates/gpui/src/elements/div.rs |  4 +-
crates/gpui/src/key_dispatch.rs |  2 
crates/gpui/src/window.rs       | 56 ++++++++++++++++++++++++++--------
4 files changed, 49 insertions(+), 19 deletions(-)

Detailed changes

crates/gpui/src/app.rs 🔗

@@ -196,7 +196,6 @@ pub struct AppContext {
     pending_updates: usize,
     pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
-    pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: FxHashMap<DisplayId, Vec<FrameCallback>>,
     pub(crate) frame_consumers: FxHashMap<DisplayId, Task<()>>,
     pub(crate) background_executor: BackgroundExecutor,
@@ -258,7 +257,6 @@ impl AppContext {
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
-                active_tooltip: None,
                 next_frame_callbacks: FxHashMap::default(),
                 frame_consumers: FxHashMap::default(),
                 background_executor: executor,
@@ -1268,8 +1266,10 @@ pub struct AnyDrag {
     pub cursor_offset: Point<Pixels>,
 }
 
+/// Contains state associated with a tooltip. You'll only need this struct if you're implementing
+/// tooltip behavior on a custom element. Otherwise, use [Div::tooltip].
 #[derive(Clone)]
-pub(crate) struct AnyTooltip {
+pub struct AnyTooltip {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }

crates/gpui/src/elements/div.rs 🔗

@@ -1433,8 +1433,8 @@ impl Interactivity {
                                 .borrow()
                                 .as_ref()
                             {
-                                if active_tooltip.tooltip.is_some() {
-                                    cx.active_tooltip = active_tooltip.tooltip.clone()
+                                if let Some(tooltip) = active_tooltip.tooltip.clone() {
+                                    cx.set_tooltip(tooltip);
                                 }
                             }
                         }

crates/gpui/src/key_dispatch.rs 🔗

@@ -112,7 +112,7 @@ impl DispatchTree {
         target.action_listeners = mem::take(&mut source.action_listeners);
     }
 
-    pub fn graft(&mut self, view_id: EntityId, source: &mut Self) -> SmallVec<[EntityId; 8]> {
+    pub fn reuse_view(&mut self, view_id: EntityId, source: &mut Self) -> SmallVec<[EntityId; 8]> {
         let view_source_node_id = source
             .view_node_ids
             .get(&view_id)

crates/gpui/src/window.rs 🔗

@@ -1,10 +1,10 @@
 #![deny(missing_docs)]
 
 use crate::{
-    px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
-    AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels,
-    DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity,
-    EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
+    px, size, transparent_black, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Arena,
+    AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
+    DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect,
+    Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla,
     ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId,
     Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent,
     Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
@@ -300,6 +300,11 @@ struct RequestedInputHandler {
     handler: Option<Box<dyn PlatformInputHandler>>,
 }
 
+struct TooltipRequest {
+    view_id: EntityId,
+    tooltip: AnyTooltip,
+}
+
 pub(crate) struct Frame {
     focus: Option<FocusId>,
     window_active: bool,
@@ -313,6 +318,7 @@ pub(crate) struct Frame {
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
     requested_input_handler: Option<RequestedInputHandler>,
+    tooltip_request: Option<TooltipRequest>,
     cursor_styles: FxHashMap<EntityId, CursorStyle>,
     requested_cursor_style: Option<CursorStyle>,
     pub(crate) view_stack: Vec<EntityId>,
@@ -328,12 +334,13 @@ impl Frame {
             mouse_listeners: FxHashMap::default(),
             dispatch_tree,
             scene: Scene::default(),
+            depth_map: Vec::new(),
             z_index_stack: StackingOrder::default(),
             next_stacking_order_id: 0,
-            depth_map: Vec::new(),
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
             requested_input_handler: None,
+            tooltip_request: None,
             cursor_styles: FxHashMap::default(),
             requested_cursor_style: None,
             view_stack: Vec::new(),
@@ -350,6 +357,7 @@ impl Frame {
         self.reused_views.clear();
         self.scene.clear();
         self.requested_input_handler.take();
+        self.tooltip_request.take();
         self.cursor_styles.clear();
         self.requested_cursor_style.take();
         debug_assert_eq!(self.view_stack.len(), 0);
@@ -1052,6 +1060,12 @@ impl<'a> WindowContext<'a> {
         self.window.next_frame.requested_cursor_style = Some(style);
     }
 
+    /// Set a tooltip to be rendered for the upcoming frame
+    pub fn set_tooltip(&mut self, tooltip: AnyTooltip) {
+        let view_id = self.parent_view_id();
+        self.window.next_frame.tooltip_request = Some(TooltipRequest { view_id, tooltip });
+    }
+
     /// Called during painting to track which z-index is on top at each pixel position
     pub fn add_opaque_layer(&mut self, bounds: Bounds<Pixels>) {
         let stacking_order = self.window.next_frame.z_index_stack.clone();
@@ -1432,12 +1446,11 @@ impl<'a> WindowContext<'a> {
             .window
             .next_frame
             .dispatch_tree
-            .graft(view_id, &mut self.window.rendered_frame.dispatch_tree);
+            .reuse_view(view_id, &mut self.window.rendered_frame.dispatch_tree);
         for view_id in grafted_view_ids {
             assert!(self.window.next_frame.reused_views.insert(view_id));
 
-            // Reuse the previous input handler if it was associated with one of
-            // the views grafted from the tree in the previous frame.
+            // Reuse the previous input handler requested during painting of the reused view.
             if self
                 .window
                 .rendered_frame
@@ -1449,6 +1462,19 @@ impl<'a> WindowContext<'a> {
                     self.window.rendered_frame.requested_input_handler.take();
             }
 
+            // Reuse the tooltip previously requested during painting of the reused view.
+            if self
+                .window
+                .rendered_frame
+                .tooltip_request
+                .as_ref()
+                .map_or(false, |requested| requested.view_id == view_id)
+            {
+                self.window.next_frame.tooltip_request =
+                    self.window.rendered_frame.tooltip_request.take();
+            }
+
+            // Reuse the cursor styles previously requested during painting of the reused view.
             if let Some(style) = self.window.rendered_frame.cursor_styles.remove(&view_id) {
                 self.window.next_frame.cursor_styles.insert(view_id, style);
                 self.window.next_frame.requested_cursor_style = Some(style);
@@ -1498,13 +1524,16 @@ impl<'a> WindowContext<'a> {
                 active_drag.view.draw(offset, available_space, cx);
             });
             self.active_drag = Some(active_drag);
-        } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
+        } else if let Some(tooltip_request) = self.window.next_frame.tooltip_request.take() {
             self.with_z_index(1, |cx| {
                 let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent);
-                active_tooltip
-                    .view
-                    .draw(active_tooltip.cursor_offset, available_space, cx);
+                tooltip_request.tooltip.view.draw(
+                    tooltip_request.tooltip.cursor_offset,
+                    available_space,
+                    cx,
+                );
             });
+            self.window.next_frame.tooltip_request = Some(tooltip_request);
         }
         self.window.dirty_views.clear();
 
@@ -2145,7 +2174,8 @@ impl<'a> WindowContext<'a> {
 
     /// Set an input handler, such as [`ElementInputHandler`][element_input_handler], which interfaces with the
     /// platform to receive textual input with proper integration with concerns such
-    /// as IME interactions.
+    /// as IME interactions. This handler will be active for the upcoming frame until the following frame is
+    /// rendered.
     ///
     /// [element_input_handler]: crate::ElementInputHandler
     pub fn handle_input(