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