use ratatui::widgets::Paragraph;
use super::{Component, EventContext, RenderContext, Toggleable};
use crate::input::{Event, Key};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwitchMessage {
Toggle,
SetOn(bool),
SetLabel(Option<String>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwitchOutput {
Toggled(bool),
On,
Off,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct SwitchState {
on: bool,
label: Option<String>,
on_label: String,
off_label: String,
}
impl Default for SwitchState {
fn default() -> Self {
Self {
on: false,
label: None,
on_label: "ON".to_string(),
off_label: "OFF".to_string(),
}
}
}
impl SwitchState {
pub fn new() -> Self {
Self::default()
}
pub fn with_on(mut self, on: bool) -> Self {
self.on = on;
self
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_on_label(mut self, on_label: impl Into<String>) -> Self {
self.on_label = on_label.into();
self
}
pub fn with_off_label(mut self, off_label: impl Into<String>) -> Self {
self.off_label = off_label.into();
self
}
pub fn is_on(&self) -> bool {
self.on
}
pub fn set_on(&mut self, on: bool) {
self.on = on;
}
pub fn toggle(&mut self) {
self.on = !self.on;
}
pub fn label(&self) -> Option<&str> {
self.label.as_deref()
}
pub fn set_label(&mut self, label: Option<String>) {
self.label = label;
}
pub fn update(&mut self, msg: SwitchMessage) -> Option<SwitchOutput> {
Switch::update(self, msg)
}
}
pub struct Switch;
impl Component for Switch {
type State = SwitchState;
type Message = SwitchMessage;
type Output = SwitchOutput;
fn init() -> Self::State {
SwitchState::default()
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
match msg {
SwitchMessage::Toggle => {
state.on = !state.on;
if state.on {
Some(SwitchOutput::On)
} else {
Some(SwitchOutput::Off)
}
}
SwitchMessage::SetOn(on) => {
let changed = state.on != on;
state.on = on;
if changed {
Some(SwitchOutput::Toggled(on))
} else {
None
}
}
SwitchMessage::SetLabel(label) => {
state.label = label;
None
}
}
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Enter | Key::Char(' ') => Some(SwitchMessage::Toggle),
_ => None,
}
} else {
None
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let indicator = if state.on {
format!("(*) {}", state.on_label)
} else {
format!("( ) {}", state.off_label)
};
let text = if let Some(label) = &state.label {
format!("{} {}", label, indicator)
} else {
indicator
};
let style = if ctx.disabled {
ctx.theme.disabled_style()
} else if state.on {
if ctx.focused {
ctx.theme.focused_style()
} else {
ctx.theme.success_style()
}
} else if ctx.focused {
ctx.theme.focused_style()
} else {
ctx.theme.normal_style()
};
let paragraph = Paragraph::new(text).style(style);
let annotation = crate::annotation::Annotation::switch("switch").with_selected(state.on);
let annotation = if let Some(label) = &state.label {
annotation.with_label(label.as_str())
} else {
annotation
};
let annotated = crate::annotation::Annotate::new(paragraph, annotation)
.focused(ctx.focused)
.disabled(ctx.disabled);
ctx.frame.render_widget(annotated, ctx.area);
}
}
impl Toggleable for Switch {
fn is_visible(state: &Self::State) -> bool {
state.on
}
fn set_visible(state: &mut Self::State, visible: bool) {
state.on = visible;
}
}
#[cfg(test)]
mod tests;