diff --git a/src/app.rs b/src/app.rs index cee618e..56bfcc0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,6 @@ use crate::{ constants::TICK_VALUE_MS, events::{Event, EventHandler, Events}, terminal::Terminal, - utils::center, widgets::{ countdown::{Countdown, CountdownWidget}, footer::Footer, @@ -17,7 +16,7 @@ use ratatui::{ buffer::Buffer, crossterm::event::{KeyCode, KeyEvent}, layout::{Constraint, Layout, Rect}, - widgets::{Block, StatefulWidget, Widget}, + widgets::{StatefulWidget, Widget}, }; use tracing::debug; @@ -114,8 +113,6 @@ struct AppWidget; impl AppWidget { fn render_content(&self, area: Rect, buf: &mut Buffer, state: &mut App) { - // center content - let area = center(area, Constraint::Length(50), Constraint::Length(2)); match state.content { Content::Timer => TimerWidget.render(area, buf, &mut state.timer), Content::Countdown => CountdownWidget.render(area, buf, &mut state.countdown), @@ -129,14 +126,13 @@ impl StatefulWidget for AppWidget { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { let vertical = Layout::vertical([ Constraint::Length(1), - Constraint::Fill(0), + Constraint::Percentage(100), Constraint::Length(if state.show_menu { 2 } else { 1 }), ]); - let [v0, v1, v4] = vertical.areas(area); + let [v0, v1, v2] = vertical.areas(area); - Block::new().render(area, buf); Header::new(true).render(v0, buf); self.render_content(v1, buf, state); - Footer::new(state.show_menu, state.content).render(v4, buf); + Footer::new(state.show_menu, state.content).render(v2, buf); } } diff --git a/src/clock.rs b/src/clock.rs index 4bfef96..6a55950 100644 --- a/src/clock.rs +++ b/src/clock.rs @@ -3,6 +3,12 @@ use std::marker::PhantomData; use std::time::Duration; use strum::Display; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Direction, Layout, Position, Rect}, + widgets::StatefulWidget, +}; + #[derive(Debug, Copy, Clone, Display, PartialEq, Eq)] pub enum Mode { Initial, @@ -115,3 +121,227 @@ impl Clock { } } } + +const DIGIT_SYMBOL: &str = "█"; + +const DIGIT_SIZE: usize = 5; + +#[rustfmt::skip] +const DIGIT_0: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_1: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_2: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_3: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_4: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 0, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_5: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_6: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_7: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, + 0, 0, 0, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_8: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_9: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 1, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 1, 1, + 1, 1, 1, 1, 1, +]; + +#[rustfmt::skip] +const DIGIT_ERROR: [u8; DIGIT_SIZE * DIGIT_SIZE] = [ + 1, 1, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 0, + 1, 1, 0, 0, 0, + 1, 1, 1, 1, 1, +]; + +pub struct ClockWidget { + phantom: PhantomData, +} + +impl ClockWidget { + pub fn new() -> Self { + Self { + phantom: PhantomData, + } + } + + fn get_horizontal_lengths(&self) -> [u16; 3] { + [11, 4, 11] + } + + pub fn get_width(&self) -> u16 { + self.get_horizontal_lengths().iter().sum() + } + + pub fn get_height(&self) -> u16 { + DIGIT_SIZE as u16 + } + + fn render_number(number: u64, area: Rect, buf: &mut Buffer) { + let left = area.left(); + let top = area.top(); + + let digits = match number { + 0 => DIGIT_0, + 1 => DIGIT_1, + 2 => DIGIT_2, + 3 => DIGIT_3, + 4 => DIGIT_4, + 5 => DIGIT_5, + 6 => DIGIT_6, + 7 => DIGIT_7, + 8 => DIGIT_8, + 9 => DIGIT_9, + _ => DIGIT_ERROR, + }; + + digits.iter().enumerate().for_each(|(i, item)| { + let x = i % DIGIT_SIZE; + let y = i / DIGIT_SIZE; + if *item == 1 { + let p = Position { + x: left + x as u16, + y: top + y as u16, + }; + if let Some(cell) = buf.cell_mut(p) { + cell.set_symbol(DIGIT_SYMBOL); + } + } + }); + } + + fn render_digit_pair(d: u64, area: Rect, buf: &mut Buffer) { + let h = Layout::new( + Direction::Horizontal, + Constraint::from_lengths([DIGIT_SIZE as u16, 2, DIGIT_SIZE as u16]), + ) + .split(area); + Self::render_number(d / 10, h[0], buf); + Self::render_number(d % 10, h[2], buf); + } + + fn render_colon(area: Rect, buf: &mut Buffer) { + let left = area.left(); + let top = area.top(); + + let positions = [ + Position { + x: left + 1, + y: top + 1, + }, + Position { + x: left + 2, + y: top + 1, + }, + Position { + x: left + 1, + y: top + 3, + }, + Position { + x: left + 2, + y: top + 3, + }, + ]; + + for pos in positions { + if let Some(cell) = buf.cell_mut(pos) { + cell.set_symbol(DIGIT_SYMBOL); + } + } + } +} + +impl StatefulWidget for ClockWidget { + type State = Clock; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + // center + let [_, h, _] = Layout::horizontal([ + Constraint::Fill(0), + Constraint::Length(self.get_width()), + Constraint::Fill(0), + ]) + .areas(area); + + let [h1, h2, h3] = Layout::new( + Direction::Horizontal, + Constraint::from_lengths(self.get_horizontal_lengths()), + ) + .areas(h); + + Self::render_digit_pair(state.minutes(), h1, buf); + Self::render_colon(h2, buf); + Self::render_digit_pair(state.seconds(), h3, buf); + } +} diff --git a/src/utils.rs b/src/utils.rs index 8f6b8db..d7bc857 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,25 @@ use ratatui::layout::{Constraint, Flex, Layout, Rect}; -/// Helper to center an area by given `Constraint`'s +/// Helper to center an area horizontally by given `Constraint` /// based on [Center a Rect](https://ratatui.rs/recipes/layout/center-a-rect) -pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { +pub fn center_horizontal(base_area: Rect, horizontal: Constraint) -> Rect { let [area] = Layout::horizontal([horizontal]) .flex(Flex::Center) .areas(base_area); - let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area); area } + +/// Helper to center an area vertically by given `Constraint` +/// based on [Center a Rect](https://ratatui.rs/recipes/layout/center-a-rect) +pub fn center_vertical(base_area: Rect, vertical: Constraint) -> Rect { + let [area] = Layout::vertical([vertical]) + .flex(Flex::Center) + .areas(base_area); + area +} +/// Helper to center an area by given `Constraint`'s +/// based on [Center a Rect](https://ratatui.rs/recipes/layout/center-a-rect) +pub fn center(base_area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect { + let area = center_horizontal(base_area, horizontal); + center_vertical(area, vertical) +} diff --git a/src/widgets/countdown.rs b/src/widgets/countdown.rs index 611acdf..4584705 100644 --- a/src/widgets/countdown.rs +++ b/src/widgets/countdown.rs @@ -2,12 +2,15 @@ use ratatui::{ buffer::Buffer, crossterm::event::KeyCode, layout::{Constraint, Layout, Rect}, - widgets::{Paragraph, StatefulWidget, Widget}, + text::Line, + widgets::{StatefulWidget, Widget}, }; +use std::cmp::max; use crate::{ - clock::{self, Clock}, + clock::{self, Clock, ClockWidget}, events::{Event, EventHandler}, + utils::center, }; #[derive(Debug, Clone)] @@ -41,14 +44,21 @@ impl EventHandler for Countdown { pub struct CountdownWidget; -impl StatefulWidget for &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(format!("{}", state.clock)).centered(); - let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); + let clock = ClockWidget::new(); + let headline = Line::raw(state.headline.clone()); - h.render(v1, buf); - c.render(v2, buf); + let area = center( + area, + Constraint::Length(max(clock.get_width(), headline.width() as u16)), + Constraint::Length(clock.get_height() + 2), + ); + let [v1, _, v2] = + Layout::vertical(Constraint::from_lengths([clock.get_height(), 1, 1])).areas(area); + + clock.render(v1, buf, &mut state.clock); + headline.centered().render(v2, buf); } } diff --git a/src/widgets/pomodoro.rs b/src/widgets/pomodoro.rs index 919c72e..87cc0d8 100644 --- a/src/widgets/pomodoro.rs +++ b/src/widgets/pomodoro.rs @@ -1,9 +1,13 @@ use ratatui::{ buffer::Buffer, - layout::Rect, - widgets::{Paragraph, Widget}, + layout::{Constraint, Layout, Rect}, + style::Stylize, + text::Line, + widgets::Widget, }; +use crate::utils::center; + #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Pomodoro { headline: String, @@ -17,7 +21,17 @@ impl Pomodoro { impl Widget for Pomodoro { fn render(self, area: Rect, buf: &mut Buffer) { - let h = Paragraph::new(self.headline).centered(); - h.render(area, buf); + let headline = Line::raw(self.headline.clone()); + + let area = center( + area, + Constraint::Length(headline.width() as u16), + Constraint::Length(3), + ); + + let [v1, _, v2] = Layout::vertical(Constraint::from_lengths([1, 1, 1])).areas(area); + + headline.render(v2, buf); + Line::raw("SOON").centered().italic().render(v1, buf); } } diff --git a/src/widgets/timer.rs b/src/widgets/timer.rs index d3f7624..191044f 100644 --- a/src/widgets/timer.rs +++ b/src/widgets/timer.rs @@ -1,14 +1,16 @@ +use crate::{ + clock::{self, Clock, ClockWidget}, + events::{Event, EventHandler}, + utils::center, +}; use ratatui::{ buffer::Buffer, crossterm::event::KeyCode, layout::{Constraint, Layout, Rect}, - widgets::{Paragraph, StatefulWidget, Widget}, -}; - -use crate::{ - clock::{self, Clock}, - events::{Event, EventHandler}, + text::Line, + widgets::{StatefulWidget, Widget}, }; +use std::cmp::max; #[derive(Debug, Clone)] pub struct Timer { @@ -41,14 +43,21 @@ impl EventHandler for Timer { pub struct TimerWidget; -impl StatefulWidget for 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(format!("{}", state.clock)).centered(); - let [v1, v2] = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area); + let clock = ClockWidget::new(); + let headline = Line::raw(state.headline.clone()); - h.render(v1, buf); - c.render(v2, buf) + let area = center( + area, + Constraint::Length(max(clock.get_width(), headline.width() as u16)), + Constraint::Length(clock.get_height() + 2), + ); + let [v1, _, v2] = + Layout::vertical(Constraint::from_lengths([clock.get_height(), 1, 1])).areas(area); + + clock.render(v1, buf, &mut state.clock); + headline.centered().render(v2, buf); } }