1use std::cell::{Cell, RefCell};
13use std::rc::Rc;
14use std::sync::Arc;
15
16use crate::color::Color;
17use crate::draw_ctx::DrawCtx;
18use crate::event::{Event, EventResult, Key, MouseButton};
19use crate::geometry::{Point, Rect, Size};
20use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
21use crate::text::Font;
22use crate::widget::{paint_subtree, Widget};
23use crate::widgets::label::Label;
24
25use super::scroll_view::{current_scroll_style, current_scroll_visibility, ScrollBarStyle};
26use super::scrollbar::{
27 paint_prepared_scrollbar, PreparedScrollbar, ScrollbarAxis, ScrollbarGeometry,
28 ScrollbarOrientation, DEFAULT_GRAB_MARGIN,
29};
30
31const CLOSED_H: f64 = 24.0;
32const ITEM_H: f64 = 22.0;
33const PAD_X: f64 = 8.0;
34const ARROW_W: f64 = 20.0;
35const CORNER_R: f64 = 4.0;
36const POPUP_MARGIN: f64 = 4.0;
37const MIN_VISIBLE_ITEMS: usize = 3;
38const DEFAULT_VISIBLE_ITEMS: usize = 8;
39const SCROLLBAR_W: f64 = 6.0;
40
41struct ComboPopupRequest {
42 x: f64,
43 y: f64,
44 width: f64,
45 popup_h: f64,
46 opens_up: bool,
47 first_item: usize,
48 visible_count: usize,
49 selected: usize,
50 hovered_item: Option<usize>,
51 scrollbar: Option<PreparedScrollbar>,
52 options: Vec<String>,
53 font: Arc<Font>,
54 font_size: f64,
55 item_fonts: Option<Vec<Arc<Font>>>,
56}
57
58thread_local! {
59 static COMBO_POPUP_QUEUE: RefCell<Vec<ComboPopupRequest>> = const { RefCell::new(Vec::new()) };
60 static CURRENT_COMBO_VIEWPORT: Cell<Option<Size>> = const { Cell::new(None) };
61}
62
63pub struct ComboBox {
71 bounds: Rect,
72 children: Vec<Box<dyn Widget>>, base: WidgetBase,
74
75 options: Vec<String>,
76 selected: usize,
77 open: bool,
78 hovered_item: Option<usize>,
80
81 font: Arc<Font>,
82 font_size: f64,
83
84 on_change: Option<Box<dyn FnMut(usize)>>,
85 selected_cell: Option<Rc<Cell<usize>>>,
90
91 selected_label: Label,
94 item_labels: Vec<Label>,
96 item_fonts: Option<Vec<Arc<Font>>>,
102
103 popup_opens_up: bool,
104 popup_visible_count: usize,
105 scroll_offset: usize,
106 scrollbar: ScrollbarAxis,
107 middle_dragging: bool,
108 middle_last_pos: Point,
109}
110
111impl ComboBox {
112 pub fn new(options: Vec<impl Into<String>>, selected: usize, font: Arc<Font>) -> Self {
117 let font_size = 13.0;
118 let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
119 let sel = selected.min(opts.len().saturating_sub(1));
120
121 let selected_label = Self::make_label(
122 opts.get(sel).map(|s| s.as_str()).unwrap_or(""),
123 font_size,
124 Arc::clone(&font),
125 );
126 let item_labels = opts
127 .iter()
128 .map(|t| Self::make_label(t, font_size, Arc::clone(&font)))
129 .collect();
130
131 Self {
132 bounds: Rect::default(),
133 children: Vec::new(),
134 base: WidgetBase::new(),
135 options: opts,
136 selected: sel,
137 open: false,
138 hovered_item: None,
139 font,
140 font_size,
141 on_change: None,
142 selected_cell: None,
143 selected_label,
144 item_labels,
145 item_fonts: None,
146 popup_opens_up: false,
147 popup_visible_count: DEFAULT_VISIBLE_ITEMS,
148 scroll_offset: 0,
149 scrollbar: ScrollbarAxis {
150 enabled: true,
151 ..ScrollbarAxis::default()
152 },
153 middle_dragging: false,
154 middle_last_pos: Point::ORIGIN,
155 }
156 }
157
158 pub fn with_selected_cell(mut self, cell: Rc<Cell<usize>>) -> Self {
164 let n = self.options.len();
165 let v = cell.get();
166 if n > 0 {
167 let clamped = v.min(n - 1);
168 self.set_selected(clamped);
171 }
172 self.selected_cell = Some(cell);
173 self
174 }
175
176 fn make_label(text: &str, font_size: f64, font: Arc<Font>) -> Label {
177 Label::new(text, font).with_font_size(font_size)
178 }
179
180 pub fn with_font_size(mut self, size: f64) -> Self {
183 self.font_size = size;
184 self.selected_label = Self::make_label(
185 self.options
186 .get(self.selected)
187 .map(|s| s.as_str())
188 .unwrap_or(""),
189 size,
190 Arc::clone(&self.font),
191 );
192 self.item_labels = self
193 .options
194 .iter()
195 .map(|t| Self::make_label(t, size, Arc::clone(&self.font)))
196 .collect();
197 self
198 }
199
200 pub fn with_margin(mut self, m: Insets) -> Self {
201 self.base.margin = m;
202 self
203 }
204 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
205 self.base.h_anchor = h;
206 self
207 }
208 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
209 self.base.v_anchor = v;
210 self
211 }
212 pub fn with_min_size(mut self, s: Size) -> Self {
213 self.base.min_size = s;
214 self
215 }
216 pub fn with_max_size(mut self, s: Size) -> Self {
217 self.base.max_size = s;
218 self
219 }
220
221 pub fn on_change(mut self, cb: impl FnMut(usize) + 'static) -> Self {
223 self.on_change = Some(Box::new(cb));
224 self
225 }
226
227 pub fn with_item_fonts(mut self, fonts: Vec<Arc<Font>>) -> Self {
240 self.set_item_fonts(fonts);
241 self
242 }
243
244 pub fn set_item_fonts(&mut self, fonts: Vec<Arc<Font>>) {
246 self.item_fonts = Some(fonts.clone());
247 let size = self.font_size;
248 self.item_labels = self
249 .options
250 .iter()
251 .enumerate()
252 .map(|(i, t)| {
253 let f = fonts
254 .get(i)
255 .cloned()
256 .unwrap_or_else(|| Arc::clone(&self.font));
257 Label::new(t, f)
258 .with_font_size(size)
259 .with_ignore_system_font(true)
260 })
261 .collect();
262 if let Some(sel_font) = fonts.get(self.selected).cloned() {
263 self.selected_label = Label::new(
264 self.options
265 .get(self.selected)
266 .map(|s| s.as_str())
267 .unwrap_or(""),
268 sel_font,
269 )
270 .with_font_size(size)
271 .with_ignore_system_font(true);
272 }
273 }
274
275 pub fn selected(&self) -> usize {
278 self.selected
279 }
280
281 pub fn set_selected(&mut self, idx: usize) {
282 if idx < self.options.len() {
283 self.selected = idx;
284 if let Some(ref fonts) = self.item_fonts {
289 if let Some(f) = fonts.get(idx).cloned() {
290 self.selected_label = Label::new(self.options[idx].as_str(), f)
291 .with_font_size(self.font_size)
292 .with_ignore_system_font(true);
293 return;
294 }
295 }
296 self.selected_label.set_text(self.options[idx].as_str());
297 }
298 }
299}
300
301mod geometry;
302
303impl Widget for ComboBox {
304 fn type_name(&self) -> &'static str {
305 "ComboBox"
306 }
307 fn bounds(&self) -> Rect {
308 self.bounds
309 }
310 fn set_bounds(&mut self, b: Rect) {
311 self.bounds = b;
312 }
313 fn children(&self) -> &[Box<dyn Widget>] {
314 &self.children
315 }
316 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
317 &mut self.children
318 }
319
320 fn is_focusable(&self) -> bool {
321 true
322 }
323
324 fn needs_draw(&self) -> bool {
325 self.scrollbar.animation_active()
326 }
327
328 fn hit_test(&self, local_pos: Point) -> bool {
329 self.in_button(local_pos) || self.pos_in_popup(local_pos)
330 }
331
332 fn hit_test_global_overlay(&self, local_pos: Point) -> bool {
333 self.pos_in_popup(local_pos)
334 }
335
336 fn margin(&self) -> Insets {
337 self.base.margin
338 }
339 fn h_anchor(&self) -> HAnchor {
340 self.base.h_anchor
341 }
342 fn v_anchor(&self) -> VAnchor {
343 self.base.v_anchor
344 }
345 fn min_size(&self) -> Size {
346 self.base.min_size
347 }
348 fn max_size(&self) -> Size {
349 self.base.max_size
350 }
351
352 fn layout(&mut self, available: Size) -> Size {
353 if !self.open {
358 if let Some(cell) = &self.selected_cell {
359 let n = self.options.len();
360 if n > 0 {
361 let v = cell.get().min(n - 1);
362 if v != self.selected {
363 self.set_selected(v);
366 }
367 }
368 }
369 }
370
371 self.bounds = Rect::new(0.0, 0.0, available.width, CLOSED_H);
372 let inner_w = (available.width - PAD_X * 2.0 - ARROW_W).max(0.0);
373
374 let sl = self.selected_label.layout(Size::new(inner_w, CLOSED_H));
376 let sl_y = (CLOSED_H - sl.height) * 0.5;
377 self.selected_label
378 .set_bounds(Rect::new(PAD_X, sl_y, sl.width, sl.height));
379
380 for i in 0..self.item_labels.len() {
383 let s = self.item_labels[i].layout(Size::new(inner_w, ITEM_H));
384 let ir = self.item_rect(i);
385 let ly = ir.y + (ITEM_H - s.height) * 0.5;
386 self.item_labels[i].set_bounds(Rect::new(PAD_X, ly, s.width, s.height));
387 }
388
389 Size::new(available.width, CLOSED_H)
390 }
391
392 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
393 let v = ctx.visuals();
394 let w = self.bounds.width;
395
396 ctx.set_fill_color(v.widget_bg);
398 ctx.begin_path();
399 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
400 ctx.fill();
401
402 ctx.set_stroke_color(v.widget_stroke);
403 ctx.set_line_width(1.0);
404 ctx.begin_path();
405 ctx.rounded_rect(0.0, 0.0, w, CLOSED_H, CORNER_R);
406 ctx.stroke();
407
408 let arrow_x = w - ARROW_W * 0.5;
410 let arrow_cy = CLOSED_H * 0.5;
411 let arrow_sz = 4.0;
412 ctx.set_fill_color(v.text_dim);
413 ctx.begin_path();
414 ctx.move_to(arrow_x - arrow_sz, arrow_cy + arrow_sz * 0.5);
416 ctx.line_to(arrow_x + arrow_sz, arrow_cy + arrow_sz * 0.5);
417 ctx.line_to(arrow_x, arrow_cy - arrow_sz * 0.5);
418 ctx.close_path();
419 ctx.fill();
420
421 self.selected_label.set_color(v.text_color);
423 let sl_bounds = self.selected_label.bounds();
424
425 ctx.save();
426 ctx.translate(sl_bounds.x, sl_bounds.y);
427 paint_subtree(&mut self.selected_label, ctx);
428 ctx.restore();
429 }
430
431 fn paint_overlay(&mut self, _ctx: &mut dyn DrawCtx) {}
432
433 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
434 if self.open {
435 let mut x = 0.0;
436 let mut y = 0.0;
437 let t = ctx.root_transform();
438 t.transform(&mut x, &mut y);
439 let viewport_h = crate::widgets::combo_box::current_combo_viewport()
440 .map(|s| s.height)
441 .unwrap_or(f64::MAX / 4.0);
442 self.configure_popup_geometry(y, viewport_h);
443 let style = self.popup_scroll_style();
444 let visibility = current_scroll_visibility();
445 let viewport = self.popup_scroll_viewport();
446 let geom = self.scrollbar_geometry(style);
447 let scrollbar = self
448 .scrollbar
449 .prepare_paint(viewport, style, visibility, geom)
450 .map(|bar| bar.translated(x, y));
451 submit_combo_popup(ComboPopupRequest {
452 x,
453 y,
454 width: self.bounds.width,
455 popup_h: self.popup_h(),
456 opens_up: self.popup_opens_up,
457 first_item: self.scroll_offset,
458 visible_count: self.popup_visible_count,
459 selected: self.selected,
460 hovered_item: self.hovered_item,
461 scrollbar,
462 options: self.options.clone(),
463 font: Arc::clone(&self.font),
464 font_size: self.font_size,
465 item_fonts: self.item_fonts.clone(),
466 });
467 }
468 }
469
470 fn on_event(&mut self, event: &Event) -> EventResult {
471 match event {
472 Event::MouseDown {
473 button: MouseButton::Middle,
474 pos,
475 ..
476 } => {
477 if self.pos_in_popup(*pos) {
478 self.middle_dragging = true;
479 self.middle_last_pos = *pos;
480 self.hovered_item = None;
481 crate::animation::request_draw();
482 return EventResult::Consumed;
483 }
484 EventResult::Ignored
485 }
486 Event::MouseDown {
487 button: MouseButton::Left,
488 pos,
489 ..
490 } => {
491 if self.in_button(*pos) {
492 self.open = !self.open;
493 self.hovered_item = None;
494 self.scrollbar.hovered_bar = false;
495 self.scrollbar.hovered_thumb = false;
496 self.scrollbar.dragging = false;
497 self.middle_dragging = false;
498 if self.open {
499 self.ensure_selected_visible();
500 }
501 crate::animation::request_draw();
502 return EventResult::Consumed;
503 }
504 if self.open {
505 if self.pos_in_scrollbar(*pos) {
506 let style = self.popup_scroll_style();
507 let viewport = self.popup_scroll_viewport();
508 let geom = self.scrollbar_geometry(style);
509 self.sync_scrollbar_from_rows();
510 if self.scrollbar.begin_drag(*pos, viewport, style, geom) {
511 } else if self.scrollbar.page_at(*pos, viewport, style, geom) {
513 self.sync_rows_from_scrollbar();
514 }
515 self.hovered_item = None;
516 self.scrollbar.hovered_thumb = self.pos_on_scroll_thumb(*pos);
517 crate::animation::request_draw();
518 return EventResult::Consumed;
519 }
520 if let Some(i) = self.item_for_pos(*pos) {
521 self.set_selected(i);
531 self.open = false;
532 self.hovered_item = None;
533 self.scrollbar.hovered_bar = false;
534 self.scrollbar.hovered_thumb = false;
535 self.scrollbar.dragging = false;
536 self.middle_dragging = false;
537 self.fire();
538 crate::animation::request_draw();
539 return EventResult::Consumed;
540 }
541 self.open = false;
543 self.hovered_item = None;
544 self.scrollbar.hovered_bar = false;
545 self.scrollbar.hovered_thumb = false;
546 self.scrollbar.dragging = false;
547 self.middle_dragging = false;
548 crate::animation::request_draw();
549 return EventResult::Consumed;
550 }
551 EventResult::Ignored
552 }
553 Event::MouseMove { pos } => {
554 if self.middle_dragging {
555 let dy = pos.y - self.middle_last_pos.y;
556 self.middle_last_pos = *pos;
557 self.sync_scrollbar_from_rows();
558 if self.scrollbar.scroll_by(dy, self.popup_scroll_viewport()) {
559 self.sync_rows_from_scrollbar();
560 self.hovered_item = None;
561 crate::animation::request_draw();
562 }
563 return EventResult::Consumed;
564 }
565 if self.scrollbar.dragging {
566 let style = self.popup_scroll_style();
567 let viewport = self.popup_scroll_viewport();
568 let geom = self.scrollbar_geometry(style);
569 if self.scrollbar.drag_to(*pos, viewport, style, geom) {
570 self.sync_rows_from_scrollbar();
571 self.hovered_item = None;
572 crate::animation::request_draw();
573 }
574 return EventResult::Consumed;
575 }
576 let hovered_item = self.item_for_pos(*pos);
577 let style = self.popup_scroll_style();
578 let viewport = self.popup_scroll_viewport();
579 let geom = self.scrollbar_geometry(style);
580 let scroll_hover_changed = self.scrollbar.update_hover(*pos, viewport, style, geom);
581 if hovered_item != self.hovered_item || scroll_hover_changed {
582 self.hovered_item = hovered_item;
583 crate::animation::request_draw();
584 }
585 EventResult::Ignored
586 }
587 Event::MouseWheel { delta_y, .. } => {
588 if self.open && self.options.len() > self.popup_visible_count {
589 self.sync_scrollbar_from_rows();
590 if self
591 .scrollbar
592 .scroll_by(delta_y * 40.0, self.popup_scroll_viewport())
593 {
594 self.sync_rows_from_scrollbar();
595 self.hovered_item = None;
596 crate::animation::request_draw();
597 }
598 EventResult::Consumed
599 } else {
600 EventResult::Ignored
601 }
602 }
603 Event::KeyDown { key, .. } => {
604 let n = self.options.len();
605 match key {
606 Key::Enter | Key::Char(' ') => {
607 self.open = !self.open;
608 self.scrollbar.hovered_bar = false;
609 self.scrollbar.hovered_thumb = false;
610 self.scrollbar.dragging = false;
611 self.middle_dragging = false;
612 if self.open {
613 self.ensure_selected_visible();
614 }
615 crate::animation::request_draw();
616 EventResult::Consumed
617 }
618 Key::Escape => {
619 if self.open {
620 self.open = false;
621 self.scrollbar.hovered_bar = false;
622 self.scrollbar.hovered_thumb = false;
623 self.scrollbar.dragging = false;
624 self.middle_dragging = false;
625 crate::animation::request_draw();
626 EventResult::Consumed
627 } else {
628 EventResult::Ignored
629 }
630 }
631 Key::ArrowDown => {
632 if self.selected + 1 < n {
633 self.set_selected(self.selected + 1);
634 self.ensure_selected_visible();
635 self.fire();
636 crate::animation::request_draw();
637 }
638 EventResult::Consumed
639 }
640 Key::ArrowUp => {
641 if self.selected > 0 {
642 self.set_selected(self.selected - 1);
643 self.ensure_selected_visible();
644 self.fire();
645 crate::animation::request_draw();
646 }
647 EventResult::Consumed
648 }
649 _ => EventResult::Ignored,
650 }
651 }
652 Event::FocusLost => {
653 let was_open = self.open;
654 self.open = false;
655 self.hovered_item = None;
656 self.scrollbar.hovered_bar = false;
657 self.scrollbar.hovered_thumb = false;
658 self.scrollbar.dragging = false;
659 self.middle_dragging = false;
660 if was_open {
661 crate::animation::request_draw();
662 }
663 EventResult::Ignored
664 }
665 Event::MouseUp { button, .. } => {
666 if *button == MouseButton::Left && self.scrollbar.dragging {
667 self.scrollbar.dragging = false;
668 crate::animation::request_draw();
669 EventResult::Consumed
670 } else if *button == MouseButton::Middle && self.middle_dragging {
671 self.middle_dragging = false;
672 crate::animation::request_draw();
673 EventResult::Consumed
674 } else {
675 EventResult::Ignored
676 }
677 }
678 _ => EventResult::Ignored,
679 }
680 }
681
682 fn properties(&self) -> Vec<(&'static str, String)> {
683 vec![
684 ("selected", self.selected.to_string()),
685 ("open", self.open.to_string()),
686 ("options", self.options.len().to_string()),
687 ("popup_opens_up", self.popup_opens_up.to_string()),
688 ("popup_visible_count", self.popup_visible_count.to_string()),
689 ("scroll_offset", self.scroll_offset.to_string()),
690 ]
691 }
692}
693
694fn submit_combo_popup(request: ComboPopupRequest) {
695 COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().push(request));
696}
697
698fn current_combo_viewport() -> Option<Size> {
699 CURRENT_COMBO_VIEWPORT.with(|v| v.get())
700}
701
702pub(crate) fn begin_combo_popup_frame(viewport: Size) {
703 CURRENT_COMBO_VIEWPORT.with(|v| v.set(Some(viewport)));
704 COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().clear());
705}
706
707pub(crate) fn paint_global_combo_popups(ctx: &mut dyn DrawCtx) {
708 let requests = COMBO_POPUP_QUEUE.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>());
709 if requests.is_empty() {
710 return;
711 }
712
713 ctx.save();
714 ctx.reset_clip();
715 for request in requests {
716 paint_combo_popup(ctx, request);
717 }
718 ctx.restore();
719}
720
721fn paint_combo_popup(ctx: &mut dyn DrawCtx, request: ComboPopupRequest) {
722 let v = ctx.visuals();
723 let popup_y = if request.opens_up {
724 request.y + CLOSED_H
725 } else {
726 request.y - request.popup_h
727 };
728
729 ctx.set_fill_color(v.window_fill);
732 ctx.begin_path();
733 ctx.rounded_rect(request.x, popup_y, request.width, request.popup_h, CORNER_R);
734 ctx.fill();
735
736 ctx.set_fill_color(v.widget_bg);
737 ctx.begin_path();
738 ctx.rounded_rect(request.x, popup_y, request.width, request.popup_h, CORNER_R);
739 ctx.fill();
740
741 let has_scroll = request.options.len() > request.visible_count;
742 let text_w = if has_scroll {
743 (request.width - SCROLLBAR_W - 4.0).max(0.0)
744 } else {
745 request.width
746 };
747
748 for row in 0..request.visible_count {
749 let idx = request.first_item + row;
750 let Some(text) = request.options.get(idx) else {
751 break;
752 };
753 let item_y = popup_y + request.popup_h - (row as f64 + 1.0) * ITEM_H;
754 let is_selected = idx == request.selected;
755 let is_hovered = request.hovered_item == Some(idx);
756 if is_selected || is_hovered {
757 let bg = if is_selected {
758 v.accent
759 } else {
760 v.widget_bg_hovered
761 };
762 ctx.set_fill_color(bg);
763 ctx.begin_path();
764 ctx.rounded_rect(
765 request.x + 2.0,
766 item_y + 1.0,
767 text_w - 4.0,
768 ITEM_H - 2.0,
769 3.0,
770 );
771 ctx.fill();
772 }
773
774 let font = request
775 .item_fonts
776 .as_ref()
777 .and_then(|fonts| fonts.get(idx))
778 .cloned()
779 .unwrap_or_else(|| Arc::clone(&request.font));
780 ctx.set_font(font);
781 ctx.set_font_size(request.font_size);
782 ctx.set_fill_color(if is_selected {
783 Color::white()
784 } else {
785 v.text_color
786 });
787 let baseline = item_y + (ITEM_H - request.font_size) * 0.5;
788 ctx.fill_text(text, request.x + PAD_X, baseline);
789 }
790
791 if let Some(scrollbar) = request.scrollbar {
792 paint_prepared_scrollbar(ctx, scrollbar);
793 }
794
795 ctx.set_stroke_color(v.widget_stroke);
796 ctx.set_line_width(1.0);
797 ctx.begin_path();
798 ctx.rounded_rect(request.x, popup_y, request.width, request.popup_h, CORNER_R);
799 ctx.stroke();
800}