1use crate::adapt::AdaptEvents;
9use crate::{Column, Label, Mark, menu::MenuEntry};
10use kas::event::FocusSource;
11use kas::messages::{Collapse, Expand, SetIndex};
12use kas::prelude::*;
13use kas::theme::FrameStyle;
14use kas::theme::{MarkStyle, TextClass};
15use kas::window::Popup;
16use std::fmt::Debug;
17
18#[impl_self]
19mod ComboBox {
20 #[widget]
38 #[layout(
39 frame!(row! [self.label, Mark::new(MarkStyle::Chevron(Direction::Down), "Expand")])
40 .with_style(FrameStyle::Button)
41 .align(AlignHints::CENTER)
42 )]
43 pub struct ComboBox<A, V: Clone + Debug + Eq + 'static> {
44 core: widget_core!(),
45 #[widget(&())]
46 label: Label<String>,
47 #[widget(&())]
48 popup: Popup<AdaptEvents<Column<Vec<MenuEntry<V>>>>>,
49 active: usize,
50 opening: bool,
51 state_fn: Box<dyn Fn(&ConfigCx, &A) -> V>,
52 on_select: Option<Box<dyn Fn(&mut EventCx, V)>>,
53 }
54
55 impl Tile for Self {
56 fn navigable(&self) -> bool {
57 true
58 }
59
60 fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
61 Role::ComboBox {
62 active: self.active,
63 text: self.label.as_str(),
64 expanded: self.popup.is_open(),
65 }
66 }
67
68 fn nav_next(&self, _: bool, _: Option<usize>) -> Option<usize> {
69 None
71 }
72
73 fn probe(&self, _: Coord) -> Id {
74 self.id()
75 }
76 }
77
78 impl Events for Self {
79 const REDRAW_ON_MOUSE_OVER: bool = true;
80
81 type Data = A;
82
83 fn update(&mut self, cx: &mut ConfigCx, data: &A) {
84 let msg = (self.state_fn)(cx, data);
85 let opt_index = self
86 .popup
87 .inner
88 .inner
89 .iter()
90 .enumerate()
91 .find_map(|(i, w)| (*w == msg).then_some(i));
92 if let Some(index) = opt_index {
93 self.set_active(cx, index);
94 } else {
95 log::warn!("ComboBox::update: unknown entry {msg:?}");
96 };
97 }
98
99 fn handle_event(&mut self, cx: &mut EventCx, _: &A, event: Event) -> IsUsed {
100 match event {
101 Event::Command(cmd, code) => {
102 if self.popup.is_open() {
103 let next = |cx: &mut EventCx, clr, rev| {
104 if clr {
105 cx.clear_nav_focus();
106 }
107 cx.next_nav_focus(None, rev, FocusSource::Key);
108 };
109 match cmd {
110 cmd if cmd.is_activate() => {
111 self.popup.close(cx);
112 cx.depress_with_key(&self, code);
113 }
114 Command::Up => next(cx, false, true),
115 Command::Down => next(cx, false, false),
116 Command::Home => next(cx, true, false),
117 Command::End => next(cx, true, true),
118 _ => return Unused,
119 }
120 } else {
121 let last = self.len().saturating_sub(1);
122 match cmd {
123 cmd if cmd.is_activate() => {
124 self.open_popup(cx, FocusSource::Key);
125 cx.depress_with_key(&self, code);
126 }
127 Command::Up => self.set_active(cx, self.active.saturating_sub(1)),
128 Command::Down => self.set_active(cx, (self.active + 1).min(last)),
129 Command::Home => self.set_active(cx, 0),
130 Command::End => self.set_active(cx, last),
131 _ => return Unused,
132 };
133 }
134 Used
135 }
136 Event::Scroll(delta) if !self.popup.is_open() => {
137 if let Some(y) = delta.as_wheel_action(cx) {
138 let index = if y > 0 {
139 self.active.saturating_sub(y as usize)
140 } else {
141 self.active
142 .saturating_add((-y) as usize)
143 .min(self.len().saturating_sub(1))
144 };
145 self.set_active(cx, index);
146 Used
147 } else {
148 Unused
149 }
150 }
151 Event::PressStart(press) => {
152 if press
153 .id
154 .as_ref()
155 .map(|id| self.is_ancestor_of(id))
156 .unwrap_or(false)
157 {
158 if press.is_primary() {
159 press.grab_move(self.id()).complete(cx);
160 cx.set_grab_depress(*press, press.id);
161 self.opening = !self.popup.is_open();
162 }
163 Used
164 } else {
165 Unused
166 }
167 }
168 Event::CursorMove { press } | Event::PressMove { press, .. } => {
169 self.open_popup(cx, FocusSource::Pointer);
170 let cond = self.popup.rect().contains(press.coord);
171 let target = if cond { press.id } else { None };
172 cx.set_grab_depress(press.source, target.clone());
173 if let Some(id) = target {
174 cx.request_nav_focus(id, FocusSource::Pointer);
175 }
176 Used
177 }
178 Event::PressEnd { press, success } if success => {
179 if let Some(id) = press.id {
180 if self.eq_id(&id) {
181 if self.opening {
182 self.open_popup(cx, FocusSource::Pointer);
183 return Used;
184 }
185 } else if self.popup.is_open() && self.popup.is_ancestor_of(&id) {
186 cx.send(id, Command::Activate);
187 return Used;
188 }
189 }
190 self.popup.close(cx);
191 Used
192 }
193 _ => Unused,
194 }
195 }
196
197 fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
198 if let Some(SetIndex(index)) = cx.try_pop() {
199 self.set_active(cx, index);
200 self.popup.close(cx);
201 if let Some(ref f) = self.on_select {
202 if let Some(msg) = cx.try_pop() {
203 (f)(cx, msg);
204 }
205 }
206 } else if let Some(Expand) = cx.try_pop() {
207 self.open_popup(cx, FocusSource::Synthetic);
208 } else if let Some(Collapse) = cx.try_pop() {
209 self.popup.close(cx);
210 }
211 }
212 }
213
214 impl Self {
215 fn open_popup(&mut self, cx: &mut EventCx, source: FocusSource) {
216 if self.popup.open(cx, &(), self.id(), true) {
217 if let Some(w) = self.popup.inner.inner.get_child(self.active) {
218 cx.next_nav_focus(w.id(), false, source);
219 }
220 }
221 }
222 }
223}
224
225impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
226 pub fn new<T, I>(iter: I, state_fn: impl Fn(&ConfigCx, &A) -> V + 'static) -> Self
243 where
244 T: Into<AccessString>,
245 I: IntoIterator<Item = (T, V)>,
246 {
247 let entries = iter
248 .into_iter()
249 .map(|(label, msg)| MenuEntry::new_msg(label, msg))
250 .collect();
251 Self::new_vec(entries, state_fn)
252 }
253
254 pub fn new_vec(
260 entries: Vec<MenuEntry<V>>,
261 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
262 ) -> Self {
263 let label = entries.first().map(|entry| entry.as_str().to_string());
264 let label = Label::new(label.unwrap_or_default()).with_class(TextClass::Button);
265 ComboBox {
266 core: Default::default(),
267 label,
268 popup: Popup::new(
269 AdaptEvents::new(Column::new(entries)).on_messages(|cx, _, _| {
270 if let Some(_) = cx.try_peek::<V>() {
271 if let Some(index) = cx.last_child() {
272 cx.push(SetIndex(index));
273 }
274 }
275 }),
276 Direction::Down,
277 ),
278 active: 0,
279 opening: false,
280 state_fn: Box::new(state_fn),
281 on_select: None,
282 }
283 }
284
285 #[must_use]
287 pub fn with_msg<M: Debug + 'static>(self, f: impl Fn(V) -> M + 'static) -> Self {
288 self.with(move |cx, m| cx.push(f(m)))
289 }
290
291 #[must_use]
296 pub fn with<F>(mut self, f: F) -> ComboBox<A, V>
297 where
298 F: Fn(&mut EventCx, V) + 'static,
299 {
300 self.on_select = Some(Box::new(f));
301 self
302 }
303
304 pub fn new_msg<T, I, M>(
308 iter: I,
309 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
310 msg_fn: impl Fn(V) -> M + 'static,
311 ) -> Self
312 where
313 T: Into<AccessString>,
314 I: IntoIterator<Item = (T, V)>,
315 M: Debug + 'static,
316 {
317 Self::new(iter, state_fn).with_msg(msg_fn)
318 }
319}
320
321impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
322 #[inline]
327 pub fn active(&self) -> usize {
328 self.active
329 }
330
331 pub fn set_active(&mut self, cx: &mut EventState, index: usize) {
333 if self.active != index && index < self.popup.inner.inner.len() {
334 self.active = index;
335 let string = if index < self.len() {
336 self.popup.inner.inner[index].as_str().to_string()
337 } else {
338 "".to_string()
339 };
340 self.label.set_string(cx, string);
341 }
342 }
343
344 #[inline]
346 pub fn len(&self) -> usize {
347 self.popup.inner.inner.len()
348 }
349
350 #[inline]
352 pub fn is_empty(&self) -> bool {
353 self.popup.inner.inner.is_empty()
354 }
355
356 pub fn clear(&mut self) {
358 self.popup.inner.inner.clear()
359 }
360
361 pub fn push<T: Into<AccessString>>(&mut self, cx: &mut ConfigCx, label: T, msg: V) -> usize {
368 let column = &mut self.popup.inner.inner;
369 column.push(cx, &(), MenuEntry::new_msg(label, msg))
370 }
371
372 pub fn pop(&mut self, cx: &mut EventState) -> Option<()> {
374 self.popup.inner.inner.pop(cx).map(|_| ())
375 }
376
377 pub fn insert<T: Into<AccessString>>(
381 &mut self,
382 cx: &mut ConfigCx,
383 index: usize,
384 label: T,
385 msg: V,
386 ) {
387 let column = &mut self.popup.inner.inner;
388 column.insert(cx, &(), index, MenuEntry::new_msg(label, msg));
389 }
390
391 pub fn remove(&mut self, cx: &mut EventState, index: usize) {
395 self.popup.inner.inner.remove(cx, index);
396 }
397
398 pub fn replace<T: Into<AccessString>>(
402 &mut self,
403 cx: &mut ConfigCx,
404 index: usize,
405 label: T,
406 msg: V,
407 ) {
408 self.popup
409 .inner
410 .inner
411 .replace(cx, &(), index, MenuEntry::new_msg(label, msg));
412 }
413}