Add link, link_with_icon to PopupMenu, and remove track_focus. (#121)

- Add `popup_menu` quick API to Button to let easy to use.
This commit is contained in:
Jason Lee 2024-08-08 19:50:10 +08:00 committed by GitHub
parent 27d094c590
commit 966ce4c2dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 80 deletions

View file

@ -365,19 +365,17 @@ impl Render for LocaleSelector {
.anchor(AnchorCorner::TopRight) .anchor(AnchorCorner::TopRight)
.trigger(Button::new("btn", cx).small().ghost().icon(IconName::Globe)) .trigger(Button::new("btn", cx).small().ghost().icon(IconName::Globe))
.content(move |cx| { .content(move |cx| {
let focus_handle = focus_handle.clone();
PopupMenu::build(cx, |this, _cx| { PopupMenu::build(cx, |this, _cx| {
this.track_focus(focus_handle) this.menu_with_check(
.menu_with_check( "English",
"English", locale == "en",
locale == "en", Box::new(SelectLocale("en".into())),
Box::new(SelectLocale("en".into())), )
) .menu_with_check(
.menu_with_check( "简体中文",
"简体中文", locale == "zh-CN",
locale == "zh-CN", Box::new(SelectLocale("zh-CN".into())),
Box::new(SelectLocale("zh-CN".into())), )
)
}) })
}), }),
) )

View file

@ -10,7 +10,7 @@ use ui::{
h_flex, h_flex,
input::TextInput, input::TextInput,
popover::{Popover, PopoverContent}, popover::{Popover, PopoverContent},
popup_menu::PopupMenu, popup_menu::PopupMenuExt,
prelude::FluentBuilder, prelude::FluentBuilder,
switch::Switch, switch::Switch,
v_flex, Clickable as _, IconName, Sizable, v_flex, Clickable as _, IconName, Sizable,
@ -103,7 +103,6 @@ impl FocusableView for PopupStory {
impl Render for PopupStory { impl Render for PopupStory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let form = self.form.clone(); let form = self.form.clone();
let focus_handle = self.focus_handle.clone();
let window_mode = self.window_mode; let window_mode = self.window_mode;
v_flex() v_flex()
@ -120,10 +119,8 @@ impl Render for PopupStory {
cx.focus(&this.focus_handle); cx.focus(&this.focus_handle);
})) }))
.context_menu({ .context_menu({
let focus_handle = focus_handle.clone(); move |this, _cx| {
move |menu, _cx| { this.menu("Cut", Box::new(Cut))
menu.track_focus(focus_handle.clone())
.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()
@ -133,7 +130,7 @@ impl Render for PopupStory {
.gap_6() .gap_6()
.child( .child(
Switch::new("switch-window-mode") Switch::new("switch-window-mode")
.checked(self.window_mode) .checked(window_mode)
.label("Use Window Popover") .label("Use Window Popover")
.on_click(cx.listener(|this, checked, _| { .on_click(cx.listener(|this, checked, _| {
this.window_mode = *checked; this.window_mode = *checked;
@ -146,7 +143,7 @@ impl Render for PopupStory {
.child( .child(
v_flex().gap_4().child( v_flex().gap_4().child(
Popover::new("info-top-left") Popover::new("info-top-left")
.when(self.window_mode, |this| this.window_mode()) .when(window_mode, |this| this.window_mode())
.trigger(Button::new("info-top-left", cx).label("Top Left")) .trigger(Button::new("info-top-left", cx).label("Top Left"))
.content(|cx| { .content(|cx| {
PopoverContent::new(cx, |cx| { PopoverContent::new(cx, |cx| {
@ -167,7 +164,7 @@ impl Render for PopupStory {
) )
.child( .child(
Popover::new("info-top-right") Popover::new("info-top-right")
.when(self.window_mode, |this| this.window_mode()) .when(window_mode, |this| this.window_mode())
.anchor(AnchorCorner::TopRight) .anchor(AnchorCorner::TopRight)
.trigger(Button::new("info-top-right", cx).label("Top Right")) .trigger(Button::new("info-top-right", cx).label("Top Right"))
.content(|cx| { .content(|cx| {
@ -192,25 +189,22 @@ impl Render for PopupStory {
h_flex() h_flex()
.gap_3() .gap_3()
.child( .child(
Popover::new("popup-menu") Button::new("popup-menu-1", cx)
.when(window_mode, |this| this.window_mode()) .icon(IconName::Ellipsis)
.trigger(Button::new("popup-menu-1", cx).icon(IconName::Ellipsis)) .popup_menu(|this, _| {
.content(move |cx| { this.menu("Copy", Box::new(Copy))
let focus_handle = focus_handle.clone(); .menu("Cut", Box::new(Cut))
PopupMenu::build(cx, |menu, _cx| { .menu("Paste", Box::new(Paste))
menu.track_focus(focus_handle) .separator()
.menu("Copy", Box::new(Copy)) .menu_with_icon("Search", IconName::Search, Box::new(SearchAll))
.menu("Cut", Box::new(Cut)) .separator()
.menu("Paste", Box::new(Paste)) .menu_with_check("Check Menu", true, Box::new(SearchAll))
.separator() .separator()
.menu_with_icon( .link_with_icon(
IconName::Search, "GitHub Repository",
"Search", IconName::GitHub,
Box::new(SearchAll), "https://github.com/huacnlee/gpui-component",
) )
.separator()
.menu_with_check("Check Menu", true, Box::new(SearchAll))
})
}), }),
) )
.child(self.message.clone()), .child(self.message.clone()),
@ -223,7 +217,7 @@ impl Render for PopupStory {
.justify_between() .justify_between()
.child( .child(
Popover::new("info-bottom-left") Popover::new("info-bottom-left")
.when(self.window_mode, |this| this.window_mode()) .when(window_mode, |this| this.window_mode())
.anchor(AnchorCorner::BottomLeft) .anchor(AnchorCorner::BottomLeft)
.trigger( .trigger(
Button::new("pop", cx).label("Popup with Form").w(px(300.)), Button::new("pop", cx).label("Popup with Form").w(px(300.)),
@ -232,7 +226,7 @@ impl Render for PopupStory {
) )
.child( .child(
Popover::new("info-bottom-right") Popover::new("info-bottom-right")
.when(self.window_mode, |this| this.window_mode()) .when(window_mode, |this| this.window_mode())
.anchor(AnchorCorner::BottomRight) .anchor(AnchorCorner::BottomRight)
.mouse_button(MouseButton::Right) .mouse_button(MouseButton::Right)
.trigger( .trigger(

View file

@ -1,14 +1,15 @@
use std::rc::Rc; use std::rc::Rc;
use gpui::FocusableView;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder, px, Action, AnyWindowHandle, AppContext, Context, actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter,
DismissEvent, EventEmitter, FocusHandle, FocusableView, InteractiveElement, KeyBinding, FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render,
ParentElement, Pixels, Render, SharedString, Styled as _, View, ViewContext, SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext,
VisualContext as _, WindowContext,
}; };
use crate::{ use crate::{
h_flex, list::ListItem, styled_ext::Sizable as _, theme::ActiveTheme, v_flex, Icon, IconName, button::Button, h_flex, list::ListItem, popover::Popover, styled_ext::Sizable as _,
theme::ActiveTheme, v_flex, Icon, IconName, Selectable,
}; };
actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]); actions!(menu, [Confirm, Dismiss, SelectNext, SelectPrev]);
@ -23,12 +24,24 @@ pub fn init(cx: &mut AppContext) {
]); ]);
} }
pub trait PopupMenuExt: Selectable + IntoElement + 'static {
fn popup_menu(
self,
f: impl Fn(PopupMenu, &mut WindowContext) -> PopupMenu + 'static,
) -> Popover<PopupMenu> {
Popover::new("popup-menu")
.trigger(self)
.content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx)))
}
}
impl PopupMenuExt for Button {}
enum PopupMenuItem { enum PopupMenuItem {
Separator, Separator,
Item { Item {
icon: Option<Icon>, icon: Option<Icon>,
label: SharedString, label: SharedString,
handler: Rc<dyn Fn(Option<&FocusHandle>, &mut WindowContext)>, handler: Rc<dyn Fn(&mut WindowContext)>,
}, },
} }
@ -44,9 +57,6 @@ impl PopupMenuItem {
pub struct PopupMenu { pub struct PopupMenu {
focus_handle: FocusHandle, focus_handle: FocusHandle,
/// The parent window handle
window_handle: AnyWindowHandle,
action_context: Option<FocusHandle>,
menu_items: Vec<PopupMenuItem>, menu_items: Vec<PopupMenuItem>,
has_icon: bool, has_icon: bool,
selected_index: Option<usize>, selected_index: Option<usize>,
@ -68,8 +78,6 @@ impl PopupMenu {
let menu = Self { let menu = Self {
focus_handle, focus_handle,
action_context: None,
window_handle: cx.window_handle(),
menu_items: Vec::new(), menu_items: Vec::new(),
selected_index: None, selected_index: None,
min_width: px(120.), min_width: px(120.),
@ -94,28 +102,46 @@ impl PopupMenu {
self self
} }
/// You must set content (FocusHandle) with the parent view, if the menu action is listening on the parent view.
/// When the Menu Item confirmed, the parent view will be focused again to ensure to receive the action.
#[must_use]
pub fn track_focus(mut self, focus_handle: FocusHandle) -> Self {
self.action_context = Some(focus_handle);
self
}
/// Add Menu Item /// Add Menu Item
pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self { pub fn menu(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.add_menu_item(None, label, action); self.add_menu_item(label, None, action);
self self
} }
/// Add Menu to open link
pub fn link(mut self, label: impl Into<SharedString>, href: impl Into<String>) -> Self {
let href = href.into();
self.menu_items.push(PopupMenuItem::Item {
icon: None,
label: label.into(),
handler: Rc::new(move |cx| cx.open_url(&href)),
});
self
}
/// Add Menu to open link
pub fn link_with_icon(
mut self,
label: impl Into<SharedString>,
icon: impl Into<Icon>,
href: impl Into<String>,
) -> Self {
let href = href.into();
self.menu_items.push(PopupMenuItem::Item {
icon: Some(icon.into()),
label: label.into(),
handler: Rc::new(move |cx| cx.open_url(&href)),
});
self
}
/// Add Menu Item with Icon /// Add Menu Item with Icon
pub fn menu_with_icon( pub fn menu_with_icon(
mut self, mut self,
icon: impl Into<Icon>,
label: impl Into<SharedString>, label: impl Into<SharedString>,
icon: impl Into<Icon>,
action: Box<dyn Action>, action: Box<dyn Action>,
) -> Self { ) -> Self {
self.add_menu_item(Some(icon.into()), label, action); self.add_menu_item(label, Some(icon.into()), action);
self self
} }
@ -127,9 +153,9 @@ impl PopupMenu {
action: Box<dyn Action>, action: Box<dyn Action>,
) -> Self { ) -> Self {
if checked { if checked {
self.add_menu_item(Some(IconName::Check.into()), label, action); self.add_menu_item(label, Some(IconName::Check.into()), action);
} else { } else {
self.add_menu_item(None, label, action); self.add_menu_item(label, None, action);
} }
self self
@ -137,11 +163,10 @@ impl PopupMenu {
fn add_menu_item( fn add_menu_item(
&mut self, &mut self,
icon: Option<Icon>,
label: impl Into<SharedString>, label: impl Into<SharedString>,
icon: Option<Icon>,
action: Box<dyn Action>, action: Box<dyn Action>,
) -> &mut Self { ) -> &mut Self {
let window_handle = self.window_handle;
if icon.is_some() { if icon.is_some() {
self.has_icon = true; self.has_icon = true;
} }
@ -149,15 +174,9 @@ impl PopupMenu {
self.menu_items.push(PopupMenuItem::Item { self.menu_items.push(PopupMenuItem::Item {
icon, icon,
label: label.into(), label: label.into(),
handler: Rc::new(move |handle, cx| { handler: Rc::new(move |cx| {
if let Some(handle) = handle { cx.activate_window();
cx.update_window(window_handle, |_, cx| { cx.dispatch_action(action.boxed_clone());
cx.activate_window();
cx.focus(handle);
cx.dispatch_action(action.boxed_clone());
})
.unwrap();
}
}), }),
}); });
self self
@ -184,13 +203,12 @@ impl PopupMenu {
} }
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
let handle = self.action_context.as_ref();
match self.selected_index { match self.selected_index {
Some(index) => { Some(index) => {
let item = self.menu_items.get(index); let item = self.menu_items.get(index);
match item { match item {
Some(PopupMenuItem::Item { handler, .. }) => { Some(PopupMenuItem::Item { handler, .. }) => {
handler(handle, cx); handler(cx);
self.dismiss(&Dismiss, cx) self.dismiss(&Dismiss, cx)
} }
_ => {} _ => {}
@ -290,7 +308,7 @@ impl Render for PopupMenu {
}) })
.child( .child(
div() div()
.when(has_icon, |this| this.pl(px(18.))) .when(has_icon, |this| this.pl(px(19.)).pr_2())
.child(label.clone()), .child(label.clone()),
), ),
) )