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 {
71 search: SearchList,
72 menu: popover::Popup<()>,
73 chosen: Option<usize>,
74 placeholder: SharedString,
75 leading: Option<Rc<Leading>>,
76 trigger_width: Option<Pixels>,
80 focus_handle: FocusHandle,
81}
82
83impl EventEmitter<ComboboxEvent> for Combobox {}
84
85impl Combobox {
86 pub fn new(
87 items: Vec<SharedString>,
88 placeholder: impl Into<SharedString>,
89 cx: &mut Context<Self>,
90 ) -> Self {
91 Self {
92 search: SearchList::new(items, "Search…", |view: &mut Self| &mut view.search, cx),
93 menu: popover::Popup::default(),
94 chosen: None,
95 placeholder: placeholder.into(),
96 leading: None,
97 trigger_width: None,
98 focus_handle: cx.focus_handle().tab_stop(true),
101 }
102 }
103
104 pub fn with_selection(mut self, item: usize) -> Self {
106 self.chosen = (item < self.search.filter.items().len()).then_some(item);
107 self
108 }
109
110 pub fn with_leading(
113 mut self,
114 leading: impl Fn(usize, &Theme) -> gpui::AnyElement + 'static,
115 ) -> Self {
116 self.leading = Some(Rc::new(leading));
117 self
118 }
119
120 pub fn selection(&self) -> Option<usize> {
121 self.chosen
122 }
123
124 fn open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
125 self.search.clear(cx);
126 if let Some(chosen) = self.chosen {
127 self.search.filter.set_active(chosen);
128 }
129 self.menu.open(());
130 window.focus(&self.search.query.focus_handle(cx), cx);
131 cx.notify();
132 }
133
134 fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
135 if self.menu.take_press_was_open() {
136 self.close(window, cx);
137 } else {
138 self.open(window, cx);
139 }
140 }
141
142 fn close(&mut self, window: &mut Window, cx: &mut Context<Self>) {
143 if self.search.query.focus_handle(cx).is_focused(window) {
146 window.focus(&self.focus_handle, cx);
147 }
148 popover::close_popup(self, cx, |view| &mut view.menu);
149 }
150
151 fn choose(&mut self, item: usize, window: &mut Window, cx: &mut Context<Self>) {
152 if !self.menu.is_open() {
153 return;
154 }
155 self.chosen = Some(item);
156 cx.emit(ComboboxEvent::Selected(item));
157 self.close(window, cx);
158 }
159
160 fn step(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
161 if self.menu.is_open() {
162 self.search.filter.step(delta);
163 cx.notify();
164 } else if !self.menu.is_closing() {
165 self.open(window, cx);
166 }
167 }
168
169 fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
170 self.step(1, window, cx);
171 }
172
173 fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context<Self>) {
174 self.step(-1, window, cx);
175 }
176
177 fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
178 if self.menu.is_open() {
179 if let Some(item) = self.search.filter.active_item() {
180 self.choose(item, window, cx);
181 }
182 } else if !self.menu.is_closing() {
183 self.open(window, cx);
184 }
185 }
186
187 fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context<Self>) {
188 if self.menu.is_open() {
189 self.close(window, cx);
190 } else {
191 cx.propagate();
192 }
193 }
194
195 fn menu_card(&self, theme: &Theme, cx: &mut Context<Self>) -> gpui::AnyElement {
196 popover::popover_card(theme)
197 .w(self.trigger_width.unwrap_or(px(200.0)))
198 .on_mouse_down_out(cx.listener(|view, _, window, cx| view.close(window, cx)))
199 .child(self.search.body(
200 theme,
201 self.chosen,
202 self.leading.as_deref(),
203 |view| &mut view.search,
204 Self::choose,
205 cx,
206 ))
207 .into_any_element()
208 }
209}
210
211impl Focusable for Combobox {
212 fn focus_handle(&self, _: &App) -> FocusHandle {
214 self.focus_handle.clone()
215 }
216}
217
218impl Render for Combobox {
219 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
220 let theme = Theme::of(cx).clone();
221 let open = self.menu.is_open() || self.menu.is_closing();
222 let label = match self.chosen {
223 Some(item) => self.search.filter.items()[item].clone(),
224 None => self.placeholder.clone(),
225 };
226 let card = open.then(|| self.menu_card(&theme, cx));
227 let combobox = cx.entity().downgrade();
228
229 div()
230 .key_context(KEY_CONTEXT)
231 .track_focus(&self.focus_handle)
232 .on_action(cx.listener(Self::select_next))
233 .on_action(cx.listener(Self::select_previous))
234 .on_action(cx.listener(Self::confirm))
235 .on_action(cx.listener(Self::dismiss))
236 .relative()
237 .w_full()
238 .child(
242 canvas(
243 move |bounds, _, cx| {
244 combobox
245 .update(cx, |combobox, _| {
246 combobox.trigger_width = Some(bounds.size.width);
247 })
248 .ok();
249 },
250 |_, _, _, _| {},
251 )
252 .absolute()
253 .size_full(),
254 )
255 .child(popover::trigger_press(
256 div()
257 .id("combobox-trigger")
258 .on_click(cx.listener(|combobox, _, window, cx| combobox.toggle(window, cx)))
259 .child(theme.select_trigger(label)),
260 |combobox: &mut Self| &mut combobox.menu,
261 cx,
262 ))
263 .when_some(card, |trigger, card| {
264 trigger.child(popover::anchored_menu_below(
265 "combobox-menu",
266 card,
267 self.menu.closing_since(),
268 ))
269 })
270 }
271}
272
273pub use input::KEY_CONTEXT as FIELD_KEY_CONTEXT;