ratatui/widgets/
calendar.rs

1//! A simple calendar widget. `(feature: widget-calendar)`
2//!
3//!
4//!
5//! The [`Monthly`] widget will display a calendar for the month provided in `display_date`. Days
6//! are styled using the default style unless:
7//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
8//! * a style is returned by the [`DateStyler`] for the day
9//!
10//! [`Monthly`] has several controls for what should be displayed
11use std::collections::HashMap;
12
13use time::{Date, Duration, OffsetDateTime};
14
15use crate::{
16    buffer::Buffer,
17    layout::{Alignment, Constraint, Layout, Rect},
18    style::Style,
19    text::{Line, Span},
20    widgets::{block::BlockExt, Block, Widget, WidgetRef},
21};
22
23/// Display a month calendar for the month containing `display_date`
24#[derive(Debug, Clone, Eq, PartialEq, Hash)]
25pub struct Monthly<'a, DS: DateStyler> {
26    display_date: Date,
27    events: DS,
28    show_surrounding: Option<Style>,
29    show_weekday: Option<Style>,
30    show_month: Option<Style>,
31    default_style: Style,
32    block: Option<Block<'a>>,
33}
34
35impl<'a, DS: DateStyler> Monthly<'a, DS> {
36    /// Construct a calendar for the `display_date` and highlight the `events`
37    pub const fn new(display_date: Date, events: DS) -> Self {
38        Self {
39            display_date,
40            events,
41            show_surrounding: None,
42            show_weekday: None,
43            show_month: None,
44            default_style: Style::new(),
45            block: None,
46        }
47    }
48
49    /// Fill the calendar slots for days not in the current month also, this causes each line to be
50    /// completely filled. If there is an event style for a date, this style will be patched with
51    /// the event's style
52    ///
53    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
54    /// your own type that implements [`Into<Style>`]).
55    ///
56    /// [`Color`]: crate::style::Color
57    #[must_use = "method moves the value of self and returns the modified value"]
58    pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
59        self.show_surrounding = Some(style.into());
60        self
61    }
62
63    /// Display a header containing weekday abbreviations
64    ///
65    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
66    /// your own type that implements [`Into<Style>`]).
67    ///
68    /// [`Color`]: crate::style::Color
69    #[must_use = "method moves the value of self and returns the modified value"]
70    pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
71        self.show_weekday = Some(style.into());
72        self
73    }
74
75    /// Display a header containing the month and year
76    ///
77    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
78    /// your own type that implements [`Into<Style>`]).
79    ///
80    /// [`Color`]: crate::style::Color
81    #[must_use = "method moves the value of self and returns the modified value"]
82    pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
83        self.show_month = Some(style.into());
84        self
85    }
86
87    /// How to render otherwise unstyled dates
88    ///
89    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
90    /// your own type that implements [`Into<Style>`]).
91    ///
92    /// [`Color`]: crate::style::Color
93    #[must_use = "method moves the value of self and returns the modified value"]
94    pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
95        self.default_style = style.into();
96        self
97    }
98
99    /// Render the calendar within a [Block]
100    #[must_use = "method moves the value of self and returns the modified value"]
101    pub fn block(mut self, block: Block<'a>) -> Self {
102        self.block = Some(block);
103        self
104    }
105
106    /// Return a style with only the background from the default style
107    const fn default_bg(&self) -> Style {
108        match self.default_style.bg {
109            None => Style::new(),
110            Some(c) => Style::new().bg(c),
111        }
112    }
113
114    /// All logic to style a date goes here.
115    fn format_date(&self, date: Date) -> Span {
116        if date.month() == self.display_date.month() {
117            Span::styled(
118                format!("{:2?}", date.day()),
119                self.default_style.patch(self.events.get_style(date)),
120            )
121        } else {
122            match self.show_surrounding {
123                None => Span::styled("  ", self.default_bg()),
124                Some(s) => {
125                    let style = self
126                        .default_style
127                        .patch(s)
128                        .patch(self.events.get_style(date));
129                    Span::styled(format!("{:2?}", date.day()), style)
130                }
131            }
132        }
133    }
134}
135
136impl<DS: DateStyler> Widget for Monthly<'_, DS> {
137    fn render(self, area: Rect, buf: &mut Buffer) {
138        self.render_ref(area, buf);
139    }
140}
141
142impl<DS: DateStyler> WidgetRef for Monthly<'_, DS> {
143    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
144        self.block.render_ref(area, buf);
145        let inner = self.block.inner_if_some(area);
146        self.render_monthly(inner, buf);
147    }
148}
149
150impl<DS: DateStyler> Monthly<'_, DS> {
151    fn render_monthly(&self, area: Rect, buf: &mut Buffer) {
152        let layout = Layout::vertical([
153            Constraint::Length(self.show_month.is_some().into()),
154            Constraint::Length(self.show_weekday.is_some().into()),
155            Constraint::Fill(1),
156        ]);
157        let [month_header, days_header, days_area] = layout.areas(area);
158
159        // Draw the month name and year
160        if let Some(style) = self.show_month {
161            Line::styled(
162                format!("{} {}", self.display_date.month(), self.display_date.year()),
163                style,
164            )
165            .alignment(Alignment::Center)
166            .render(month_header, buf);
167        }
168
169        // Draw days of week
170        if let Some(style) = self.show_weekday {
171            Span::styled(" Su Mo Tu We Th Fr Sa", style).render(days_header, buf);
172        }
173
174        // Set the start of the calendar to the Sunday before the 1st (or the sunday of the first)
175        let first_of_month = self.display_date.replace_day(1).unwrap();
176        let offset = Duration::days(first_of_month.weekday().number_days_from_sunday().into());
177        let mut curr_day = first_of_month - offset;
178
179        let mut y = days_area.y;
180        // go through all the weeks containing a day in the target month.
181        while curr_day.month() != self.display_date.month().next() {
182            let mut spans = Vec::with_capacity(14);
183            for i in 0..7 {
184                // Draw the gutter. Do it here so we can avoid worrying about
185                // styling the ' ' in the format_date method
186                if i == 0 {
187                    spans.push(Span::styled(" ", Style::default()));
188                } else {
189                    spans.push(Span::styled(" ", self.default_bg()));
190                }
191                spans.push(self.format_date(curr_day));
192                curr_day += Duration::DAY;
193            }
194            if buf.area.height > y {
195                buf.set_line(days_area.x, y, &spans.into(), area.width);
196            }
197            y += 1;
198        }
199    }
200}
201
202/// Provides a method for styling a given date. [Monthly] is generic on this trait, so any type
203/// that implements this trait can be used.
204pub trait DateStyler {
205    /// Given a date, return a style for that date
206    fn get_style(&self, date: Date) -> Style;
207}
208
209/// A simple `DateStyler` based on a [`HashMap`]
210#[derive(Debug, Clone, Eq, PartialEq)]
211pub struct CalendarEventStore(pub HashMap<Date, Style>);
212
213impl CalendarEventStore {
214    /// Construct a store that has the current date styled.
215    ///
216    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
217    /// your own type that implements [`Into<Style>`]).
218    ///
219    /// [`Color`]: crate::style::Color
220    pub fn today<S: Into<Style>>(style: S) -> Self {
221        let mut res = Self::default();
222        res.add(
223            OffsetDateTime::now_local()
224                .unwrap_or_else(|_| OffsetDateTime::now_utc())
225                .date(),
226            style.into(),
227        );
228        res
229    }
230
231    /// Add a date and style to the store
232    ///
233    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
234    /// your own type that implements [`Into<Style>`]).
235    ///
236    /// [`Color`]: crate::style::Color
237    pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
238        // to simplify style nonsense, last write wins
239        let _ = self.0.insert(date, style.into());
240    }
241
242    /// Helper for trait impls
243    fn lookup_style(&self, date: Date) -> Style {
244        self.0.get(&date).copied().unwrap_or_default()
245    }
246}
247
248impl DateStyler for CalendarEventStore {
249    fn get_style(&self, date: Date) -> Style {
250        self.lookup_style(date)
251    }
252}
253
254impl DateStyler for &CalendarEventStore {
255    fn get_style(&self, date: Date) -> Style {
256        self.lookup_style(date)
257    }
258}
259
260impl Default for CalendarEventStore {
261    fn default() -> Self {
262        Self(HashMap::with_capacity(4))
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use time::Month;
269
270    use super::*;
271    use crate::style::Color;
272
273    #[test]
274    fn event_store() {
275        let a = (
276            Date::from_calendar_date(2023, Month::January, 1).unwrap(),
277            Style::default(),
278        );
279        let b = (
280            Date::from_calendar_date(2023, Month::January, 2).unwrap(),
281            Style::default().bg(Color::Red).fg(Color::Blue),
282        );
283        let mut s = CalendarEventStore::default();
284        s.add(b.0, b.1);
285
286        assert_eq!(
287            s.get_style(a.0),
288            a.1,
289            "Date not added to the styler should look up as Style::default()"
290        );
291        assert_eq!(
292            s.get_style(b.0),
293            b.1,
294            "Date added to styler should return the provided style"
295        );
296    }
297
298    #[test]
299    fn test_today() {
300        CalendarEventStore::today(Style::default());
301    }
302}