Skip to content

Commit

Permalink
feat: Add button with mouse event example ✨ (ratatui-org#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Jan 2, 2024
1 parent 6eff900 commit d08c4c6
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 94 deletions.
6 changes: 2 additions & 4 deletions async/ratatui-counter/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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()?;
Expand Down
300 changes: 210 additions & 90 deletions async/ratatui-counter/src/components/home.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand All @@ -32,6 +43,12 @@ pub struct Home {
pub keymap: HashMap<KeyEvent, Action>,
pub text: Vec<String>,
pub last_events: Vec<KeyEvent>,
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 {
Expand Down Expand Up @@ -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<Line> = 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::<Vec<_>>()))
.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 {
Expand Down Expand Up @@ -154,106 +281,99 @@ impl Component for Home {
Ok(None)
}

fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>> {
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<Line> = 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::<Vec<_>>()
))
.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(())
}
Expand Down

0 comments on commit d08c4c6

Please sign in to comment.