envision 0.16.0

A ratatui framework for collaborative TUI development with headless testing support
Documentation
//! An on/off toggle switch component with keyboard activation.
//!
//! [`Switch`] provides a boolean on/off input that can be toggled via keyboard
//! (Enter or Space) when focused. Visually distinct from [`Checkbox`](super::Checkbox),
//! it displays a sliding toggle indicator rather than a checkmark.
//!
//! State is stored in [`SwitchState`], updated via [`SwitchMessage`], and
//! produces [`SwitchOutput`].
//!
//!
//! # Example
//!
//! ```rust
//! use envision::component::{Switch, SwitchMessage, SwitchOutput, SwitchState, Component};
//!
//! // Create an off switch
//! let mut state = SwitchState::new();
//! assert!(!state.is_on());
//!
//! // Toggle it on
//! let output = Switch::update(&mut state, SwitchMessage::Toggle);
//! assert_eq!(output, Some(SwitchOutput::On));
//! assert!(state.is_on());
//!
//! // Toggle it off
//! let output = Switch::update(&mut state, SwitchMessage::Toggle);
//! assert_eq!(output, Some(SwitchOutput::Off));
//! assert!(!state.is_on());
//! ```

use ratatui::widgets::Paragraph;

use super::{Component, EventContext, RenderContext, Toggleable};
use crate::input::{Event, Key};

/// Messages that can be sent to a Switch.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwitchMessage {
    /// Toggle the switch state.
    Toggle,
    /// Set the switch to a specific on/off state.
    SetOn(bool),
    /// Set the switch label.
    SetLabel(Option<String>),
}

/// Output messages from a Switch.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwitchOutput {
    /// The switch was toggled. Contains the new on/off value.
    Toggled(bool),
    /// The switch was turned on.
    On,
    /// The switch was turned off.
    Off,
}

/// State for a Switch component.
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(
    feature = "serialization",
    derive(serde::Serialize, serde::Deserialize)
)]
pub struct SwitchState {
    /// Whether the switch is on.
    on: bool,
    /// Optional label displayed next to the switch.
    label: Option<String>,
    /// Text shown when the switch is on.
    on_label: String,
    /// Text shown when the switch is off.
    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 {
    /// Creates a new switch in the off state with no label.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new();
    /// assert!(!state.is_on());
    /// assert!(state.label().is_none());
    /// ```
    pub fn new() -> Self {
        Self::default()
    }

    /// Sets the initial on/off state using builder pattern.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_on(true);
    /// assert!(state.is_on());
    /// ```
    pub fn with_on(mut self, on: bool) -> Self {
        self.on = on;
        self
    }

    /// Sets the label using builder pattern.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_label("Dark Mode");
    /// assert_eq!(state.label(), Some("Dark Mode"));
    /// ```
    pub fn with_label(mut self, label: impl Into<String>) -> Self {
        self.label = Some(label.into());
        self
    }

    /// Sets the text shown when the switch is on using builder pattern.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_on_label("YES");
    /// ```
    pub fn with_on_label(mut self, on_label: impl Into<String>) -> Self {
        self.on_label = on_label.into();
        self
    }

    /// Sets the text shown when the switch is off using builder pattern.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_off_label("NO");
    /// ```
    pub fn with_off_label(mut self, off_label: impl Into<String>) -> Self {
        self.off_label = off_label.into();
        self
    }

    /// Returns true if the switch is on.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_on(true);
    /// assert!(state.is_on());
    /// ```
    pub fn is_on(&self) -> bool {
        self.on
    }

    /// Sets the on/off state directly.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let mut state = SwitchState::new();
    /// state.set_on(true);
    /// assert!(state.is_on());
    /// ```
    pub fn set_on(&mut self, on: bool) {
        self.on = on;
    }

    /// Toggles the switch between on and off.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let mut state = SwitchState::new();
    /// assert!(!state.is_on());
    /// state.toggle();
    /// assert!(state.is_on());
    /// state.toggle();
    /// assert!(!state.is_on());
    /// ```
    pub fn toggle(&mut self) {
        self.on = !self.on;
    }

    /// Returns the optional label.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let state = SwitchState::new().with_label("Notifications");
    /// assert_eq!(state.label(), Some("Notifications"));
    ///
    /// let state = SwitchState::new();
    /// assert_eq!(state.label(), None);
    /// ```
    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    /// Sets the label.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::SwitchState;
    ///
    /// let mut state = SwitchState::new();
    /// state.set_label(Some("Wi-Fi".to_string()));
    /// assert_eq!(state.label(), Some("Wi-Fi"));
    /// state.set_label(None);
    /// assert_eq!(state.label(), None);
    /// ```
    pub fn set_label(&mut self, label: Option<String>) {
        self.label = label;
    }

    /// Updates the switch state with a message, returning any output.
    ///
    /// # Example
    ///
    /// ```rust
    /// use envision::component::{SwitchMessage, SwitchOutput, SwitchState};
    ///
    /// let mut state = SwitchState::new();
    /// let output = state.update(SwitchMessage::Toggle);
    /// assert_eq!(output, Some(SwitchOutput::On));
    /// assert!(state.is_on());
    /// ```
    pub fn update(&mut self, msg: SwitchMessage) -> Option<SwitchOutput> {
        Switch::update(self, msg)
    }
}

/// An on/off toggle switch component.
///
/// This component provides a boolean on/off input that can be toggled via
/// keyboard when focused. The switch emits [`SwitchOutput::Toggled`],
/// [`SwitchOutput::On`], or [`SwitchOutput::Off`] messages when toggled.
///
/// # Keyboard Activation
///
/// When focused, Enter or Space toggles the switch state.
///
/// # Visual States
///
/// - **Off**: `( ) OFF`
/// - **On**: `(*) ON` with success/green coloring
/// - **Focused**: Yellow/focused text
/// - **Disabled**: Dark gray text, doesn't respond to toggle
/// - **With label**: `Label  (*) ON`
///
/// # Example
///
/// ```rust
/// use envision::component::{Switch, SwitchMessage, SwitchOutput, SwitchState, Component};
///
/// let mut state = SwitchState::new();
///
/// // Toggle the switch on
/// let output = Switch::update(&mut state, SwitchMessage::Toggle);
/// assert_eq!(output, Some(SwitchOutput::On));
/// assert!(state.is_on());
/// ```
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;