StatefulWidgets (#7)
* trait EventHandler * StatefulWidget: AppWidget, CountdownWidget * StatefulWidget: TimerWidget
This commit is contained in:
parent
4f66ea86d4
commit
cbb6a60ee9
88
src/app.rs
88
src/app.rs
@ -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) {
|
|
||||||
match self.content {
|
|
||||||
Content::Timer => self.clock_timer.toggle_pause(),
|
|
||||||
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 {
|
impl StatefulWidget for AppWidget {
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
type State = App;
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user