Skip to main content

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 crate::fl;
7use crate::widget::{button, column, grid, icon, row, text};
8use apply::Apply;
9use iced::alignment::Vertical;
10use iced_core::{Alignment, Length};
11use jiff::ToSpan;
12use jiff::civil::{Date, Weekday};
13
14/// A widget that displays an interactive calendar.
15pub fn calendar<M>(
16    model: &CalendarModel,
17    on_select: impl Fn(Date) -> M + 'static,
18    on_prev: impl Fn() -> M + 'static,
19    on_next: impl Fn() -> M + 'static,
20    first_day_of_week: Weekday,
21) -> Calendar<'_, M> {
22    Calendar {
23        model,
24        on_select: Box::new(on_select),
25        on_prev: Box::new(on_prev),
26        on_next: Box::new(on_next),
27        first_day_of_week,
28    }
29}
30
31pub fn set_day(date_selected: Date, day: i8) -> Date {
32    date_selected
33        .with()
34        .day(day)
35        .build()
36        .unwrap_or(date_selected)
37}
38
39#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
40pub struct CalendarModel {
41    pub selected: Date,
42    pub visible: Date,
43}
44
45impl CalendarModel {
46    pub fn now() -> Self {
47        let now = jiff::Zoned::now().date();
48        CalendarModel {
49            selected: now,
50            visible: now,
51        }
52    }
53
54    #[inline]
55    pub fn new(selected: Date, visible: Date) -> Self {
56        CalendarModel { selected, visible }
57    }
58
59    pub fn show_prev_month(&mut self) {
60        self.visible = self.visible.checked_sub(1.month()).expect("valid date");
61    }
62
63    pub fn show_next_month(&mut self) {
64        self.visible = self.visible.checked_add(1.month()).expect("valid date");
65    }
66
67    #[inline]
68    pub fn set_prev_month(&mut self) {
69        self.show_prev_month();
70        self.selected = self.visible;
71    }
72
73    #[inline]
74    pub fn set_next_month(&mut self) {
75        self.show_next_month();
76        self.selected = self.visible;
77    }
78
79    #[inline]
80    pub fn set_selected_visible(&mut self, selected: Date) {
81        self.selected = selected;
82        self.visible = self.selected;
83    }
84}
85
86pub struct Calendar<'a, M> {
87    model: &'a CalendarModel,
88    on_select: Box<dyn Fn(Date) -> M>,
89    on_prev: Box<dyn Fn() -> M>,
90    on_next: Box<dyn Fn() -> M>,
91    first_day_of_week: Weekday,
92}
93
94impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
95where
96    Message: Clone + 'static,
97{
98    fn from(this: Calendar<'a, Message>) -> Self {
99        macro_rules! translate_month {
100            ($month:expr, $year:expr) => {{
101                match $month {
102                    1 => fl!("january", year = $year),
103                    2 => fl!("february", year = $year),
104                    3 => fl!("march", year = $year),
105                    4 => fl!("april", year = $year),
106                    5 => fl!("may", year = $year),
107                    6 => fl!("june", year = $year),
108                    7 => fl!("july", year = $year),
109                    8 => fl!("august", year = $year),
110                    9 => fl!("september", year = $year),
111                    10 => fl!("october", year = $year),
112                    11 => fl!("november", year = $year),
113                    12 => fl!("december", year = $year),
114                    _ => unreachable!(),
115                }
116            }};
117        }
118        macro_rules! translate_weekday {
119            ($weekday:expr, short) => {{
120                match $weekday {
121                    Weekday::Monday => fl!("mon"),
122                    Weekday::Tuesday => fl!("tue"),
123                    Weekday::Wednesday => fl!("wed"),
124                    Weekday::Thursday => fl!("thu"),
125                    Weekday::Friday => fl!("fri"),
126                    Weekday::Saturday => fl!("sat"),
127                    Weekday::Sunday => fl!("sun"),
128                }
129            }};
130            ($weekday:expr, long) => {{
131                match $weekday {
132                    Weekday::Monday => fl!("monday"),
133                    Weekday::Tuesday => fl!("tuesday"),
134                    Weekday::Wednesday => fl!("wednesday"),
135                    Weekday::Thursday => fl!("thursday"),
136                    Weekday::Friday => fl!("friday"),
137                    Weekday::Saturday => fl!("saturday"),
138                    Weekday::Sunday => fl!("sunday"),
139                }
140            }};
141        }
142
143        let date = text(translate_month!(
144            this.model.visible.month(),
145            this.model.visible.year()
146        ))
147        .size(18);
148
149        let day = text::body(translate_weekday!(this.model.visible.weekday(), long));
150
151        let month_controls = row::with_capacity(2)
152            .spacing(8)
153            .push(
154                icon::from_name("go-previous-symbolic")
155                    .apply(button::icon)
156                    .on_press((this.on_prev)()),
157            )
158            .push(
159                icon::from_name("go-next-symbolic")
160                    .apply(button::icon)
161                    .on_press((this.on_next)()),
162            );
163
164        // Calendar
165        let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill);
166
167        let mut first_day_of_week = this.first_day_of_week;
168        for _ in 0..7 {
169            calendar_grid = calendar_grid.push(
170                text::caption(translate_weekday!(first_day_of_week, short))
171                    .width(Length::Fixed(44.0))
172                    .align_x(Alignment::Center),
173            );
174
175            first_day_of_week = first_day_of_week.next();
176        }
177        calendar_grid = calendar_grid.insert_row();
178
179        let first = get_calendar_first(
180            this.model.visible.year(),
181            this.model.visible.month(),
182            this.first_day_of_week,
183        );
184
185        let today = jiff::Zoned::now().date();
186        for i in 0..42 {
187            if i > 0 && i % 7 == 0 {
188                calendar_grid = calendar_grid.insert_row();
189            }
190
191            let date = first
192                .checked_add(i.days())
193                .expect("valid date in calendar range");
194            let is_currently_viewed_month =
195                date.first_of_month() == this.model.visible.first_of_month();
196            let is_currently_selected_month =
197                date.first_of_month() == this.model.selected.first_of_month();
198            let is_currently_selected_day =
199                date.day() == this.model.selected.day() && is_currently_selected_month;
200            let is_today = date == today;
201
202            calendar_grid = calendar_grid.push(date_button(
203                date,
204                is_currently_viewed_month,
205                is_currently_selected_day,
206                is_today,
207                &this.on_select,
208            ));
209        }
210
211        let content_list = column::with_children([
212            row::with_children([
213                column([date.into(), day.into()]).into(),
214                crate::widget::space::horizontal()
215                    .width(Length::Fill)
216                    .into(),
217                month_controls.into(),
218            ])
219            .align_y(Vertical::Center)
220            .padding([12, 20])
221            .into(),
222            calendar_grid.into(),
223        ])
224        .width(360)
225        .padding([8, 0]);
226
227        Self::new(content_list)
228    }
229}
230
231fn date_button<Message: Clone + 'static>(
232    date: Date,
233    is_currently_viewed_month: bool,
234    is_currently_selected_day: bool,
235    is_today: bool,
236    on_select: &dyn Fn(Date) -> Message,
237) -> crate::widget::Button<'static, Message> {
238    let style = if is_currently_selected_day {
239        button::ButtonClass::Suggested
240    } else if is_today {
241        button::ButtonClass::Standard
242    } else {
243        button::ButtonClass::Text
244    };
245
246    let button = button::custom(text(format!("{}", date.day())).center())
247        .class(style)
248        .height(Length::Fixed(44.0))
249        .width(Length::Fixed(44.0));
250
251    if is_currently_viewed_month {
252        button.on_press((on_select)(set_day(date, date.day())))
253    } else {
254        button
255    }
256}
257
258/// Gets the first date that will be visible on the calendar
259#[must_use]
260pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date {
261    let date = Date::new(year, month, 1).expect("valid date");
262    let num_days = date.weekday().since(from_weekday);
263    date.checked_sub(num_days.days()).expect("valid date")
264}