ui: Improve submenu rendering when there's not enough space (#54773)

This PR makes the submenu render on the left of the trigger if there's
not enough space (200px of available width) on the right. This improves
the rendering overall, I believe, as it's better than the submenu
rendering _on top_ of the parent menu.

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2026-04-24 09:47:36 -03:00 committed by GitHub
parent 442790c9bb
commit 77805330c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,6 +26,7 @@ struct OpenSubmenu {
entity: Entity<ContextMenu>,
trigger_bounds: Option<Bounds<Pixels>>,
offset: Option<Pixels>,
flip_left: bool,
_dismiss_subscription: Subscription,
}
@ -1301,6 +1302,11 @@ impl ContextMenu {
let (submenu, dismiss_subscription) =
Self::create_submenu(builder, cx.entity(), window, cx);
let flip_left = self
.main_menu_observed_bounds
.get()
.is_some_and(|bounds| bounds.right() + px(200.0) > window.viewport_size().width);
// If we're switching from one submenu item to another, throw away any previously-captured
// offset so we don't reuse a stale position.
self.main_menu_observed_bounds.set(None);
@ -1322,6 +1328,7 @@ impl ContextMenu {
entity: submenu,
trigger_bounds,
offset: None,
flip_left,
_dismiss_subscription: dismiss_subscription,
});
@ -1665,6 +1672,7 @@ impl ContextMenu {
ix: usize,
submenu: Entity<ContextMenu>,
offset: Pixels,
flip_left: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
let bounds_cell = self.main_menu_observed_bounds.clone();
@ -1684,9 +1692,9 @@ impl ContextMenu {
div()
.id(("submenu-container", ix))
.absolute()
.left_full()
.ml_neg_0p5()
.top(offset)
.when(flip_left, |this| this.right_full().mr_neg_0p5())
.when(!flip_left, |this| this.left_full().ml_neg_0p5())
.on_hover(cx.listener(|this, hovered, _, _| {
if *hovered {
this.hover_target = HoverTarget::Submenu;
@ -1694,7 +1702,11 @@ impl ContextMenu {
}))
.child(
anchored()
.anchor(Anchor::TopLeft)
.anchor(if flip_left {
Anchor::TopRight
} else {
Anchor::TopLeft
})
.snap_to_window_with_margin(px(8.0))
.child(
div()
@ -2093,7 +2105,12 @@ impl Render for ContextMenu {
}
focus_submenu = Some(open_submenu.entity.read(cx).focus_handle.clone());
Some((open_submenu.item_index, open_submenu.entity.clone(), offset))
Some((
open_submenu.item_index,
open_submenu.entity.clone(),
offset,
open_submenu.flip_left,
))
} else {
None
}
@ -2262,9 +2279,14 @@ impl Render for ContextMenu {
.child(render_aside(aside, cx))
}))
})
.when_some(submenu_container, |this, (ix, submenu, offset)| {
this.child(self.render_submenu_container(ix, submenu, offset, cx))
})
.when_some(
submenu_container,
|this, (ix, submenu, offset, flip_left)| {
this.child(
self.render_submenu_container(ix, submenu, offset, flip_left, cx),
)
},
)
} else {
v_flex()
.w_full()
@ -2273,9 +2295,14 @@ impl Render for ContextMenu {
.justify_end()
.children(aside.map(|(_, aside)| render_aside(aside, cx)))
.child(render_menu(cx, window))
.when_some(submenu_container, |this, (ix, submenu, offset)| {
this.child(self.render_submenu_container(ix, submenu, offset, cx))
})
.when_some(
submenu_container,
|this, (ix, submenu, offset, flip_left)| {
this.child(
self.render_submenu_container(ix, submenu, offset, flip_left, cx),
)
},
)
}
}
}