From 5f1b68a431c1a395408b12d0bda74e00ec116092 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 10 Oct 2024 20:22:25 +0800 Subject: [PATCH] menu: Add `menu_with_element` to add a custom render item. (#330) image --- crates/story/src/popup_story.rs | 13 +++++ crates/ui/src/popup_menu.rs | 95 +++++++++++++++++++++++---------- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index 23871455..c4b9a662 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -14,6 +14,7 @@ use ui::{ popover::{Popover, PopoverContent}, popup_menu::PopupMenuExt, switch::Switch, + theme::ActiveTheme as _, v_flex, ContextModal, IconName, Sizable, }; @@ -274,6 +275,18 @@ impl Render for PopupStory { Box::new(ToggleWindowMode), ) .separator() + .menu_with_element( + |cx| { + v_flex().gap_1().child("Custom Element").child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child("THis is sub-title"), + ) + }, + Box::new(Info(0)), + ) + .separator() .submenu("Links", cx, |menu, _| { menu.link_with_icon( "GitHub Repository", diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 8af36a53..75ae08ed 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -8,8 +8,8 @@ use gpui::{ SharedString, Styled as _, View, ViewContext, VisualContext as _, WindowContext, }; use gpui::{ - anchored, canvas, rems, AnchorCorner, Bounds, Edges, FocusableView, Keystroke, ScrollHandle, - StatefulInteractiveElement, WeakView, + anchored, canvas, rems, AnchorCorner, AnyElement, Bounds, Edges, FocusableView, Keystroke, + ScrollHandle, StatefulInteractiveElement, WeakView, }; use crate::scroll::{Scrollbar, ScrollbarState}; @@ -53,6 +53,10 @@ enum PopupMenuItem { action: Option>, handler: Rc, }, + ElementItem { + render: Box AnyElement + 'static>, + handler: Rc, + }, Submenu { icon: Option, label: SharedString, @@ -214,6 +218,45 @@ impl PopupMenu { self } + /// Add Menu Item with custom element render. + pub fn menu_with_element(mut self, builder: F, action: Box) -> Self + where + F: Fn(&mut WindowContext) -> E + 'static, + E: IntoElement, + { + self.menu_items.push(PopupMenuItem::ElementItem { + render: Box::new(move |cx| builder(cx).into_any_element()), + handler: self.wrap_handler(action), + }); + self + } + + fn wrap_handler(&self, action: Box) -> Rc { + let action_focus_handle = self.action_focus_handle.clone(); + + Rc::new(move |cx| { + cx.activate_window(); + + // Focus back to the user expected focus handle + // Then the actions listened on that focus handle can be received + // + // For example: + // + // TabPanel + // |- PopupMenu + // |- PanelContent (actions are listened here) + // + // The `PopupMenu` and `PanelContent` are at the same level in the TabPanel + // If the actions are listened on the `PanelContent`, + // it can't receive the actions from the `PopupMenu`, unless we focus on `PanelContent`. + if let Some(handle) = action_focus_handle.as_ref() { + cx.focus(&handle); + } + + cx.dispatch_action(action.boxed_clone()); + }) + } + fn add_menu_item( &mut self, label: impl Into, @@ -224,33 +267,11 @@ impl PopupMenu { self.has_icon = true; } - let action_focus_handle = self.action_focus_handle.clone(); - self.menu_items.push(PopupMenuItem::Item { icon, label: label.into(), action: Some(action.boxed_clone()), - handler: Rc::new(move |cx| { - cx.activate_window(); - - // Focus back to the user expected focus handle - // Then the actions listened on that focus handle can be received - // - // For example: - // - // TabPanel - // |- PopupMenu - // |- PanelContent (actions are listened here) - // - // The `PopupMenu` and `PanelContent` are at the same level in the TabPanel - // If the actions are listened on the `PanelContent`, - // it can't receive the actions from the `PopupMenu`, unless we focus on `PanelContent`. - if let Some(handle) = action_focus_handle.as_ref() { - cx.focus(&handle); - } - - cx.dispatch_action(action.boxed_clone()); - }), + handler: self.wrap_handler(action), }); self } @@ -336,6 +357,10 @@ impl PopupMenu { handler(cx); self.dismiss(&Dismiss, cx) } + Some(PopupMenuItem::ElementItem { handler, .. }) => { + handler(cx); + self.dismiss(&Dismiss, cx) + } _ => {} } } @@ -422,7 +447,7 @@ impl PopupMenu { .text_sm() .map(|this| { if let Some(icon) = icon { - this.child(icon.clone().small().clone()) + this.child(icon.clone().small()) } else { this.children(icon_placeholder.clone()) } @@ -451,6 +476,8 @@ impl Render for PopupMenu { let window_haft_height = cx.window_bounds().get_bounds().size.height * 0.5; let max_height = window_haft_height.min(px(450.)); + const ITEM_HEIGHT: Pixels = px(26.); + v_flex() .id("popup-menu") .key_context("PopupMenu") @@ -521,6 +548,20 @@ impl Render for PopupMenu { .bg(cx.theme().muted), ) } + PopupMenuItem::ElementItem { render, .. } => this + .on_click(cx.listener(move |this, _, cx| { + this.on_click(ix, cx) + })) + .child( + h_flex() + .min_h(ITEM_HEIGHT) + .items_center() + .gap_x_1p5() + .children(Self::render_icon( + has_icon, None, cx, + )) + .child((render)(cx)), + ), PopupMenuItem::Item { icon, label, @@ -537,7 +578,7 @@ impl Render for PopupMenu { })) .child( h_flex() - .h(px(26.)) + .h(ITEM_HEIGHT) .items_center() .gap_x_1p5() .children(Self::render_icon(