1use crate::theme::Theme;
2use crate::tokens::{ColorPalette, ControlSize, mix};
3use egui::{
4 Color32, CornerRadius, Event, FontId, Key, LayerId, Order, Painter, Pos2, Rect, Response,
5 Sense, Stroke, StrokeKind, Ui, Vec2, pos2, vec2,
6};
7use log::trace;
8use std::fmt::Debug;
9use std::hash::Hash;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum SelectDirection {
13 Ltr,
14 Rtl,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
18pub enum SelectSide {
19 Top,
20 Right,
21
22 #[default]
23 Bottom,
24
25 Left,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
29pub enum SelectAlign {
30 #[default]
31 Start,
32 Center,
33 End,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct SelectCollisionPadding {
38 pub top: f32,
39 pub right: f32,
40 pub bottom: f32,
41 pub left: f32,
42}
43
44impl SelectCollisionPadding {
45 pub fn all(value: f32) -> Self {
46 Self {
47 top: value,
48 right: value,
49 bottom: value,
50 left: value,
51 }
52 }
53}
54
55impl Default for SelectCollisionPadding {
56 fn default() -> Self {
57 Self::all(10.0)
58 }
59}
60
61impl From<f32> for SelectCollisionPadding {
62 fn from(value: f32) -> Self {
63 SelectCollisionPadding::all(value)
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
68pub enum SelectSticky {
69 #[default]
70 Partial,
71 Always,
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
75pub enum SelectUpdatePositionStrategy {
76 #[default]
77 Optimized,
78 Always,
79}
80
81#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82pub enum SelectPortalContainer {
83 Tooltip,
84 Foreground,
85 Middle,
86 Background,
87}
88
89impl SelectPortalContainer {
90 fn order(self) -> Order {
91 match self {
92 SelectPortalContainer::Tooltip => Order::Tooltip,
93 SelectPortalContainer::Foreground => Order::Foreground,
94 SelectPortalContainer::Middle => Order::Middle,
95 SelectPortalContainer::Background => Order::Background,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
101pub struct SelectPreventable {
102 default_prevented: bool,
103}
104
105impl SelectPreventable {
106 pub fn prevent_default(&mut self) {
107 self.default_prevented = true;
108 }
109
110 pub fn default_prevented(&self) -> bool {
111 self.default_prevented
112 }
113}
114
115#[derive(Clone, Copy, Debug, PartialEq)]
116pub struct SelectAutoFocusEvent {
117 pub preventable: SelectPreventable,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq)]
121pub struct SelectEscapeKeyDownEvent {
122 pub key: Key,
123 pub preventable: SelectPreventable,
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
127pub struct SelectPointerDownOutsideEvent {
128 pub pointer_pos: Option<Pos2>,
129 pub preventable: SelectPreventable,
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
133pub enum SelectSize {
134 Size1,
135
136 #[default]
137 Size2,
138
139 Size3,
140}
141
142impl SelectSize {
143 fn canonical(self) -> Self {
144 self
145 }
146
147 pub fn trigger_height(self) -> f32 {
148 match self.canonical() {
149 SelectSize::Size1 => 24.0,
150 SelectSize::Size2 => 32.0,
151 _ => 36.0,
152 }
153 }
154
155 pub fn item_height(self) -> f32 {
156 match self.canonical() {
157 SelectSize::Size1 => 20.0,
158 SelectSize::Size2 => 24.0,
159 _ => 28.0,
160 }
161 }
162
163 pub fn trigger_padding(self) -> Vec2 {
164 match self.canonical() {
165 SelectSize::Size1 => vec2(8.0, 4.0),
166 SelectSize::Size2 => vec2(12.0, 6.0),
167 _ => vec2(14.0, 8.0),
168 }
169 }
170
171 pub fn font_size(self) -> f32 {
172 match self.canonical() {
173 SelectSize::Size1 => 12.0,
174 SelectSize::Size2 => 14.0,
175 _ => 16.0,
176 }
177 }
178
179 pub fn icon_size(self) -> f32 {
180 match self.canonical() {
181 SelectSize::Size1 => 12.0,
182 SelectSize::Size2 => 14.0,
183 _ => 16.0,
184 }
185 }
186
187 pub fn gap(self) -> f32 {
188 match self.canonical() {
189 SelectSize::Size1 => 4.0,
190 SelectSize::Size2 => 6.0,
191 _ => 8.0,
192 }
193 }
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
197pub enum SelectRadius {
198 None,
199
200 Small,
201
202 #[default]
203 Medium,
204
205 Large,
206
207 Full,
208}
209
210impl SelectRadius {
211 pub fn corner_radius(self) -> CornerRadius {
212 match self {
213 SelectRadius::None => CornerRadius::same(0),
214 SelectRadius::Small => CornerRadius::same(2),
215 SelectRadius::Medium => CornerRadius::same(4),
216 SelectRadius::Large => CornerRadius::same(6),
217 SelectRadius::Full => CornerRadius::same(255),
218 }
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
223pub enum PopupPosition {
224 Popper,
225
226 #[default]
227 ItemAligned,
228}
229
230#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
231pub enum TriggerVariant {
232 #[default]
233 Surface,
234
235 Classic,
236
237 Soft,
238
239 Ghost,
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
243pub enum ContentVariant {
244 #[default]
245 Soft,
246
247 Solid,
248}
249
250impl From<ControlSize> for SelectSize {
251 fn from(size: ControlSize) -> Self {
252 match size {
253 ControlSize::Sm | ControlSize::IconSm => SelectSize::Size1,
254 _ => SelectSize::Size2,
255 }
256 }
257}
258
259#[derive(Clone, Debug)]
260pub struct SelectStyle {
261 pub trigger_bg: Color32,
262 pub trigger_bg_hover: Color32,
263 pub trigger_border: Color32,
264 pub trigger_text: Color32,
265 pub trigger_placeholder: Color32,
266 pub trigger_icon: Color32,
267 pub trigger_rounding: CornerRadius,
268
269 pub focus_ring_color: Color32,
270 pub focus_ring_width: f32,
271
272 pub invalid_border: Color32,
273 pub invalid_ring: Color32,
274
275 pub disabled_opacity: f32,
276
277 pub content_bg: Color32,
278 pub content_border: Color32,
279 pub content_rounding: CornerRadius,
280 pub content_shadow: Color32,
281 pub content_padding: f32,
282
283 pub item_bg: Color32,
284 pub item_bg_hover: Color32,
285 pub item_bg_selected: Color32,
286 pub item_text: Color32,
287 pub item_text_hover: Color32,
288 pub item_rounding: CornerRadius,
289 pub item_padding: Vec2,
290 pub item_icon_color: Color32,
291
292 pub item_solid_bg_hover: Color32,
293 pub item_solid_text_hover: Color32,
294 pub item_solid_high_contrast_bg: Color32,
295 pub item_solid_high_contrast_text: Color32,
296
297 pub label_text: Color32,
298
299 pub separator_color: Color32,
300
301 pub scroll_button_color: Color32,
302}
303
304impl SelectStyle {
305 fn base_from_palette(palette: &ColorPalette) -> Self {
306 Self {
307 trigger_bg: Color32::from_rgba_unmultiplied(
308 palette.input.r(),
309 palette.input.g(),
310 palette.input.b(),
311 77,
312 ),
313 trigger_bg_hover: Color32::from_rgba_unmultiplied(
314 palette.input.r(),
315 palette.input.g(),
316 palette.input.b(),
317 128,
318 ),
319 trigger_border: palette.input,
320 trigger_text: palette.foreground,
321 trigger_placeholder: palette.muted_foreground,
322 trigger_icon: palette.muted_foreground,
323 trigger_rounding: CornerRadius::same(6),
324
325 focus_ring_color: Color32::from_rgba_unmultiplied(
326 palette.ring.r(),
327 palette.ring.g(),
328 palette.ring.b(),
329 128,
330 ),
331 focus_ring_width: 3.0,
332
333 invalid_border: palette.destructive,
334 invalid_ring: Color32::from_rgba_unmultiplied(
335 palette.destructive.r(),
336 palette.destructive.g(),
337 palette.destructive.b(),
338 102,
339 ),
340
341 disabled_opacity: 0.5,
342
343 content_bg: palette.popover,
344 content_border: palette.border,
345 content_rounding: CornerRadius::same(6),
346 content_shadow: Color32::from_rgba_unmultiplied(
347 palette.foreground.r(),
348 palette.foreground.g(),
349 palette.foreground.b(),
350 40,
351 ),
352 content_padding: 4.0,
353
354 item_bg: Color32::TRANSPARENT,
355 item_bg_hover: palette.accent,
356 item_bg_selected: palette.accent,
357 item_text: palette.popover_foreground,
358 item_text_hover: palette.accent_foreground,
359 item_rounding: CornerRadius::same(3),
360 item_padding: vec2(8.0, 6.0),
361 item_icon_color: palette.muted_foreground,
362
363 item_solid_bg_hover: palette.primary,
364 item_solid_text_hover: palette.primary_foreground,
365
366 item_solid_high_contrast_bg: palette.foreground,
367 item_solid_high_contrast_text: palette.background,
368
369 label_text: palette.muted_foreground,
370
371 separator_color: palette.border,
372
373 scroll_button_color: palette.muted_foreground,
374 }
375 }
376
377 fn accent(mut self, palette: &ColorPalette, accent: Color32) -> Self {
378 let accent_tint_soft =
379 Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 42);
380 let accent_tint_hover =
381 Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 56);
382 let accent_border =
383 Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 160);
384
385 self.trigger_bg = accent_tint_soft;
386 self.trigger_bg_hover = accent_tint_hover;
387 self.trigger_border = accent_border;
388 self.trigger_text = accent;
389 self.trigger_placeholder = mix(accent, palette.muted_foreground, 0.35);
390 self.trigger_icon = accent;
391 self.focus_ring_color =
392 Color32::from_rgba_unmultiplied(accent.r(), accent.g(), accent.b(), 128);
393
394 self.content_bg = mix(palette.input, accent, 0.15);
395 self.content_border = accent_border;
396 self.item_bg_hover = mix(accent, Color32::WHITE, 0.12);
397 self.item_bg_selected = mix(accent, palette.background, 0.15);
398 self.item_text_hover = palette.foreground;
399 self.item_icon_color = mix(accent, palette.foreground, 0.15);
400 self.item_solid_bg_hover = accent;
401 self.item_solid_text_hover = palette.primary_foreground;
402
403 self.separator_color = mix(accent, palette.border, 0.25);
404 self.scroll_button_color = mix(accent, palette.muted_foreground, 0.2);
405
406 self
407 }
408
409 fn trigger_variant(
410 mut self,
411 variant: TriggerVariant,
412 palette: &ColorPalette,
413 accent: Color32,
414 ) -> Self {
415 match variant {
416 TriggerVariant::Surface => {}
417 TriggerVariant::Classic => {
418 let bg = mix(palette.input, palette.background, 0.1);
419 self.trigger_bg = bg;
420 self.trigger_bg_hover = mix(bg, palette.foreground, 0.08);
421 self.trigger_border = mix(palette.border, palette.foreground, 0.25);
422 self.trigger_text = palette.foreground;
423 self.focus_ring_color = mix(palette.primary, palette.foreground, 0.35);
424 }
425 TriggerVariant::Soft => {
426 let tint = mix(accent, palette.background, 0.85);
427 self.trigger_bg = tint;
428 self.trigger_bg_hover = mix(tint, accent, 0.22);
429 self.trigger_border = Color32::TRANSPARENT;
430 self.trigger_text = accent;
431 self.trigger_placeholder = mix(accent, palette.muted_foreground, 0.4);
432 self.trigger_icon = accent;
433 self.focus_ring_color = mix(accent, palette.foreground, 0.35);
434 }
435 TriggerVariant::Ghost => {
436 self.trigger_bg = Color32::TRANSPARENT;
437 self.trigger_bg_hover = mix(palette.muted, palette.background, 0.5);
438 self.trigger_border = Color32::TRANSPARENT;
439 self.trigger_text = mix(accent, palette.foreground, 0.6);
440 self.trigger_placeholder = mix(self.trigger_text, palette.muted_foreground, 0.5);
441 self.trigger_icon = self.trigger_text;
442 self.focus_ring_color = mix(accent, palette.foreground, 0.4);
443 }
444 }
445 self
446 }
447
448 fn content_variant(
449 mut self,
450 variant: ContentVariant,
451 palette: &ColorPalette,
452 accent: Color32,
453 ) -> Self {
454 match variant {
455 ContentVariant::Soft => {
456 let tinted = mix(self.item_bg_hover, accent, 0.25);
457 self.item_bg_selected =
458 Color32::from_rgba_unmultiplied(tinted.r(), tinted.g(), tinted.b(), 80);
459 }
460 ContentVariant::Solid => {
461 self.content_bg = mix(palette.input, accent, 0.12);
462 self.content_border = mix(palette.border, accent, 0.25);
463 self.item_bg_hover = self.item_solid_bg_hover;
464 let solid_selected = mix(self.item_solid_bg_hover, accent, 0.2);
465 self.item_bg_selected = Color32::from_rgba_unmultiplied(
466 solid_selected.r(),
467 solid_selected.g(),
468 solid_selected.b(),
469 200,
470 );
471 self.item_text_hover = self.item_solid_text_hover;
472 }
473 }
474 self
475 }
476
477 pub fn from_palette(palette: &ColorPalette) -> Self {
478 Self::from_palette_for_variants(
479 palette,
480 TriggerVariant::Surface,
481 ContentVariant::Soft,
482 None,
483 )
484 }
485
486 pub fn from_palette_for_variants(
487 palette: &ColorPalette,
488 trigger_variant: TriggerVariant,
489 content_variant: ContentVariant,
490 accent: Option<Color32>,
491 ) -> Self {
492 let mut style = Self::base_from_palette(palette);
493 let effective_accent = accent.unwrap_or(palette.accent);
494 if accent.is_some() {
495 style = style.accent(palette, effective_accent);
496 }
497 style = style.trigger_variant(trigger_variant, palette, effective_accent);
498 style.content_variant(content_variant, palette, effective_accent)
499 }
500
501 pub fn from_palette_with_accent(palette: &ColorPalette, accent: Color32) -> Self {
502 Self::from_palette_for_variants(
503 palette,
504 TriggerVariant::Surface,
505 ContentVariant::Soft,
506 Some(accent),
507 )
508 }
509
510 pub fn high_contrast(mut self, palette: &ColorPalette) -> Self {
511 self.trigger_bg = mix(self.trigger_bg, palette.foreground, 0.08);
512 self.trigger_bg_hover = mix(self.trigger_bg_hover, palette.foreground, 0.12);
513 self.trigger_text = palette.foreground;
514 self.trigger_icon = palette.foreground;
515 self.content_bg = mix(self.content_bg, palette.foreground, 0.06);
516 self.content_border = mix(self.content_border, palette.foreground, 0.2);
517 self.item_bg_hover = mix(self.item_bg_hover, palette.foreground, 0.1);
518 self.item_bg_selected = mix(self.item_bg_selected, palette.foreground, 0.15);
519 self.item_text_hover = palette.foreground;
520 self
521 }
522}
523
524impl Default for SelectStyle {
525 fn default() -> Self {
526 Self::from_palette(&ColorPalette::default())
527 }
528}
529
530#[derive(Clone, Debug)]
531pub enum SelectItem {
532 Option {
533 value: String,
534 label: String,
535 disabled: bool,
536 text_value: Option<String>,
537 },
538
539 Group {
540 label: String,
541 items: Vec<SelectItem>,
542 },
543
544 Separator,
545
546 Label(String),
547}
548
549impl SelectItem {
550 pub fn option(value: impl Into<String>, label: impl Into<String>) -> Self {
551 Self::Option {
552 value: value.into(),
553 label: label.into(),
554 disabled: false,
555 text_value: None,
556 }
557 }
558
559 pub fn option_disabled(value: impl Into<String>, label: impl Into<String>) -> Self {
560 Self::Option {
561 value: value.into(),
562 label: label.into(),
563 disabled: true,
564 text_value: None,
565 }
566 }
567
568 pub fn option_with_text_value(
569 value: impl Into<String>,
570 label: impl Into<String>,
571 text_value: impl Into<String>,
572 ) -> Self {
573 Self::Option {
574 value: value.into(),
575 label: label.into(),
576 disabled: false,
577 text_value: Some(text_value.into()),
578 }
579 }
580
581 pub fn option_disabled_with_text_value(
582 value: impl Into<String>,
583 label: impl Into<String>,
584 text_value: impl Into<String>,
585 ) -> Self {
586 Self::Option {
587 value: value.into(),
588 label: label.into(),
589 disabled: true,
590 text_value: Some(text_value.into()),
591 }
592 }
593
594 pub fn group(label: impl Into<String>, items: Vec<SelectItem>) -> Self {
595 Self::Group {
596 label: label.into(),
597 items,
598 }
599 }
600
601 pub fn separator() -> Self {
602 Self::Separator
603 }
604
605 pub fn label(text: impl Into<String>) -> Self {
606 Self::Label(text.into())
607 }
608}
609
610pub struct SelectProps<'a, Id>
611where
612 Id: Hash + Debug,
613{
614 pub id_source: Id,
615
616 pub selected: &'a mut Option<String>,
617 pub value: Option<String>,
618 pub default_value: Option<String>,
619 pub on_value_change: Option<&'a mut dyn FnMut(&str)>,
620
621 pub placeholder: &'a str,
622
623 pub size: SelectSize,
624
625 pub trigger_variant: TriggerVariant,
626
627 pub content_variant: ContentVariant,
628
629 pub enabled: bool,
630
631 pub open: Option<bool>,
632 pub default_open: bool,
633 pub on_open_change: Option<&'a mut dyn FnMut(bool)>,
634
635 pub dir: Option<SelectDirection>,
636 pub name: Option<String>,
637 pub auto_complete: Option<String>,
638 pub required: bool,
639 pub form: Option<String>,
640
641 pub side: SelectSide,
642 pub side_offset: f32,
643 pub align: SelectAlign,
644 pub align_offset: f32,
645
646 pub avoid_collisions: bool,
647 pub collision_boundary: Option<Rect>,
648 pub collision_padding: SelectCollisionPadding,
649 pub arrow_padding: f32,
650 pub sticky: SelectSticky,
651 pub hide_when_detached: bool,
652 pub update_position_strategy: SelectUpdatePositionStrategy,
653 pub container: Option<SelectPortalContainer>,
654
655 pub on_close_auto_focus: Option<&'a mut dyn FnMut(&mut SelectAutoFocusEvent)>,
656 pub on_escape_key_down: Option<&'a mut dyn FnMut(&mut SelectEscapeKeyDownEvent)>,
657 pub on_pointer_down_outside: Option<&'a mut dyn FnMut(&mut SelectPointerDownOutsideEvent)>,
658
659 pub is_invalid: bool,
660
661 pub width: Option<f32>,
662
663 pub style: Option<SelectStyle>,
664
665 pub accent_color: Option<Color32>,
666
667 pub radius: SelectRadius,
668
669 pub high_contrast: bool,
670
671 pub position: PopupPosition,
672}
673
674impl<Id> std::fmt::Debug for SelectProps<'_, Id>
675where
676 Id: Hash + Debug,
677{
678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 f.debug_struct("SelectProps")
680 .field("id_source", &self.id_source)
681 .field("selected", &self.selected)
682 .field("value", &self.value)
683 .field("default_value", &self.default_value)
684 .field("placeholder", &self.placeholder)
685 .field("size", &self.size)
686 .field("trigger_variant", &self.trigger_variant)
687 .field("content_variant", &self.content_variant)
688 .field("enabled", &self.enabled)
689 .field("open", &self.open)
690 .field("default_open", &self.default_open)
691 .field("dir", &self.dir)
692 .field("name", &self.name)
693 .field("auto_complete", &self.auto_complete)
694 .field("required", &self.required)
695 .field("form", &self.form)
696 .field("side", &self.side)
697 .field("side_offset", &self.side_offset)
698 .field("align", &self.align)
699 .field("align_offset", &self.align_offset)
700 .field("avoid_collisions", &self.avoid_collisions)
701 .field("collision_boundary", &self.collision_boundary)
702 .field("collision_padding", &self.collision_padding)
703 .field("arrow_padding", &self.arrow_padding)
704 .field("sticky", &self.sticky)
705 .field("hide_when_detached", &self.hide_when_detached)
706 .field("update_position_strategy", &self.update_position_strategy)
707 .field("container", &self.container)
708 .field("is_invalid", &self.is_invalid)
709 .field("width", &self.width)
710 .field("style", &self.style.is_some())
711 .field("accent_color", &self.accent_color)
712 .field("radius", &self.radius)
713 .field("high_contrast", &self.high_contrast)
714 .field("position", &self.position)
715 .field("on_open_change", &self.on_open_change.is_some())
716 .field("on_value_change", &self.on_value_change.is_some())
717 .field("on_close_auto_focus", &self.on_close_auto_focus.is_some())
718 .field("on_escape_key_down", &self.on_escape_key_down.is_some())
719 .field(
720 "on_pointer_down_outside",
721 &self.on_pointer_down_outside.is_some(),
722 )
723 .finish()
724 }
725}
726
727impl<'a, Id: Hash + Debug> SelectProps<'a, Id> {
728 pub fn new(id_source: Id, selected: &'a mut Option<String>) -> Self {
729 Self {
730 id_source,
731 selected,
732 value: None,
733 default_value: None,
734 on_value_change: None,
735 placeholder: "Select...",
736 size: SelectSize::Size2,
737 trigger_variant: TriggerVariant::Surface,
738 content_variant: ContentVariant::Soft,
739 enabled: true,
740 open: None,
741 default_open: false,
742 on_open_change: None,
743 dir: None,
744 name: None,
745 auto_complete: None,
746 required: false,
747 form: None,
748 side: SelectSide::Bottom,
749 side_offset: 4.0,
750 align: SelectAlign::Start,
751 align_offset: 0.0,
752 avoid_collisions: true,
753 collision_boundary: None,
754 collision_padding: SelectCollisionPadding::default(),
755 arrow_padding: 0.0,
756 sticky: SelectSticky::default(),
757 hide_when_detached: false,
758 update_position_strategy: SelectUpdatePositionStrategy::default(),
759 container: None,
760 on_close_auto_focus: None,
761 on_escape_key_down: None,
762 on_pointer_down_outside: None,
763 is_invalid: false,
764 width: None,
765 style: None,
766 accent_color: None,
767 radius: SelectRadius::Medium,
768 high_contrast: false,
769 position: PopupPosition::ItemAligned,
770 }
771 }
772
773 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
774 self.placeholder = placeholder;
775 self
776 }
777
778 pub fn size(mut self, size: SelectSize) -> Self {
779 self.size = size;
780 self
781 }
782
783 pub fn trigger_variant(mut self, variant: TriggerVariant) -> Self {
784 self.trigger_variant = variant;
785 self
786 }
787
788 pub fn content_variant(mut self, variant: ContentVariant) -> Self {
789 self.content_variant = variant;
790 self
791 }
792
793 pub fn enabled(mut self, enabled: bool) -> Self {
794 self.enabled = enabled;
795 self
796 }
797
798 pub fn disabled(mut self, disabled: bool) -> Self {
799 self.enabled = !disabled;
800 self
801 }
802
803 pub fn invalid(mut self, is_invalid: bool) -> Self {
804 self.is_invalid = is_invalid;
805 self
806 }
807
808 pub fn open(mut self, open: bool) -> Self {
809 self.open = Some(open);
810 self
811 }
812
813 pub fn default_open(mut self, default_open: bool) -> Self {
814 self.default_open = default_open;
815 self
816 }
817
818 pub fn on_open_change(mut self, on_open_change: &'a mut dyn FnMut(bool)) -> Self {
819 self.on_open_change = Some(on_open_change);
820 self
821 }
822
823 pub fn value(mut self, value: impl Into<String>) -> Self {
824 self.value = Some(value.into());
825 self
826 }
827
828 pub fn default_value(mut self, default_value: impl Into<String>) -> Self {
829 self.default_value = Some(default_value.into());
830 self
831 }
832
833 pub fn on_value_change(mut self, on_value_change: &'a mut dyn FnMut(&str)) -> Self {
834 self.on_value_change = Some(on_value_change);
835 self
836 }
837
838 pub fn dir(mut self, dir: SelectDirection) -> Self {
839 self.dir = Some(dir);
840 self
841 }
842
843 pub fn name(mut self, name: impl Into<String>) -> Self {
844 self.name = Some(name.into());
845 self
846 }
847
848 pub fn auto_complete(mut self, auto_complete: impl Into<String>) -> Self {
849 self.auto_complete = Some(auto_complete.into());
850 self
851 }
852
853 pub fn required(mut self, required: bool) -> Self {
854 self.required = required;
855 self
856 }
857
858 pub fn form(mut self, form: impl Into<String>) -> Self {
859 self.form = Some(form.into());
860 self
861 }
862
863 pub fn side(mut self, side: SelectSide) -> Self {
864 self.side = side;
865 self
866 }
867
868 pub fn side_offset(mut self, side_offset: f32) -> Self {
869 self.side_offset = side_offset;
870 self
871 }
872
873 pub fn align(mut self, align: SelectAlign) -> Self {
874 self.align = align;
875 self
876 }
877
878 pub fn align_offset(mut self, align_offset: f32) -> Self {
879 self.align_offset = align_offset;
880 self
881 }
882
883 pub fn avoid_collisions(mut self, avoid_collisions: bool) -> Self {
884 self.avoid_collisions = avoid_collisions;
885 self
886 }
887
888 pub fn collision_boundary(mut self, boundary: Rect) -> Self {
889 self.collision_boundary = Some(boundary);
890 self
891 }
892
893 pub fn collision_padding(mut self, padding: impl Into<SelectCollisionPadding>) -> Self {
894 self.collision_padding = padding.into();
895 self
896 }
897
898 pub fn arrow_padding(mut self, arrow_padding: f32) -> Self {
899 self.arrow_padding = arrow_padding;
900 self
901 }
902
903 pub fn sticky(mut self, sticky: SelectSticky) -> Self {
904 self.sticky = sticky;
905 self
906 }
907
908 pub fn hide_when_detached(mut self, hide_when_detached: bool) -> Self {
909 self.hide_when_detached = hide_when_detached;
910 self
911 }
912
913 pub fn update_position_strategy(
914 mut self,
915 update_position_strategy: SelectUpdatePositionStrategy,
916 ) -> Self {
917 self.update_position_strategy = update_position_strategy;
918 self
919 }
920
921 pub fn container(mut self, container: SelectPortalContainer) -> Self {
922 self.container = Some(container);
923 self
924 }
925
926 pub fn on_close_auto_focus(
927 mut self,
928 on_close_auto_focus: &'a mut dyn FnMut(&mut SelectAutoFocusEvent),
929 ) -> Self {
930 self.on_close_auto_focus = Some(on_close_auto_focus);
931 self
932 }
933
934 pub fn on_escape_key_down(
935 mut self,
936 on_escape_key_down: &'a mut dyn FnMut(&mut SelectEscapeKeyDownEvent),
937 ) -> Self {
938 self.on_escape_key_down = Some(on_escape_key_down);
939 self
940 }
941
942 pub fn on_pointer_down_outside(
943 mut self,
944 on_pointer_down_outside: &'a mut dyn FnMut(&mut SelectPointerDownOutsideEvent),
945 ) -> Self {
946 self.on_pointer_down_outside = Some(on_pointer_down_outside);
947 self
948 }
949
950 pub fn width(mut self, width: f32) -> Self {
951 self.width = Some(width);
952 self
953 }
954
955 pub fn style(mut self, style: SelectStyle) -> Self {
956 self.style = Some(style);
957 self
958 }
959
960 pub fn accent_color(mut self, color: Color32) -> Self {
961 self.accent_color = Some(color);
962 self
963 }
964
965 pub fn radius(mut self, radius: SelectRadius) -> Self {
966 self.radius = radius;
967 self
968 }
969
970 pub fn high_contrast(mut self, high_contrast: bool) -> Self {
971 self.high_contrast = high_contrast;
972 self
973 }
974
975 pub fn position(mut self, position: PopupPosition) -> Self {
976 self.position = position;
977 self
978 }
979}
980
981#[derive(Debug)]
982pub struct SelectPropsSimple<'a, Id>
983where
984 Id: Hash + Debug,
985{
986 pub id_source: Id,
987 pub selected: &'a mut Option<String>,
988 pub options: &'a [String],
989 pub placeholder: &'a str,
990 pub size: ControlSize,
991 pub enabled: bool,
992 pub is_invalid: bool,
993}
994
995#[derive(Clone, Debug, Default)]
996struct SelectState {
997 is_open: bool,
998
999 focused_index: Option<usize>,
1000
1001 scroll_offset: f32,
1002
1003 show_scroll_up: bool,
1004
1005 show_scroll_down: bool,
1006
1007 typed_buffer: String,
1008
1009 last_type_time: f64,
1010}
1011
1012fn draw_chevron_down(painter: &Painter, center: Pos2, size: f32, color: Color32) {
1013 let half = size * 0.35;
1014 let stroke = Stroke::new(1.5, color);
1015
1016 painter.line_segment(
1017 [
1018 pos2(center.x - half, center.y - half * 0.5),
1019 pos2(center.x, center.y + half * 0.5),
1020 ],
1021 stroke,
1022 );
1023 painter.line_segment(
1024 [
1025 pos2(center.x, center.y + half * 0.5),
1026 pos2(center.x + half, center.y - half * 0.5),
1027 ],
1028 stroke,
1029 );
1030}
1031
1032fn draw_chevron_up(painter: &Painter, center: Pos2, size: f32, color: Color32) {
1033 let half = size * 0.35;
1034 let stroke = Stroke::new(1.5, color);
1035
1036 painter.line_segment(
1037 [
1038 pos2(center.x - half, center.y + half * 0.5),
1039 pos2(center.x, center.y - half * 0.5),
1040 ],
1041 stroke,
1042 );
1043 painter.line_segment(
1044 [
1045 pos2(center.x, center.y - half * 0.5),
1046 pos2(center.x + half, center.y + half * 0.5),
1047 ],
1048 stroke,
1049 );
1050}
1051
1052fn draw_check_icon(painter: &Painter, center: Pos2, size: f32, color: Color32) {
1053 let stroke = Stroke::new(2.0, color);
1054
1055 let s = size * 0.4;
1056 painter.line_segment(
1057 [
1058 pos2(center.x - s * 0.6, center.y),
1059 pos2(center.x - s * 0.1, center.y + s * 0.5),
1060 ],
1061 stroke,
1062 );
1063 painter.line_segment(
1064 [
1065 pos2(center.x - s * 0.1, center.y + s * 0.5),
1066 pos2(center.x + s * 0.6, center.y - s * 0.4),
1067 ],
1068 stroke,
1069 );
1070}
1071
1072#[allow(clippy::too_many_arguments)]
1073fn compute_select_popup_rect(
1074 trigger_rect: Rect,
1075 popup_size: Vec2,
1076 boundary: Rect,
1077 side: SelectSide,
1078 align: SelectAlign,
1079 side_offset: f32,
1080 align_offset: f32,
1081 avoid_collisions: bool,
1082 collision_padding: SelectCollisionPadding,
1083) -> Rect {
1084 let boundary = Rect::from_min_max(
1085 pos2(
1086 boundary.left() + collision_padding.left,
1087 boundary.top() + collision_padding.top,
1088 ),
1089 pos2(
1090 boundary.right() - collision_padding.right,
1091 boundary.bottom() - collision_padding.bottom,
1092 ),
1093 );
1094
1095 let (left, top) = match side {
1096 SelectSide::Bottom => {
1097 let top = trigger_rect.bottom() + side_offset;
1098 let left = match align {
1099 SelectAlign::Start => trigger_rect.left(),
1100 SelectAlign::Center => trigger_rect.center().x - popup_size.x * 0.5,
1101 SelectAlign::End => trigger_rect.right() - popup_size.x,
1102 } + align_offset;
1103 (left, top)
1104 }
1105 SelectSide::Top => {
1106 let top = trigger_rect.top() - side_offset - popup_size.y;
1107 let left = match align {
1108 SelectAlign::Start => trigger_rect.left(),
1109 SelectAlign::Center => trigger_rect.center().x - popup_size.x * 0.5,
1110 SelectAlign::End => trigger_rect.right() - popup_size.x,
1111 } + align_offset;
1112 (left, top)
1113 }
1114 SelectSide::Right => {
1115 let left = trigger_rect.right() + side_offset;
1116 let top = match align {
1117 SelectAlign::Start => trigger_rect.top(),
1118 SelectAlign::Center => trigger_rect.center().y - popup_size.y * 0.5,
1119 SelectAlign::End => trigger_rect.bottom() - popup_size.y,
1120 } + align_offset;
1121 (left, top)
1122 }
1123 SelectSide::Left => {
1124 let left = trigger_rect.left() - side_offset - popup_size.x;
1125 let top = match align {
1126 SelectAlign::Start => trigger_rect.top(),
1127 SelectAlign::Center => trigger_rect.center().y - popup_size.y * 0.5,
1128 SelectAlign::End => trigger_rect.bottom() - popup_size.y,
1129 } + align_offset;
1130 (left, top)
1131 }
1132 };
1133
1134 let mut rect = Rect::from_min_size(pos2(left, top), popup_size);
1135
1136 if avoid_collisions {
1137 let mut translation = vec2(0.0, 0.0);
1138 if rect.left() < boundary.left() {
1139 translation.x = boundary.left() - rect.left();
1140 } else if rect.right() > boundary.right() {
1141 translation.x = boundary.right() - rect.right();
1142 }
1143
1144 if rect.top() < boundary.top() {
1145 translation.y = boundary.top() - rect.top();
1146 } else if rect.bottom() > boundary.bottom() {
1147 translation.y = boundary.bottom() - rect.bottom();
1148 }
1149
1150 rect = rect.translate(translation);
1151 rect.set_height(rect.height().min(boundary.height()));
1152 }
1153
1154 rect
1155}
1156
1157pub fn select_with_items<Id>(
1158 ui: &mut Ui,
1159 theme: &Theme,
1160 mut props: SelectProps<'_, Id>,
1161 items: &[SelectItem],
1162) -> Response
1163where
1164 Id: Hash + Debug,
1165{
1166 let style = props.style.clone().unwrap_or_else(|| {
1167 SelectStyle::from_palette_for_variants(
1168 &theme.palette,
1169 props.trigger_variant,
1170 props.content_variant,
1171 props.accent_color,
1172 )
1173 });
1174 let style = if props.high_contrast {
1175 style.high_contrast(&theme.palette)
1176 } else {
1177 style
1178 };
1179 let id = ui.make_persistent_id(&props.id_source);
1180
1181 trace!(
1182 "Rendering select size={:?} enabled={} items={}",
1183 props.size,
1184 props.enabled,
1185 items.len()
1186 );
1187
1188 let mut state = ui
1189 .ctx()
1190 .data_mut(|d| d.get_temp::<SelectState>(id).unwrap_or_default());
1191
1192 let default_value_init_key = id.with("default-value-initialized");
1193 let default_value_initialized = ui
1194 .ctx()
1195 .data(|d| d.get_temp::<bool>(default_value_init_key))
1196 .unwrap_or(false);
1197 if !default_value_initialized {
1198 if props.value.is_none()
1199 && props.selected.is_none()
1200 && let Some(default_value) = props.default_value.as_ref()
1201 {
1202 *props.selected = Some(default_value.clone());
1203 }
1204 ui.ctx()
1205 .data_mut(|d| d.insert_temp(default_value_init_key, true));
1206 }
1207
1208 let default_open_init_key = id.with("default-open-initialized");
1209 let default_open_initialized = ui
1210 .ctx()
1211 .data(|d| d.get_temp::<bool>(default_open_init_key))
1212 .unwrap_or(false);
1213 if !default_open_initialized {
1214 if props.open.is_none() && props.default_open {
1215 state.is_open = true;
1216 }
1217 ui.ctx()
1218 .data_mut(|d| d.insert_temp(default_open_init_key, true));
1219 }
1220
1221 if let Some(controlled_open) = props.open {
1222 state.is_open = controlled_open;
1223 }
1224
1225 let trigger_height = props.size.trigger_height();
1226 let trigger_width = props.width.unwrap_or(180.0);
1227 let icon_size = props.size.icon_size();
1228
1229 let desired_size = vec2(trigger_width, trigger_height);
1230 let (trigger_rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
1231
1232 if response.clicked() && props.enabled {
1233 let next_open = !state.is_open;
1234 if props.open.is_some() {
1235 if let Some(cb) = props.on_open_change.as_mut() {
1236 cb(next_open);
1237 }
1238 } else {
1239 state.is_open = next_open;
1240 if let Some(cb) = props.on_open_change.as_mut() {
1241 cb(next_open);
1242 }
1243 }
1244
1245 response.request_focus();
1246
1247 if next_open {
1248 let item_height = style.item_padding.y * 2.0 + props.size.font_size();
1249 let separator_height = 9.0;
1250 let label_height = style.item_padding.y * 2.0 + 12.0;
1251
1252 if let Some(selected_value) = props.value.as_deref().or(props.selected.as_deref()) {
1253 let flat_options = flatten_options(items);
1254 state.focused_index = flat_options
1255 .iter()
1256 .position(|(value, _, _)| value == selected_value);
1257
1258 if let Some((offset, _item_h)) = calculate_selected_offset(
1259 items,
1260 selected_value,
1261 item_height,
1262 separator_height,
1263 label_height,
1264 ) {
1265 let visible_height = 300.0 - style.content_padding * 2.0 - 36.0;
1266 state.scroll_offset =
1267 (offset - visible_height / 2.0 + item_height / 2.0).max(0.0);
1268 } else {
1269 state.scroll_offset = 0.0;
1270 }
1271 } else {
1272 state.focused_index = None;
1273 state.scroll_offset = 0.0;
1274 }
1275 }
1276 response.mark_changed();
1277 }
1278
1279 if response.has_focus() && props.enabled {
1280 let input = ui.input(|i| {
1281 (
1282 i.key_pressed(Key::Enter) || i.key_pressed(Key::Space),
1283 i.key_pressed(Key::Escape),
1284 i.key_pressed(Key::ArrowDown),
1285 i.key_pressed(Key::ArrowUp),
1286 )
1287 });
1288
1289 if input.0 && !state.is_open {
1290 if props.open.is_some() {
1291 if let Some(cb) = props.on_open_change.as_mut() {
1292 cb(true);
1293 }
1294 } else {
1295 state.is_open = true;
1296 state.focused_index = None;
1297 if let Some(cb) = props.on_open_change.as_mut() {
1298 cb(true);
1299 }
1300 }
1301 } else if input.1 && state.is_open {
1302 let mut evt = SelectEscapeKeyDownEvent {
1303 key: Key::Escape,
1304 preventable: SelectPreventable::default(),
1305 };
1306 if let Some(cb) = props.on_escape_key_down.as_mut() {
1307 cb(&mut evt);
1308 }
1309 if !evt.preventable.default_prevented() {
1310 if props.open.is_some() {
1311 if let Some(cb) = props.on_open_change.as_mut() {
1312 cb(false);
1313 }
1314 } else {
1315 state.is_open = false;
1316 if let Some(cb) = props.on_open_change.as_mut() {
1317 cb(false);
1318 }
1319 }
1320
1321 let mut auto_focus = SelectAutoFocusEvent {
1322 preventable: SelectPreventable::default(),
1323 };
1324 if let Some(cb) = props.on_close_auto_focus.as_mut() {
1325 cb(&mut auto_focus);
1326 }
1327 if !auto_focus.preventable.default_prevented() {
1328 response.request_focus();
1329 }
1330 }
1331 }
1332 }
1333
1334 let anim_t = ui.ctx().animate_bool_with_time_and_easing(
1335 id.with("open"),
1336 state.is_open,
1337 theme.motion.base_ms / 1000.0,
1338 crate::tokens::ease_out_cubic,
1339 );
1340
1341 let painter = ui.painter();
1342
1343 let bg_color = if !props.enabled {
1344 mix(
1345 style.trigger_bg,
1346 Color32::TRANSPARENT,
1347 style.disabled_opacity,
1348 )
1349 } else if response.hovered() && !state.is_open {
1350 style.trigger_bg_hover
1351 } else {
1352 style.trigger_bg
1353 };
1354
1355 let border_color = if props.is_invalid {
1356 style.invalid_border
1357 } else if state.is_open {
1358 style.focus_ring_color
1359 } else {
1360 style.trigger_border
1361 };
1362
1363 painter.rect_filled(trigger_rect, style.trigger_rounding, bg_color);
1364 painter.rect_stroke(
1365 trigger_rect,
1366 style.trigger_rounding,
1367 Stroke::new(1.0, border_color),
1368 StrokeKind::Inside,
1369 );
1370
1371 if state.is_open && props.enabled {
1372 let ring_rect = trigger_rect.expand(style.focus_ring_width * 0.5);
1373 let ring_color = if props.is_invalid {
1374 style.invalid_ring
1375 } else {
1376 style.focus_ring_color
1377 };
1378 painter.rect_stroke(
1379 ring_rect,
1380 style.trigger_rounding,
1381 Stroke::new(style.focus_ring_width, ring_color),
1382 StrokeKind::Outside,
1383 );
1384 }
1385
1386 let text_rect = trigger_rect.shrink2(vec2(style.content_padding * 3.0, 0.0));
1387 let current_value_for_display = props.value.clone().or_else(|| props.selected.clone());
1388 let text_color = if !props.enabled {
1389 mix(
1390 style.trigger_text,
1391 Color32::TRANSPARENT,
1392 style.disabled_opacity,
1393 )
1394 } else if current_value_for_display.is_some() {
1395 style.trigger_text
1396 } else {
1397 style.trigger_placeholder
1398 };
1399
1400 let display_text = if let Some(selected_value) = current_value_for_display.as_ref() {
1401 find_label_for_value(items, selected_value).unwrap_or_else(|| selected_value.clone())
1402 } else {
1403 props.placeholder.to_string()
1404 };
1405
1406 let galley = painter.layout_no_wrap(
1407 display_text,
1408 FontId::proportional(props.size.font_size()),
1409 text_color,
1410 );
1411 let text_pos = pos2(
1412 text_rect.left(),
1413 trigger_rect.center().y - galley.size().y * 0.5,
1414 );
1415 painter.galley(text_pos, galley, Color32::TRANSPARENT);
1416
1417 let icon_center = pos2(
1418 trigger_rect.right() - icon_size * 0.75 - style.content_padding,
1419 trigger_rect.center().y,
1420 );
1421 let icon_color = if !props.enabled {
1422 mix(
1423 style.trigger_icon,
1424 Color32::TRANSPARENT,
1425 style.disabled_opacity,
1426 )
1427 } else {
1428 style.trigger_icon
1429 };
1430 draw_chevron_down(painter, icon_center, icon_size, icon_color);
1431
1432 if anim_t > 0.0 {
1433 let popup_id = id.with("popup");
1434 let layer_order = props
1435 .container
1436 .unwrap_or(SelectPortalContainer::Foreground)
1437 .order();
1438 let layer_id = LayerId::new(layer_order, popup_id);
1439
1440 let flat_options = flatten_options(items);
1441
1442 let item_height = style.item_padding.y * 2.0 + props.size.font_size();
1443 let separator_height = 9.0;
1444 let label_height = style.item_padding.y * 2.0 + 12.0;
1445
1446 let content_height =
1447 calculate_content_height(items, item_height, separator_height, label_height);
1448 let max_popup_height = 300.0;
1449 let popup_height = content_height.min(max_popup_height) + style.content_padding * 2.0;
1450 let popup_width = trigger_width.max(128.0);
1451
1452 let boundary = props
1453 .collision_boundary
1454 .unwrap_or_else(|| ui.ctx().available_rect());
1455 let popup_rect = compute_select_popup_rect(
1456 trigger_rect,
1457 vec2(popup_width, popup_height),
1458 boundary,
1459 props.side,
1460 props.align,
1461 props.side_offset,
1462 props.align_offset,
1463 props.avoid_collisions,
1464 props.collision_padding,
1465 );
1466
1467 let scale = 0.95 + 0.05 * anim_t;
1468 let alpha = (anim_t * 255.0) as u8;
1469
1470 let animated_rect = Rect::from_center_size(popup_rect.center(), popup_rect.size() * scale);
1471
1472 let pointer_pos = ui.input(|i| i.pointer.interact_pos());
1473 if let Some(pos) = pointer_pos
1474 && state.is_open
1475 && ui.input(|i| i.pointer.any_click())
1476 && !animated_rect.contains(pos)
1477 && !trigger_rect.contains(pos)
1478 {
1479 let mut evt = SelectPointerDownOutsideEvent {
1480 pointer_pos: Some(pos),
1481 preventable: SelectPreventable::default(),
1482 };
1483 if let Some(cb) = props.on_pointer_down_outside.as_mut() {
1484 cb(&mut evt);
1485 }
1486 if !evt.preventable.default_prevented() {
1487 if props.open.is_some() {
1488 if let Some(cb) = props.on_open_change.as_mut() {
1489 cb(false);
1490 }
1491 } else {
1492 state.is_open = false;
1493 if let Some(cb) = props.on_open_change.as_mut() {
1494 cb(false);
1495 }
1496 }
1497
1498 let mut auto_focus = SelectAutoFocusEvent {
1499 preventable: SelectPreventable::default(),
1500 };
1501 if let Some(cb) = props.on_close_auto_focus.as_mut() {
1502 cb(&mut auto_focus);
1503 }
1504 if !auto_focus.preventable.default_prevented() {
1505 response.request_focus();
1506 }
1507 }
1508 }
1509
1510 if state.is_open {
1511 let input = ui.input(|i| {
1512 (
1513 i.key_pressed(Key::ArrowDown),
1514 i.key_pressed(Key::ArrowUp),
1515 i.key_pressed(Key::Enter),
1516 i.key_pressed(Key::Escape),
1517 )
1518 });
1519
1520 let now = ui.input(|i| i.time);
1521 let mut typed = String::new();
1522 let events: Vec<Event> = ui.input(|i| i.events.clone());
1523 for event in events {
1524 if let Event::Text(text) = event
1525 && !text.is_empty()
1526 && !text.chars().any(|c| c.is_control())
1527 {
1528 typed.push_str(&text);
1529 }
1530 }
1531
1532 if now - state.last_type_time > 0.8 {
1533 state.typed_buffer.clear();
1534 }
1535
1536 if !typed.is_empty() {
1537 state.typed_buffer.push_str(&typed);
1538 state.last_type_time = now;
1539 if let Some(idx) = find_typeahead_match(items, &state.typed_buffer) {
1540 state.focused_index = Some(idx);
1541 }
1542 }
1543
1544 if input.0 {
1545 state.focused_index = Some(
1546 state
1547 .focused_index
1548 .map(|i| (i + 1).min(flat_options.len().saturating_sub(1)))
1549 .unwrap_or(0),
1550 );
1551 }
1552 if input.1 {
1553 state.focused_index = state.focused_index.map(|i| i.saturating_sub(1)).or(Some(0));
1554 }
1555 if input.2
1556 && let Some(idx) = state.focused_index
1557 && let Some((value, _, disabled)) = flat_options.get(idx)
1558 && !disabled
1559 {
1560 let current_value = props.value.clone().or_else(|| props.selected.clone());
1561 let did_change = match current_value.as_deref() {
1562 Some(current) => current != value,
1563 None => true,
1564 };
1565 if props.value.is_none() {
1566 *props.selected = Some(value.clone());
1567 }
1568 if did_change && let Some(cb) = props.on_value_change.as_mut() {
1569 cb(value);
1570 }
1571
1572 if props.open.is_some() {
1573 if let Some(cb) = props.on_open_change.as_mut() {
1574 cb(false);
1575 }
1576 } else {
1577 state.is_open = false;
1578 if let Some(cb) = props.on_open_change.as_mut() {
1579 cb(false);
1580 }
1581 }
1582
1583 let mut auto_focus = SelectAutoFocusEvent {
1584 preventable: SelectPreventable::default(),
1585 };
1586 if let Some(cb) = props.on_close_auto_focus.as_mut() {
1587 cb(&mut auto_focus);
1588 }
1589 if !auto_focus.preventable.default_prevented() {
1590 response.request_focus();
1591 }
1592 response.mark_changed();
1593 }
1594 if input.3 {
1595 let mut evt = SelectEscapeKeyDownEvent {
1596 key: Key::Escape,
1597 preventable: SelectPreventable::default(),
1598 };
1599 if let Some(cb) = props.on_escape_key_down.as_mut() {
1600 cb(&mut evt);
1601 }
1602 if !evt.preventable.default_prevented() {
1603 if props.open.is_some() {
1604 if let Some(cb) = props.on_open_change.as_mut() {
1605 cb(false);
1606 }
1607 } else {
1608 state.is_open = false;
1609 if let Some(cb) = props.on_open_change.as_mut() {
1610 cb(false);
1611 }
1612 }
1613
1614 let mut auto_focus = SelectAutoFocusEvent {
1615 preventable: SelectPreventable::default(),
1616 };
1617 if let Some(cb) = props.on_close_auto_focus.as_mut() {
1618 cb(&mut auto_focus);
1619 }
1620 if !auto_focus.preventable.default_prevented() {
1621 response.request_focus();
1622 }
1623 }
1624 }
1625 }
1626
1627 let popup_painter = ui.ctx().layer_painter(layer_id);
1628
1629 let content_painter = popup_painter.with_clip_rect(animated_rect);
1630
1631 let shadow_rect = animated_rect.translate(vec2(0.0, 2.0));
1632 popup_painter.rect_filled(
1633 shadow_rect,
1634 style.content_rounding,
1635 Color32::from_rgba_unmultiplied(
1636 style.content_shadow.r(),
1637 style.content_shadow.g(),
1638 style.content_shadow.b(),
1639 (style.content_shadow.a() as f32 * anim_t) as u8,
1640 ),
1641 );
1642
1643 let bg_with_alpha = Color32::from_rgba_unmultiplied(
1644 style.content_bg.r(),
1645 style.content_bg.g(),
1646 style.content_bg.b(),
1647 alpha,
1648 );
1649 content_painter.rect_filled(animated_rect, style.content_rounding, bg_with_alpha);
1650 content_painter.rect_stroke(
1651 animated_rect,
1652 style.content_rounding,
1653 Stroke::new(
1654 1.0,
1655 Color32::from_rgba_unmultiplied(
1656 style.content_border.r(),
1657 style.content_border.g(),
1658 style.content_border.b(),
1659 alpha,
1660 ),
1661 ),
1662 StrokeKind::Inside,
1663 );
1664
1665 let needs_scroll = content_height > max_popup_height;
1666
1667 let content_rect = animated_rect.shrink(style.content_padding);
1668
1669 let scroll_button_h = 18.0;
1670 let mut items_rect = content_rect;
1671
1672 let max_scroll = if needs_scroll {
1673 let eps = 1.0;
1674
1675 let base_height = content_rect.height();
1676
1677 let max_scroll_with_both =
1678 (content_height - (base_height - 2.0 * scroll_button_h)).max(0.0);
1679
1680 let max_scroll_with_up = (content_height - (base_height - scroll_button_h)).max(0.0);
1681
1682 let max_scroll_with_down = (content_height - (base_height - scroll_button_h)).max(0.0);
1683
1684 let max_scroll_no_buttons = (content_height - base_height).max(0.0);
1685
1686 state.show_scroll_up = state.scroll_offset > eps;
1687
1688 let visible_height_for_down_check = if state.show_scroll_up {
1689 base_height - scroll_button_h
1690 } else {
1691 base_height
1692 };
1693 state.show_scroll_down =
1694 state.scroll_offset + visible_height_for_down_check < content_height - eps;
1695
1696 let max_scroll = match (state.show_scroll_up, state.show_scroll_down) {
1697 (true, true) => max_scroll_with_both,
1698 (true, false) => max_scroll_with_up,
1699 (false, true) => max_scroll_with_down,
1700 (false, false) => max_scroll_no_buttons,
1701 };
1702
1703 state.scroll_offset = state.scroll_offset.clamp(0.0, max_scroll);
1704
1705 state.show_scroll_up = state.scroll_offset > eps;
1706 let visible_height_for_down_check = if state.show_scroll_up {
1707 base_height - scroll_button_h
1708 } else {
1709 base_height
1710 };
1711 state.show_scroll_down =
1712 state.scroll_offset + visible_height_for_down_check < content_height - eps;
1713
1714 let top_margin = if state.show_scroll_up {
1715 scroll_button_h
1716 } else {
1717 0.0
1718 };
1719 let bottom_margin = if state.show_scroll_down {
1720 scroll_button_h
1721 } else {
1722 0.0
1723 };
1724 items_rect = Rect::from_min_max(
1725 pos2(content_rect.left(), content_rect.top() + top_margin),
1726 pos2(content_rect.right(), content_rect.bottom() - bottom_margin),
1727 );
1728
1729 (content_height - items_rect.height()).max(0.0)
1730 } else {
1731 state.show_scroll_up = false;
1732 state.show_scroll_down = false;
1733 state.scroll_offset = 0.0;
1734 0.0
1735 };
1736
1737 if needs_scroll
1738 && let Some(idx) = state.focused_index
1739 && let Some((offset, item_h)) = calculate_selected_offset(
1740 items,
1741 &flat_options[idx].0,
1742 item_height,
1743 separator_height,
1744 label_height,
1745 )
1746 {
1747 let visible_h = items_rect.height();
1748 let target = (offset - (visible_h - item_h) * 0.5).max(0.0);
1749 state.scroll_offset = target.clamp(0.0, max_scroll);
1750 }
1751
1752 let items_painter = content_painter.with_clip_rect(items_rect);
1753 let mut y_offset = items_rect.top() - state.scroll_offset;
1754 let mut option_index = 0;
1755 let mut clicked_value: Option<String> = None;
1756
1757 if let Some(pos) = ui.input(|i| i.pointer.hover_pos())
1758 && items_rect.contains(pos)
1759 {
1760 state.focused_index = None;
1761 }
1762
1763 let selected_ref = props.value.clone().or_else(|| props.selected.clone());
1764
1765 for item in items {
1766 let (new_y, clicked) = draw_select_item(
1767 &items_painter,
1768 item,
1769 items_rect,
1770 y_offset,
1771 &style,
1772 props.size,
1773 alpha,
1774 selected_ref.as_ref(),
1775 &mut option_index,
1776 state.focused_index,
1777 ui,
1778 item_height,
1779 separator_height,
1780 label_height,
1781 props.content_variant,
1782 props.high_contrast,
1783 );
1784 y_offset = new_y;
1785 if clicked.is_some() {
1786 clicked_value = clicked;
1787 }
1788 }
1789
1790 if let Some(value) = clicked_value {
1791 let current_value = props.value.clone().or_else(|| props.selected.clone());
1792 let did_change = match current_value.as_deref() {
1793 Some(current) => current != value,
1794 None => true,
1795 };
1796
1797 if props.value.is_none() {
1798 *props.selected = Some(value.clone());
1799 }
1800 if did_change && let Some(cb) = props.on_value_change.as_mut() {
1801 cb(&value);
1802 }
1803
1804 if props.open.is_some() {
1805 if let Some(cb) = props.on_open_change.as_mut() {
1806 cb(false);
1807 }
1808 } else {
1809 state.is_open = false;
1810 if let Some(cb) = props.on_open_change.as_mut() {
1811 cb(false);
1812 }
1813 }
1814
1815 let mut auto_focus = SelectAutoFocusEvent {
1816 preventable: SelectPreventable::default(),
1817 };
1818 if let Some(cb) = props.on_close_auto_focus.as_mut() {
1819 cb(&mut auto_focus);
1820 }
1821 if !auto_focus.preventable.default_prevented() {
1822 response.request_focus();
1823 }
1824 response.mark_changed();
1825 }
1826
1827 if state.show_scroll_up {
1828 let btn_rect = Rect::from_min_size(
1829 pos2(content_rect.left(), content_rect.top()),
1830 vec2(content_rect.width(), scroll_button_h),
1831 );
1832 content_painter.rect_filled(btn_rect, CornerRadius::ZERO, style.content_bg);
1833 draw_chevron_up(
1834 &content_painter,
1835 btn_rect.center(),
1836 16.0,
1837 style.scroll_button_color,
1838 );
1839
1840 if let Some(pos) = ui.input(|i| i.pointer.hover_pos())
1841 && btn_rect.contains(pos)
1842 {
1843 state.scroll_offset = (state.scroll_offset - 4.0).clamp(0.0, max_scroll);
1844 ui.ctx().request_repaint();
1845 }
1846 }
1847
1848 if state.show_scroll_down {
1849 let btn_rect = Rect::from_min_size(
1850 pos2(content_rect.left(), content_rect.bottom() - scroll_button_h),
1851 vec2(content_rect.width(), scroll_button_h),
1852 );
1853 content_painter.rect_filled(btn_rect, CornerRadius::ZERO, style.content_bg);
1854 draw_chevron_down(
1855 &content_painter,
1856 btn_rect.center(),
1857 16.0,
1858 style.scroll_button_color,
1859 );
1860
1861 if let Some(pos) = ui.input(|i| i.pointer.hover_pos())
1862 && btn_rect.contains(pos)
1863 {
1864 state.scroll_offset = (state.scroll_offset + 4.0).clamp(0.0, max_scroll);
1865 ui.ctx().request_repaint();
1866 }
1867 }
1868
1869 if needs_scroll {
1870 let scroll_delta = ui.input(|i| i.raw_scroll_delta.y);
1871 if animated_rect.contains(ui.input(|i| i.pointer.hover_pos().unwrap_or_default())) {
1872 state.scroll_offset = (state.scroll_offset - scroll_delta).clamp(0.0, max_scroll);
1873 }
1874 }
1875 }
1876
1877 if !state.is_open {
1878 state.typed_buffer.clear();
1879 state.last_type_time = 0.0;
1880 }
1881
1882 ui.ctx().data_mut(|d| d.insert_temp(id, state));
1883
1884 response
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888fn draw_select_item(
1889 painter: &Painter,
1890 item: &SelectItem,
1891 content_rect: Rect,
1892 y_offset: f32,
1893 style: &SelectStyle,
1894 size: SelectSize,
1895 alpha: u8,
1896 selected: Option<&String>,
1897 option_index: &mut usize,
1898 focused_index: Option<usize>,
1899 ui: &Ui,
1900 item_height: f32,
1901 separator_height: f32,
1902 label_height: f32,
1903 content_variant: ContentVariant,
1904 high_contrast: bool,
1905) -> (f32, Option<String>) {
1906 match item {
1907 SelectItem::Option {
1908 value,
1909 label,
1910 disabled,
1911 ..
1912 } => {
1913 let item_rect = Rect::from_min_size(
1914 pos2(content_rect.left(), y_offset),
1915 vec2(content_rect.width(), item_height),
1916 );
1917
1918 if item_rect.bottom() < content_rect.top() || item_rect.top() > content_rect.bottom() {
1919 *option_index += 1;
1920 return (y_offset + item_height, None);
1921 }
1922
1923 let is_selected = selected.map(|s| s == value).unwrap_or(false);
1924 let is_focused = focused_index == Some(*option_index);
1925 let is_hovered = ui.input(|i| {
1926 i.pointer
1927 .hover_pos()
1928 .map(|p| item_rect.contains(p))
1929 .unwrap_or(false)
1930 });
1931
1932 let selected_bg = if is_selected {
1933 if high_contrast && content_variant == ContentVariant::Solid {
1934 style.item_solid_high_contrast_bg
1935 } else {
1936 style.item_bg_selected
1937 }
1938 } else {
1939 Color32::TRANSPARENT
1940 };
1941
1942 let (bg, text_base) = if *disabled {
1943 (Color32::TRANSPARENT, style.item_text)
1944 } else if is_hovered || is_focused {
1945 match content_variant {
1946 ContentVariant::Solid => {
1947 if high_contrast {
1948 (
1949 style.item_solid_high_contrast_bg,
1950 style.item_solid_high_contrast_text,
1951 )
1952 } else {
1953 (style.item_solid_bg_hover, style.item_solid_text_hover)
1954 }
1955 }
1956 ContentVariant::Soft => (style.item_bg_hover, style.item_text_hover),
1957 }
1958 } else if is_selected {
1959 (
1960 selected_bg,
1961 if high_contrast && content_variant == ContentVariant::Solid {
1962 style.item_solid_high_contrast_text
1963 } else {
1964 style.item_text
1965 },
1966 )
1967 } else {
1968 (style.item_bg, style.item_text)
1969 };
1970 let bg_with_alpha = Color32::from_rgba_unmultiplied(
1971 bg.r(),
1972 bg.g(),
1973 bg.b(),
1974 (bg.a() as f32 * alpha as f32 / 255.0) as u8,
1975 );
1976 painter.rect_filled(item_rect, style.item_rounding, bg_with_alpha);
1977
1978 if is_selected {
1979 let check_center = pos2(item_rect.right() - 20.0, item_rect.center().y);
1980
1981 let check_color = if (is_hovered || is_focused) && !*disabled {
1982 text_base
1983 } else {
1984 style.item_text
1985 };
1986 draw_check_icon(
1987 painter,
1988 check_center,
1989 16.0,
1990 Color32::from_rgba_unmultiplied(
1991 check_color.r(),
1992 check_color.g(),
1993 check_color.b(),
1994 alpha,
1995 ),
1996 );
1997 }
1998
1999 let text_color = if *disabled {
2000 Color32::from_rgba_unmultiplied(
2001 style.item_text.r(),
2002 style.item_text.g(),
2003 style.item_text.b(),
2004 (alpha as f32 * 0.5) as u8,
2005 )
2006 } else {
2007 Color32::from_rgba_unmultiplied(text_base.r(), text_base.g(), text_base.b(), alpha)
2008 };
2009
2010 let galley = painter.layout_no_wrap(
2011 label.clone(),
2012 FontId::proportional(size.font_size()),
2013 text_color,
2014 );
2015 let text_pos = pos2(
2016 item_rect.left() + style.item_padding.x,
2017 item_rect.center().y - galley.size().y * 0.5,
2018 );
2019 painter.galley(text_pos, galley, Color32::TRANSPARENT);
2020
2021 let clicked = if !*disabled && is_hovered && ui.input(|i| i.pointer.any_click()) {
2022 Some(value.clone())
2023 } else {
2024 None
2025 };
2026
2027 *option_index += 1;
2028 (y_offset + item_height, clicked)
2029 }
2030
2031 SelectItem::Group { label, items } => {
2032 let label_rect = Rect::from_min_size(
2033 pos2(content_rect.left(), y_offset),
2034 vec2(content_rect.width(), label_height),
2035 );
2036
2037 let galley = painter.layout_no_wrap(
2038 label.clone(),
2039 FontId::proportional(12.0),
2040 Color32::from_rgba_unmultiplied(
2041 style.label_text.r(),
2042 style.label_text.g(),
2043 style.label_text.b(),
2044 alpha,
2045 ),
2046 );
2047 let text_pos = pos2(
2048 label_rect.left() + style.item_padding.x,
2049 label_rect.center().y - galley.size().y * 0.5,
2050 );
2051 painter.galley(text_pos, galley, Color32::TRANSPARENT);
2052
2053 let mut next_y = y_offset + label_height;
2054 let mut clicked_value: Option<String> = None;
2055
2056 for sub_item in items {
2057 let (new_y, clicked) = draw_select_item(
2058 painter,
2059 sub_item,
2060 content_rect,
2061 next_y,
2062 style,
2063 size,
2064 alpha,
2065 selected,
2066 option_index,
2067 focused_index,
2068 ui,
2069 item_height,
2070 separator_height,
2071 label_height,
2072 content_variant,
2073 high_contrast,
2074 );
2075 next_y = new_y;
2076 if clicked.is_some() {
2077 clicked_value = clicked;
2078 }
2079 }
2080
2081 (next_y, clicked_value)
2082 }
2083
2084 SelectItem::Separator => {
2085 let sep_rect = Rect::from_min_size(
2086 pos2(content_rect.left() - 4.0, y_offset + 4.0),
2087 vec2(content_rect.width() + 8.0, 1.0),
2088 );
2089 painter.rect_filled(
2090 sep_rect,
2091 CornerRadius::ZERO,
2092 Color32::from_rgba_unmultiplied(
2093 style.separator_color.r(),
2094 style.separator_color.g(),
2095 style.separator_color.b(),
2096 alpha,
2097 ),
2098 );
2099 (y_offset + separator_height, None)
2100 }
2101
2102 SelectItem::Label(text) => {
2103 let label_rect = Rect::from_min_size(
2104 pos2(content_rect.left(), y_offset),
2105 vec2(content_rect.width(), label_height),
2106 );
2107
2108 let galley = painter.layout_no_wrap(
2109 text.clone(),
2110 FontId::proportional(12.0),
2111 Color32::from_rgba_unmultiplied(
2112 style.label_text.r(),
2113 style.label_text.g(),
2114 style.label_text.b(),
2115 alpha,
2116 ),
2117 );
2118 let text_pos = pos2(
2119 label_rect.left() + style.item_padding.x,
2120 label_rect.center().y - galley.size().y * 0.5,
2121 );
2122 painter.galley(text_pos, galley, Color32::TRANSPARENT);
2123
2124 (y_offset + label_height, None)
2125 }
2126 }
2127}
2128
2129fn calculate_content_height(items: &[SelectItem], item_h: f32, sep_h: f32, label_h: f32) -> f32 {
2130 let mut height = 0.0;
2131 for item in items {
2132 match item {
2133 SelectItem::Option { .. } => height += item_h,
2134 SelectItem::Separator => height += sep_h,
2135 SelectItem::Label(_) => height += label_h,
2136 SelectItem::Group { items, .. } => {
2137 height += label_h;
2138 height += calculate_content_height(items, item_h, sep_h, label_h);
2139 }
2140 }
2141 }
2142 height
2143}
2144
2145fn flatten_options(items: &[SelectItem]) -> Vec<(String, String, bool)> {
2146 let mut result = Vec::new();
2147 for item in items {
2148 match item {
2149 SelectItem::Option {
2150 value,
2151 label,
2152 disabled,
2153 ..
2154 } => {
2155 result.push((value.clone(), label.clone(), *disabled));
2156 }
2157 SelectItem::Group { items, .. } => {
2158 result.extend(flatten_options(items));
2159 }
2160 _ => {}
2161 }
2162 }
2163 result
2164}
2165
2166pub fn find_typeahead_match(items: &[SelectItem], needle: &str) -> Option<usize> {
2167 if needle.is_empty() {
2168 return None;
2169 }
2170 let needle_lower = needle.to_lowercase();
2171 let mut index: usize = 0;
2172
2173 fn traverse(items: &[SelectItem], needle_lower: &str, index: &mut usize) -> Option<usize> {
2174 for item in items {
2175 match item {
2176 SelectItem::Option {
2177 value,
2178 label,
2179 disabled,
2180 text_value,
2181 } => {
2182 if !*disabled {
2183 let label_lower = text_value.as_deref().unwrap_or(label).to_lowercase();
2184 let value_lower = value.to_lowercase();
2185 if label_lower.starts_with(needle_lower)
2186 || value_lower.starts_with(needle_lower)
2187 {
2188 return Some(*index);
2189 }
2190 }
2191 *index += 1;
2192 }
2193 SelectItem::Group { items, .. } => {
2194 if let Some(found) = traverse(items, needle_lower, index) {
2195 return Some(found);
2196 }
2197 }
2198 _ => {}
2199 }
2200 }
2201 None
2202 }
2203
2204 traverse(items, &needle_lower, &mut index)
2205}
2206
2207fn find_label_for_value(items: &[SelectItem], value: &str) -> Option<String> {
2208 for item in items {
2209 match item {
2210 SelectItem::Option {
2211 value: v, label, ..
2212 } if v == value => {
2213 return Some(label.clone());
2214 }
2215 SelectItem::Group { items, .. } => {
2216 if let Some(label) = find_label_for_value(items, value) {
2217 return Some(label);
2218 }
2219 }
2220 _ => {}
2221 }
2222 }
2223 None
2224}
2225
2226fn calculate_selected_offset(
2227 items: &[SelectItem],
2228 selected_value: &str,
2229 item_h: f32,
2230 sep_h: f32,
2231 label_h: f32,
2232) -> Option<(f32, f32)> {
2233 fn find_offset(
2234 items: &[SelectItem],
2235 selected_value: &str,
2236 item_h: f32,
2237 sep_h: f32,
2238 label_h: f32,
2239 current_offset: f32,
2240 ) -> Option<(f32, f32)> {
2241 let mut offset = current_offset;
2242 for item in items {
2243 match item {
2244 SelectItem::Option { value, .. } => {
2245 if value == selected_value {
2246 return Some((offset, item_h));
2247 }
2248 offset += item_h;
2249 }
2250 SelectItem::Separator => {
2251 offset += sep_h;
2252 }
2253 SelectItem::Label(_) => {
2254 offset += label_h;
2255 }
2256 SelectItem::Group {
2257 label: _,
2258 items: sub_items,
2259 } => {
2260 offset += label_h;
2261 if let Some(result) =
2262 find_offset(sub_items, selected_value, item_h, sep_h, label_h, offset)
2263 {
2264 return Some(result);
2265 }
2266
2267 for sub_item in sub_items {
2268 match sub_item {
2269 SelectItem::Option { .. } => offset += item_h,
2270 SelectItem::Separator => offset += sep_h,
2271 SelectItem::Label(_) => offset += label_h,
2272 SelectItem::Group { .. } => {}
2273 }
2274 }
2275 }
2276 }
2277 }
2278 None
2279 }
2280
2281 find_offset(items, selected_value, item_h, sep_h, label_h, 0.0)
2282}
2283
2284pub fn select<Id>(ui: &mut Ui, theme: &Theme, props: SelectPropsSimple<'_, Id>) -> Response
2285where
2286 Id: Hash + Debug,
2287{
2288 trace!(
2289 "Rendering select (legacy) size={:?} enabled={} options={}",
2290 props.size,
2291 props.enabled,
2292 props.options.len()
2293 );
2294
2295 let items: Vec<SelectItem> = props
2296 .options
2297 .iter()
2298 .map(|opt| SelectItem::option(opt.clone(), opt.clone()))
2299 .collect();
2300
2301 let new_props = SelectProps::new(props.id_source, props.selected)
2302 .placeholder(props.placeholder)
2303 .size(props.size.into())
2304 .enabled(props.enabled)
2305 .invalid(props.is_invalid);
2306
2307 select_with_items(ui, theme, new_props, &items)
2308}