StatefulWidgets (#7)

* trait EventHandler
* StatefulWidget: AppWidget, CountdownWidget
* StatefulWidget: TimerWidget
This commit is contained in:
Jens K. 2024-12-03 11:23:43 +01:00 committed by GitHub
parent 4f66ea86d4
commit cbb6a60ee9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 107 additions and 73 deletions

View File

@ -1,11 +1,15 @@
use crate::{ use crate::{
clock::{self, Clock}, clock::{self, Clock},
constants::TICK_VALUE_MS, constants::TICK_VALUE_MS,
events::{Event, Events}, events::{Event, EventHandler, Events},
terminal::Terminal, terminal::Terminal,
utils::center, utils::center,
widgets::{ widgets::{
countdown::Countdown, footer::Footer, header::Header, pomodoro::Pomodoro, timer::Timer, countdown::{Countdown, CountdownWidget},
footer::Footer,
header::Header,
pomodoro::Pomodoro,
timer::{Timer, TimerWidget},
}, },
}; };
use color_eyre::Result; use color_eyre::Result;
@ -13,7 +17,7 @@ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::{KeyCode, KeyEvent}, crossterm::event::{KeyCode, KeyEvent},
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
widgets::{Block, Widget}, widgets::{Block, StatefulWidget, Widget},
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -34,8 +38,8 @@ pub struct App {
content: Content, content: Content,
mode: Mode, mode: Mode,
show_menu: bool, show_menu: bool,
clock_countdown: Clock<clock::Countdown>, countdown: Countdown,
clock_timer: Clock<clock::Timer>, timer: Timer,
} }
impl Default for App { impl Default for App {
@ -44,11 +48,14 @@ impl Default for App {
mode: Mode::Running, mode: Mode::Running,
content: Content::Countdown, content: Content::Countdown,
show_menu: false, show_menu: false,
clock_countdown: Clock::<clock::Countdown>::new( countdown: Countdown::new(
"Countdown".into(),
Clock::<clock::Countdown>::new(
10 * 60 * 1000, /* 10min in milliseconds */ 10 * 60 * 1000, /* 10min in milliseconds */
TICK_VALUE_MS, TICK_VALUE_MS,
), ),
clock_timer: Clock::<clock::Timer>::new(0, TICK_VALUE_MS), ),
timer: Timer::new("Timer".into(), Clock::<clock::Timer>::new(0, TICK_VALUE_MS)),
} }
} }
} }
@ -61,13 +68,15 @@ impl App {
pub async fn run(&mut self, mut terminal: Terminal, mut events: Events) -> Result<()> { pub async fn run(&mut self, mut terminal: Terminal, mut events: Events) -> Result<()> {
while self.is_running() { while self.is_running() {
if let Some(event) = events.next().await { if let Some(event) = events.next().await {
match self.content {
Content::Countdown => self.countdown.update(event.clone()),
Content::Timer => self.timer.update(event.clone()),
_ => {}
};
match event { match event {
Event::Render | Event::Resize(_, _) => { Event::Render | Event::Resize => {
self.draw(&mut terminal)?; self.draw(&mut terminal)?;
} }
Event::Tick => {
self.tick();
}
Event::Key(key) => self.handle_key_event(key), Event::Key(key) => self.handle_key_event(key),
_ => {} _ => {}
} }
@ -84,73 +93,48 @@ impl App {
match key.code { match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit, KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
KeyCode::Char('c') => self.content = Content::Countdown, KeyCode::Char('c') => self.content = Content::Countdown,
KeyCode::Char('s') => self.toggle(),
KeyCode::Char('t') => self.content = Content::Timer, KeyCode::Char('t') => self.content = Content::Timer,
KeyCode::Char('p') => self.content = Content::Pomodoro, KeyCode::Char('p') => self.content = Content::Pomodoro,
KeyCode::Char('m') => self.show_menu = !self.show_menu, KeyCode::Char('m') => self.show_menu = !self.show_menu,
KeyCode::Char('r') => self.reset(),
_ => {} _ => {}
}; };
} }
fn draw(&self, terminal: &mut Terminal) -> Result<()> { fn draw(&mut self, terminal: &mut Terminal) -> Result<()> {
terminal.draw(|frame| { terminal.draw(|frame| {
frame.render_widget(self, frame.area()); frame.render_stateful_widget(AppWidget, frame.area(), self);
})?; })?;
Ok(()) Ok(())
} }
}
fn render_content(&self, area: Rect, buf: &mut Buffer) { struct AppWidget;
impl AppWidget {
fn render_content(&self, area: Rect, buf: &mut Buffer, state: &mut App) {
// center content // center content
let area = center(area, Constraint::Length(50), Constraint::Length(2)); let area = center(area, Constraint::Length(50), Constraint::Length(2));
match self.content { match state.content {
Content::Timer => { Content::Timer => TimerWidget.render(area, buf, &mut state.timer),
Timer::new("Timer".into(), self.clock_timer.clone()).render(area, buf) Content::Countdown => CountdownWidget.render(area, buf, &mut state.countdown),
}
Content::Countdown => {
Countdown::new("Countdown".into(), self.clock_countdown.clone()).render(area, buf)
}
Content::Pomodoro => Pomodoro::new("Pomodoro".into()).render(area, buf), Content::Pomodoro => Pomodoro::new("Pomodoro".into()).render(area, buf),
}; };
} }
fn reset(&mut self) {
match self.content {
Content::Timer => self.clock_timer.reset(),
Content::Countdown => self.clock_countdown.reset(),
_ => {}
};
} }
fn toggle(&mut self) { impl StatefulWidget for AppWidget {
match self.content { type State = App;
Content::Timer => self.clock_timer.toggle_pause(), fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Content::Countdown => self.clock_countdown.toggle_pause(),
_ => {}
};
}
fn tick(&mut self) {
match self.content {
Content::Timer => self.clock_timer.tick(),
Content::Countdown => self.clock_countdown.tick(),
_ => {}
};
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let vertical = Layout::vertical([ let vertical = Layout::vertical([
Constraint::Length(1), Constraint::Length(1),
Constraint::Fill(0), Constraint::Fill(0),
Constraint::Length(if self.show_menu { 2 } else { 1 }), Constraint::Length(if state.show_menu { 2 } else { 1 }),
]); ]);
let [v0, v1, v4] = vertical.areas(area); let [v0, v1, v4] = vertical.areas(area);
Block::new().render(area, buf); Block::new().render(area, buf);
Header::new(true).render(v0, buf); Header::new(true).render(v0, buf);
self.render_content(v1, buf); self.render_content(v1, buf, state);
Footer::new(self.show_menu, self.content).render(v4, buf); Footer::new(state.show_menu, state.content).render(v4, buf);
} }
} }

View File

@ -14,13 +14,11 @@ enum StreamKey {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Event { pub enum Event {
Init,
Quit,
Error, Error,
Tick, Tick,
Render, Render,
Key(KeyEvent), Key(KeyEvent),
Resize(u16, u16), Resize,
} }
pub struct Events { pub struct Events {
@ -69,10 +67,14 @@ fn crossterm_stream() -> Pin<Box<dyn Stream<Item = Event>>> {
Ok(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => { Ok(CrosstermEvent::Key(key)) if key.kind == KeyEventKind::Press => {
Some(Event::Key(key)) Some(Event::Key(key))
} }
Ok(CrosstermEvent::Resize(x, y)) => Some(Event::Resize(x, y)), Ok(CrosstermEvent::Resize(_, _)) => Some(Event::Resize),
Err(_) => Some(Event::Error), Err(_) => Some(Event::Error),
_ => None, _ => None,
} }
}), }),
) )
} }
pub trait EventHandler {
fn update(&mut self, _: Event);
}

View File

@ -1,12 +1,16 @@
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::KeyCode,
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
widgets::{Paragraph, Widget}, widgets::{Paragraph, StatefulWidget, Widget},
}; };
use crate::clock::{self, Clock}; use crate::{
clock::{self, Clock},
events::{Event, EventHandler},
};
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Countdown { pub struct Countdown {
headline: String, headline: String,
clock: Clock<clock::Countdown>, clock: Clock<clock::Countdown>,
@ -18,13 +22,33 @@ impl Countdown {
} }
} }
impl Widget for Countdown { impl EventHandler for Countdown {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn update(&mut self, event: Event) {
let h = Paragraph::new(self.headline).centered(); match event {
let c = Paragraph::new(self.clock.format()).centered(); Event::Tick => {
self.clock.tick();
}
Event::Key(key) if key.code == KeyCode::Char('s') => {
self.clock.toggle_pause();
}
Event::Key(key) if key.code == KeyCode::Char('r') => {
self.clock.reset();
}
_ => {}
}
}
}
pub struct CountdownWidget;
impl StatefulWidget for &CountdownWidget {
type State = Countdown;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let h = Paragraph::new(state.headline.clone()).centered();
let c = Paragraph::new(state.clock.format()).centered();
let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
h.render(v1, buf); h.render(v1, buf);
c.render(v2, buf) c.render(v2, buf);
} }
} }

View File

@ -1,12 +1,16 @@
use ratatui::{ use ratatui::{
buffer::Buffer, buffer::Buffer,
crossterm::event::KeyCode,
layout::{Constraint, Layout, Rect}, layout::{Constraint, Layout, Rect},
widgets::{Paragraph, Widget}, widgets::{Paragraph, StatefulWidget, Widget},
}; };
use crate::clock::{self, Clock}; use crate::{
clock::{self, Clock},
events::{Event, EventHandler},
};
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Timer { pub struct Timer {
headline: String, headline: String,
clock: Clock<clock::Timer>, clock: Clock<clock::Timer>,
@ -18,10 +22,30 @@ impl Timer {
} }
} }
impl Widget for Timer { impl EventHandler for Timer {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn update(&mut self, event: Event) {
let h = Paragraph::new(self.headline).centered(); match event {
let c = Paragraph::new(self.clock.format()).centered(); Event::Tick => {
self.clock.tick();
}
Event::Key(key) if key.code == KeyCode::Char('s') => {
self.clock.toggle_pause();
}
Event::Key(key) if key.code == KeyCode::Char('r') => {
self.clock.reset();
}
_ => {}
}
}
}
pub struct TimerWidget;
impl StatefulWidget for TimerWidget {
type State = Timer;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let h = Paragraph::new(state.headline.clone()).centered();
let c = Paragraph::new(state.clock.format()).centered();
let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
h.render(v1, buf); h.render(v1, buf);