1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;
/// A dropdown select widget.
///
/// Displays the currently selected option when collapsed ("▼"), and expands
/// inline to show all options with keyboard navigation when activated.
pub struct Select {
/// List of option strings
options: Vec<String>,
/// Currently selected index
selected: usize,
/// Whether the dropdown is expanded
expanded: bool,
/// Whether this widget currently has keyboard focus
focused: bool,
/// Current layout rect
rect: Rect,
/// Collapsed/default style
style: Style,
/// Selected/focused option style
selected_style: Style,
}
impl Select {
/// Creates an empty select.
pub fn new() -> Self {
Self {
options: Vec::new(),
selected: 0,
expanded: false,
focused: false,
rect: Rect::default(),
style: Style::default(),
selected_style: Style::default().bg(crate::style::Color::White).fg(crate::style::Color::Black),
}
}
/// Sets the option list.
pub fn options(mut self, options: Vec<String>) -> Self {
self.options = options;
self
}
/// Sets the collapsed/default style.
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
/// Sets the selected option style.
pub fn selected_style(mut self, style: Style) -> Self {
self.selected_style = style;
self
}
/// Returns the currently selected index.
pub fn selected(&self) -> usize {
self.selected
}
/// Returns the text of the currently selected option.
pub fn selected_text(&self) -> &str {
self.options.get(self.selected).map(|s| s.as_str()).unwrap_or("")
}
/// Sets the selected option.
pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
if index < self.options.len() {
self.selected = index;
cx.invalidate_paint();
}
}
}
impl Component for Select {
fn render(&self, cx: &mut RenderCx) {
if self.options.is_empty() {
return;
}
// First line: selected option + indicator
let indicator = if self.expanded { "▲" } else { "▼" };
let display = format!("{} {}", self.selected_text(), indicator);
if self.focused {
cx.set_style(self.selected_style.clone());
} else {
cx.set_style(self.style.clone());
}
cx.line(&display);
// Expanded: show all options
if self.expanded {
for (i, opt) in self.options.iter().enumerate() {
if i == self.selected {
cx.set_style(self.selected_style.clone());
cx.text("❯ ");
} else {
cx.set_style(self.style.clone());
cx.text(" ");
}
cx.line(opt);
}
}
}
fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
if self.options.is_empty() {
return Size { width: 0, height: 0 };
}
let max_w: u16 = self.options.iter()
.map(|o| o.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum::<u16>())
.max()
.unwrap_or(0)
+ 2; // pointer/spacing
let height = if self.expanded {
1u16.saturating_add(self.options.len() as u16)
} else {
1
};
Size { width: max_w, height }
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
match event {
Event::Focus => {
self.focused = true;
cx.invalidate_paint();
return;
}
Event::Blur => {
self.focused = false;
self.expanded = false;
cx.invalidate_layout();
return;
}
_ => {}
}
if self.options.is_empty() { return; }
// Only handle key events during Target phase
if cx.phase() != crate::event::EventPhase::Target { return; }
if let Event::Key(key_event) = event {
match &key_event.key {
crate::event::Key::Enter | crate::event::Key::Char(' ') => {
if self.expanded {
self.expanded = false;
} else {
self.expanded = true;
}
cx.invalidate_layout();
return;
}
crate::event::Key::Esc => {
if self.expanded {
self.expanded = false;
cx.invalidate_layout();
}
return;
}
crate::event::Key::Up => {
if self.expanded {
if self.selected > 0 {
self.selected -= 1;
} else {
self.selected = self.options.len() - 1;
}
cx.invalidate_paint();
}
return;
}
crate::event::Key::Down => {
if self.expanded {
if self.selected + 1 < self.options.len() {
self.selected += 1;
} else {
self.selected = 0;
}
cx.invalidate_paint();
}
return;
}
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) {
self.rect = rect;
}
fn focusable(&self) -> bool {
true
}
fn style(&self) -> Style {
self.style.clone()
}
}