Skip to main content

flux_tui/components/
checkbox.rs

1use super::*;
2use crate::canvas::*;
3
4use std::num::NonZero;
5use std::rc::Rc;
6
7use crossterm::event::{Event, KeyCode};
8use unicode_width::UnicodeWidthStr;
9
10pub type StateChangedCallback = dyn Fn(&mut TextCheckbox);
11pub type CheckboxKeyHandler = fn(&mut Checkbox, event: &mut WindowEvent);
12
13/// Checkbox that shows a boolean state and swaps state when pressing Spacebar (while focused)
14/// Focus highlights the whole Checkbox
15///
16/// Design:
17///
18/// checked:    unchecked:
19/// ```text
20/// \[x\] Text  \[ \] Text
21/// ```
22pub struct TextCheckbox {
23	checkbox: Checkbox,
24	text: String,
25	callback: Rc<StateChangedCallback>,
26}
27
28impl TextCheckbox {
29	pub fn default_callback(_: &mut Self) {}
30
31	pub fn set_state_changed_callback<F: Fn(&mut Self) + 'static>(&mut self, callback: F) {
32		self.callback = Rc::new(callback);
33	}
34
35	pub fn set_text(&mut self, text: &str) {
36		self.text.clear();
37		self.text.push_str(text);
38		let min = 3 + !self.text.is_empty() as TSize * 2;
39		let max = 3 + self.text.width() as TSize + !self.text.is_empty() as TSize;
40		self.checkbox.base.constraints.x.max = Size::Fixed(NonZero::new(max).unwrap());
41		self.checkbox.base.constraints.x.min = NonZero::new(min).unwrap();
42	}
43
44	/// Returns the internal text
45	pub fn get_text(&self) -> &str {
46		&self.text
47	}
48
49	// Returns the internal boolean state
50	pub fn get_state(&self) -> bool {
51		self.checkbox.state
52	}
53
54	// Sets the internal boolean state
55	pub fn set_state(&mut self, state: bool) {
56		self.checkbox.state = state;
57	}
58
59	pub fn set_key_handler(&mut self, f: CheckboxKeyHandler) {
60		self.checkbox.set_key_handler(f);
61	}
62}
63
64impl Default for TextCheckbox {
65	fn default() -> Self {
66		let mut s = Self {
67			checkbox: Checkbox::default(),
68			text: String::new(),
69			callback: Rc::new(Self::default_callback),
70		};
71		s.checkbox.base.constraints.x.min = NonZero::new(3).unwrap();
72		s.checkbox.base.constraints.x.max = Size::Relative(Percent::from_int(100));
73		s
74	}
75}
76
77impl Window for TextCheckbox {
78	fn render(&self, canvas: &mut Canvas) {
79		self.checkbox.render(canvas);
80		let mut row = canvas.get_row_variable_width(0).unwrap();
81		row.skip(4);
82		row.add_string(
83			&self.text,
84			None,
85			None,
86			Style::None,
87			Some(VariableWidthGlyphRow::DOT3_REPLACEMENT),
88		)
89		.unwrap();
90	}
91
92	fn handle_event(&mut self, event: &mut WindowEvent) {
93		if self.checkbox.handle_event(event) {
94			let cb = self.callback.clone();
95			cb(self);
96		}
97	}
98
99	fn is_enabled(&self) -> bool {
100		self.checkbox.base.enabled
101	}
102}
103
104impl WindowLayout for TextCheckbox {
105	fn desired_size(&self, available_size: TPoint) -> TPoint {
106		self.checkbox.base.desired_size(available_size)
107	}
108
109	fn border(&self) -> BorderStyle {
110		self.checkbox.base.border
111	}
112
113	fn alignment(&self) -> (HorizontalAlignment, VerticalAlignment) {
114		self.checkbox.base.alignment
115	}
116
117	fn is_visible(&self) -> bool {
118		self.checkbox.base.visibility
119	}
120
121	fn margin(&self) -> Thickness {
122		self.checkbox.base.margin
123	}
124}
125
126impl HasWindowUID for TextCheckbox {
127	fn uid(&self) -> WindowUID {
128		self.checkbox.base.uid
129	}
130}
131
132impl Widget for TextCheckbox {
133	fn set_visibility(&mut self, visibility: bool) {
134		self.checkbox.base.visibility = visibility;
135		self.provoke_changed_property(WindowProperty::IsVisible);
136	}
137
138	fn set_width(&mut self, _: Size) {}
139
140	fn set_height(&mut self, _: Size) {}
141
142	fn set_margin(&mut self, margin: Thickness) {
143		self.checkbox.base.margin = margin;
144		self.provoke_changed_property(WindowProperty::Margin);
145	}
146
147	fn set_border(&mut self, border: BorderStyle) {
148		self.checkbox.base.border = border;
149		self.provoke_changed_property(WindowProperty::Border);
150	}
151
152	fn set_alignment(&mut self, horizontal: HorizontalAlignment, vertical: VerticalAlignment) {
153		self.checkbox.base.alignment = (horizontal, vertical);
154		self.provoke_changed_property(WindowProperty::Alignment);
155	}
156
157	fn set_enabled_state(&mut self, is_enabled: bool) {
158		self.checkbox.base.enabled = is_enabled;
159	}
160
161	fn set_width_constraint(&mut self, _: SizeConstraint) {}
162
163	fn set_height_constraint(&mut self, _: SizeConstraint) {}
164}
165
166impl WidgetColors for TextCheckbox {
167	fn set_disabled_color(&mut self, color: Option<Color>) {
168		self.checkbox.base.colors.disabled = color;
169	}
170
171	fn set_base_fg_color(&mut self, color: Option<Color>) {
172		self.checkbox.base.colors.base_fg = color;
173	}
174
175	fn set_base_bg_color(&mut self, color: Option<Color>) {
176		self.checkbox.base.colors.base_bg = color;
177	}
178}
179
180/// Base struct which can be used to make custom checkbox components
181pub struct Checkbox {
182	has_focus: bool,
183	base: WidgetBase,
184	state: bool,
185	key_handler: CheckboxKeyHandler,
186}
187
188impl Default for Checkbox {
189	fn default() -> Self {
190		let mut s = Self {
191			state: false,
192			has_focus: false,
193			base: WidgetBase::default(),
194			key_handler: Self::default_key_handler,
195		};
196		s.base.constraints.x.min = NonZero::new(3).unwrap();
197		s.base.constraints.x.max = Size::Fixed(NonZero::new(3).unwrap());
198		s.base.constraints.y.min = NonZero::<TSize>::MIN;
199		s.base.constraints.y.max = Size::Fixed(NonZero::<TSize>::MIN);
200		s
201	}
202}
203
204impl Checkbox {
205	pub const CROSS_MARK: Grapheme = Grapheme::new_unchecked("x", GlyphWidth::Half);
206	pub const OPEN_BOX: Grapheme = Grapheme::new_unchecked("[", GlyphWidth::Half);
207	pub const CLOSE_BOX: Grapheme = Grapheme::new_unchecked("]", GlyphWidth::Half);
208
209	pub fn default_key_handler(&mut self, event: &mut WindowEvent) {
210		if let Event::Key(k) = event.raw() {
211			if k.code == KeyCode::Char(' ') {
212				self.state = !self.state;
213				event.handled = true;
214			}
215		}
216	}
217
218	pub fn set_key_handler(&mut self, f: CheckboxKeyHandler) {
219		self.key_handler = f;
220	}
221
222	// Returns the internal boolean state
223	pub fn get_state(&self) -> bool {
224		self.state
225	}
226
227	// Sets the internal boolean state
228	pub fn set_state(&mut self, state: bool) {
229		self.state = state;
230	}
231
232	pub fn render(&self, canvas: &mut Canvas) {
233		let mut row = canvas.get_row(0, GlyphWidth::Half).unwrap();
234		row.get(0).unwrap().set_grapheme(Self::OPEN_BOX).unwrap();
235		if self.state {
236			row.get(1).unwrap().set_grapheme(Self::CROSS_MARK).unwrap();
237		}
238		row.get(2).unwrap().set_grapheme(Self::CLOSE_BOX).unwrap();
239
240		if self.has_focus {
241			row.get(0).unwrap().set_style(Style::Reverse);
242			row.get(2).unwrap().set_style(Style::ResetAfter);
243		}
244
245		if let Some(color) = self.base.colors.base_fg {
246			row.fill_foreground(color);
247		}
248		if let Some(color) = self.base.colors.base_bg {
249			row.fill_background(color);
250		}
251
252		if let Some(color) = self.base.colors.disabled
253			&& !self.base.enabled
254		{
255			row.fill_foreground(color);
256		}
257	}
258
259	pub fn handle_event(&mut self, event: &mut WindowEvent) -> bool {
260		let old = self.state;
261		match event.raw() {
262			Event::FocusGained => self.has_focus = true,
263			Event::FocusLost => self.has_focus = false,
264			_ => (self.key_handler)(self, event),
265		}
266
267		self.state != old
268	}
269}