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