flux_tui/components/
combobox.rs1use std::sync::atomic::{AtomicBool, Ordering};
2
3use super::*;
4use crate::canvas::*;
5
6use std::num::NonZero;
7use std::rc::Rc;
8
9use as_any::Downcast;
10use crossterm::event::{Event, KeyCode};
11use unicode_width::UnicodeWidthStr;
12
13pub type ComboboxSelectionChangedCallback<T> = dyn Fn(&mut Combobox<T>);
14pub type ComboboxKeyHandler<T> = fn(&mut Combobox<T>, event: &mut WindowEvent);
15
16pub struct Combobox<T: PartialEq + 'static> {
27 has_focus: bool,
28 border_visible: AtomicBool,
30 border_bg: Option<Color>,
31 border_fg: Option<Color>,
32 base: WidgetBase,
33 items: Vec<DisplayValue<T>>,
34 focus: usize,
35 callback: Rc<ComboboxSelectionChangedCallback<T>>,
36 key_handler: ComboboxKeyHandler<T>,
37}
38
39impl<T: PartialEq> Default for Combobox<T> {
40 fn default() -> Self {
41 let mut s = Self {
42 items: Vec::new(),
43 has_focus: false,
44 border_visible: AtomicBool::default(),
45 border_fg: None,
46 border_bg: None,
47 base: WidgetBase::default(),
48 focus: usize::MIN,
49 callback: Rc::new(Self::default_callback),
50 key_handler: Self::default_key_handler,
51 };
52 s.base.border = BorderStyle::Custom(Self::render_border, Thickness::new(1, 3, 3, 1));
53 s.base.constraints.x.min = NonZero::new(3).unwrap();
54 s.base.constraints.y.min = NonZero::<TSize>::MIN;
55 s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
56 s
57 }
58}
59
60impl<T: PartialEq> Combobox<T> {
61 pub const INDICATOR_LEFT: Grapheme = ScrollViewer::ARROW_LEFT;
62 pub const INDICATOR_RIGHT: Grapheme = ScrollViewer::ARROW_RIGHT;
63 pub const DEFAULT_BORDER: BorderKind = BorderKind::Solid;
64
65 pub fn default_callback(_: &mut Self) {}
66
67 pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
68 if let Event::Key(k) = event.raw() {
69 if self.base.enabled {
70 if k.code == KeyCode::Left {
71 self.select_previous();
72 event.handled = true;
73 }
74 else if k.code == KeyCode::Right {
75 self.select_next();
76 event.handled = true;
77 }
78 }
79 }
80 }
81
82 pub fn set_key_handler(&mut self, f: ComboboxKeyHandler<T>) {
83 self.key_handler = f;
84 }
85
86 pub fn set_selection_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
87 self.callback = Rc::new(callback);
88 }
89
90 pub fn get_value(&self) -> Option<&DisplayValue<T>> {
91 self.items.get(self.focus)
92 }
93
94 pub fn set_border_fg(&mut self, color: Option<Color>) {
95 self.border_fg = color;
96 }
97
98 pub fn set_border_bg(&mut self, color: Option<Color>) {
99 self.border_bg = color;
100 }
101
102 pub fn select_next(&mut self) {
104 if self.focus < self.items.len() - 1 {
105 let cb = self.callback.clone();
106 self.focus += 1;
107 cb(self);
108 }
109 }
110
111 pub fn select_previous(&mut self) {
113 if self.focus > 0 {
114 let cb = self.callback.clone();
115 self.focus -= 1;
116 cb(self);
117 }
118 }
119
120 fn render_border(w: &dyn Window, mut canvas: BorderCanvas<'_>) {
121 let slf: &Self = w.downcast_ref().unwrap();
122 slf.border_visible.store(true, Ordering::Release);
123
124 let corners = Self::DEFAULT_BORDER.corner_style();
125 let lines = Self::DEFAULT_BORDER.line_style();
126 let conns = Self::DEFAULT_BORDER.connector_style();
127 let connector_bottom_lr = conns[2];
128 let connector_top_lr = conns[3];
129 {
130 let w = canvas.size().x;
131 canvas
132 .get_glyph(0, 1)
133 .unwrap()
134 .set_grapheme(lines[1])
135 .unwrap();
136 canvas
137 .get_glyph(1, 1)
138 .unwrap()
139 .set_grapheme(Self::INDICATOR_LEFT)
140 .unwrap();
141 canvas
142 .get_glyph(2, 1)
143 .unwrap()
144 .set_grapheme(lines[1])
145 .unwrap();
146
147 canvas
148 .get_glyph(w - 3, 1)
149 .unwrap()
150 .set_grapheme(lines[1])
151 .unwrap();
152 canvas
153 .get_glyph(w - 2, 1)
154 .unwrap()
155 .set_grapheme(Self::INDICATOR_RIGHT)
156 .unwrap();
157 canvas
158 .get_glyph(w - 1, 1)
159 .unwrap()
160 .set_grapheme(lines[1])
161 .unwrap();
162 }
163
164
165 let mut row = canvas.get_row(0).unwrap();
166 row.get(0).unwrap().set_grapheme(corners[0]).unwrap();
167 row.get(1).unwrap().set_grapheme(lines[0]).unwrap();
168 row.get(2)
169 .unwrap()
170 .set_grapheme(connector_bottom_lr)
171 .unwrap();
172 row.get(row.length() - 1)
173 .unwrap()
174 .set_grapheme(corners[1])
175 .unwrap();
176 row.get(row.length() - 2)
177 .unwrap()
178 .set_grapheme(lines[0])
179 .unwrap();
180 row.get(row.length() - 3)
181 .unwrap()
182 .set_grapheme(connector_bottom_lr)
183 .unwrap();
184 for idx in 3..row.length() - 3 {
185 row.get(idx).unwrap().set_grapheme(lines[0]).unwrap();
186 }
187
188 let mut row = canvas.get_row(canvas.size().y - 1).unwrap();
189 row.get(0).unwrap().set_grapheme(corners[2]).unwrap();
190 row.get(1).unwrap().set_grapheme(lines[0]).unwrap();
191 row.get(2).unwrap().set_grapheme(connector_top_lr).unwrap();
192 row.get(row.length() - 1)
193 .unwrap()
194 .set_grapheme(corners[3])
195 .unwrap();
196 row.get(row.length() - 2)
197 .unwrap()
198 .set_grapheme(lines[0])
199 .unwrap();
200 row.get(row.length() - 3)
201 .unwrap()
202 .set_grapheme(connector_top_lr)
203 .unwrap();
204
205 for idx in 3..row.length() - 3 {
206 row.get(idx).unwrap().set_grapheme(lines[0]).unwrap();
207 }
208
209 if let Some(color) = slf.border_fg {
210 canvas.fill_foreground(color);
211 }
212 if let Some(color) = slf.border_bg {
213 canvas.fill_background(color);
214 }
215 }
216}
217
218impl<T: PartialEq> ItemCollection<DisplayValue<T>> for Combobox<T> {
219 type Index = usize;
220
221 fn add_item(&mut self, item: DisplayValue<T>) {
222 self.items.push(item);
223 self.items
224 .sort_by(|a, b| a.display.as_str().cmp(b.display.as_str()));
225 self.provoke_changed_property(WindowProperty::Children);
226 }
227
228 fn remove_item(&mut self, item: &DisplayValue<T>) {
229 if let Some(idx) = self.items.iter().position(|x| x.value == item.value) {
230 self.items.remove(idx);
231 if idx == self.focus {
232 let cb = self.callback.clone();
233 cb(self);
234 }
235 }
236 }
237
238 fn remove_at(&mut self, index: Self::Index) -> DisplayValue<T> {
239 self.items.remove(index)
240 }
241
242 fn get_item(&mut self, index: Self::Index) -> Option<&DisplayValue<T>> {
243 self.items.get(index)
244 }
245
246 fn clear_items(&mut self) {
247 if !self.items.is_empty() {
248 self.items.clear();
249 self.focus = usize::MIN;
250 self.provoke_changed_property(WindowProperty::Focus);
251 let cb = self.callback.clone();
252 cb(self);
253 }
254 }
255
256 fn items(&self) -> Self::Index {
257 self.items.len()
258 }
259}
260
261impl<T: PartialEq> Window for Combobox<T> {
262 fn render(&self, canvas: &mut Canvas) {
263 let has_border = self.border_visible.load(Ordering::Relaxed);
264
265 if !has_border {
266 let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();
267 row.get(0)
268 .unwrap()
269 .set_grapheme(Self::INDICATOR_LEFT)
270 .unwrap();
271 row.get(row.length() - 1)
272 .unwrap()
273 .set_grapheme(Self::INDICATOR_RIGHT)
274 .unwrap();
275 }
276
277 let mut row = canvas.get_row_variable_width(0).unwrap();
278
279 if let Some(v) = self.items.get(self.focus) {
280 let cb_index = format!("[{}/{}]", self.focus + 1, self.items.len());
281 let index_w = cb_index.width() as TSize;
282 let w = v.display.width() as TSize;
283 let width = row.width();
284 let style = Style::Reverse.when(self.has_focus);
285
286 row = row.with_custom_width(width).unwrap();
287 if row.width() > w + index_w {
288 row.skip((row.width() - w - (index_w + 1)) / 2);
289 }
290
291 row.add_string(
292 &v.display,
293 None,
294 None,
295 style,
296 Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
297 )
298 .unwrap();
299 row.skip(1);
300 row.add_string(
301 &cb_index,
302 self.base.colors.base_fg,
303 self.base.colors.base_bg,
304 Style::default(),
305 None,
306 )
307 .ok();
308 }
309 }
310
311 fn handle_event(&mut self, event: &mut WindowEvent) {
312 match event.raw() {
313 Event::Resize(_, _) => self.border_visible.store(false, Ordering::Relaxed),
314 Event::FocusGained => self.has_focus = true,
315 Event::FocusLost => self.has_focus = false,
316 _ => (self.key_handler)(self, event),
317 }
318 }
319
320 fn is_enabled(&self) -> bool {
321 self.base.enabled
322 }
323}
324
325impl<T: PartialEq> WindowLayout for Combobox<T> {
326 fn desired_size(&self, available_size: TPoint) -> TPoint {
327 self.base.desired_size(available_size)
328 }
329
330 fn border(&self) -> BorderStyle {
331 self.base.border
332 }
333
334 fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
335 self.base.alignment
336 }
337
338 fn is_visible(&self) -> bool {
339 self.base.visibility
340 }
341
342 fn margin(&self) -> Thickness {
343 self.base.margin
344 }
345}
346
347impl<T: PartialEq + 'static> HasWindowUID for Combobox<T> {
348 fn uid(&self) -> WindowUID {
349 self.base.uid
350 }
351}
352
353impl<T: PartialEq> Widget for Combobox<T> {
354 fn set_visibility(&mut self, visibility: bool) {
355 self.base.visibility = visibility;
356 self.provoke_changed_property(WindowProperty::IsVisible);
357 }
358
359 fn set_width(&mut self, width: Size) {
360 self.base.size.x = width;
361 self.provoke_changed_property(WindowProperty::Size);
362 }
363
364 fn set_height(&mut self, height: Size) {
365 let _ = height;
366 }
367
368 fn set_margin(&mut self, margin: Thickness) {
369 self.base.margin = margin;
370 self.provoke_changed_property(WindowProperty::Margin);
371 }
372
373 fn set_border(&mut self, border: BorderStyle) {
375 let _ = border;
376 }
377
378 fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
379 self.base.alignment = (horizontal, vertical);
380 self.provoke_changed_property(WindowProperty::Alignment);
381 }
382
383 fn set_enabled_state(&mut self, is_enabled: bool) {
384 self.base.enabled = is_enabled;
385 }
386
387 fn set_width_constraint(&mut self, width: SizeConstraint) {
388 self.base.constraints.x = width;
389 self.provoke_changed_property(WindowProperty::Size);
390 }
391
392 fn set_height_constraint(&mut self, height: SizeConstraint) {
393 self.base.constraints.y = height;
394 self.provoke_changed_property(WindowProperty::Size);
395 }
396}
397
398impl<T: PartialEq> WidgetColors for Combobox<T> {
399 fn set_disabled_color(&mut self, color: Option<Color>) {
400 self.base.colors.disabled = color;
401 }
402
403 fn set_base_fg_color(&mut self, color: Option<Color>) {
404 self.base.colors.base_fg = color;
405 }
406
407 fn set_base_bg_color(&mut self, color: Option<Color>) {
408 self.base.colors.base_bg = color;
409 }
410}