1use crate::events::{EventHandler, Key, KeyPressedEvent};
2use itertools::Itertools;
3use ratatui::{
4 buffer::Buffer,
5 layout::{Constraint, Layout, Rect},
6 style::{Color, Stylize},
7 text::Text,
8 widgets::Widget,
9};
10
11#[derive(Debug, Clone)]
27pub struct ToggleSwitch<'text> {
28 text: Text<'text>,
29 theme: Theme,
30 state: State,
31 focus: Focus,
32}
33
34#[derive(Default, PartialEq, Eq, Clone, Debug, Copy)]
35pub enum State {
36 On,
37 #[default]
38 Off,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Focus {
43 Focused,
44 Unfocused,
45}
46
47#[derive(Copy, Clone, Debug)]
48pub struct Theme {
49 focused_text: Color,
50
51 focused_on_fg: Color,
52 focused_on_bg_main: Color,
53 focused_on_bg_highlight: Color,
54 focused_on_bg_shadow: Color,
55
56 focused_off_fg: Color,
57 focused_off_bg_main: Color,
58 focused_off_bg_highlight: Color,
59 focused_off_bg_shadow: Color,
60
61 unfocused_text: Color,
62
63 unfocused_on_fg: Color,
64 unfocused_on_bg_main: Color,
65 unfocused_on_bg_highlight: Color,
66 unfocused_on_bg_shadow: Color,
67
68 unfocused_off_fg: Color,
69 unfocused_off_bg_main: Color,
70 unfocused_off_bg_highlight: Color,
71 unfocused_off_bg_shadow: Color,
72}
73
74impl Default for Theme {
75 fn default() -> Self {
76 themes::NORMAL
77 }
78}
79
80impl<'text> ToggleSwitch<'text> {
81 pub fn new<T: Into<Text<'text>>>(text: T, default_state: State) -> Self {
82 Self {
83 text: text.into(),
84 theme: Theme::default(),
85 state: default_state,
86 focus: Focus::Unfocused,
87 }
88 }
89
90 pub fn with_theme(mut self, theme: Theme) -> Self {
91 self.theme = theme;
92 self
93 }
94}
95
96impl EventHandler for ToggleSwitch<'_> {
97 fn handle_key(&mut self, key_event: KeyPressedEvent) {
98 match key_event.key {
99 Key::Char(' ') | Key::Enter => self.toggle_state(),
100 Key::Char('h') | Key::Left => self.toggle_off(),
101 Key::Char('l') | Key::Right => self.toggle_on(),
102 _ => {}
103 }
104 }
105}
106
107impl ToggleSwitch<'_> {
108 pub fn toggle_state(&mut self) {
109 self.focus();
110 match self.state {
111 State::On => self.toggle_off(),
112 State::Off => self.toggle_on(),
113 }
114 }
115
116 pub fn toggle_on(&mut self) {
117 self.state = State::On;
118 }
119
120 pub fn toggle_off(&mut self) {
121 self.state = State::Off;
122 }
123
124 pub fn focus(&mut self) {
125 self.focus = Focus::Focused;
126 }
127
128 pub fn blur(&mut self) {
129 self.focus = Focus::Unfocused;
130 }
131}
132
133impl Widget for &ToggleSwitch<'_> {
134 fn render(self, area: Rect, buf: &mut Buffer) {
135 let theme = self.theme;
136
137 let (tick_fg, tick_bg, cross_fg, cross_bg) = match (self.focus, self.state) {
139 (Focus::Focused, State::On) => (
140 theme.focused_on_fg,
141 theme.focused_on_bg_main,
142 theme.focused_off_fg,
143 theme.focused_off_bg_main,
144 ),
145 (Focus::Focused, State::Off) => (
146 theme.focused_off_fg,
147 theme.focused_off_bg_main,
148 theme.focused_on_fg,
149 theme.focused_on_bg_main,
150 ),
151 (Focus::Unfocused, State::On) => (
152 theme.unfocused_on_fg,
153 theme.unfocused_on_bg_main,
154 theme.unfocused_off_fg,
155 theme.unfocused_off_bg_main,
156 ),
157 (Focus::Unfocused, State::Off) => (
158 theme.unfocused_off_fg,
159 theme.unfocused_off_bg_main,
160 theme.unfocused_on_fg,
161 theme.unfocused_on_bg_main,
162 ),
163 };
164
165 let (tick_highlight, tick_shadow, cross_highlight, cross_shadow) =
166 match (self.state, self.focus) {
167 (State::On, Focus::Focused) => (
168 theme.focused_on_bg_highlight,
169 theme.focused_on_bg_shadow,
170 theme.focused_off_bg_highlight,
171 theme.focused_off_bg_shadow,
172 ),
173 (State::On, Focus::Unfocused) => (
174 theme.unfocused_on_bg_highlight,
175 theme.unfocused_on_bg_shadow,
176 theme.unfocused_off_bg_highlight,
177 theme.unfocused_off_bg_shadow,
178 ),
179 (State::Off, Focus::Focused) => (
180 theme.focused_off_bg_highlight,
181 theme.focused_off_bg_shadow,
182 theme.focused_on_bg_highlight,
183 theme.focused_on_bg_shadow,
184 ),
185 (State::Off, Focus::Unfocused) => (
186 theme.unfocused_off_bg_highlight,
187 theme.unfocused_off_bg_shadow,
188 theme.unfocused_on_bg_highlight,
189 theme.unfocused_on_bg_shadow,
190 ),
191 };
192
193 let [switch, label] = Layout::horizontal([Constraint::Max(10), Constraint::Fill(1)])
194 .spacing(2)
195 .areas(area);
196 let [cross, tick] = Layout::horizontal([Constraint::Fill(1); 2]).areas(switch);
197
198 buf.set_style(cross, (cross_fg, cross_bg));
199 buf.set_style(tick, (tick_fg, tick_bg));
200
201 let rows = switch.rows().collect_vec();
202 let last_index = rows.len().saturating_sub(1);
203 let (first, middle, last) = match rows.len() {
204 0 | 1 => (None, &rows[..], None),
205 2 => (None, &rows[..last_index], Some(rows[last_index])),
206 _ => (Some(rows[0]), &rows[1..last_index], Some(rows[last_index])),
207 };
208
209 if let Some(first) = first {
211 let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(first);
212 "▔"
213 .repeat(cross.width as usize)
214 .fg(cross_highlight)
215 .bg(cross_bg)
216 .render(left, buf);
217 "▔"
218 .repeat(tick.width as usize)
219 .fg(tick_highlight)
220 .bg(tick_bg)
221 .render(right, buf);
222 }
223 if let Some(last) = last {
225 let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(last);
226 "▁"
227 .repeat(cross.width as usize)
228 .fg(cross_shadow)
229 .bg(cross_bg)
230 .render(left, buf);
231 "▁"
232 .repeat(tick.width as usize)
233 .fg(tick_shadow)
234 .bg(tick_bg)
235 .render(right, buf);
236 }
237 let text_style = match self.focus {
238 Focus::Focused => theme.focused_text,
239 Focus::Unfocused => theme.unfocused_text,
240 };
241 buf.set_style(label, text_style);
242 let middle_row_index = label.height as usize / 2;
243 let middle_row = label.rows().collect_vec()[middle_row_index];
244 self.text.clone().left_aligned().render(middle_row, buf);
245
246 let middle_row_index = middle.len() / 2;
247 let middle_row = middle[middle_row_index];
248 let [cross, tick] = Layout::horizontal([Constraint::Fill(1); 2]).areas(middle_row);
249 Text::from("✗").centered().render(cross, buf);
250 Text::from("✓").centered().render(tick, buf);
251 }
252}
253
254pub mod themes {
255 use super::Theme;
256 use ratatui::style::palette::tailwind;
257
258 pub const NORMAL: Theme = Theme {
259 focused_text: tailwind::SLATE.c300,
260
261 focused_on_fg: tailwind::BLUE.c100,
262 focused_on_bg_main: tailwind::BLUE.c500,
263 focused_on_bg_highlight: tailwind::BLUE.c300,
264 focused_on_bg_shadow: tailwind::BLUE.c700,
265
266 focused_off_fg: tailwind::SLATE.c300,
267 focused_off_bg_main: tailwind::SLATE.c700,
268 focused_off_bg_highlight: tailwind::SLATE.c500,
269 focused_off_bg_shadow: tailwind::SLATE.c900,
270
271 unfocused_text: tailwind::SLATE.c400,
272
273 unfocused_on_fg: tailwind::BLUE.c200,
274 unfocused_on_bg_main: tailwind::BLUE.c600,
275 unfocused_on_bg_highlight: tailwind::BLUE.c400,
276 unfocused_on_bg_shadow: tailwind::BLUE.c800,
277
278 unfocused_off_fg: tailwind::SLATE.c400,
279 unfocused_off_bg_main: tailwind::SLATE.c800,
280 unfocused_off_bg_highlight: tailwind::SLATE.c600,
281 unfocused_off_bg_shadow: tailwind::SLATE.c950,
282 };
283}