Skip to main content

egui_material3/
select.rs

1use crate::theme::get_global_color;
2use eframe::egui::{
3    self, Color32, FontFamily, FontId, Key, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget,
4};
5
6/// Material Design select/dropdown component.
7///
8/// Select components allow users to choose one option from a list.
9/// They display the currently selected option in a text field-style input
10/// and show all options in a dropdown menu when activated.
11///
12/// Supports Material Design 3 variants (filled and outlined), filtering,
13/// validation, and comprehensive keyboard navigation.
14///
15/// # Example
16/// ```rust
17/// # egui::__run_test_ui(|ui| {
18/// let mut selected = Some(1);
19///
20/// ui.add(MaterialSelect::new(&mut selected)
21///     .variant(SelectVariant::Outlined)
22///     .label("Choose an option")
23///     .option(0, "Option 1")
24///     .option(1, "Option 2")
25///     .option(2, "Option 3")
26///     .helper_text("Select your preferred option"));
27/// # });
28/// ```
29/// Visual variant of the select component.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SelectVariant {
32    /// Filled variant with background color
33    Filled,
34    /// Outlined variant with border
35    Outlined,
36}
37
38impl Default for SelectVariant {
39    fn default() -> Self {
40        Self::Filled
41    }
42}
43
44/// Menu alignment options.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum MenuAlignment {
47    /// Align menu to start edge
48    Start,
49    /// Align menu to end edge
50    End,
51}
52
53impl Default for MenuAlignment {
54    fn default() -> Self {
55        Self::Start
56    }
57}
58
59#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
60pub struct MaterialSelect<'a> {
61    /// Reference to the currently selected option
62    selected: &'a mut Option<usize>,
63    /// List of available options
64    options: Vec<SelectOption>,
65    /// Placeholder text when no option is selected
66    placeholder: String,
67    /// Label text (floats above when focused or has content)
68    label: Option<String>,
69    /// Visual variant (filled or outlined)
70    variant: SelectVariant,
71    /// Whether the select is enabled for interaction
72    enabled: bool,
73    /// Fixed width of the select component
74    width: Option<f32>,
75    /// Error message to display below the select
76    error_text: Option<String>,
77    /// Helper text to display below the select
78    helper_text: Option<String>,
79    /// Icon to show at the start of the select field
80    leading_icon: Option<String>,
81    /// Icon to show at the end of the select field (overrides default dropdown arrow)
82    trailing_icon: Option<String>,
83    /// Whether to keep the dropdown open after selecting an option
84    keep_open_on_select: bool,
85    /// Enable filtering of options by typing
86    enable_filter: bool,
87    /// Enable search highlighting while typing
88    enable_search: bool,
89    /// Mark field as required
90    required: bool,
91    /// Independent menu width
92    menu_width: Option<f32>,
93    /// Maximum menu height
94    menu_max_height: Option<f32>,
95    /// Border radius for menu
96    border_radius: Option<f32>,
97    /// Menu alignment
98    menu_alignment: MenuAlignment,
99}
100
101/// Individual option in a select component.
102pub struct SelectOption {
103    /// Unique identifier for this option
104    value: usize,
105    /// Display text for this option
106    text: String,
107}
108
109impl<'a> MaterialSelect<'a> {
110    /// Create a new select component.
111    ///
112    /// # Arguments
113    /// * `selected` - Mutable reference to the currently selected option value
114    ///
115    /// # Example
116    /// ```rust
117    /// # egui::__run_test_ui(|ui| {
118    /// let mut selection = None;
119    /// let select = MaterialSelect::new(&mut selection);
120    /// # });
121    /// ```
122    pub fn new(selected: &'a mut Option<usize>) -> Self {
123        Self {
124            selected,
125            options: Vec::new(),
126            placeholder: "Select an option".to_string(),
127            label: None,
128            variant: SelectVariant::default(),
129            enabled: true,
130            width: None,
131            error_text: None,
132            helper_text: None,
133            leading_icon: None,
134            trailing_icon: None,
135            keep_open_on_select: false,
136            enable_filter: false,
137            enable_search: true,
138            required: false,
139            menu_width: None,
140            menu_max_height: None,
141            border_radius: None,
142            menu_alignment: MenuAlignment::default(),
143        }
144    }
145
146    /// Add an option to the select component.
147    ///
148    /// # Arguments
149    /// * `value` - Unique identifier for this option
150    /// * `text` - Display text for this option
151    ///
152    /// # Example
153    /// ```rust
154    /// # egui::__run_test_ui(|ui| {
155    /// let mut selection = None;
156    /// ui.add(MaterialSelect::new(&mut selection)
157    ///     .option(1, "First Option")
158    ///     .option(2, "Second Option"));
159    /// # });
160    /// ```
161    pub fn option(mut self, value: usize, text: impl Into<String>) -> Self {
162        self.options.push(SelectOption {
163            value,
164            text: text.into(),
165        });
166        self
167    }
168
169    /// Set placeholder text shown when no option is selected.
170    ///
171    /// # Arguments
172    /// * `placeholder` - The placeholder text to display
173    ///
174    /// # Example
175    /// ```rust
176    /// # egui::__run_test_ui(|ui| {
177    /// let mut selection = None;
178    /// ui.add(MaterialSelect::new(&mut selection)
179    ///     .placeholder("Choose your option"));
180    /// # });
181    /// ```
182    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
183        self.placeholder = placeholder.into();
184        self
185    }
186
187    /// Set label text that floats above the field when focused or has content.
188    ///
189    /// # Arguments
190    /// * `label` - The label text to display
191    ///
192    /// # Example
193    /// ```rust
194    /// # egui::__run_test_ui(|ui| {
195    /// let mut selection = None;
196    /// ui.add(MaterialSelect::new(&mut selection)
197    ///     .label("Color"));
198    /// # });
199    /// ```
200    pub fn label(mut self, label: impl Into<String>) -> Self {
201        self.label = Some(label.into());
202        self
203    }
204
205    /// Set the visual variant of the select component.
206    ///
207    /// # Arguments
208    /// * `variant` - The variant (Filled or Outlined)
209    ///
210    /// # Example
211    /// ```rust
212    /// # egui::__run_test_ui(|ui| {
213    /// let mut selection = None;
214    /// ui.add(MaterialSelect::new(&mut selection)
215    ///     .variant(SelectVariant::Outlined));
216    /// # });
217    /// ```
218    pub fn variant(mut self, variant: SelectVariant) -> Self {
219        self.variant = variant;
220        self
221    }
222
223    /// Enable or disable the select component.
224    ///
225    /// # Arguments
226    /// * `enabled` - Whether the select should be enabled (true) or disabled (false)
227    ///
228    /// # Example
229    /// ```rust
230    /// # egui::__run_test_ui(|ui| {
231    /// let mut selection = None;
232    /// ui.add(MaterialSelect::new(&mut selection)
233    ///     .enabled(false)); // Disabled select
234    /// # });
235    /// ```
236    pub fn enabled(mut self, enabled: bool) -> Self {
237        self.enabled = enabled;
238        self
239    }
240
241    /// Set a fixed width for the select component.
242    ///
243    /// # Arguments
244    /// * `width` - The width in pixels
245    ///
246    /// # Example
247    /// ```rust
248    /// # egui::__run_test_ui(|ui| {
249    /// let mut selection = None;
250    /// ui.add(MaterialSelect::new(&mut selection)
251    ///     .width(300.0)); // Fixed width of 300 pixels
252    /// # });
253    /// ```
254    pub fn width(mut self, width: f32) -> Self {
255        self.width = Some(width);
256        self
257    }
258
259    /// Set error text to display below the select component.
260    ///
261    /// # Arguments
262    /// * `text` - The error message text
263    ///
264    /// # Example
265    /// ```rust
266    /// # egui::__run_test_ui(|ui| {
267    /// let mut selection = None;
268    /// ui.add(MaterialSelect::new(&mut selection)
269    ///     .error_text("This field is required")); // Error message
270    /// # });
271    /// ```
272    pub fn error_text(mut self, text: impl Into<String>) -> Self {
273        self.error_text = Some(text.into());
274        self
275    }
276
277    /// Set helper text to display below the select component.
278    ///
279    /// # Arguments
280    /// * `text` - The helper message text
281    ///
282    /// # Example
283    /// ```rust
284    /// # egui::__run_test_ui(|ui| {
285    /// let mut selection = None;
286    /// ui.add(MaterialSelect::new(&mut selection)
287    ///     .helper_text("Select an option from the list")); // Helper text
288    /// # });
289    /// ```
290    pub fn helper_text(mut self, text: impl Into<String>) -> Self {
291        self.helper_text = Some(text.into());
292        self
293    }
294
295    /// Set an icon to display at the start of the select field.
296    ///
297    /// # Arguments
298    /// * `icon` - The icon identifier (e.g., "home", "settings")
299    ///
300    /// # Example
301    /// ```rust
302    /// # egui::__run_test_ui(|ui| {
303    /// let mut selection = None;
304    /// ui.add(MaterialSelect::new(&mut selection)
305    ///     .leading_icon("settings")); // Gear icon on the left
306    /// # });
307    /// ```
308    pub fn leading_icon(mut self, icon: impl Into<String>) -> Self {
309        self.leading_icon = Some(icon.into());
310        self
311    }
312
313    /// Set an icon to display at the end of the select field (overrides default dropdown arrow).
314    ///
315    /// # Arguments
316    /// * `icon` - The icon identifier (e.g., "check", "close")
317    ///
318    /// # Example
319    /// ```rust
320    /// # egui::__run_test_ui(|ui| {
321    /// let mut selection = None;
322    /// ui.add(MaterialSelect::new(&mut selection)
323    ///     .trailing_icon("check")); // Check icon on the right
324    /// # });
325    /// ```
326    pub fn trailing_icon(mut self, icon: impl Into<String>) -> Self {
327        self.trailing_icon = Some(icon.into());
328        self
329    }
330
331    /// Set whether to keep the dropdown open after selecting an option.
332    ///
333    /// # Arguments
334    /// * `keep_open` - If true, the dropdown remains open after selection;
335    ///                 if false, it closes (default behavior)
336    ///
337    /// # Example
338    /// ```rust
339    /// # egui::__run_test_ui(|ui| {
340    /// let mut selection = None;
341    /// ui.add(MaterialSelect::new(&mut selection)
342    ///     .keep_open_on_select(true)); // Dropdown stays open after selection
343    /// # });
344    /// ```
345    pub fn keep_open_on_select(mut self, keep_open: bool) -> Self {
346        self.keep_open_on_select = keep_open;
347        self
348    }
349
350    /// Enable filtering of options by typing.
351    ///
352    /// # Arguments
353    /// * `enable` - If true, allows filtering options by text input
354    pub fn enable_filter(mut self, enable: bool) -> Self {
355        self.enable_filter = enable;
356        self
357    }
358
359    /// Enable search highlighting while typing.
360    ///
361    /// # Arguments
362    /// * `enable` - If true, highlights matching options while typing
363    pub fn enable_search(mut self, enable: bool) -> Self {
364        self.enable_search = enable;
365        self
366    }
367
368    /// Mark the field as required.
369    ///
370    /// # Arguments
371    /// * `required` - If true, marks the field as required
372    pub fn required(mut self, required: bool) -> Self {
373        self.required = required;
374        self
375    }
376
377    /// Set independent menu width.
378    ///
379    /// # Arguments
380    /// * `width` - The width of the menu in pixels
381    pub fn menu_width(mut self, width: f32) -> Self {
382        self.menu_width = Some(width);
383        self
384    }
385
386    /// Set maximum menu height.
387    ///
388    /// # Arguments
389    /// * `height` - The maximum height of the menu in pixels
390    pub fn menu_max_height(mut self, height: f32) -> Self {
391        self.menu_max_height = Some(height);
392        self
393    }
394
395    /// Set border radius for menu.
396    ///
397    /// # Arguments
398    /// * `radius` - The border radius in pixels
399    pub fn border_radius(mut self, radius: f32) -> Self {
400        self.border_radius = Some(radius);
401        self
402    }
403
404    /// Set menu alignment.
405    ///
406    /// # Arguments
407    /// * `alignment` - The menu alignment (Start or End)
408    pub fn menu_alignment(mut self, alignment: MenuAlignment) -> Self {
409        self.menu_alignment = alignment;
410        self
411    }
412}
413
414impl<'a> Widget for MaterialSelect<'a> {
415    fn ui(self, ui: &mut Ui) -> Response {
416        let width = self.width.unwrap_or(200.0);
417        let height = 56.0;
418        let desired_size = Vec2::new(width, height);
419
420        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
421
422        // Use persistent state for dropdown open/close with global coordination
423        let select_id = egui::Id::new((
424            "select_widget",
425            rect.min.x as i32,
426            rect.min.y as i32,
427            self.placeholder.clone(),
428            self.label.clone(),
429        ));
430        let mut open = ui.memory(|mem| mem.data.get_temp::<bool>(select_id).unwrap_or(false));
431        
432        // Handle Escape key to close dropdown
433        if open && ui.input(|i| i.key_pressed(Key::Escape)) {
434            open = false;
435            ui.memory_mut(|mem| mem.data.insert_temp(select_id, false));
436        }
437
438        // Global state to close other select menus
439        let global_open_select_id = egui::Id::new("global_open_select");
440        let current_open_select =
441            ui.memory(|mem| mem.data.get_temp::<egui::Id>(global_open_select_id));
442
443        if response.clicked() && self.enabled {
444            if open {
445                // Close this select
446                open = false;
447                ui.memory_mut(|mem| mem.data.remove::<egui::Id>(global_open_select_id));
448            } else {
449                // Close any other open select and open this one
450                if let Some(other_id) = current_open_select {
451                    if other_id != select_id {
452                        ui.memory_mut(|mem| mem.data.insert_temp(other_id, false));
453                    }
454                }
455                open = true;
456                ui.memory_mut(|mem| mem.data.insert_temp(global_open_select_id, select_id));
457            }
458            ui.memory_mut(|mem| mem.data.insert_temp(select_id, open));
459        }
460
461        // Material Design colors
462        let primary_color = get_global_color("primary");
463        let surface = get_global_color("surface");
464        let surface_variant = get_global_color("surfaceVariant");
465        let on_surface = get_global_color("onSurface");
466        let on_surface_variant = get_global_color("onSurfaceVariant");
467        let outline = get_global_color("outline");
468        let error_color = get_global_color("error");
469
470        // Determine if we should show floating label
471        let has_content = self.selected.is_some();
472        let should_float_label = has_content || open || response.hovered();
473        
474        // Hide label if field is empty and not focused (placeholder will be shown instead)
475        let should_show_label = self.label.is_some() && should_float_label;
476        
477        // Determine colors based on state
478        let (bg_color, border_color, text_color) = if !self.enabled {
479            (
480                surface_variant.linear_multiply(0.38),
481                outline.linear_multiply(0.38),
482                on_surface.linear_multiply(0.38),
483            )
484        } else if self.error_text.is_some() {
485            match self.variant {
486                SelectVariant::Filled => (surface_variant, error_color, on_surface),
487                SelectVariant::Outlined => (surface, error_color, on_surface),
488            }
489        } else if response.hovered() || open {
490            match self.variant {
491                SelectVariant::Filled => (surface_variant, primary_color, on_surface),
492                SelectVariant::Outlined => (surface, primary_color, on_surface),
493            }
494        } else {
495            match self.variant {
496                SelectVariant::Filled => (surface_variant, outline, on_surface_variant),
497                SelectVariant::Outlined => (surface, outline, on_surface_variant),
498            }
499        };
500
501        // Draw select field background
502        match self.variant {
503            SelectVariant::Filled => {
504                ui.painter().rect_filled(rect, 4.0, bg_color);
505                // Draw bottom border for filled variant
506                if !self.enabled {
507                    ui.painter().line_segment(
508                        [
509                            Pos2::new(rect.min.x, rect.max.y),
510                            Pos2::new(rect.max.x, rect.max.y),
511                        ],
512                        Stroke::new(1.0, border_color),
513                    );
514                } else {
515                    ui.painter().line_segment(
516                        [
517                            Pos2::new(rect.min.x, rect.max.y),
518                            Pos2::new(rect.max.x, rect.max.y),
519                        ],
520                        Stroke::new(if open || response.hovered() { 2.0 } else { 1.0 }, border_color),
521                    );
522                }
523            }
524            SelectVariant::Outlined => {
525                ui.painter().rect_filled(rect, 4.0, bg_color);
526                ui.painter().rect_stroke(
527                    rect,
528                    4.0,
529                    Stroke::new(if open || response.hovered() { 2.0 } else { 1.0 }, border_color),
530                    egui::epaint::StrokeKind::Outside,
531                );
532            }
533        }
534
535        // Draw floating label if present and should be shown
536        if should_show_label {
537            let label_text = self.label.as_ref().unwrap();
538            let label_font = if should_float_label {
539                FontId::new(12.0, FontFamily::Proportional)
540            } else {
541                FontId::new(16.0, FontFamily::Proportional)
542            };
543            
544            let label_color = if !self.enabled {
545                on_surface.linear_multiply(0.38)
546            } else if self.error_text.is_some() {
547                error_color
548            } else if open {
549                primary_color
550            } else {
551                on_surface_variant
552            };
553            
554            let label_pos = if should_float_label {
555                Pos2::new(rect.min.x + 16.0, rect.min.y + 8.0)
556            } else {
557                Pos2::new(rect.min.x + 16.0, rect.center().y)
558            };
559            
560            ui.painter().text(
561                label_pos,
562                egui::Align2::LEFT_TOP,
563                label_text,
564                label_font,
565                label_color,
566            );
567        }
568
569        // Draw selected text or placeholder
570        let display_text = if let Some(selected_value) = *self.selected {
571            self.options
572                .iter()
573                .find(|option| option.value == selected_value)
574                .map(|option| option.text.as_str())
575                .unwrap_or(&self.placeholder)
576        } else {
577            &self.placeholder
578        };
579
580        // Use consistent font styling for select field
581        let select_font = FontId::new(16.0, FontFamily::Proportional);
582        let text_y_offset = if should_show_label && should_float_label { 12.0 } else { 0.0 };
583        let text_pos = Pos2::new(rect.min.x + 16.0, rect.center().y + text_y_offset);
584        
585        let display_color = if self.selected.is_none() {
586            on_surface_variant.linear_multiply(0.6)
587        } else {
588            text_color
589        };
590        
591        ui.painter().text(
592            text_pos,
593            egui::Align2::LEFT_CENTER,
594            display_text,
595            select_font.clone(),
596            display_color,
597        );
598
599        // Draw dropdown arrow
600        let arrow_center = Pos2::new(rect.max.x - 24.0, rect.center().y);
601        let arrow_size = 8.0;
602
603        if open {
604            // Up arrow
605            ui.painter().line_segment(
606                [
607                    Pos2::new(
608                        arrow_center.x - arrow_size / 2.0,
609                        arrow_center.y + arrow_size / 4.0,
610                    ),
611                    Pos2::new(arrow_center.x, arrow_center.y - arrow_size / 4.0),
612                ],
613                Stroke::new(2.0, text_color),
614            );
615            ui.painter().line_segment(
616                [
617                    Pos2::new(arrow_center.x, arrow_center.y - arrow_size / 4.0),
618                    Pos2::new(
619                        arrow_center.x + arrow_size / 2.0,
620                        arrow_center.y + arrow_size / 4.0,
621                    ),
622                ],
623                Stroke::new(2.0, text_color),
624            );
625        } else {
626            // Down arrow
627            ui.painter().line_segment(
628                [
629                    Pos2::new(
630                        arrow_center.x - arrow_size / 2.0,
631                        arrow_center.y - arrow_size / 4.0,
632                    ),
633                    Pos2::new(arrow_center.x, arrow_center.y + arrow_size / 4.0),
634                ],
635                Stroke::new(2.0, text_color),
636            );
637            ui.painter().line_segment(
638                [
639                    Pos2::new(arrow_center.x, arrow_center.y + arrow_size / 4.0),
640                    Pos2::new(
641                        arrow_center.x + arrow_size / 2.0,
642                        arrow_center.y - arrow_size / 4.0,
643                    ),
644                ],
645                Stroke::new(2.0, text_color),
646            );
647        }
648
649        // Show dropdown if open
650        if open {
651            // Calculate available space below and above
652            let available_space_below = ui.max_rect().max.y - rect.max.y - 4.0;
653            let available_space_above = rect.min.y - ui.max_rect().min.y - 4.0;
654
655            let item_height = 48.0;
656            let dropdown_padding = 16.0;
657            
658            // Use menu_max_height if specified, otherwise use available space
659            let effective_max_height = if let Some(max_h) = self.menu_max_height {
660                max_h
661            } else {
662                available_space_below.max(available_space_above)
663            };
664            
665            let max_items_below =
666                ((available_space_below.min(effective_max_height) - dropdown_padding) / item_height).floor() as usize;
667            let max_items_above =
668                ((available_space_above.min(effective_max_height) - dropdown_padding) / item_height).floor() as usize;
669
670            // Determine dropdown position and size
671            let (dropdown_y, visible_items, scroll_needed) = if max_items_below
672                >= self.options.len()
673            {
674                // Fit below
675                (rect.max.y + 4.0, self.options.len(), false)
676            } else if max_items_above >= self.options.len() {
677                // Fit above
678                let dropdown_height = self.options.len() as f32 * item_height + dropdown_padding;
679                (
680                    rect.min.y - 4.0 - dropdown_height,
681                    self.options.len(),
682                    false,
683                )
684            } else if max_items_below >= max_items_above {
685                // Partial fit below with scroll
686                (rect.max.y + 4.0, max_items_below.max(3), true)
687            } else {
688                // Partial fit above with scroll
689                let visible_items = max_items_above.max(3);
690                let dropdown_height = visible_items as f32 * item_height + dropdown_padding;
691                (rect.min.y - 4.0 - dropdown_height, visible_items, true)
692            };
693
694            let dropdown_height = visible_items as f32 * item_height + dropdown_padding;
695            
696            // Use menu_width if specified, otherwise use field width
697            let menu_width = self.menu_width.unwrap_or(width);
698            let menu_border_radius = self.border_radius.unwrap_or(8.0);
699            
700            let dropdown_rect = Rect::from_min_size(
701                Pos2::new(rect.min.x, dropdown_y),
702                Vec2::new(menu_width, dropdown_height),
703            );
704
705            // Use page background color as specified
706            let dropdown_bg_color = ui.visuals().window_fill;
707
708            // Draw dropdown background with proper elevation
709            ui.painter()
710                .rect_filled(dropdown_rect, menu_border_radius, dropdown_bg_color);
711
712            // Draw dropdown border with elevation shadow
713            ui.painter().rect_stroke(
714                dropdown_rect,
715                menu_border_radius,
716                Stroke::new(1.0, outline),
717                egui::epaint::StrokeKind::Outside,
718            );
719
720            // Draw subtle elevation shadow
721            let shadow_color = Color32::from_rgba_premultiplied(0, 0, 0, 20);
722            ui.painter().rect_filled(
723                dropdown_rect.translate(Vec2::new(0.0, 2.0)),
724                menu_border_radius,
725                shadow_color,
726            );
727
728            // Render options with scrolling support and edge attachment
729            if scroll_needed && visible_items < self.options.len() {
730                // Use scroll area for overflow with edge attachment
731                let scroll_area_rect = Rect::from_min_size(
732                    Pos2::new(dropdown_rect.min.x + 8.0, dropdown_rect.min.y + 8.0),
733                    Vec2::new(menu_width - 16.0, dropdown_height - 16.0),
734                );
735
736                ui.scope_builder(egui::UiBuilder::new().max_rect(scroll_area_rect), |ui| {
737                    egui::ScrollArea::vertical()
738                        .max_height(dropdown_height - 16.0)
739                        .scroll_bar_visibility(
740                            egui::scroll_area::ScrollBarVisibility::VisibleWhenNeeded,
741                        )
742                        .auto_shrink([false; 2])
743                        .show(ui, |ui| {
744                            for option in &self.options {
745                                // Create custom option layout with proper text styling
746                                let option_height = 48.0;
747                                let (option_rect, option_response) = ui.allocate_exact_size(
748                                    Vec2::new(ui.available_width(), option_height),
749                                    Sense::click(),
750                                );
751
752                                // Match select field styling
753                                let is_selected = *self.selected == Some(option.value);
754                                let option_bg_color = if is_selected {
755                                    Color32::from_rgba_premultiplied(
756                                        on_surface.r(),
757                                        on_surface.g(),
758                                        on_surface.b(),
759                                        30,
760                                    )
761                                } else if option_response.hovered() {
762                                    Color32::from_rgba_premultiplied(
763                                        on_surface.r(),
764                                        on_surface.g(),
765                                        on_surface.b(),
766                                        20,
767                                    )
768                                } else {
769                                    Color32::TRANSPARENT
770                                };
771
772                                if option_bg_color != Color32::TRANSPARENT {
773                                    ui.painter().rect_filled(option_rect, 4.0, option_bg_color);
774                                }
775
776                                // Use same font as select field with text wrapping
777                                let text_pos =
778                                    Pos2::new(option_rect.min.x + 16.0, option_rect.center().y);
779                                let text_color = if is_selected {
780                                    get_global_color("primary")
781                                } else {
782                                    on_surface
783                                };
784
785                                // Handle text wrapping for long content
786                                let available_width = option_rect.width() - 32.0; // Account for padding
787                                let galley = ui.fonts(|f| {
788                                    f.layout_job(egui::text::LayoutJob {
789                                        text: option.text.clone(),
790                                        sections: vec![egui::text::LayoutSection {
791                                            leading_space: 0.0,
792                                            byte_range: 0..option.text.len(),
793                                            format: egui::TextFormat {
794                                                font_id: select_font.clone(),
795                                                color: text_color,
796                                                ..Default::default()
797                                            },
798                                        }],
799                                        wrap: egui::text::TextWrapping {
800                                            max_width: available_width,
801                                            ..Default::default()
802                                        },
803                                        break_on_newline: true,
804                                        halign: egui::Align::LEFT,
805                                        justify: false,
806                                        first_row_min_height: 0.0,
807                                        round_output_to_gui: true,
808                                    })
809                                });
810
811                                ui.painter().galley(text_pos, galley, text_color);
812
813                                if option_response.clicked() {
814                                    *self.selected = Some(option.value);
815                                    if !self.keep_open_on_select {
816                                        open = false;
817                                        ui.memory_mut(|mem| {
818                                            mem.data.insert_temp(select_id, open);
819                                            mem.data.remove::<egui::Id>(global_open_select_id);
820                                        });
821                                    }
822                                    response.mark_changed();
823                                }
824                            }
825                        });
826                });
827            } else {
828                // Draw options normally without scrolling
829                let mut current_y = dropdown_rect.min.y + 8.0;
830                let items_to_show = visible_items.min(self.options.len());
831
832                for option in self.options.iter().take(items_to_show) {
833                    let option_rect = Rect::from_min_size(
834                        Pos2::new(dropdown_rect.min.x + 8.0, current_y),
835                        Vec2::new(menu_width - 16.0, item_height),
836                    );
837
838                    let option_response = ui.interact(
839                        option_rect,
840                        egui::Id::new(("select_option", option.value, option.text.clone())),
841                        Sense::click(),
842                    );
843
844                    // Highlight selected option
845                    let is_selected = *self.selected == Some(option.value);
846                    let option_bg_color = if is_selected {
847                        Color32::from_rgba_premultiplied(
848                            on_surface.r(),
849                            on_surface.g(),
850                            on_surface.b(),
851                            30,
852                        )
853                    } else if option_response.hovered() {
854                        Color32::from_rgba_premultiplied(
855                            on_surface.r(),
856                            on_surface.g(),
857                            on_surface.b(),
858                            20,
859                        )
860                    } else {
861                        Color32::TRANSPARENT
862                    };
863
864                    if option_bg_color != Color32::TRANSPARENT {
865                        ui.painter().rect_filled(option_rect, 4.0, option_bg_color);
866                    }
867
868                    if option_response.clicked() {
869                        *self.selected = Some(option.value);
870                        if !self.keep_open_on_select {
871                            open = false;
872                            ui.memory_mut(|mem| {
873                                mem.data.insert_temp(select_id, open);
874                                mem.data.remove::<egui::Id>(global_open_select_id);
875                            });
876                        }
877                        response.mark_changed();
878                    }
879
880                    let text_pos = Pos2::new(option_rect.min.x + 16.0, option_rect.center().y);
881                    let text_color = if is_selected {
882                        get_global_color("primary")
883                    } else {
884                        on_surface
885                    };
886
887                    // Handle text wrapping for long content
888                    let available_width = option_rect.width() - 32.0; // Account for padding
889                    let galley = ui.fonts(|f| {
890                        f.layout_job(egui::text::LayoutJob {
891                            text: option.text.clone(),
892                            sections: vec![egui::text::LayoutSection {
893                                leading_space: 0.0,
894                                byte_range: 0..option.text.len(),
895                                format: egui::TextFormat {
896                                    font_id: select_font.clone(),
897                                    color: text_color,
898                                    ..Default::default()
899                                },
900                            }],
901                            wrap: egui::text::TextWrapping {
902                                max_width: available_width,
903                                ..Default::default()
904                            },
905                            break_on_newline: true,
906                            halign: egui::Align::LEFT,
907                            justify: false,
908                            first_row_min_height: 0.0,
909                            round_output_to_gui: true,
910                        })
911                    });
912
913                    ui.painter().galley(text_pos, galley, text_color);
914
915                    current_y += item_height;
916                }
917            }
918        }
919        
920        // Reserve space for dropdown menu when open to prevent overlap
921        if open {
922            let item_height = 48.0;
923            let dropdown_padding = 16.0;
924            let effective_max_height = self.menu_max_height.unwrap_or(300.0);
925            let estimated_dropdown_height = (self.options.len() as f32 * item_height + dropdown_padding).min(effective_max_height);
926            ui.add_space(estimated_dropdown_height + 8.0); // Add space for dropdown + margin
927        }
928        
929        // Draw helper text or error text below the field
930        if let Some(ref error) = self.error_text {
931            let error_font = FontId::new(12.0, FontFamily::Proportional);
932            let error_pos = Pos2::new(rect.min.x + 16.0, rect.max.y + 4.0);
933            ui.painter().text(
934                error_pos,
935                egui::Align2::LEFT_TOP,
936                error,
937                error_font,
938                error_color,
939            );
940        } else if let Some(ref helper) = self.helper_text {
941            let helper_font = FontId::new(12.0, FontFamily::Proportional);
942            let helper_pos = Pos2::new(rect.min.x + 16.0, rect.max.y + 4.0);
943            ui.painter().text(
944                helper_pos,
945                egui::Align2::LEFT_TOP,
946                helper,
947                helper_font,
948                on_surface_variant,
949            );
950        }
951
952        response
953    }
954}
955
956/// Convenience function to create a select component.
957///
958/// Shorthand for `MaterialSelect::new()`.
959///
960/// # Arguments
961/// * `selected` - Mutable reference to the currently selected option value
962///
963/// # Example
964/// ```rust
965/// # egui::__run_test_ui(|ui| {
966/// let mut selection = Some(1);
967/// ui.add(select(&mut selection)
968///     .option(0, "Option 1")
969///     .option(1, "Option 2"));
970/// # });
971/// ```
972pub fn select<'a>(selected: &'a mut Option<usize>) -> MaterialSelect<'a> {
973    MaterialSelect::new(selected)
974}