gpui_component/menu/
dropdown_menu.rs1use std::rc::Rc;
2
3use gpui::{
4 Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement,
5 RenderOnce, SharedString, StyleRefinement, Styled, Window,
6};
7
8use crate::{button::Button, menu::PopupMenu, popover::Popover, Selectable};
9
10pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
12 fn dropdown_menu(
14 self,
15 f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
16 ) -> DropdownMenuPopover<Self> {
17 self.dropdown_menu_with_anchor(Corner::TopLeft, f)
18 }
19
20 fn dropdown_menu_with_anchor(
22 mut self,
23 anchor: impl Into<Corner>,
24 f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
25 ) -> DropdownMenuPopover<Self> {
26 let style = self.style().clone();
27 let id = self.interactivity().element_id.clone();
28
29 DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
30 }
31}
32
33impl DropdownMenu for Button {}
34
35#[derive(IntoElement)]
36pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
37 id: ElementId,
38 style: StyleRefinement,
39 anchor: Corner,
40 trigger: T,
41 builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
42}
43
44impl<T> DropdownMenuPopover<T>
45where
46 T: Selectable + IntoElement + 'static,
47{
48 fn new(
49 id: ElementId,
50 anchor: impl Into<Corner>,
51 trigger: T,
52 builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
53 ) -> Self {
54 Self {
55 id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
56 style: StyleRefinement::default(),
57 anchor: anchor.into(),
58 trigger,
59 builder: Rc::new(builder),
60 }
61 }
62
63 pub fn anchor(mut self, anchor: impl Into<Corner>) -> Self {
65 self.anchor = anchor.into();
66 self
67 }
68
69 fn trigger_style(mut self, style: StyleRefinement) -> Self {
71 self.style = style;
72 self
73 }
74}
75
76#[derive(Default)]
77struct DropdownMenuState {
78 menu: Option<Entity<PopupMenu>>,
79}
80
81impl<T> RenderOnce for DropdownMenuPopover<T>
82where
83 T: Selectable + IntoElement + 'static,
84{
85 fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
86 let builder = self.builder.clone();
87 let menu_state =
88 window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
89
90 Popover::new(SharedString::from(format!("popover:{}", self.id)))
91 .appearance(false)
92 .overlay_closable(false)
93 .trigger(self.trigger)
94 .trigger_style(self.style)
95 .anchor(self.anchor)
96 .content(move |_, window, cx| {
97 let menu = match menu_state.read(cx).menu.clone() {
104 Some(menu) => menu,
105 None => {
106 let builder = builder.clone();
107 let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
108 builder(menu, window, cx)
109 });
110 menu_state.update(cx, |state, _| {
111 state.menu = Some(menu.clone());
112 });
113 menu.focus_handle(cx).focus(window);
114
115 let popover_state = cx.entity();
117 window
118 .subscribe(&menu, cx, {
119 let menu_state = menu_state.clone();
120 move |_, _: &DismissEvent, window, cx| {
121 popover_state.update(cx, |state, cx| {
122 state.dismiss(window, cx);
123 });
124 menu_state.update(cx, |state, _| {
125 state.menu = None;
126 });
127 }
128 })
129 .detach();
130
131 menu.clone()
132 }
133 };
134
135 menu.clone()
136 })
137 }
138}