1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::Style;
7use crate::text::Text;
8
9pub struct Select {
14 options: Vec<Text>,
16 selected: usize,
18 expanded: bool,
20 focused: bool,
22 rect: Rect,
24 style: Style,
26 selected_style: Style,
28}
29
30impl Select {
31 pub fn new() -> Self {
33 Self {
34 options: Vec::new(),
35 selected: 0,
36 expanded: false,
37 focused: false,
38 rect: Rect::default(),
39 style: Style::default(),
40 selected_style: Style::default().bg(crate::style::Color::White).fg(crate::style::Color::Black),
41 }
42 }
43
44 pub fn options(mut self, options: Vec<impl Into<Text>>) -> Self {
46 self.options = options.into_iter().map(|o| o.into()).collect();
47 self
48 }
49
50 pub fn style(mut self, style: Style) -> Self {
52 self.style = style;
53 self
54 }
55
56 pub fn selected_style(mut self, style: Style) -> Self {
58 self.selected_style = style;
59 self
60 }
61
62 pub fn selected(&self) -> usize {
64 self.selected
65 }
66
67 pub fn selected_text(&self) -> &str {
69 self.options.get(self.selected).map(|t| t.first_text()).unwrap_or("")
70 }
71
72 pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
74 if index < self.options.len() {
75 self.selected = index;
76 cx.invalidate_paint();
77 }
78 }
79}
80
81impl Component for Select {
82 fn render(&self, cx: &mut RenderCx) {
83 if self.options.is_empty() {
84 return;
85 }
86
87 let indicator = if self.expanded { "▲" } else { "▼" };
89 let display = format!("{} {}", self.selected_text(), indicator);
90 if self.focused {
91 cx.set_style(self.selected_style.clone());
92 } else {
93 cx.set_style(self.style.clone());
94 }
95 cx.line(&display);
96
97 if self.expanded {
99 for (i, opt) in self.options.iter().enumerate() {
100 if i == self.selected {
101 cx.set_style(self.selected_style.clone());
102 cx.text("❯ ");
103 } else {
104 cx.set_style(self.style.clone());
105 cx.text(" ");
106 }
107 cx.line(opt.first_text());
108 }
109 }
110 }
111
112 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
113 if self.options.is_empty() {
114 return Size { width: 0, height: 0 };
115 }
116
117 let max_w = self.options.iter().map(|o| o.max_width()).max().unwrap_or(0) + 2;
118
119 let height = if self.expanded {
120 1u16.saturating_add(self.options.len() as u16)
121 } else {
122 1
123 };
124
125 Size { width: max_w, height }
126 }
127
128 fn event(&mut self, event: &Event, cx: &mut EventCx) {
129 match event {
130 Event::Focus => {
131 self.focused = true;
132 cx.invalidate_paint();
133 return;
134 }
135 Event::Blur => {
136 self.focused = false;
137 self.expanded = false;
138 cx.invalidate_layout();
139 return;
140 }
141 _ => {}
142 }
143
144 if self.options.is_empty() { return; }
145
146 if cx.phase() != crate::event::EventPhase::Target { return; }
148
149 if let Event::Key(key_event) = event {
150 match &key_event.key {
151 crate::event::Key::Enter | crate::event::Key::Char(' ') => {
152 if self.expanded {
153 self.expanded = false;
154 } else {
155 self.expanded = true;
156 }
157 cx.invalidate_layout();
158 return;
159 }
160 crate::event::Key::Esc => {
161 if self.expanded {
162 self.expanded = false;
163 cx.invalidate_layout();
164 }
165 return;
166 }
167 crate::event::Key::Up => {
168 if self.expanded {
169 if self.selected > 0 {
170 self.selected -= 1;
171 } else {
172 self.selected = self.options.len() - 1;
173 }
174 cx.invalidate_paint();
175 }
176 return;
177 }
178 crate::event::Key::Down => {
179 if self.expanded {
180 if self.selected + 1 < self.options.len() {
181 self.selected += 1;
182 } else {
183 self.selected = 0;
184 }
185 cx.invalidate_paint();
186 }
187 return;
188 }
189 _ => {}
190 }
191 }
192 }
193
194 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
195 self.rect = rect;
196 }
197
198 fn focusable(&self) -> bool {
199 true
200 }
201
202 fn style(&self) -> Style {
203 self.style.clone()
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::testbuffer::TestBuffer;
211
212 #[test]
213 fn test_collapsed() {
214 let mut tb = TestBuffer::new(20, 1);
215 tb.render(&Select::new().options(vec![Text::from("A")]));
216 assert!(tb.buffer.cells[0].symbol.contains("A"));
217 }
218}