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