1use tuirealm::command::{Cmd, CmdResult, Direction};
29use tuirealm::props::{
30 Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
31};
32use tuirealm::ratatui::text::Line as Spans;
33use tuirealm::ratatui::{layout::Rect, text::Span, widgets::Tabs};
34use tuirealm::{Frame, MockComponent, State, StateValue};
35
36#[derive(Default)]
42pub struct CheckboxStates {
43 pub choice: usize, pub choices: Vec<String>, pub selection: Vec<usize>, }
47
48impl CheckboxStates {
49 pub fn next_choice(&mut self, rewind: bool) {
53 if rewind && self.choice + 1 >= self.choices.len() {
54 self.choice = 0;
55 } else if self.choice + 1 < self.choices.len() {
56 self.choice += 1;
57 }
58 }
59
60 pub fn prev_choice(&mut self, rewind: bool) {
64 if rewind && self.choice == 0 && !self.choices.is_empty() {
65 self.choice = self.choices.len() - 1;
66 } else if self.choice > 0 {
67 self.choice -= 1;
68 }
69 }
70
71 pub fn toggle(&mut self) {
75 let option = self.choice;
76 if self.selection.contains(&option) {
77 let target_index = self.selection.iter().position(|x| *x == option).unwrap();
78 self.selection.remove(target_index);
79 } else {
80 self.selection.push(option);
81 }
82 }
83
84 pub fn select(&mut self, i: usize) {
85 if i < self.choices.len() && !self.selection.contains(&i) {
86 self.selection.push(i);
87 }
88 }
89
90 pub fn has(&self, option: usize) -> bool {
94 self.selection.contains(&option)
95 }
96
97 pub fn set_choices(&mut self, choices: &[String]) {
103 self.choices = choices.to_vec();
104 self.selection.clear();
106 if self.choice >= self.choices.len() {
108 self.choice = match self.choices.len() {
109 0 => 0,
110 l => l - 1,
111 };
112 }
113 }
114}
115
116#[derive(Default)]
122pub struct Checkbox {
123 props: Props,
124 pub states: CheckboxStates,
125}
126
127impl Checkbox {
128 pub fn foreground(mut self, fg: Color) -> Self {
129 self.attr(Attribute::Foreground, AttrValue::Color(fg));
130 self
131 }
132
133 pub fn background(mut self, bg: Color) -> Self {
134 self.attr(Attribute::Background, AttrValue::Color(bg));
135 self
136 }
137
138 pub fn borders(mut self, b: Borders) -> Self {
139 self.attr(Attribute::Borders, AttrValue::Borders(b));
140 self
141 }
142
143 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
144 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
145 self
146 }
147
148 pub fn inactive(mut self, s: Style) -> Self {
149 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
150 self
151 }
152
153 pub fn rewind(mut self, r: bool) -> Self {
154 self.attr(Attribute::Rewind, AttrValue::Flag(r));
155 self
156 }
157
158 pub fn choices<S: AsRef<str>>(mut self, choices: &[S]) -> Self {
159 self.attr(
160 Attribute::Content,
161 AttrValue::Payload(PropPayload::Vec(
162 choices
163 .iter()
164 .map(|x| PropValue::Str(x.as_ref().to_string()))
165 .collect(),
166 )),
167 );
168 self
169 }
170
171 pub fn values(mut self, selected: &[usize]) -> Self {
172 self.attr(
174 Attribute::Value,
175 AttrValue::Payload(PropPayload::Vec(
176 selected.iter().map(|x| PropValue::Usize(*x)).collect(),
177 )),
178 );
179 self
180 }
181
182 fn rewindable(&self) -> bool {
183 self.props
184 .get_or(Attribute::Rewind, AttrValue::Flag(false))
185 .unwrap_flag()
186 }
187}
188
189impl MockComponent for Checkbox {
190 fn view(&mut self, render: &mut Frame, area: Rect) {
191 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
192 let foreground = self
193 .props
194 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
195 .unwrap_color();
196 let background = self
197 .props
198 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
199 .unwrap_color();
200 let borders = self
201 .props
202 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
203 .unwrap_borders();
204 let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
205 let focus = self
206 .props
207 .get_or(Attribute::Focus, AttrValue::Flag(false))
208 .unwrap_flag();
209 let inactive_style = self
210 .props
211 .get(Attribute::FocusStyle)
212 .map(|x| x.unwrap_style());
213 let div = crate::utils::get_block(borders, title, focus, inactive_style);
214 let (bg, fg, block_color): (Color, Color, Color) = match &focus {
216 true => (foreground, background, foreground),
217 false => (Color::Reset, foreground, Color::Reset),
218 };
219 let choices: Vec<Spans> = self
221 .states
222 .choices
223 .iter()
224 .enumerate()
225 .map(|(idx, x)| {
226 let checkbox: &str = match self.states.has(idx) {
227 true => "☑ ",
228 false => "☐ ",
229 };
230 let (fg, bg) = match focus {
231 true => match self.states.choice == idx {
232 true => (fg, bg),
233 false => (bg, fg),
234 },
235 false => (fg, bg),
236 };
237 Spans::from(vec![
239 Span::styled(checkbox, Style::default().fg(fg).bg(bg)),
240 Span::styled(x.to_string(), Style::default().fg(fg).bg(bg)),
241 ])
242 })
243 .collect();
244 let checkbox: Tabs = Tabs::new(choices)
245 .block(div)
246 .select(self.states.choice)
247 .style(Style::default().fg(block_color));
248 render.render_widget(checkbox, area);
249 }
250 }
251
252 fn query(&self, attr: Attribute) -> Option<AttrValue> {
253 self.props.get(attr)
254 }
255
256 fn attr(&mut self, attr: Attribute, value: AttrValue) {
257 match attr {
258 Attribute::Content => {
259 let current_selection = self.states.selection.clone();
261 let choices: Vec<String> = value
262 .unwrap_payload()
263 .unwrap_vec()
264 .iter()
265 .cloned()
266 .map(|x| x.unwrap_str())
267 .collect();
268 self.states.set_choices(&choices);
269 for c in current_selection.into_iter() {
271 self.states.select(c);
272 }
273 }
274 Attribute::Value => {
275 self.states.selection.clear();
277 for c in value.unwrap_payload().unwrap_vec() {
278 self.states.select(c.unwrap_usize());
279 }
280 }
281 attr => {
282 self.props.set(attr, value);
283 }
284 }
285 }
286
287 fn state(&self) -> State {
292 State::Vec(
293 self.states
294 .selection
295 .iter()
296 .map(|x| StateValue::Usize(*x))
297 .collect(),
298 )
299 }
300
301 fn perform(&mut self, cmd: Cmd) -> CmdResult {
302 match cmd {
303 Cmd::Move(Direction::Right) => {
304 self.states.next_choice(self.rewindable());
306 CmdResult::None
307 }
308 Cmd::Move(Direction::Left) => {
309 self.states.prev_choice(self.rewindable());
311 CmdResult::None
312 }
313 Cmd::Toggle => {
314 self.states.toggle();
315 CmdResult::Changed(self.state())
316 }
317 Cmd::Submit => {
318 CmdResult::Submit(self.state())
320 }
321 _ => CmdResult::None,
322 }
323 }
324}
325
326#[cfg(test)]
327mod test {
328
329 use super::*;
330
331 use pretty_assertions::{assert_eq, assert_ne};
332 use tuirealm::props::{PropPayload, PropValue};
333
334 #[test]
335 fn test_components_checkbox_states() {
336 let mut states: CheckboxStates = CheckboxStates::default();
337 assert_eq!(states.choice, 0);
338 assert_eq!(states.choices.len(), 0);
339 assert_eq!(states.selection.len(), 0);
340 let choices: &[String] = &[
341 "lemon".to_string(),
342 "strawberry".to_string(),
343 "vanilla".to_string(),
344 "chocolate".to_string(),
345 ];
346 states.set_choices(choices);
347 assert_eq!(states.choice, 0);
348 assert_eq!(states.choices.len(), 4);
349 assert_eq!(states.selection.len(), 0);
350 states.toggle();
352 assert_eq!(states.selection, vec![0]);
353 states.prev_choice(false);
355 assert_eq!(states.choice, 0);
356 states.next_choice(false);
357 assert_eq!(states.choice, 1);
358 states.next_choice(false);
359 assert_eq!(states.choice, 2);
360 states.toggle();
361 assert_eq!(states.selection, vec![0, 2]);
362 states.next_choice(false);
364 states.next_choice(false);
365 assert_eq!(states.choice, 3);
366 states.prev_choice(false);
367 assert_eq!(states.choice, 2);
368 states.toggle();
369 assert_eq!(states.selection, vec![0]);
370 assert_eq!(states.has(0), true);
372 assert_ne!(states.has(2), true);
373 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
375 states.set_choices(choices);
376 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
378 assert_eq!(states.selection.len(), 0);
379 let choices: &[String] = &[];
380 states.set_choices(choices);
381 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
383 assert_eq!(states.selection.len(), 0);
384 let choices: &[String] = &[
386 "lemon".to_string(),
387 "strawberry".to_string(),
388 "vanilla".to_string(),
389 "chocolate".to_string(),
390 ];
391 states.set_choices(choices);
392 assert_eq!(states.choice, 0);
393 states.prev_choice(true);
394 assert_eq!(states.choice, 3);
395 states.next_choice(true);
396 assert_eq!(states.choice, 0);
397 states.next_choice(true);
398 assert_eq!(states.choice, 1);
399 states.prev_choice(true);
400 assert_eq!(states.choice, 0);
401 }
402
403 #[test]
404 fn test_components_checkbox() {
405 let mut component = Checkbox::default()
407 .background(Color::Blue)
408 .foreground(Color::Red)
409 .borders(Borders::default())
410 .title("Which food do you prefer?", Alignment::Center)
411 .choices(&["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
412 .values(&[1, 4])
413 .rewind(false);
414 assert_eq!(component.states.selection, vec![1, 4]);
416 assert_eq!(component.states.choice, 0);
417 assert_eq!(component.states.choices.len(), 5);
418 component.attr(
419 Attribute::Content,
420 AttrValue::Payload(PropPayload::Vec(vec![
421 PropValue::Str(String::from("Pizza")),
422 PropValue::Str(String::from("Hummus")),
423 PropValue::Str(String::from("Ramen")),
424 PropValue::Str(String::from("Gyoza")),
425 PropValue::Str(String::from("Pasta")),
426 PropValue::Str(String::from("Falafel")),
427 ])),
428 );
429 assert_eq!(component.states.selection, vec![1, 4]);
430 assert_eq!(component.states.choices.len(), 6);
431 component.attr(
433 Attribute::Value,
434 AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
435 );
436 assert_eq!(component.states.selection, vec![1]);
437 assert_eq!(component.states.choices.len(), 6);
438 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
439 assert_eq!(
441 component.perform(Cmd::Move(Direction::Left)),
442 CmdResult::None,
443 );
444 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
445 assert_eq!(
447 component.perform(Cmd::Toggle),
448 CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
449 );
450 assert_eq!(
452 component.perform(Cmd::Move(Direction::Left)),
453 CmdResult::None,
454 );
455 assert_eq!(component.states.choice, 0);
456 assert_eq!(
458 component.perform(Cmd::Move(Direction::Right)),
459 CmdResult::None,
460 );
461 assert_eq!(
463 component.perform(Cmd::Toggle),
464 CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
465 );
466 assert_eq!(
468 component.perform(Cmd::Move(Direction::Right)),
469 CmdResult::None,
470 );
471 assert_eq!(component.states.choice, 2);
472 assert_eq!(
474 component.perform(Cmd::Move(Direction::Right)),
475 CmdResult::None,
476 );
477 assert_eq!(component.states.choice, 3);
478 assert_eq!(
480 component.perform(Cmd::Move(Direction::Right)),
481 CmdResult::None,
482 );
483 assert_eq!(component.states.choice, 4);
484 assert_eq!(
486 component.perform(Cmd::Move(Direction::Right)),
487 CmdResult::None,
488 );
489 assert_eq!(component.states.choice, 5);
490 assert_eq!(
492 component.perform(Cmd::Move(Direction::Right)),
493 CmdResult::None,
494 );
495 assert_eq!(component.states.choice, 5);
496 assert_eq!(
498 component.perform(Cmd::Submit),
499 CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
500 );
501 }
502}