use crate::event::{Key, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
use crate::layout::Rect;
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::widget::theme::{DISABLED_FG, SEPARATOR_COLOR};
use crate::widget::traits::{EventResult, Interactive, RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SwitchStyle {
#[default]
Default,
IOS,
Material,
Text,
Emoji,
Block,
}
pub struct Switch {
on: bool,
label: Option<String>,
label_left: bool,
style: SwitchStyle,
width: u16,
focused: bool,
disabled: bool,
on_color: Color,
off_color: Color,
track_color: Color,
on_text: Option<String>,
off_text: Option<String>,
props: WidgetProps,
}
impl Switch {
pub fn new() -> Self {
Self {
on: false,
label: None,
label_left: true,
style: SwitchStyle::Default,
width: 6,
focused: false,
disabled: false,
on_color: Color::GREEN,
off_color: DISABLED_FG,
track_color: SEPARATOR_COLOR,
on_text: None,
off_text: None,
props: WidgetProps::new(),
}
}
pub fn on(mut self, on: bool) -> Self {
self.on = on;
self
}
pub fn checked(self, checked: bool) -> Self {
self.on(checked)
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn label_right(mut self) -> Self {
self.label_left = false;
self
}
pub fn style(mut self, style: SwitchStyle) -> Self {
self.style = style;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width.max(4);
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_color(mut self, color: Color) -> Self {
self.on_color = color;
self
}
pub fn off_color(mut self, color: Color) -> Self {
self.off_color = color;
self
}
pub fn track_color(mut self, color: Color) -> Self {
self.track_color = color;
self
}
pub fn text(mut self, on: impl Into<String>, off: impl Into<String>) -> Self {
self.on_text = Some(on.into());
self.off_text = Some(off.into());
self
}
pub fn toggle(&mut self) {
if !self.disabled {
self.on = !self.on;
}
}
pub fn set(&mut self, on: bool) {
if !self.disabled {
self.on = on;
}
}
pub fn is_on(&self) -> bool {
self.on
}
pub fn is_checked(&self) -> bool {
self.is_on()
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
if self.disabled || !self.focused {
return false;
}
match key {
Key::Enter | Key::Char(' ') => {
self.toggle();
true
}
_ => false,
}
}
fn render_default(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let color = if self.on {
self.on_color
} else {
self.off_color
};
let track_len = self.width.saturating_sub(2);
let mut open = Cell::new('[');
open.fg = Some(if self.focused { Color::CYAN } else { color });
ctx.set(x, y, open);
for i in 0..track_len {
let is_knob = if self.on { i == track_len - 1 } else { i == 0 };
let ch = if is_knob { '●' } else { '━' };
let mut cell = Cell::new(ch);
cell.fg = Some(if is_knob { color } else { self.track_color });
ctx.set(x + 1 + i, y, cell);
}
let mut close = Cell::new(']');
close.fg = Some(if self.focused { Color::CYAN } else { color });
ctx.set(x + self.width - 1, y, close);
}
fn render_ios(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let color = if self.on {
self.on_color
} else {
self.off_color
};
let bg = if self.on {
self.on_color
} else {
self.track_color
};
let track_len = self.width.saturating_sub(2);
let mut open = Cell::new('(');
open.fg = Some(color);
ctx.set(x, y, open);
for i in 0..track_len {
let is_knob = if self.on { i == track_len - 1 } else { i == 0 };
let ch = if is_knob { '●' } else { ' ' };
let mut cell = Cell::new(ch);
cell.fg = Some(Color::WHITE);
cell.bg = Some(bg);
ctx.set(x + 1 + i, y, cell);
}
let mut close = Cell::new(')');
close.fg = Some(color);
ctx.set(x + self.width - 1, y, close);
}
fn render_material(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let color = if self.on {
self.on_color
} else {
self.off_color
};
let track_len = self.width;
for i in 0..track_len {
let is_left_knob = i == 0;
let is_right_knob = i == track_len - 1;
let (ch, fg) = if self.on {
if is_right_knob {
('●', color)
} else if is_left_knob {
('○', self.track_color)
} else {
('━', color)
}
} else if is_left_knob {
('●', color)
} else if is_right_knob {
('○', self.track_color)
} else {
('━', self.track_color)
};
let mut cell = Cell::new(ch);
cell.fg = Some(fg);
ctx.set(x + i, y, cell);
}
}
fn render_text(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let (text, color) = if self.on {
(self.on_text.as_deref().unwrap_or("ON"), self.on_color)
} else {
(self.off_text.as_deref().unwrap_or("OFF"), self.off_color)
};
let mut open = Cell::new('[');
open.fg = Some(if self.focused {
Color::CYAN
} else {
Color::WHITE
});
ctx.set(x, y, open);
for (i, ch) in text.chars().enumerate() {
let mut cell = Cell::new(ch);
cell.fg = Some(color);
if self.on {
cell.modifier |= Modifier::BOLD;
}
ctx.set(x + 1 + i as u16, y, cell);
}
let mut close = Cell::new(']');
close.fg = Some(if self.focused {
Color::CYAN
} else {
Color::WHITE
});
ctx.set(x + 1 + text.len() as u16, y, close);
}
fn render_emoji(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let ch = if self.on { '✅' } else { '❌' };
let mut cell = Cell::new(ch);
cell.fg = Some(if self.on {
self.on_color
} else {
self.off_color
});
ctx.set(x, y, cell);
ctx.set(x + 1, y, Cell::new(' '));
}
fn render_block(&self, ctx: &mut RenderContext, x: u16, y: u16) {
let track_len = self.width;
let half = track_len / 2;
for i in 0..track_len {
let is_filled = if self.on { i >= half } else { i < half };
let ch = if is_filled { '▓' } else { '░' };
let color = if self.on {
self.on_color
} else {
self.off_color
};
let mut cell = Cell::new(ch);
cell.fg = Some(color);
ctx.set(x + i, y, cell);
}
}
}
impl Default for Switch {
fn default() -> Self {
Self::new()
}
}
impl View for Switch {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 {
return;
}
let mut x: u16 = 0;
let y: u16 = 0;
if self.label_left {
if let Some(ref label) = self.label {
let label_color = if self.disabled {
DISABLED_FG
} else if let Some(css_fg) = ctx
.style
.map(|s| s.visual.color)
.filter(|c| *c != Color::default())
{
css_fg
} else {
Color::WHITE
};
ctx.draw_text_clipped(x, y, label, label_color, area.width.saturating_sub(x));
x += crate::utils::display_width(label) as u16 + 1;
}
}
if x < area.width {
match self.style {
SwitchStyle::Default => self.render_default(ctx, x, y),
SwitchStyle::IOS => self.render_ios(ctx, x, y),
SwitchStyle::Material => self.render_material(ctx, x, y),
SwitchStyle::Text => self.render_text(ctx, x, y),
SwitchStyle::Emoji => self.render_emoji(ctx, x, y),
SwitchStyle::Block => self.render_block(ctx, x, y),
}
x += match self.style {
SwitchStyle::Text => {
let text = if self.on {
self.on_text.as_deref().unwrap_or("ON")
} else {
self.off_text.as_deref().unwrap_or("OFF")
};
text.len() as u16 + 2
}
SwitchStyle::Emoji => 2,
_ => self.width,
};
}
if !self.label_left {
if let Some(ref label) = self.label {
x += 1;
let label_color = if self.disabled {
DISABLED_FG
} else if let Some(css_fg) = ctx
.style
.map(|s| s.visual.color)
.filter(|c| *c != Color::default())
{
css_fg
} else {
Color::WHITE
};
ctx.draw_text_clipped(x, y, label, label_color, area.width.saturating_sub(x));
}
}
if self.focused && !self.disabled {
let switch_x = if self.label_left {
self.label
.as_ref()
.map(|l| crate::utils::display_width(l) as u16 + 1)
.unwrap_or(0)
} else {
0u16
};
if switch_x > 0 {
let mut left = Cell::new('[');
left.fg = Some(Color::CYAN);
ctx.set(switch_x.saturating_sub(1), y, left);
} else {
let mut left = Cell::new('[');
left.fg = Some(Color::CYAN);
ctx.set(0, y, left);
}
let switch_width = match self.style {
SwitchStyle::Text => {
let text = if self.on {
self.on_text.as_deref().unwrap_or("ON")
} else {
self.off_text.as_deref().unwrap_or("OFF")
};
text.len() as u16 + 2
}
SwitchStyle::Emoji => 2,
_ => self.width,
};
let right_x = switch_x + switch_width;
if right_x < area.width {
let mut right = Cell::new(']');
right.fg = Some(Color::CYAN);
ctx.set(right_x, y, right);
}
}
}
crate::impl_view_meta!("Switch");
}
impl Interactive for Switch {
fn handle_key(&mut self, event: &KeyEvent) -> EventResult {
if self.disabled {
return EventResult::Ignored;
}
match event.key {
Key::Enter | Key::Char(' ') => {
self.toggle();
EventResult::ConsumedAndRender
}
_ => EventResult::Ignored,
}
}
fn handle_mouse(&mut self, event: &MouseEvent, _area: Rect) -> EventResult {
if self.disabled {
return EventResult::Ignored;
}
match event.kind {
MouseEventKind::Down(MouseButton::Left) => {
self.toggle();
EventResult::ConsumedAndRender
}
_ => EventResult::Ignored,
}
}
crate::impl_focus_handlers!(direct);
}
pub fn switch() -> Switch {
Switch::new()
}
pub fn toggle(label: impl Into<String>) -> Switch {
Switch::new().label(label)
}
impl_styled_view!(Switch);
impl_props_builders!(Switch);