1use std::cmp;
7
8use crate::fl;
9use crate::iced_core::{Alignment, Length, Padding};
10use crate::widget::{Grid, button, column, grid, icon, row, text};
11use apply::Apply;
12use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
13
14pub fn calendar<M>(
16    model: &CalendarModel,
17    on_select: impl Fn(NaiveDate) -> 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: NaiveDate, day: u32) -> NaiveDate {
32    let current = date_selected.day();
33
34    let new_date = match current.cmp(&day) {
35        cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)),
36
37        cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)),
38
39        _ => None,
40    };
41
42    if let Some(new) = new_date {
43        new
44    } else {
45        date_selected
46    }
47}
48
49#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
50pub struct CalendarModel {
51    pub selected: NaiveDate,
52    pub visible: NaiveDate,
53}
54
55impl CalendarModel {
56    pub fn now() -> Self {
57        let now = Local::now();
58        let naive_now = NaiveDate::from(now.naive_local());
59        CalendarModel {
60            selected: naive_now,
61            visible: naive_now,
62        }
63    }
64
65    #[inline]
66    pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self {
67        CalendarModel { selected, visible }
68    }
69
70    pub fn show_prev_month(&mut self) {
71        let prev_month_date = self
72            .visible
73            .checked_sub_months(Months::new(1))
74            .expect("valid naivedate");
75
76        self.visible = prev_month_date;
77    }
78
79    pub fn show_next_month(&mut self) {
80        let next_month_date = self
81            .visible
82            .checked_add_months(Months::new(1))
83            .expect("valid naivedate");
84
85        self.visible = next_month_date;
86    }
87
88    #[inline]
89    pub fn set_prev_month(&mut self) {
90        self.show_prev_month();
91        self.selected = self.visible;
92    }
93
94    #[inline]
95    pub fn set_next_month(&mut self) {
96        self.show_next_month();
97        self.selected = self.visible;
98    }
99
100    #[inline]
101    pub fn set_selected_visible(&mut self, selected: NaiveDate) {
102        self.selected = selected;
103        self.visible = self.selected;
104    }
105}
106
107pub struct Calendar<'a, M> {
108    model: &'a CalendarModel,
109    on_select: Box<dyn Fn(NaiveDate) -> M>,
110    on_prev: Box<dyn Fn() -> M>,
111    on_next: Box<dyn Fn() -> M>,
112    first_day_of_week: Weekday,
113}
114
115impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
116where
117    Message: Clone + 'static,
118{
119    fn from(this: Calendar<'a, Message>) -> Self {
120        macro_rules! icon {
121            ($name:expr, $on_press:expr) => {{
122                #[cfg(target_os = "linux")]
123                let icon = { icon::from_name($name).apply(button::icon) };
124                #[cfg(not(target_os = "linux"))]
125                let icon = {
126                    icon::from_svg_bytes(include_bytes!(concat!("../../res/icons/", $name, ".svg")))
127                        .symbolic(true)
128                        .apply(button::icon)
129                };
130                icon.padding([0, 12]).on_press($on_press)
131            }};
132        }
133        macro_rules! translate_month {
134            ($month:expr, $year:expr) => {{
135                match $month {
136                    chrono::Month::January => fl!("january", year = $year),
137                    chrono::Month::February => fl!("february", year = $year),
138                    chrono::Month::March => fl!("march", year = $year),
139                    chrono::Month::April => fl!("april", year = $year),
140                    chrono::Month::May => fl!("may", year = $year),
141                    chrono::Month::June => fl!("june", year = $year),
142                    chrono::Month::July => fl!("july", year = $year),
143                    chrono::Month::August => fl!("august", year = $year),
144                    chrono::Month::September => fl!("september", year = $year),
145                    chrono::Month::October => fl!("october", year = $year),
146                    chrono::Month::November => fl!("november", year = $year),
147                    chrono::Month::December => fl!("december", year = $year),
148                }
149            }};
150        }
151        macro_rules! translate_weekday {
152            ($weekday:expr) => {{
153                match $weekday {
154                    Weekday::Mon => fl!("monday"),
155                    Weekday::Tue => fl!("tuesday"),
156                    Weekday::Wed => fl!("wednesday"),
157                    Weekday::Thu => fl!("thursday"),
158                    Weekday::Fri => fl!("friday"),
159                    Weekday::Sat => fl!("saturday"),
160                    Weekday::Sun => fl!("sunday"),
161                }
162            }};
163        }
164
165        let date = text(translate_month!(
166            Month::try_from(this.model.visible.month() as u8)
167                .expect("Previously valid month is suddenly invalid"),
168            this.model.visible.year()
169        ))
170        .size(18);
171
172        let month_controls = row::with_capacity(2)
173            .push(icon!("go-previous-symbolic", (this.on_prev)()))
174            .push(icon!("go-next-symbolic", (this.on_next)()));
175
176        let mut calendar_grid: Grid<'_, Message> =
178            grid().padding([0, 12].into()).width(Length::Fill);
179
180        let mut first_day_of_week = this.first_day_of_week;
181        for _ in 0..7 {
182            calendar_grid = calendar_grid.push(
183                text(translate_weekday!(first_day_of_week))
184                    .size(12)
185                    .width(Length::Fixed(36.0))
186                    .align_x(Alignment::Center),
187            );
188
189            first_day_of_week = first_day_of_week.succ();
190        }
191        calendar_grid = calendar_grid.insert_row();
192
193        let monday = get_calender_first(
194            this.model.visible.year(),
195            this.model.visible.month(),
196            first_day_of_week,
197        );
198        let mut day_iter = monday.iter_days();
199        for i in 0..42 {
200            if i > 0 && i % 7 == 0 {
201                calendar_grid = calendar_grid.insert_row();
202            }
203
204            let date = day_iter.next().unwrap();
205            let is_currently_viewed_month = date.month() == this.model.visible.month()
206                && date.year_ce() == this.model.visible.year_ce();
207            let is_currently_selected_month = date.month() == this.model.selected.month()
208                && date.year_ce() == this.model.selected.year_ce();
209            let is_currently_selected_day =
210                date.day() == this.model.selected.day() && is_currently_selected_month;
211
212            calendar_grid = calendar_grid.push(date_button(
213                date,
214                is_currently_viewed_month,
215                is_currently_selected_day,
216                &this.on_select,
217            ));
218        }
219
220        let content_list = column::with_children([
221            row::with_children([
222                date.into(),
223                crate::widget::Space::with_width(Length::Fill).into(),
224                month_controls.into(),
225            ])
226            .padding([12, 20])
227            .into(),
228            calendar_grid.into(),
229            padded_control(crate::widget::divider::horizontal::default()).into(),
230        ])
231        .width(315)
232        .padding([8, 0]);
233
234        Self::new(content_list)
235    }
236}
237
238fn date_button<Message: Clone + 'static>(
239    date: NaiveDate,
240    is_currently_viewed_month: bool,
241    is_currently_selected_day: bool,
242    on_select: &dyn Fn(NaiveDate) -> Message,
243) -> crate::widget::Button<'static, Message> {
244    let style = if is_currently_selected_day {
245        button::ButtonClass::Suggested
246    } else {
247        button::ButtonClass::Text
248    };
249
250    let button = button::custom(text(format!("{}", date.day())).center())
251        .class(style)
252        .height(Length::Fixed(36.0))
253        .width(Length::Fixed(36.0));
254
255    if is_currently_viewed_month {
256        button.on_press((on_select)(set_day(date, date.day())))
257    } else {
258        button
259    }
260}
261
262#[must_use]
264pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate {
265    let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
266    let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; date.checked_sub_days(Days::new(num_days as u64)).unwrap()
268}
269
270fn padded_control<'a, Message>(
272    content: impl Into<crate::Element<'a, Message>>,
273) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> {
274    crate::widget::container(content)
275        .padding(menu_control_padding())
276        .width(Length::Fill)
277}
278
279#[inline]
280fn menu_control_padding() -> Padding {
281    let guard = crate::theme::THEME.lock().unwrap();
282    let cosmic = guard.cosmic();
283    [cosmic.space_xxs(), cosmic.space_m()].into()
284}