From a6a13682507846320a79538ffad673a58c1143f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orhun=20Parmaks=C4=B1z?= Date: Mon, 16 Dec 2024 22:41:41 +0300 Subject: [PATCH] chore(examples): add calendar explorer demo app (#1571) Related to #1512 As discussed, this moves the calendar example from ratatui to app examples as an "explorer" example. It also adds interactivity where you can press s to toggle between different styles of calendars. --------- Co-authored-by: Josh McKinney --- Cargo.lock | 10 + examples/README.md | 4 + examples/apps/calendar-explorer/Cargo.toml | 15 ++ examples/apps/calendar-explorer/README.md | 9 + examples/apps/calendar-explorer/src/main.rs | 248 +++++++++++++++++++ ratatui/Cargo.toml | 5 - ratatui/examples/calendar.rs | 249 -------------------- 7 files changed, 286 insertions(+), 254 deletions(-) create mode 100644 examples/apps/calendar-explorer/Cargo.toml create mode 100644 examples/apps/calendar-explorer/README.md create mode 100644 examples/apps/calendar-explorer/src/main.rs delete mode 100644 ratatui/examples/calendar.rs diff --git a/Cargo.lock b/Cargo.lock index 2708978f..99a4b62f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,16 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +[[package]] +name = "calendar-explorer" +version = "0.0.0" +dependencies = [ + "color-eyre", + "crossterm", + "ratatui", + "time", +] + [[package]] name = "camino" version = "1.1.9" diff --git a/examples/README.md b/examples/README.md index 7b195c0f..8332b665 100644 --- a/examples/README.md +++ b/examples/README.md @@ -60,3 +60,7 @@ Shows how to fetch data from GitHub API asynchronously. [Source](./apps/async-gi ## Weather demo Shows how to render weather data using barchart widget. [Source](./apps/weather/). + +## Calendar explorer demo + +Shows how to render a calendar with different styles. [Source](./apps/calendar-explorer/). diff --git a/examples/apps/calendar-explorer/Cargo.toml b/examples/apps/calendar-explorer/Cargo.toml new file mode 100644 index 00000000..7c3da818 --- /dev/null +++ b/examples/apps/calendar-explorer/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "calendar-explorer" +publish = false +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +color-eyre.workspace = true +crossterm.workspace = true +ratatui.workspace = true +time = { version = "0.3.37", features = ["formatting", "parsing"] } + +[lints] +workspace = true diff --git a/examples/apps/calendar-explorer/README.md b/examples/apps/calendar-explorer/README.md new file mode 100644 index 00000000..670287ee --- /dev/null +++ b/examples/apps/calendar-explorer/README.md @@ -0,0 +1,9 @@ +# Calendar explorer demo + +This example shows how to render a calendar with different styles. + +To run this demo: + +```shell +cargo run -p calendar-explorer +``` diff --git a/examples/apps/calendar-explorer/src/main.rs b/examples/apps/calendar-explorer/src/main.rs new file mode 100644 index 00000000..12f435b9 --- /dev/null +++ b/examples/apps/calendar-explorer/src/main.rs @@ -0,0 +1,248 @@ +//! A Ratatui example that demonstrates how to render calendar with different styles. +//! +//! Marks the holidays and seasons on the calendar. +//! +//! This example runs with the Ratatui library code in the branch that you are currently reading. +//! See the [`latest`] branch for the code which works with the most recent Ratatui release. +//! +//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest +//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html + +use std::fmt; + +use color_eyre::Result; +use crossterm::event::{self, Event, KeyCode, KeyEventKind}; +use ratatui::{ + layout::{Constraint, Layout, Margin, Rect}, + style::{Color, Modifier, Style, Stylize}, + text::{Line, Text}, + widgets::calendar::{CalendarEventStore, Monthly}, + DefaultTerminal, Frame, +}; +use time::{ext::NumericalDuration, Date, Month, OffsetDateTime}; + +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init(); + let result = run(terminal); + ratatui::restore(); + result +} + +/// Run the application. +fn run(mut terminal: DefaultTerminal) -> Result<()> { + let mut selected_date = OffsetDateTime::now_local()?.date(); + let mut calendar_style = StyledCalendar::Default; + loop { + terminal.draw(|frame| render(frame, calendar_style, selected_date))?; + if let Event::Key(key) = event::read()? { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Char('q') => break Ok(()), + KeyCode::Char('s') => calendar_style = calendar_style.next(), + KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date), + KeyCode::Char('p') | KeyCode::BackTab => { + selected_date = previous_month(selected_date); + } + KeyCode::Char('h') | KeyCode::Left => selected_date -= 1.days(), + KeyCode::Char('j') | KeyCode::Down => selected_date += 1.weeks(), + KeyCode::Char('k') | KeyCode::Up => selected_date -= 1.weeks(), + KeyCode::Char('l') | KeyCode::Right => selected_date += 1.days(), + _ => {} + } + } + } + } +} + +fn next_month(date: Date) -> Date { + if date.month() == Month::December { + date.replace_month(Month::January) + .unwrap() + .replace_year(date.year() + 1) + .unwrap() + } else { + date.replace_month(date.month().next()).unwrap() + } +} + +fn previous_month(date: Date) -> Date { + if date.month() == Month::January { + date.replace_month(Month::December) + .unwrap() + .replace_year(date.year() - 1) + .unwrap() + } else { + date.replace_month(date.month().previous()).unwrap() + } +} + +/// Draw the UI with a calendar. +fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date) { + let header = Text::from_iter([ + Line::from("Calendar Example".bold()), + Line::from( + " Quit | Change Style | Next Month |

Previous Month, Move", + ), + Line::from(format!( + "Current date: {selected_date} | Current style: {calendar_style}" + )), + ]); + + let vertical = Layout::vertical([ + Constraint::Length(header.height() as u16), + Constraint::Fill(1), + ]); + let [text_area, area] = vertical.areas(frame.area()); + frame.render_widget(header.centered(), text_area); + calendar_style + .render_year(frame, area, selected_date) + .unwrap(); +} + +#[derive(Debug, Clone, Copy)] +enum StyledCalendar { + Default, + Surrounding, + WeekdaysHeader, + SurroundingAndWeekdaysHeader, + MonthHeader, + MonthAndWeekdaysHeader, +} + +impl StyledCalendar { + // Cycle through the different styles. + const fn next(self) -> Self { + match self { + Self::Default => Self::Surrounding, + Self::Surrounding => Self::WeekdaysHeader, + Self::WeekdaysHeader => Self::SurroundingAndWeekdaysHeader, + Self::SurroundingAndWeekdaysHeader => Self::MonthHeader, + Self::MonthHeader => Self::MonthAndWeekdaysHeader, + Self::MonthAndWeekdaysHeader => Self::Default, + } + } +} + +impl fmt::Display for StyledCalendar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Default => write!(f, "Default"), + Self::Surrounding => write!(f, "Show Surrounding"), + Self::WeekdaysHeader => write!(f, "Show Weekdays Header"), + Self::SurroundingAndWeekdaysHeader => write!(f, "Show Surrounding and Weekdays Header"), + Self::MonthHeader => write!(f, "Show Month Header"), + Self::MonthAndWeekdaysHeader => write!(f, "Show Month Header and Weekdays Header"), + } + } +} + +impl StyledCalendar { + fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> { + let events = events(date)?; + + let area = area.inner(Margin { + vertical: 1, + horizontal: 1, + }); + let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area); + let areas = rows.iter().flat_map(|row| { + Layout::horizontal([Constraint::Ratio(1, 4); 4]) + .split(*row) + .to_vec() + }); + for (i, area) in areas.enumerate() { + let month = date + .replace_day(1) + .unwrap() + .replace_month(Month::try_from(i as u8 + 1).unwrap()) + .unwrap(); + self.render_month(frame, area, month, &events); + } + Ok(()) + } + + fn render_month(self, frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) { + let calendar = match self { + Self::Default => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()), + Self::Surrounding => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()) + .show_surrounding(Style::new().dim()), + Self::WeekdaysHeader => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()) + .show_weekdays_header(Style::new().bold().green()), + Self::SurroundingAndWeekdaysHeader => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()) + .show_surrounding(Style::new().dim()) + .show_weekdays_header(Style::new().bold().green()), + Self::MonthHeader => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()) + .show_month_header(Style::new().bold().green()), + Self::MonthAndWeekdaysHeader => Monthly::new(date, events) + .default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50))) + .show_month_header(Style::default()) + .show_weekdays_header(Style::new().bold().dim().light_yellow()), + }; + frame.render_widget(calendar, area); + } +} + +/// Makes a list of dates for the current year. +fn events(selected_date: Date) -> Result { + const SELECTED: Style = Style::new() + .fg(Color::White) + .bg(Color::Red) + .add_modifier(Modifier::BOLD); + const HOLIDAY: Style = Style::new() + .fg(Color::Red) + .add_modifier(Modifier::UNDERLINED); + const SEASON: Style = Style::new() + .fg(Color::Green) + .bg(Color::Black) + .add_modifier(Modifier::UNDERLINED); + + let mut list = CalendarEventStore::today( + Style::default() + .add_modifier(Modifier::BOLD) + .bg(Color::Blue), + ); + let y = selected_date.year(); + + // new year's + list.add(Date::from_calendar_date(y, Month::January, 1)?, HOLIDAY); + // next new_year's for December "show surrounding" + list.add(Date::from_calendar_date(y + 1, Month::January, 1)?, HOLIDAY); + // groundhog day + list.add(Date::from_calendar_date(y, Month::February, 2)?, HOLIDAY); + // april fool's + list.add(Date::from_calendar_date(y, Month::April, 1)?, HOLIDAY); + // earth day + list.add(Date::from_calendar_date(y, Month::April, 22)?, HOLIDAY); + // star wars day + list.add(Date::from_calendar_date(y, Month::May, 4)?, HOLIDAY); + // festivus + list.add(Date::from_calendar_date(y, Month::December, 23)?, HOLIDAY); + // new year's eve + list.add(Date::from_calendar_date(y, Month::December, 31)?, HOLIDAY); + + // seasons + // spring equinox + list.add(Date::from_calendar_date(y, Month::March, 22)?, SEASON); + // summer solstice + list.add(Date::from_calendar_date(y, Month::June, 21)?, SEASON); + // fall equinox + list.add(Date::from_calendar_date(y, Month::September, 22)?, SEASON); + // winter solstice + list.add(Date::from_calendar_date(y, Month::December, 21)?, SEASON); + + // selected date + list.add(selected_date, SELECTED); + + Ok(list) +} diff --git a/ratatui/Cargo.toml b/ratatui/Cargo.toml index bb2f3c4c..f42f3e5d 100644 --- a/ratatui/Cargo.toml +++ b/ratatui/Cargo.toml @@ -142,11 +142,6 @@ bench = false name = "main" harness = false -[[example]] -name = "calendar" -required-features = ["crossterm", "widget-calendar"] -doc-scrape-examples = true - [[example]] name = "canvas" required-features = ["crossterm"] diff --git a/ratatui/examples/calendar.rs b/ratatui/examples/calendar.rs deleted file mode 100644 index 29fc2c50..00000000 --- a/ratatui/examples/calendar.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! # [Ratatui] Calendar example -//! -//! The latest version of this example is available in the [examples] folder in the repository. -//! -//! Please note that the examples are designed to be run against the `main` branch of the Github -//! repository. This means that you may not be able to compile with the latest release version on -//! crates.io, or the one that you have installed locally. -//! -//! See the [examples readme] for more information on finding examples that match the version of the -//! library you are using. -//! -//! [Ratatui]: https://github.com/ratatui/ratatui -//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples -//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md - -use color_eyre::Result; -use ratatui::{ - crossterm::event::{self, Event, KeyCode, KeyEventKind}, - layout::{Constraint, Layout, Margin}, - style::{Color, Modifier, Style}, - widgets::calendar::{CalendarEventStore, DateStyler, Monthly}, - DefaultTerminal, Frame, -}; -use time::{Date, Month, OffsetDateTime}; - -fn main() -> Result<()> { - color_eyre::install()?; - let terminal = ratatui::init(); - let result = run(terminal); - ratatui::restore(); - result -} - -fn run(mut terminal: DefaultTerminal) -> Result<()> { - loop { - terminal.draw(draw)?; - if let Event::Key(key) = event::read()? { - if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') { - break Ok(()); - } - } - } -} - -fn draw(frame: &mut Frame) { - let area = frame.area().inner(Margin { - vertical: 1, - horizontal: 1, - }); - - let mut start = OffsetDateTime::now_local() - .unwrap() - .date() - .replace_month(Month::January) - .unwrap() - .replace_day(1) - .unwrap(); - - let list = make_dates(start.year()); - - let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area); - let cols = rows.iter().flat_map(|row| { - Layout::horizontal([Constraint::Ratio(1, 4); 4]) - .split(*row) - .to_vec() - }); - for col in cols { - let cal = cals::get_cal(start.month(), start.year(), &list); - frame.render_widget(cal, col); - start = start.replace_month(start.month().next()).unwrap(); - } -} - -fn make_dates(current_year: i32) -> CalendarEventStore { - let mut list = CalendarEventStore::today( - Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Blue), - ); - - // Holidays - let holiday_style = Style::default() - .fg(Color::Red) - .add_modifier(Modifier::UNDERLINED); - - // new year's - list.add( - Date::from_calendar_date(current_year, Month::January, 1).unwrap(), - holiday_style, - ); - // next new_year's for December "show surrounding" - list.add( - Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(), - holiday_style, - ); - // groundhog day - list.add( - Date::from_calendar_date(current_year, Month::February, 2).unwrap(), - holiday_style, - ); - // april fool's - list.add( - Date::from_calendar_date(current_year, Month::April, 1).unwrap(), - holiday_style, - ); - // earth day - list.add( - Date::from_calendar_date(current_year, Month::April, 22).unwrap(), - holiday_style, - ); - // star wars day - list.add( - Date::from_calendar_date(current_year, Month::May, 4).unwrap(), - holiday_style, - ); - // festivus - list.add( - Date::from_calendar_date(current_year, Month::December, 23).unwrap(), - holiday_style, - ); - // new year's eve - list.add( - Date::from_calendar_date(current_year, Month::December, 31).unwrap(), - holiday_style, - ); - - // seasons - let season_style = Style::default() - .fg(Color::White) - .bg(Color::Yellow) - .add_modifier(Modifier::UNDERLINED); - // spring equinox - list.add( - Date::from_calendar_date(current_year, Month::March, 22).unwrap(), - season_style, - ); - // summer solstice - list.add( - Date::from_calendar_date(current_year, Month::June, 21).unwrap(), - season_style, - ); - // fall equinox - list.add( - Date::from_calendar_date(current_year, Month::September, 22).unwrap(), - season_style, - ); - list.add( - Date::from_calendar_date(current_year, Month::December, 21).unwrap(), - season_style, - ); - list -} - -mod cals { - #[allow(clippy::wildcard_imports)] - use super::*; - - pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - match m { - Month::May => example1(m, y, es), - Month::June => example2(m, y, es), - Month::July | Month::December => example3(m, y, es), - Month::February => example4(m, y, es), - Month::November => example5(m, y, es), - _ => default(m, y, es), - } - } - - fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_month_header(Style::default()) - .default_style(default_style) - } - - fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_surrounding(default_style) - .default_style(default_style) - .show_month_header(Style::default()) - } - - fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let header_style = Style::default() - .add_modifier(Modifier::BOLD) - .add_modifier(Modifier::DIM) - .fg(Color::LightYellow); - - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_weekdays_header(header_style) - .default_style(default_style) - .show_month_header(Style::default()) - } - - fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let header_style = Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Green); - - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_surrounding(Style::default().add_modifier(Modifier::DIM)) - .show_weekdays_header(header_style) - .default_style(default_style) - .show_month_header(Style::default()) - } - - fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let header_style = Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Green); - - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_weekdays_header(header_style) - .default_style(default_style) - } - - fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> { - let header_style = Style::default() - .add_modifier(Modifier::BOLD) - .fg(Color::Green); - - let default_style = Style::default() - .add_modifier(Modifier::BOLD) - .bg(Color::Rgb(50, 50, 50)); - - Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es) - .show_month_header(header_style) - .default_style(default_style) - } -}