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}