Skip to main content

flux_tui/components/
combobox.rs

1use 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
16/// Combobox contains a list of values of which one can be selected
17/// The selected value can be changed by using the Left and Right buttons
18/// Focus highlights the Combobox's text
19///
20/// Design (Border is part of the component):
21/// ```text
22/// ┌─┬─────────────┬─┐
23/// │<│ Text [2/28] │>│
24/// └─┴─────────────┴─┘
25/// ```
26pub struct Combobox<T: PartialEq + 'static> {
27	has_focus: bool,
28	// Keeps track if the border has actually been drawn
29	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	/// Selects the next possible element
103	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	/// Selects the previous possible element
112	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	/// This is ignored
374	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}