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 highlight_style_inactive(mut self, s: Style) -> Self {
183 self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
184 self
185 }
186
187 pub fn rewind(mut self, r: bool) -> Self {
189 self.attr(Attribute::Rewind, AttrValue::Flag(r));
190 self
191 }
192
193 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
195 self.attr(
197 Attribute::Content,
198 AttrValue::Payload(PropPayload::Vec(
199 choices
200 .into_iter()
201 .map(|v| PropValue::Str(v.into()))
202 .collect(),
203 )),
204 );
205 self
206 }
207
208 pub fn values(mut self, selected: &[usize]) -> Self {
210 self.attr(
212 Attribute::Value,
213 AttrValue::Payload(PropPayload::Vec(
214 selected.iter().map(|x| PropValue::Usize(*x)).collect(),
215 )),
216 );
217 self
218 }
219
220 pub fn always_active(mut self) -> Self {
222 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
223 self
224 }
225
226 fn rewindable(&self) -> bool {
227 self.props
228 .get(Attribute::Rewind)
229 .and_then(AttrValue::as_flag)
230 .unwrap_or_default()
231 }
232}
233
234impl Component for Checkbox {
235 fn view(&mut self, render: &mut Frame, area: Rect) {
236 if !self.common.display {
237 return;
238 }
239
240 let choices: Vec<Line> = self
242 .states
243 .choices
244 .iter()
245 .enumerate()
246 .map(|(idx, x)| {
247 let checkbox: &str = if self.states.has(idx) { "☑ " } else { "☐ " };
248 Line::from(vec![Span::raw(checkbox), Span::raw(x.to_string())])
250 })
251 .collect();
252 let mut widget: Tabs = Tabs::new(choices)
253 .select(self.states.choice)
254 .style(self.common.style)
255 .highlight_style(
256 self.common_hg
257 .get_style_focus(self.common.style, self.common.is_active()),
258 );
259
260 if let Some(block) = self.common.get_block() {
261 widget = widget.block(block);
262 }
263
264 render.render_widget(widget, area);
265 }
266
267 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
268 if let Some(value) = self
269 .common
270 .get_for_query(attr)
271 .or_else(|| self.common_hg.get_for_query(attr))
272 {
273 return Some(value);
274 }
275
276 self.props.get_for_query(attr)
277 }
278
279 fn attr(&mut self, attr: Attribute, value: AttrValue) {
280 if let Some(value) = self
281 .common
282 .set(attr, value)
283 .and_then(|value| self.common_hg.set(attr, value))
284 {
285 match attr {
286 Attribute::Content => {
287 let current_selection = self.states.selection.clone();
289 let choices: Vec<String> = value
290 .unwrap_payload()
291 .unwrap_vec()
292 .iter()
293 .cloned()
294 .map(|x| x.unwrap_str())
295 .collect();
296 self.states.set_choices(choices);
297 for c in current_selection {
299 self.states.select(c);
300 }
301 }
302 Attribute::Value => {
303 self.states.selection.clear();
305 for c in value.unwrap_payload().unwrap_vec() {
306 self.states.select(c.unwrap_usize());
307 }
308 }
309 attr => {
310 self.props.set(attr, value);
311 }
312 }
313 }
314 }
315
316 fn state(&self) -> State {
321 State::Vec(
322 self.states
323 .selection
324 .iter()
325 .map(|x| StateValue::Usize(*x))
326 .collect(),
327 )
328 }
329
330 fn perform(&mut self, cmd: Cmd) -> CmdResult {
331 match cmd {
332 Cmd::Move(Direction::Right) => {
333 self.states.next_choice(self.rewindable());
335 CmdResult::Visual
336 }
337 Cmd::Move(Direction::Left) => {
338 self.states.prev_choice(self.rewindable());
340 CmdResult::Visual
341 }
342 Cmd::Toggle => {
343 self.states.toggle();
344 CmdResult::Changed(self.state())
345 }
346 Cmd::Submit => {
347 CmdResult::Submit(self.state())
349 }
350 _ => CmdResult::Invalid(cmd),
351 }
352 }
353}
354
355#[cfg(test)]
356mod test {
357
358 use pretty_assertions::{assert_eq, assert_ne};
359 use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
360
361 use super::*;
362
363 #[test]
364 fn test_components_checkbox_states() {
365 let mut states: CheckboxStates = CheckboxStates::default();
366 assert_eq!(states.choice, 0);
367 assert_eq!(states.choices.len(), 0);
368 assert_eq!(states.selection.len(), 0);
369 let choices: &[String] = &[
370 "lemon".to_string(),
371 "strawberry".to_string(),
372 "vanilla".to_string(),
373 "chocolate".to_string(),
374 ];
375 states.set_choices(choices);
376 assert_eq!(states.choice, 0);
377 assert_eq!(states.choices.len(), 4);
378 assert_eq!(states.selection.len(), 0);
379 states.toggle();
381 assert_eq!(states.selection, vec![0]);
382 states.prev_choice(false);
384 assert_eq!(states.choice, 0);
385 states.next_choice(false);
386 assert_eq!(states.choice, 1);
387 states.next_choice(false);
388 assert_eq!(states.choice, 2);
389 states.toggle();
390 assert_eq!(states.selection, vec![0, 2]);
391 states.next_choice(false);
393 states.next_choice(false);
394 assert_eq!(states.choice, 3);
395 states.prev_choice(false);
396 assert_eq!(states.choice, 2);
397 states.toggle();
398 assert_eq!(states.selection, vec![0]);
399 assert_eq!(states.has(0), true);
401 assert_ne!(states.has(2), true);
402 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
404 states.set_choices(choices);
405 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
407 assert_eq!(states.selection.len(), 0);
408 let choices: &[String] = &[];
409 states.set_choices(choices);
410 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
412 assert_eq!(states.selection.len(), 0);
413 let choices: &[String] = &[
415 "lemon".to_string(),
416 "strawberry".to_string(),
417 "vanilla".to_string(),
418 "chocolate".to_string(),
419 ];
420 states.set_choices(choices);
421 assert_eq!(states.choice, 0);
422 states.prev_choice(true);
423 assert_eq!(states.choice, 3);
424 states.next_choice(true);
425 assert_eq!(states.choice, 0);
426 states.next_choice(true);
427 assert_eq!(states.choice, 1);
428 states.prev_choice(true);
429 assert_eq!(states.choice, 0);
430 }
431
432 #[test]
433 fn test_components_checkbox() {
434 let mut component = Checkbox::default()
436 .background(Color::Blue)
437 .foreground(Color::Red)
438 .borders(Borders::default())
439 .title(Title::from("Which food do you prefer?").alignment(HorizontalAlignment::Center))
440 .choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
441 .values(&[1, 4])
442 .rewind(false);
443 assert_eq!(component.states.selection, vec![1, 4]);
445 assert_eq!(component.states.choice, 0);
446 assert_eq!(component.states.choices.len(), 5);
447 component.attr(
448 Attribute::Content,
449 AttrValue::Payload(PropPayload::Vec(vec![
450 PropValue::Str(String::from("Pizza")),
451 PropValue::Str(String::from("Hummus")),
452 PropValue::Str(String::from("Ramen")),
453 PropValue::Str(String::from("Gyoza")),
454 PropValue::Str(String::from("Pasta")),
455 PropValue::Str(String::from("Falafel")),
456 ])),
457 );
458 assert_eq!(component.states.selection, vec![1, 4]);
459 assert_eq!(component.states.choices.len(), 6);
460 component.attr(
462 Attribute::Value,
463 AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
464 );
465 assert_eq!(component.states.selection, vec![1]);
466 assert_eq!(component.states.choices.len(), 6);
467 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
468 assert_eq!(
470 component.perform(Cmd::Move(Direction::Left)),
471 CmdResult::Visual,
472 );
473 assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
474 assert_eq!(
476 component.perform(Cmd::Toggle),
477 CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
478 );
479 assert_eq!(
481 component.perform(Cmd::Move(Direction::Left)),
482 CmdResult::Visual,
483 );
484 assert_eq!(component.states.choice, 0);
485 assert_eq!(
487 component.perform(Cmd::Move(Direction::Right)),
488 CmdResult::Visual,
489 );
490 assert_eq!(
492 component.perform(Cmd::Toggle),
493 CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
494 );
495 assert_eq!(
497 component.perform(Cmd::Move(Direction::Right)),
498 CmdResult::Visual,
499 );
500 assert_eq!(component.states.choice, 2);
501 assert_eq!(
503 component.perform(Cmd::Move(Direction::Right)),
504 CmdResult::Visual,
505 );
506 assert_eq!(component.states.choice, 3);
507 assert_eq!(
509 component.perform(Cmd::Move(Direction::Right)),
510 CmdResult::Visual,
511 );
512 assert_eq!(component.states.choice, 4);
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::Move(Direction::Right)),
522 CmdResult::Visual,
523 );
524 assert_eq!(component.states.choice, 5);
525 assert_eq!(
527 component.perform(Cmd::Submit),
528 CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
529 );
530 }
531
532 #[test]
533 fn various_set_choice_types() {
534 CheckboxStates::default().set_choices(&["hello".to_string()]);
536 CheckboxStates::default().set_choices(vec!["hello".to_string()]);
538 CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
540 }
541
542 #[test]
543 fn various_choice_types() {
544 let _ = Checkbox::default().choices(["hello"]);
546 let _ = Checkbox::default().choices(["hello".to_string()]);
548 let _ = Checkbox::default().choices(vec!["hello"]);
550 let _ = Checkbox::default().choices(vec!["hello".to_string()]);
552 let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
554 let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
556 }
557}