1use 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
16pub 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 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#[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}