cosmic/widget/
calendar.rs

1// Copyright 2024 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! A widget that displays an interactive calendar.
5
6use std::cmp;
7
8use crate::iced_core::{Alignment, Length, Padding};
9use crate::widget::{Grid, button, column, grid, icon, row, text};
10use chrono::{Datelike, Days, Local, Months, NaiveDate, Weekday};
11
12/// A widget that displays an interactive calendar.
13pub fn calendar<M>(
14    model: &CalendarModel,
15    on_select: impl Fn(NaiveDate) -> M + 'static,
16    on_prev: impl Fn() -> M + 'static,
17    on_next: impl Fn() -> M + 'static,
18    first_day_of_week: Weekday,
19) -> Calendar<M> {
20    Calendar {
21        model,
22        on_select: Box::new(on_select),
23        on_prev: Box::new(on_prev),
24        on_next: Box::new(on_next),
25        first_day_of_week,
26    }
27}
28
29pub fn set_day(date_selected: NaiveDate, day: u32) -> NaiveDate {
30    let current = date_selected.day();
31
32    let new_date = match current.cmp(&day) {
33        cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)),
34
35        cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)),
36
37        _ => None,
38    };
39
40    if let Some(new) = new_date {
41        new
42    } else {
43        date_selected
44    }
45}
46
47#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
48pub struct CalendarModel {
49    pub selected: NaiveDate,
50    pub visible: NaiveDate,
51}
52
53impl CalendarModel {
54    pub fn now() -> Self {
55        let now = Local::now();
56        let naive_now = NaiveDate::from(now.naive_local());
57        CalendarModel {
58            selected: naive_now,
59            visible: naive_now,
60        }
61    }
62
63    #[inline]
64    pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self {
65        CalendarModel { selected, visible }
66    }
67
68    pub fn show_prev_month(&mut self) {
69        let prev_month_date = self
70            .visible
71            .checked_sub_months(Months::new(1))
72            .expect("valid naivedate");
73
74        self.visible = prev_month_date;
75    }
76
77    pub fn show_next_month(&mut self) {
78        let next_month_date = self
79            .visible
80            .checked_add_months(Months::new(1))
81            .expect("valid naivedate");
82
83        self.visible = next_month_date;
84    }
85
86    #[inline]
87    pub fn set_prev_month(&mut self) {
88        self.show_prev_month();
89        self.selected = self.visible;
90    }
91
92    #[inline]
93    pub fn set_next_month(&mut self) {
94        self.show_next_month();
95        self.selected = self.visible;
96    }
97
98    #[inline]
99    pub fn set_selected_visible(&mut self, selected: NaiveDate) {
100        self.selected = selected;
101        self.visible = self.selected;
102    }
103}
104
105pub struct Calendar<'a, M> {
106    model: &'a CalendarModel,
107    on_select: Box<dyn Fn(NaiveDate) -> M>,
108    on_prev: Box<dyn Fn() -> M>,
109    on_next: Box<dyn Fn() -> M>,
110    first_day_of_week: Weekday,
111}
112
113impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
114where
115    Message: Clone + 'static,
116{
117    fn from(this: Calendar<'a, Message>) -> Self {
118        let date = text(this.model.visible.format("%B %Y").to_string()).size(18);
119
120        let month_controls = row::with_capacity(2)
121            .push(
122                button::icon(icon::from_name("go-previous-symbolic"))
123                    .padding([0, 12])
124                    .on_press((this.on_prev)()),
125            )
126            .push(
127                button::icon(icon::from_name("go-next-symbolic"))
128                    .padding([0, 12])
129                    .on_press((this.on_next)()),
130            );
131
132        // Calender
133        let mut calendar_grid: Grid<'_, Message> =
134            grid().padding([0, 12].into()).width(Length::Fill);
135
136        let mut first_day_of_week = this.first_day_of_week;
137        for _ in 0..7 {
138            calendar_grid = calendar_grid.push(
139                text(first_day_of_week.to_string())
140                    .size(12)
141                    .width(Length::Fixed(36.0))
142                    .align_x(Alignment::Center),
143            );
144
145            first_day_of_week = first_day_of_week.succ();
146        }
147        calendar_grid = calendar_grid.insert_row();
148
149        let monday = get_calender_first(
150            this.model.visible.year(),
151            this.model.visible.month(),
152            first_day_of_week,
153        );
154        let mut day_iter = monday.iter_days();
155        for i in 0..42 {
156            if i > 0 && i % 7 == 0 {
157                calendar_grid = calendar_grid.insert_row();
158            }
159
160            let date = day_iter.next().unwrap();
161            let is_currently_viewed_month = date.month() == this.model.visible.month()
162                && date.year_ce() == this.model.visible.year_ce();
163            let is_currently_selected_month = date.month() == this.model.selected.month()
164                && date.year_ce() == this.model.selected.year_ce();
165            let is_currently_selected_day =
166                date.day() == this.model.selected.day() && is_currently_selected_month;
167
168            calendar_grid = calendar_grid.push(date_button(
169                date,
170                is_currently_viewed_month,
171                is_currently_selected_day,
172                &this.on_select,
173            ));
174        }
175
176        let content_list = column::with_children(vec![
177            row::with_children(vec![
178                date.into(),
179                crate::widget::Space::with_width(Length::Fill).into(),
180                month_controls.into(),
181            ])
182            .padding([12, 20])
183            .into(),
184            calendar_grid.into(),
185            padded_control(crate::widget::divider::horizontal::default()).into(),
186        ])
187        .width(315)
188        .padding([8, 0]);
189
190        Self::new(content_list)
191    }
192}
193
194fn date_button<Message: Clone + 'static>(
195    date: NaiveDate,
196    is_currently_viewed_month: bool,
197    is_currently_selected_day: bool,
198    on_select: &dyn Fn(NaiveDate) -> Message,
199) -> crate::widget::Button<'static, Message> {
200    let style = if is_currently_selected_day {
201        button::ButtonClass::Suggested
202    } else {
203        button::ButtonClass::Text
204    };
205
206    let button = button::custom(text(format!("{}", date.day())).center())
207        .class(style)
208        .height(Length::Fixed(36.0))
209        .width(Length::Fixed(36.0));
210
211    if is_currently_viewed_month {
212        button.on_press((on_select)(set_day(date, date.day())))
213    } else {
214        button
215    }
216}
217
218/// Gets the first date that will be visible on the calender
219#[must_use]
220pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate {
221    let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
222    let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from
223    date.checked_sub_days(Days::new(num_days as u64)).unwrap()
224}
225
226// TODO: Refactor to use same function from applet module.
227fn padded_control<'a, Message>(
228    content: impl Into<crate::Element<'a, Message>>,
229) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> {
230    crate::widget::container(content)
231        .padding(menu_control_padding())
232        .width(Length::Fill)
233}
234
235#[inline]
236fn menu_control_padding() -> Padding {
237    let guard = crate::theme::THEME.lock().unwrap();
238    let cosmic = guard.cosmic();
239    [cosmic.space_xxs(), cosmic.space_m()].into()
240}