1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, ElementId, FocusHandle, IntoElement, KeyBinding, ParentElement, RenderOnce,
5 StyleRefinement, Styled, Window, prelude::FluentBuilder as _,
6};
7
8use crate::{
9 Select,
10 actions::{Cancel, Confirm, SelectDown, SelectUp},
11 styled::StyledExt as _,
12};
13
14const CONTEXT: &str = "Combobox";
15
16pub(crate) fn init(cx: &mut App) {
17 cx.bind_keys([
18 KeyBinding::new("up", SelectUp, Some(CONTEXT)),
19 KeyBinding::new("down", SelectDown, Some(CONTEXT)),
20 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
21 KeyBinding::new(
22 "secondary-enter",
23 Confirm { secondary: true },
24 Some(CONTEXT),
25 ),
26 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
27 ]);
28}
29
30type OpenChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
31type ActionHandler = Rc<dyn Fn(&mut Window, &mut App)>;
32
33#[derive(IntoElement)]
38pub struct Combobox {
39 id: ElementId,
40 open: bool,
41 disabled: bool,
42 focus_handle: Option<FocusHandle>,
43 content_focus_handle: Option<FocusHandle>,
44 style: StyleRefinement,
45 children: Vec<AnyElement>,
46 on_open_change: Option<OpenChangeHandler>,
47 on_confirm: Option<ActionHandler>,
48 on_dismiss: Option<ActionHandler>,
49}
50
51impl Combobox {
52 pub fn new(id: impl Into<ElementId>) -> Self {
53 Self {
54 id: id.into(),
55 open: false,
56 disabled: false,
57 focus_handle: None,
58 content_focus_handle: None,
59 style: StyleRefinement::default(),
60 children: Vec::new(),
61 on_open_change: None,
62 on_confirm: None,
63 on_dismiss: None,
64 }
65 }
66
67 pub fn open(mut self, open: bool) -> Self {
68 self.open = open;
69 self
70 }
71
72 pub fn disabled(mut self, disabled: bool) -> Self {
73 self.disabled = disabled;
74 self
75 }
76
77 pub fn focus_handle(mut self, focus_handle: &FocusHandle) -> Self {
78 self.focus_handle = Some(focus_handle.clone());
79 self
80 }
81
82 pub fn content_focus_handle(mut self, focus_handle: &FocusHandle) -> Self {
83 self.content_focus_handle = Some(focus_handle.clone());
84 self
85 }
86
87 pub fn on_open_change(
88 mut self,
89 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
90 ) -> Self {
91 self.on_open_change = Some(Rc::new(handler));
92 self
93 }
94
95 pub fn on_confirm(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
97 self.on_confirm = Some(Rc::new(handler));
98 self
99 }
100
101 pub fn on_dismiss(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
107 self.on_dismiss = Some(Rc::new(handler));
108 self
109 }
110}
111
112impl Styled for Combobox {
113 fn style(&mut self) -> &mut StyleRefinement {
114 &mut self.style
115 }
116}
117
118impl ParentElement for Combobox {
119 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
120 self.children.extend(elements);
121 }
122}
123
124impl RenderOnce for Combobox {
125 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
126 Select::new(self.id)
127 .open(self.open)
128 .disabled(self.disabled)
129 .key_context(CONTEXT)
130 .when_some(self.focus_handle, |this, handle| this.focus_handle(&handle))
131 .when_some(self.content_focus_handle, |this, handle| {
132 this.content_focus_handle(&handle)
133 })
134 .when_some(self.on_open_change, |this, handler| {
135 this.on_open_change(move |open, window, cx| handler(open, window, cx))
136 })
137 .when_some(self.on_confirm, |this, handler| {
138 this.on_confirm(move |window, cx| handler(window, cx))
139 })
140 .when_some(self.on_dismiss, |this, handler| {
141 this.on_dismiss(move |window, cx| handler(window, cx))
142 })
143 .children(self.children)
144 .refine_style(&self.style)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use gpui::{
152 Context, Focusable, InteractiveElement as _, Render, TestAppContext, VisualTestContext,
153 div, px,
154 };
155 use std::sync::{Arc, Mutex};
156
157 struct Harness {
158 open: bool,
159 trigger_focus: FocusHandle,
160 content_focus: FocusHandle,
161 events: Arc<Mutex<Vec<&'static str>>>,
162 }
163
164 impl Harness {
165 fn new(cx: &mut Context<Self>) -> Self {
166 Self {
167 open: true,
168 trigger_focus: cx.focus_handle(),
169 content_focus: cx.focus_handle(),
170 events: Arc::new(Mutex::new(Vec::new())),
171 }
172 }
173 }
174
175 impl Focusable for Harness {
176 fn focus_handle(&self, _: &App) -> FocusHandle {
177 self.trigger_focus.clone()
178 }
179 }
180
181 impl Render for Harness {
182 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
183 let state = cx.entity();
184 let confirm_events = self.events.clone();
185 let dismiss_events = self.events.clone();
186 let open_events = self.events.clone();
187
188 Combobox::new("combobox")
189 .open(self.open)
190 .focus_handle(&self.trigger_focus)
191 .content_focus_handle(&self.content_focus)
192 .on_confirm(move |_, _| confirm_events.lock().unwrap().push("confirm"))
193 .on_dismiss(move |_, _| dismiss_events.lock().unwrap().push("dismiss"))
194 .on_open_change(move |open, _, cx| {
195 open_events.lock().unwrap().push("close");
196 state.update(cx, |state, cx| {
197 state.open = open;
198 cx.notify();
199 });
200 })
201 .child(div().track_focus(&self.content_focus).size(px(20.)))
202 }
203 }
204
205 fn harness(cx: &mut TestAppContext) -> (&mut VisualTestContext, gpui::Entity<Harness>) {
206 cx.update(crate::init);
207 let (state, cx) = cx.add_window_view(|_, cx| Harness::new(cx));
208 cx.update(|window, cx| {
209 let content_focus = state.read(cx).content_focus.clone();
210 content_focus.focus(window, cx);
211 window.draw(cx).clear(cx);
212 });
213 (cx, state)
214 }
215
216 #[gpui::test]
217 fn escape_dismisses_then_closes_and_restores_trigger_focus(cx: &mut TestAppContext) {
218 let (cx, state) = harness(cx);
219 cx.simulate_keystrokes("escape");
220
221 cx.update(|window, cx| {
222 assert!(!state.read(cx).open);
223 assert!(state.read(cx).trigger_focus.is_focused(window));
224 assert_eq!(
227 state.read(cx).events.lock().unwrap().as_slice(),
228 &["dismiss", "close"]
229 );
230 });
231 }
232
233 #[gpui::test]
234 fn enter_confirms_without_dismissing_while_open(cx: &mut TestAppContext) {
235 let (cx, state) = harness(cx);
236 cx.simulate_keystrokes("enter");
237
238 cx.update(|_, cx| {
239 assert!(state.read(cx).open);
240 assert_eq!(
241 state.read(cx).events.lock().unwrap().as_slice(),
242 &["confirm"]
243 );
244 });
245 }
246}