1use tuirealm::command::{Cmd, CmdResult, Direction};
27use tuirealm::component::Component;
28use tuirealm::props::{
29 AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
30 TextModifiers, Title,
31};
32use tuirealm::ratatui::Frame;
33use tuirealm::ratatui::layout::Rect;
34use tuirealm::ratatui::text::{Line, Span};
35use tuirealm::ratatui::widgets::Tabs;
36use tuirealm::state::{State, StateValue};
37
38use crate::prop_ext::{CommonHighlight, CommonProps};
39
40#[derive(Default)]
44pub struct CheckboxStates {
45 pub choice: usize,
47 pub choices: Vec<String>,
49 pub selection: Vec<usize>,
51}
52
53impl CheckboxStates {
54 pub fn next_choice(&mut self, rewind: bool) {
56 if rewind && self.choice + 1 >= self.choices.len() {
57 self.choice = 0;
58 } else if self.choice + 1 < self.choices.len() {
59 self.choice += 1;
60 }
61 }
62
63 pub fn prev_choice(&mut self, rewind: bool) {
65 if rewind && self.choice == 0 && !self.choices.is_empty() {
66 self.choice = self.choices.len() - 1;
67 } else if self.choice > 0 {
68 self.choice -= 1;
69 }
70 }
71
72 pub fn toggle(&mut self) {
74 let option = self.choice;
75 if self.selection.contains(&option) {
76 let target_index = self.selection.iter().position(|x| *x == option).unwrap();
77 self.selection.remove(target_index);
78 } else {
79 self.selection.push(option);
80 }
81 }
82
83 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 #[must_use]
92 pub fn has(&self, option: usize) -> bool {
93 self.selection.contains(&option)
94 }
95
96 pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
101 self.choices = choices.into();
102 self.selection.clear();
104 if self.choice >= self.choices.len() {
106 self.choice = match self.choices.len() {
107 0 => 0,
108 l => l - 1,
109 };
110 }
111 }
112}
113
114#[derive(Default)]
120#[must_use]
121pub struct Checkbox {
122 common: CommonProps,
123 common_hg: CommonHighlight,
124 props: Props,
125 pub states: CheckboxStates,
126}
127
128impl Checkbox {
129 pub fn foreground(mut self, fg: Color) -> Self {
131 self.attr(Attribute::Foreground, AttrValue::Color(fg));
132 self
133 }
134
135 pub fn background(mut self, bg: Color) -> Self {
137 self.attr(Attribute::Background, AttrValue::Color(bg));
138 self
139 }
140
141 pub fn modifiers(mut self, m: TextModifiers) -> Self {
143 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
144 self
145 }
146
147 pub fn style(mut self, style: Style) -> Self {
151 self.attr(Attribute::Style, AttrValue::Style(style));
152 self
153 }
154
155 pub fn inactive(mut self, s: Style) -> Self {
157 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
158 self
159 }
160
161 pub fn borders(mut self, b: Borders) -> Self {
163 self.attr(Attribute::Borders, AttrValue::Borders(b));
164 self
165 }
166
167 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
169 self.attr(Attribute::Title, AttrValue::Title(title.into()));
170 self
171 }
172
173 pub fn highlight_style(mut self, s: Style) -> Self {
177 self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
178 self
179 }
180
181 pub fn rewind(mut self, r: bool) -> Self {
183 self.attr(Attribute::Rewind, AttrValue::Flag(r));
184 self
185 }
186
187 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
189 self.attr(
191 Attribute::Content,
192 AttrValue::Payload(PropPayload::Vec(
193 choices
194 .into_iter()
195 .map(|v| PropValue::Str(v.into()))
196 .collect(),
197 )),
198 );
199 self
200 }
201
202 pub fn values(mut self, selected: &[usize]) -> Self {
204 self.attr(
206 Attribute::Value,
207 AttrValue::Payload(PropPayload::Vec(
208 selected.iter().map(|x| PropValue::Usize(*x)).collect(),
209 )),
210 );
211 self
212 }
213
214 pub fn always_active(mut self) -> Self {
216 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
217 self
218 }
219
220 fn rewindable(&self) -> bool {
221 self.props
222 .get(Attribute::Rewind)
223 .and_then(AttrValue::as_flag)
224 .unwrap_or_default()
225 }
226}
227
228impl Component for Checkbox {
229 fn view(&mut self, render: &mut Frame, area: Rect) {
230 if !self.common.display {
231 return;
232 }
233
234 let choices: Vec<Line> = self
236 .states
237 .choices
238 .iter()
239 .enumerate()
240 .map(|(idx, x)| {
241 let checkbox: &str = if self.states.has(idx) { "☑ " } else { "☐ " };
242 Line::from(vec![Span::raw(checkbox), Span::raw(x.to_string())])
244 })
245 .collect();
246 let mut widget: Tabs = Tabs::new(choices)
247 .select(self.states.choice)
248 .style(self.common.style);
249
250 if self.common.is_active() {
251 widget = widget.highlight_style(self.common_hg.get_style(self.common.style));
252 }
253
254 if let Some(block) = self.common.get_block() {
255 widget = widget.block(block);
256 }
257
258 render.render_widget(widget, area);
259 }
260
261 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
262 if let Some(value) = self
263 .common
264 .get_for_query(attr)
265 .or_else(|| self.common_hg.get_for_query(attr))
266 {
267 return Some(value);
268 }
269
270 self.props.get_for_query(attr)
271 }
272
273 fn attr(&mut self, attr: Attribute, value: AttrValue) {
274 if let Some(value) = self
275 .common
276 .set(attr, value)
277 .and_then(|value| self.common_hg.set(attr, value))
278 {
279 match attr {
280 Attribute::Content => {
281 let current_selection = self.states.selection.clone();
283 let choices: Vec<String> = value
284 .unwrap_payload()
285 .unwrap_vec()
286 .iter()
287 .cloned()
288 .map(|x| x.unwrap_str())
289 .collect();
290 self.states.set_choices(choices);
291 for c in current_selection {
293 self.states.select(c);
294 }
295 }
296 Attribute::Value => {
297 self.states.selection.clear();
299 for c in value.unwrap_payload().unwrap_vec() {
300 self.states.select(c.unwrap_usize());
301 }
302 }
303 attr => {
304 self.props.set(attr, value);
305 }
306 }
307 }
308 }
309
310 fn state(&self) -> State {
315 State::Vec(
316 self.states
317 .selection
318 .iter()
319 .map(|x| StateValue::Usize(*x))
320 .collect(),
321 )
322 }
323
324 fn perform(&mut self, cmd: Cmd) -> CmdResult {
325 match cmd {
326 Cmd::Move(Direction::Right) => {
327 self.states.next_choice(self.rewindable());
329 CmdResult::Visual
330 }
331 Cmd::Move(Direction::Left) => {
332 self.states.prev_choice(self.rewindable());
334 CmdResult::Visual
335 }
336 Cmd::Toggle => {
337 self.states.toggle();
338 CmdResult::Changed(self.state())
339 }
340 Cmd::Submit => {
341 CmdResult::Submit(self.state())
343 }
344 _ => CmdResult::Invalid(cmd),
345 }
346 }
347}
348
349#[cfg(test)]
350mod test {
351
352 use pretty_assertions::{assert_eq, assert_ne};
353 use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
354
355 use super::*;
356
357 #[test]
358 fn test_components_checkbox_states() {
359 let mut states: CheckboxStates = CheckboxStates::default();
360 assert_eq!(states.choice, 0);
361 assert_eq!(states.choices.len(), 0);
362 assert_eq!(states.selection.len(), 0);
363 let choices: &[String] = &[
364 "lemon".to_string(),
365 "strawberry".to_string(),
366 "vanilla".to_string(),
367 "chocolate".to_string(),
368 ];
369 states.set_choices(choices);
370 assert_eq!(states.choice, 0);
371 assert_eq!(states.choices.len(), 4);
372 assert_eq!(states.selection.len(), 0);
373 states.toggle();
375 assert_eq!(states.selection, vec![0]);
376 states.prev_choice(false);
378 assert_eq!(states.choice, 0);
379 states.next_choice(false);
380 assert_eq!(states.choice, 1);
381 states.next_choice(false);
382 assert_eq!(states.choice, 2);
383 states.toggle();
384 assert_eq!(states.selection, vec![0, 2]);
385 states.next_choice(false);
387 states.next_choice(false);
388 assert_eq!(states.choice, 3);
389 states.prev_choice(false);
390 assert_eq!(states.choice, 2);
391 states.toggle();
392 assert_eq!(states.selection, vec![0]);
393 assert_eq!(states.has(0), true);
395 assert_ne!(states.has(2), true);
396 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
398 states.set_choices(choices);
399 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
401 assert_eq!(states.selection.len(), 0);
402 let choices: &[String] = &[];
403 states.set_choices(choices);
404 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
406 assert_eq!(states.selection.len(), 0);
407 let choices: &[String] = &[
409 "lemon".to_string(),
410 "strawberry".to_string(),
411 "vanilla".to_string(),
412 "chocolate".to_string(),
413 ];
414 states.set_choices(choices);
415 assert_eq!(states.choice, 0);
416 states.prev_choice(true);
417 assert_eq!(states.choice, 3);
418 states.next_choice(true);
419 assert_eq!(states.choice, 0);
420 states.next_choice(true);
421 assert_eq!(states.choice, 1);
422 states.prev_choice(true);
423 assert_eq!(states.choice, 0);
424 }
425
426 #[test]
427 fn test_components_checkbox() {
428 let mut component = Checkbox::default()
430 .background(Color::Blue)
431 .foreground(Color::Red)
432 .borders(Borders::default())
433 .title(Title::from("Which food do you prefer?").alignment(HorizontalAlignment::Center))
434 .choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
435 .values(&[1, 4])
436 .rewind(false);
437 assert_eq!(component.states.selection, vec![1, 4]);
439 assert_eq!(component.states.choice, 0);
440 assert_eq!(component.states.choices.len(), 5);
441 component.attr(
442 Attribute::Content,
443 AttrValue::Payload(PropPayload::Vec(vec![
444 PropValue::Str(String::from("Pizza")),
445 PropValue::Str(String::from("Hummus")),
446 PropValue::Str(String::from("Ramen")),
447 PropValue::Str(String::from("Gyoza")),
448 PropValue::Str(String::from("Pasta")),
449 PropValue::Str(String::from("Falafel")),
450 ])),
451 );
452 assert_eq!(component.states.selection, vec![1, 4]);
453 assert_eq!(component.states.choices.len(), 6);
454 component.attr(
456 Attribute::Value,
457 AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
458 );
459 assert_eq!(component.states.selection, vec![1]);
460 assert_eq!(component.states.choices.len(), 6);
461 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
462 assert_eq!(
464 component.perform(Cmd::Move(Direction::Left)),
465 CmdResult::Visual,
466 );
467 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
468 assert_eq!(
470 component.perform(Cmd::Toggle),
471 CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
472 );
473 assert_eq!(
475 component.perform(Cmd::Move(Direction::Left)),
476 CmdResult::Visual,
477 );
478 assert_eq!(component.states.choice, 0);
479 assert_eq!(
481 component.perform(Cmd::Move(Direction::Right)),
482 CmdResult::Visual,
483 );
484 assert_eq!(
486 component.perform(Cmd::Toggle),
487 CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
488 );
489 assert_eq!(
491 component.perform(Cmd::Move(Direction::Right)),
492 CmdResult::Visual,
493 );
494 assert_eq!(component.states.choice, 2);
495 assert_eq!(
497 component.perform(Cmd::Move(Direction::Right)),
498 CmdResult::Visual,
499 );
500 assert_eq!(component.states.choice, 3);
501 assert_eq!(
503 component.perform(Cmd::Move(Direction::Right)),
504 CmdResult::Visual,
505 );
506 assert_eq!(component.states.choice, 4);
507 assert_eq!(
509 component.perform(Cmd::Move(Direction::Right)),
510 CmdResult::Visual,
511 );
512 assert_eq!(component.states.choice, 5);
513 assert_eq!(
515 component.perform(Cmd::Move(Direction::Right)),
516 CmdResult::Visual,
517 );
518 assert_eq!(component.states.choice, 5);
519 assert_eq!(
521 component.perform(Cmd::Submit),
522 CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
523 );
524 }
525
526 #[test]
527 fn various_set_choice_types() {
528 CheckboxStates::default().set_choices(&["hello".to_string()]);
530 CheckboxStates::default().set_choices(vec!["hello".to_string()]);
532 CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
534 }
535
536 #[test]
537 fn various_choice_types() {
538 let _ = Checkbox::default().choices(["hello"]);
540 let _ = Checkbox::default().choices(["hello".to_string()]);
542 let _ = Checkbox::default().choices(vec!["hello"]);
544 let _ = Checkbox::default().choices(vec!["hello".to_string()]);
546 let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
548 let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
550 }
551}