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
74 impl Events for Self {
75 const REDRAW_ON_MOUSE_OVER: bool = true;
76
77 type Data = A;
78
79 fn probe(&self, _: Coord) -> Id {
80 self.id()
81 }
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 if cmd.is_activate() {
123 self.open_popup(cx, FocusSource::Key);
124 cx.depress_with_key(&self, code);
125 } else {
126 let index = match cmd {
127 Command::Up => self.active.saturating_sub(1),
128 Command::Down => (self.active + 1).min(last),
129 Command::Home => 0,
130 Command::End => last,
131 _ => return Unused,
132 };
133 self.set_active(cx, index);
134 }
135 }
136 Used
137 }
138 Event::Scroll(delta) if !self.popup.is_open() => {
139 if let Some(y) = delta.as_wheel_action(cx) {
140 let index = if y > 0 {
141 self.active.saturating_sub(y as usize)
142 } else {
143 self.active
144 .saturating_add((-y) as usize)
145 .min(self.len().saturating_sub(1))
146 };
147 self.set_active(cx, index);
148 Used
149 } else {
150 Unused
151 }
152 }
153 Event::PressStart(press) => {
154 if press
155 .id
156 .as_ref()
157 .map(|id| self.is_ancestor_of(id))
158 .unwrap_or(false)
159 {
160 if press.is_primary() {
161 press.grab_move(self.id()).complete(cx);
162 cx.set_grab_depress(*press, press.id);
163 self.opening = !self.popup.is_open();
164 }
165 Used
166 } else {
167 Unused
168 }
169 }
170 Event::PointerMove { press } | Event::PressMove { press, .. } => {
171 self.open_popup(cx, FocusSource::Pointer);
172 let cond = self.popup.rect().contains(press.coord);
173 let target = if cond { press.id } else { None };
174 cx.set_grab_depress(press.source, target.clone());
175 if let Some(id) = target {
176 cx.request_nav_focus(id, FocusSource::Pointer);
177 }
178 Used
179 }
180 Event::PressEnd { press, success } if success => {
181 if let Some(id) = press.id {
182 if self.eq_id(&id) {
183 if self.opening {
184 self.open_popup(cx, FocusSource::Pointer);
185 return Used;
186 }
187 } else if self.popup.is_open() && self.popup.is_ancestor_of(&id) {
188 cx.send(id, Command::Activate);
189 return Used;
190 }
191 }
192 self.popup.close(cx);
193 Used
194 }
195 _ => Unused,
196 }
197 }
198
199 fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
200 if let Some(SetIndex(index)) = cx.try_pop() {
201 self.set_active(cx, index);
202 self.popup.close(cx);
203 if let Some(ref f) = self.on_select {
204 if let Some(msg) = cx.try_pop() {
205 (f)(cx, msg);
206 }
207 }
208 } else if let Some(Expand) = cx.try_pop() {
209 self.open_popup(cx, FocusSource::Synthetic);
210 } else if let Some(Collapse) = cx.try_pop() {
211 self.popup.close(cx);
212 }
213 }
214 }
215
216 impl Self {
217 fn open_popup(&mut self, cx: &mut EventCx, source: FocusSource) {
218 if self.popup.open(cx, &(), self.id(), true) {
219 if let Some(w) = self.popup.inner.inner.get_child(self.active) {
220 cx.next_nav_focus(w.id(), false, source);
221 }
222 }
223 }
224 }
225}
226
227impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
228 pub fn new<T, I>(iter: I, state_fn: impl Fn(&ConfigCx, &A) -> V + 'static) -> Self
245 where
246 T: Into<AccessString>,
247 I: IntoIterator<Item = (T, V)>,
248 {
249 let entries = iter
250 .into_iter()
251 .map(|(label, msg)| MenuEntry::new_msg(label, msg))
252 .collect();
253 Self::new_vec(entries, state_fn)
254 }
255
256 pub fn new_vec(
262 entries: Vec<MenuEntry<V>>,
263 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
264 ) -> Self {
265 let label = entries.first().map(|entry| entry.as_str().to_string());
266 let label = Label::new(label.unwrap_or_default()).with_class(TextClass::Label);
267 ComboBox {
268 core: Default::default(),
269 label,
270 popup: Popup::new(
271 AdaptEvents::new(Column::new(entries)).on_messages(|cx, _, _| {
272 if let Some(_) = cx.try_peek::<V>() {
273 if let Some(index) = cx.last_child() {
274 cx.push(SetIndex(index));
275 }
276 }
277 }),
278 Direction::Down,
279 ),
280 active: 0,
281 opening: false,
282 state_fn: Box::new(state_fn),
283 on_select: None,
284 }
285 }
286
287 #[must_use]
289 pub fn with_msg<M: Debug + 'static>(self, f: impl Fn(V) -> M + 'static) -> Self {
290 self.with(move |cx, m| cx.push(f(m)))
291 }
292
293 #[must_use]
298 pub fn with<F>(mut self, f: F) -> ComboBox<A, V>
299 where
300 F: Fn(&mut EventCx, V) + 'static,
301 {
302 self.on_select = Some(Box::new(f));
303 self
304 }
305
306 pub fn new_msg<T, I, M>(
310 iter: I,
311 state_fn: impl Fn(&ConfigCx, &A) -> V + 'static,
312 msg_fn: impl Fn(V) -> M + 'static,
313 ) -> Self
314 where
315 T: Into<AccessString>,
316 I: IntoIterator<Item = (T, V)>,
317 M: Debug + 'static,
318 {
319 Self::new(iter, state_fn).with_msg(msg_fn)
320 }
321}
322
323impl<A, V: Clone + Debug + Eq + 'static> ComboBox<A, V> {
324 #[inline]
329 pub fn active(&self) -> usize {
330 self.active
331 }
332
333 pub fn set_active(&mut self, cx: &mut ConfigCx, index: usize) {
335 if self.active != index && index < self.popup.inner.inner.len() {
336 self.active = index;
337 let string = if index < self.len() {
338 self.popup.inner.inner[index].as_str().to_string()
339 } else {
340 "".to_string()
341 };
342 self.label.set_string(cx, string);
343 }
344 }
345
346 #[inline]
348 pub fn len(&self) -> usize {
349 self.popup.inner.inner.len()
350 }
351
352 #[inline]
354 pub fn is_empty(&self) -> bool {
355 self.popup.inner.inner.is_empty()
356 }
357
358 pub fn clear(&mut self) {
360 self.popup.inner.inner.clear()
361 }
362
363 pub fn push<T: Into<AccessString>>(&mut self, cx: &mut ConfigCx, label: T, msg: V) -> usize {
370 let column = &mut self.popup.inner.inner;
371 column.push(cx, &(), MenuEntry::new_msg(label, msg))
372 }
373
374 pub fn pop(&mut self, cx: &mut ConfigCx) -> Option<()> {
376 self.popup.inner.inner.pop(cx).map(|_| ())
377 }
378
379 pub fn insert<T: Into<AccessString>>(
383 &mut self,
384 cx: &mut ConfigCx,
385 index: usize,
386 label: T,
387 msg: V,
388 ) {
389 let column = &mut self.popup.inner.inner;
390 column.insert(cx, &(), index, MenuEntry::new_msg(label, msg));
391 }
392
393 pub fn remove(&mut self, cx: &mut ConfigCx, index: usize) {
397 self.popup.inner.inner.remove(cx, index);
398 }
399
400 pub fn replace<T: Into<AccessString>>(
404 &mut self,
405 cx: &mut ConfigCx,
406 index: usize,
407 label: T,
408 msg: V,
409 ) {
410 self.popup
411 .inner
412 .inner
413 .replace(cx, &(), index, MenuEntry::new_msg(label, msg));
414 }
415}