scrin 0.1.1

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::widgets::Widget;

#[derive(Debug, Clone)]
pub struct Toggle {
    pub label: String,
    pub active: bool,
    pub active_color: Color,
    pub inactive_color: Color,
    pub label_color: Color,
    pub style: ToggleStyle,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToggleStyle {
    /// ━━━━━━━●━━━━━  (slider)
    Slider,
    /// [●] / [○] (checkbox)
    Checkbox,
    /// ● / ○ (radio)
    Radio,
    /// ON / OFF
    Text,
    /// ████ / ░░░░ (block)
    Block,
}

impl Toggle {
    pub fn new(label: &str, active: bool) -> Self {
        Self {
            label: label.to_string(),
            active,
            active_color: Color::rgb(63, 185, 80),
            inactive_color: Color::rgb(110, 118, 129),
            label_color: Color::rgb(201, 209, 217),
            style: ToggleStyle::Slider,
        }
    }

    pub fn with_style(mut self, style: ToggleStyle) -> Self {
        self.style = style;
        self
    }

    pub fn with_active_color(mut self, c: Color) -> Self {
        self.active_color = c;
        self
    }

    pub fn with_inactive_color(mut self, c: Color) -> Self {
        self.inactive_color = c;
        self
    }

    pub fn with_label_color(mut self, c: Color) -> Self {
        self.label_color = c;
        self
    }

    pub fn toggle(&mut self) {
        self.active = !self.active;
    }

    pub fn set_active(&mut self, v: bool) {
        self.active = v;
    }
}

impl Widget for Toggle {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.width < 4 || area.height < 1 {
            return;
        }
        let (indicator, indicator_color) = match self.style {
            ToggleStyle::Slider => {
                let indicator_width = (area.width.saturating_sub(4)).min(12);
                let filled = if self.active { indicator_width } else { 0 };
                let mut slider = String::from("[");
                for i in 0..indicator_width {
                    if i < filled {
                        slider.push('');
                    } else {
                        slider.push('');
                    }
                }
                slider.push(']');
                if self.active {
                    let last = slider.len() - 1;
                    slider.replace_range(last.., "●]");
                }
                (
                    format!("{} {}", slider, self.label),
                    if self.active {
                        self.active_color
                    } else {
                        self.inactive_color
                    },
                )
            }
            ToggleStyle::Checkbox => {
                let ch = if self.active { "" } else { "" };
                (
                    format!("[{}] {}", ch, self.label),
                    if self.active {
                        self.active_color
                    } else {
                        self.inactive_color
                    },
                )
            }
            ToggleStyle::Radio => {
                let ch = if self.active { "" } else { "" };
                (
                    format!("{} {}", ch, self.label),
                    if self.active {
                        self.active_color
                    } else {
                        self.inactive_color
                    },
                )
            }
            ToggleStyle::Text => {
                let txt = if self.active { "ON" } else { "OFF" };
                (
                    format!("[{}] {}", txt, self.label),
                    if self.active {
                        self.active_color
                    } else {
                        self.inactive_color
                    },
                )
            }
            ToggleStyle::Block => {
                let block_width = (area.width.saturating_sub(4)).min(10) as usize;
                let filled = if self.active { block_width } else { 0 };
                let mut blocks = String::new();
                for i in 0..block_width {
                    if i < filled {
                        blocks.push('');
                    } else {
                        blocks.push('');
                    }
                }
                (
                    format!("{} {}", blocks, self.label),
                    if self.active {
                        self.active_color
                    } else {
                        self.inactive_color
                    },
                )
            }
        };

        let display: String = indicator.chars().take(area.width as usize).collect();
        buffer.set_str(
            area.x as usize,
            area.y as usize,
            &display,
            indicator_color,
            None,
        );
        if !self.label.is_empty() {
            if let Some(byte_idx) = display.find(&self.label) {
                let label_col = display[..byte_idx].chars().count();
                if label_col < area.width as usize {
                    let label: String = self
                        .label
                        .chars()
                        .take(area.width as usize - label_col)
                        .collect();
                    buffer.set_str(
                        area.x as usize + label_col,
                        area.y as usize,
                        &label,
                        self.label_color,
                        None,
                    );
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_toggle_creation() {
        let t = Toggle::new("Light", false);
        assert!(!t.active);
        assert_eq!(t.label, "Light");
    }

    #[test]
    fn test_toggle_switch() {
        let mut t = Toggle::new("Dark", false);
        t.toggle();
        assert!(t.active);
        t.toggle();
        assert!(!t.active);
    }

    #[test]
    fn test_toggle_render_no_panic() {
        let mut buf = Buffer::new(40, 5);
        let toggle = Toggle::new("Test", true).with_style(ToggleStyle::Checkbox);
        toggle.render(&mut buf, Rect::new(0, 0, 40, 1));
        assert_eq!(buf.get(0, 0).unwrap().ch, '[');
    }

    #[test]
    fn test_toggle_all_styles_render() {
        let mut buf = Buffer::new(40, 5);
        let styles = [
            ToggleStyle::Slider,
            ToggleStyle::Checkbox,
            ToggleStyle::Radio,
            ToggleStyle::Text,
            ToggleStyle::Block,
        ];
        for (i, style) in styles.iter().enumerate() {
            let toggle = Toggle::new("Test", true).with_style(*style);
            toggle.render(&mut buf, Rect::new(0, i as u16, 40, 1));
        }
    }

    #[test]
    fn test_toggle_render_too_small() {
        let mut buf = Buffer::new(2, 1);
        let toggle = Toggle::new("Test", true);
        toggle.render(&mut buf, Rect::new(0, 0, 2, 1));
    }

    #[test]
    fn test_toggle_uses_label_color() {
        let mut buf = Buffer::new(20, 1);
        let label_color = Color::rgb(255, 0, 128);
        let toggle = Toggle::new("Label", true)
            .with_style(ToggleStyle::Checkbox)
            .with_label_color(label_color);
        toggle.render(&mut buf, Rect::new(0, 0, 20, 1));
        assert_eq!(buf.get(4, 0).unwrap().fg, label_color);
    }
}