use std::io;
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use ratatui_interact::{
components::{Input, InputState},
events::{
get_char, is_backspace, is_backtab, is_close_key, is_ctrl_a, is_ctrl_e, is_ctrl_k,
is_ctrl_u, is_ctrl_w, is_delete, is_end, is_home, is_left_click, is_tab,
},
state::FocusManager,
traits::ClickRegionRegistry,
};
struct App {
inputs: Vec<(&'static str, &'static str, InputState)>,
focus: FocusManager<usize>,
click_regions: ClickRegionRegistry<usize>,
should_quit: bool,
}
impl App {
fn new() -> Self {
let mut focus = FocusManager::new();
let inputs = vec![
("Name", "Enter your name...", InputState::empty()),
("Email", "user@example.com", InputState::empty()),
(
"Message",
"Type your message...",
InputState::new("Hello, World!"),
),
];
for i in 0..inputs.len() {
focus.register(i);
}
Self {
inputs,
focus,
click_regions: ClickRegionRegistry::new(),
should_quit: false,
}
}
fn current_input(&mut self) -> Option<&mut InputState> {
self.focus.current().map(|&idx| &mut self.inputs[idx].2)
}
fn handle_click(&mut self, col: u16, row: u16) {
if let Some(&idx) = self.click_regions.handle_click(col, row) {
self.focus.set(idx);
}
}
}
fn main() -> io::Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new();
loop {
terminal.draw(|f| ui(f, &mut app))?;
if let Event::Key(key) = event::read()? {
if is_close_key(&key) {
app.should_quit = true;
} else if is_tab(&key) {
app.focus.next();
} else if is_backtab(&key) {
app.focus.prev();
} else if is_backspace(&key) {
if let Some(input) = app.current_input() {
input.delete_char_backward();
}
} else if is_delete(&key) {
if let Some(input) = app.current_input() {
input.delete_char_forward();
}
} else if is_home(&key) || is_ctrl_a(&key) {
if let Some(input) = app.current_input() {
input.move_home();
}
} else if is_end(&key) || is_ctrl_e(&key) {
if let Some(input) = app.current_input() {
input.move_end();
}
} else if is_ctrl_u(&key) {
if let Some(input) = app.current_input() {
while input.cursor_pos > 0 {
input.delete_char_backward();
}
}
} else if is_ctrl_k(&key) {
if let Some(input) = app.current_input() {
while input.cursor_pos < input.len() {
input.delete_char_forward();
}
}
} else if is_ctrl_w(&key) {
if let Some(input) = app.current_input() {
input.delete_word_backward();
}
} else if key.code == KeyCode::Left {
if let Some(input) = app.current_input() {
input.move_left();
}
} else if key.code == KeyCode::Right {
if let Some(input) = app.current_input() {
input.move_right();
}
} else if let Some(c) = get_char(&key) {
if let Some(input) = app.current_input() {
input.insert_char(c);
}
}
} else if let Event::Mouse(mouse) = event::read().unwrap_or(Event::FocusGained) {
if is_left_click(&mouse) {
app.handle_click(mouse.column, mouse.row);
}
}
if app.should_quit {
break;
}
}
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
Ok(())
}
fn ui(f: &mut Frame, app: &mut App) {
app.click_regions.clear();
let area = f.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(2)
.constraints([
Constraint::Length(3), Constraint::Min(1), Constraint::Length(5), ])
.split(area);
let title = Paragraph::new("Input Demo")
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::BOTTOM));
f.render_widget(title, chunks[0]);
let input_area = chunks[1];
let input_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
app.inputs
.iter()
.map(|_| Constraint::Length(3))
.collect::<Vec<_>>(),
)
.split(input_area);
for (idx, (label, placeholder, state)) in app.inputs.iter_mut().enumerate() {
state.focused = app.focus.is_focused(&idx);
let input_widget = Input::new(state).label(label).placeholder(placeholder);
let input_area = Rect::new(
input_chunks[idx].x + 2,
input_chunks[idx].y,
input_chunks[idx].width.saturating_sub(4).min(50),
3,
);
let region = input_widget.render_stateful(f, input_area);
app.click_regions.register(region.area, idx);
}
let help_lines = vec![
Line::from(vec![
Span::styled("Tab", Style::default().fg(Color::Yellow)),
Span::raw(": Next field "),
Span::styled("Shift+Tab", Style::default().fg(Color::Yellow)),
Span::raw(": Prev field "),
Span::styled("Esc", Style::default().fg(Color::Yellow)),
Span::raw(": Quit"),
]),
Line::from(vec![
Span::styled("←/→", Style::default().fg(Color::Yellow)),
Span::raw(": Move cursor "),
Span::styled("Home/Ctrl+A", Style::default().fg(Color::Yellow)),
Span::raw(": Start "),
Span::styled("End/Ctrl+E", Style::default().fg(Color::Yellow)),
Span::raw(": End"),
]),
Line::from(vec![
Span::styled("Ctrl+U", Style::default().fg(Color::Yellow)),
Span::raw(": Clear to start "),
Span::styled("Ctrl+K", Style::default().fg(Color::Yellow)),
Span::raw(": Clear to end "),
Span::styled("Ctrl+W", Style::default().fg(Color::Yellow)),
Span::raw(": Delete word"),
]),
];
let help = Paragraph::new(help_lines).block(Block::default().borders(Borders::TOP));
f.render_widget(help, chunks[2]);
}