1use std::marker::PhantomData;
2
3use crate::prelude::*;
4
5#[derive(Lens)]
7pub struct ComboBox<
8 L1: Lens<Target = Vec<T>>,
9 L2: Lens<Target = usize>,
10 T: 'static + Data + ToString,
11> {
12 filter_text: String,
14 placeholder: String,
16 on_select: Option<Box<dyn Fn(&mut EventContext, usize)>>,
18 list_lens: L1,
20 selected: L2,
22 is_open: bool,
24
25 p: PhantomData<T>,
26}
27
28pub(crate) enum ComboBoxEvent {
29 SetOption(usize),
30 SetFilterText(String),
31}
32
33impl<L1, L2, T> ComboBox<L1, L2, T>
34where
35 L1: Copy + Lens<Target = Vec<T>>,
36 T: 'static + Data + ToString,
37 L2: Copy + Lens<Target = usize>,
38{
39 pub fn new(cx: &mut Context, list_lens: L1, selected: L2) -> Handle<Self> {
41 Self {
42 filter_text: String::from(""),
43 on_select: None,
44 list_lens,
45 selected,
46 p: PhantomData,
47 is_open: false,
48 placeholder: String::from("One"),
49 }
50 .build(cx, |cx| {
51 cx.add_listener(move |popup: &mut Self, cx, event| {
53 let flag: bool = popup.is_open;
54 event.map(|window_event, meta| match window_event {
55 WindowEvent::MouseDown(_) => {
56 if flag && meta.origin != cx.current() {
57 if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
60 cx.emit(TextEvent::Submit(false));
61 cx.emit_custom(
62 Event::new(TextEvent::EndEdit)
63 .target(cx.current)
64 .propagate(Propagation::Subtree),
65 );
66 meta.consume();
67 }
68 }
69 }
70
71 _ => {}
72 });
73 });
74
75 Textbox::new(cx, Self::filter_text)
76 .on_edit(|cx, txt| cx.emit(ComboBoxEvent::SetFilterText(txt)))
77 .on_blur(|_| {})
79 .on_cancel(|_| {})
81 .width(Stretch(1.0))
82 .height(Pixels(32.0))
83 .placeholder(Self::placeholder)
84 .class("title");
85
86 Binding::new(cx, Self::is_open, move |cx, is_open| {
87 if is_open.get(cx) {
88 Popup::new(cx, move |cx: &mut Context| {
89 Binding::new(cx, Self::filter_text, move |cx, filter_text| {
91 let f = filter_text.get(cx);
92 List::new_filtered(
93 cx,
94 list_lens,
95 move |item| {
96 if f.is_empty() {
97 true
98 } else {
99 item.to_string()
100 .to_ascii_lowercase()
101 .contains(&f.to_ascii_lowercase())
102 }
103 },
104 |cx, _, item| {
105 Label::new(cx, item);
106 },
107 )
108 .selectable(Selectable::Single)
109 .selected(selected.map(|s| vec![*s]))
110 .on_select(|cx, index| {
111 cx.emit(ComboBoxEvent::SetOption(index));
112 cx.emit(PopupEvent::Close);
113 });
114 });
115 })
116 .should_reposition(false)
117 .arrow_size(Pixels(4.0));
118 }
119 });
120 })
121 .bind(selected, move |handle, selected| {
122 let selected_item = list_lens.idx(selected.get(&handle)).get(&handle);
123 handle.modify(|combobox| combobox.placeholder = selected_item.to_string());
124 })
125 }
126}
127
128impl<L1, L2, T> View for ComboBox<L1, L2, T>
129where
130 L1: Lens<Target = Vec<T>>,
131 T: 'static + Data + ToString,
132 L2: Lens<Target = usize>,
133{
134 fn element(&self) -> Option<&'static str> {
135 Some("combobox")
136 }
137
138 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
139 event.map(|combobox_event, _| match combobox_event {
140 ComboBoxEvent::SetOption(index) => {
141 let selected_item = self.list_lens.idx(*index).get(cx);
143 self.placeholder = selected_item.to_string();
144
145 if let Some(callback) = &self.on_select {
147 (callback)(cx, *index);
148 }
149
150 self.is_open = false;
152
153 self.filter_text = String::new();
155
156 cx.emit_custom(
159 Event::new(TextEvent::EndEdit)
160 .target(cx.current)
161 .propagate(Propagation::Subtree),
162 );
163 }
164
165 ComboBoxEvent::SetFilterText(text) => {
166 self.placeholder.clone_from(text);
167 self.filter_text.clone_from(text);
168
169 self.is_open = true;
171 }
172 });
173
174 event.map(|textbox_event, _| match textbox_event {
175 TextEvent::StartEdit => {
177 self.is_open = true;
178 }
179
180 TextEvent::Submit(enter) => {
181 let selected = self.selected.get(cx);
182 if *enter {
183 } else {
186 cx.emit(ComboBoxEvent::SetOption(selected));
188 }
189 }
190
191 _ => {}
192 });
193
194 event.map(|window_event, meta| match window_event {
195 WindowEvent::KeyDown(code, _) => match code {
196 Code::ArrowDown => {
197 if meta.origin != cx.current() {
199 cx.emit_custom(
200 Event::new(window_event.clone())
201 .origin(cx.current())
202 .target(Entity::root())
203 .propagate(Propagation::Subtree),
204 );
205 }
206 }
207
208 Code::ArrowUp => {
209 if meta.origin != cx.current() {
211 cx.emit_custom(
212 Event::new(window_event.clone())
213 .origin(cx.current())
214 .target(Entity::root())
215 .propagate(Propagation::Subtree),
216 );
217 }
218 }
219
220 Code::Enter => {
221 if meta.origin != cx.current() {
222 cx.emit_custom(
223 Event::new(window_event.clone())
224 .origin(cx.current())
225 .target(Entity::root())
226 .propagate(Propagation::Subtree),
227 );
228 }
229 }
230
231 Code::Escape => {
232 if self.is_open {
233 self.is_open = false;
234 } else {
235 cx.emit(TextEvent::Submit(false));
236 }
237 }
238
239 _ => {}
240 },
241
242 _ => {}
243 });
244 }
245}
246
247impl<L1, L2, T> Handle<'_, ComboBox<L1, L2, T>>
248where
249 L1: Lens<Target = Vec<T>>,
250 T: 'static + Data + ToString,
251 L2: Lens<Target = usize>,
252{
253 pub fn on_select<F>(self, callback: F) -> Self
255 where
256 F: 'static + Fn(&mut EventContext, usize),
257 {
258 self.modify(|combobox: &mut ComboBox<L1, L2, T>| {
259 combobox.on_select = Some(Box::new(callback))
260 })
261 }
262}