menu: Improve Menu to avoid invalid or duplicate separators. (#276)

This commit is contained in:
Jason Lee 2024-09-26 21:28:42 +08:00 committed by GitHub
parent 7a8eaefd30
commit 709c706adf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 125 additions and 88 deletions

View file

@ -162,10 +162,12 @@ impl Render for PopupStory {
})) }))
.context_menu({ .context_menu({
move |this, cx| { move |this, cx| {
this.menu("Cut", Box::new(Cut)) this.separator()
.menu("Cut", Box::new(Cut))
.menu("Copy", Box::new(Copy)) .menu("Copy", Box::new(Copy))
.menu("Paste", Box::new(Paste)) .menu("Paste", Box::new(Paste))
.separator() .separator()
.separator()
.submenu("Settings", cx, move |menu, _| { .submenu("Settings", cx, move |menu, _| {
menu.menu_with_check( menu.menu_with_check(
"Toggle Window Mode", "Toggle Window Mode",
@ -179,6 +181,7 @@ impl Render for PopupStory {
}) })
.separator() .separator()
.menu("Search All", Box::new(SearchAll)) .menu("Search All", Box::new(SearchAll))
.separator()
} }
}) })
.gap_6() .gap_6()

View file

@ -59,6 +59,10 @@ impl PopupMenuItem {
!matches!(self, PopupMenuItem::Separator) !matches!(self, PopupMenuItem::Separator)
} }
fn is_separator(&self) -> bool {
matches!(self, PopupMenuItem::Separator)
}
fn has_icon(&self) -> bool { fn has_icon(&self) -> bool {
matches!(self, PopupMenuItem::Item { icon: Some(_), .. }) matches!(self, PopupMenuItem::Item { icon: Some(_), .. })
} }
@ -232,6 +236,14 @@ impl PopupMenu {
/// Add a separator Menu Item /// Add a separator Menu Item
pub fn separator(mut self) -> Self { pub fn separator(mut self) -> Self {
if self.menu_items.is_empty() {
return self;
}
if let Some(PopupMenuItem::Separator) = self.menu_items.last() {
return self;
}
self.menu_items.push(PopupMenuItem::Separator); self.menu_items.push(PopupMenuItem::Separator);
self self
} }
@ -411,6 +423,7 @@ impl Render for PopupMenu {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let view = cx.view().clone(); let view = cx.view().clone();
let has_icon = self.menu_items.iter().any(|item| item.has_icon()); let has_icon = self.menu_items.iter().any(|item| item.has_icon());
let items_count = self.menu_items.len();
let max_width = self.max_width; let max_width = self.max_width;
let bounds = self.bounds; let bounds = self.bounds;
@ -438,103 +451,124 @@ impl Render for PopupMenu {
.absolute() .absolute()
.size_full() .size_full()
}) })
.children(self.menu_items.iter_mut().enumerate().map(|(ix, item)| { .children(
let group_id = format!("item:{}", ix); self.menu_items
let this = ListItem::new(("menu-item", ix)) .iter_mut()
.group(group_id.clone()) .enumerate()
.relative() // Skip last separator
.text_sm() .filter(|(ix, item)| !(*ix == items_count - 1 && item.is_separator()))
.py_0() .map(|(ix, item)| {
.px_2() let group_id = format!("item:{}", ix);
.h(px(28.))
.rounded_md()
.items_center()
.on_mouse_enter(cx.listener(move |this, _, cx| {
this.hovered_menu_ix = Some(ix);
cx.notify();
}));
match item {
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
div()
.rounded_none()
.h(px(1.))
.mx_neg_1()
.my_0p5()
.bg(cx.theme().muted),
),
PopupMenuItem::Item {
icon,
label,
action,
..
} => {
let action = action.as_ref().map(|action| action.boxed_clone());
let key = Self::render_keybinding(action, cx);
this.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx))) let this = ListItem::new(("menu-item", ix))
.child( .group(group_id.clone())
h_flex() .relative()
.items_center() .text_sm()
.gap_x_1p5() .py_0()
.children(Self::render_icon(has_icon, icon.clone(), cx)) .px_2()
.h(px(28.))
.rounded_md()
.items_center()
.on_mouse_enter(cx.listener(move |this, _, cx| {
this.hovered_menu_ix = Some(ix);
cx.notify();
}));
match item {
PopupMenuItem::Separator => this.h_auto().p_0().disabled(true).child(
div()
.rounded_none()
.h(px(1.))
.mx_neg_1()
.my_0p5()
.bg(cx.theme().muted),
),
PopupMenuItem::Item {
icon,
label,
action,
..
} => {
let action = action.as_ref().map(|action| action.boxed_clone());
let key = Self::render_keybinding(action, cx);
this.on_click(cx.listener(move |this, _, cx| this.on_click(ix, cx)))
.child( .child(
h_flex() h_flex()
.flex_1()
.gap_2()
.items_center() .items_center()
.justify_between() .gap_x_1p5()
.child(label.clone()) .children(Self::render_icon(has_icon, icon.clone(), cx))
.children(key), .child(
), h_flex()
) .flex_1()
} .gap_2()
PopupMenuItem::Submenu { icon, label, menu } => this .items_center()
.when(self.hovered_menu_ix == Some(ix), |this| this.selected(true)) .justify_between()
.child( .child(label.clone())
h_flex() .children(key),
.items_start() ),
)
}
PopupMenuItem::Submenu { icon, label, menu } => this
.when(self.hovered_menu_ix == Some(ix), |this| this.selected(true))
.child( .child(
h_flex() h_flex()
.size_full() .items_start()
.items_center()
.gap_x_1p5()
.children(Self::render_icon(has_icon, icon.clone(), cx))
.child( .child(
h_flex() h_flex()
.flex_1() .size_full()
.gap_2()
.items_center() .items_center()
.justify_between() .gap_x_1p5()
.child(label.clone()) .children(Self::render_icon(
.child(IconName::ChevronRight), has_icon,
), icon.clone(),
) cx,
.when_some(self.hovered_menu_ix, |this, hovered_ix| { ))
let (anchor, left) = .child(
if cx.bounds().size.width - bounds.origin.x < max_width { h_flex()
(AnchorCorner::TopRight, -px(15.)) .flex_1()
} else { .gap_2()
(AnchorCorner::TopLeft, bounds.size.width - px(10.)) .items_center()
}; .justify_between()
.child(label.clone())
.child(IconName::ChevronRight),
),
)
.when_some(self.hovered_menu_ix, |this, hovered_ix| {
let (anchor, left) = if cx.bounds().size.width
- bounds.origin.x
< max_width
{
(AnchorCorner::TopRight, -px(15.))
} else {
(AnchorCorner::TopLeft, bounds.size.width - px(10.))
};
let top = if bounds.origin.y + bounds.size.height let top = if bounds.origin.y + bounds.size.height
> cx.bounds().size.height > cx.bounds().size.height
{ {
px(32.) px(32.)
} else { } else {
-px(10.) -px(10.)
}; };
if hovered_ix == ix { if hovered_ix == ix {
this.child(anchored().anchor(anchor).child( this.child(
div().occlude().top(top).left(left).child(menu.clone()), anchored().anchor(anchor).child(
)) div()
} else { .occlude()
this .top(top)
} .left(left)
}), .child(menu.clone()),
), ),
} )
})) } else {
this
}
}),
),
}
}),
)
} }
} }