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 self.scrollbar.animation_active()
362 }
363
364 fn hit_test(&self, local_pos: Point) -> bool {
365 self.in_button(local_pos) || self.pos_in_popup(local_pos)
366 }
367
368 fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
369 self.pos_in_popup(local_pos)
370 }
371
372 fn margin(&self) -> Insets {
373 self.base.margin
374 }
375 fn widget_base(&self) -> Option<&WidgetBase> {
376 Some(&self.base)
377 }
378 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
379 Some(&mut self.base)
380 }
381 fn h_anchor(&self) -> HAnchor {
382 self.base.h_anchor
383 }
384 fn v_anchor(&self) -> VAnchor {
385 self.base.v_anchor
386 }
387 fn min_size(&self) -> Size {
388 self.base.min_size
389 }
390 fn max_size(&self) -> Size {
391 self.base.max_size
392 }
393
394 fn layout(&mut self, available: Size) -> Size {
395 if !self.open {
400 if let Some(cell) = &self.selected_cell {
401 let n = self.options.len();
402 if n > 0 {
403 let v = cell.get().min(n - 1);
404 if v != self.selected {
405 self.set_selected(v);
408 }
409 }
410 }
411 }
412
413 self.bounds = Rect::new(0.0, 0.0, available.width, CLOSED_H);
414 let inner_w = (available.width - PAD_X * 2.0 - ARROW_W).max(0.0);
415
416 let sl = self.selected_label.layout(Size::new(inner_w, CLOSED_H));
418 let sl_y = (CLOSED_H - sl.height) * 0.5;
419 self.selected_label
420 .set_bounds(Rect::new(PAD_X, sl_y, sl.width, sl.height));
421
422 let mut labels = self.item_labels.borrow_mut();
425 for i in 0..labels.len() {
426 let s = labels[i].layout(Size::new(inner_w, ITEM_H));
427 let ir = self.item_rect(i);
428 let ly = ir.y + (ITEM_H - s.height) * 0.5;
429 labels[i].set_bounds(Rect::new(PAD_X, ly, s.width, s.height));
430 }
431 drop(labels);
432
433 Size::new(available.width, CLOSED_H)
434 }
435
436 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
437 let v = ctx.visuals();
438 let w = self.bounds.width;
439
440 ctx.set_fill_color(v.widget_bg);
442 ctx.begin_path();
443 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
444 ctx.fill();
445
446 ctx.set_stroke_color(v.widget_stroke);
447 ctx.set_line_width(1.0);
448 ctx.begin_path();
449 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
450 ctx.stroke();
451
452 let arrow_x = w - ARROW_W * 0.5;
454 let arrow_cy = CLOSED_H * 0.5;
455 let arrow_sz = 4.0;
456 ctx.set_fill_color(v.text_dim);
457 ctx.begin_path();
458 ctx.move_to(arrow_x - arrow_sz, arrow_cy + arrow_sz * 0.5);
460 ctx.line_to(arrow_x + arrow_sz, arrow_cy + arrow_sz * 0.5);
461 ctx.line_to(arrow_x, arrow_cy - arrow_sz * 0.5);
462 ctx.close_path();
463 ctx.fill();
464
465 self.selected_label.set_color(v.text_color);
467 let sl_bounds = self.selected_label.bounds();
468
469 ctx.save();
470 ctx.translate(sl_bounds.x, sl_bounds.y);
471 paint_subtree(&mut self.selected_label, ctx);
472 ctx.restore();
473 }
474
475 fn paint_overlay(&mut self, _ctx: &mut dyn DrawCtx) {}
476
477 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
478 if self.open {
479 let mut x = 0.0;
480 let mut y = 0.0;
481 let t = ctx.root_transform();
482 t.transform(&mut x, &mut y);
483 let scale = crate::device_scale::device_scale().max(1e-6);
491 let x = x / scale;
492 let y = y / scale;
493 let viewport_h = crate::widgets::combo_box::current_combo_viewport()
494 .map(|s| s.height)
495 .unwrap_or(f64::MAX / 4.0);
496 self.configure_popup_geometry(y, viewport_h);
497 let style = self.popup_scroll_style();
498 let visibility = current_scroll_visibility();
499 let viewport = self.popup_scroll_viewport();
500 let geom = self.scrollbar_geometry(style);
501 let scrollbar = self
502 .scrollbar
503 .prepare_paint(viewport, style, visibility, geom)
504 .map(|bar| bar.translated(x, y));
505 submit_combo_popup(ComboPopupRequest {
506 x,
507 y,
508 width: self.bounds.width,
509 popup_h: self.popup_h(),
510 opens_up: self.popup_opens_up,
511 first_item: self.scroll_offset,
512 visible_count: self.popup_visible_count,
513 selected: self.selected,
514 hovered_item: self.hovered_item,
515 scrollbar,
516 item_count: self.options.len(),
517 item_labels: Rc::clone(&self.item_labels),
518 hover_tooltip: self.hovered_item.and_then(|idx| {
519 let text = self.item_tooltips.as_ref()?.get(idx)?;
520 if text.is_empty() {
521 return None;
522 }
523 Some((text.clone(), Arc::clone(&self.font)))
524 }),
525 });
526 }
527 }
528
529 fn on_event(&mut self, event: &Event) -> EventResult {
530 match event {
531 Event::MouseDown {
532 button: MouseButton::Middle,
533 pos,
534 ..
535 } => {
536 if self.pos_in_popup(*pos) {
537 self.middle_dragging = true;
538 self.middle_last_pos = *pos;
539 self.hovered_item = None;
540 crate::animation::request_draw();
541 return EventResult::Consumed;
542 }
543 EventResult::Ignored
544 }
545 Event::MouseDown {
546 button: MouseButton::Left,
547 pos,
548 ..
549 } => {
550 if self.in_button(*pos) {
551 self.open = !self.open;
552 self.hovered_item = None;
553 self.scrollbar.hovered_bar = false;
554 self.scrollbar.hovered_thumb = false;
555 self.scrollbar.dragging = false;
556 self.middle_dragging = false;
557 if self.open {
558 self.ensure_selected_visible();
559 }
560 crate::animation::request_draw();
561 return EventResult::Consumed;
562 }
563 if self.open {
564 if self.pos_in_scrollbar(*pos) {
565 let style = self.popup_scroll_style();
566 let viewport = self.popup_scroll_viewport();
567 let geom = self.scrollbar_geometry(style);
568 self.sync_scrollbar_from_rows();
569 if self.scrollbar.begin_drag(*pos, viewport, style, geom) {
570 } else if self.scrollbar.page_at(*pos, viewport, style, geom) {
572 self.sync_rows_from_scrollbar();
573 }
574 self.hovered_item = None;
575 self.scrollbar.hovered_thumb = self.pos_on_scroll_thumb(*pos);
576 crate::animation::request_draw();
577 return EventResult::Consumed;
578 }
579 if let Some(i) = self.item_for_pos(*pos) {
580 self.set_selected(i);
590 self.open = false;
591 self.hovered_item = None;
592 self.scrollbar.hovered_bar = false;
593 self.scrollbar.hovered_thumb = false;
594 self.scrollbar.dragging = false;
595 self.middle_dragging = false;
596 self.fire();
597 crate::animation::request_draw();
598 return EventResult::Consumed;
599 }
600 self.open = false;
602 self.hovered_item = None;
603 self.scrollbar.hovered_bar = false;
604 self.scrollbar.hovered_thumb = false;
605 self.scrollbar.dragging = false;
606 self.middle_dragging = false;
607 crate::animation::request_draw();
608 return EventResult::Consumed;
609 }
610 EventResult::Ignored
611 }
612 Event::MouseMove { pos } => {
613 if self.middle_dragging {
614 let dy = pos.y - self.middle_last_pos.y;
615 self.middle_last_pos = *pos;
616 self.sync_scrollbar_from_rows();
617 if self.scrollbar.scroll_by(dy, self.popup_scroll_viewport()) {
618 self.sync_rows_from_scrollbar();
619 self.hovered_item = None;
620 crate::animation::request_draw();
621 }
622 return EventResult::Consumed;
623 }
624 if self.scrollbar.dragging {
625 let style = self.popup_scroll_style();
626 let viewport = self.popup_scroll_viewport();
627 let geom = self.scrollbar_geometry(style);
628 if self.scrollbar.drag_to(*pos, viewport, style, geom) {
629 self.sync_rows_from_scrollbar();
630 self.hovered_item = None;
631 crate::animation::request_draw();
632 }
633 return EventResult::Consumed;
634 }
635 let hovered_item = self.item_for_pos(*pos);
636 let style = self.popup_scroll_style();
637 let viewport = self.popup_scroll_viewport();
638 let geom = self.scrollbar_geometry(style);
639 let scroll_hover_changed = self.scrollbar.update_hover(*pos, viewport, style, geom);
640 if hovered_item != self.hovered_item || scroll_hover_changed {
641 self.hovered_item = hovered_item;
642 crate::animation::request_draw();
643 }
644 EventResult::Ignored
645 }
646 Event::MouseWheel { delta_y, .. } => {
647 if self.open && self.options.len() > self.popup_visible_count {
648 self.sync_scrollbar_from_rows();
649 if self
653 .scrollbar
654 .scroll_by(-delta_y * 40.0, self.popup_scroll_viewport())
655 {
656 self.sync_rows_from_scrollbar();
657 self.hovered_item = None;
658 crate::animation::request_draw();
659 }
660 EventResult::Consumed
661 } else {
662 EventResult::Ignored
663 }
664 }
665 Event::KeyDown { key, .. } => {
666 let n = self.options.len();
667 match key {
668 Key::Enter | Key::Char(' ') => {
669 self.open = !self.open;
670 self.scrollbar.hovered_bar = false;
671 self.scrollbar.hovered_thumb = false;
672 self.scrollbar.dragging = false;
673 self.middle_dragging = false;
674 if self.open {
675 self.ensure_selected_visible();
676 }
677 crate::animation::request_draw();
678 EventResult::Consumed
679 }
680 Key::Escape => {
681 if self.open {
682 self.open = false;
683 self.scrollbar.hovered_bar = false;
684 self.scrollbar.hovered_thumb = false;
685 self.scrollbar.dragging = false;
686 self.middle_dragging = false;
687 crate::animation::request_draw();
688 EventResult::Consumed
689 } else {
690 EventResult::Ignored
691 }
692 }
693 Key::ArrowDown => {
694 if self.selected + 1 < n {
695 self.set_selected(self.selected + 1);
696 self.ensure_selected_visible();
697 self.fire();
698 crate::animation::request_draw();
699 }
700 EventResult::Consumed
701 }
702 Key::ArrowUp => {
703 if self.selected > 0 {
704 self.set_selected(self.selected - 1);
705 self.ensure_selected_visible();
706 self.fire();
707 crate::animation::request_draw();
708 }
709 EventResult::Consumed
710 }
711 _ => EventResult::Ignored,
712 }
713 }
714 Event::FocusLost => {
715 let was_open = self.open;
716 self.open = false;
717 self.hovered_item = None;
718 self.scrollbar.hovered_bar = false;
719 self.scrollbar.hovered_thumb = false;
720 self.scrollbar.dragging = false;
721 self.middle_dragging = false;
722 if was_open {
723 crate::animation::request_draw();
724 }
725 EventResult::Ignored
726 }
727 Event::MouseUp { button, .. } => {
728 if *button == MouseButton::Left && self.scrollbar.dragging {
729 self.scrollbar.dragging = false;
730 crate::animation::request_draw();
731 EventResult::Consumed
732 } else if *button == MouseButton::Middle && self.middle_dragging {
733 self.middle_dragging = false;
734 crate::animation::request_draw();
735 EventResult::Consumed
736 } else {
737 EventResult::Ignored
738 }
739 }
740 _ => EventResult::Ignored,
741 }
742 }
743
744 fn properties(&self) -> Vec<(&'static str, String)> {
745 vec![
746 ("selected", self.selected.to_string()),
747 ("open", self.open.to_string()),
748 ("options", self.options.len().to_string()),
749 ("popup_opens_up", self.popup_opens_up.to_string()),
750 ("popup_visible_count", self.popup_visible_count.to_string()),
751 ("scroll_offset", self.scroll_offset.to_string()),
752 ]
753 }
754}
755
756fn submit_combo_popup_internal(request: ComboPopupRequest) {
757 COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().push(request));
758}
759
760mod popup_paint;
761pub(crate) use popup_paint::{begin_combo_popup_frame, paint_global_combo_popups};
762pub(super) use popup_paint::{current_combo_viewport, submit_combo_popup};