1use crate::ext::ArmasContextExt;
12use crate::Theme;
13use egui::{
14 vec2, Color32, CornerRadius, Key, Painter, Rect, Response, Sense, Stroke, TextEdit, Ui,
15};
16
17const TRIGGER_HEIGHT: f32 = 36.0;
22const ITEM_HEIGHT: f32 = 32.0;
23const ITEM_HEIGHT_WITH_DESC: f32 = 48.0;
24const CORNER_RADIUS: u8 = 6;
25const CORNER_RADIUS_SM: u8 = 4;
26const PADDING: f32 = 8.0;
27const ICON_WIDTH: f32 = 24.0;
28
29#[derive(Clone, Debug)]
35pub struct SelectOption {
36 pub value: String,
38 pub label: String,
40 pub icon: Option<String>,
42 pub description: Option<String>,
44 pub disabled: bool,
46}
47
48impl SelectOption {
49 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
51 Self {
52 value: value.into(),
53 label: label.into(),
54 icon: None,
55 description: None,
56 disabled: false,
57 }
58 }
59
60 #[must_use]
62 pub fn icon(mut self, icon: impl Into<String>) -> Self {
63 self.icon = Some(icon.into());
64 self
65 }
66
67 #[must_use]
69 pub fn description(mut self, description: impl Into<String>) -> Self {
70 self.description = Some(description.into());
71 self
72 }
73
74 #[must_use]
76 pub const fn disabled(mut self, disabled: bool) -> Self {
77 self.disabled = disabled;
78 self
79 }
80}
81
82pub struct Select {
107 id: Option<egui::Id>,
108 options: Vec<SelectOption>,
109 selected_value: Option<String>,
110 is_open: bool,
111 search_text: String,
112 filtered_indices: Vec<usize>,
113 highlighted_index: Option<usize>,
114 label: Option<String>,
115 placeholder: String,
116 width: Option<f32>,
117 custom_height: Option<f32>,
118 max_height: f32,
119 searchable: bool,
120 custom_font_size: Option<f32>,
121 custom_corner_radius: Option<u8>,
122 custom_padding_x: Option<f32>,
123}
124
125impl Select {
126 #[must_use]
128 pub fn new(options: Vec<SelectOption>) -> Self {
129 let filtered_indices: Vec<usize> = (0..options.len()).collect();
130 Self {
131 id: None,
132 options,
133 selected_value: None,
134 is_open: false,
135 search_text: String::new(),
136 filtered_indices,
137 highlighted_index: None,
138 label: None,
139 placeholder: "Select an option...".to_string(),
140 width: None,
141 custom_height: None,
142 max_height: 300.0,
143 searchable: true,
144 custom_font_size: None,
145 custom_corner_radius: None,
146 custom_padding_x: None,
147 }
148 }
149
150 pub fn build(builder: impl FnOnce(&mut SelectBuilder)) -> Self {
152 let mut select_builder = SelectBuilder {
153 options: Vec::new(),
154 };
155 builder(&mut select_builder);
156 Self::new(select_builder.options)
157 }
158
159 #[must_use]
161 pub fn id(mut self, id: impl Into<egui::Id>) -> Self {
162 self.id = Some(id.into());
163 self
164 }
165
166 #[must_use]
168 pub fn selected(mut self, value: impl Into<String>) -> Self {
169 self.selected_value = Some(value.into());
170 self
171 }
172
173 #[must_use]
175 pub fn label(mut self, label: impl Into<String>) -> Self {
176 self.label = Some(label.into());
177 self
178 }
179
180 #[must_use]
182 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
183 self.placeholder = placeholder.into();
184 self
185 }
186
187 #[must_use]
189 pub const fn width(mut self, width: f32) -> Self {
190 self.width = Some(width);
191 self
192 }
193
194 #[must_use]
196 pub const fn height(mut self, height: f32) -> Self {
197 self.custom_height = Some(height);
198 self
199 }
200
201 #[must_use]
203 pub const fn max_height(mut self, height: f32) -> Self {
204 self.max_height = height;
205 self
206 }
207
208 #[must_use]
210 pub const fn searchable(mut self, searchable: bool) -> Self {
211 self.searchable = searchable;
212 self
213 }
214
215 #[must_use]
217 pub const fn font_size(mut self, size: f32) -> Self {
218 self.custom_font_size = Some(size);
219 self
220 }
221
222 #[must_use]
224 pub const fn corner_radius(mut self, radius: u8) -> Self {
225 self.custom_corner_radius = Some(radius);
226 self
227 }
228
229 #[must_use]
231 pub const fn padding_x(mut self, padding: f32) -> Self {
232 self.custom_padding_x = Some(padding);
233 self
234 }
235
236 #[must_use]
238 pub fn selected_value(&self) -> Option<&str> {
239 self.selected_value.as_deref()
240 }
241
242 pub fn set_selected(&mut self, value: Option<String>) {
244 self.selected_value = value;
245 }
246
247 pub fn show(&mut self, ui: &mut Ui) -> SelectResponse {
253 let theme = ui.ctx().armas_theme();
254 let avail = ui.available_width();
255 let width = self
256 .width
257 .unwrap_or_else(|| if avail.is_finite() { avail } else { 200.0 });
258 let mut changed = false;
259 let mut new_value = None;
260
261 self.load_state(ui);
262
263 ui.vertical(|ui| {
264 ui.spacing_mut().item_spacing.y = theme.spacing.xs;
265
266 self.show_label(ui, &theme);
267 let (button_rect, response) = self.show_trigger(ui, &theme, width);
268
269 if response.clicked() {
270 self.toggle_dropdown();
271 }
272
273 if self.is_open {
274 let dropdown_response = self.show_dropdown(ui, &theme, button_rect, width);
275 if let Some(value) = dropdown_response.selected_value {
276 self.selected_value = Some(value.clone());
277 new_value = Some(value);
278 changed = true;
279 self.is_open = false;
280 }
281 if dropdown_response.should_close {
282 self.is_open = false;
283 }
284 }
285
286 self.save_state(ui);
287
288 SelectResponse {
289 response,
290 changed,
291 selected_value: new_value,
292 is_open: self.is_open,
293 }
294 })
295 .inner
296 }
297
298 fn load_state(&mut self, ui: &Ui) {
303 let Some(id) = self.id else { return };
304 let state_id = id.with("select_state");
305 let stored: Option<(Option<String>, bool, String, Option<usize>)> =
306 ui.ctx().data_mut(|d| d.get_temp(state_id));
307
308 if let Some((selected_value, is_open, search_text, highlighted_index)) = stored {
309 self.selected_value = selected_value;
310 self.is_open = is_open;
311 self.search_text = search_text;
312 self.highlighted_index = highlighted_index;
313 if !self.search_text.is_empty() {
314 self.update_filter();
315 }
316 }
317 }
318
319 fn save_state(&self, ui: &Ui) {
320 let Some(id) = self.id else { return };
321 let state_id = id.with("select_state");
322 ui.ctx().data_mut(|d| {
323 d.insert_temp(
324 state_id,
325 (
326 self.selected_value.clone(),
327 self.is_open,
328 self.search_text.clone(),
329 self.highlighted_index,
330 ),
331 );
332 });
333 }
334
335 fn show_label(&self, ui: &mut Ui, theme: &Theme) {
340 if let Some(label) = &self.label {
341 ui.label(
342 egui::RichText::new(label)
343 .size(theme.typography.base)
344 .color(theme.foreground()),
345 );
346 }
347 }
348
349 fn show_trigger(&self, ui: &mut Ui, theme: &Theme, width: f32) -> (Rect, Response) {
350 let avail_h = ui.available_height();
351 let default_h = if avail_h.is_finite() && avail_h > 0.0 && avail_h < TRIGGER_HEIGHT {
352 avail_h
353 } else {
354 TRIGGER_HEIGHT
355 };
356 let height = self.custom_height.unwrap_or(default_h);
357 let (rect, response) = ui.allocate_exact_size(vec2(width, height), Sense::click());
358
359 if ui.is_rect_visible(rect) {
360 self.paint_trigger(ui.painter(), rect, &response, theme, height);
361 }
362
363 (rect, response)
364 }
365
366 fn paint_trigger(
367 &self,
368 painter: &Painter,
369 rect: Rect,
370 response: &Response,
371 theme: &Theme,
372 height: f32,
373 ) {
374 let hovered = response.hovered();
375 let is_focused = self.is_open;
376 let cr = self.custom_corner_radius.unwrap_or(CORNER_RADIUS);
377 let corner_radius = CornerRadius::same(cr);
378
379 let bg_color = if hovered && !is_focused {
381 let input = theme.input();
382 Color32::from_rgba_unmultiplied(input.r(), input.g(), input.b(), 128)
383 } else {
384 Color32::TRANSPARENT
385 };
386 painter.rect_filled(rect, corner_radius, bg_color);
387
388 let border_color = if is_focused {
390 theme.ring()
391 } else {
392 theme.input()
393 };
394 painter.rect_stroke(
395 rect,
396 corner_radius,
397 Stroke::new(1.0, border_color),
398 egui::StrokeKind::Inside,
399 );
400
401 if is_focused {
403 let ring_color = {
404 let r = theme.ring();
405 Color32::from_rgba_unmultiplied(r.r(), r.g(), r.b(), 128)
406 };
407 painter.rect_stroke(
408 rect.expand(2.0),
409 CornerRadius::same(cr + 2),
410 Stroke::new(2.0, ring_color),
411 egui::StrokeKind::Outside,
412 );
413 }
414
415 let font_size = self.custom_font_size.unwrap_or_else(|| {
417 if height < 30.0 {
418 (height * 0.55).max(8.0)
419 } else {
420 theme.typography.base
421 }
422 });
423 let padding_x = self.custom_padding_x.unwrap_or_else(|| {
424 if height < 30.0 {
425 (height * 0.3).max(4.0)
426 } else {
427 12.0
428 }
429 });
430
431 let display_text = self.get_display_text();
433 let text_color = if self.selected_value.is_some() {
434 theme.foreground()
435 } else {
436 theme.muted_foreground()
437 };
438 painter.text(
439 rect.left_center() + vec2(padding_x, 0.0),
440 egui::Align2::LEFT_CENTER,
441 display_text,
442 egui::FontId::proportional(font_size),
443 text_color,
444 );
445
446 let tri_size = if height < 30.0 {
448 (height * 0.15).max(2.5)
449 } else {
450 4.0
451 };
452 let center = rect.right_center() - vec2(padding_x + tri_size, 0.0);
453 let triangle = if self.is_open {
454 vec![
456 egui::pos2(center.x, center.y - tri_size),
457 egui::pos2(center.x - tri_size, center.y + tri_size * 0.6),
458 egui::pos2(center.x + tri_size, center.y + tri_size * 0.6),
459 ]
460 } else {
461 vec![
463 egui::pos2(center.x, center.y + tri_size),
464 egui::pos2(center.x - tri_size, center.y - tri_size * 0.6),
465 egui::pos2(center.x + tri_size, center.y - tri_size * 0.6),
466 ]
467 };
468 painter.add(egui::Shape::convex_polygon(
469 triangle,
470 theme.muted_foreground(),
471 Stroke::NONE,
472 ));
473 }
474
475 fn get_display_text(&self) -> &str {
476 if let Some(selected) = &self.selected_value {
477 self.options
478 .iter()
479 .find(|opt| opt.value == *selected)
480 .map_or(&self.placeholder, |opt| opt.label.as_str())
481 } else {
482 &self.placeholder
483 }
484 }
485
486 fn toggle_dropdown(&mut self) {
487 self.is_open = !self.is_open;
488 if self.is_open {
489 self.search_text.clear();
490 self.update_filter();
491 self.highlighted_index = self.filtered_indices.first().copied();
492 }
493 }
494
495 fn show_dropdown(
500 &mut self,
501 ui: &mut Ui,
502 theme: &Theme,
503 button_rect: Rect,
504 width: f32,
505 ) -> DropdownResponse {
506 let mut selected_value = None;
507 let mut should_close = false;
508
509 let dropdown_id = ui.id().with("dropdown");
510 let area_response = egui::Area::new(dropdown_id)
511 .fixed_pos(button_rect.left_bottom() + vec2(0.0, 4.0))
512 .order(egui::Order::Foreground)
513 .show(ui.ctx(), |ui| {
514 egui::Frame::new()
515 .fill(theme.popover())
516 .stroke(Stroke::new(1.0, theme.border()))
517 .corner_radius(CornerRadius::same(
518 self.custom_corner_radius.unwrap_or(CORNER_RADIUS),
519 ))
520 .inner_margin(4.0)
521 .shadow(egui::epaint::Shadow {
522 offset: [0, 4],
523 blur: 8,
524 spread: 0,
525 color: Color32::from_black_alpha(60),
526 })
527 .show(ui, |ui| {
528 ui.set_width(width - 8.0);
529
530 if self.searchable {
531 should_close |= self.show_search_box(ui, width);
532 self.show_separator(ui, theme, width);
533 }
534
535 self.show_options_list(ui, theme, width, &mut selected_value);
536 });
537 });
538
539 should_close |= self.handle_keyboard_input(ui, &mut selected_value);
540 should_close |=
541 self.should_close_on_click_outside(ui, &area_response.response, button_rect);
542
543 DropdownResponse {
544 selected_value,
545 should_close,
546 }
547 }
548
549 fn show_search_box(&mut self, ui: &mut Ui, width: f32) -> bool {
550 let search_response = ui.add(
551 TextEdit::singleline(&mut self.search_text)
552 .hint_text("Search...")
553 .desired_width(width - 16.0)
554 .frame(true),
555 );
556
557 if search_response.changed() {
558 self.update_filter();
559 self.highlighted_index = self.filtered_indices.first().copied();
560 }
561
562 ui.input(|i| i.key_pressed(Key::Escape))
563 }
564
565 fn show_separator(&self, ui: &mut Ui, theme: &Theme, width: f32) {
566 ui.add_space(4.0);
567 let sep_rect = ui.available_rect_before_wrap();
568 let sep_rect = Rect::from_min_size(sep_rect.min, vec2(width - 16.0, 1.0));
569 ui.painter().rect_filled(sep_rect, 0.0, theme.border());
570 ui.allocate_space(vec2(width - 16.0, 1.0));
571 ui.add_space(4.0);
572 }
573
574 fn show_options_list(
575 &mut self,
576 ui: &mut Ui,
577 theme: &Theme,
578 width: f32,
579 selected_value: &mut Option<String>,
580 ) {
581 egui::ScrollArea::vertical()
582 .max_height(self.max_height)
583 .show(ui, |ui| {
584 if self.filtered_indices.is_empty() {
585 ui.label(
586 egui::RichText::new("No results found.")
587 .color(theme.muted_foreground())
588 .size(theme.typography.base),
589 );
590 return;
591 }
592
593 let indices = self.filtered_indices.clone();
594 for option_idx in indices {
595 let option = self.options[option_idx].clone();
596
597 if option.disabled {
598 self.show_disabled_option(ui, &option, theme, width);
599 } else if let Some(value) =
600 self.show_option(ui, &option, option_idx, theme, width)
601 {
602 *selected_value = Some(value);
603 }
604 }
605 });
606 }
607
608 fn item_height(&self) -> f32 {
609 self.custom_height.unwrap_or(ITEM_HEIGHT)
610 }
611
612 fn item_font_size(&self) -> f32 {
613 self.custom_font_size.unwrap_or_else(|| {
614 let h = self.item_height();
615 if h < 30.0 {
616 (h * 0.55).max(8.0)
617 } else {
618 14.0
619 }
620 })
621 }
622
623 fn show_disabled_option(&self, ui: &mut Ui, option: &SelectOption, theme: &Theme, width: f32) {
624 let (rect, _) =
625 ui.allocate_exact_size(vec2(width - 16.0, self.item_height()), Sense::hover());
626
627 if !ui.is_rect_visible(rect) {
628 return;
629 }
630
631 let content_rect = rect.shrink2(vec2(PADDING, 0.0));
632 let color = theme.muted_foreground().linear_multiply(0.5);
633 let mut label_x = 0.0;
634
635 let font_size = self.item_font_size();
636 if let Some(icon) = &option.icon {
637 ui.painter().text(
638 content_rect.left_center(),
639 egui::Align2::LEFT_CENTER,
640 icon,
641 egui::FontId::proportional(font_size),
642 color,
643 );
644 label_x = ICON_WIDTH;
645 }
646
647 ui.painter().text(
648 content_rect.left_center() + vec2(label_x, 0.0),
649 egui::Align2::LEFT_CENTER,
650 &option.label,
651 egui::FontId::proportional(font_size),
652 color,
653 );
654 }
655
656 fn show_option(
657 &mut self,
658 ui: &mut Ui,
659 option: &SelectOption,
660 option_idx: usize,
661 theme: &Theme,
662 width: f32,
663 ) -> Option<String> {
664 let is_highlighted = self.highlighted_index == Some(option_idx);
665 let base_height = self.item_height();
666 let height = if option.description.is_some() {
667 base_height + (ITEM_HEIGHT_WITH_DESC - ITEM_HEIGHT)
668 } else {
669 base_height
670 };
671
672 let (rect, response) = ui.allocate_exact_size(vec2(width - 16.0, height), Sense::click());
673
674 if !ui.is_rect_visible(rect) {
675 return None;
676 }
677
678 let is_active = is_highlighted || response.hovered();
679
680 if is_active {
682 ui.painter()
683 .rect_filled(rect, CornerRadius::same(CORNER_RADIUS_SM), theme.accent());
684 }
685
686 let text_color = if is_active {
688 theme.accent_foreground()
689 } else {
690 theme.popover_foreground()
691 };
692 self.paint_option_content(ui.painter(), rect, option, text_color, theme);
693
694 if response.hovered() {
696 self.highlighted_index = Some(option_idx);
697 }
698
699 if response.clicked() {
700 Some(option.value.clone())
701 } else {
702 None
703 }
704 }
705
706 fn paint_option_content(
707 &self,
708 painter: &Painter,
709 rect: Rect,
710 option: &SelectOption,
711 text_color: Color32,
712 theme: &Theme,
713 ) {
714 let font_size = self.item_font_size();
715 let content_rect = rect.shrink2(vec2(PADDING, 0.0));
716 if let Some(icon) = &option.icon {
718 painter.text(
719 content_rect.left_center(),
720 egui::Align2::LEFT_CENTER,
721 icon,
722 egui::FontId::proportional(font_size),
723 text_color,
724 );
725 }
726 let label_x = if option.icon.is_some() {
727 ICON_WIDTH
728 } else {
729 0.0
730 };
731
732 if let Some(description) = &option.description {
734 let label_pos = content_rect.left_top() + vec2(label_x, 6.0);
735 painter.text(
736 label_pos,
737 egui::Align2::LEFT_TOP,
738 &option.label,
739 egui::FontId::proportional(font_size),
740 text_color,
741 );
742 painter.text(
743 label_pos + vec2(0.0, font_size + 4.0),
744 egui::Align2::LEFT_TOP,
745 description,
746 egui::FontId::proportional((font_size - 2.0).max(8.0)),
747 theme.muted_foreground(),
748 );
749 } else {
750 painter.text(
751 content_rect.left_center() + vec2(label_x, 0.0),
752 egui::Align2::LEFT_CENTER,
753 &option.label,
754 egui::FontId::proportional(font_size),
755 text_color,
756 );
757 }
758 }
759
760 fn handle_keyboard_input(&mut self, ui: &Ui, selected_value: &mut Option<String>) -> bool {
765 let mut should_close = false;
766
767 ui.input(|i| {
768 if i.key_pressed(Key::ArrowDown) {
769 self.move_highlight(1);
770 }
771 if i.key_pressed(Key::ArrowUp) {
772 self.move_highlight(-1);
773 }
774 if i.key_pressed(Key::Enter) {
775 if let Some(idx) = self.highlighted_index {
776 let option = &self.options[idx];
777 if !option.disabled {
778 *selected_value = Some(option.value.clone());
779 }
780 }
781 }
782 if i.key_pressed(Key::Escape) {
783 should_close = true;
784 }
785 });
786
787 should_close
788 }
789
790 #[allow(clippy::cast_possible_wrap)]
791 fn move_highlight(&mut self, delta: i32) {
792 let Some(current) = self.highlighted_index else {
793 self.highlighted_index = self.filtered_indices.first().copied();
794 return;
795 };
796
797 let Some(pos) = self.filtered_indices.iter().position(|&idx| idx == current) else {
798 return;
799 };
800
801 let new_pos =
802 (pos as i32 + delta).clamp(0, self.filtered_indices.len() as i32 - 1) as usize;
803 self.highlighted_index = Some(self.filtered_indices[new_pos]);
804 }
805
806 fn should_close_on_click_outside(
807 &self,
808 ui: &Ui,
809 area_response: &Response,
810 button_rect: Rect,
811 ) -> bool {
812 let clicked = ui.input(|i| i.pointer.any_click());
813 let pointer_pos = ui.input(|i| i.pointer.interact_pos()).unwrap_or_default();
814
815 clicked && !area_response.rect.contains(pointer_pos) && !button_rect.contains(pointer_pos)
816 }
817
818 fn update_filter(&mut self) {
823 if self.search_text.is_empty() {
824 self.filtered_indices = (0..self.options.len()).collect();
825 } else {
826 let search_lower = self.search_text.to_lowercase();
827 self.filtered_indices = self
828 .options
829 .iter()
830 .enumerate()
831 .filter(|(_, opt)| {
832 opt.label.to_lowercase().contains(&search_lower)
833 || opt.value.to_lowercase().contains(&search_lower)
834 || opt
835 .description
836 .as_ref()
837 .is_some_and(|d| d.to_lowercase().contains(&search_lower))
838 })
839 .map(|(idx, _)| idx)
840 .collect();
841 }
842
843 if let Some(idx) = self.highlighted_index {
845 if !self.filtered_indices.contains(&idx) {
846 self.highlighted_index = self.filtered_indices.first().copied();
847 }
848 }
849 }
850}
851
852pub struct SelectResponse {
858 pub response: Response,
860 pub changed: bool,
862 pub selected_value: Option<String>,
864 pub is_open: bool,
866}
867
868struct DropdownResponse {
870 selected_value: Option<String>,
871 should_close: bool,
872}
873
874#[doc(hidden)]
880pub struct SelectBuilder {
881 options: Vec<SelectOption>,
882}
883
884impl SelectBuilder {
885 pub fn option(&mut self, value: &str, label: &str) -> SelectOptionBuilder<'_> {
887 self.options.push(SelectOption::new(value, label));
888 let idx = self.options.len() - 1;
889 SelectOptionBuilder {
890 options: &mut self.options,
891 option_index: idx,
892 }
893 }
894}
895
896#[doc(hidden)]
898pub struct SelectOptionBuilder<'a> {
899 options: &'a mut Vec<SelectOption>,
900 option_index: usize,
901}
902
903impl SelectOptionBuilder<'_> {
904 #[must_use]
906 pub fn icon(self, icon: &str) -> Self {
907 if let Some(opt) = self.options.get_mut(self.option_index) {
908 opt.icon = Some(icon.to_string());
909 }
910 self
911 }
912
913 #[must_use]
915 pub fn description(self, description: &str) -> Self {
916 if let Some(opt) = self.options.get_mut(self.option_index) {
917 opt.description = Some(description.to_string());
918 }
919 self
920 }
921
922 #[must_use]
924 pub fn disabled(self, disabled: bool) -> Self {
925 if let Some(opt) = self.options.get_mut(self.option_index) {
926 opt.disabled = disabled;
927 }
928 self
929 }
930}
931
932#[cfg(test)]
937mod tests {
938 use super::*;
939
940 #[test]
941 fn test_select_option_creation() {
942 let option = SelectOption::new("value1", "Label 1")
943 .icon("x")
944 .description("This is a description");
945
946 assert_eq!(option.value, "value1");
947 assert_eq!(option.label, "Label 1");
948 assert_eq!(option.icon, Some("x".to_string()));
949 assert_eq!(
950 option.description,
951 Some("This is a description".to_string())
952 );
953 assert!(!option.disabled);
954 }
955
956 #[test]
957 fn test_select_creation() {
958 let options = vec![
959 SelectOption::new("1", "Option 1"),
960 SelectOption::new("2", "Option 2"),
961 ];
962
963 let select = Select::new(options);
964 assert_eq!(select.options.len(), 2);
965 assert!(select.selected_value.is_none());
966 assert!(!select.is_open);
967 }
968
969 #[test]
970 fn test_select_filtering() {
971 let options = vec![
972 SelectOption::new("apple", "Apple"),
973 SelectOption::new("banana", "Banana"),
974 SelectOption::new("cherry", "Cherry"),
975 ];
976
977 let mut select = Select::new(options);
978 select.search_text = "app".to_string();
979 select.update_filter();
980
981 assert_eq!(select.filtered_indices.len(), 1);
982 assert_eq!(select.filtered_indices[0], 0);
983 }
984}