1use crate::{
19 input, popover,
20 search::{Leading, SearchList},
21 widgets::Controls,
22};
23use gpui::{
24 App, Context, EventEmitter, FocusHandle, Focusable, KeyBinding, Pixels, SharedString, Window,
25 actions, canvas, div, prelude::*, px,
26};
27use std::rc::Rc;
28use theme::Theme;
29
30actions!(
31 bezel_combobox,
32 [SelectNext, SelectPrevious, Confirm, Dismiss]
33);
34
35pub const KEY_CONTEXT: &str = "Combobox";
38
39pub fn init(cx: &mut App) {
42 cx.bind_keys(bindings());
43}
44
45pub fn bindings() -> Vec<KeyBinding> {
49 let mut bindings = Vec::new();
50 let ctx = Some(KEY_CONTEXT);
51 bindings.extend([
52 KeyBinding::new("down", SelectNext, ctx),
53 KeyBinding::new("up", SelectPrevious, ctx),
54 KeyBinding::new("enter", Confirm, ctx),
55 KeyBinding::new("escape", Dismiss, ctx),
56 KeyBinding::new("ctrl-n", SelectNext, ctx),
57 KeyBinding::new("ctrl-p", SelectPrevious, ctx),
58 ]);
59
60 bindings
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
66pub enum ComboboxEvent {
67 Selected(usize),
68}
69
70pub struct Combobox {
75 search: SearchList,
76 menu: popover::Popup<()>,
77 chosen: Option<usize>,
78 placeholder: SharedString,
79 leading: Option<Rc<Leading>>,
80 trigger_width: Option<Pixels>,
84 focus_handle: FocusHandle,
85}
86
87impl EventEmitter<ComboboxEvent> for Combobox {}
88
89impl Combobox {
90 pub fn new(
91 items: Vec<SharedString>,
92 placeholder: impl Into<SharedString>,
93 cx: &mut Context<Self>,
94 ) -> Self {
95 Self {
96 search: SearchList::new(items, "Search…", |view: &mut Self| &mut view.search, cx),
97 menu: popover::Popup::default(),
98 chosen: None,
99 placeholder: placeholder.into(),
100 leading: None,
101 trigger_width: None,
102 focus_handle: cx.focus_handle().tab_stop(true),
105 }
106 }
107
108 pub fn with_selection(mut self, item: usize) -> Self {
110 self.chosen = (item < self.search.filter.items().len()).then_some(item);
111 self
112 }
113
114 pub fn with_leading(
117 mut self,
118 leading: impl Fn(usize, &Theme) -> gpui::AnyElement + 'static,
119 ) -> Self {
120 self.leading = Some(Rc::new(leading));
121 self
122 }
123
124 pub fn selection(&self) -> Option<usize> {
125 self.chosen
126 }
127
128 fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
129 self.search.clear(cx);
130 if let Some(chosen) = self.chosen {
131 self.search.filter.set_active(chosen);
132 }
133 self.menu.open(());
134 window.focus(&self.search.query.focus_handle(cx), cx);
135 cx.notify();
136 }
137
138 fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
139 if self.menu.take_press_was_open() {
140 self.close(window, cx);
141 } else {
142 self.open(window, cx);
143 }
144 }
145
146 fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
147 if self.search.query.focus_handle(cx).is_focused(window) {
150 window.focus(&self.focus_handle, cx);
151 }
152 popover::close_popup(self, cx, |view| &mut view.menu);
153 }
154
155 fn choose(&mut self, item: usize, window: &mut Window, cx: &mut Context<Self>) {
156 if !self.menu.is_open() {
157 return;
158 }
159 self.chosen = Some(item);
160 cx.emit(ComboboxEvent::Selected(item));
161 self.close(window, cx);
162 }
163
164 fn step(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
165 if self.menu.is_open() {
166 self.search.filter.step(delta);
167 cx.notify();
168 } else if !self.menu.is_closing() {
169 self.open(window, cx);
170 }
171 }
172
173 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
174 self.step(1, window, cx);
175 }
176
177 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
178 self.step(-1, window, cx);
179 }
180
181 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
182 if self.menu.is_open() {
183 if let Some(item) = self.search.filter.active_item() {
184 self.choose(item, window, cx);
185 }
186 } else if !self.menu.is_closing() {
187 self.open(window, cx);
188 }
189 }
190
191 fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
192 if self.menu.is_open() {
193 self.close(window, cx);
194 } else {
195 cx.propagate();
196 }
197 }
198
199 fn menu_card(&self, theme: &Theme, cx: &mut Context<Self>) -> gpui::AnyElement {
200 popover::popover_card(theme)
201 .w(self.trigger_width.unwrap_or(px(200.0)))
202 .on_mouse_down_out(cx.listener(|view, _, window, cx| view.close(window, cx)))
203 .child(self.search.body(
204 theme,
205 self.chosen,
206 self.leading.as_deref(),
207 |view| &mut view.search,
208 Self::choose,
209 cx,
210 ))
211 .into_any_element()
212 }
213}
214
215impl Focusable for Combobox {
216 fn focus_handle(&self, _: &App) -> FocusHandle {
218 self.focus_handle.clone()
219 }
220}
221
222impl Render for Combobox {
223 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
224 let theme = Theme::of(cx).clone();
225 let open = self.menu.is_open() || self.menu.is_closing();
226 let label = match self.chosen {
227 Some(item) => self.search.filter.items()[item].clone(),
228 None => self.placeholder.clone(),
229 };
230 let card = open.then(|| self.menu_card(&theme, cx));
231 let combobox = cx.entity().downgrade();
232
233 div()
234 .key_context(KEY_CONTEXT)
235 .track_focus(&self.focus_handle)
236 .on_action(cx.listener(Self::select_next))
237 .on_action(cx.listener(Self::select_previous))
238 .on_action(cx.listener(Self::confirm))
239 .on_action(cx.listener(Self::dismiss))
240 .relative()
241 .w_full()
242 .child(
246 canvas(
247 move |bounds, _, cx| {
248 combobox
249 .update(cx, |combobox, _| {
250 combobox.trigger_width = Some(bounds.size.width);
251 })
252 .ok();
253 },
254 |_, _, _, _| {},
255 )
256 .absolute()
257 .size_full(),
258 )
259 .child(popover::trigger_press(
260 div()
261 .id("combobox-trigger")
262 .on_click(cx.listener(|combobox, _, window, cx| combobox.toggle(window, cx)))
263 .child(theme.select_trigger(label)),
264 |combobox: &mut Self| &mut combobox.menu,
265 cx,
266 ))
267 .when_some(card, |trigger, card| {
268 trigger.child(popover::anchored_menu_below(
269 "combobox-menu",
270 card,
271 self.menu.closing_since(),
272 ))
273 })
274 }
275}
276
277pub use input::KEY_CONTEXT as FIELD_KEY_CONTEXT;