From fd837565c4dbe80707b2f95516d2fb9defd9e5c8 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 16 Aug 2024 18:57:43 +0800 Subject: [PATCH] Add KeyBinding to MenuItem. (#164) image --- crates/story/src/lib.rs | 1 + crates/story/src/popup_story.rs | 36 +++++++++--- crates/ui/src/popup_menu.rs | 100 ++++++++++++++++++++++++-------- crates/ui/src/theme.rs | 5 -- 4 files changed, 106 insertions(+), 36 deletions(-) diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 8152f9d1..e6887c36 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -51,6 +51,7 @@ use ui::{divider::Divider, h_flex, label::Label, v_flex}; pub fn init(cx: &mut AppContext) { input_story::init(cx); dropdown_story::init(cx); + popup_story::init(cx); } pub fn section(title: impl IntoElement, cx: &WindowContext) -> Div { diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index 776378df..02fd415d 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -1,6 +1,6 @@ use gpui::{ actions, div, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, + FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Render, Styled as _, View, ViewContext, VisualContext, WindowContext, }; use ui::{ @@ -21,6 +21,15 @@ actions!( [Copy, Paste, Cut, SearchAll, ToggleWindowMode] ); +pub fn init(cx: &mut AppContext) { + cx.bind_keys([ + KeyBinding::new("cmd-c", Copy, None), + KeyBinding::new("cmd-v", Paste, None), + KeyBinding::new("cmd-x", Cut, None), + KeyBinding::new("cmd-shift-f", SearchAll, None), + ]) +} + struct Form { input1: View, } @@ -80,17 +89,25 @@ impl PopupStory { } } - fn on_copy(&mut self, _: &Copy, _: &mut ViewContext) { + fn on_copy(&mut self, _: &Copy, cx: &mut ViewContext) { self.message = "You have clicked copy".to_string(); + cx.notify() } - fn on_cut(&mut self, _: &Cut, _: &mut ViewContext) { + fn on_cut(&mut self, _: &Cut, cx: &mut ViewContext) { self.message = "You have clicked cut".to_string(); + cx.notify() } - fn on_paste(&mut self, _: &Paste, _: &mut ViewContext) { + fn on_paste(&mut self, _: &Paste, cx: &mut ViewContext) { self.message = "You have clicked paste".to_string(); + cx.notify() } - fn on_search_all(&mut self, _: &SearchAll, _: &mut ViewContext) { + fn on_search_all(&mut self, _: &SearchAll, cx: &mut ViewContext) { self.message = "You have clicked search all".to_string(); + cx.notify() + } + fn on_toggle_window_mode(&mut self, _: &ToggleWindowMode, cx: &mut ViewContext) { + self.window_mode = !self.window_mode; + cx.notify() } } @@ -111,6 +128,7 @@ impl Render for PopupStory { .on_action(cx.listener(Self::on_cut)) .on_action(cx.listener(Self::on_paste)) .on_action(cx.listener(Self::on_search_all)) + .on_action(cx.listener(Self::on_toggle_window_mode)) .p_4() .mb_5() .size_full() @@ -191,14 +209,18 @@ impl Render for PopupStory { .child( Button::new("popup-menu-1", cx) .icon(IconName::Ellipsis) - .popup_menu(|this, _| { + .popup_menu(move |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)) + .menu_with_check( + "Window Mode", + window_mode, + Box::new(ToggleWindowMode), + ) .separator() .link_with_icon( "GitHub Repository", diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 77a2bb38..7e1ddd3e 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -1,11 +1,12 @@ +use std::ops::Deref; use std::rc::Rc; -use gpui::FocusableView; use gpui::{ 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 gpui::{rems, FocusableView}; use crate::{ button::Button, h_flex, list::ListItem, popover::Popover, theme::ActiveTheme, v_flex, Icon, @@ -41,6 +42,7 @@ enum PopupMenuItem { Item { icon: Option, label: SharedString, + action: Option>, handler: Rc, }, } @@ -114,6 +116,7 @@ impl PopupMenu { self.menu_items.push(PopupMenuItem::Item { icon: None, label: label.into(), + action: None, handler: Rc::new(move |cx| cx.open_url(&href)), }); self @@ -130,6 +133,7 @@ impl PopupMenu { self.menu_items.push(PopupMenuItem::Item { icon: Some(icon.into()), label: label.into(), + action: None, handler: Rc::new(move |cx| cx.open_url(&href)), }); self @@ -174,6 +178,7 @@ impl PopupMenu { self.menu_items.push(PopupMenuItem::Item { icon, label: label.into(), + action: Some(action.boxed_clone()), handler: Rc::new(move |cx| { cx.activate_window(); cx.dispatch_action(action.boxed_clone()); @@ -246,6 +251,26 @@ impl PopupMenu { fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { cx.emit(DismissEvent); } + + fn render_keybinding( + action: Option>, + cx: &ViewContext, + ) -> Option { + if let Some(action) = action { + if let Some(keybinding) = cx.bindings_for_action(action.deref()).first() { + let el = div().text_color(cx.theme().muted_foreground).children( + keybinding + .keystrokes() + .into_iter() + .map(|keystroke| format!("{}", keystroke)), + ); + + return Some(el); + } + } + + return None; + } } impl FluentBuilder for PopupMenu {} @@ -278,7 +303,8 @@ impl Render for PopupMenu { .min_w(self.min_width) .p_1() .gap_y_0p5() - .bg(cx.theme().menu) + .min_w(rems(8.)) + .text_color(cx.theme().popover_foreground) .children(self.menu_items.iter_mut().enumerate().map(|(ix, item)| { let this = ListItem::new(("menu-item", ix)) .p_0() @@ -287,31 +313,57 @@ impl Render for PopupMenu { PopupMenuItem::Separator => this.disabled(true).child( div() .h(px(1.)) - .w_full() + .mx_neg_1() .my_px() .border_0() - .bg(cx.theme().border), + .bg(cx.theme().muted), ), - PopupMenuItem::Item { icon, label, .. } => { - this.py(px(2.)).px_2().rounded_md().text_sm().child( - h_flex() - .size_full() - .items_center() - .map(|this| { - this.child(div().absolute().text_sm().map(|this| { - if let Some(icon) = icon { - this.child(icon.clone().small().clone()) - } else { - this.children(icon_placeholder.clone()) - } - })) - }) - .child( - div() - .when(has_icon, |this| this.pl(px(19.)).pr_2()) - .child(label.clone()), - ), - ) + PopupMenuItem::Item { + icon, + label, + action, + .. + } => { + let action = action.as_ref().map(|action| action.boxed_clone()); + let key = Self::render_keybinding(action, cx); + + this.relative() + .py_1p5() + .px_2() + .rounded_md() + .text_sm() + .line_height(rems(1.25)) + .items_center() + .child( + h_flex() + .items_center() + .gap_x_1p5() + .when(has_icon, |this| { + this.child( + h_flex() + .w_3p5() + .h_3p5() + .items_center() + .justify_center() + .text_sm() + .map(|this| { + if let Some(icon) = icon { + this.child(icon.clone().small().clone()) + } else { + this.children(icon_placeholder.clone()) + } + }), + ) + }) + .child( + h_flex() + .flex_1() + .items_center() + .justify_between() + .child(label.clone()) + .children(key), + ), + ) } } })) diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 5f7636a8..26d1d257 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -162,7 +162,6 @@ struct Colors { pub list_active: Hsla, pub list_head: Hsla, pub link: Hsla, - pub menu: Hsla, } impl Colors { @@ -204,7 +203,6 @@ impl Colors { list_active: hsl(240.0, 7., 88.0).opacity(0.75), list_head: hsl(0.0, 0.0, 100.), link: hsl(221.0, 83.0, 53.0), - menu: hsl(0.0, 0.0, 97.0), } } @@ -246,7 +244,6 @@ impl Colors { list_active: hsl(240.0, 3.7, 15.0), list_head: hsl(0.0, 0.0, 6.0), link: hsl(221.0, 83.0, 53.0), - menu: hsl(300.0, 2.0, 12.), } } } @@ -312,7 +309,6 @@ pub struct Theme { pub link: Hsla, pub link_hover: Hsla, pub link_active: Hsla, - pub menu: Hsla, pub skeleton: Hsla, } @@ -391,7 +387,6 @@ impl From for Theme { link: colors.link, link_hover: colors.link.lighten(0.2), link_active: colors.link.darken(0.2), - menu: colors.menu, skeleton: hsla(colors.primary.h, colors.primary.s, colors.primary.l, 0.1), } }