Skip to main content

egui_shadcn/
select.rs

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}