use crate::core::buffer::{Buffer, Cell};
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,
Checkbox,
Radio,
Text,
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 == 0 || area.height == 0 {
return;
}
clear_row_preserving_bg(buffer, area, self.label_color);
let indicator_color = if self.active {
self.active_color
} else {
self.inactive_color
};
let indicator = self.indicator(area.width as usize);
let indicator_len = indicator.chars().count();
write_preserving_bg(
buffer,
area.x as usize,
area.y as usize,
&indicator,
indicator_color,
area.width as usize,
);
if self.label.is_empty() || indicator_len >= area.width as usize {
return;
}
let label_x = area.x as usize + indicator_len + 1;
if label_x >= area.right() as usize {
return;
}
let label_width = area.right() as usize - label_x;
let label: String = self.label.chars().take(label_width).collect();
write_preserving_bg(
buffer,
label_x,
area.y as usize,
&label,
self.label_color,
label_width,
);
}
}
impl Toggle {
fn indicator(&self, available_width: usize) -> String {
match self.style {
ToggleStyle::Slider => self.slider_indicator(available_width),
ToggleStyle::Checkbox => {
let ch = if self.active { '●' } else { '○' };
format!("[{}]", ch)
}
ToggleStyle::Radio => {
if self.active {
"●".to_string()
} else {
"○".to_string()
}
}
ToggleStyle::Text => {
if self.active {
"[ON]".to_string()
} else {
"[OFF]".to_string()
}
}
ToggleStyle::Block => {
let reserved_for_label = if self.label.is_empty() {
0
} else {
self.label.chars().count().saturating_add(1)
};
let block_width = available_width
.saturating_sub(reserved_for_label)
.clamp(1, 10);
let ch = if self.active { '█' } else { '░' };
std::iter::repeat(ch).take(block_width).collect()
}
}
}
fn slider_indicator(&self, available_width: usize) -> String {
let label_reserve = if self.label.is_empty() {
0
} else {
self.label.chars().count().saturating_add(1)
};
let reserved_space = available_width.saturating_sub(label_reserve);
let indicator_space = if reserved_space >= 3 {
reserved_space
} else {
available_width
};
let track_width = indicator_space.saturating_sub(2).clamp(1, 12);
let knob = if self.active {
track_width.saturating_sub(1)
} else {
0
};
let mut slider = String::with_capacity(track_width + 2);
slider.push('[');
for i in 0..track_width {
if i == knob {
slider.push('●');
} else if self.active && i < knob {
slider.push('━');
} else {
slider.push('─');
}
}
slider.push(']');
slider
}
}
fn clear_row_preserving_bg(buffer: &mut Buffer, area: Rect, fg: Color) {
let y = area.y as usize;
for x in area.x as usize..area.right() as usize {
let bg = buffer.get(x, y).and_then(|cell| cell.bg);
buffer.set(x, y, Cell::new(' ', fg, bg));
}
}
fn write_preserving_bg(
buffer: &mut Buffer,
x: usize,
y: usize,
text: &str,
fg: Color,
max_width: usize,
) {
for (offset, ch) in text.chars().enumerate() {
if offset >= max_width {
break;
}
let cell_x = x + offset;
if cell_x >= buffer.width || y >= buffer.height {
break;
}
let bg = buffer.get(cell_x, y).and_then(|cell| cell.bg);
buffer.set(cell_x, y, Cell::new(ch, fg, bg));
}
}
#[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);
}
#[test]
fn test_slider_renders_knob_in_both_states() {
let mut buf = Buffer::new(20, 2);
Toggle::new("", false)
.with_style(ToggleStyle::Slider)
.render(&mut buf, Rect::new(0, 0, 20, 1));
Toggle::new("", true)
.with_style(ToggleStyle::Slider)
.render(&mut buf, Rect::new(0, 1, 20, 1));
assert_eq!(buf.get(1, 0).unwrap().ch, '●');
assert_eq!(buf.get(12, 1).unwrap().ch, '●');
}
#[test]
fn test_toggle_clears_stale_text() {
let mut buf = Buffer::new(20, 1);
let mut toggle = Toggle::new("X", false).with_style(ToggleStyle::Text);
toggle.render(&mut buf, Rect::new(0, 0, 20, 1));
toggle.set_active(true);
toggle.render(&mut buf, Rect::new(0, 0, 20, 1));
assert_eq!(buf.get(6, 0).unwrap().ch, ' ');
}
#[test]
fn test_toggle_preserves_existing_background() {
let bg = Color::rgb(1, 2, 3);
let mut buf = Buffer::with_background(20, 1, Some(bg));
Toggle::new("Label", true)
.with_style(ToggleStyle::Checkbox)
.render(&mut buf, Rect::new(0, 0, 20, 1));
assert_eq!(buf.get(0, 0).unwrap().bg, Some(bg));
assert_eq!(buf.get(4, 0).unwrap().bg, Some(bg));
}
}