use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::component::Component;
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
TextModifiers, Title,
};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::text::{Line, Span};
use tuirealm::ratatui::widgets::Tabs;
use tuirealm::state::{State, StateValue};
use crate::prop_ext::{CommonHighlight, CommonProps};
#[derive(Default)]
pub struct CheckboxStates {
pub choice: usize,
pub choices: Vec<String>,
pub selection: Vec<usize>,
}
impl CheckboxStates {
pub fn next_choice(&mut self, rewind: bool) {
if rewind && self.choice + 1 >= self.choices.len() {
self.choice = 0;
} else if self.choice + 1 < self.choices.len() {
self.choice += 1;
}
}
pub fn prev_choice(&mut self, rewind: bool) {
if rewind && self.choice == 0 && !self.choices.is_empty() {
self.choice = self.choices.len() - 1;
} else if self.choice > 0 {
self.choice -= 1;
}
}
pub fn toggle(&mut self) {
let option = self.choice;
if self.selection.contains(&option) {
let target_index = self.selection.iter().position(|x| *x == option).unwrap();
self.selection.remove(target_index);
} else {
self.selection.push(option);
}
}
pub fn select(&mut self, i: usize) {
if i < self.choices.len() && !self.selection.contains(&i) {
self.selection.push(i);
}
}
#[must_use]
pub fn has(&self, option: usize) -> bool {
self.selection.contains(&option)
}
pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
self.choices = choices.into();
self.selection.clear();
if self.choice >= self.choices.len() {
self.choice = match self.choices.len() {
0 => 0,
l => l - 1,
};
}
}
}
#[derive(Default)]
#[must_use]
pub struct Checkbox {
common: CommonProps,
common_hg: CommonHighlight,
props: Props,
pub states: CheckboxStates,
}
impl Checkbox {
pub fn foreground(mut self, fg: Color) -> Self {
self.attr(Attribute::Foreground, AttrValue::Color(fg));
self
}
pub fn background(mut self, bg: Color) -> Self {
self.attr(Attribute::Background, AttrValue::Color(bg));
self
}
pub fn modifiers(mut self, m: TextModifiers) -> Self {
self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
self
}
pub fn style(mut self, style: Style) -> Self {
self.attr(Attribute::Style, AttrValue::Style(style));
self
}
pub fn inactive(mut self, s: Style) -> Self {
self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
self
}
pub fn borders(mut self, b: Borders) -> Self {
self.attr(Attribute::Borders, AttrValue::Borders(b));
self
}
pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
self.attr(Attribute::Title, AttrValue::Title(title.into()));
self
}
pub fn highlight_style(mut self, s: Style) -> Self {
self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
self
}
pub fn rewind(mut self, r: bool) -> Self {
self.attr(Attribute::Rewind, AttrValue::Flag(r));
self
}
pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
self.attr(
Attribute::Content,
AttrValue::Payload(PropPayload::Vec(
choices
.into_iter()
.map(|v| PropValue::Str(v.into()))
.collect(),
)),
);
self
}
pub fn values(mut self, selected: &[usize]) -> Self {
self.attr(
Attribute::Value,
AttrValue::Payload(PropPayload::Vec(
selected.iter().map(|x| PropValue::Usize(*x)).collect(),
)),
);
self
}
pub fn always_active(mut self) -> Self {
self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
self
}
fn rewindable(&self) -> bool {
self.props
.get(Attribute::Rewind)
.and_then(AttrValue::as_flag)
.unwrap_or_default()
}
}
impl Component for Checkbox {
fn view(&mut self, render: &mut Frame, area: Rect) {
if !self.common.display {
return;
}
let choices: Vec<Line> = self
.states
.choices
.iter()
.enumerate()
.map(|(idx, x)| {
let checkbox: &str = if self.states.has(idx) { "☑ " } else { "☐ " };
Line::from(vec![Span::raw(checkbox), Span::raw(x.to_string())])
})
.collect();
let mut widget: Tabs = Tabs::new(choices)
.select(self.states.choice)
.style(self.common.style);
if self.common.is_active() {
widget = widget.highlight_style(self.common_hg.get_style(self.common.style));
}
if let Some(block) = self.common.get_block() {
widget = widget.block(block);
}
render.render_widget(widget, area);
}
fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
if let Some(value) = self
.common
.get_for_query(attr)
.or_else(|| self.common_hg.get_for_query(attr))
{
return Some(value);
}
self.props.get_for_query(attr)
}
fn attr(&mut self, attr: Attribute, value: AttrValue) {
if let Some(value) = self
.common
.set(attr, value)
.and_then(|value| self.common_hg.set(attr, value))
{
match attr {
Attribute::Content => {
let current_selection = self.states.selection.clone();
let choices: Vec<String> = value
.unwrap_payload()
.unwrap_vec()
.iter()
.cloned()
.map(|x| x.unwrap_str())
.collect();
self.states.set_choices(choices);
for c in current_selection {
self.states.select(c);
}
}
Attribute::Value => {
self.states.selection.clear();
for c in value.unwrap_payload().unwrap_vec() {
self.states.select(c.unwrap_usize());
}
}
attr => {
self.props.set(attr, value);
}
}
}
}
fn state(&self) -> State {
State::Vec(
self.states
.selection
.iter()
.map(|x| StateValue::Usize(*x))
.collect(),
)
}
fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd {
Cmd::Move(Direction::Right) => {
self.states.next_choice(self.rewindable());
CmdResult::Visual
}
Cmd::Move(Direction::Left) => {
self.states.prev_choice(self.rewindable());
CmdResult::Visual
}
Cmd::Toggle => {
self.states.toggle();
CmdResult::Changed(self.state())
}
Cmd::Submit => {
CmdResult::Submit(self.state())
}
_ => CmdResult::Invalid(cmd),
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::{assert_eq, assert_ne};
use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
use super::*;
#[test]
fn test_components_checkbox_states() {
let mut states: CheckboxStates = CheckboxStates::default();
assert_eq!(states.choice, 0);
assert_eq!(states.choices.len(), 0);
assert_eq!(states.selection.len(), 0);
let choices: &[String] = &[
"lemon".to_string(),
"strawberry".to_string(),
"vanilla".to_string(),
"chocolate".to_string(),
];
states.set_choices(choices);
assert_eq!(states.choice, 0);
assert_eq!(states.choices.len(), 4);
assert_eq!(states.selection.len(), 0);
states.toggle();
assert_eq!(states.selection, vec![0]);
states.prev_choice(false);
assert_eq!(states.choice, 0);
states.next_choice(false);
assert_eq!(states.choice, 1);
states.next_choice(false);
assert_eq!(states.choice, 2);
states.toggle();
assert_eq!(states.selection, vec![0, 2]);
states.next_choice(false);
states.next_choice(false);
assert_eq!(states.choice, 3);
states.prev_choice(false);
assert_eq!(states.choice, 2);
states.toggle();
assert_eq!(states.selection, vec![0]);
assert_eq!(states.has(0), true);
assert_ne!(states.has(2), true);
let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
states.set_choices(choices);
assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
assert_eq!(states.selection.len(), 0);
let choices: &[String] = &[];
states.set_choices(choices);
assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
assert_eq!(states.selection.len(), 0);
let choices: &[String] = &[
"lemon".to_string(),
"strawberry".to_string(),
"vanilla".to_string(),
"chocolate".to_string(),
];
states.set_choices(choices);
assert_eq!(states.choice, 0);
states.prev_choice(true);
assert_eq!(states.choice, 3);
states.next_choice(true);
assert_eq!(states.choice, 0);
states.next_choice(true);
assert_eq!(states.choice, 1);
states.prev_choice(true);
assert_eq!(states.choice, 0);
}
#[test]
fn test_components_checkbox() {
let mut component = Checkbox::default()
.background(Color::Blue)
.foreground(Color::Red)
.borders(Borders::default())
.title(Title::from("Which food do you prefer?").alignment(HorizontalAlignment::Center))
.choices(["Pizza", "Hummus", "Ramen", "Gyoza", "Pasta"])
.values(&[1, 4])
.rewind(false);
assert_eq!(component.states.selection, vec![1, 4]);
assert_eq!(component.states.choice, 0);
assert_eq!(component.states.choices.len(), 5);
component.attr(
Attribute::Content,
AttrValue::Payload(PropPayload::Vec(vec![
PropValue::Str(String::from("Pizza")),
PropValue::Str(String::from("Hummus")),
PropValue::Str(String::from("Ramen")),
PropValue::Str(String::from("Gyoza")),
PropValue::Str(String::from("Pasta")),
PropValue::Str(String::from("Falafel")),
])),
);
assert_eq!(component.states.selection, vec![1, 4]);
assert_eq!(component.states.choices.len(), 6);
component.attr(
Attribute::Value,
AttrValue::Payload(PropPayload::Vec(vec![PropValue::Usize(1)])),
);
assert_eq!(component.states.selection, vec![1]);
assert_eq!(component.states.choices.len(), 6);
assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::Visual,
);
assert_eq!(component.state(), State::Vec(vec![StateValue::Usize(1)]));
assert_eq!(
component.perform(Cmd::Toggle),
CmdResult::Changed(State::Vec(vec![StateValue::Usize(1), StateValue::Usize(0)]))
);
assert_eq!(
component.perform(Cmd::Move(Direction::Left)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 0);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(
component.perform(Cmd::Toggle),
CmdResult::Changed(State::Vec(vec![StateValue::Usize(0)]))
);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 2);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 3);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 4);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 5);
assert_eq!(
component.perform(Cmd::Move(Direction::Right)),
CmdResult::Visual,
);
assert_eq!(component.states.choice, 5);
assert_eq!(
component.perform(Cmd::Submit),
CmdResult::Submit(State::Vec(vec![StateValue::Usize(0)])),
);
}
#[test]
fn various_set_choice_types() {
CheckboxStates::default().set_choices(&["hello".to_string()]);
CheckboxStates::default().set_choices(vec!["hello".to_string()]);
CheckboxStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
}
#[test]
fn various_choice_types() {
let _ = Checkbox::default().choices(["hello"]);
let _ = Checkbox::default().choices(["hello".to_string()]);
let _ = Checkbox::default().choices(vec!["hello"]);
let _ = Checkbox::default().choices(vec!["hello".to_string()]);
let _ = Checkbox::default().choices(vec!["hello"].into_boxed_slice());
let _ = Checkbox::default().choices(vec!["hello".to_string()].into_boxed_slice());
}
}