diff --git a/async/ratatui-counter/src/app.rs b/async/ratatui-counter/src/app.rs index efcf1d6..da0cd81 100644 --- a/async/ratatui-counter/src/app.rs +++ b/async/ratatui-counter/src/app.rs @@ -44,8 +44,7 @@ impl App { pub async fn run(&mut self) -> Result<()> { let (action_tx, mut action_rx) = mpsc::unbounded_channel(); - let mut tui = tui::Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate); - // tui.mouse(true); + let mut tui = tui::Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate).mouse(true); tui.enter()?; for component in self.components.iter_mut() { @@ -137,8 +136,7 @@ impl App { if self.should_suspend { tui.suspend()?; action_tx.send(Action::Resume)?; - tui = tui::Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate); - // tui.mouse(true); + tui = tui::Tui::new()?.tick_rate(self.tick_rate).frame_rate(self.frame_rate).mouse(true); tui.enter()?; } else if self.should_quit { tui.stop()?; diff --git a/async/ratatui-counter/src/components/home.rs b/async/ratatui-counter/src/components/home.rs index e744813..607fb28 100644 --- a/async/ratatui-counter/src/components/home.rs +++ b/async/ratatui-counter/src/components/home.rs @@ -1,15 +1,18 @@ use std::{collections::HashMap, time::Duration}; use color_eyre::eyre::Result; -use crossterm::event::{KeyCode, KeyEvent}; +use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use log::error; -use ratatui::{prelude::*, widgets::*}; +use ratatui::{ + prelude::*, + widgets::{block::Title, *}, +}; use tokio::sync::mpsc::UnboundedSender; use tracing::trace; use tui_input::{backend::crossterm::EventHandler, Input}; use super::{Component, Frame}; -use crate::{action::Action, config::key_event_to_string}; +use crate::{action::Action, config::key_event_to_string, tui::Event}; #[derive(Default, Copy, Clone, PartialEq, Eq)] pub enum Mode { @@ -20,6 +23,14 @@ pub enum Mode { Help, } +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] +pub enum ButtonState { + #[default] + Normal, + Hover, + Clicked, +} + #[derive(Default)] pub struct Home { pub show_help: bool, @@ -32,6 +43,12 @@ pub struct Home { pub keymap: HashMap, pub text: Vec, pub last_events: Vec, + pub main_rect: Rect, + pub input_rect: Rect, + pub increment_rect: Rect, + pub decrement_rect: Rect, + pub increment_btn_state: ButtonState, + pub decrement_btn_state: ButtonState, } impl Home { @@ -86,6 +103,116 @@ impl Home { pub fn decrement(&mut self, i: usize) { self.counter = self.counter.saturating_sub(i); } + + pub fn main_widget(&mut self) -> Paragraph<'_> { + let mut text: Vec = self.text.clone().iter().map(|l| Line::from(l.clone())).collect(); + text.insert(0, "".into()); + text.insert(0, "Type into input and hit enter to display here".dim().into()); + text.insert(0, "".into()); + text.insert(0, format!("Render Ticker: {}", self.render_ticker).into()); + text.insert(0, format!("App Ticker: {}", self.app_ticker).into()); + text.insert(0, format!("Counter: {}", self.counter).into()); + text.insert(0, "".into()); + text.insert( + 0, + Line::from(vec![ + "Press ".into(), + Span::styled("j", Style::default().fg(Color::Red)), + " or ".into(), + Span::styled("k", Style::default().fg(Color::Red)), + " to ".into(), + Span::styled("increment", Style::default().fg(Color::Yellow)), + " or ".into(), + Span::styled("decrement", Style::default().fg(Color::Yellow)), + ".".into(), + ]), + ); + text.insert(0, "".into()); + Paragraph::new(text) + .block( + Block::default() + .title("ratatui async template") + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_style(match self.mode { + Mode::Processing => Style::default().fg(Color::Yellow), + _ => Style::default(), + }) + .border_type(BorderType::Rounded), + ) + .style(Style::default().fg(Color::Cyan)) + .alignment(Alignment::Center) + } + + fn input_widget(&mut self) -> Paragraph<'_> { + let width = self.main_rect.width.max(3) - 3; // keep 2 for borders and 1 for cursor + let scroll = self.input.visual_scroll(width as usize); + Paragraph::new(self.input.value()) + .style(match self.mode { + Mode::Insert => Style::default().fg(Color::Yellow), + _ => Style::default(), + }) + .scroll((0, scroll as u16)) + .block(Block::default().borders(Borders::ALL).title(Line::from(vec![ + Span::raw("Enter Input Mode "), + Span::styled("(Press ", Style::default().fg(Color::DarkGray)), + Span::styled("/", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), + Span::styled(" to start, ", Style::default().fg(Color::DarkGray)), + Span::styled("ESC", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), + Span::styled(" to finish)", Style::default().fg(Color::DarkGray)), + ]))) + } + + fn help_widget(&mut self) -> (Block<'_>, Table<'_>) { + let block = Block::default() + .title(Line::from(vec![Span::styled("Key Bindings", Style::default().add_modifier(Modifier::BOLD))])) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)); + let rows = vec![ + Row::new(vec!["j", "Increment"]), + Row::new(vec!["k", "Decrement"]), + Row::new(vec!["/", "Enter Input"]), + Row::new(vec!["ESC", "Exit Input"]), + Row::new(vec!["Enter", "Submit Input"]), + Row::new(vec!["q", "Quit"]), + Row::new(vec!["?", "Open Help"]), + ]; + let table = Table::new(rows, &[Constraint::Percentage(10), Constraint::Percentage(90)]) + .header(Row::new(vec!["Key", "Action"]).bottom_margin(1).style(Style::default().add_modifier(Modifier::BOLD))) + .column_spacing(1); + (block, table) + } + + fn title_widget(&mut self) -> Block<'_> { + Block::default() + .title( + Title::from(format!("{:?}", &self.last_events.iter().map(|k| key_event_to_string(k)).collect::>())) + .alignment(Alignment::Right), + ) + .title_style(Style::default().add_modifier(Modifier::BOLD)) + } + + pub fn increment_widget(&mut self) -> Paragraph<'_> { + let color = if self.increment_btn_state == ButtonState::Hover { + Color::Red + } else if self.increment_btn_state == ButtonState::Clicked { + Color::Blue + } else { + Color::Yellow + }; + Paragraph::new("Increment").alignment(Alignment::Center).style(Style::new().bg(color)) + } + + pub fn decrement_widget(&mut self) -> Paragraph<'_> { + let color = if self.decrement_btn_state == ButtonState::Hover { + Color::Red + } else if self.decrement_btn_state == ButtonState::Clicked { + Color::Blue + } else { + Color::Yellow + }; + Paragraph::new("Decrement").alignment(Alignment::Center).style(Style::new().bg(color)) + } } impl Component for Home { @@ -154,106 +281,99 @@ impl Component for Home { Ok(None) } + fn handle_events(&mut self, event: Option) -> Result> { + if let Some(Event::Mouse(MouseEvent { kind, column, row, modifiers })) = event { + // TODO: simulate better button clicks + self.increment_btn_state = ButtonState::Normal; + self.decrement_btn_state = ButtonState::Normal; + if column >= self.increment_rect.left() + && column <= self.increment_rect.right() + && row >= self.increment_rect.top() + && row <= self.increment_rect.bottom() + { + if kind == MouseEventKind::Moved { + self.increment_btn_state = ButtonState::Hover; + } else if kind == MouseEventKind::Down(MouseButton::Left) { + self.increment_btn_state = ButtonState::Clicked; + return Ok(Some(Action::ScheduleIncrement)); + } else if kind == MouseEventKind::Up(MouseButton::Left) { + self.increment_btn_state = ButtonState::Hover; + } + }; + if column >= self.decrement_rect.left() + && column <= self.decrement_rect.right() + && row >= self.decrement_rect.top() + && row <= self.decrement_rect.bottom() + { + if kind == MouseEventKind::Moved { + self.decrement_btn_state = ButtonState::Hover; + } else if kind == MouseEventKind::Down(MouseButton::Left) { + self.decrement_btn_state = ButtonState::Clicked; + return Ok(Some(Action::ScheduleDecrement)); + } else if kind == MouseEventKind::Up(MouseButton::Left) { + self.decrement_btn_state = ButtonState::Hover; + } + } + } + Ok(None) + } + fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> Result<()> { - let rects = Layout::default().constraints([Constraint::Percentage(100), Constraint::Min(3)].as_ref()).split(rect); + let [main_rect, input_rect] = + *Layout::default().constraints([Constraint::Percentage(100), Constraint::Min(3)].as_ref()).split(rect) + else { + panic!("Unable to split rects into a refutable pattern"); + }; - let mut text: Vec = self.text.clone().iter().map(|l| Line::from(l.clone())).collect(); - text.insert(0, "".into()); - text.insert(0, "Type into input and hit enter to display here".dim().into()); - text.insert(0, "".into()); - text.insert(0, format!("Render Ticker: {}", self.render_ticker).into()); - text.insert(0, format!("App Ticker: {}", self.app_ticker).into()); - text.insert(0, format!("Counter: {}", self.counter).into()); - text.insert(0, "".into()); - text.insert( - 0, - Line::from(vec![ - "Press ".into(), - Span::styled("j", Style::default().fg(Color::Red)), - " or ".into(), - Span::styled("k", Style::default().fg(Color::Red)), - " to ".into(), - Span::styled("increment", Style::default().fg(Color::Yellow)), - " or ".into(), - Span::styled("decrement", Style::default().fg(Color::Yellow)), - ".".into(), - ]), - ); - text.insert(0, "".into()); + f.render_widget(self.main_widget(), main_rect); + self.main_rect = main_rect; + + let buttons = Layout::default() + .constraints([Constraint::Percentage(100), Constraint::Min(3), Constraint::Min(1)].as_ref()) + .split(main_rect)[1]; + let buttons = Layout::default() + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Min(1), + Constraint::Percentage(25), + Constraint::Percentage(25), + ] + .as_ref(), + ) + .direction(Direction::Horizontal) + .split(buttons); + + f.render_widget(self.increment_widget(), buttons[1]); + f.render_widget(self.decrement_widget(), buttons[3]); + self.increment_rect = buttons[1]; + self.decrement_rect = buttons[3]; + + f.render_widget(self.input_widget(), input_rect); + self.input_rect = input_rect; - f.render_widget( - Paragraph::new(text) - .block( - Block::default() - .title("ratatui async template") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_style(match self.mode { - Mode::Processing => Style::default().fg(Color::Yellow), - _ => Style::default(), - }) - .border_type(BorderType::Rounded), - ) - .style(Style::default().fg(Color::Cyan)) - .alignment(Alignment::Center), - rects[0], - ); - let width = rects[1].width.max(3) - 3; // keep 2 for borders and 1 for cursor - let scroll = self.input.visual_scroll(width as usize); - let input = Paragraph::new(self.input.value()) - .style(match self.mode { - Mode::Insert => Style::default().fg(Color::Yellow), - _ => Style::default(), - }) - .scroll((0, scroll as u16)) - .block(Block::default().borders(Borders::ALL).title(Line::from(vec![ - Span::raw("Enter Input Mode "), - Span::styled("(Press ", Style::default().fg(Color::DarkGray)), - Span::styled("/", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), - Span::styled(" to start, ", Style::default().fg(Color::DarkGray)), - Span::styled("ESC", Style::default().add_modifier(Modifier::BOLD).fg(Color::Gray)), - Span::styled(" to finish)", Style::default().fg(Color::DarkGray)), - ]))); - f.render_widget(input, rects[1]); if self.mode == Mode::Insert { - f.set_cursor((rects[1].x + 1 + self.input.cursor() as u16).min(rects[1].x + rects[1].width - 2), rects[1].y + 1) + f.set_cursor( + (input_rect.x + 1 + self.input.cursor() as u16).min(input_rect.x + input_rect.width - 2), + input_rect.y + 1, + ) } if self.show_help { let rect = rect.inner(&Margin { horizontal: 4, vertical: 2 }); f.render_widget(Clear, rect); - let block = Block::default() - .title(Line::from(vec![Span::styled("Key Bindings", Style::default().add_modifier(Modifier::BOLD))])) - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)); + let (block, table) = self.help_widget(); f.render_widget(block, rect); - let rows = vec![ - Row::new(vec!["j", "Increment"]), - Row::new(vec!["k", "Decrement"]), - Row::new(vec!["/", "Enter Input"]), - Row::new(vec!["ESC", "Exit Input"]), - Row::new(vec!["Enter", "Submit Input"]), - Row::new(vec!["q", "Quit"]), - Row::new(vec!["?", "Open Help"]), - ]; - let table = Table::new(rows, &[Constraint::Percentage(10), Constraint::Percentage(90)]) - .header(Row::new(vec!["Key", "Action"]).bottom_margin(1).style(Style::default().add_modifier(Modifier::BOLD))) - .column_spacing(1); f.render_widget(table, rect.inner(&Margin { vertical: 4, horizontal: 2 })); }; - f.render_widget( - Block::default() - .title( - ratatui::widgets::block::Title::from(format!( - "{:?}", - &self.last_events.iter().map(|k| key_event_to_string(k)).collect::>() - )) - .alignment(Alignment::Right), - ) - .title_style(Style::default().add_modifier(Modifier::BOLD)), - Rect { x: rect.x + 1, y: rect.height.saturating_sub(1), width: rect.width.saturating_sub(2), height: 1 }, - ); + f.render_widget(self.title_widget(), Rect { + x: rect.x + 1, + y: rect.height.saturating_sub(1), + width: rect.width.saturating_sub(2), + height: 1, + }); Ok(()) }