use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CheckboxStyle {
#[default]
Bracket,
Unicode,
Circle,
Check,
Toggle,
}
impl CheckboxStyle {
pub fn chars(&self) -> (&'static str, &'static str) {
match self {
CheckboxStyle::Bracket => ("[x]", "[ ]"),
CheckboxStyle::Unicode => ("☑", "☐"),
CheckboxStyle::Circle => ("●", "○"),
CheckboxStyle::Check => ("✓", "✗"),
CheckboxStyle::Toggle => ("[■]", "[ ]"),
}
}
}
#[derive(Debug, Clone)]
pub struct CheckboxProps {
pub checked: bool,
pub label: Option<String>,
pub focused: bool,
pub style: CheckboxStyle,
pub checked_color: Option<Color>,
pub unchecked_color: Option<Color>,
pub label_color: Option<Color>,
pub disabled: bool,
pub focus_indicator: Option<String>,
}
impl Default for CheckboxProps {
fn default() -> Self {
Self {
checked: false,
label: None,
focused: false,
style: CheckboxStyle::Bracket,
checked_color: None,
unchecked_color: None,
label_color: None,
disabled: false,
focus_indicator: Some("> ".to_string()),
}
}
}
impl CheckboxProps {
pub fn new() -> Self {
Self::default()
}
pub fn with_label(label: impl Into<String>) -> Self {
Self {
label: Some(label.into()),
..Default::default()
}
}
#[must_use]
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
#[must_use]
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
#[must_use]
pub fn style(mut self, style: CheckboxStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn checked_color(mut self, color: Color) -> Self {
self.checked_color = Some(color);
self
}
#[must_use]
pub fn unchecked_color(mut self, color: Color) -> Self {
self.unchecked_color = Some(color);
self
}
#[must_use]
pub fn label_color(mut self, color: Color) -> Self {
self.label_color = Some(color);
self
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.checked_color = Some(color);
self.unchecked_color = Some(color);
self.label_color = Some(color);
self
}
#[must_use]
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
#[must_use]
pub fn focus_indicator(mut self, indicator: impl Into<String>) -> Self {
self.focus_indicator = Some(indicator.into());
self
}
#[must_use]
pub fn no_focus_indicator(mut self) -> Self {
self.focus_indicator = None;
self
}
pub fn render_string(&self) -> String {
let (checked_str, unchecked_str) = self.style.chars();
let indicator = if self.checked {
checked_str
} else {
unchecked_str
};
let focus_prefix = if self.focused {
self.focus_indicator.as_deref().unwrap_or("")
} else {
if let Some(ref fi) = self.focus_indicator {
&" ".repeat(fi.len())
} else {
""
}
};
if let Some(ref label) = self.label {
format!("{}{} {}", focus_prefix, indicator, label)
} else {
format!("{}{}", focus_prefix, indicator)
}
}
}
pub struct Checkbox;
impl Component for Checkbox {
type Props = CheckboxProps;
fn render(props: &Self::Props) -> Element {
let content = props.render_string();
let mut style = Style::new();
let color = if props.checked {
props.checked_color
} else {
props.unchecked_color
};
if let Some(c) = color {
style = style.fg(c);
}
if props.disabled {
style = style.add_modifier(Modifier::DIM);
}
if props.focused && !props.disabled {
style = style.add_modifier(Modifier::BOLD);
}
Element::styled_text(&content, style)
}
}
pub fn checkbox(checked: bool, label: &str) -> String {
CheckboxProps::with_label(label)
.checked(checked)
.no_focus_indicator()
.render_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checkbox_props_default() {
let props = CheckboxProps::default();
assert!(!props.checked);
assert!(!props.focused);
assert!(props.label.is_none());
}
#[test]
fn test_checkbox_props_with_label() {
let props = CheckboxProps::with_label("Test");
assert_eq!(props.label, Some("Test".to_string()));
}
#[test]
fn test_checkbox_props_builder() {
let props = CheckboxProps::with_label("Option")
.checked(true)
.focused(true)
.style(CheckboxStyle::Unicode)
.checked_color(Color::Green);
assert!(props.checked);
assert!(props.focused);
assert_eq!(props.style, CheckboxStyle::Unicode);
assert_eq!(props.checked_color, Some(Color::Green));
}
#[test]
fn test_checkbox_style_bracket() {
let (checked, unchecked) = CheckboxStyle::Bracket.chars();
assert_eq!(checked, "[x]");
assert_eq!(unchecked, "[ ]");
}
#[test]
fn test_checkbox_style_unicode() {
let (checked, unchecked) = CheckboxStyle::Unicode.chars();
assert_eq!(checked, "☑");
assert_eq!(unchecked, "☐");
}
#[test]
fn test_checkbox_style_circle() {
let (checked, unchecked) = CheckboxStyle::Circle.chars();
assert_eq!(checked, "●");
assert_eq!(unchecked, "○");
}
#[test]
fn test_checkbox_render_string_unchecked() {
let props = CheckboxProps::with_label("Test").no_focus_indicator();
assert_eq!(props.render_string(), "[ ] Test");
}
#[test]
fn test_checkbox_render_string_checked() {
let props = CheckboxProps::with_label("Test")
.checked(true)
.no_focus_indicator();
assert_eq!(props.render_string(), "[x] Test");
}
#[test]
fn test_checkbox_render_string_focused() {
let props = CheckboxProps::with_label("Test").focused(true);
assert_eq!(props.render_string(), "> [ ] Test");
}
#[test]
fn test_checkbox_render_string_not_focused_aligned() {
let props = CheckboxProps::with_label("Test").focused(false);
assert_eq!(props.render_string(), " [ ] Test");
}
#[test]
fn test_checkbox_render_string_unicode_style() {
let props = CheckboxProps::with_label("Option")
.checked(true)
.style(CheckboxStyle::Unicode)
.no_focus_indicator();
assert_eq!(props.render_string(), "☑ Option");
}
#[test]
fn test_checkbox_render_string_no_label() {
let props = CheckboxProps::new().checked(true).no_focus_indicator();
assert_eq!(props.render_string(), "[x]");
}
#[test]
fn test_checkbox_helper_function() {
assert_eq!(checkbox(true, "Yes"), "[x] Yes");
assert_eq!(checkbox(false, "No"), "[ ] No");
}
#[test]
fn test_checkbox_component_render() {
let props = CheckboxProps::with_label("Test")
.checked(true)
.no_focus_indicator();
let elem = Checkbox::render(&props);
match elem {
Element::Text { content, .. } => {
assert_eq!(content, "[x] Test");
}
_ => panic!("Expected Text element"),
}
}
#[test]
fn test_all_checkbox_styles() {
let styles = [
CheckboxStyle::Bracket,
CheckboxStyle::Unicode,
CheckboxStyle::Circle,
CheckboxStyle::Check,
CheckboxStyle::Toggle,
];
for style in &styles {
let (checked, unchecked) = style.chars();
assert!(
!checked.is_empty(),
"Style {:?} has empty checked char",
style
);
assert!(
!unchecked.is_empty(),
"Style {:?} has empty unchecked char",
style
);
}
}
}