1use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14use std::sync::Arc;
15
16use crate::draw_ctx::DrawCtx;
17use crate::event::{Event, EventResult, Key, MouseButton};
18use crate::geometry::{Point, Rect, Size};
19use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
20use crate::text::Font;
21use crate::widget::{paint_subtree, Widget};
22use crate::widgets::label::Label;
23
24use super::scroll_view::{current_scroll_style, current_scroll_visibility, ScrollBarStyle};
25use super::scrollbar::{
26 PreparedScrollbar, ScrollbarAxis, ScrollbarGeometry, ScrollbarOrientation, DEFAULT_GRAB_MARGIN,
27};
28
29const CLOSED_H: f64 = 24.0;
30const ITEM_H: f64 = 22.0;
31const PAD_X: f64 = 8.0;
32const ARROW_W: f64 = 20.0;
33const CORNER_R: f64 = 4.0;
34const POPUP_MARGIN: f64 = 4.0;
35const MIN_VISIBLE_ITEMS: usize = 3;
36const DEFAULT_VISIBLE_ITEMS: usize = 8;
37const SCROLLBAR_W: f64 = 6.0;
38
39pub(super) struct ComboPopupRequest {
40 pub(super) x: f64,
41 pub(super) y: f64,
42 pub(super) width: f64,
43 pub(super) popup_h: f64,
44 pub(super) opens_up: bool,
45 pub(super) first_item: usize,
46 pub(super) visible_count: usize,
47 pub(super) selected: usize,
48 pub(super) hovered_item: Option<usize>,
49 pub(super) scrollbar: Option<PreparedScrollbar>,
50 pub(super) item_count: usize,
51 pub(super) item_labels: Rc<RefCell<Vec<Label>>>,
57}
58
59thread_local! {
60 static COMBO_POPUP_QUEUE: RefCell<Vec<ComboPopupRequest>> = const { RefCell::new(Vec::new()) };
61 static CURRENT_COMBO_VIEWPORT: Cell<Option<Size>> = const { Cell::new(None) };
62}
63
64pub struct ComboBox {
72 bounds: Rect,
73 children: Vec<Box<dyn Widget>>, base: WidgetBase,
75
76 options: Vec<String>,
77 selected: usize,
78 open: bool,
79 hovered_item: Option<usize>,
81
82 font: Arc<Font>,
83 font_size: f64,
84
85 on_change: Option<Box<dyn FnMut(usize)>>,
86 selected_cell: Option<Rc<Cell<usize>>>,
91
92 selected_label: Label,
95 item_labels: Rc<RefCell<Vec<Label>>>,
101 item_fonts: Option<Vec<Arc<Font>>>,
107
108 popup_opens_up: bool,
109 popup_visible_count: usize,
110 scroll_offset: usize,
111 scrollbar: ScrollbarAxis,
112 middle_dragging: bool,
113 middle_last_pos: Point,
114}
115
116impl ComboBox {
117 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
122 let font_size = 13.0;
123 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
124 let sel = selected.min(opts.len().saturating_sub(1));
125
126 let selected_label = Self::make_label(
127 opts.get(sel).map(|s| s.as_str()).unwrap_or(""),
128 font_size,
129 Arc::clone(&font),
130 );
131 let item_labels: Vec<Label> = opts
132 .iter()
133 .map(|t| Self::make_label(t, font_size, Arc::clone(&font)))
134 .collect();
135 let item_labels = Rc::new(RefCell::new(item_labels));
136
137 Self {
138 bounds: Rect::default(),
139 children: Vec::new(),
140 base: WidgetBase::new(),
141 options: opts,
142 selected: sel,
143 open: false,
144 hovered_item: None,
145 font,
146 font_size,
147 on_change: None,
148 selected_cell: None,
149 selected_label,
150 item_labels,
151 item_fonts: None,
152 popup_opens_up: false,
153 popup_visible_count: DEFAULT_VISIBLE_ITEMS,
154 scroll_offset: 0,
155 scrollbar: ScrollbarAxis {
156 enabled: true,
157 ..ScrollbarAxis::default()
158 },
159 middle_dragging: false,
160 middle_last_pos: Point::ORIGIN,
161 }
162 }
163
164 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
170 let n = self.options.len();
171 let v = cell.get();
172 if n > 0 {
173 let clamped = v.min(n - 1);
174 self.set_selected(clamped);
177 }
178 self.selected_cell = Some(cell);
179 self
180 }
181
182 fn make_label(text: &str, font_size: f64, font: Arc<Font>) -> Label {
183 Label::new(text, font).with_font_size(font_size)
184 }
185
186 pub fn with_font_size(mut self, size: f64) -> Self {
189 self.font_size = size;
190 self.selected_label = Self::make_label(
191 self.options
192 .get(self.selected)
193 .map(|s| s.as_str())
194 .unwrap_or(""),
195 size,
196 Arc::clone(&self.font),
197 );
198 let new_labels: Vec<Label> = self
199 .options
200 .iter()
201 .map(|t| Self::make_label(t, size, Arc::clone(&self.font)))
202 .collect();
203 *self.item_labels.borrow_mut() = new_labels;
204 self
205 }
206
207 pub fn with_margin(mut self, m: Insets) -> Self {
208 self.base.margin = m;
209 self
210 }
211 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
212 self.base.h_anchor = h;
213 self
214 }
215 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
216 self.base.v_anchor = v;
217 self
218 }
219 pub fn with_min_size(mut self, s: Size) -> Self {
220 self.base.min_size = s;
221 self
222 }
223 pub fn with_max_size(mut self, s: Size) -> Self {
224 self.base.max_size = s;
225 self
226 }
227
228 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
230 self.on_change = Some(Box::new(cb));
231 self
232 }
233
234 pub fn with_item_fonts(mut self, fonts: Vec<Arc<Font>>) -> Self {
247 self.set_item_fonts(fonts);
248 self
249 }
250
251 pub fn set_item_fonts(&mut self, fonts: Vec<Arc<Font>>) {
253 self.item_fonts = Some(fonts.clone());
254 let size = self.font_size;
255 let new_labels: Vec<Label> = self
256 .options
257 .iter()
258 .enumerate()
259 .map(|(i, t)| {
260 let f = fonts
261 .get(i)
262 .cloned()
263 .unwrap_or_else(|| Arc::clone(&self.font));
264 Label::new(t, f)
265 .with_font_size(size)
266 .with_ignore_system_font(true)
267 })
268 .collect();
269 *self.item_labels.borrow_mut() = new_labels;
270 if let Some(sel_font) = fonts.get(self.selected).cloned() {
271 self.selected_label = Label::new(
272 self.options
273 .get(self.selected)
274 .map(|s| s.as_str())
275 .unwrap_or(""),
276 sel_font,
277 )
278 .with_font_size(size)
279 .with_ignore_system_font(true);
280 }
281 }
282
283 pub fn selected(&self) -> usize {
286 self.selected
287 }
288
289 pub fn set_selected(&mut self, idx: usize) {
290 if idx < self.options.len() {
291 self.selected = idx;
292 if let Some(ref fonts) = self.item_fonts {
297 if let Some(f) = fonts.get(idx).cloned() {
298 self.selected_label = Label::new(self.options[idx].as_str(), f)
299 .with_font_size(self.font_size)
300 .with_ignore_system_font(true);
301 return;
302 }
303 }
304 self.selected_label.set_text(self.options[idx].as_str());
305 }
306 }
307}
308
309mod geometry;
310
311impl Widget for ComboBox {
312 fn type_name(&self) -> &'static str {
313 "ComboBox"
314 }
315 fn bounds(&self) -> Rect {
316 self.bounds
317 }
318 fn set_bounds(&mut self, b: Rect) {
319 self.bounds = b;
320 }
321 fn children(&self) -> &[Box<dyn Widget>] {
322 &self.children
323 }
324 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
325 &mut self.children
326 }
327
328 fn is_focusable(&self) -> bool {
329 true
330 }
331
332 fn needs_draw(&self) -> bool {
333 self.scrollbar.animation_active()
334 }
335
336 fn hit_test(&self, local_pos: Point) -> bool {
337 self.in_button(local_pos) || self.pos_in_popup(local_pos)
338 }
339
340 fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
341 self.pos_in_popup(local_pos)
342 }
343
344 fn margin(&self) -> Insets {
345 self.base.margin
346 }
347 fn widget_base(&self) -> Option<&WidgetBase> {
348 Some(&self.base)
349 }
350 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
351 Some(&mut self.base)
352 }
353 fn h_anchor(&self) -> HAnchor {
354 self.base.h_anchor
355 }
356 fn v_anchor(&self) -> VAnchor {
357 self.base.v_anchor
358 }
359 fn min_size(&self) -> Size {
360 self.base.min_size
361 }
362 fn max_size(&self) -> Size {
363 self.base.max_size
364 }
365
366 fn layout(&mut self, available: Size) -> Size {
367 if !self.open {
372 if let Some(cell) = &self.selected_cell {
373 let n = self.options.len();
374 if n > 0 {
375 let v = cell.get().min(n - 1);
376 if v != self.selected {
377 self.set_selected(v);
380 }
381 }
382 }
383 }
384
385 self.bounds = Rect::new(0.0, 0.0, available.width, CLOSED_H);
386 let inner_w = (available.width - PAD_X * 2.0 - ARROW_W).max(0.0);
387
388 let sl = self.selected_label.layout(Size::new(inner_w, CLOSED_H));
390 let sl_y = (CLOSED_H - sl.height) * 0.5;
391 self.selected_label
392 .set_bounds(Rect::new(PAD_X, sl_y, sl.width, sl.height));
393
394 let mut labels = self.item_labels.borrow_mut();
397 for i in 0..labels.len() {
398 let s = labels[i].layout(Size::new(inner_w, ITEM_H));
399 let ir = self.item_rect(i);
400 let ly = ir.y + (ITEM_H - s.height) * 0.5;
401 labels[i].set_bounds(Rect::new(PAD_X, ly, s.width, s.height));
402 }
403 drop(labels);
404
405 Size::new(available.width, CLOSED_H)
406 }
407
408 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
409 let v = ctx.visuals();
410 let w = self.bounds.width;
411
412 ctx.set_fill_color(v.widget_bg);
414 ctx.begin_path();
415 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
416 ctx.fill();
417
418 ctx.set_stroke_color(v.widget_stroke);
419 ctx.set_line_width(1.0);
420 ctx.begin_path();
421 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
422 ctx.stroke();
423
424 let arrow_x = w - ARROW_W * 0.5;
426 let arrow_cy = CLOSED_H * 0.5;
427 let arrow_sz = 4.0;
428 ctx.set_fill_color(v.text_dim);
429 ctx.begin_path();
430 ctx.move_to(arrow_x - arrow_sz, arrow_cy + arrow_sz * 0.5);
432 ctx.line_to(arrow_x + arrow_sz, arrow_cy + arrow_sz * 0.5);
433 ctx.line_to(arrow_x, arrow_cy - arrow_sz * 0.5);
434 ctx.close_path();
435 ctx.fill();
436
437 self.selected_label.set_color(v.text_color);
439 let sl_bounds = self.selected_label.bounds();
440
441 ctx.save();
442 ctx.translate(sl_bounds.x, sl_bounds.y);
443 paint_subtree(&mut self.selected_label, ctx);
444 ctx.restore();
445 }
446
447 fn paint_overlay(&mut self, _ctx: &mut dyn DrawCtx) {}
448
449 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
450 if self.open {
451 let mut x = 0.0;
452 let mut y = 0.0;
453 let t = ctx.root_transform();
454 t.transform(&mut x, &mut y);
455 let scale = crate::device_scale::device_scale().max(1e-6);
463 let x = x / scale;
464 let y = y / scale;
465 let viewport_h = crate::widgets::combo_box::current_combo_viewport()
466 .map(|s| s.height)
467 .unwrap_or(f64::MAX / 4.0);
468 self.configure_popup_geometry(y, viewport_h);
469 let style = self.popup_scroll_style();
470 let visibility = current_scroll_visibility();
471 let viewport = self.popup_scroll_viewport();
472 let geom = self.scrollbar_geometry(style);
473 let scrollbar = self
474 .scrollbar
475 .prepare_paint(viewport, style, visibility, geom)
476 .map(|bar| bar.translated(x, y));
477 submit_combo_popup(ComboPopupRequest {
478 x,
479 y,
480 width: self.bounds.width,
481 popup_h: self.popup_h(),
482 opens_up: self.popup_opens_up,
483 first_item: self.scroll_offset,
484 visible_count: self.popup_visible_count,
485 selected: self.selected,
486 hovered_item: self.hovered_item,
487 scrollbar,
488 item_count: self.options.len(),
489 item_labels: Rc::clone(&self.item_labels),
490 });
491 }
492 }
493
494 fn on_event(&mut self, event: &Event) -> EventResult {
495 match event {
496 Event::MouseDown {
497 button: MouseButton::Middle,
498 pos,
499 ..
500 } => {
501 if self.pos_in_popup(*pos) {
502 self.middle_dragging = true;
503 self.middle_last_pos = *pos;
504 self.hovered_item = None;
505 crate::animation::request_draw();
506 return EventResult::Consumed;
507 }
508 EventResult::Ignored
509 }
510 Event::MouseDown {
511 button: MouseButton::Left,
512 pos,
513 ..
514 } => {
515 if self.in_button(*pos) {
516 self.open = !self.open;
517 self.hovered_item = None;
518 self.scrollbar.hovered_bar = false;
519 self.scrollbar.hovered_thumb = false;
520 self.scrollbar.dragging = false;
521 self.middle_dragging = false;
522 if self.open {
523 self.ensure_selected_visible();
524 }
525 crate::animation::request_draw();
526 return EventResult::Consumed;
527 }
528 if self.open {
529 if self.pos_in_scrollbar(*pos) {
530 let style = self.popup_scroll_style();
531 let viewport = self.popup_scroll_viewport();
532 let geom = self.scrollbar_geometry(style);
533 self.sync_scrollbar_from_rows();
534 if self.scrollbar.begin_drag(*pos, viewport, style, geom) {
535 } else if self.scrollbar.page_at(*pos, viewport, style, geom) {
537 self.sync_rows_from_scrollbar();
538 }
539 self.hovered_item = None;
540 self.scrollbar.hovered_thumb = self.pos_on_scroll_thumb(*pos);
541 crate::animation::request_draw();
542 return EventResult::Consumed;
543 }
544 if let Some(i) = self.item_for_pos(*pos) {
545 self.set_selected(i);
555 self.open = false;
556 self.hovered_item = None;
557 self.scrollbar.hovered_bar = false;
558 self.scrollbar.hovered_thumb = false;
559 self.scrollbar.dragging = false;
560 self.middle_dragging = false;
561 self.fire();
562 crate::animation::request_draw();
563 return EventResult::Consumed;
564 }
565 self.open = false;
567 self.hovered_item = None;
568 self.scrollbar.hovered_bar = false;
569 self.scrollbar.hovered_thumb = false;
570 self.scrollbar.dragging = false;
571 self.middle_dragging = false;
572 crate::animation::request_draw();
573 return EventResult::Consumed;
574 }
575 EventResult::Ignored
576 }
577 Event::MouseMove { pos } => {
578 if self.middle_dragging {
579 let dy = pos.y - self.middle_last_pos.y;
580 self.middle_last_pos = *pos;
581 self.sync_scrollbar_from_rows();
582 if self.scrollbar.scroll_by(dy, self.popup_scroll_viewport()) {
583 self.sync_rows_from_scrollbar();
584 self.hovered_item = None;
585 crate::animation::request_draw();
586 }
587 return EventResult::Consumed;
588 }
589 if self.scrollbar.dragging {
590 let style = self.popup_scroll_style();
591 let viewport = self.popup_scroll_viewport();
592 let geom = self.scrollbar_geometry(style);
593 if self.scrollbar.drag_to(*pos, viewport, style, geom) {
594 self.sync_rows_from_scrollbar();
595 self.hovered_item = None;
596 crate::animation::request_draw();
597 }
598 return EventResult::Consumed;
599 }
600 let hovered_item = self.item_for_pos(*pos);
601 let style = self.popup_scroll_style();
602 let viewport = self.popup_scroll_viewport();
603 let geom = self.scrollbar_geometry(style);
604 let scroll_hover_changed = self.scrollbar.update_hover(*pos, viewport, style, geom);
605 if hovered_item != self.hovered_item || scroll_hover_changed {
606 self.hovered_item = hovered_item;
607 crate::animation::request_draw();
608 }
609 EventResult::Ignored
610 }
611 Event::MouseWheel { delta_y, .. } => {
612 if self.open && self.options.len() > self.popup_visible_count {
613 self.sync_scrollbar_from_rows();
614 if self
615 .scrollbar
616 .scroll_by(delta_y * 40.0, self.popup_scroll_viewport())
617 {
618 self.sync_rows_from_scrollbar();
619 self.hovered_item = None;
620 crate::animation::request_draw();
621 }
622 EventResult::Consumed
623 } else {
624 EventResult::Ignored
625 }
626 }
627 Event::KeyDown { key, .. } => {
628 let n = self.options.len();
629 match key {
630 Key::Enter | Key::Char(' ') => {
631 self.open = !self.open;
632 self.scrollbar.hovered_bar = false;
633 self.scrollbar.hovered_thumb = false;
634 self.scrollbar.dragging = false;
635 self.middle_dragging = false;
636 if self.open {
637 self.ensure_selected_visible();
638 }
639 crate::animation::request_draw();
640 EventResult::Consumed
641 }
642 Key::Escape => {
643 if self.open {
644 self.open = false;
645 self.scrollbar.hovered_bar = false;
646 self.scrollbar.hovered_thumb = false;
647 self.scrollbar.dragging = false;
648 self.middle_dragging = false;
649 crate::animation::request_draw();
650 EventResult::Consumed
651 } else {
652 EventResult::Ignored
653 }
654 }
655 Key::ArrowDown => {
656 if self.selected + 1 < n {
657 self.set_selected(self.selected + 1);
658 self.ensure_selected_visible();
659 self.fire();
660 crate::animation::request_draw();
661 }
662 EventResult::Consumed
663 }
664 Key::ArrowUp => {
665 if self.selected > 0 {
666 self.set_selected(self.selected - 1);
667 self.ensure_selected_visible();
668 self.fire();
669 crate::animation::request_draw();
670 }
671 EventResult::Consumed
672 }
673 _ => EventResult::Ignored,
674 }
675 }
676 Event::FocusLost => {
677 let was_open = self.open;
678 self.open = false;
679 self.hovered_item = None;
680 self.scrollbar.hovered_bar = false;
681 self.scrollbar.hovered_thumb = false;
682 self.scrollbar.dragging = false;
683 self.middle_dragging = false;
684 if was_open {
685 crate::animation::request_draw();
686 }
687 EventResult::Ignored
688 }
689 Event::MouseUp { button, .. } => {
690 if *button == MouseButton::Left && self.scrollbar.dragging {
691 self.scrollbar.dragging = false;
692 crate::animation::request_draw();
693 EventResult::Consumed
694 } else if *button == MouseButton::Middle && self.middle_dragging {
695 self.middle_dragging = false;
696 crate::animation::request_draw();
697 EventResult::Consumed
698 } else {
699 EventResult::Ignored
700 }
701 }
702 _ => EventResult::Ignored,
703 }
704 }
705
706 fn properties(&self) -> Vec<(&'static str, String)> {
707 vec![
708 ("selected", self.selected.to_string()),
709 ("open", self.open.to_string()),
710 ("options", self.options.len().to_string()),
711 ("popup_opens_up", self.popup_opens_up.to_string()),
712 ("popup_visible_count", self.popup_visible_count.to_string()),
713 ("scroll_offset", self.scroll_offset.to_string()),
714 ]
715 }
716}
717
718fn submit_combo_popup_internal(request: ComboPopupRequest) {
719 COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().push(request));
720}
721
722mod popup_paint;
723pub(crate) use popup_paint::{begin_combo_popup_frame, paint_global_combo_popups};
724pub(super) use popup_paint::{current_combo_viewport, submit_combo_popup};