1use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
27use ratatui::{
28 Frame,
29 buffer::Buffer,
30 layout::Rect,
31 style::{Color, Modifier, Style},
32 text::{Line, Span},
33 widgets::{Block, Borders, Clear, Paragraph, Widget},
34};
35
36use crate::traits::{ClickRegion, FocusId};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum SelectAction {
41 Focus,
43 Open,
45 Close,
47 Select(usize),
49}
50
51#[derive(Debug, Clone)]
53pub struct SelectState {
54 pub selected_index: Option<usize>,
56 pub is_open: bool,
58 pub focused: bool,
60 pub enabled: bool,
62 pub highlighted_index: usize,
64 pub scroll_offset: u16,
66 pub total_options: usize,
68}
69
70impl Default for SelectState {
71 fn default() -> Self {
72 Self {
73 selected_index: None,
74 is_open: false,
75 focused: false,
76 enabled: true,
77 highlighted_index: 0,
78 scroll_offset: 0,
79 total_options: 0,
80 }
81 }
82}
83
84impl SelectState {
85 pub fn new(total_options: usize) -> Self {
87 Self {
88 total_options,
89 ..Default::default()
90 }
91 }
92
93 pub fn with_selected(total_options: usize, selected: usize) -> Self {
95 let mut state = Self::new(total_options);
96 if selected < total_options {
97 state.selected_index = Some(selected);
98 state.highlighted_index = selected;
99 }
100 state
101 }
102
103 pub fn open(&mut self) {
105 if self.enabled {
106 self.is_open = true;
107 if let Some(idx) = self.selected_index {
109 self.highlighted_index = idx;
110 }
111 }
112 }
113
114 pub fn close(&mut self) {
116 self.is_open = false;
117 }
118
119 pub fn toggle(&mut self) {
121 if self.is_open {
122 self.close();
123 } else {
124 self.open();
125 }
126 }
127
128 pub fn highlight_prev(&mut self) {
130 if self.highlighted_index > 0 {
131 self.highlighted_index -= 1;
132 }
133 }
134
135 pub fn highlight_next(&mut self) {
137 if self.highlighted_index + 1 < self.total_options {
138 self.highlighted_index += 1;
139 }
140 }
141
142 pub fn highlight_first(&mut self) {
144 self.highlighted_index = 0;
145 self.scroll_offset = 0;
146 }
147
148 pub fn highlight_last(&mut self) {
150 if self.total_options > 0 {
151 self.highlighted_index = self.total_options - 1;
152 }
153 }
154
155 pub fn select_highlighted(&mut self) {
157 if self.total_options > 0 {
158 self.selected_index = Some(self.highlighted_index);
159 }
160 self.close();
161 }
162
163 pub fn select(&mut self, index: usize) {
165 if index < self.total_options {
166 self.selected_index = Some(index);
167 self.highlighted_index = index;
168 }
169 self.close();
170 }
171
172 pub fn clear_selection(&mut self) {
174 self.selected_index = None;
175 }
176
177 pub fn set_total(&mut self, total: usize) {
179 self.total_options = total;
180 if let Some(idx) = self.selected_index {
181 if idx >= total {
182 self.selected_index = if total > 0 { Some(total - 1) } else { None };
183 }
184 }
185 if self.highlighted_index >= total && total > 0 {
186 self.highlighted_index = total - 1;
187 }
188 }
189
190 pub fn ensure_visible(&mut self, viewport_height: usize) {
192 if viewport_height == 0 {
193 return;
194 }
195 if self.highlighted_index < self.scroll_offset as usize {
196 self.scroll_offset = self.highlighted_index as u16;
197 } else if self.highlighted_index >= self.scroll_offset as usize + viewport_height {
198 self.scroll_offset = (self.highlighted_index - viewport_height + 1) as u16;
199 }
200 }
201
202 pub fn selected(&self) -> Option<usize> {
204 self.selected_index
205 }
206
207 pub fn has_selection(&self) -> bool {
209 self.selected_index.is_some()
210 }
211}
212
213#[derive(Debug, Clone)]
215pub struct SelectStyle {
216 pub focused_border: Color,
218 pub unfocused_border: Color,
220 pub disabled_border: Color,
222 pub text_fg: Color,
224 pub placeholder_fg: Color,
226 pub dropdown_indicator: &'static str,
228 pub highlight_style: Style,
230 pub option_style: Style,
232 pub selected_indicator: &'static str,
234 pub unselected_indicator: &'static str,
236 pub dropdown_border: Color,
238 pub max_visible_options: u16,
240}
241
242impl Default for SelectStyle {
243 fn default() -> Self {
244 Self {
245 focused_border: Color::Yellow,
246 unfocused_border: Color::Gray,
247 disabled_border: Color::DarkGray,
248 text_fg: Color::White,
249 placeholder_fg: Color::DarkGray,
250 dropdown_indicator: "▼",
251 highlight_style: Style::default()
252 .fg(Color::Black)
253 .bg(Color::Yellow)
254 .add_modifier(Modifier::BOLD),
255 option_style: Style::default().fg(Color::White),
256 selected_indicator: "✓ ",
257 unselected_indicator: " ",
258 dropdown_border: Color::Cyan,
259 max_visible_options: 8,
260 }
261 }
262}
263
264impl From<&crate::theme::Theme> for SelectStyle {
265 fn from(theme: &crate::theme::Theme) -> Self {
266 let p = &theme.palette;
267 Self {
268 focused_border: p.border_focused,
269 unfocused_border: p.border,
270 disabled_border: p.border_disabled,
271 text_fg: p.text,
272 placeholder_fg: p.text_placeholder,
273 dropdown_indicator: "▼",
274 highlight_style: Style::default()
275 .fg(p.highlight_fg)
276 .bg(p.highlight_bg)
277 .add_modifier(Modifier::BOLD),
278 option_style: Style::default().fg(p.text),
279 selected_indicator: "✓ ",
280 unselected_indicator: " ",
281 dropdown_border: p.border_accent,
282 max_visible_options: 8,
283 }
284 }
285}
286
287impl SelectStyle {
288 pub fn minimal() -> Self {
290 Self {
291 highlight_style: Style::default()
292 .fg(Color::Yellow)
293 .add_modifier(Modifier::BOLD),
294 ..Default::default()
295 }
296 }
297
298 pub fn arrow() -> Self {
300 Self {
301 selected_indicator: "→ ",
302 unselected_indicator: " ",
303 ..Default::default()
304 }
305 }
306
307 pub fn bracket() -> Self {
309 Self {
310 selected_indicator: "[x] ",
311 unselected_indicator: "[ ] ",
312 ..Default::default()
313 }
314 }
315
316 pub fn max_options(mut self, max: u16) -> Self {
318 self.max_visible_options = max;
319 self
320 }
321
322 pub fn focused_border(mut self, color: Color) -> Self {
324 self.focused_border = color;
325 self
326 }
327
328 pub fn unfocused_border(mut self, color: Color) -> Self {
330 self.unfocused_border = color;
331 self
332 }
333
334 pub fn indicator(mut self, indicator: &'static str) -> Self {
336 self.dropdown_indicator = indicator;
337 self
338 }
339
340 pub fn highlight(mut self, style: Style) -> Self {
342 self.highlight_style = style;
343 self
344 }
345}
346
347type DefaultRenderFn<T> = fn(&T) -> String;
349
350pub struct Select<'a, T, F = DefaultRenderFn<T>>
355where
356 F: Fn(&T) -> String,
357{
358 options: &'a [T],
359 state: &'a SelectState,
360 style: SelectStyle,
361 placeholder: &'a str,
362 label: Option<&'a str>,
363 render_option: F,
364 focus_id: FocusId,
365}
366
367impl<'a, T: std::fmt::Display> Select<'a, T, DefaultRenderFn<T>> {
368 pub fn new(options: &'a [T], state: &'a SelectState) -> Self {
370 Self {
371 options,
372 state,
373 style: SelectStyle::default(),
374 placeholder: "Please select an option",
375 label: None,
376 render_option: |opt| opt.to_string(),
377 focus_id: FocusId::default(),
378 }
379 }
380}
381
382impl<'a, T, F> Select<'a, T, F>
383where
384 F: Fn(&T) -> String,
385{
386 pub fn render_option<G>(self, render_fn: G) -> Select<'a, T, G>
388 where
389 G: Fn(&T) -> String,
390 {
391 Select {
392 options: self.options,
393 state: self.state,
394 style: self.style,
395 placeholder: self.placeholder,
396 label: self.label,
397 render_option: render_fn,
398 focus_id: self.focus_id,
399 }
400 }
401
402 pub fn placeholder(mut self, placeholder: &'a str) -> Self {
404 self.placeholder = placeholder;
405 self
406 }
407
408 pub fn label(mut self, label: &'a str) -> Self {
410 self.label = Some(label);
411 self
412 }
413
414 pub fn style(mut self, style: SelectStyle) -> Self {
416 self.style = style;
417 self
418 }
419
420 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
422 self.style(SelectStyle::from(theme))
423 }
424
425 pub fn focus_id(mut self, id: FocusId) -> Self {
427 self.focus_id = id;
428 self
429 }
430
431 pub fn render_stateful(self, frame: &mut Frame, area: Rect) -> ClickRegion<SelectAction> {
436 let border_color = if !self.state.enabled {
437 self.style.disabled_border
438 } else if self.state.focused {
439 self.style.focused_border
440 } else {
441 self.style.unfocused_border
442 };
443
444 let mut block = Block::default()
445 .borders(Borders::ALL)
446 .border_style(Style::default().fg(border_color));
447
448 if let Some(label) = self.label {
449 block = block.title(format!(" {} ", label));
450 }
451
452 let inner = block.inner(area);
453 frame.render_widget(block, area);
454
455 let display_text = if let Some(idx) = self.state.selected_index {
457 if idx < self.options.len() {
458 let text = (self.render_option)(&self.options[idx]);
459 Span::styled(text, Style::default().fg(self.style.text_fg))
460 } else {
461 Span::styled(
462 self.placeholder,
463 Style::default().fg(self.style.placeholder_fg),
464 )
465 }
466 } else {
467 Span::styled(
468 self.placeholder,
469 Style::default().fg(self.style.placeholder_fg),
470 )
471 };
472
473 let indicator_color = if self.state.focused {
475 self.style.focused_border
476 } else {
477 self.style.unfocused_border
478 };
479
480 let indicator = Span::styled(
481 format!(" {}", self.style.dropdown_indicator),
482 Style::default().fg(indicator_color),
483 );
484
485 let line = Line::from(vec![display_text, indicator]);
486 let paragraph = Paragraph::new(line);
487 frame.render_widget(paragraph, inner);
488
489 ClickRegion::new(area, SelectAction::Focus)
490 }
491
492 pub fn render_dropdown(
502 &self,
503 frame: &mut Frame,
504 anchor: Rect,
505 screen: Rect,
506 ) -> Vec<ClickRegion<SelectAction>> {
507 let mut regions = Vec::new();
508
509 if self.options.is_empty() {
510 return regions;
511 }
512
513 let visible_count = (self.options.len() as u16).min(self.style.max_visible_options);
514 let dropdown_height = visible_count + 2; let dropdown_width = anchor.width;
517
518 let space_below = screen.height.saturating_sub(anchor.y + anchor.height);
520 let space_above = anchor.y.saturating_sub(screen.y);
521
522 let (dropdown_y, flip_up) = if space_below >= dropdown_height {
523 (anchor.y + anchor.height, false)
524 } else if space_above >= dropdown_height {
525 (anchor.y.saturating_sub(dropdown_height), true)
526 } else {
527 (anchor.y + anchor.height, false)
529 };
530
531 let dropdown_area = Rect::new(
532 anchor.x,
533 dropdown_y,
534 dropdown_width,
535 dropdown_height.min(if flip_up { space_above } else { space_below }),
536 );
537
538 frame.render_widget(Clear, dropdown_area);
540
541 let block = Block::default()
543 .borders(Borders::ALL)
544 .border_style(Style::default().fg(self.style.dropdown_border));
545
546 let inner = block.inner(dropdown_area);
547 frame.render_widget(block, dropdown_area);
548
549 let actual_visible = inner.height as usize;
551 let scroll = self.state.scroll_offset as usize;
552
553 for (i, option) in self
554 .options
555 .iter()
556 .enumerate()
557 .skip(scroll)
558 .take(actual_visible)
559 {
560 let y = inner.y + (i - scroll) as u16;
561 let option_area = Rect::new(inner.x, y, inner.width, 1);
562
563 let is_highlighted = i == self.state.highlighted_index;
564 let is_selected = self.state.selected_index == Some(i);
565
566 let style = if is_highlighted {
567 self.style.highlight_style
568 } else {
569 self.style.option_style
570 };
571
572 let prefix = if is_selected {
573 self.style.selected_indicator
574 } else {
575 self.style.unselected_indicator
576 };
577
578 let text = format!("{}{}", prefix, (self.render_option)(option));
579
580 let max_width = inner.width as usize;
582 let display_text: String = text.chars().take(max_width).collect();
583
584 let paragraph = Paragraph::new(Span::styled(display_text, style));
585 frame.render_widget(paragraph, option_area);
586
587 regions.push(ClickRegion::new(option_area, SelectAction::Select(i)));
589 }
590
591 regions
592 }
593
594 pub fn render_to_buffer(self, area: Rect, buf: &mut Buffer) -> ClickRegion<SelectAction> {
598 let border_color = if !self.state.enabled {
599 self.style.disabled_border
600 } else if self.state.focused {
601 self.style.focused_border
602 } else {
603 self.style.unfocused_border
604 };
605
606 let mut block = Block::default()
607 .borders(Borders::ALL)
608 .border_style(Style::default().fg(border_color));
609
610 if let Some(label) = self.label {
611 block = block.title(format!(" {} ", label));
612 }
613
614 let inner = block.inner(area);
615 block.render(area, buf);
616
617 let display_text = if let Some(idx) = self.state.selected_index {
619 if idx < self.options.len() {
620 let text = (self.render_option)(&self.options[idx]);
621 Span::styled(text, Style::default().fg(self.style.text_fg))
622 } else {
623 Span::styled(
624 self.placeholder,
625 Style::default().fg(self.style.placeholder_fg),
626 )
627 }
628 } else {
629 Span::styled(
630 self.placeholder,
631 Style::default().fg(self.style.placeholder_fg),
632 )
633 };
634
635 let indicator_color = if self.state.focused {
636 self.style.focused_border
637 } else {
638 self.style.unfocused_border
639 };
640
641 let indicator = Span::styled(
642 format!(" {}", self.style.dropdown_indicator),
643 Style::default().fg(indicator_color),
644 );
645
646 let line = Line::from(vec![display_text, indicator]);
647 let paragraph = Paragraph::new(line);
648 paragraph.render(inner, buf);
649
650 ClickRegion::new(area, SelectAction::Focus)
651 }
652}
653
654pub fn handle_select_key(key: &KeyEvent, state: &mut SelectState) -> Option<SelectAction> {
673 if !state.enabled {
674 return None;
675 }
676
677 if state.is_open {
678 match key.code {
680 KeyCode::Esc => {
681 state.close();
682 Some(SelectAction::Close)
683 }
684 KeyCode::Enter | KeyCode::Char(' ') => {
685 let idx = state.highlighted_index;
686 state.select_highlighted();
687 Some(SelectAction::Select(idx))
688 }
689 KeyCode::Up => {
690 state.highlight_prev();
691 state.ensure_visible(8); None
693 }
694 KeyCode::Down => {
695 state.highlight_next();
696 state.ensure_visible(8);
697 None
698 }
699 KeyCode::Home => {
700 state.highlight_first();
701 None
702 }
703 KeyCode::End => {
704 state.highlight_last();
705 state.ensure_visible(8);
706 None
707 }
708 KeyCode::PageUp => {
709 for _ in 0..5 {
710 state.highlight_prev();
711 }
712 state.ensure_visible(8);
713 None
714 }
715 KeyCode::PageDown => {
716 for _ in 0..5 {
717 state.highlight_next();
718 }
719 state.ensure_visible(8);
720 None
721 }
722 _ => None,
723 }
724 } else {
725 match key.code {
727 KeyCode::Enter | KeyCode::Char(' ') | KeyCode::Down => {
728 state.open();
729 Some(SelectAction::Open)
730 }
731 _ => None,
732 }
733 }
734}
735
736pub fn handle_select_mouse(
747 mouse: &MouseEvent,
748 state: &mut SelectState,
749 select_area: Rect,
750 dropdown_regions: &[ClickRegion<SelectAction>],
751) -> Option<SelectAction> {
752 if !state.enabled {
753 return None;
754 }
755
756 if let MouseEventKind::Down(MouseButton::Left) = mouse.kind {
757 let col = mouse.column;
758 let row = mouse.row;
759
760 if state.is_open {
761 for region in dropdown_regions {
763 if region.contains(col, row) {
764 if let SelectAction::Select(idx) = region.data {
765 state.select(idx);
766 return Some(SelectAction::Select(idx));
767 }
768 }
769 }
770
771 if col >= select_area.x
773 && col < select_area.x + select_area.width
774 && row >= select_area.y
775 && row < select_area.y + select_area.height
776 {
777 state.close();
778 return Some(SelectAction::Close);
779 }
780
781 state.close();
783 Some(SelectAction::Close)
784 } else {
785 if col >= select_area.x
787 && col < select_area.x + select_area.width
788 && row >= select_area.y
789 && row < select_area.y + select_area.height
790 {
791 state.open();
792 return Some(SelectAction::Open);
793 }
794 None
795 }
796 } else {
797 None
798 }
799}
800
801pub fn calculate_dropdown_height(option_count: usize, max_visible: u16) -> u16 {
805 let visible = (option_count as u16).min(max_visible);
806 visible + 2 }
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812
813 #[test]
814 fn test_state_default() {
815 let state = SelectState::default();
816 assert!(state.selected_index.is_none());
817 assert!(!state.is_open);
818 assert!(!state.focused);
819 assert!(state.enabled);
820 assert_eq!(state.highlighted_index, 0);
821 }
822
823 #[test]
824 fn test_state_new() {
825 let state = SelectState::new(5);
826 assert_eq!(state.total_options, 5);
827 assert!(state.selected_index.is_none());
828 }
829
830 #[test]
831 fn test_state_with_selected() {
832 let state = SelectState::with_selected(5, 2);
833 assert_eq!(state.selected_index, Some(2));
834 assert_eq!(state.highlighted_index, 2);
835 }
836
837 #[test]
838 fn test_state_with_selected_out_of_bounds() {
839 let state = SelectState::with_selected(5, 10);
840 assert!(state.selected_index.is_none());
841 assert_eq!(state.highlighted_index, 0);
842 }
843
844 #[test]
845 fn test_open_close() {
846 let mut state = SelectState::new(5);
847
848 state.open();
849 assert!(state.is_open);
850
851 state.close();
852 assert!(!state.is_open);
853
854 state.toggle();
855 assert!(state.is_open);
856
857 state.toggle();
858 assert!(!state.is_open);
859 }
860
861 #[test]
862 fn test_open_disabled() {
863 let mut state = SelectState::new(5);
864 state.enabled = false;
865
866 state.open();
867 assert!(!state.is_open);
868 }
869
870 #[test]
871 fn test_highlight_navigation() {
872 let mut state = SelectState::new(5);
873
874 state.highlight_next();
875 assert_eq!(state.highlighted_index, 1);
876
877 state.highlight_next();
878 assert_eq!(state.highlighted_index, 2);
879
880 state.highlight_prev();
881 assert_eq!(state.highlighted_index, 1);
882
883 state.highlight_first();
884 assert_eq!(state.highlighted_index, 0);
885
886 state.highlight_last();
887 assert_eq!(state.highlighted_index, 4);
888 }
889
890 #[test]
891 fn test_highlight_bounds() {
892 let mut state = SelectState::new(3);
893
894 state.highlight_prev();
896 assert_eq!(state.highlighted_index, 0);
897
898 state.highlighted_index = 2;
900 state.highlight_next();
901 assert_eq!(state.highlighted_index, 2);
902 }
903
904 #[test]
905 fn test_select() {
906 let mut state = SelectState::new(5);
907 state.is_open = true;
908
909 state.select(2);
910 assert_eq!(state.selected_index, Some(2));
911 assert_eq!(state.highlighted_index, 2);
912 assert!(!state.is_open); }
914
915 #[test]
916 fn test_select_highlighted() {
917 let mut state = SelectState::new(5);
918 state.is_open = true;
919 state.highlighted_index = 3;
920
921 state.select_highlighted();
922 assert_eq!(state.selected_index, Some(3));
923 assert!(!state.is_open);
924 }
925
926 #[test]
927 fn test_clear_selection() {
928 let mut state = SelectState::with_selected(5, 2);
929 assert!(state.has_selection());
930
931 state.clear_selection();
932 assert!(!state.has_selection());
933 assert!(state.selected_index.is_none());
934 }
935
936 #[test]
937 fn test_set_total() {
938 let mut state = SelectState::with_selected(10, 8);
939 state.highlighted_index = 9;
940
941 state.set_total(5);
942 assert_eq!(state.total_options, 5);
943 assert_eq!(state.selected_index, Some(4)); assert_eq!(state.highlighted_index, 4); }
946
947 #[test]
948 fn test_ensure_visible() {
949 let mut state = SelectState::new(20);
950 state.highlighted_index = 15;
951 state.scroll_offset = 0;
952
953 state.ensure_visible(10);
954 assert!(state.scroll_offset >= 6); }
956
957 #[test]
958 fn test_style_default() {
959 let style = SelectStyle::default();
960 assert_eq!(style.focused_border, Color::Yellow);
961 assert_eq!(style.max_visible_options, 8);
962 }
963
964 #[test]
965 fn test_style_builders() {
966 let style = SelectStyle::minimal();
967 assert_eq!(style.highlight_style.add_modifier, Modifier::BOLD);
968
969 let style = SelectStyle::arrow();
970 assert_eq!(style.selected_indicator, "→ ");
971
972 let style = SelectStyle::bracket();
973 assert_eq!(style.selected_indicator, "[x] ");
974 }
975
976 #[test]
977 fn test_style_builder_methods() {
978 let style = SelectStyle::default()
979 .max_options(10)
980 .focused_border(Color::Cyan)
981 .indicator("↓");
982
983 assert_eq!(style.max_visible_options, 10);
984 assert_eq!(style.focused_border, Color::Cyan);
985 assert_eq!(style.dropdown_indicator, "↓");
986 }
987
988 #[test]
989 fn test_handle_key_closed() {
990 let mut state = SelectState::new(5);
991
992 let key = KeyEvent::from(KeyCode::Enter);
994 let action = handle_select_key(&key, &mut state);
995 assert_eq!(action, Some(SelectAction::Open));
996 assert!(state.is_open);
997 }
998
999 #[test]
1000 fn test_handle_key_open_navigation() {
1001 let mut state = SelectState::new(5);
1002 state.open();
1003
1004 let key = KeyEvent::from(KeyCode::Down);
1006 handle_select_key(&key, &mut state);
1007 assert_eq!(state.highlighted_index, 1);
1008
1009 let key = KeyEvent::from(KeyCode::Up);
1011 handle_select_key(&key, &mut state);
1012 assert_eq!(state.highlighted_index, 0);
1013 }
1014
1015 #[test]
1016 fn test_handle_key_open_select() {
1017 let mut state = SelectState::new(5);
1018 state.open();
1019 state.highlighted_index = 2;
1020
1021 let key = KeyEvent::from(KeyCode::Enter);
1022 let action = handle_select_key(&key, &mut state);
1023
1024 assert_eq!(action, Some(SelectAction::Select(2)));
1025 assert_eq!(state.selected_index, Some(2));
1026 assert!(!state.is_open);
1027 }
1028
1029 #[test]
1030 fn test_handle_key_open_escape() {
1031 let mut state = SelectState::new(5);
1032 state.open();
1033
1034 let key = KeyEvent::from(KeyCode::Esc);
1035 let action = handle_select_key(&key, &mut state);
1036
1037 assert_eq!(action, Some(SelectAction::Close));
1038 assert!(!state.is_open);
1039 }
1040
1041 #[test]
1042 fn test_handle_key_disabled() {
1043 let mut state = SelectState::new(5);
1044 state.enabled = false;
1045
1046 let key = KeyEvent::from(KeyCode::Enter);
1047 let action = handle_select_key(&key, &mut state);
1048
1049 assert!(action.is_none());
1050 assert!(!state.is_open);
1051 }
1052
1053 #[test]
1054 fn test_calculate_dropdown_height() {
1055 assert_eq!(calculate_dropdown_height(3, 8), 5); assert_eq!(calculate_dropdown_height(10, 8), 10); assert_eq!(calculate_dropdown_height(0, 8), 2); }
1059
1060 #[test]
1061 fn test_click_region_contains() {
1062 let region = ClickRegion::new(Rect::new(10, 5, 20, 3), SelectAction::Select(0));
1063
1064 assert!(region.contains(10, 5));
1065 assert!(region.contains(29, 7));
1066 assert!(!region.contains(9, 5));
1067 assert!(!region.contains(30, 5));
1068 }
1069}