#![warn(missing_docs)]
#![warn(clippy::pedantic)]
use std::borrow::Cow;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Style, Styled};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Widget};
pub mod symbols;
#[expect(clippy::struct_field_names)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Checkbox<'a> {
label: Line<'a>,
checked: bool,
block: Option<Block<'a>>,
style: Style,
checkbox_style: Style,
label_style: Style,
checked_symbol: Cow<'a, str>,
unchecked_symbol: Cow<'a, str>,
}
impl Default for Checkbox<'_> {
fn default() -> Self {
Self {
label: Line::default(),
checked: false,
block: None,
style: Style::default(),
checkbox_style: Style::default(),
label_style: Style::default(),
checked_symbol: Cow::Borrowed(symbols::CHECKED),
unchecked_symbol: Cow::Borrowed(symbols::UNCHECKED),
}
}
}
impl<'a> Checkbox<'a> {
pub fn new<T>(label: T, checked: bool) -> Self
where
T: Into<Line<'a>>,
{
Self {
label: label.into(),
checked,
..Default::default()
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label<T>(mut self, label: T) -> Self
where
T: Into<Line<'a>>,
{
self.label = label.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn checkbox_style<S: Into<Style>>(mut self, style: S) -> Self {
self.checkbox_style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
self.label_style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn checked_symbol<T>(mut self, symbol: T) -> Self
where
T: Into<Cow<'a, str>>,
{
self.checked_symbol = symbol.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn unchecked_symbol<T>(mut self, symbol: T) -> Self
where
T: Into<Cow<'a, str>>,
{
self.unchecked_symbol = symbol.into();
self
}
}
impl Styled for Checkbox<'_> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
self.style = style.into();
self
}
}
impl Widget for Checkbox<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
Widget::render(&self, area, buf);
}
}
impl Widget for &Checkbox<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let inner = if let Some(ref block) = self.block {
let inner_area = block.inner(area);
block.render(area, buf);
inner_area
} else {
area
};
self.render_checkbox(inner, buf);
}
}
impl Checkbox<'_> {
fn render_checkbox(&self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
let symbol = if self.checked {
&self.checked_symbol
} else {
&self.unchecked_symbol
};
let checkbox_style = self.style.patch(self.checkbox_style);
let label_style = self.style.patch(self.label_style);
let checkbox_span = Span::styled(symbol.as_ref(), checkbox_style);
let styled_label = self.label.clone().patch_style(label_style);
let mut spans = vec![checkbox_span, Span::raw(" ")];
spans.extend(styled_label.spans);
let line = Line::from(spans);
line.render(area, buf);
}
}
#[cfg(test)]
mod tests {
use ratatui::style::{Color, Modifier, Stylize};
use super::*;
#[test]
fn checkbox_new() {
let checkbox = Checkbox::new("Test", true);
assert_eq!(checkbox.label, Line::from("Test"));
assert!(checkbox.checked);
}
#[test]
fn checkbox_default() {
let checkbox = Checkbox::default();
assert_eq!(checkbox.label, Line::default());
assert!(!checkbox.checked);
}
#[test]
fn checkbox_label() {
let checkbox = Checkbox::default().label("New label");
assert_eq!(checkbox.label, Line::from("New label"));
}
#[test]
fn checkbox_checked() {
let checkbox = Checkbox::default().checked(true);
assert!(checkbox.checked);
}
#[test]
fn checkbox_style() {
let style = Style::default().fg(Color::Red);
let checkbox = Checkbox::default().style(style);
assert_eq!(checkbox.style, style);
}
#[test]
fn checkbox_checkbox_style() {
let style = Style::default().fg(Color::Green);
let checkbox = Checkbox::default().checkbox_style(style);
assert_eq!(checkbox.checkbox_style, style);
}
#[test]
fn checkbox_label_style() {
let style = Style::default().fg(Color::Blue);
let checkbox = Checkbox::default().label_style(style);
assert_eq!(checkbox.label_style, style);
}
#[test]
fn checkbox_checked_symbol() {
let checkbox = Checkbox::default().checked_symbol("[X]");
assert_eq!(checkbox.checked_symbol, "[X]");
}
#[test]
fn checkbox_unchecked_symbol() {
let checkbox = Checkbox::default().unchecked_symbol("[ ]");
assert_eq!(checkbox.unchecked_symbol, "[ ]");
}
#[test]
fn checkbox_styled_trait() {
let checkbox = Checkbox::default().red();
assert_eq!(checkbox.style, Style::default().fg(Color::Red));
}
#[test]
fn checkbox_render_unchecked() {
let checkbox = Checkbox::new("Test", false);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
checkbox.render(buffer.area, &mut buffer);
assert!(buffer
.cell(buffer.area.as_position())
.unwrap()
.symbol()
.starts_with('☐'));
}
#[test]
fn checkbox_render_checked() {
let checkbox = Checkbox::new("Test", true);
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
checkbox.render(buffer.area, &mut buffer);
assert!(buffer
.cell(buffer.area.as_position())
.unwrap()
.symbol()
.starts_with('☑'));
}
#[test]
fn checkbox_render_empty_area() {
let checkbox = Checkbox::new("Test", true);
let mut buffer = Buffer::empty(Rect::new(0, 0, 0, 0));
checkbox.render(buffer.area, &mut buffer);
}
#[test]
fn checkbox_render_with_block() {
let checkbox = Checkbox::new("Test", true).block(Block::bordered());
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 3));
checkbox.render(buffer.area, &mut buffer);
}
#[test]
fn checkbox_render_with_custom_symbols() {
let checkbox = Checkbox::new("Test", true)
.checked_symbol("[X]")
.unchecked_symbol("[ ]");
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
checkbox.render(buffer.area, &mut buffer);
assert!(buffer
.cell(buffer.area.as_position())
.unwrap()
.symbol()
.starts_with('['));
}
#[test]
fn checkbox_with_styled_label() {
let checkbox = Checkbox::new("Test".blue(), true);
assert_eq!(checkbox.label.spans[0].style.fg, Some(Color::Blue));
}
#[test]
fn checkbox_complex_styling() {
let checkbox = Checkbox::new("Feature", true)
.style(Style::default().fg(Color::White))
.checkbox_style(
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
)
.label_style(Style::default().fg(Color::Gray));
assert_eq!(checkbox.style.fg, Some(Color::White));
assert_eq!(checkbox.checkbox_style.fg, Some(Color::Green));
assert_eq!(checkbox.label_style.fg, Some(Color::Gray));
}
#[test]
fn checkbox_emoji_symbols() {
let checkbox = Checkbox::new("Test", true)
.checked_symbol("✅ ")
.unchecked_symbol("⬜ ");
assert_eq!(checkbox.checked_symbol, "✅ ");
assert_eq!(checkbox.unchecked_symbol, "⬜ ");
}
#[test]
fn checkbox_unicode_symbols() {
let checkbox = Checkbox::new("Test", false)
.checked_symbol("● ")
.unchecked_symbol("○ ");
assert_eq!(checkbox.checked_symbol, "● ");
assert_eq!(checkbox.unchecked_symbol, "○ ");
}
#[test]
fn checkbox_arrow_symbols() {
let checkbox = Checkbox::new("Test", true)
.checked_symbol("▶ ")
.unchecked_symbol("▷ ");
assert_eq!(checkbox.checked_symbol, "▶ ");
assert_eq!(checkbox.unchecked_symbol, "▷ ");
}
#[test]
fn checkbox_parenthesis_symbols() {
let checkbox = Checkbox::new("Test", false)
.checked_symbol("(X)")
.unchecked_symbol("(O)");
assert_eq!(checkbox.checked_symbol, "(X)");
assert_eq!(checkbox.unchecked_symbol, "(O)");
}
#[test]
fn checkbox_minus_symbols() {
let checkbox = Checkbox::new("Test", false)
.checked_symbol("[+]")
.unchecked_symbol("[-]");
assert_eq!(checkbox.checked_symbol, "[+]");
assert_eq!(checkbox.unchecked_symbol, "[-]");
}
#[test]
fn checkbox_predefined_minus_symbol() {
use crate::symbols;
let checkbox = Checkbox::new("Test", false).unchecked_symbol(symbols::UNCHECKED_MINUS);
assert_eq!(checkbox.unchecked_symbol, "[-]");
}
#[test]
fn checkbox_predefined_parenthesis_symbols() {
use crate::symbols;
let checkbox = Checkbox::new("Test", true)
.checked_symbol(symbols::CHECKED_PARENTHESIS_X)
.unchecked_symbol(symbols::UNCHECKED_PARENTHESIS_O);
assert_eq!(checkbox.checked_symbol, "(X)");
assert_eq!(checkbox.unchecked_symbol, "(O)");
}
#[test]
fn checkbox_render_emoji() {
let checkbox = Checkbox::new("Emoji", true)
.checked_symbol("✅ ")
.unchecked_symbol("⬜ ");
let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 1));
checkbox.render(buffer.area, &mut buffer);
assert!(buffer.area.area() > 0);
}
#[test]
fn checkbox_label_style_overrides() {
let checkbox = Checkbox::new("Test", true)
.style(Style::default().fg(Color::White))
.label_style(Style::default().fg(Color::Blue));
assert_eq!(checkbox.style.fg, Some(Color::White));
assert_eq!(checkbox.label_style.fg, Some(Color::Blue));
}
}