use std::io::{stdout, Write};
use std::process::Command;
use crossterm::{
cursor::{MoveTo, MoveToNextLine},
event::{self, Event as CEvent, KeyCode, KeyEventKind},
execute, queue,
style::Print,
terminal::{disable_raw_mode, enable_raw_mode},
};
use crate::widget::{
button::Button, checkbox::Checkbox, input::Input, label::Label, slider::Slider,
};
#[derive(Debug, Clone, Copy)]
pub enum Event {
Key(char),
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowDown,
}
#[derive(Debug)]
pub enum WidgetValue {
Bool(bool),
Int(i32),
Text(String),
}
pub enum WidgetType {
Button(Button),
Checkbox(Checkbox),
Slider(Slider),
Label(Label),
View(View),
Input(Input),
}
impl From<Button> for WidgetType {
fn from(value: Button) -> Self {
Self::Button(value)
}
}
impl From<Checkbox> for WidgetType {
fn from(value: Checkbox) -> Self {
Self::Checkbox(value)
}
}
impl From<Slider> for WidgetType {
fn from(value: Slider) -> Self {
Self::Slider(value)
}
}
impl From<Label> for WidgetType {
fn from(value: Label) -> Self {
Self::Label(value)
}
}
impl From<Input> for WidgetType {
fn from(value: Input) -> Self {
Self::Input(value)
}
}
impl From<View> for WidgetType {
fn from(value: View) -> Self {
Self::View(value)
}
}
impl WidgetType {
pub fn render(
&self,
focused: bool,
indent: usize,
_focus_index: usize,
_focusable_index: &mut usize,
current_y: &mut u16,
) -> String {
let prefix = if focused { ">" } else { " " };
let pad = " ".repeat(indent);
match self {
WidgetType::Button(b) => {
*current_y += 1;
format!("{pad}{}{label}", prefix, label = b.render())
}
WidgetType::Checkbox(c) => {
*current_y += 1;
format!("{pad}{}{label}", prefix, label = c.render())
}
WidgetType::Slider(s) => {
*current_y += 1;
format!("{pad}{}{label}", prefix, label = s.render())
}
WidgetType::Label(l) => {
*current_y += 1;
format!("{pad} {}", l.render())
}
WidgetType::View(v) => v.render(indent + 2, _focus_index, _focusable_index, current_y),
WidgetType::Input(i) => format!("{pad}{}{label}", prefix, label = i.render(focused)),
}
}
pub fn handle_event(&mut self, event: &Event, focus_index: usize) {
match self {
WidgetType::Checkbox(c) => {
if let Event::Key(' ') = event {
c.toggle();
}
}
WidgetType::Slider(s) => match event {
Event::Key('+') => {
if s.value < s.max {
s.value += 1;
}
}
Event::Key('-') => {
if s.value > s.min {
s.value -= 1;
}
}
_ => {}
},
WidgetType::Input(i) => {
match event {
Event::Key(c) => match c {
'\x08' => i.handle_backspace(), '\x1b' => {} '\n' => {} _ => i.handle_char(*c), },
Event::ArrowLeft => i.move_cursor_left(),
Event::ArrowRight => i.move_cursor_right(),
_ => {}
}
}
WidgetType::View(v) => {
v.handle_event(event, focus_index);
}
_ => {}
}
}
pub fn is_focusable(&self) -> bool {
match self {
WidgetType::View(v) => count_focusables(v) > 0,
_ => matches!(
self,
WidgetType::Button(_)
| WidgetType::Checkbox(_)
| WidgetType::Slider(_)
| WidgetType::Input(_)
),
}
}
pub fn value(&self) -> Option<(String, WidgetValue)> {
match self {
WidgetType::Checkbox(c) => Some((c.label.clone(), WidgetValue::Bool(c.checked))),
WidgetType::Slider(s) => {
Some((format!("Slider({})", s.label), WidgetValue::Int(s.value)))
}
WidgetType::Input(i) => Some((
i.label.clone(),
WidgetValue::Text(i.get_value().to_string()),
)),
WidgetType::View(_) => None,
_ => None,
}
}
}
pub struct View {
pub label: String,
pub widgets: Vec<WidgetType>,
}
impl View {
pub fn new(label: &str) -> Self {
View {
label: label.to_string(),
widgets: Vec::new(),
}
}
pub fn add(&mut self, widget: impl Into<WidgetType>) {
self.widgets.push(widget.into());
}
pub fn flatten_focusable(&mut self) -> Vec<&mut WidgetType> {
let mut result = Vec::new();
for widget in &mut self.widgets {
match widget {
WidgetType::View(v) => result.extend(v.flatten_focusable()),
_ if widget.is_focusable() => result.push(widget),
_ => {}
}
}
result
}
pub fn handle_event(&mut self, event: &Event, focus_index: usize) {
let mut focusables = self.flatten_focusable();
if focusables.is_empty() {
return;
}
if let Some(widget) = focusables.get_mut(focus_index) {
widget.handle_event(event, focus_index);
}
}
pub fn render(
&self,
indent: usize,
global_focus_index: usize,
focusable_index: &mut usize,
current_y: &mut u16,
) -> String {
let mut output = vec![format!("{}=== {} ===", " ".repeat(indent), self.label)];
*current_y += 1;
for widget in &self.widgets {
match widget {
WidgetType::View(v) => {
output.push(v.render(
indent + 2,
global_focus_index,
focusable_index,
current_y,
));
}
_ => {
let is_focusable = widget.is_focusable();
let focused = is_focusable && *focusable_index == global_focus_index;
output.push(widget.render(
focused,
indent,
global_focus_index,
&mut 0, current_y,
));
*current_y += match widget {
WidgetType::Input(_) => 3,
_ => 1,
};
if is_focusable {
*focusable_index += 1;
}
}
}
}
output.join("\n")
}
pub fn get_values(&self) -> Vec<(String, WidgetValue)> {
let mut values = Vec::new();
for widget in &self.widgets {
match widget {
WidgetType::View(v) => values.extend(v.get_values()),
_ => {
if let Some(val) = widget.value() {
values.push(val);
}
}
}
}
values
}
pub fn run(&mut self) -> std::io::Result<Vec<String>> {
enable_raw_mode()?;
let mut stdout = stdout();
let mut needs_redraw = true;
let mut global_focus_index = 0;
loop {
let mut flat_index = 0;
let mut current_y = 0;
if needs_redraw {
clear_screen();
execute!(stdout, MoveTo(0, 0))?;
for line in self
.render(0, global_focus_index, &mut flat_index, &mut current_y)
.split('\n')
{
queue!(stdout, Print(line), MoveToNextLine(1))?;
}
stdout.flush()?;
needs_redraw = false;
}
if let CEvent::Key(key_event) = event::read()? {
if key_event.kind != KeyEventKind::Press {
continue;
}
match key_event.code {
KeyCode::Esc => {
clear_screen();
break;
}
KeyCode::Tab => {
let total = count_focusables(self);
global_focus_index = (global_focus_index + 1) % total;
needs_redraw = true;
}
KeyCode::BackTab => {
let total = count_focusables(self);
global_focus_index = (global_focus_index + total - 1) % total;
needs_redraw = true;
}
KeyCode::Char(c) => {
self.handle_event(&crate::Event::Key(c), global_focus_index);
needs_redraw = true;
}
KeyCode::Backspace => {
self.handle_event(&crate::Event::Key('\x08'), global_focus_index);
needs_redraw = true;
}
KeyCode::Left => {
self.handle_event(&crate::Event::ArrowLeft, global_focus_index);
needs_redraw = true;
}
KeyCode::Right => {
self.handle_event(&crate::Event::ArrowRight, global_focus_index);
needs_redraw = true;
}
_ => {}
}
}
}
disable_raw_mode()?;
let selected: Vec<String> = self
.get_values()
.into_iter()
.filter_map(|(label, value)| match value {
crate::WidgetValue::Bool(true) => Some(label),
crate::WidgetValue::Text(text) => Some(format!("{}: {}", label, text)),
_ => None,
})
.collect();
Ok(selected)
}
}
fn count_focusables(view: &View) -> usize {
view.widgets
.iter()
.map(|w| match w {
WidgetType::View(v) => count_focusables(v),
_ if w.is_focusable() => 1,
_ => 0,
})
.sum()
}
fn clear_screen() {
if cfg!(target_os = "windows") {
let _ = Command::new("cmd").args(&["/C", "cls"]).status();
} else {
let _ = Command::new("clear").status();
}
}