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}