Align context menu to fit within the window bounds

Antonio Scandurra created

Change summary

crates/context_menu/src/context_menu.rs |  2 +
crates/gpui/src/elements/overlay.rs     | 45 ++++++++++++++++++++----
crates/gpui/src/elements/tooltip.rs     | 48 ++++++++++----------------
3 files changed, 58 insertions(+), 37 deletions(-)

Detailed changes

crates/context_menu/src/context_menu.rs 🔗

@@ -92,6 +92,8 @@ impl View for ContextMenu {
             .boxed();
 
         Overlay::new(expanded_menu)
+            .hoverable(true)
+            .align_to_fit(true)
             .with_abs_position(self.position)
             .boxed()
     }

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

@@ -10,6 +10,8 @@ use crate::{
 pub struct Overlay {
     child: ElementBox,
     abs_position: Option<Vector2F>,
+    align_to_fit: bool,
+    hoverable: bool,
 }
 
 impl Overlay {
@@ -17,6 +19,8 @@ impl Overlay {
         Self {
             child,
             abs_position: None,
+            align_to_fit: false,
+            hoverable: false,
         }
     }
 
@@ -24,6 +28,16 @@ impl Overlay {
         self.abs_position = Some(position);
         self
     }
+
+    pub fn align_to_fit(mut self, align_to_fit: bool) -> Self {
+        self.align_to_fit = align_to_fit;
+        self
+    }
+
+    pub fn hoverable(mut self, hoverable: bool) -> Self {
+        self.hoverable = hoverable;
+        self
+    }
 }
 
 impl Element for Overlay {
@@ -51,15 +65,30 @@ impl Element for Overlay {
         size: &mut Self::LayoutState,
         cx: &mut PaintContext,
     ) {
-        let origin = self.abs_position.unwrap_or(bounds.origin());
-        let visible_bounds = RectF::new(origin, *size);
+        let mut bounds = RectF::new(self.abs_position.unwrap_or(bounds.origin()), *size);
         cx.scene.push_stacking_context(None);
-        cx.scene.push_mouse_region(MouseRegion {
-            view_id: cx.current_view_id(),
-            bounds: visible_bounds,
-            ..Default::default()
-        });
-        self.child.paint(origin, visible_bounds, cx);
+
+        if self.hoverable {
+            cx.scene.push_mouse_region(MouseRegion {
+                view_id: cx.current_view_id(),
+                bounds,
+                ..Default::default()
+            });
+        }
+
+        if self.align_to_fit {
+            // Align overlay to the left if its bounds overflow the window width.
+            if bounds.lower_right().x() > cx.window_size.x() {
+                bounds.set_origin_x(bounds.origin_x() - bounds.width());
+            }
+
+            // Align overlay to the top if its bounds overflow the window height.
+            if bounds.lower_right().y() > cx.window_size.y() {
+                bounds.set_origin_y(bounds.origin_y() - bounds.height());
+            }
+        }
+
+        self.child.paint(bounds.origin(), bounds, cx);
         cx.scene.pop_stacking_context();
     }
 

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

@@ -1,6 +1,6 @@
 use super::{
-    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, ParentElement,
-    Text,
+    ContainerStyle, Element, ElementBox, Flex, KeystrokeLabel, MouseEventHandler, Overlay,
+    ParentElement, Text,
 };
 use crate::{
     fonts::TextStyle,
@@ -21,7 +21,7 @@ const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(500);
 pub struct Tooltip {
     child: ElementBox,
     tooltip: Option<ElementBox>,
-    state: ElementStateHandle<Rc<TooltipState>>,
+    _state: ElementStateHandle<Rc<TooltipState>>,
 }
 
 #[derive(Default)]
@@ -68,15 +68,20 @@ impl Tooltip {
             )
             .boxed();
             Some(
-                Self::render_tooltip(text, style, action, false)
-                    .constrained()
-                    .dynamically(move |constraint, cx| {
-                        SizeConstraint::strict_along(
-                            Axis::Vertical,
-                            collapsed_tooltip.layout(constraint, cx).y(),
-                        )
-                    })
-                    .boxed(),
+                Overlay::new(
+                    Self::render_tooltip(text, style, action, false)
+                        .constrained()
+                        .dynamically(move |constraint, cx| {
+                            SizeConstraint::strict_along(
+                                Axis::Vertical,
+                                collapsed_tooltip.layout(constraint, cx).y(),
+                            )
+                        })
+                        .boxed(),
+                )
+                .align_to_fit(true)
+                .with_abs_position(state.position.get())
+                .boxed(),
             )
         } else {
             None
@@ -111,7 +116,7 @@ impl Tooltip {
         Self {
             child,
             tooltip,
-            state: state_handle,
+            _state: state_handle,
         }
     }
 
@@ -171,22 +176,7 @@ impl Element for Tooltip {
     ) {
         self.child.paint(bounds.origin(), visible_bounds, cx);
         if let Some(tooltip) = self.tooltip.as_mut() {
-            let origin = self.state.read(cx).position.get();
-            let mut bounds = RectF::new(origin, tooltip.size());
-
-            // Align tooltip to the left if its bounds overflow the window width.
-            if bounds.lower_right().x() > cx.window_size.x() {
-                bounds.set_origin_x(bounds.origin_x() - bounds.width());
-            }
-
-            // Align tooltip to the top if its bounds overflow the window height.
-            if bounds.lower_right().y() > cx.window_size.y() {
-                bounds.set_origin_y(bounds.origin_y() - bounds.height());
-            }
-
-            cx.scene.push_stacking_context(None);
-            tooltip.paint(bounds.origin(), bounds, cx);
-            cx.scene.pop_stacking_context();
+            tooltip.paint(bounds.origin(), visible_bounds, cx);
         }
     }