1use std::rc::Rc;
2
3use gpui_kit::{
4 AnyElement, App, ElementId, Entity, FocusHandle, InteractiveElement as _, IntoElement, Length,
5 MouseButton, ParentElement as _, RenderOnce, StyleRefinement, Styled, Window,
6 base::{
7 Align, ElementExt as _, GlobalState, Selectable, StyledExt as _, TestSupportExt as _,
8 actions::{Confirm, SelectDown},
9 },
10 div,
11 prelude::FluentBuilder as _,
12};
13
14use super::{MenuAnchor, MenuEntry, MenuState, TRIGGER_CONTEXT, panel::MenuPanels};
15
16type TriggerBuilder = Box<dyn FnOnce(bool) -> AnyElement>;
18pub(crate) type EntriesBuilder = Rc<dyn Fn(&mut Window, &mut App) -> Vec<MenuEntry>>;
19type Opener = Rc<dyn Fn(bool, &mut Window, &mut App)>;
21
22#[derive(IntoElement)]
51pub struct DropdownMenu {
52 id: ElementId,
53 state: Entity<MenuState>,
54 trigger: Option<TriggerBuilder>,
55 items: Option<EntriesBuilder>,
56 align: Align,
57 action_context: Option<FocusHandle>,
58 menu_width: Option<Length>,
59 style: StyleRefinement,
60}
61
62impl DropdownMenu {
63 pub fn new(id: impl Into<ElementId>, state: &Entity<MenuState>) -> Self {
65 Self {
66 id: id.into(),
67 state: state.clone(),
68 trigger: None,
69 items: None,
70 align: Align::End,
71 action_context: None,
72 menu_width: None,
73 style: StyleRefinement::default(),
74 }
75 }
76
77 pub fn trigger(mut self, trigger: impl Selectable + IntoElement + 'static) -> Self {
81 self.trigger = Some(Box::new(move |open| trigger.open(open).into_any_element()));
82 self
83 }
84
85 pub fn items(
87 mut self,
88 items: impl Fn(&mut Window, &mut App) -> Vec<MenuEntry> + 'static,
89 ) -> Self {
90 self.items = Some(Rc::new(items));
91 self
92 }
93
94 pub fn align(mut self, align: Align) -> Self {
97 self.align = align;
98 self
99 }
100
101 pub fn action_context(mut self, focus_handle: &FocusHandle) -> Self {
106 self.action_context = Some(focus_handle.clone());
107 self
108 }
109
110 pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
113 self.menu_width = Some(width.into());
114 self
115 }
116}
117
118impl Styled for DropdownMenu {
119 fn style(&mut self) -> &mut StyleRefinement {
120 &mut self.style
121 }
122}
123
124impl RenderOnce for DropdownMenu {
125 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
126 let state = self.state;
127 let (open, trigger_bounds) = {
128 let state = state.read(cx);
129 (state.is_open(), state.trigger_bounds().clone())
130 };
131 let open_menu: Opener = {
132 let state = state.clone();
133 let items = self.items;
134 let align = self.align;
135 let context = self.action_context;
136 let trigger_bounds = trigger_bounds.clone();
137 Rc::new(move |keyboard, window, cx| {
138 let entries = items
139 .as_ref()
140 .map(|items| items(window, cx))
141 .unwrap_or_default();
142 let anchor = MenuAnchor::Trigger(trigger_bounds.get(), align);
143 state.update(cx, |state, cx| {
144 match &context {
145 Some(context) => state.open_for(entries, anchor, context, window, cx),
146 None => state.open(entries, anchor, window, cx),
147 }
148 if keyboard {
149 state.highlight_edge(false, cx);
150 }
151 });
152 })
153 };
154 div()
155 .id(self.id.clone())
156 .test_support()
157 .key_context(TRIGGER_CONTEXT)
158 .flex_shrink_0()
159 .refine_style(&self.style)
160 .relative()
161 .on_prepaint(move |bounds, _, _| trigger_bounds.set(bounds))
162 .on_mouse_down(MouseButton::Left, {
163 let state = state.clone();
164 let open_menu = open_menu.clone();
165 move |_, window, cx| {
166 window.prevent_default();
167 cx.stop_propagation();
168 GlobalState::suppress_text_selection(cx);
169 if state.read(cx).is_open() != open {
173 return;
174 }
175 if open {
176 state.update(cx, |state, cx| state.close(window, cx));
177 } else {
178 open_menu(false, window, cx);
179 }
180 }
181 })
182 .when(!open, |this| {
183 let confirm = open_menu.clone();
184 this.on_action(move |_: &Confirm, window, cx| confirm(true, window, cx))
185 .on_action(move |_: &SelectDown, window, cx| open_menu(true, window, cx))
186 })
187 .children(self.trigger.map(|trigger| trigger(open)))
188 .child(MenuPanels::new(self.id, &state).width(self.menu_width))
189 }
190}