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 pub(super) hover_tooltip: Option<(String, Arc<Font>)>,
61}
62
63thread_local! {
64 static COMBO_POPUP_QUEUE: RefCell<Vec<ComboPopupRequest>> = const { RefCell::new(Vec::new()) };
65 static CURRENT_COMBO_VIEWPORT: Cell<Option<Size>> = const { Cell::new(None) };
66}
67
68pub struct ComboBox {
76 bounds: Rect,
77 children: Vec<Box<dyn Widget>>, base: WidgetBase,
79
80 options: Vec<String>,
81 selected: usize,
82 open: bool,
83 hovered_item: Option<usize>,
85
86 font: Arc<Font>,
87 font_size: f64,
88
89 on_change: Option<Box<dyn FnMut(usize)>>,
90 selected_cell: Option<Rc<Cell<usize>>>,
95
96 selected_label: Label,
99 item_labels: Rc<RefCell<Vec<Label>>>,
105 item_fonts: Option<Vec<Arc<Font>>>,
111 item_tooltips: Option<Vec<String>>,
116
117 popup_opens_up: bool,
118 popup_visible_count: usize,
119 scroll_offset: usize,
120 scrollbar: ScrollbarAxis,
121 middle_dragging: bool,
122 middle_last_pos: Point,
123}
124
125impl ComboBox {
126 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
131 let font_size = 13.0;
132 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
133 let sel = selected.min(opts.len().saturating_sub(1));
134
135 let selected_label = Self::make_label(
136 opts.get(sel).map(|s| s.as_str()).unwrap_or(""),
137 font_size,
138 Arc::clone(&font),
139 );
140 let item_labels: Vec<Label> = opts
141 .iter()
142 .map(|t| Self::make_label(t, font_size, Arc::clone(&font)))
143 .collect();
144 let item_labels = Rc::new(RefCell::new(item_labels));
145
146 Self {
147 bounds: Rect::default(),
148 children: Vec::new(),
149 base: WidgetBase::new(),
150 options: opts,
151 selected: sel,
152 open: false,
153 hovered_item: None,
154 font,
155 font_size,
156 on_change: None,
157 selected_cell: None,
158 selected_label,
159 item_labels,
160 item_fonts: None,
161 item_tooltips: None,
162 popup_opens_up: false,
163 popup_visible_count: DEFAULT_VISIBLE_ITEMS,
164 scroll_offset: 0,
165 scrollbar: ScrollbarAxis {
166 enabled: true,
167 ..ScrollbarAxis::default()
168 },
169 middle_dragging: false,
170 middle_last_pos: Point::ORIGIN,
171 }
172 }
173
174 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
180 let n = self.options.len();
181 let v = cell.get();
182 if n > 0 {
183 let clamped = v.min(n - 1);
184 self.set_selected(clamped);
187 }
188 self.selected_cell = Some(cell);
189 self
190 }
191
192 fn make_label(text: &str, font_size: f64, font: Arc<Font>) -> Label {
193 Label::new(text, font).with_font_size(font_size)
194 }
195
196 pub fn with_font_size(mut self, size: f64) -> Self {
199 self.font_size = size;
200 self.selected_label = Self::make_label(
201 self.options
202 .get(self.selected)
203 .map(|s| s.as_str())
204 .unwrap_or(""),
205 size,
206 Arc::clone(&self.font),
207 );
208 let new_labels: Vec<Label> = self
209 .options
210 .iter()
211 .map(|t| Self::make_label(t, size, Arc::clone(&self.font)))
212 .collect();
213 *self.item_labels.borrow_mut() = new_labels;
214 self
215 }
216
217 pub fn with_margin(mut self, m: Insets) -> Self {
218 self.base.margin = m;
219 self
220 }
221 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
222 self.base.h_anchor = h;
223 self
224 }
225 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
226 self.base.v_anchor = v;
227 self
228 }
229 pub fn with_min_size(mut self, s: Size) -> Self {
230 self.base.min_size = s;
231 self
232 }
233 pub fn with_max_size(mut self, s: Size) -> Self {
234 self.base.max_size = s;
235 self
236 }
237
238 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
240 self.on_change = Some(Box::new(cb));
241 self
242 }
243
244 pub fn with_item_fonts(mut self, fonts: Vec<Arc<Font>>) -> Self {
257 self.set_item_fonts(fonts);
258 self
259 }
260
261 pub fn with_item_tooltips(mut self, tooltips: Vec<impl Into<String>>) -> Self {
266 self.item_tooltips = Some(tooltips.into_iter().map(|t| t.into()).collect());
267 self
268 }
269
270 pub fn set_item_fonts(&mut self, fonts: Vec<Arc<Font>>) {
272 self.item_fonts = Some(fonts.clone());
273 let size = self.font_size;
274 let new_labels: Vec<Label> = self
275 .options
276 .iter()
277 .enumerate()
278 .map(|(i, t)| {
279 let f = fonts
280 .get(i)
281 .cloned()
282 .unwrap_or_else(|| Arc::clone(&self.font));
283 Label::new(t, f)
284 .with_font_size(size)
285 .with_ignore_system_font(true)
286 })
287 .collect();
288 *self.item_labels.borrow_mut() = new_labels;
289 if let Some(sel_font) = fonts.get(self.selected).cloned() {
290 self.selected_label = Label::new(
291 self.options
292 .get(self.selected)
293 .map(|s| s.as_str())
294 .unwrap_or(""),
295 sel_font,
296 )
297 .with_font_size(size)
298 .with_ignore_system_font(true);
299 }
300 }
301
302 pub fn selected(&self) -> usize {
305 self.selected
306 }
307
308 pub fn is_open(&self) -> bool {
314 self.open
315 }
316
317 pub fn set_selected(&mut self, idx: usize) {
318 if idx < self.options.len() {
319 self.selected = idx;
320 if let Some(ref fonts) = self.item_fonts {
325 if let Some(f) = fonts.get(idx).cloned() {
326 self.selected_label = Label::new(self.options[idx].as_str(), f)
327 .with_font_size(self.font_size)
328 .with_ignore_system_font(true);
329 return;
330 }
331 }
332 self.selected_label.set_text(self.options[idx].as_str());
333 }
334 }
335}
336
337mod geometry;
338
339impl Widget for ComboBox {
340 fn type_name(&self) -> &'static str {
341 "ComboBox"
342 }
343 fn bounds(&self) -> Rect {
344 self.bounds
345 }
346 fn set_bounds(&mut self, b: Rect) {
347 self.bounds = b;
348 }
349 fn children(&self) -> &[Box<dyn Widget>] {
350 &self.children
351 }
352 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
353 &mut self.children
354 }
355
356 fn is_focusable(&self) -> bool {
357 true
358 }
359
360 fn needs_draw(&self) -> bool {
361 if !self.is_visible() {
362 return false;
363 }
364 self.scrollbar.animation_active() || self.children().iter().any(|c| c.needs_draw())
365 }
366
367 fn hit_test(&self, local_pos: Point) -> bool {
368 self.in_button(local_pos) || self.pos_in_popup(local_pos)
369 }
370
371 fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
372 self.pos_in_popup(local_pos)
373 }
374
375 fn margin(&self) -> Insets {
376 self.base.margin
377 }
378 fn widget_base(&self) -> Option<&WidgetBase> {
379 Some(&self.base)
380 }
381 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
382 Some(&mut self.base)
383 }
384 fn h_anchor(&self) -> HAnchor {
385 self.base.h_anchor
386 }
387 fn v_anchor(&self) -> VAnchor {
388 self.base.v_anchor
389 }
390 fn min_size(&self) -> Size {
391 self.base.min_size
392 }
393 fn max_size(&self) -> Size {
394 self.base.max_size
395 }
396
397 fn layout(&mut self, available: Size) -> Size {
398 if !self.open {
403 if let Some(cell) = &self.selected_cell {
404 let n = self.options.len();
405 if n > 0 {
406 let v = cell.get().min(n - 1);
407 if v != self.selected {
408 self.set_selected(v);
411 }
412 }
413 }
414 }
415
416 self.bounds = Rect::new(0.0, 0.0, available.width, CLOSED_H);
417 let inner_w = (available.width - PAD_X * 2.0 - ARROW_W).max(0.0);
418
419 let sl = self.selected_label.layout(Size::new(inner_w, CLOSED_H));
421 let sl_y = (CLOSED_H - sl.height) * 0.5;
422 self.selected_label
423 .set_bounds(Rect::new(PAD_X, sl_y, sl.width, sl.height));
424
425 let mut labels = self.item_labels.borrow_mut();
428 for i in 0..labels.len() {
429 let s = labels[i].layout(Size::new(inner_w, ITEM_H));
430 let ir = self.item_rect(i);
431 let ly = ir.y + (ITEM_H - s.height) * 0.5;
432 labels[i].set_bounds(Rect::new(PAD_X, ly, s.width, s.height));
433 }
434 drop(labels);
435
436 Size::new(available.width, CLOSED_H)
437 }
438
439 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
440 let v = ctx.visuals();
441 let w = self.bounds.width;
442
443 ctx.set_fill_color(v.widget_bg);
445 ctx.begin_path();
446 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
447 ctx.fill();
448
449 ctx.set_stroke_color(v.widget_stroke);
450 ctx.set_line_width(1.0);
451 ctx.begin_path();
452 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
453 ctx.stroke();
454
455 let arrow_x = w - ARROW_W * 0.5;
457 let arrow_cy = CLOSED_H * 0.5;
458 let arrow_sz = 4.0;
459 ctx.set_fill_color(v.text_dim);
460 ctx.begin_path();
461 ctx.move_to(arrow_x - arrow_sz, arrow_cy + arrow_sz * 0.5);
463 ctx.line_to(arrow_x + arrow_sz, arrow_cy + arrow_sz * 0.5);
464 ctx.line_to(arrow_x, arrow_cy - arrow_sz * 0.5);
465 ctx.close_path();
466 ctx.fill();
467
468 self.selected_label.set_color(v.text_color);
470 let sl_bounds = self.selected_label.bounds();
471
472 ctx.save();
473 ctx.translate(sl_bounds.x, sl_bounds.y);
474 paint_subtree(&mut self.selected_label, ctx);
475 ctx.restore();
476 }
477
478 fn paint_overlay(&mut self, _ctx: &mut dyn DrawCtx) {}
479
480 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
481 if self.open {
482 let mut x = 0.0;
483 let mut y = 0.0;
484 let t = ctx.root_transform();
485 t.transform(&mut x, &mut y);
486 let scale = crate::device_scale::device_scale().max(1e-6);
494 let x = x / scale;
495 let y = y / scale;
496 let viewport_h = crate::widgets::combo_box::current_combo_viewport()
497 .map(|s| s.height)
498 .unwrap_or(f64::MAX / 4.0);
499 self.configure_popup_geometry(y, viewport_h);
500 let style = self.popup_scroll_style();
501 let visibility = current_scroll_visibility();
502 let viewport = self.popup_scroll_viewport();
503 let geom = self.scrollbar_geometry(style);
504 let scrollbar = self
505 .scrollbar
506 .prepare_paint(viewport, style, visibility, geom)
507 .map(|bar| bar.translated(x, y));
508 submit_combo_popup(ComboPopupRequest {
509 x,
510 y,
511 width: self.bounds.width,
512 popup_h: self.popup_h(),
513 opens_up: self.popup_opens_up,
514 first_item: self.scroll_offset,
515 visible_count: self.popup_visible_count,
516 selected: self.selected,
517 hovered_item: self.hovered_item,
518 scrollbar,
519 item_count: self.options.len(),
520 item_labels: Rc::clone(&self.item_labels),
521 hover_tooltip: self.hovered_item.and_then(|idx| {
522 let text = self.item_tooltips.as_ref()?.get(idx)?;
523 if text.is_empty() {
524 return None;
525 }
526 Some((text.clone(), Arc::clone(&self.font)))
527 }),
528 });
529 }
530 }
531
532 fn on_event(&mut self, event: &Event) -> EventResult {
533 match event {
534 Event::MouseDown {
535 button: MouseButton::Middle,
536 pos,
537 ..
538 } => {
539 if self.pos_in_popup(*pos) {
540 self.middle_dragging = true;
541 self.middle_last_pos = *pos;
542 self.hovered_item = None;
543 crate::animation::request_draw();
544 return EventResult::Consumed;
545 }
546 EventResult::Ignored
547 }
548 Event::MouseDown {
549 button: MouseButton::Left,
550 pos,
551 ..
552 } => {
553 if self.in_button(*pos) {
554 self.open = !self.open;
555 self.hovered_item = None;
556 self.scrollbar.hovered_bar = false;
557 self.scrollbar.hovered_thumb = false;
558 self.scrollbar.dragging = false;
559 self.middle_dragging = false;
560 if self.open {
561 self.ensure_selected_visible();
562 }
563 crate::animation::request_draw();
564 return EventResult::Consumed;
565 }
566 if self.open {
567 if self.pos_in_scrollbar(*pos) {
568 let style = self.popup_scroll_style();
569 let viewport = self.popup_scroll_viewport();
570 let geom = self.scrollbar_geometry(style);
571 self.sync_scrollbar_from_rows();
572 if self.scrollbar.begin_drag(*pos, viewport, style, geom) {
573 } else if self.scrollbar.page_at(*pos, viewport, style, geom) {
575 self.sync_rows_from_scrollbar();
576 }
577 self.hovered_item = None;
578 self.scrollbar.hovered_thumb = self.pos_on_scroll_thumb(*pos);
579 crate::animation::request_draw();
580 return EventResult::Consumed;
581 }
582 if let Some(i) = self.item_for_pos(*pos) {
583 self.set_selected(i);
593 self.open = false;
594 self.hovered_item = None;
595 self.scrollbar.hovered_bar = false;
596 self.scrollbar.hovered_thumb = false;
597 self.scrollbar.dragging = false;
598 self.middle_dragging = false;
599 self.fire();
600 crate::animation::request_draw();
601 return EventResult::Consumed;
602 }
603 self.open = false;
605 self.hovered_item = None;
606 self.scrollbar.hovered_bar = false;
607 self.scrollbar.hovered_thumb = false;
608 self.scrollbar.dragging = false;
609 self.middle_dragging = false;
610 crate::animation::request_draw();
611 return EventResult::Consumed;
612 }
613 EventResult::Ignored
614 }
615 Event::MouseMove { pos } => {
616 if self.middle_dragging {
617 let dy = pos.y - self.middle_last_pos.y;
618 self.middle_last_pos = *pos;
619 self.sync_scrollbar_from_rows();
620 if self.scrollbar.scroll_by(dy, self.popup_scroll_viewport()) {
621 self.sync_rows_from_scrollbar();
622 self.hovered_item = None;
623 crate::animation::request_draw();
624 }
625 return EventResult::Consumed;
626 }
627 if self.scrollbar.dragging {
628 let style = self.popup_scroll_style();
629 let viewport = self.popup_scroll_viewport();
630 let geom = self.scrollbar_geometry(style);
631 if self.scrollbar.drag_to(*pos, viewport, style, geom) {
632 self.sync_rows_from_scrollbar();
633 self.hovered_item = None;
634 crate::animation::request_draw();
635 }
636 return EventResult::Consumed;
637 }
638 let hovered_item = self.item_for_pos(*pos);
639 let style = self.popup_scroll_style();
640 let viewport = self.popup_scroll_viewport();
641 let geom = self.scrollbar_geometry(style);
642 let scroll_hover_changed = self.scrollbar.update_hover(*pos, viewport, style, geom);
643 if hovered_item != self.hovered_item || scroll_hover_changed {
644 self.hovered_item = hovered_item;
645 crate::animation::request_draw();
646 }
647 EventResult::Ignored
648 }
649 Event::MouseWheel { delta_y, .. } => {
650 if self.open && self.options.len() > self.popup_visible_count {
651 self.sync_scrollbar_from_rows();
652 if self
656 .scrollbar
657 .scroll_by(-delta_y * 40.0, self.popup_scroll_viewport())
658 {
659 self.sync_rows_from_scrollbar();
660 self.hovered_item = None;
661 crate::animation::request_draw();
662 }
663 EventResult::Consumed
664 } else {
665 EventResult::Ignored
666 }
667 }
668 Event::KeyDown { key, .. } => {
669 let n = self.options.len();
670 match key {
671 Key::Enter | Key::Char(' ') => {
672 self.open = !self.open;
673 self.scrollbar.hovered_bar = false;
674 self.scrollbar.hovered_thumb = false;
675 self.scrollbar.dragging = false;
676 self.middle_dragging = false;
677 if self.open {
678 self.ensure_selected_visible();
679 }
680 crate::animation::request_draw();
681 EventResult::Consumed
682 }
683 Key::Escape => {
684 if self.open {
685 self.open = false;
686 self.scrollbar.hovered_bar = false;
687 self.scrollbar.hovered_thumb = false;
688 self.scrollbar.dragging = false;
689 self.middle_dragging = false;
690 crate::animation::request_draw();
691 EventResult::Consumed
692 } else {
693 EventResult::Ignored
694 }
695 }
696 Key::ArrowDown => {
697 if self.selected + 1 < n {
698 self.set_selected(self.selected + 1);
699 self.ensure_selected_visible();
700 self.fire();
701 crate::animation::request_draw();
702 }
703 EventResult::Consumed
704 }
705 Key::ArrowUp => {
706 if self.selected > 0 {
707 self.set_selected(self.selected - 1);
708 self.ensure_selected_visible();
709 self.fire();
710 crate::animation::request_draw();
711 }
712 EventResult::Consumed
713 }
714 _ => EventResult::Ignored,
715 }
716 }
717 Event::FocusLost => {
718 let was_open = self.open;
719 self.open = false;
720 self.hovered_item = None;
721 self.scrollbar.hovered_bar = false;
722 self.scrollbar.hovered_thumb = false;
723 self.scrollbar.dragging = false;
724 self.middle_dragging = false;
725 if was_open {
726 crate::animation::request_draw();
727 }
728 EventResult::Ignored
729 }
730 Event::MouseUp { button, .. } => {
731 if *button == MouseButton::Left && self.scrollbar.dragging {
732 self.scrollbar.dragging = false;
733 crate::animation::request_draw();
734 EventResult::Consumed
735 } else if *button == MouseButton::Middle && self.middle_dragging {
736 self.middle_dragging = false;
737 crate::animation::request_draw();
738 EventResult::Consumed
739 } else {
740 EventResult::Ignored
741 }
742 }
743 _ => EventResult::Ignored,
744 }
745 }
746
747 fn properties(&self) -> Vec<(&'static str, String)> {
748 vec![
749 ("selected", self.selected.to_string()),
750 ("open", self.open.to_string()),
751 ("options", self.options.len().to_string()),
752 ("popup_opens_up", self.popup_opens_up.to_string()),
753 ("popup_visible_count", self.popup_visible_count.to_string()),
754 ("scroll_offset", self.scroll_offset.to_string()),
755 ]
756 }
757}
758
759fn submit_combo_popup_internal(request: ComboPopupRequest) {
760 COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().push(request));
761}
762
763mod popup_paint;
764pub(crate) use popup_paint::{begin_combo_popup_frame, paint_global_combo_popups};
765pub(super) use popup_paint::{current_combo_viewport, submit_combo_popup};