use crate::_private::NonExhaustive;
use crate::checkbox::event::CheckOutcome;
use crate::util::{block_size, revert_style};
use rat_event::util::MouseFlags;
use rat_event::{ct_event, HandleEvent, MouseOnly, Regular};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
use rat_reloc::{relocate_area, RelocatableState};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::{BlockExt, StatefulWidget, Text, Widget};
use ratatui::style::Style;
use ratatui::text::Span;
use ratatui::widgets::Block;
#[cfg(feature = "unstable-widget-ref")]
use ratatui::widgets::StatefulWidgetRef;
use std::cmp::max;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug, Clone)]
pub struct Checkbox<'a> {
text: Text<'a>,
checked: Option<bool>,
default: Option<bool>,
true_str: Span<'a>,
false_str: Span<'a>,
style: Style,
focus_style: Option<Style>,
block: Option<Block<'a>>,
}
#[derive(Debug, Clone)]
pub struct CheckboxStyle {
pub style: Style,
pub focus: Option<Style>,
pub block: Option<Block<'static>>,
pub true_str: Option<Span<'static>>,
pub false_str: Option<Span<'static>>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug)]
pub struct CheckboxState {
pub area: Rect,
pub inner: Rect,
pub check_area: Rect,
pub text_area: Rect,
pub checked: bool,
pub default: bool,
pub focus: FocusFlag,
pub mouse: MouseFlags,
pub non_exhaustive: NonExhaustive,
}
pub(crate) mod event {
use rat_event::{ConsumedEvent, Outcome};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckOutcome {
Continue,
Unchanged,
Changed,
Value,
}
impl ConsumedEvent for CheckOutcome {
fn is_consumed(&self) -> bool {
*self != CheckOutcome::Continue
}
}
impl From<CheckOutcome> for Outcome {
fn from(value: CheckOutcome) -> Self {
match value {
CheckOutcome::Continue => Outcome::Continue,
CheckOutcome::Unchanged => Outcome::Unchanged,
CheckOutcome::Changed => Outcome::Changed,
CheckOutcome::Value => Outcome::Changed,
}
}
}
}
impl Default for CheckboxStyle {
fn default() -> Self {
Self {
style: Default::default(),
focus: None,
block: Default::default(),
true_str: None,
false_str: None,
non_exhaustive: NonExhaustive,
}
}
}
impl Default for Checkbox<'_> {
fn default() -> Self {
Self {
text: Default::default(),
checked: None,
default: None,
true_str: Span::from("[\u{2713}]"),
false_str: Span::from("[ ]"),
style: Default::default(),
focus_style: None,
block: None,
}
}
}
impl<'a> Checkbox<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn styles(mut self, styles: CheckboxStyle) -> Self {
self.style = styles.style;
if styles.focus.is_some() {
self.focus_style = styles.focus;
}
if let Some(block) = styles.block {
self.block = Some(block);
}
if let Some(true_str) = styles.true_str {
self.true_str = true_str;
}
if let Some(false_str) = styles.false_str {
self.false_str = false_str;
}
self.block = self.block.map(|v| v.style(self.style));
self
}
#[inline]
pub fn style(mut self, style: impl Into<Style>) -> Self {
self.style = style.into();
self
}
#[inline]
pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
self.focus_style = Some(style.into());
self
}
#[inline]
pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
self.text = text.into();
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = Some(checked);
self
}
pub fn default_(mut self, default: bool) -> Self {
self.default = Some(default);
self
}
#[inline]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self.block = self.block.map(|v| v.style(self.style));
self
}
pub fn true_str(mut self, str: Span<'a>) -> Self {
self.true_str = str;
self
}
pub fn false_str(mut self, str: Span<'a>) -> Self {
self.false_str = str;
self
}
fn check_len(&self) -> u16 {
max(
self.true_str.content.graphemes(true).count(),
self.false_str.content.graphemes(true).count(),
) as u16
}
pub fn width(&self) -> u16 {
let chk_len = self.check_len();
let txt_len = self.text.width() as u16;
chk_len + 1 + txt_len + block_size(&self.block).width
}
pub fn height(&self) -> u16 {
self.text.height() as u16 + block_size(&self.block).height
}
}
#[cfg(feature = "unstable-widget-ref")]
impl<'a> StatefulWidgetRef for Checkbox<'a> {
type State = CheckboxState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(self, area, buf, state);
}
}
impl StatefulWidget for Checkbox<'_> {
type State = CheckboxState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_ref(&self, area, buf, state);
}
}
fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
state.area = area;
state.inner = widget.block.inner_if_some(area);
let chk_len = widget.check_len();
state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
state.text_area = Rect::new(
state.inner.x + chk_len + 1,
state.inner.y,
state.inner.width.saturating_sub(chk_len + 1),
state.inner.height,
);
if let Some(checked) = widget.checked {
state.checked = checked;
}
if let Some(default) = widget.default {
state.default = default;
}
let style = widget.style;
let focus_style = if let Some(focus_style) = widget.focus_style {
style.patch(focus_style)
} else {
revert_style(style)
};
if widget.block.is_some() {
widget.block.render(area, buf);
if state.focus.get() {
buf.set_style(state.inner, focus_style);
}
} else {
if state.focus.get() {
buf.set_style(state.inner, focus_style);
} else {
buf.set_style(state.inner, widget.style);
}
}
let cc = if state.checked {
&widget.true_str
} else {
&widget.false_str
};
cc.render(state.check_area, buf);
(&widget.text).render(state.text_area, buf);
}
impl Clone for CheckboxState {
fn clone(&self) -> Self {
Self {
area: self.area,
inner: self.inner,
check_area: self.check_area,
text_area: self.text_area,
checked: self.checked,
default: self.default,
focus: FocusFlag::named(self.focus.name()),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl Default for CheckboxState {
fn default() -> Self {
Self {
area: Default::default(),
inner: Default::default(),
check_area: Default::default(),
text_area: Default::default(),
checked: false,
default: false,
focus: Default::default(),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl HasFocus for CheckboxState {
fn build(&self, builder: &mut FocusBuilder) {
builder.leaf_widget(self);
}
fn focus(&self) -> FocusFlag {
self.focus.clone()
}
fn area(&self) -> Rect {
self.area
}
}
impl RelocatableState for CheckboxState {
fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
self.area = relocate_area(self.area, shift, clip);
self.inner = relocate_area(self.inner, shift, clip);
}
}
impl CheckboxState {
pub fn new() -> Self {
Self::default()
}
pub fn named(name: &str) -> Self {
Self {
focus: FocusFlag::named(name),
..Default::default()
}
}
pub fn checked(&self) -> bool {
self.checked
}
pub fn set_checked(&mut self, checked: bool) -> bool {
let old_value = self.checked;
self.checked = checked;
old_value != self.checked
}
pub fn default_(&self) -> bool {
self.default
}
pub fn set_default(&mut self, default: bool) -> bool {
let old_value = self.default;
self.default = default;
old_value != self.default
}
pub fn value(&self) -> bool {
self.checked
}
pub fn set_value(&mut self, checked: bool) -> bool {
let old_value = self.checked;
self.checked = checked;
old_value != self.checked
}
pub fn flip_checked(&mut self) {
self.checked = !self.checked;
}
}
impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
let r = if self.is_focused() {
match event {
ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
self.flip_checked();
CheckOutcome::Value
}
ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
self.set_value(self.default);
CheckOutcome::Value
}
_ => CheckOutcome::Continue,
}
} else {
CheckOutcome::Continue
};
if r == CheckOutcome::Continue {
HandleEvent::handle(self, event, MouseOnly)
} else {
r
}
}
}
impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
match event {
ct_event!(mouse any for m) if self.mouse.doubleclick(self.area, m) => {
self.flip_checked();
CheckOutcome::Value
}
_ => CheckOutcome::Continue,
}
}
}
pub fn handle_events(
state: &mut CheckboxState,
focus: bool,
event: &crossterm::event::Event,
) -> CheckOutcome {
state.focus.set(focus);
HandleEvent::handle(state, event, Regular)
}
pub fn handle_mouse_events(
state: &mut CheckboxState,
event: &crossterm::event::Event,
) -> CheckOutcome {
HandleEvent::handle(state, event, MouseOnly)
}