Fix hover state when element is occluded

Conrad Irwin created

Change summary

crates/gpui2/src/elements/div.rs | 29 +++++++++++++++++++-------
crates/gpui2/src/window.rs       | 37 ++++++++++++++++++++++++++++++---
2 files changed, 54 insertions(+), 12 deletions(-)

Detailed changes

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

@@ -755,6 +755,14 @@ impl Interactivity {
     ) {
         let style = self.compute_style(Some(bounds), element_state, cx);
 
+        if style
+            .background
+            .as_ref()
+            .is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
+        {
+            cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
+        }
+
         if let Some(mouse_cursor) = style.mouse_cursor {
             let hovered = bounds.contains_point(&cx.mouse_position());
             if hovered {
@@ -1098,19 +1106,21 @@ impl Interactivity {
                     }
                 }
             }
-            // if self.hover_style.is_some() {
-            if bounds.contains_point(&mouse_position) {
-                // eprintln!("div hovered {bounds:?} {mouse_position:?}");
-                style.refine(&self.hover_style);
-            } else {
-                // eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
+            if self.hover_style.is_some() {
+                if bounds
+                    .intersect(&cx.content_mask().bounds)
+                    .contains_point(&mouse_position)
+                    && cx.was_top_layer(&mouse_position, cx.stacking_order())
+                {
+                    style.refine(&self.hover_style);
+                }
             }
-            // }
 
             if let Some(drag) = cx.active_drag.take() {
                 for (state_type, group_drag_style) in &self.group_drag_over_styles {
                     if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
                         if *state_type == drag.view.entity_type()
+                        // todo!() needs to handle cx.content_mask() and cx.is_top()
                             && group_bounds.contains_point(&mouse_position)
                         {
                             style.refine(&group_drag_style.style);
@@ -1120,7 +1130,10 @@ impl Interactivity {
 
                 for (state_type, drag_over_style) in &self.drag_over_styles {
                     if *state_type == drag.view.entity_type()
-                        && bounds.contains_point(&mouse_position)
+                        && bounds
+                            .intersect(&cx.content_mask().bounds)
+                            .contains_point(&mouse_position)
+                        && cx.was_top_layer(&mouse_position, cx.stacking_order())
                     {
                         style.refine(drag_over_style);
                     }

crates/gpui2/src/window.rs 🔗

@@ -12,7 +12,7 @@ use crate::{
     VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
 };
 use anyhow::{anyhow, Context as _, Result};
-use collections::HashMap;
+use collections::{BTreeMap, HashMap};
 use derive_more::{Deref, DerefMut};
 use futures::{
     channel::{mpsc, oneshot},
@@ -39,8 +39,8 @@ use util::ResultExt;
 
 /// A global stacking order, which is created by stacking successive z-index values.
 /// Each z-index will always be interpreted in the context of its parent z-index.
-#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default)]
-pub(crate) struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
+#[derive(Deref, DerefMut, Ord, PartialOrd, Eq, PartialEq, Clone, Default, Debug)]
+pub struct StackingOrder(pub(crate) SmallVec<[u32; 16]>);
 
 /// Represents the two different phases when dispatching events.
 #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)]
@@ -243,7 +243,8 @@ pub(crate) struct Frame {
     pub(crate) dispatch_tree: DispatchTree,
     pub(crate) focus_listeners: Vec<AnyFocusListener>,
     pub(crate) scene_builder: SceneBuilder,
-    z_index_stack: StackingOrder,
+    pub(crate) depth_map: BTreeMap<StackingOrder, Bounds<Pixels>>,
+    pub(crate) z_index_stack: StackingOrder,
     content_mask_stack: Vec<ContentMask<Pixels>>,
     element_offset_stack: Vec<Point<Pixels>>,
 }
@@ -257,6 +258,7 @@ impl Frame {
             focus_listeners: Vec::new(),
             scene_builder: SceneBuilder::default(),
             z_index_stack: StackingOrder::default(),
+            depth_map: Default::default(),
             content_mask_stack: Vec::new(),
             element_offset_stack: Vec::new(),
         }
@@ -806,6 +808,32 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    /// 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.current_frame.z_index_stack.clone();
+        self.window
+            .current_frame
+            .depth_map
+            .insert(stacking_order, bounds);
+    }
+
+    /// Returns true if the top-most opaque layer painted over this point was part of the
+    /// same layer as the given stacking order.
+    pub fn was_top_layer(&self, point: &Point<Pixels>, level: &StackingOrder) -> bool {
+        for (stack, bounds) in self.window.previous_frame.depth_map.iter() {
+            if bounds.contains_point(point) {
+                return level.starts_with(stack) || stack.starts_with(level);
+            }
+        }
+
+        false
+    }
+
+    /// Called during painting to get the current stacking order.
+    pub fn stacking_order(&self) -> &StackingOrder {
+        &self.window.current_frame.z_index_stack
+    }
+
     /// Paint one or more drop shadows into the scene for the current frame at the current z-index.
     pub fn paint_shadows(
         &mut self,
@@ -1153,6 +1181,7 @@ impl<'a> WindowContext<'a> {
         frame.mouse_listeners.values_mut().for_each(Vec::clear);
         frame.focus_listeners.clear();
         frame.dispatch_tree.clear();
+        frame.depth_map.clear();
     }
 
     /// Dispatch a mouse or keyboard event on the window.