835 lines
25 KiB
Rust
835 lines
25 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::prelude::FluentBuilder;
|
|
use gpui::{
|
|
Action, Anchor, App, AppContext, Axis, Bounds, ClickEvent, Context, DismissEvent, Edges,
|
|
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, KeyBinding,
|
|
MouseDownEvent, ParentElement, Pixels, Point, Render, SharedString, Styled, WeakEntity, Window,
|
|
anchored, div, px, rems,
|
|
};
|
|
use gpui_base::actions::{Cancel, Confirm, SelectDown, SelectLeft, SelectRight, SelectUp};
|
|
use theme::{ActiveTheme, Side, TextStyle, Typeset as _};
|
|
|
|
use crate::kbd::Kbd;
|
|
use crate::menu::menu_item::MenuItemElement;
|
|
use crate::{Disableable, ElementExt, Icon, IconName, Sizable as _, StyledExt, h_flex, v_flex};
|
|
|
|
const CONTEXT: &str = "PopupMenu";
|
|
const MAX_WIDTH: Pixels = px(500.);
|
|
|
|
pub(crate) fn init(cx: &mut App) {
|
|
cx.bind_keys([
|
|
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
|
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
|
KeyBinding::new("up", SelectUp, Some(CONTEXT)),
|
|
KeyBinding::new("down", SelectDown, Some(CONTEXT)),
|
|
KeyBinding::new("left", SelectLeft, Some(CONTEXT)),
|
|
KeyBinding::new("right", SelectRight, Some(CONTEXT)),
|
|
]);
|
|
}
|
|
|
|
/// A menu item in a popup menu.
|
|
pub enum PopupMenuItem {
|
|
/// A menu separator item.
|
|
Separator,
|
|
/// A non-interactive label item.
|
|
Label(SharedString),
|
|
/// A standard menu item.
|
|
Item {
|
|
icon: Option<Icon>,
|
|
label: SharedString,
|
|
disabled: bool,
|
|
checked: bool,
|
|
action: Option<Box<dyn Action>>,
|
|
#[allow(clippy::type_complexity)]
|
|
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
|
},
|
|
/// A submenu item that opens another popup menu.
|
|
Submenu {
|
|
icon: Option<Icon>,
|
|
label: SharedString,
|
|
disabled: bool,
|
|
menu: Entity<PopupMenu>,
|
|
},
|
|
}
|
|
|
|
impl FluentBuilder for PopupMenuItem {}
|
|
|
|
impl PopupMenuItem {
|
|
/// Create a new menu item with the given label.
|
|
#[inline]
|
|
pub fn new(label: impl Into<SharedString>) -> Self {
|
|
PopupMenuItem::Item {
|
|
icon: None,
|
|
label: label.into(),
|
|
disabled: false,
|
|
checked: false,
|
|
action: None,
|
|
handler: None,
|
|
}
|
|
}
|
|
|
|
/// Create a new submenu item that opens another popup menu.
|
|
#[inline]
|
|
fn submenu(label: impl Into<SharedString>, menu: Entity<PopupMenu>) -> Self {
|
|
PopupMenuItem::Submenu {
|
|
icon: None,
|
|
label: label.into(),
|
|
disabled: false,
|
|
menu,
|
|
}
|
|
}
|
|
|
|
/// Create a separator menu item.
|
|
#[inline]
|
|
fn separator() -> Self {
|
|
PopupMenuItem::Separator
|
|
}
|
|
|
|
/// Create a label menu item.
|
|
#[inline]
|
|
fn label(label: impl Into<SharedString>) -> Self {
|
|
PopupMenuItem::Label(label.into())
|
|
}
|
|
|
|
/// Set the icon for the menu item.
|
|
fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
|
match &mut self {
|
|
PopupMenuItem::Item { icon: i, .. } => {
|
|
*i = Some(icon.into());
|
|
}
|
|
PopupMenuItem::Submenu { icon: i, .. } => {
|
|
*i = Some(icon.into());
|
|
}
|
|
_ => {}
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Set the action for the menu item.
|
|
fn action(mut self, action: Box<dyn Action>) -> Self {
|
|
if let PopupMenuItem::Item { action: a, .. } = &mut self {
|
|
*a = Some(action);
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Set the disabled state for the menu item.
|
|
fn disabled(mut self, disabled: bool) -> Self {
|
|
match &mut self {
|
|
PopupMenuItem::Item { disabled: d, .. } => {
|
|
*d = disabled;
|
|
}
|
|
PopupMenuItem::Submenu { disabled: d, .. } => {
|
|
*d = disabled;
|
|
}
|
|
_ => {}
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Set the checked state for the menu item.
|
|
fn checked(mut self, checked: bool) -> Self {
|
|
if let PopupMenuItem::Item { checked: c, .. } = &mut self {
|
|
*c = checked;
|
|
}
|
|
self
|
|
}
|
|
|
|
/// Add a click handler for the menu item.
|
|
pub fn on_click<F>(mut self, handler: F) -> Self
|
|
where
|
|
F: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
{
|
|
if let PopupMenuItem::Item { handler: h, .. } = &mut self {
|
|
*h = Some(Rc::new(handler));
|
|
}
|
|
self
|
|
}
|
|
|
|
#[inline]
|
|
fn is_clickable(&self) -> bool {
|
|
matches!(
|
|
self,
|
|
PopupMenuItem::Item {
|
|
disabled: false,
|
|
..
|
|
} | PopupMenuItem::Submenu {
|
|
disabled: false,
|
|
..
|
|
}
|
|
)
|
|
}
|
|
|
|
#[inline]
|
|
fn is_separator(&self) -> bool {
|
|
matches!(self, PopupMenuItem::Separator)
|
|
}
|
|
|
|
fn has_left_icon(&self) -> bool {
|
|
match self {
|
|
PopupMenuItem::Item { icon, checked, .. } => icon.is_some() || *checked,
|
|
PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn is_checked(&self) -> bool {
|
|
matches!(self, PopupMenuItem::Item { checked: true, .. })
|
|
}
|
|
}
|
|
|
|
pub struct PopupMenu {
|
|
focus_handle: FocusHandle,
|
|
menu_items: Vec<PopupMenuItem>,
|
|
axis: Axis,
|
|
selected_index: Option<usize>,
|
|
bounds: Bounds<Pixels>,
|
|
|
|
/// The parent menu of this menu, if this is a submenu
|
|
parent_menu: Option<WeakEntity<Self>>,
|
|
|
|
/// This will update on render
|
|
submenu_anchor: (Anchor, Pixels),
|
|
}
|
|
|
|
impl PopupMenu {
|
|
pub(crate) fn new(cx: &mut App) -> Self {
|
|
Self {
|
|
focus_handle: cx.focus_handle(),
|
|
parent_menu: None,
|
|
menu_items: Vec::new(),
|
|
selected_index: None,
|
|
axis: Axis::Vertical,
|
|
bounds: Bounds::default(),
|
|
submenu_anchor: (Anchor::TopLeft, Pixels::ZERO),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn build(
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
f: impl FnOnce(Self, &mut Window, &mut Context<PopupMenu>) -> Self,
|
|
) -> Entity<Self> {
|
|
cx.new(|cx| f(Self::new(cx), window, cx))
|
|
}
|
|
|
|
/// Set the axis of children to horizontal.
|
|
pub fn horizontal(mut self) -> Self {
|
|
self.axis = Axis::Horizontal;
|
|
self
|
|
}
|
|
|
|
/// Add Menu Item
|
|
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
|
|
self.add_menu_item(label, None, action, false, false);
|
|
self
|
|
}
|
|
|
|
/// Add label
|
|
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
|
|
self.menu_items.push(PopupMenuItem::label(label.into()));
|
|
self
|
|
}
|
|
|
|
/// Add Menu Item with Icon.
|
|
pub fn menu_with_icon(
|
|
mut self,
|
|
label: impl Into<SharedString>,
|
|
icon: impl Into<Icon>,
|
|
action: Box<dyn Action>,
|
|
) -> Self {
|
|
self.add_menu_item(label, Some(icon.into()), action, false, false);
|
|
self
|
|
}
|
|
|
|
/// Add Menu Item with check icon
|
|
pub fn menu_with_check(
|
|
self,
|
|
label: impl Into<SharedString>,
|
|
checked: bool,
|
|
action: Box<dyn Action>,
|
|
) -> Self {
|
|
self.menu_with_check_and_disabled(label, checked, action, false)
|
|
}
|
|
|
|
/// Add Menu Item with check icon and disabled state
|
|
pub fn menu_with_check_and_disabled(
|
|
mut self,
|
|
label: impl Into<SharedString>,
|
|
checked: bool,
|
|
action: Box<dyn Action>,
|
|
disabled: bool,
|
|
) -> Self {
|
|
self.add_menu_item(label, None, action, disabled, checked);
|
|
self
|
|
}
|
|
|
|
/// Add a separator Menu Item
|
|
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
|
|
}
|
|
|
|
/// Add a Submenu
|
|
pub fn submenu(
|
|
mut self,
|
|
label: impl Into<SharedString>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
|
) -> Self {
|
|
let submenu = PopupMenu::build(window, cx, f);
|
|
let parent_menu = cx.entity().downgrade();
|
|
submenu.update(cx, |view, _| {
|
|
view.parent_menu = Some(parent_menu);
|
|
});
|
|
|
|
self.menu_items.push(PopupMenuItem::submenu(label, submenu));
|
|
self
|
|
}
|
|
|
|
/// Add menu item.
|
|
pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
|
|
let item: PopupMenuItem = item.into();
|
|
self.menu_items.push(item);
|
|
self
|
|
}
|
|
|
|
fn add_menu_item(
|
|
&mut self,
|
|
label: impl Into<SharedString>,
|
|
icon: Option<Icon>,
|
|
action: Box<dyn Action>,
|
|
disabled: bool,
|
|
checked: bool,
|
|
) -> &mut Self {
|
|
self.menu_items.push(
|
|
PopupMenuItem::new(label)
|
|
.when_some(icon, |item, icon| item.icon(icon))
|
|
.disabled(disabled)
|
|
.checked(checked)
|
|
.action(action),
|
|
);
|
|
self
|
|
}
|
|
|
|
fn active_submenu(&self) -> Option<Entity<PopupMenu>> {
|
|
if let Some(ix) = self.selected_index
|
|
&& let Some(item) = self.menu_items.get(ix)
|
|
{
|
|
return match item {
|
|
PopupMenuItem::Submenu { menu, .. } => Some(menu.clone()),
|
|
_ => None,
|
|
};
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn clickable_menu_items(&self) -> impl Iterator<Item = (usize, &PopupMenuItem)> {
|
|
self.menu_items
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, item)| item.is_clickable())
|
|
}
|
|
|
|
fn on_click(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
|
|
cx.stop_propagation();
|
|
window.prevent_default();
|
|
self.selected_index = Some(ix);
|
|
self.confirm(&Confirm { secondary: false }, window, cx);
|
|
}
|
|
|
|
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
|
if let Some(index) = self.selected_index
|
|
&& let Some(PopupMenuItem::Item {
|
|
handler, action, ..
|
|
}) = self.menu_items.get(index)
|
|
{
|
|
if let Some(handler) = handler {
|
|
handler(&ClickEvent::default(), window, cx);
|
|
} else if let Some(action) = action.as_ref() {
|
|
window.dispatch_action(action.boxed_clone(), cx);
|
|
}
|
|
|
|
self.dismiss(&Cancel, window, cx)
|
|
}
|
|
}
|
|
|
|
fn set_selected_index(&mut self, ix: usize, cx: &mut Context<Self>) {
|
|
if self.selected_index != Some(ix) {
|
|
self.selected_index = Some(ix);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
fn select_up(&mut self, _: &SelectUp, _: &mut Window, cx: &mut Context<Self>) {
|
|
cx.stop_propagation();
|
|
let ix = self.selected_index.unwrap_or(0);
|
|
|
|
if let Some((prev_ix, _)) = self
|
|
.menu_items
|
|
.iter()
|
|
.enumerate()
|
|
.rev()
|
|
.find(|(i, item)| *i < ix && item.is_clickable())
|
|
{
|
|
self.set_selected_index(prev_ix, cx);
|
|
return;
|
|
}
|
|
|
|
let last_clickable_ix = self.clickable_menu_items().last().map(|(ix, _)| ix);
|
|
self.set_selected_index(last_clickable_ix.unwrap_or(0), cx);
|
|
}
|
|
|
|
fn select_down(&mut self, _: &SelectDown, _: &mut Window, cx: &mut Context<Self>) {
|
|
cx.stop_propagation();
|
|
let Some(ix) = self.selected_index else {
|
|
self.set_selected_index(0, cx);
|
|
return;
|
|
};
|
|
|
|
if let Some((next_ix, _)) = self
|
|
.menu_items
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(i, item)| *i > ix && item.is_clickable())
|
|
{
|
|
self.set_selected_index(next_ix, cx);
|
|
return;
|
|
}
|
|
|
|
self.set_selected_index(0, cx);
|
|
}
|
|
|
|
fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
|
let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
|
|
self._unselect_submenu(window, cx)
|
|
} else {
|
|
self._select_submenu(window, cx)
|
|
};
|
|
|
|
if self.parent_side(cx).is_left() {
|
|
self._focus_parent_menu(window, cx);
|
|
}
|
|
|
|
if handled {
|
|
return;
|
|
}
|
|
|
|
// For parent AppMenuBar to handle.
|
|
if self.parent_menu.is_none() {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
|
let handled = if matches!(self.submenu_anchor.0, Anchor::TopLeft | Anchor::BottomLeft) {
|
|
self._select_submenu(window, cx)
|
|
} else {
|
|
self._unselect_submenu(window, cx)
|
|
};
|
|
|
|
if self.parent_side(cx).is_right() {
|
|
self._focus_parent_menu(window, cx);
|
|
}
|
|
|
|
if handled {
|
|
return;
|
|
}
|
|
|
|
// For parent AppMenuBar to handle.
|
|
if self.parent_menu.is_none() {
|
|
cx.propagate();
|
|
}
|
|
}
|
|
|
|
fn _select_submenu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
|
|
if let Some(active_submenu) = self.active_submenu() {
|
|
// Focus the submenu, so that can be handle the action.
|
|
active_submenu.update(cx, |view, cx| {
|
|
view.set_selected_index(0, cx);
|
|
view.focus_handle.focus(window, cx);
|
|
});
|
|
cx.notify();
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool {
|
|
if let Some(active_submenu) = self.active_submenu() {
|
|
active_submenu.update(cx, |view, cx| {
|
|
view.selected_index = None;
|
|
cx.notify();
|
|
});
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(parent) = self.parent_menu.as_ref() else {
|
|
return;
|
|
};
|
|
let Some(parent) = parent.upgrade() else {
|
|
return;
|
|
};
|
|
|
|
self.selected_index = None;
|
|
parent.update(cx, |view, cx| {
|
|
view.focus_handle.focus(window, cx);
|
|
cx.notify();
|
|
});
|
|
}
|
|
|
|
fn parent_side(&self, cx: &App) -> Side {
|
|
let Some(parent) = self.parent_menu.as_ref() else {
|
|
return Side::Left;
|
|
};
|
|
|
|
let Some(parent) = parent.upgrade() else {
|
|
return Side::Left;
|
|
};
|
|
|
|
match parent.read(cx).submenu_anchor.0 {
|
|
Anchor::TopLeft | Anchor::BottomLeft => Side::Left,
|
|
Anchor::TopRight | Anchor::BottomRight => Side::Right,
|
|
_ => Side::Left,
|
|
}
|
|
}
|
|
|
|
#[expect(clippy::only_used_in_recursion)]
|
|
fn dismiss(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
|
if self.active_submenu().is_some() {
|
|
return;
|
|
}
|
|
|
|
cx.emit(DismissEvent);
|
|
|
|
let Some(parent_menu) = self.parent_menu.clone() else {
|
|
return;
|
|
};
|
|
|
|
// Dismiss parent menu, when this menu is dismissed
|
|
if let Err(error) = parent_menu.update(cx, |view, cx| {
|
|
view.selected_index = None;
|
|
view.dismiss(&Cancel, window, cx);
|
|
}) {
|
|
log::warn!("Failed to dismiss parent menu: {error}");
|
|
}
|
|
}
|
|
|
|
fn handle_dismiss(
|
|
&mut self,
|
|
position: &Point<Pixels>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// Do not dismiss, if click inside the parent menu
|
|
if let Some(parent) = self.parent_menu.as_ref()
|
|
&& let Some(parent) = parent.upgrade()
|
|
&& parent.read(cx).bounds.contains(position)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.dismiss(&Cancel, window, cx);
|
|
}
|
|
|
|
fn on_mouse_down_out(
|
|
&mut self,
|
|
e: &MouseDownEvent,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.handle_dismiss(&e.position, window, cx);
|
|
}
|
|
|
|
fn render_key_binding(
|
|
&self,
|
|
action: Option<Box<dyn Action>>,
|
|
window: &mut Window,
|
|
_: &mut Context<Self>,
|
|
) -> Option<Kbd> {
|
|
let action = action?;
|
|
|
|
Kbd::binding_for_action(action.as_ref(), None, window).map(|this| {
|
|
this.p_0()
|
|
.flex_nowrap()
|
|
.border_0()
|
|
.bg(gpui::transparent_white())
|
|
})
|
|
}
|
|
|
|
fn render_icon(
|
|
has_icon: bool,
|
|
checked: bool,
|
|
icon: Option<Icon>,
|
|
_: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<impl IntoElement> {
|
|
if !has_icon {
|
|
return None;
|
|
}
|
|
|
|
let icon = if let Some(icon) = icon {
|
|
icon.clone()
|
|
} else if checked {
|
|
Icon::new(IconName::Check).text_color(cx.theme().accent)
|
|
} else {
|
|
Icon::empty()
|
|
};
|
|
|
|
Some(icon)
|
|
}
|
|
|
|
/// Calculate the anchor corner and left offset for child submenu
|
|
fn update_submenu_menu_anchor(&mut self, window: &Window) {
|
|
let bounds = self.bounds;
|
|
let (anchor, left) = if MAX_WIDTH + bounds.origin.x > window.bounds().size.width {
|
|
(Anchor::TopRight, -px(16.))
|
|
} else {
|
|
(Anchor::TopLeft, bounds.size.width - px(8.))
|
|
};
|
|
|
|
let is_bottom_pos = bounds.origin.y + bounds.size.height > window.bounds().size.height;
|
|
self.submenu_anchor = if is_bottom_pos {
|
|
(anchor.other_side_along(gpui::Axis::Vertical), left)
|
|
} else {
|
|
(anchor, left)
|
|
};
|
|
}
|
|
|
|
fn render_item(
|
|
&self,
|
|
ix: usize,
|
|
item: &PopupMenuItem,
|
|
options: RenderOptions,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> MenuItemElement {
|
|
let has_left_icon = options.has_left_icon;
|
|
let is_checked = item.is_checked();
|
|
|
|
let selected = self.selected_index == Some(ix);
|
|
const EDGE_PADDING: Pixels = px(4.);
|
|
const INNER_PADDING: Pixels = px(4.);
|
|
|
|
let is_submenu = matches!(item, PopupMenuItem::Submenu { .. });
|
|
let group_name = format!("{}:item-{}", cx.entity().entity_id(), ix);
|
|
|
|
let item_height = px(26.);
|
|
|
|
let this = MenuItemElement::new(ix, &group_name)
|
|
.relative()
|
|
.text_style(TextStyle::Callout)
|
|
.py_0()
|
|
.px(INNER_PADDING)
|
|
.rounded(options.radius)
|
|
.items_center()
|
|
.selected(selected)
|
|
.on_hover(cx.listener(move |this, hovered, _, cx| {
|
|
if *hovered {
|
|
this.selected_index = Some(ix);
|
|
} else if !is_submenu && this.selected_index == Some(ix) {
|
|
// TODO: Better handle the submenu unselection when hover out
|
|
this.selected_index = None;
|
|
}
|
|
|
|
cx.notify();
|
|
}));
|
|
|
|
match item {
|
|
PopupMenuItem::Separator => this
|
|
.h_auto()
|
|
.p_0()
|
|
.my_0p5()
|
|
.mx_neg_1()
|
|
.border_b(px(2.))
|
|
.border_color(cx.theme().border_faint)
|
|
.disabled(true),
|
|
PopupMenuItem::Label(label) => this.disabled(true).cursor_default().child(
|
|
h_flex().cursor_default().items_center().gap_x_1().child(
|
|
div()
|
|
.flex_1()
|
|
.text_style(TextStyle::Callout)
|
|
.font_semibold()
|
|
.text_color(cx.theme().text_muted)
|
|
.child(label.clone()),
|
|
),
|
|
),
|
|
PopupMenuItem::Item {
|
|
icon,
|
|
label,
|
|
action,
|
|
disabled,
|
|
..
|
|
} => {
|
|
let action = action.as_ref().map(|action| action.boxed_clone());
|
|
let key = self.render_key_binding(action, window, cx);
|
|
|
|
this.when(!disabled, |this| {
|
|
this.on_click(
|
|
cx.listener(move |this, _, window, cx| this.on_click(ix, window, cx)),
|
|
)
|
|
})
|
|
.disabled(*disabled)
|
|
.h(item_height)
|
|
.gap_x(px(theme::SPACE))
|
|
.children(Self::render_icon(
|
|
has_left_icon,
|
|
is_checked,
|
|
icon.clone(),
|
|
window,
|
|
cx,
|
|
))
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.gap_3()
|
|
.items_center()
|
|
.justify_between()
|
|
.child(label.clone())
|
|
.children(key),
|
|
)
|
|
}
|
|
PopupMenuItem::Submenu {
|
|
icon,
|
|
label,
|
|
menu,
|
|
disabled,
|
|
} => this
|
|
.selected(selected)
|
|
.disabled(*disabled)
|
|
.items_start()
|
|
.child(
|
|
h_flex()
|
|
.min_h(item_height)
|
|
.size_full()
|
|
.items_center()
|
|
.gap_x(px(theme::SPACE))
|
|
.children(Self::render_icon(
|
|
has_left_icon,
|
|
false,
|
|
icon.clone(),
|
|
window,
|
|
cx,
|
|
))
|
|
.child(
|
|
h_flex()
|
|
.flex_1()
|
|
.gap(px(theme::SPACE))
|
|
.items_center()
|
|
.justify_between()
|
|
.child(label.clone())
|
|
.child(
|
|
Icon::new(IconName::CaretRight)
|
|
.xsmall()
|
|
.text_color(cx.theme().text_muted),
|
|
),
|
|
),
|
|
)
|
|
.when(selected, |this| {
|
|
this.child({
|
|
let (anchor, left) = self.submenu_anchor;
|
|
let is_bottom_pos =
|
|
matches!(anchor, Anchor::BottomLeft | Anchor::BottomRight);
|
|
anchored()
|
|
.anchor(anchor)
|
|
.child(
|
|
div()
|
|
.id("submenu")
|
|
.occlude()
|
|
.when(is_bottom_pos, |this| this.bottom_0())
|
|
.when(!is_bottom_pos, |this| this.top_neg_1())
|
|
.left(left)
|
|
.child(menu.clone()),
|
|
)
|
|
.snap_to_window_with_margin(Edges::all(EDGE_PADDING))
|
|
})
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl FluentBuilder for PopupMenu {}
|
|
|
|
impl EventEmitter<DismissEvent> for PopupMenu {}
|
|
|
|
impl Focusable for PopupMenu {
|
|
fn focus_handle(&self, _: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct RenderOptions {
|
|
has_left_icon: bool,
|
|
radius: Pixels,
|
|
}
|
|
|
|
impl Render for PopupMenu {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
self.update_submenu_menu_anchor(window);
|
|
|
|
let view = cx.entity().clone();
|
|
let items_count = self.menu_items.len();
|
|
|
|
let has_left_icon = self.menu_items.iter().any(|item| item.has_left_icon());
|
|
|
|
let options = RenderOptions {
|
|
has_left_icon,
|
|
// Items sit p_1 (4px) inside the surface plate; keep corners concentric.
|
|
radius: px(theme::inset_radius(theme::surface_radius(), 4.0)),
|
|
};
|
|
|
|
v_flex()
|
|
.id("popup-menu")
|
|
.key_context(CONTEXT)
|
|
.track_focus(&self.focus_handle)
|
|
.on_action(cx.listener(Self::select_up))
|
|
.on_action(cx.listener(Self::select_down))
|
|
.on_action(cx.listener(Self::select_left))
|
|
.on_action(cx.listener(Self::select_right))
|
|
.on_action(cx.listener(Self::confirm))
|
|
.on_action(cx.listener(Self::dismiss))
|
|
.on_mouse_down_out(cx.listener(Self::on_mouse_down_out))
|
|
.popover_style(cx)
|
|
.text_color(cx.theme().text)
|
|
.relative()
|
|
.occlude()
|
|
.child(
|
|
div()
|
|
.id("items")
|
|
.p_1()
|
|
.gap_y_0p5()
|
|
.min_w(rems(8.))
|
|
.max_w(MAX_WIDTH)
|
|
.map(|this| match self.axis {
|
|
Axis::Horizontal => this.flex().flex_row().items_center(),
|
|
Axis::Vertical => this.flex().flex_col(),
|
|
})
|
|
.children(
|
|
self.menu_items
|
|
.iter()
|
|
.enumerate()
|
|
// Ignore last separator
|
|
.filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
|
|
.map(|(ix, item)| self.render_item(ix, item, options, window, cx)),
|
|
)
|
|
.on_prepaint(move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds)),
|
|
)
|
|
}
|
|
}
|