1use crate::adapt::AdaptEvents;
9use crate::{menu::MenuEntry, Column, Label, Mark};
10use kas::event::{Command, FocusSource, ScrollDelta};
11use kas::prelude::*;
12use kas::theme::{MarkStyle, TextClass};
13use kas::Popup;
14use std::fmt::Debug;
15use std::ops::{Deref, DerefMut};
16
17#[derive(Clone, Debug)]
18struct IndexMsg(usize);
19
20impl_scope! {
21 #[widget {
32 layout = button! 'frame(row! [self.label, self.mark]);
33 navigable = true;
34 hover_highlight = true;
35 }]
36 pub struct ComboBox<A, V: Clone + Debug + Eq + 'static> {
37 core: widget_core!(),
38 #[widget(&())]
39 label: Label<String>,
40 #[widget(&())]
41 mark: Mark,
42 #[widget(&())]
43 popup: Popup<AdaptEvents<Column<Vec<MenuEntry<V>>>>>,
44 active: usize,
45 opening: bool,
46 state_fn: Box<dyn Fn(&ConfigCx, &A) -> V>,
47 on_select: Option<Box<dyn Fn(&mut EventCx, V)>>,
48 }
49
50 impl Layout for Self {
51 fn nav_next(&self, _: bool, _: Option<usize>) -> Option<usize> {
52 None
54 }
55 }
56
57 impl Events for Self {
58 type Data = A;
59
60 fn update(&mut self, cx: &mut ConfigCx, data: &A) {
61 let msg = (self.state_fn)(cx, data);
62 if let Some(index) = self.popup
63 .iter()
64 .enumerate()
65 .find_map(|(i, w)| (*w == msg).then_some(i))
66 {
67 if index != self.active {
68 self.active = index;
69 cx.redraw(&self);
70 }
71 } else {
72 log::warn!("ComboBox::update: unknown entry {msg:?}");
73 };
74 }
75
76 fn handle_event(&mut self, cx: &mut EventCx, _: &A, event: Event) -> IsUsed {
77 let open_popup = |s: &mut Self, cx: &mut EventCx, source: FocusSource| {
78 if s.popup.open(cx, &(), s.id()) {
79 if let Some(w) = s.popup.deref().deref().get_child(s.active) {
80 cx.next_nav_focus(w.id(), false, source);
81 }
82 }
83 };
84
85 match event {
86 Event::Command(cmd, code) => {
87 if self.popup.is_open() {
88 let next = |cx: &mut EventCx, clr, rev| {
89 if clr {
90 cx.clear_nav_focus();
91 }
92 cx.next_nav_focus(None, rev, FocusSource::Key);
93 };
94 match cmd {
95 cmd if cmd.is_activate() => {
96 self.popup.close(cx);
97 cx.depress_with_key(self.id(), code);
98 }
99 Command::Up => next(cx, false, true),
100 Command::Down => next(cx, false, false),
101 Command::Home => next(cx, true, false),
102 Command::End => next(cx, true, true),
103 _ => return Unused,
104 }
105 } else {
106 let last = self.len().saturating_sub(1);
107 let action = match cmd {
108 cmd if cmd.is_activate() => {
109 open_popup(self, cx, FocusSource::Key);
110 cx.depress_with_key(self.id(), code);
111 Action::empty()
112 }
113 Command::Up => self.set_active(self.active.saturating_sub(1)),
114 Command::Down => self.set_active((self.active + 1).min(last)),
115 Command::Home => self.set_active(0),
116 Command::End => self.set_active(last),
117 _ => return Unused,
118 };
119 cx.action(self, action);
120 }
121 Used
122 }
123 Event::Scroll(ScrollDelta::LineDelta(_, y)) if !self.popup.is_open() => {
124 if y > 0.0 {
125 let action = self.set_active(self.active.saturating_sub(1));
126 cx.action(&self, action);
127 } else if y < 0.0 {
128 let last = self.len().saturating_sub(1);
129 let action = self.set_active((self.active + 1).min(last));
130 cx.action(&self, action);
131 }
132 Used
133 }
134 Event::PressStart { press } => {
135 if press.id.as_ref().map(|id| self.is_ancestor_of(id)).unwrap_or(false) {
136 if press.is_primary() {
137 press.grab(self.id()).with_cx(cx);
138 cx.set_grab_depress(*press, press.id);
139 self.opening = !self.popup.is_open();
140 }
141 Used
142 } else {
143 Unused
144 }
145 }
146 Event::CursorMove { press } | Event::PressMove { press, .. } => {
147 open_popup(self, cx, FocusSource::Pointer);
148 let cond = self.popup.rect().contains(press.coord);
149 let target = if cond { press.id } else { None };
150 cx.set_grab_depress(press.source, target.clone());
151 if let Some(id) = target {
152 cx.set_nav_focus(id, FocusSource::Pointer);
153 }
154 Used
155 }
156 Event::PressEnd { press, success } if success => {
157 if let Some(id) = press.id {
158 if self.eq_id(&id) {
159 if self.opening {
160 open_popup(self, cx, FocusSource::Pointer);
161 return Used;
162 }
163 } else if self.popup.is_open() && self.popup.is_ancestor_of(&id) {
164 cx.send_command(id, Command::Activate);
165 return Used;
166 }
167 }
168 self.popup.close(cx);
169 Used
170 }
171 _ => Unused,
172 }
173 }
174
175 fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
176 if let Some(IndexMsg(index)) = cx.try_pop() {
177 let action = self.set_active(index);
178 cx.action(&self, action);
179 self.popup.close(cx);
180 if let Some(ref f) = self.on_select {
181 if let Some(msg) = cx.try_pop() {
182 (f)(cx, msg);
183 }
184 }
185 }
186 }
187 }
188}
189
190impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
191 pub fn new<T, I>(iter: I, state_fn: impl Fn(&ConfigCx, &A) -> V + 'static) -> Self
208 where
209 T: Into<AccessString>,
210 I: IntoIterator<Item = (T, V)>,
211 {
212 let entries = iter
213 .into_iter()
214 .map(|(label, msg)| MenuEntry::new_msg(label, msg))
215 .collect();
216 Self::new_vec(entries, state_fn)
217 }
218
219 pub fn new_vec(
225 entries: Vec<MenuEntry<V>>,
226 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
227 ) -> Self {
228 let label = entries.first().map(|entry| entry.get_string());
229 let label = Label::new(label.unwrap_or_default()).with_class(TextClass::Button);
230 ComboBox {
231 core: Default::default(),
232 label,
233 mark: Mark::new(MarkStyle::Point(Direction::Down)),
234 popup: Popup::new(
235 AdaptEvents::new(Column::new(entries)).on_messages(|cx, _, _| {
236 if let Some(_) = cx.try_observe::<V>() {
237 if let Some(index) = cx.last_child() {
238 cx.push(IndexMsg(index));
239 }
240 }
241 }),
242 Direction::Down,
243 ),
244 active: 0,
245 opening: false,
246 state_fn: Box::new(state_fn),
247 on_select: None,
248 }
249 }
250
251 #[must_use]
253 pub fn with_msg<M: Debug + 'static>(self, f: impl Fn(V) -> M + 'static) -> Self {
254 self.with(move |cx, m| cx.push(f(m)))
255 }
256
257 #[must_use]
262 pub fn with<F>(mut self, f: F) -> ComboBox<A, V>
263 where
264 F: Fn(&mut EventCx, V) + 'static,
265 {
266 self.on_select = Some(Box::new(f));
267 self
268 }
269
270 pub fn new_msg<T, I, M>(
274 iter: I,
275 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
276 msg_fn: impl Fn(V) -> M + 'static,
277 ) -> Self
278 where
279 T: Into<AccessString>,
280 I: IntoIterator<Item = (T, V)>,
281 M: Debug + 'static,
282 {
283 Self::new(iter, state_fn).with_msg(msg_fn)
284 }
285}
286
287impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
288 #[inline]
293 pub fn active(&self) -> usize {
294 self.active
295 }
296
297 #[inline]
299 pub fn with_active(mut self, index: usize) -> Self {
300 let _ = self.set_active(index);
301 self
302 }
303
304 pub fn set_active(&mut self, index: usize) -> Action {
306 if self.active != index && index < self.popup.len() {
307 self.active = index;
308 let string = if index < self.len() {
309 self.popup[index].get_string()
310 } else {
311 "".to_string()
312 };
313 self.label.set_string(string)
314 } else {
315 Action::empty()
316 }
317 }
318
319 #[inline]
321 pub fn len(&self) -> usize {
322 self.popup.len()
323 }
324
325 #[inline]
327 pub fn is_empty(&self) -> bool {
328 self.popup.is_empty()
329 }
330
331 pub fn clear(&mut self) {
333 self.popup.clear()
334 }
335
336 pub fn push<T: Into<AccessString>>(&mut self, cx: &mut ConfigCx, label: T, msg: V) -> usize {
343 let column = self.popup.deref_mut().deref_mut();
344 column.push(cx, &(), MenuEntry::new_msg(label, msg))
345 }
346
347 pub fn pop(&mut self, cx: &mut EventState) -> Option<()> {
349 self.popup.pop(cx).map(|_| ())
350 }
351
352 pub fn insert<T: Into<AccessString>>(
356 &mut self,
357 cx: &mut ConfigCx,
358 index: usize,
359 label: T,
360 msg: V,
361 ) {
362 let column = self.popup.deref_mut().deref_mut();
363 column.insert(cx, &(), index, MenuEntry::new_msg(label, msg));
364 }
365
366 pub fn remove(&mut self, cx: &mut EventState, index: usize) {
370 self.popup.remove(cx, index);
371 }
372
373 pub fn replace<T: Into<AccessString>>(
377 &mut self,
378 cx: &mut ConfigCx,
379 index: usize,
380 label: T,
381 msg: V,
382 ) {
383 self.popup
384 .replace(cx, &(), index, MenuEntry::new_msg(label, msg));
385 }
386}