use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Paragraph, Widget},
};
use crate::traits::{ClickRegion, FocusId};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckBoxAction {
Toggle,
}
#[derive(Debug, Clone)]
pub struct CheckBoxState {
pub checked: bool,
pub focused: bool,
pub enabled: bool,
}
impl Default for CheckBoxState {
fn default() -> Self {
Self {
checked: false,
focused: false,
enabled: true,
}
}
}
impl CheckBoxState {
pub fn new(checked: bool) -> Self {
Self {
checked,
..Default::default()
}
}
pub fn toggle(&mut self) {
if self.enabled {
self.checked = !self.checked;
}
}
pub fn set_checked(&mut self, checked: bool) {
if self.enabled {
self.checked = checked;
}
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
#[derive(Debug, Clone)]
pub struct CheckBoxStyle {
pub checked_symbol: &'static str,
pub unchecked_symbol: &'static str,
pub focused_fg: Color,
pub unfocused_fg: Color,
pub disabled_fg: Color,
pub checked_fg: Color,
}
impl Default for CheckBoxStyle {
fn default() -> Self {
Self {
checked_symbol: "[x]",
unchecked_symbol: "[ ]",
focused_fg: Color::Yellow,
unfocused_fg: Color::White,
disabled_fg: Color::DarkGray,
checked_fg: Color::Green,
}
}
}
impl From<&crate::theme::Theme> for CheckBoxStyle {
fn from(theme: &crate::theme::Theme) -> Self {
let p = &theme.palette;
Self {
checked_symbol: "[x]",
unchecked_symbol: "[ ]",
focused_fg: p.primary,
unfocused_fg: p.text,
disabled_fg: p.text_disabled,
checked_fg: p.success,
}
}
}
impl CheckBoxStyle {
pub fn ascii() -> Self {
Self::default()
}
pub fn unicode() -> Self {
Self {
checked_symbol: "☑",
unchecked_symbol: "☐",
..Default::default()
}
}
pub fn checkmark() -> Self {
Self {
checked_symbol: "✓",
unchecked_symbol: "○",
..Default::default()
}
}
pub fn custom(checked: &'static str, unchecked: &'static str) -> Self {
Self {
checked_symbol: checked,
unchecked_symbol: unchecked,
..Default::default()
}
}
pub fn focused_fg(mut self, color: Color) -> Self {
self.focused_fg = color;
self
}
pub fn unfocused_fg(mut self, color: Color) -> Self {
self.unfocused_fg = color;
self
}
pub fn disabled_fg(mut self, color: Color) -> Self {
self.disabled_fg = color;
self
}
pub fn checked_fg(mut self, color: Color) -> Self {
self.checked_fg = color;
self
}
}
pub struct CheckBox<'a> {
label: &'a str,
state: &'a CheckBoxState,
style: CheckBoxStyle,
focus_id: FocusId,
}
impl<'a> CheckBox<'a> {
pub fn new(label: &'a str, state: &'a CheckBoxState) -> Self {
Self {
label,
state,
style: CheckBoxStyle::default(),
focus_id: FocusId::default(),
}
}
pub fn style(mut self, style: CheckBoxStyle) -> Self {
self.style = style;
self
}
pub fn theme(self, theme: &crate::theme::Theme) -> Self {
self.style(CheckBoxStyle::from(theme))
}
pub fn focus_id(mut self, id: FocusId) -> Self {
self.focus_id = id;
self
}
fn build_line(&self) -> Line<'a> {
let symbol = if self.state.checked {
self.style.checked_symbol
} else {
self.style.unchecked_symbol
};
let fg_color = if !self.state.enabled {
self.style.disabled_fg
} else if self.state.focused {
self.style.focused_fg
} else if self.state.checked {
self.style.checked_fg
} else {
self.style.unfocused_fg
};
let mut style = Style::default().fg(fg_color);
if self.state.focused && self.state.enabled {
style = style.add_modifier(Modifier::BOLD);
}
Line::from(vec![
Span::styled(symbol, style),
Span::styled(" ", style),
Span::styled(self.label, style),
])
}
pub fn width(&self) -> u16 {
let symbol_len = if self.state.checked {
self.style.checked_symbol.chars().count()
} else {
self.style.unchecked_symbol.chars().count()
};
(symbol_len + 1 + self.label.chars().count()) as u16
}
pub fn render_stateful(self, area: Rect, buf: &mut Buffer) -> ClickRegion<CheckBoxAction> {
let width = self.width().min(area.width);
let click_area = Rect::new(area.x, area.y, width, 1);
let line = self.build_line();
let paragraph = Paragraph::new(line);
paragraph.render(area, buf);
ClickRegion::new(click_area, CheckBoxAction::Toggle)
}
}
impl Widget for CheckBox<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let line = self.build_line();
let paragraph = Paragraph::new(line);
paragraph.render(area, buf);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_state_default() {
let state = CheckBoxState::default();
assert!(!state.checked);
assert!(!state.focused);
assert!(state.enabled);
}
#[test]
fn test_state_new() {
let state = CheckBoxState::new(true);
assert!(state.checked);
assert!(!state.focused);
assert!(state.enabled);
}
#[test]
fn test_toggle() {
let mut state = CheckBoxState::new(false);
assert!(!state.checked);
state.toggle();
assert!(state.checked);
state.toggle();
assert!(!state.checked);
}
#[test]
fn test_toggle_disabled() {
let mut state = CheckBoxState::new(false);
state.enabled = false;
state.toggle();
assert!(!state.checked); }
#[test]
fn test_set_checked() {
let mut state = CheckBoxState::new(false);
state.set_checked(true);
assert!(state.checked);
state.set_checked(false);
assert!(!state.checked);
}
#[test]
fn test_set_checked_disabled() {
let mut state = CheckBoxState::new(false);
state.enabled = false;
state.set_checked(true);
assert!(!state.checked); }
#[test]
fn test_style_default() {
let style = CheckBoxStyle::default();
assert_eq!(style.checked_symbol, "[x]");
assert_eq!(style.unchecked_symbol, "[ ]");
}
#[test]
fn test_style_unicode() {
let style = CheckBoxStyle::unicode();
assert_eq!(style.checked_symbol, "☑");
assert_eq!(style.unchecked_symbol, "☐");
}
#[test]
fn test_style_checkmark() {
let style = CheckBoxStyle::checkmark();
assert_eq!(style.checked_symbol, "✓");
assert_eq!(style.unchecked_symbol, "○");
}
#[test]
fn test_style_custom() {
let style = CheckBoxStyle::custom("ON", "OFF");
assert_eq!(style.checked_symbol, "ON");
assert_eq!(style.unchecked_symbol, "OFF");
}
#[test]
fn test_checkbox_width() {
let state = CheckBoxState::new(false);
let checkbox = CheckBox::new("Test", &state);
assert_eq!(checkbox.width(), 8);
}
#[test]
fn test_checkbox_width_unicode() {
let state = CheckBoxState::new(true);
let checkbox = CheckBox::new("Test", &state).style(CheckBoxStyle::unicode());
assert_eq!(checkbox.width(), 6);
}
#[test]
fn test_render_basic() {
let state = CheckBoxState::new(true);
let checkbox = CheckBox::new("Test", &state);
let area = Rect::new(0, 0, 20, 1);
let mut buffer = Buffer::empty(area);
checkbox.render(area, &mut buffer);
let content: String = (0..8)
.map(|x| buffer[(x, 0)].symbol().to_string())
.collect();
assert!(content.contains("[x]"));
}
#[test]
fn test_render_stateful() {
let state = CheckBoxState::new(false);
let checkbox = CheckBox::new("Click me", &state);
let area = Rect::new(5, 3, 20, 1);
let mut buffer = Buffer::empty(Rect::new(0, 0, 30, 10));
let click_region = checkbox.render_stateful(area, &mut buffer);
assert_eq!(click_region.area.x, 5);
assert_eq!(click_region.area.y, 3);
assert_eq!(click_region.data, CheckBoxAction::Toggle);
}
#[test]
fn test_click_region_detection() {
let state = CheckBoxState::new(false);
let checkbox = CheckBox::new("Test", &state);
let area = Rect::new(10, 5, 20, 1);
let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 10));
let click_region = checkbox.render_stateful(area, &mut buffer);
assert!(click_region.contains(10, 5));
assert!(click_region.contains(15, 5));
assert!(!click_region.contains(9, 5));
assert!(!click_region.contains(10, 4));
assert!(!click_region.contains(10, 6));
}
}