menu: Add check_side to PopupMenu to support display check at right side. (#1677)
<img width="507" height="511" alt="image" src="https://github.com/user-attachments/assets/2d89381a-2acb-49d8-be69-8cb6ab211f41" />
This commit is contained in:
parent
5ae9311202
commit
e70d072b88
6 changed files with 247 additions and 176 deletions
|
|
@ -3,7 +3,7 @@ use gpui::{
|
||||||
ParentElement as _, Render, SharedString, Styled as _, Window, actions, div, px,
|
ParentElement as _, Render, SharedString, Styled as _, Window, actions, div, px,
|
||||||
};
|
};
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme as _, IconName, StyledExt,
|
ActiveTheme as _, IconName, Side, StyledExt,
|
||||||
button::Button,
|
button::Button,
|
||||||
h_flex,
|
h_flex,
|
||||||
menu::{ContextMenuExt, DropdownMenu as _, PopupMenuItem},
|
menu::{ContextMenuExt, DropdownMenu as _, PopupMenuItem},
|
||||||
|
|
@ -38,11 +38,12 @@ pub fn init(cx: &mut App) {
|
||||||
KeyBinding::new("cmd-shift-f", SearchAll, Some(CONTEXT)),
|
KeyBinding::new("cmd-shift-f", SearchAll, Some(CONTEXT)),
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyBinding::new("ctrl-shift-f", SearchAll, Some(CONTEXT)),
|
KeyBinding::new("ctrl-shift-f", SearchAll, Some(CONTEXT)),
|
||||||
|
KeyBinding::new("ctrl-shift-alt-t", ToggleCheck, Some(CONTEXT)),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MenuStory {
|
pub struct MenuStory {
|
||||||
checked: bool,
|
check_side: Option<Side>,
|
||||||
message: String,
|
message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +68,7 @@ impl MenuStory {
|
||||||
|
|
||||||
fn new(_: &mut Window, _: &mut Context<Self>) -> Self {
|
fn new(_: &mut Window, _: &mut Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
checked: true,
|
check_side: None,
|
||||||
message: "".to_string(),
|
message: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -98,15 +99,22 @@ impl MenuStory {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_action_toggle_check(&mut self, _: &ToggleCheck, _: &mut Window, cx: &mut Context<Self>) {
|
fn on_action_toggle_check(&mut self, _: &ToggleCheck, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.checked = !self.checked;
|
self.check_side = if self.check_side == Some(Side::Left) {
|
||||||
self.message = format!("You have clicked toggle check: {}", self.checked);
|
Some(Side::Right)
|
||||||
|
} else if self.check_side == Some(Side::Right) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(Side::Left)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.message = format!("You have used check at side: {:?}", self.check_side);
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for MenuStory {
|
impl Render for MenuStory {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let checked = self.checked;
|
let check_side = self.check_side;
|
||||||
let view = cx.entity();
|
let view = cx.entity();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|
@ -128,6 +136,7 @@ impl Render for MenuStory {
|
||||||
.label("Edit")
|
.label("Edit")
|
||||||
.dropdown_menu(move |this, window, cx| {
|
.dropdown_menu(move |this, window, cx| {
|
||||||
this.link("About", "https://github.com/longbridge/gpui-component")
|
this.link("About", "https://github.com/longbridge/gpui-component")
|
||||||
|
.check_side(check_side.unwrap_or(Side::Left))
|
||||||
.separator()
|
.separator()
|
||||||
.item(PopupMenuItem::new("Handle Click").on_click(
|
.item(PopupMenuItem::new("Handle Click").on_click(
|
||||||
window.listener_for(&view, |this, _, _, cx| {
|
window.listener_for(&view, |this, _, _, cx| {
|
||||||
|
|
@ -141,7 +150,11 @@ impl Render for MenuStory {
|
||||||
.menu("Cut", Box::new(Cut))
|
.menu("Cut", Box::new(Cut))
|
||||||
.menu("Paste", Box::new(Paste))
|
.menu("Paste", Box::new(Paste))
|
||||||
.separator()
|
.separator()
|
||||||
.menu_with_check("Toggle Check", checked, Box::new(ToggleCheck))
|
.menu_with_check(
|
||||||
|
format!("Check Side {:?}", check_side),
|
||||||
|
check_side.is_some(),
|
||||||
|
Box::new(ToggleCheck),
|
||||||
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.menu_with_icon("Search", IconName::Search, Box::new(SearchAll))
|
.menu_with_icon("Search", IconName::Search, Box::new(SearchAll))
|
||||||
.separator()
|
.separator()
|
||||||
|
|
@ -162,14 +175,18 @@ impl Render for MenuStory {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.menu_element_with_check(checked, Box::new(Info(0)), |_, cx| {
|
.menu_element_with_check(
|
||||||
h_flex().gap_1().child("Custom Element").child(
|
check_side.is_some(),
|
||||||
div()
|
Box::new(ToggleCheck),
|
||||||
.text_xs()
|
|_, cx| {
|
||||||
.text_color(cx.theme().muted_foreground)
|
h_flex().gap_1().child("Custom Element").child(
|
||||||
.child("checked"),
|
div()
|
||||||
)
|
.text_xs()
|
||||||
})
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("checked"),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
.menu_element_with_icon(
|
.menu_element_with_icon(
|
||||||
IconName::Info,
|
IconName::Info,
|
||||||
Box::new(Info(0)),
|
Box::new(Info(0)),
|
||||||
|
|
@ -222,7 +239,8 @@ impl Render for MenuStory {
|
||||||
.child("Right click to open ContextMenu")
|
.child("Right click to open ContextMenu")
|
||||||
.context_menu({
|
.context_menu({
|
||||||
move |this, window, cx| {
|
move |this, window, cx| {
|
||||||
this.external_link_icon(false)
|
this.check_side(check_side.unwrap_or(Side::Left))
|
||||||
|
.external_link_icon(false)
|
||||||
.link(
|
.link(
|
||||||
"About",
|
"About",
|
||||||
"https://github.com/longbridge/gpui-component",
|
"https://github.com/longbridge/gpui-component",
|
||||||
|
|
@ -234,8 +252,8 @@ impl Render for MenuStory {
|
||||||
.separator()
|
.separator()
|
||||||
.label("This is a label")
|
.label("This is a label")
|
||||||
.menu_with_check(
|
.menu_with_check(
|
||||||
"Toggle Check",
|
format!("Check Side {:?}", check_side),
|
||||||
checked,
|
check_side.is_some(),
|
||||||
Box::new(ToggleCheck),
|
Box::new(ToggleCheck),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,10 @@ use gpui::{
|
||||||
Window, div, px,
|
Window, div, px,
|
||||||
};
|
};
|
||||||
use gpui_component::{
|
use gpui_component::{
|
||||||
ActiveTheme as _, IconName, PixelsExt, Sizable as _, Theme, TitleBar, WindowExt as _,
|
ActiveTheme as _, IconName, PixelsExt, Side, Sizable as _, Theme, TitleBar, WindowExt as _,
|
||||||
badge::Badge,
|
badge::Badge,
|
||||||
button::{Button, ButtonVariants as _},
|
button::{Button, ButtonVariants as _},
|
||||||
menu::AppMenuBar,
|
menu::{AppMenuBar, DropdownMenu as _},
|
||||||
menu::DropdownMenu as _,
|
|
||||||
scroll::ScrollbarShow,
|
scroll::ScrollbarShow,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -155,6 +154,7 @@ impl Render for FontSizeSelector {
|
||||||
.icon(IconName::Settings2)
|
.icon(IconName::Settings2)
|
||||||
.dropdown_menu(move |this, _, _| {
|
.dropdown_menu(move |this, _, _| {
|
||||||
this.scrollable(true)
|
this.scrollable(true)
|
||||||
|
.check_side(Side::Right)
|
||||||
.max_h(px(480.))
|
.max_h(px(480.))
|
||||||
.label("Font Size")
|
.label("Font Size")
|
||||||
.menu_with_check("Large", font_size == 18, Box::new(SelectFont(18)))
|
.menu_with_check("Large", font_size == 18, Box::new(SelectFont(18)))
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ pub enum PopupMenuItem {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
checked: bool,
|
||||||
is_link: bool,
|
is_link: bool,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
// For link item
|
// For link item
|
||||||
|
|
@ -46,6 +47,7 @@ pub enum PopupMenuItem {
|
||||||
ElementItem {
|
ElementItem {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
checked: bool,
|
||||||
action: Option<Box<dyn Action>>,
|
action: Option<Box<dyn Action>>,
|
||||||
render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
|
render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
|
||||||
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
|
|
@ -70,6 +72,7 @@ impl PopupMenuItem {
|
||||||
icon: None,
|
icon: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
checked: false,
|
||||||
action: None,
|
action: None,
|
||||||
is_link: false,
|
is_link: false,
|
||||||
handler: None,
|
handler: None,
|
||||||
|
|
@ -86,6 +89,7 @@ impl PopupMenuItem {
|
||||||
PopupMenuItem::ElementItem {
|
PopupMenuItem::ElementItem {
|
||||||
icon: None,
|
icon: None,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
checked: false,
|
||||||
action: None,
|
action: None,
|
||||||
render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
|
render: Box::new(move |window, cx| builder(window, cx).into_any_element()),
|
||||||
handler: None,
|
handler: None,
|
||||||
|
|
@ -169,24 +173,16 @@ impl PopupMenuItem {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set checked state for the menu item by adding or removing check icon.
|
/// Set checked state for the menu item.
|
||||||
///
|
///
|
||||||
/// If true, will set the icon to check icon, otherwise remove the icon.
|
/// NOTE: If `check_side` is [`Side::Left`], the icon will replace with a check icon.
|
||||||
pub fn checked(mut self, checked: bool) -> Self {
|
pub fn checked(mut self, checked: bool) -> Self {
|
||||||
match &mut self {
|
match &mut self {
|
||||||
PopupMenuItem::Item { icon: i, .. } => {
|
PopupMenuItem::Item { checked: c, .. } => {
|
||||||
if checked {
|
*c = checked;
|
||||||
*i = Some(IconName::Check.into());
|
|
||||||
} else {
|
|
||||||
*i = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
PopupMenuItem::ElementItem { icon: i, .. } => {
|
PopupMenuItem::ElementItem { checked: c, .. } => {
|
||||||
if checked {
|
*c = checked;
|
||||||
*i = Some(IconName::Check.into());
|
|
||||||
} else {
|
|
||||||
*i = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +216,7 @@ impl PopupMenuItem {
|
||||||
icon: None,
|
icon: None,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
checked: false,
|
||||||
action: None,
|
action: None,
|
||||||
is_link: true,
|
is_link: true,
|
||||||
handler: Some(Rc::new(move |_, _, cx| cx.open_url(&href))),
|
handler: Some(Rc::new(move |_, _, cx| cx.open_url(&href))),
|
||||||
|
|
@ -249,14 +246,27 @@ impl PopupMenuItem {
|
||||||
matches!(self, PopupMenuItem::Separator)
|
matches!(self, PopupMenuItem::Separator)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_icon(&self) -> bool {
|
fn has_left_icon(&self, check_side: Side) -> bool {
|
||||||
match self {
|
match self {
|
||||||
PopupMenuItem::Item { icon, .. } => icon.is_some(),
|
PopupMenuItem::Item { icon, checked, .. } => {
|
||||||
PopupMenuItem::ElementItem { icon, .. } => icon.is_some(),
|
icon.is_some() || (check_side.is_left() && *checked)
|
||||||
|
}
|
||||||
|
PopupMenuItem::ElementItem { icon, checked, .. } => {
|
||||||
|
icon.is_some() || (check_side.is_left() && *checked)
|
||||||
|
}
|
||||||
PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
|
PopupMenuItem::Submenu { icon, .. } => icon.is_some(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn is_checked(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
PopupMenuItem::Item { checked, .. } => *checked,
|
||||||
|
PopupMenuItem::ElementItem { checked, .. } => *checked,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopupMenu {
|
pub struct PopupMenu {
|
||||||
|
|
@ -264,13 +274,13 @@ pub struct PopupMenu {
|
||||||
pub(crate) menu_items: Vec<PopupMenuItem>,
|
pub(crate) menu_items: Vec<PopupMenuItem>,
|
||||||
/// The focus handle of Entity to handle actions.
|
/// The focus handle of Entity to handle actions.
|
||||||
pub(crate) action_context: Option<FocusHandle>,
|
pub(crate) action_context: Option<FocusHandle>,
|
||||||
has_icon: bool,
|
|
||||||
selected_index: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
min_width: Option<Pixels>,
|
min_width: Option<Pixels>,
|
||||||
max_width: Option<Pixels>,
|
max_width: Option<Pixels>,
|
||||||
max_height: Option<Pixels>,
|
max_height: Option<Pixels>,
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
size: Size,
|
size: Size,
|
||||||
|
check_side: Side,
|
||||||
|
|
||||||
/// The parent menu of this menu, if this is a submenu
|
/// The parent menu of this menu, if this is a submenu
|
||||||
parent_menu: Option<WeakEntity<Self>>,
|
parent_menu: Option<WeakEntity<Self>>,
|
||||||
|
|
@ -295,7 +305,7 @@ impl PopupMenu {
|
||||||
min_width: None,
|
min_width: None,
|
||||||
max_width: None,
|
max_width: None,
|
||||||
max_height: None,
|
max_height: None,
|
||||||
has_icon: false,
|
check_side: Side::Left,
|
||||||
bounds: Bounds::default(),
|
bounds: Bounds::default(),
|
||||||
scrollable: false,
|
scrollable: false,
|
||||||
scroll_handle: ScrollHandle::default(),
|
scroll_handle: ScrollHandle::default(),
|
||||||
|
|
@ -351,6 +361,12 @@ impl PopupMenu {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the side to show check icon, default is `Side::Left`.
|
||||||
|
pub fn check_side(mut self, side: Side) -> Self {
|
||||||
|
self.check_side = side;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the menu to show external link icon, default is true.
|
/// Set the menu to show external link icon, default is true.
|
||||||
pub fn external_link_icon(mut self, visible: bool) -> Self {
|
pub fn external_link_icon(mut self, visible: bool) -> Self {
|
||||||
self.external_link_icon = visible;
|
self.external_link_icon = visible;
|
||||||
|
|
@ -369,7 +385,7 @@ impl PopupMenu {
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
enable: bool,
|
enable: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.add_menu_item(label, None, action, !enable);
|
self.add_menu_item(label, None, action, !enable, false);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +396,7 @@ impl PopupMenu {
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.add_menu_item(label, None, action, disabled);
|
self.add_menu_item(label, None, action, disabled, false);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -453,7 +469,7 @@ impl PopupMenu {
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.add_menu_item(label, Some(icon.into()), action, disabled);
|
self.add_menu_item(label, Some(icon.into()), action, disabled, false);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,12 +491,7 @@ impl PopupMenu {
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
if checked {
|
self.add_menu_item(label, None, action, disabled, checked);
|
||||||
self.add_menu_item(label, Some(IconName::Check.into()), action, disabled);
|
|
||||||
} else {
|
|
||||||
self.add_menu_item(label, None, action, disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -553,7 +564,6 @@ impl PopupMenu {
|
||||||
.icon(icon)
|
.icon(icon)
|
||||||
.disabled(disabled),
|
.disabled(disabled),
|
||||||
);
|
);
|
||||||
self.has_icon = true;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -572,10 +582,9 @@ impl PopupMenu {
|
||||||
self.menu_items.push(
|
self.menu_items.push(
|
||||||
PopupMenuItem::element(builder)
|
PopupMenuItem::element(builder)
|
||||||
.action(action)
|
.action(action)
|
||||||
.when(checked, |item| item.icon(IconName::Check))
|
.checked(checked)
|
||||||
.disabled(disabled),
|
.disabled(disabled),
|
||||||
);
|
);
|
||||||
self.has_icon = self.has_icon || checked;
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,9 +637,6 @@ impl PopupMenu {
|
||||||
/// Add menu item.
|
/// Add menu item.
|
||||||
pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
|
pub fn item(mut self, item: impl Into<PopupMenuItem>) -> Self {
|
||||||
let item: PopupMenuItem = item.into();
|
let item: PopupMenuItem = item.into();
|
||||||
if item.has_icon() {
|
|
||||||
self.has_icon = true;
|
|
||||||
}
|
|
||||||
self.menu_items.push(item);
|
self.menu_items.push(item);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
@ -647,15 +653,13 @@ impl PopupMenu {
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
checked: bool,
|
||||||
) -> &mut Self {
|
) -> &mut Self {
|
||||||
if icon.is_some() {
|
|
||||||
self.has_icon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.menu_items.push(
|
self.menu_items.push(
|
||||||
PopupMenuItem::new(label)
|
PopupMenuItem::new(label)
|
||||||
.when_some(icon, |item, icon| item.icon(icon))
|
.when_some(icon, |item, icon| item.icon(icon))
|
||||||
.disabled(disabled)
|
.disabled(disabled)
|
||||||
|
.checked(checked)
|
||||||
.action(action),
|
.action(action),
|
||||||
);
|
);
|
||||||
self
|
self
|
||||||
|
|
@ -966,6 +970,7 @@ impl PopupMenu {
|
||||||
|
|
||||||
fn render_icon(
|
fn render_icon(
|
||||||
has_icon: bool,
|
has_icon: bool,
|
||||||
|
checked: bool,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
_: &mut Window,
|
_: &mut Window,
|
||||||
_: &mut Context<Self>,
|
_: &mut Context<Self>,
|
||||||
|
|
@ -976,6 +981,8 @@ impl PopupMenu {
|
||||||
|
|
||||||
let icon = if let Some(icon) = icon {
|
let icon = if let Some(icon) = icon {
|
||||||
icon.clone()
|
icon.clone()
|
||||||
|
} else if checked {
|
||||||
|
Icon::new(IconName::Check)
|
||||||
} else {
|
} else {
|
||||||
Icon::empty()
|
Icon::empty()
|
||||||
};
|
};
|
||||||
|
|
@ -1010,11 +1017,18 @@ impl PopupMenu {
|
||||||
&self,
|
&self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
item: &PopupMenuItem,
|
item: &PopupMenuItem,
|
||||||
state: ItemState,
|
options: RenderOptions,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> MenuItemElement {
|
) -> MenuItemElement {
|
||||||
let has_icon = self.has_icon;
|
let has_left_icon = options.has_left_icon;
|
||||||
|
let is_left_check = options.check_side.is_left() && item.is_checked();
|
||||||
|
let right_check_icon = if options.check_side.is_right() && item.is_checked() {
|
||||||
|
Some(Icon::new(IconName::Check).xsmall())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let selected = self.selected_index == Some(ix);
|
let selected = self.selected_index == Some(ix);
|
||||||
const EDGE_PADDING: Pixels = px(4.);
|
const EDGE_PADDING: Pixels = px(4.);
|
||||||
const INNER_PADDING: Pixels = px(8.);
|
const INNER_PADDING: Pixels = px(8.);
|
||||||
|
|
@ -1023,8 +1037,8 @@ impl PopupMenu {
|
||||||
let group_name = format!("popup-menu-item-{}", ix);
|
let group_name = format!("popup-menu-item-{}", ix);
|
||||||
|
|
||||||
let (item_height, radius) = match self.size {
|
let (item_height, radius) = match self.size {
|
||||||
Size::Small => (px(20.), state.radius.half()),
|
Size::Small => (px(20.), options.radius.half()),
|
||||||
_ => (px(26.), state.radius),
|
_ => (px(26.), options.radius),
|
||||||
};
|
};
|
||||||
|
|
||||||
let this = MenuItemElement::new(ix, &group_name)
|
let this = MenuItemElement::new(ix, &group_name)
|
||||||
|
|
@ -1060,7 +1074,7 @@ impl PopupMenu {
|
||||||
.cursor_default()
|
.cursor_default()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.children(Self::render_icon(has_icon, None, window, cx))
|
.children(Self::render_icon(has_left_icon, false, None, window, cx))
|
||||||
.child(div().flex_1().child(label.clone())),
|
.child(div().flex_1().child(label.clone())),
|
||||||
),
|
),
|
||||||
PopupMenuItem::ElementItem {
|
PopupMenuItem::ElementItem {
|
||||||
|
|
@ -1081,8 +1095,15 @@ impl PopupMenu {
|
||||||
.min_h(item_height)
|
.min_h(item_height)
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
.children(Self::render_icon(
|
||||||
.child((render)(window, cx)),
|
has_left_icon,
|
||||||
|
is_left_check,
|
||||||
|
icon.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
|
.child((render)(window, cx))
|
||||||
|
.children(right_check_icon.map(|icon| icon.ml_3())),
|
||||||
),
|
),
|
||||||
PopupMenuItem::Item {
|
PopupMenuItem::Item {
|
||||||
icon,
|
icon,
|
||||||
|
|
@ -1103,14 +1124,22 @@ impl PopupMenu {
|
||||||
})
|
})
|
||||||
.disabled(*disabled)
|
.disabled(*disabled)
|
||||||
.h(item_height)
|
.h(item_height)
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
.gap_x_1()
|
||||||
|
.children(Self::render_icon(
|
||||||
|
has_left_icon,
|
||||||
|
is_left_check,
|
||||||
|
icon.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_3()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when(!show_link_icon, |this| this.child(label.clone()))
|
.when(!show_link_icon, |this| this.child(label.clone()))
|
||||||
|
.children(right_check_icon)
|
||||||
.when(show_link_icon, |this| {
|
.when(show_link_icon, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
@ -1143,7 +1172,13 @@ impl PopupMenu {
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.children(Self::render_icon(has_icon, icon.clone(), window, cx))
|
.children(Self::render_icon(
|
||||||
|
has_left_icon,
|
||||||
|
false,
|
||||||
|
icon.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|
@ -1186,7 +1221,9 @@ impl Focusable for PopupMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct ItemState {
|
struct RenderOptions {
|
||||||
|
has_left_icon: bool,
|
||||||
|
check_side: Side,
|
||||||
radius: Pixels,
|
radius: Pixels,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1205,8 +1242,15 @@ impl Render for PopupMenu {
|
||||||
|height| height,
|
|height| height,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let has_left_icon = self
|
||||||
|
.menu_items
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.has_left_icon(self.check_side));
|
||||||
|
|
||||||
let max_width = self.max_width();
|
let max_width = self.max_width();
|
||||||
let item_state = ItemState {
|
let options = RenderOptions {
|
||||||
|
has_left_icon,
|
||||||
|
check_side: self.check_side,
|
||||||
radius: cx.theme().radius.min(px(8.)),
|
radius: cx.theme().radius.min(px(8.)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1254,7 +1298,7 @@ impl Render for PopupMenu {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
// Ignore last separator
|
// Ignore last separator
|
||||||
.filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
|
.filter(|(ix, item)| !(*ix + 1 == items_count && item.is_separator()))
|
||||||
.map(|(ix, item)| self.render_item(ix, item, item_state, window, cx)),
|
.map(|(ix, item)| self.render_item(ix, item, options, window, cx)),
|
||||||
)
|
)
|
||||||
.child({
|
.child({
|
||||||
canvas(
|
canvas(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds,
|
AnyElement, App, Bounds, Context, Corner, DismissEvent, ElementId, EventEmitter, FocusHandle,
|
||||||
Context, Corner, DismissEvent, ElementId, EventEmitter, FocusHandle, Focusable,
|
Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement,
|
||||||
InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
|
Pixels, Point, Render, RenderOnce, StyleRefinement, Styled, Subscription, Window, anchored,
|
||||||
Render, RenderOnce, StyleRefinement, Styled, Subscription, Window,
|
canvas, deferred, div, prelude::FluentBuilder as _, px,
|
||||||
};
|
};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{actions::Cancel, v_flex, Selectable, StyledExt as _};
|
use crate::{Selectable, StyledExt as _, actions::Cancel, v_flex};
|
||||||
|
|
||||||
const CONTEXT: &str = "Popover";
|
const CONTEXT: &str = "Popover";
|
||||||
pub(crate) fn init(cx: &mut App) {
|
pub(crate) fn init(cx: &mut App) {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{ActiveTheme, StyledExt};
|
use crate::{ActiveTheme, StyledExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder, px, relative, App, Hsla, IntoElement, ParentElement, RenderOnce,
|
App, Hsla, IntoElement, ParentElement, RenderOnce, StyleRefinement, Styled, Window, div,
|
||||||
StyleRefinement, Styled, Window,
|
prelude::FluentBuilder, px, relative,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A Progress bar element.
|
/// A Progress bar element.
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ However, if you prefer not to use [Action]s, you can create custom menu items us
|
||||||
There have a `on_click` callback to handle the click event directly.
|
There have a `on_click` callback to handle the click event directly.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Menu with Anchor Position
|
### Anchor Position
|
||||||
|
|
||||||
Control where the dropdown menu appears relative to the trigger:
|
Control where the dropdown menu appears relative to the trigger:
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ Button::new("menu-btn")
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### Menu Items with Icons
|
### Icons
|
||||||
|
|
||||||
Add icons to menu items for better visual clarity:
|
Add icons to menu items for better visual clarity:
|
||||||
|
|
||||||
|
|
@ -101,7 +101,22 @@ menu.menu_with_icon("Search", IconName::Search, Box::new(Search))
|
||||||
.menu_with_icon("Help", IconName::Help, Box::new(ShowHelp))
|
.menu_with_icon("Help", IconName::Help, Box::new(ShowHelp))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Checkable Menu Items
|
### Disabled State
|
||||||
|
|
||||||
|
Create disabled menu items that cannot be activated:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
menu.menu("Available Action", Box::new(Action1))
|
||||||
|
.menu_with_disabled("Disabled Action", Box::new(Action2), true)
|
||||||
|
.menu_with_icon_and_disabled(
|
||||||
|
"Unavailable",
|
||||||
|
IconName::Lock,
|
||||||
|
Box::new(Action3),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check state
|
||||||
|
|
||||||
Create menu items that show a check state:
|
Create menu items that show a check state:
|
||||||
|
|
||||||
|
|
@ -112,7 +127,94 @@ menu.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature))
|
||||||
.menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar))
|
.menu_with_check("Show Sidebar", sidebar_visible, Box::new(ToggleSidebar))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Menu Items with Keyboard Shortcuts
|
By default, the check icon will be shown on the left side of the menu item, if this menu item has an icon, the check icon will replace the icon on the left side.
|
||||||
|
|
||||||
|
There also have a `check_side` option for you to config the check icon to be shown on the right side:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
menu.check_size(Side::Right)
|
||||||
|
.menu_with_check("Enable Feature", is_enabled, Box::new(ToggleFeature))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Separators
|
||||||
|
|
||||||
|
Use separators to group related menu items:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
menu.menu("New", Box::new(NewFile))
|
||||||
|
.menu("Open", Box::new(OpenFile))
|
||||||
|
.separator() // Groups file operations
|
||||||
|
.menu("Copy", Box::new(Copy))
|
||||||
|
.menu("Paste", Box::new(Paste))
|
||||||
|
.separator() // Groups edit operations
|
||||||
|
.menu("Exit", Box::new(Exit))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Labels
|
||||||
|
|
||||||
|
Add non-interactive labels to organize menu sections:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
menu.label("File Operations")
|
||||||
|
.menu("New", Box::new(NewFile))
|
||||||
|
.menu("Open", Box::new(OpenFile))
|
||||||
|
.separator()
|
||||||
|
.label("Edit Operations")
|
||||||
|
.menu("Copy", Box::new(Copy))
|
||||||
|
.menu("Paste", Box::new(Paste))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Link MenuItem
|
||||||
|
|
||||||
|
Create menu items that open external links:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
menu.link("Documentation", "https://docs.example.com")
|
||||||
|
.link_with_icon(
|
||||||
|
"GitHub",
|
||||||
|
IconName::GitHub,
|
||||||
|
"https://github.com/example/repo"
|
||||||
|
)
|
||||||
|
.separator()
|
||||||
|
.external_link_icon(false) // Hide external link icons
|
||||||
|
.link("Support", "https://support.example.com")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Element
|
||||||
|
|
||||||
|
Create custom menu items with complex content:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use gpui_component::{h_flex, v_flex};
|
||||||
|
|
||||||
|
menu.menu_element(Box::new(CustomAction), |window, cx| {
|
||||||
|
v_flex()
|
||||||
|
.child("Custom Element")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child("This is a subtitle")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.menu_element_with_icon(
|
||||||
|
IconName::Info,
|
||||||
|
Box::new(InfoAction),
|
||||||
|
|window, cx| {
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child("Status")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.text_color(cx.theme().success)
|
||||||
|
.child("✓ Connected")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Keyboard Shortcuts
|
||||||
|
|
||||||
Menu items automatically display keyboard shortcuts if they're bound to actions:
|
Menu items automatically display keyboard shortcuts if they're bound to actions:
|
||||||
|
|
||||||
|
|
@ -168,99 +270,6 @@ menu.submenu_with_icon(
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Disabled Menu Items
|
|
||||||
|
|
||||||
Create disabled menu items that cannot be activated:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
menu.menu("Available Action", Box::new(Action1))
|
|
||||||
.menu_with_disabled("Disabled Action", Box::new(Action2), true)
|
|
||||||
.menu_with_icon_and_disabled(
|
|
||||||
"Unavailable",
|
|
||||||
IconName::Lock,
|
|
||||||
Box::new(Action3),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Menu Separators
|
|
||||||
|
|
||||||
Use separators to group related menu items:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
menu.menu("New", Box::new(NewFile))
|
|
||||||
.menu("Open", Box::new(OpenFile))
|
|
||||||
.separator() // Groups file operations
|
|
||||||
.menu("Copy", Box::new(Copy))
|
|
||||||
.menu("Paste", Box::new(Paste))
|
|
||||||
.separator() // Groups edit operations
|
|
||||||
.menu("Exit", Box::new(Exit))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Menu Labels
|
|
||||||
|
|
||||||
Add non-interactive labels to organize menu sections:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
menu.label("File Operations")
|
|
||||||
.menu("New", Box::new(NewFile))
|
|
||||||
.menu("Open", Box::new(OpenFile))
|
|
||||||
.separator()
|
|
||||||
.label("Edit Operations")
|
|
||||||
.menu("Copy", Box::new(Copy))
|
|
||||||
.menu("Paste", Box::new(Paste))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Link Menu Items
|
|
||||||
|
|
||||||
Create menu items that open external links:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
menu.link("Documentation", "https://docs.example.com")
|
|
||||||
.link_with_icon(
|
|
||||||
"GitHub",
|
|
||||||
IconName::GitHub,
|
|
||||||
"https://github.com/example/repo"
|
|
||||||
)
|
|
||||||
.separator()
|
|
||||||
.external_link_icon(false) // Hide external link icons
|
|
||||||
.link("Support", "https://support.example.com")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Menu Elements
|
|
||||||
|
|
||||||
Create custom menu items with complex content:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use gpui_component::{h_flex, v_flex};
|
|
||||||
|
|
||||||
menu.menu_element(Box::new(CustomAction), |window, cx| {
|
|
||||||
v_flex()
|
|
||||||
.child("Custom Element")
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.child("This is a subtitle")
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.menu_element_with_icon(
|
|
||||||
IconName::Info,
|
|
||||||
Box::new(InfoAction),
|
|
||||||
|window, cx| {
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child("Status")
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().success)
|
|
||||||
.child("✓ Connected")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scrollable Menus
|
### Scrollable Menus
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue