Account for all hover heights & prevent un-hover between popovers (#9257)

Julia and Antonio Scandurra created

Fixes https://github.com/zed-industries/zed/issues/5227
Fixes https://github.com/zed-industries/zed/issues/7304

Release Notes:

- Fixed an issue where not all editor hover popovers would be accounted
for when choosing to hover above or below the mouse cursor
([#5227](https://github.com/zed-industries/zed/issues/5227)).
- Fixed an issue where editor hover could be dismissed by moving the
mouse between hover popovers
([#7304](https://github.com/zed-industries/zed/issues/7304)).

Co-authored-by: Antonio Scandurra <antonio@zed.dev>

Change summary

crates/editor/src/element.rs | 78 +++++++++++++++++++++++++------------
1 file changed, 53 insertions(+), 25 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1650,6 +1650,12 @@ impl EditorElement {
         em_width: Pixels,
         cx: &mut ElementContext,
     ) {
+        struct MeasuredHoverPopover {
+            element: AnyElement,
+            size: Size<Pixels>,
+            horizontal_offset: Pixels,
+        }
+
         let max_size = size(
             (120. * em_width) // Default size
                 .min(hitbox.size.width / 2.) // Shrink to half of the editor width
@@ -1669,7 +1675,7 @@ impl EditorElement {
                 cx,
             )
         });
-        let Some((position, mut hover_popovers)) = hover_popovers else {
+        let Some((position, hover_popovers)) = hover_popovers else {
             return;
         };
 
@@ -1679,48 +1685,70 @@ impl EditorElement {
         let hovered_row_layout =
             &line_layouts[(position.row() - visible_display_row_range.start) as usize].line;
 
-        // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
-        // height. This is the size we will use to decide whether to render popovers above or below
-        // the hovered line.
-        let first_size = hover_popovers[0].measure(available_space, cx);
-        let height_to_reserve = first_size.height + 1.5 * MIN_POPOVER_LINE_HEIGHT * line_height;
-
         // Compute Hovered Point
         let x =
             hovered_row_layout.x_for_index(position.column() as usize) - scroll_pixel_position.x;
         let y = position.row() as f32 * line_height - scroll_pixel_position.y;
         let hovered_point = content_origin + point(x, y);
 
-        if hovered_point.y - height_to_reserve > Pixels::ZERO {
+        let mut overall_height = Pixels::ZERO;
+        let mut measured_hover_popovers = Vec::new();
+        for mut hover_popover in hover_popovers {
+            let size = hover_popover.measure(available_space, cx);
+            let horizontal_offset =
+                (text_hitbox.upper_right().x - (hovered_point.x + size.width)).min(Pixels::ZERO);
+
+            overall_height += HOVER_POPOVER_GAP + size.height;
+
+            measured_hover_popovers.push(MeasuredHoverPopover {
+                element: hover_popover,
+                size,
+                horizontal_offset,
+            });
+        }
+        overall_height += HOVER_POPOVER_GAP;
+
+        fn draw_occluder(width: Pixels, origin: gpui::Point<Pixels>, cx: &mut ElementContext) {
+            let mut occlusion = div()
+                .size_full()
+                .occlude()
+                .on_mouse_move(|_, cx| cx.stop_propagation())
+                .into_any_element();
+            occlusion.measure(size(width, HOVER_POPOVER_GAP).into(), cx);
+            cx.defer_draw(occlusion, origin, 2);
+        }
+
+        if hovered_point.y > overall_height {
             // There is enough space above. Render popovers above the hovered point
             let mut current_y = hovered_point.y;
-            for mut hover_popover in hover_popovers {
-                let size = hover_popover.measure(available_space, cx);
-                let mut popover_origin = point(hovered_point.x, current_y - size.height);
+            for (position, popover) in measured_hover_popovers.into_iter().with_position() {
+                let size = popover.size;
+                let popover_origin = point(
+                    hovered_point.x + popover.horizontal_offset,
+                    current_y - size.height,
+                );
 
-                let x_out_of_bounds = text_hitbox.upper_right().x - (popover_origin.x + size.width);
-                if x_out_of_bounds < Pixels::ZERO {
-                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+                cx.defer_draw(popover.element, popover_origin, 2);
+                if position != itertools::Position::Last {
+                    let origin = point(popover_origin.x, popover_origin.y - HOVER_POPOVER_GAP);
+                    draw_occluder(size.width, origin, cx);
                 }
 
-                cx.defer_draw(hover_popover, popover_origin, 2);
-
                 current_y = popover_origin.y - HOVER_POPOVER_GAP;
             }
         } else {
             // There is not enough space above. Render popovers below the hovered point
             let mut current_y = hovered_point.y + line_height;
-            for mut hover_popover in hover_popovers {
-                let size = hover_popover.measure(available_space, cx);
-                let mut popover_origin = point(hovered_point.x, current_y);
-
-                let x_out_of_bounds = text_hitbox.upper_right().x - (popover_origin.x + size.width);
-                if x_out_of_bounds < Pixels::ZERO {
-                    popover_origin.x = popover_origin.x + x_out_of_bounds;
+            for (position, popover) in measured_hover_popovers.into_iter().with_position() {
+                let size = popover.size;
+                let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
+
+                cx.defer_draw(popover.element, popover_origin, 2);
+                if position != itertools::Position::Last {
+                    let origin = point(popover_origin.x, popover_origin.y + size.height);
+                    draw_occluder(size.width, origin, cx);
                 }
 
-                cx.defer_draw(hover_popover, popover_origin, 2);
-
                 current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
             }
         }