Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add button with mouse event example ✨ #35

Merged
merged 1 commit into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
{
Comment on lines +303 to +307
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have an helper method for doing this probably.

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