Improve popup menu to leave some margin with window edges (#17159)

Jason Lee created

Release Notes:

- Improved popup menu to leave some margin with window edges.

## Updates in GPUI

- gpui: Add `snap_to_window_with_margin` method to `anchored` to support
leave margin to window edges.

## Before

<img width="609" alt="before-snap-to-window 2024-08-30 222506"
src="https://github.com/user-attachments/assets/62bb6791-7c89-4558-9484-5c7b31f5e91e">

## After

<img width="698" alt="snap-to-window1 2024-08-30 222506"
src="https://github.com/user-attachments/assets/51634e79-2a95-42fe-8362-a3c7003648eb">

<img width="622" alt="snap-to-window 2024-08-30 222506"
src="https://github.com/user-attachments/assets/43a865d6-d238-4fdc-ae9d-8160b9ba7953">

Change summary

crates/editor/src/element.rs                 |  2 
crates/gpui/src/elements/anchored.rs         | 33 +++++++++++++++------
crates/gpui/src/geometry.rs                  | 15 +++++++--
crates/ui/src/components/popover_menu.rs     |  4 ++
crates/ui/src/components/right_click_menu.rs |  4 +-
5 files changed, 40 insertions(+), 18 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -2732,7 +2732,7 @@ impl EditorElement {
                         .position(position)
                         .child(context_menu)
                         .anchor(AnchorCorner::TopLeft)
-                        .snap_to_window(),
+                        .snap_to_window_with_margin(px(8.)),
                 )
                 .with_priority(1)
                 .into_any(),

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

@@ -2,8 +2,8 @@ use smallvec::SmallVec;
 use taffy::style::{Display, Position};
 
 use crate::{
-    point, AnyElement, Bounds, Element, GlobalElementId, IntoElement, LayoutId, ParentElement,
-    Pixels, Point, Size, Style, WindowContext,
+    point, AnyElement, Bounds, Edges, Element, GlobalElementId, IntoElement, LayoutId,
+    ParentElement, Pixels, Point, Size, Style, WindowContext,
 };
 
 /// The state that the anchored element element uses to track its children.
@@ -60,6 +60,12 @@ impl Anchored {
         self.fit_mode = AnchoredFitMode::SnapToWindow;
         self
     }
+
+    /// Snap to window edge and leave some margins.
+    pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
+        self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
+        self
+    }
 }
 
 impl ParentElement for Anchored {
@@ -153,22 +159,27 @@ impl Element for Anchored {
             }
         }
 
+        let edges = match self.fit_mode {
+            AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
+            _ => Edges::default(),
+        };
+
         // Snap the horizontal edges of the anchored element to the horizontal edges of the window if
         // its horizontal bounds overflow, aligning to the left if it is wider than the limits.
         if desired.right() > limits.right() {
-            desired.origin.x -= desired.right() - limits.right();
+            desired.origin.x -= desired.right() - limits.right() + edges.right;
         }
         if desired.left() < limits.left() {
-            desired.origin.x = limits.origin.x;
+            desired.origin.x = limits.origin.x + edges.left;
         }
 
         // Snap the vertical edges of the anchored element to the vertical edges of the window if
         // its vertical bounds overflow, aligning to the top if it is taller than the limits.
         if desired.bottom() > limits.bottom() {
-            desired.origin.y -= desired.bottom() - limits.bottom();
+            desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
         }
         if desired.top() < limits.top() {
-            desired.origin.y = limits.origin.y;
+            desired.origin.y = limits.origin.y + edges.top;
         }
 
         let offset = desired.origin - bounds.origin;
@@ -211,18 +222,20 @@ enum Axis {
 /// Which algorithm to use when fitting the anchored element to be inside the window.
 #[derive(Copy, Clone, PartialEq)]
 pub enum AnchoredFitMode {
-    /// Snap the anchored element to the window edge
+    /// Snap the anchored element to the window edge.
     SnapToWindow,
-    /// Switch which corner anchor this anchored element is attached to
+    /// Snap to window edge and leave some margins.
+    SnapToWindowWithMargin(Edges<Pixels>),
+    /// Switch which corner anchor this anchored element is attached to.
     SwitchAnchor,
 }
 
 /// Which algorithm to use when positioning the anchored element.
 #[derive(Copy, Clone, PartialEq)]
 pub enum AnchoredPositionMode {
-    /// Position the anchored element relative to the window
+    /// Position the anchored element relative to the window.
     Window,
-    /// Position the anchored element relative to its parent
+    /// Position the anchored element relative to its parent.
     Local,
 }
 

crates/gpui/src/geometry.rs 🔗

@@ -1836,11 +1836,18 @@ impl Edges<Pixels> {
 
 impl From<f32> for Edges<Pixels> {
     fn from(val: f32) -> Self {
+        let val: Pixels = val.into();
+        val.into()
+    }
+}
+
+impl From<Pixels> for Edges<Pixels> {
+    fn from(val: Pixels) -> Self {
         Edges {
-            top: val.into(),
-            right: val.into(),
-            bottom: val.into(),
-            left: val.into(),
+            top: val,
+            right: val,
+            bottom: val,
+            left: val,
         }
     }
 }

crates/ui/src/components/popover_menu.rs 🔗

@@ -252,7 +252,9 @@ impl<M: ManagedView> Element for PopoverMenu<M> {
                 let mut menu_layout_id = None;
 
                 let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
-                    let mut anchored = anchored().snap_to_window().anchor(self.anchor);
+                    let mut anchored = anchored()
+                        .snap_to_window_with_margin(px(8.))
+                        .anchor(self.anchor);
                     if let Some(child_bounds) = element_state.child_bounds {
                         anchored = anchored.position(
                             self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),

crates/ui/src/components/right_click_menu.rs 🔗

@@ -1,7 +1,7 @@
 use std::{cell::RefCell, rc::Rc};
 
 use gpui::{
-    anchored, deferred, div, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
+    anchored, deferred, div, px, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
     Element, ElementId, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId,
     ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext,
     WindowContext,
@@ -118,7 +118,7 @@ impl<M: ManagedView> Element for RightClickMenu<M> {
             let mut menu_layout_id = None;
 
             let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| {
-                let mut anchored = anchored().snap_to_window();
+                let mut anchored = anchored().snap_to_window_with_margin(px(8.));
                 if let Some(anchor) = this.anchor {
                     anchored = anchored.anchor(anchor);
                 }