use crate::event::{Key, KeyEvent};
use crate::render::Cell;
use crate::style::Color;
use crate::widget::theme::{LIGHT_GRAY, SUBTLE_GRAY};
use crate::widget::traits::{
EventResult, Interactive, RenderContext, View, WidgetProps, WidgetState,
};
use crate::{impl_styled_view, impl_widget_builders};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CheckboxStyle {
#[default]
Square,
Unicode,
Filled,
Circle,
}
impl CheckboxStyle {
fn chars(&self) -> (char, char) {
match self {
CheckboxStyle::Square => ('x', ' '),
CheckboxStyle::Unicode => ('☑', '☐'),
CheckboxStyle::Filled => ('■', '□'),
CheckboxStyle::Circle => ('●', '○'),
}
}
fn brackets(&self) -> Option<(char, char)> {
match self {
CheckboxStyle::Square => Some(('[', ']')),
_ => None,
}
}
}
#[derive(Clone, Debug)]
pub struct Checkbox {
label: String,
checked: bool,
state: WidgetState,
props: WidgetProps,
style: CheckboxStyle,
check_fg: Option<Color>,
}
impl Checkbox {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
checked: false,
state: WidgetState::new(),
props: WidgetProps::new(),
style: CheckboxStyle::default(),
check_fg: None,
}
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn style(mut self, style: CheckboxStyle) -> Self {
self.style = style;
self
}
pub fn check_fg(mut self, color: Color) -> Self {
self.check_fg = Some(color);
self
}
pub fn is_checked(&self) -> bool {
self.checked
}
pub fn set_checked(&mut self, checked: bool) {
self.checked = checked;
}
pub fn toggle(&mut self) {
if !self.state.disabled {
self.checked = !self.checked;
}
}
pub fn handle_key(&mut self, key: &Key) -> bool {
if self.state.disabled {
return false;
}
if matches!(key, Key::Enter | Key::Char(' ')) {
self.toggle();
true
} else {
false
}
}
}
impl Default for Checkbox {
fn default() -> Self {
Self::new("")
}
}
impl View for Checkbox {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let (checked_char, unchecked_char) = self.style.chars();
let brackets = self.style.brackets();
let mut x: u16 = 0;
let label_fg = self.state.resolve_fg(ctx.style, Color::WHITE);
let check_fg = if self.state.disabled {
if self.checked {
SUBTLE_GRAY } else {
Color::rgb(70, 70, 70) }
} else if self.checked {
self.check_fg.unwrap_or(Color::GREEN)
} else {
self.state.fg.unwrap_or(LIGHT_GRAY)
};
if self.state.focused && !self.state.disabled {
let mut cell = Cell::new('>');
cell.fg = Some(Color::CYAN);
ctx.set(x, 0, cell);
x += 1;
let space = Cell::new(' ');
ctx.set(x, 0, space);
x += 1;
}
if let Some((left, right)) = brackets {
let mut left_cell = Cell::new(left);
left_cell.fg = Some(label_fg);
ctx.set(x, 0, left_cell);
x += 1;
let check_char = if self.checked {
checked_char
} else {
unchecked_char
};
let mut check_cell = Cell::new(check_char);
check_cell.fg = Some(check_fg);
ctx.set(x, 0, check_cell);
x += 1;
let mut right_cell = Cell::new(right);
right_cell.fg = Some(label_fg);
ctx.set(x, 0, right_cell);
x += 1;
} else {
let check_char = if self.checked {
checked_char
} else {
unchecked_char
};
let mut check_cell = Cell::new(check_char);
check_cell.fg = Some(check_fg);
ctx.set(x, 0, check_cell);
x += 1;
}
ctx.set(x, 0, Cell::new(' '));
x += 1;
for ch in self.label.chars() {
if x >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(label_fg);
if self.state.focused && !self.state.disabled {
cell.modifier = crate::render::Modifier::BOLD;
}
ctx.set(x, 0, cell);
x += 1;
}
}
crate::impl_view_meta!("Checkbox");
}
impl Interactive for Checkbox {
fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
if self.state.disabled {
return EventResult::Ignored;
}
match event.key {
Key::Enter | Key::Char(' ') => {
self.checked = !self.checked;
EventResult::ConsumedAndRender
}
_ => EventResult::Ignored,
}
}
crate::impl_focus_handlers!(state);
}
pub fn checkbox(label: impl Into<String>) -> Checkbox {
Checkbox::new(label)
}
impl_styled_view!(Checkbox);
impl_widget_builders!(Checkbox);