1use chrono::{
2 Datelike,
3 Duration,
4 Local,
5 Months,
6 NaiveDate,
7};
8use freya_core::prelude::*;
9use torin::{
10 content::Content,
11 gaps::Gaps,
12 prelude::Alignment,
13 size::Size,
14};
15
16use crate::{
17 button::{
18 Button,
19 ButtonColorsThemePartialExt,
20 ButtonLayoutThemePartialExt,
21 },
22 define_theme,
23 get_theme,
24 icons::arrow::ArrowIcon,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WeekStart {
29 Sunday,
30 Monday,
31}
32
33define_theme! {
34 %[component]
35 pub Calendar {
36 %[fields]
37 background: Color,
38 day_background: Color,
39 day_hover_background: Color,
40 day_selected_background: Color,
41 color: Color,
42 day_other_month_color: Color,
43 header_color: Color,
44 corner_radius: CornerRadius,
45 padding: Gaps,
46 day_corner_radius: CornerRadius,
47 nav_button_hover_background: Color,
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct CalendarDate {
53 pub year: i32,
54 pub month: u32,
55 pub day: u32,
56}
57
58impl CalendarDate {
59 pub fn new(year: i32, month: u32, day: u32) -> Self {
60 Self { year, month, day }
61 }
62
63 pub fn now() -> Self {
65 Local::now().date_naive().into()
66 }
67}
68
69impl From<NaiveDate> for CalendarDate {
70 fn from(date: NaiveDate) -> Self {
71 Self::new(date.year(), date.month(), date.day())
72 }
73}
74
75#[cfg_attr(feature = "docs", doc = embed_doc_image::embed_image!("gallery_calendar", "images/gallery_calendar.png"))]
101#[derive(Clone, PartialEq)]
102pub struct Calendar {
103 pub(crate) theme: Option<CalendarThemePartial>,
104 selected: Option<CalendarDate>,
105 view_date: CalendarDate,
106 week_start: WeekStart,
107 on_change: Option<EventHandler<CalendarDate>>,
108 on_view_change: Option<EventHandler<CalendarDate>>,
109 key: DiffKey,
110}
111
112impl Default for Calendar {
113 fn default() -> Self {
114 Self::new()
115 }
116}
117
118impl Calendar {
119 pub fn new() -> Self {
120 Self {
121 theme: None,
122 selected: None,
123 view_date: CalendarDate::now(),
124 week_start: WeekStart::Monday,
125 on_change: None,
126 on_view_change: None,
127 key: DiffKey::None,
128 }
129 }
130
131 pub fn selected(mut self, selected: Option<CalendarDate>) -> Self {
132 self.selected = selected;
133 self
134 }
135
136 pub fn view_date(mut self, view_date: CalendarDate) -> Self {
137 self.view_date = view_date;
138 self
139 }
140
141 pub fn week_start(mut self, week_start: WeekStart) -> Self {
143 self.week_start = week_start;
144 self
145 }
146
147 pub fn on_change(mut self, on_change: impl Into<EventHandler<CalendarDate>>) -> Self {
148 self.on_change = Some(on_change.into());
149 self
150 }
151
152 pub fn on_view_change(mut self, on_view_change: impl Into<EventHandler<CalendarDate>>) -> Self {
153 self.on_view_change = Some(on_view_change.into());
154 self
155 }
156}
157
158impl KeyExt for Calendar {
159 fn write_key(&mut self) -> &mut DiffKey {
160 &mut self.key
161 }
162}
163
164impl Component for Calendar {
165 fn render(&self) -> impl IntoElement {
166 let CalendarTheme {
167 background,
168 day_background,
169 day_hover_background,
170 day_selected_background,
171 color,
172 day_other_month_color,
173 header_color,
174 corner_radius,
175 padding,
176 day_corner_radius,
177 nav_button_hover_background,
178 } = get_theme!(&self.theme, CalendarThemePreference, "calendar");
179
180 let first_day = NaiveDate::from_ymd_opt(self.view_date.year, self.view_date.month, 1)
181 .unwrap_or_default();
182 let prev_month = first_day
183 .checked_sub_months(Months::new(1))
184 .unwrap_or(first_day);
185 let next_month = first_day
186 .checked_add_months(Months::new(1))
187 .unwrap_or(first_day);
188 let days_in_month = next_month.pred_opt().map(|d| d.day()).unwrap_or(30);
189
190 let (leading, weekday_names) = match self.week_start {
191 WeekStart::Sunday => (
192 first_day.weekday().num_days_from_sunday(),
193 ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
194 ),
195 WeekStart::Monday => (
196 first_day.weekday().num_days_from_monday(),
197 ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
198 ),
199 };
200 let total_cells = (leading + days_in_month).div_ceil(7) * 7;
201
202 let nav_button = |target: NaiveDate, rotate: f32| {
203 let on_view_change = self.on_view_change.clone();
204 Button::new()
205 .flat()
206 .width(Size::px(32.))
207 .height(Size::px(32.))
208 .hover_background(nav_button_hover_background)
209 .on_press(move |_: Event<PressEventData>| {
210 if let Some(handler) = &on_view_change {
211 handler.call(target.into());
212 }
213 })
214 .child(
215 ArrowIcon::new()
216 .fill(color)
217 .width(Size::px(16.))
218 .height(Size::px(16.))
219 .rotate(rotate),
220 )
221 };
222
223 let header_cells = weekday_names.iter().map(|name| {
224 rect()
225 .width(Size::px(36.))
226 .height(Size::px(36.))
227 .center()
228 .child(label().text(*name).color(header_color).font_size(12.))
229 .into()
230 });
231
232 let day_cells = (0..total_cells).map(|i| {
233 let date = first_day
234 .checked_add_signed(Duration::days(i as i64 - leading as i64))
235 .unwrap_or(first_day);
236 let in_month = date.month() == first_day.month();
237 let is_selected = in_month && self.selected == Some(date.into());
238 let on_change = self.on_change.clone();
239
240 let (day_color, bg, hover_bg) = if is_selected {
241 (color, day_selected_background, day_selected_background)
242 } else if in_month {
243 (color, day_background, day_hover_background)
244 } else {
245 (
246 day_other_month_color,
247 Color::TRANSPARENT,
248 Color::TRANSPARENT,
249 )
250 };
251
252 Button::new()
253 .key(date)
254 .flat()
255 .padding(0.)
256 .enabled(in_month)
257 .width(Size::px(36.))
258 .height(Size::px(36.))
259 .background(bg)
260 .hover_background(hover_bg)
261 .corner_radius(day_corner_radius)
262 .maybe(in_month, |el| {
263 el.map(on_change, |el, on_change| {
264 el.on_press(move |_| on_change.call(date.into()))
265 })
266 })
267 .child(
268 label()
269 .text(date.day().to_string())
270 .color(day_color)
271 .font_size(14.),
272 )
273 .into()
274 });
275
276 rect()
277 .background(background)
278 .corner_radius(corner_radius)
279 .padding(padding)
280 .width(Size::px(280.))
281 .child(
282 rect()
283 .horizontal()
284 .width(Size::fill())
285 .padding((0., 0., 8., 0.))
286 .cross_align(Alignment::center())
287 .content(Content::flex())
288 .child(nav_button(prev_month, 90.))
289 .child(
290 label()
291 .width(Size::flex(1.))
292 .text_align(TextAlign::Center)
293 .text(first_day.format("%B %Y").to_string())
294 .color(header_color)
295 .max_lines(1)
296 .font_size(16.),
297 )
298 .child(nav_button(next_month, -90.)),
299 )
300 .child(
301 rect()
302 .horizontal()
303 .content(Content::wrap())
304 .width(Size::fill())
305 .children(header_cells)
306 .children(day_cells),
307 )
308 }
309
310 fn render_key(&self) -> DiffKey {
311 self.key.clone().or(self.default_key())
312 }
313}