1use 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#[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 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 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 while curr_day.month() != self.display_date.month().next() {
182 let mut spans = Vec::with_capacity(14);
183 for i in 0..7 {
184 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
202pub trait DateStyler {
205 fn get_style(&self, date: Date) -> Style;
207}
208
209#[derive(Debug, Clone, Eq, PartialEq)]
211pub struct CalendarEventStore(pub HashMap<Date, Style>);
212
213impl CalendarEventStore {
214 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 pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
238 let _ = self.0.insert(date, style.into());
240 }
241
242 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}