diff --git a/crates/app/src/story_workspace.rs b/crates/app/src/story_workspace.rs index 384ce4e3..87fb1610 100644 --- a/crates/app/src/story_workspace.rs +++ b/crates/app/src/story_workspace.rs @@ -365,19 +365,17 @@ impl Render for LocaleSelector { .anchor(AnchorCorner::TopRight) .trigger(Button::new("btn", cx).small().ghost().icon(IconName::Globe)) .content(move |cx| { - let focus_handle = focus_handle.clone(); PopupMenu::build(cx, |this, _cx| { - this.track_focus(focus_handle) - .menu_with_check( - "English", - locale == "en", - Box::new(SelectLocale("en".into())), - ) - .menu_with_check( - "简体中文", - locale == "zh-CN", - Box::new(SelectLocale("zh-CN".into())), - ) + this.menu_with_check( + "English", + locale == "en", + Box::new(SelectLocale("en".into())), + ) + .menu_with_check( + "简体中文", + locale == "zh-CN", + Box::new(SelectLocale("zh-CN".into())), + ) }) }), ) diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index 907874d8..7fc348e6 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -10,7 +10,7 @@ use ui::{ h_flex, input::TextInput, popover::{Popover, PopoverContent}, - popup_menu::PopupMenu, + popup_menu::PopupMenuExt, prelude::FluentBuilder, switch::Switch, v_flex, Clickable as _, IconName, Sizable, @@ -103,7 +103,6 @@ impl FocusableView for PopupStory { impl Render for PopupStory { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let form = self.form.clone(); - let focus_handle = self.focus_handle.clone(); let window_mode = self.window_mode; v_flex() @@ -120,10 +119,8 @@ impl Render for PopupStory { cx.focus(&this.focus_handle); })) .context_menu({ - let focus_handle = focus_handle.clone(); - move |menu, _cx| { - menu.track_focus(focus_handle.clone()) - .menu("Cut", Box::new(Cut)) + move |this, _cx| { + this.menu("Cut", Box::new(Cut)) .menu("Copy", Box::new(Copy)) .menu("Paste", Box::new(Paste)) .separator() @@ -133,7 +130,7 @@ impl Render for PopupStory { .gap_6() .child( Switch::new("switch-window-mode") - .checked(self.window_mode) + .checked(window_mode) .label("Use Window Popover") .on_click(cx.listener(|this, checked, _| { this.window_mode = *checked; @@ -146,7 +143,7 @@ impl Render for PopupStory { .child( v_flex().gap_4().child( 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")) .content(|cx| { PopoverContent::new(cx, |cx| { @@ -167,7 +164,7 @@ impl Render for PopupStory { ) .child( Popover::new("info-top-right") - .when(self.window_mode, |this| this.window_mode()) + .when(window_mode, |this| this.window_mode()) .anchor(AnchorCorner::TopRight) .trigger(Button::new("info-top-right", cx).label("Top Right")) .content(|cx| { @@ -192,25 +189,22 @@ impl Render for PopupStory { h_flex() .gap_3() .child( - Popover::new("popup-menu") - .when(window_mode, |this| this.window_mode()) - .trigger(Button::new("popup-menu-1", cx).icon(IconName::Ellipsis)) - .content(move |cx| { - let focus_handle = focus_handle.clone(); - PopupMenu::build(cx, |menu, _cx| { - menu.track_focus(focus_handle) - .menu("Copy", Box::new(Copy)) - .menu("Cut", Box::new(Cut)) - .menu("Paste", Box::new(Paste)) - .separator() - .menu_with_icon( - IconName::Search, - "Search", - Box::new(SearchAll), - ) - .separator() - .menu_with_check("Check Menu", true, Box::new(SearchAll)) - }) + Button::new("popup-menu-1", cx) + .icon(IconName::Ellipsis) + .popup_menu(|this, _| { + this.menu("Copy", Box::new(Copy)) + .menu("Cut", Box::new(Cut)) + .menu("Paste", Box::new(Paste)) + .separator() + .menu_with_icon("Search", IconName::Search, Box::new(SearchAll)) + .separator() + .menu_with_check("Check Menu", true, Box::new(SearchAll)) + .separator() + .link_with_icon( + "GitHub Repository", + IconName::GitHub, + "https://github.com/huacnlee/gpui-component", + ) }), ) .child(self.message.clone()), @@ -223,7 +217,7 @@ impl Render for PopupStory { .justify_between() .child( Popover::new("info-bottom-left") - .when(self.window_mode, |this| this.window_mode()) + .when(window_mode, |this| this.window_mode()) .anchor(AnchorCorner::BottomLeft) .trigger( Button::new("pop", cx).label("Popup with Form").w(px(300.)), @@ -232,7 +226,7 @@ impl Render for PopupStory { ) .child( Popover::new("info-bottom-right") - .when(self.window_mode, |this| this.window_mode()) + .when(window_mode, |this| this.window_mode()) .anchor(AnchorCorner::BottomRight) .mouse_button(MouseButton::Right) .trigger( diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index e858cb5a..7a7c2905 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -1,14 +1,15 @@ use std::rc::Rc; +use gpui::FocusableView; use gpui::{ - actions, div, prelude::FluentBuilder, px, Action, AnyWindowHandle, AppContext, Context, - DismissEvent, EventEmitter, FocusHandle, FocusableView, InteractiveElement, KeyBinding, - ParentElement, Pixels, Render, SharedString, Styled as _, View, ViewContext, - VisualContext as _, WindowContext, + actions, div, prelude::FluentBuilder, px, Action, AppContext, DismissEvent, EventEmitter, + FocusHandle, InteractiveElement, IntoElement, KeyBinding, ParentElement, Pixels, Render, + SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext, }; 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]); @@ -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 { + Popover::new("popup-menu") + .trigger(self) + .content(move |cx| PopupMenu::build(cx, |menu, cx| f(menu, cx))) + } +} +impl PopupMenuExt for Button {} + enum PopupMenuItem { Separator, Item { icon: Option, label: SharedString, - handler: Rc, &mut WindowContext)>, + handler: Rc, }, } @@ -44,9 +57,6 @@ impl PopupMenuItem { pub struct PopupMenu { focus_handle: FocusHandle, - /// The parent window handle - window_handle: AnyWindowHandle, - action_context: Option, menu_items: Vec, has_icon: bool, selected_index: Option, @@ -68,8 +78,6 @@ impl PopupMenu { let menu = Self { focus_handle, - action_context: None, - window_handle: cx.window_handle(), menu_items: Vec::new(), selected_index: None, min_width: px(120.), @@ -94,28 +102,46 @@ impl PopupMenu { 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 pub fn menu(mut self, label: impl Into, action: Box) -> Self { - self.add_menu_item(None, label, action); + self.add_menu_item(label, None, action); self } + /// Add Menu to open link + pub fn link(mut self, label: impl Into, href: impl Into) -> 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, + icon: impl Into, + href: impl Into, + ) -> 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 pub fn menu_with_icon( mut self, - icon: impl Into, label: impl Into, + icon: impl Into, action: Box, ) -> Self { - self.add_menu_item(Some(icon.into()), label, action); + self.add_menu_item(label, Some(icon.into()), action); self } @@ -127,9 +153,9 @@ impl PopupMenu { action: Box, ) -> Self { if checked { - self.add_menu_item(Some(IconName::Check.into()), label, action); + self.add_menu_item(label, Some(IconName::Check.into()), action); } else { - self.add_menu_item(None, label, action); + self.add_menu_item(label, None, action); } self @@ -137,11 +163,10 @@ impl PopupMenu { fn add_menu_item( &mut self, - icon: Option, label: impl Into, + icon: Option, action: Box, ) -> &mut Self { - let window_handle = self.window_handle; if icon.is_some() { self.has_icon = true; } @@ -149,15 +174,9 @@ impl PopupMenu { self.menu_items.push(PopupMenuItem::Item { icon, label: label.into(), - handler: Rc::new(move |handle, cx| { - if let Some(handle) = handle { - cx.update_window(window_handle, |_, cx| { - cx.activate_window(); - cx.focus(handle); - cx.dispatch_action(action.boxed_clone()); - }) - .unwrap(); - } + handler: Rc::new(move |cx| { + cx.activate_window(); + cx.dispatch_action(action.boxed_clone()); }), }); self @@ -184,13 +203,12 @@ impl PopupMenu { } fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - let handle = self.action_context.as_ref(); match self.selected_index { Some(index) => { let item = self.menu_items.get(index); match item { Some(PopupMenuItem::Item { handler, .. }) => { - handler(handle, cx); + handler(cx); self.dismiss(&Dismiss, cx) } _ => {} @@ -290,7 +308,7 @@ impl Render for PopupMenu { }) .child( div() - .when(has_icon, |this| this.pl(px(18.))) + .when(has_icon, |this| this.pl(px(19.)).pr_2()) .child(label.clone()), ), )