feat(example): add drawing feature to the canvas example (#1429)

![rec_20241018T235208](https://github.com/user-attachments/assets/cfb2f9f8-773b-4599-9312-29625ff2ca60)


fun fact: I had to do [35
pushups](https://www.youtube.com/watch?v=eS92stzBYXA) for this...

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
Orhun Parmaksız 2024-10-20 11:41:40 +03:00 committed by GitHub
parent 4f5503dbf6
commit 4c4851ca3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,16 +13,24 @@
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
use std::time::{Duration, Instant}; use std::{
io::stdout,
time::{Duration, Instant},
};
use color_eyre::Result; use color_eyre::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{ use ratatui::{
crossterm::event::{self, Event, KeyCode}, crossterm::event::{self, Event, KeyCode, MouseEventKind},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Position, Rect},
style::{Color, Stylize}, style::{Color, Stylize},
symbols::Marker, symbols::Marker,
widgets::{ widgets::{
canvas::{Canvas, Circle, Map, MapResolution, Rectangle}, canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
Block, Widget, Block, Widget,
}, },
DefaultTerminal, Frame, DefaultTerminal, Frame,
@ -30,13 +38,16 @@ use ratatui::{
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
stdout().execute(EnableMouseCapture)?;
let terminal = ratatui::init(); let terminal = ratatui::init();
let app_result = App::new().run(terminal); let app_result = App::new().run(terminal);
ratatui::restore(); ratatui::restore();
stdout().execute(DisableMouseCapture)?;
app_result app_result
} }
struct App { struct App {
exit: bool,
x: f64, x: f64,
y: f64, y: f64,
ball: Circle, ball: Circle,
@ -45,11 +56,14 @@ struct App {
vy: f64, vy: f64,
tick_count: u64, tick_count: u64,
marker: Marker, marker: Marker,
points: Vec<Position>,
is_drawing: bool,
} }
impl App { impl App {
const fn new() -> Self { const fn new() -> Self {
Self { Self {
exit: false,
x: 0.0, x: 0.0,
y: 0.0, y: 0.0,
ball: Circle { ball: Circle {
@ -63,25 +77,22 @@ impl App {
vy: 1.0, vy: 1.0,
tick_count: 0, tick_count: 0,
marker: Marker::Dot, marker: Marker::Dot,
points: vec![],
is_drawing: false,
} }
} }
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let tick_rate = Duration::from_millis(16); let tick_rate = Duration::from_millis(16);
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { while !self.exit {
terminal.draw(|frame| self.draw(frame))?; terminal.draw(|frame| self.draw(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed()); let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? { if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { match event::read()? {
match key.code { Event::Key(key) => self.handle_key_press(key),
KeyCode::Char('q') => break Ok(()), Event::Mouse(event) => self.handle_mouse_event(event),
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0, _ => (),
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
} }
} }
@ -90,6 +101,32 @@ impl App {
last_tick = Instant::now(); last_tick = Instant::now();
} }
} }
Ok(())
}
fn handle_key_press(&mut self, key: event::KeyEvent) {
if key.kind != KeyEventKind::Press {
return;
}
match key.code {
KeyCode::Char('q') => self.exit = true,
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
_ => {}
}
}
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
match event.kind {
MouseEventKind::Down(_) => self.is_drawing = true,
MouseEventKind::Up(_) => self.is_drawing = false,
MouseEventKind::Drag(_) => {
self.points.push(Position::new(event.column, event.row));
}
_ => {}
}
} }
fn on_tick(&mut self) { fn on_tick(&mut self) {
@ -126,10 +163,12 @@ impl App {
let horizontal = let horizontal =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]); Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]); let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
let [map, right] = horizontal.areas(frame.area()); let [left, right] = horizontal.areas(frame.area());
let [draw, map] = vertical.areas(left);
let [pong, boxes] = vertical.areas(right); let [pong, boxes] = vertical.areas(right);
frame.render_widget(self.map_canvas(), map); frame.render_widget(self.map_canvas(), map);
frame.render_widget(self.draw_canvas(draw), draw);
frame.render_widget(self.pong_canvas(), pong); frame.render_widget(self.pong_canvas(), pong);
frame.render_widget(self.boxes_canvas(boxes), boxes); frame.render_widget(self.boxes_canvas(boxes), boxes);
} }
@ -149,6 +188,30 @@ impl App {
.y_bounds([-90.0, 90.0]) .y_bounds([-90.0, 90.0])
} }
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
Canvas::default()
.block(Block::bordered().title("Draw here"))
.marker(self.marker)
.x_bounds([0.0, f64::from(area.width)])
.y_bounds([0.0, f64::from(area.height)])
.paint(move |ctx| {
let points = self
.points
.iter()
.map(|p| {
(
f64::from(p.x) - f64::from(area.left()),
f64::from(area.bottom()) - f64::from(p.y),
)
})
.collect_vec();
ctx.draw(&Points {
coords: &points,
color: Color::White,
});
})
}
fn pong_canvas(&self) -> impl Widget + '_ { fn pong_canvas(&self) -> impl Widget + '_ {
Canvas::default() Canvas::default()
.block(Block::bordered().title("Pong")) .block(Block::bordered().title("Pong"))