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::ToSpan;
12use jiff::civil::{Date, Weekday};
13
14pub 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 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#[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}