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