use std::rc::Rc; use gpui::{ Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled, Window, }; use crate::Selectable; use crate::avatar::Avatar; use crate::button::Button; use crate::menu::PopupMenu; use crate::popover::Popover; /// A dropdown menu trait for buttons and other interactive elements pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static { /// Create a dropdown menu with the given items, anchored to the TopLeft corner fn dropdown_menu( self, f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, ) -> DropdownMenuPopover { self.dropdown_menu_with_anchor(Corner::TopLeft, f) } /// Create a dropdown menu with the given items, anchored to the given corner fn dropdown_menu_with_anchor( mut self, anchor: impl Into, f: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, ) -> DropdownMenuPopover { let style = self.style().clone(); let id = self.interactivity().element_id.clone(); DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style) } } impl DropdownMenu for Button {} impl DropdownMenu for Avatar {} #[derive(IntoElement)] pub struct DropdownMenuPopover { id: ElementId, style: StyleRefinement, anchor: Corner, trigger: T, #[allow(clippy::type_complexity)] builder: Rc) -> PopupMenu>, } impl DropdownMenuPopover where T: Selectable + IntoElement + 'static, { fn new( id: ElementId, anchor: impl Into, trigger: T, builder: impl Fn(PopupMenu, &mut Window, &mut Context) -> PopupMenu + 'static, ) -> Self { Self { id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(), style: StyleRefinement::default(), anchor: anchor.into(), trigger, builder: Rc::new(builder), } } /// Set the anchor corner for the dropdown menu popover. pub fn anchor(mut self, anchor: impl Into) -> Self { self.anchor = anchor.into(); self } /// Set the style refinement for the dropdown menu trigger. fn trigger_style(mut self, style: StyleRefinement) -> Self { self.style = style; self } } #[derive(Default)] struct DropdownMenuState { menu: Option>, } impl RenderOnce for DropdownMenuPopover where T: Selectable + IntoElement + 'static, { fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement { let builder = self.builder.clone(); let menu_state = window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default()); Popover::new(SharedString::from(format!("popover:{}", self.id))) .appearance(false) .overlay_closable(false) .trigger(self.trigger) .trigger_style(self.style) .anchor(self.anchor) .content(move |_, window, cx| { // Here is special logic to only create the PopupMenu once and reuse it. // Because this `content` will called in every time render, so we need to store the menu // in state to avoid recreating at every render. // // And we also need to rebuild the menu when it is dismissed, to rebuild menu items // dynamically for support `dropdown_menu` method, so we listen for DismissEvent below. let menu = match menu_state.read(cx).menu.clone() { Some(menu) => menu, None => { let builder = builder.clone(); let menu = PopupMenu::build(window, cx, move |menu, window, cx| { builder(menu, window, cx) }); menu_state.update(cx, |state, _| { state.menu = Some(menu.clone()); }); menu.focus_handle(cx).focus(window, cx); // Listen for dismiss events from the PopupMenu to close the popover. let popover_state = cx.entity(); window .subscribe(&menu, cx, { let menu_state = menu_state.clone(); move |_, _: &DismissEvent, window, cx| { popover_state.update(cx, |state, cx| { state.dismiss(window, cx); }); menu_state.update(cx, |state, _| { state.menu = None; }); } }) .detach(); menu.clone() } }; menu.clone() }) } }