1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum WeekStart {
4 Sunday,
5 Monday,
6}
7
8use chrono::{
9 Datelike,
10 Local,
11 Month,
12 NaiveDate,
13};
14use freya_core::prelude::*;
15use torin::{
16 content::Content,
17 gaps::Gaps,
18 prelude::Alignment,
19 size::Size,
20};
21
22use crate::{
23 button::{
24 Button,
25 ButtonColorsThemePartialExt,
26 ButtonLayoutThemePartialExt,
27 },
28 define_theme,
29 get_theme,
30 icons::arrow::ArrowIcon,
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)]
53pub struct CalendarDate {
54 pub year: i32,
55 pub month: u32,
56 pub day: u32,
57}
58
59impl CalendarDate {
60 pub fn new(year: i32, month: u32, day: u32) -> Self {
61 Self { year, month, day }
62 }
63
64 pub fn now() -> Self {
66 let today = Local::now().date_naive();
67 Self {
68 year: today.year(),
69 month: today.month(),
70 day: today.day(),
71 }
72 }
73
74 fn days_in_month(year: i32, month: u32) -> u32 {
76 let next_month = if month == 12 { 1 } else { month + 1 };
77 let next_year = if month == 12 { year + 1 } else { year };
78 NaiveDate::from_ymd_opt(next_year, next_month, 1)
79 .and_then(|d| d.pred_opt())
80 .map(|d| d.day())
81 .unwrap_or(30)
82 }
83
84 fn first_day_of_month(year: i32, month: u32, week_start: WeekStart) -> u32 {
86 NaiveDate::from_ymd_opt(year, month, 1)
87 .map(|d| match week_start {
88 WeekStart::Sunday => d.weekday().num_days_from_sunday(),
89 WeekStart::Monday => d.weekday().num_days_from_monday(),
90 })
91 .unwrap_or(0)
92 }
93
94 fn month_name(month: u32) -> String {
96 Month::try_from(month as u8)
97 .map(|m| m.name().to_string())
98 .unwrap_or_else(|_| "Unknown".to_string())
99 }
100}
101
102#[cfg_attr(feature = "docs", doc = embed_doc_image::embed_image!("gallery_calendar", "images/gallery_calendar.png"))]
128#[derive(Clone, PartialEq)]
129pub struct Calendar {
130 pub(crate) theme: Option<CalendarThemePartial>,
131 selected: Option<CalendarDate>,
132 view_date: CalendarDate,
133 week_start: WeekStart,
134 on_change: Option<EventHandler<CalendarDate>>,
135 on_view_change: Option<EventHandler<CalendarDate>>,
136 key: DiffKey,
137}
138
139impl Default for Calendar {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl Calendar {
146 pub fn new() -> Self {
147 Self {
148 theme: None,
149 selected: None,
150 view_date: CalendarDate::now(),
151 week_start: WeekStart::Monday,
152 on_change: None,
153 on_view_change: None,
154 key: DiffKey::None,
155 }
156 }
157
158 pub fn selected(mut self, selected: Option<CalendarDate>) -> Self {
159 self.selected = selected;
160 self
161 }
162
163 pub fn view_date(mut self, view_date: CalendarDate) -> Self {
164 self.view_date = view_date;
165 self
166 }
167
168 pub fn week_start(mut self, week_start: WeekStart) -> Self {
170 self.week_start = week_start;
171 self
172 }
173
174 pub fn on_change(mut self, on_change: impl Into<EventHandler<CalendarDate>>) -> Self {
175 self.on_change = Some(on_change.into());
176 self
177 }
178
179 pub fn on_view_change(mut self, on_view_change: impl Into<EventHandler<CalendarDate>>) -> Self {
180 self.on_view_change = Some(on_view_change.into());
181 self
182 }
183}
184
185impl KeyExt for Calendar {
186 fn write_key(&mut self) -> &mut DiffKey {
187 &mut self.key
188 }
189}
190
191impl Component for Calendar {
192 fn render(&self) -> impl IntoElement {
193 let theme = get_theme!(&self.theme, CalendarThemePreference, "calendar");
194
195 let CalendarTheme {
196 background,
197 day_background,
198 day_hover_background,
199 day_selected_background,
200 color,
201 day_other_month_color,
202 header_color,
203 corner_radius,
204 padding,
205 day_corner_radius,
206 nav_button_hover_background,
207 } = theme;
208
209 let view_year = self.view_date.year;
210 let view_month = self.view_date.month;
211
212 let days_in_month = CalendarDate::days_in_month(view_year, view_month);
213 let first_day = CalendarDate::first_day_of_month(view_year, view_month, self.week_start);
214 let month_name = CalendarDate::month_name(view_month);
215
216 let prev_month = if view_month == 1 { 12 } else { view_month - 1 };
217 let prev_year = if view_month == 1 {
218 view_year - 1
219 } else {
220 view_year
221 };
222 let days_in_prev_month = CalendarDate::days_in_month(prev_year, prev_month);
223
224 let on_change = self.on_change.clone();
225 let on_view_change = self.on_view_change.clone();
226 let selected = self.selected;
227
228 let on_prev = EventHandler::from({
229 let on_view_change = on_view_change.clone();
230 move |_: Event<PressEventData>| {
231 if let Some(handler) = &on_view_change {
232 let new_month = if view_month == 1 { 12 } else { view_month - 1 };
233 let new_year = if view_month == 1 {
234 view_year - 1
235 } else {
236 view_year
237 };
238 handler.call(CalendarDate::new(new_year, new_month, 1));
239 }
240 }
241 });
242
243 let on_next = EventHandler::from(move |_: Event<PressEventData>| {
244 if let Some(handler) = &on_view_change {
245 let new_month = if view_month == 12 { 1 } else { view_month + 1 };
246 let new_year = if view_month == 12 {
247 view_year + 1
248 } else {
249 view_year
250 };
251 handler.call(CalendarDate::new(new_year, new_month, 1));
252 }
253 });
254
255 let nav_button = |on_press: EventHandler<Event<PressEventData>>, rotate| {
256 Button::new()
257 .flat()
258 .width(Size::px(32.))
259 .height(Size::px(32.))
260 .hover_background(nav_button_hover_background)
261 .on_press(on_press)
262 .child(
263 ArrowIcon::new()
264 .fill(color)
265 .width(Size::px(16.))
266 .height(Size::px(16.))
267 .rotate(rotate),
268 )
269 };
270
271 let weekday_names = match self.week_start {
272 WeekStart::Sunday => ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"],
273 WeekStart::Monday => ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
274 };
275
276 let header_cells = weekday_names.iter().map(|day_name| {
277 rect()
278 .width(Size::px(36.))
279 .height(Size::px(36.))
280 .center()
281 .child(label().text(*day_name).color(header_color).font_size(12.))
282 .into()
283 });
284
285 let total_cells = (first_day + days_in_month).div_ceil(7) * 7;
286 let day_cells = (0..total_cells).map(|i| {
287 let current_day = i as i32 - first_day as i32 + 1;
288
289 let (day, day_color, enabled) = if current_day < 1 {
290 let day = (days_in_prev_month as i32 + current_day) as u32;
291 (day, day_other_month_color, false)
292 } else if current_day as u32 > days_in_month {
293 let day = current_day as u32 - days_in_month;
294 (day, day_other_month_color, false)
295 } else {
296 (current_day as u32, color, true)
297 };
298
299 let date = CalendarDate::new(view_year, view_month, current_day as u32);
300 let is_selected = enabled && selected == Some(date);
301 let on_change = on_change.clone();
302
303 let (bg, hover_bg) = if is_selected {
304 (day_selected_background, day_selected_background)
305 } else if enabled {
306 (day_background, day_hover_background)
307 } else {
308 (Color::TRANSPARENT, Color::TRANSPARENT)
309 };
310
311 CalendarDay::new()
312 .day(day)
313 .background(bg)
314 .hover_background(hover_bg)
315 .color(day_color)
316 .corner_radius(day_corner_radius)
317 .enabled(enabled)
318 .maybe(enabled, |el| {
319 el.map(on_change, |el, on_change| {
320 el.on_press(move |_| on_change.call(date))
321 })
322 })
323 .into()
324 });
325
326 rect()
327 .background(background)
328 .corner_radius(corner_radius)
329 .padding(padding)
330 .width(Size::px(280.))
331 .child(
332 rect()
333 .horizontal()
334 .width(Size::fill())
335 .padding((0., 0., 8., 0.))
336 .cross_align(Alignment::center())
337 .content(Content::flex())
338 .child(nav_button(on_prev, 90.))
339 .child(
340 label()
341 .width(Size::flex(1.))
342 .text_align(TextAlign::Center)
343 .text(format!("{} {}", month_name, view_year))
344 .color(header_color)
345 .max_lines(1)
346 .font_size(16.),
347 )
348 .child(nav_button(on_next, -90.)),
349 )
350 .child(
351 rect()
352 .horizontal()
353 .content(Content::wrap())
354 .width(Size::fill())
355 .children(header_cells)
356 .children(day_cells),
357 )
358 }
359
360 fn render_key(&self) -> DiffKey {
361 self.key.clone().or(self.default_key())
362 }
363}
364
365#[derive(Clone, PartialEq)]
366struct CalendarDay {
367 day: u32,
368 background: Color,
369 hover_background: Color,
370 color: Color,
371 corner_radius: CornerRadius,
372 on_press: Option<EventHandler<Event<PressEventData>>>,
373 enabled: bool,
374 key: DiffKey,
375}
376
377impl CalendarDay {
378 fn new() -> Self {
379 Self {
380 day: 1,
381 background: Color::TRANSPARENT,
382 hover_background: Color::TRANSPARENT,
383 color: Color::BLACK,
384 corner_radius: CornerRadius::default(),
385 on_press: None,
386 enabled: true,
387 key: DiffKey::None,
388 }
389 }
390
391 fn day(mut self, day: u32) -> Self {
392 self.day = day;
393 self
394 }
395
396 fn background(mut self, background: Color) -> Self {
397 self.background = background;
398 self
399 }
400
401 fn hover_background(mut self, hover_background: Color) -> Self {
402 self.hover_background = hover_background;
403 self
404 }
405
406 fn color(mut self, color: Color) -> Self {
407 self.color = color;
408 self
409 }
410
411 fn corner_radius(mut self, corner_radius: CornerRadius) -> Self {
412 self.corner_radius = corner_radius;
413 self
414 }
415
416 fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
417 self.on_press = Some(on_press.into());
418 self
419 }
420
421 fn enabled(mut self, enabled: bool) -> Self {
422 self.enabled = enabled;
423 self
424 }
425}
426
427impl KeyExt for CalendarDay {
428 fn write_key(&mut self) -> &mut DiffKey {
429 &mut self.key
430 }
431}
432
433impl Component for CalendarDay {
434 fn render(&self) -> impl IntoElement {
435 Button::new()
436 .flat()
437 .padding(0.)
438 .enabled(self.enabled)
439 .width(Size::px(36.))
440 .height(Size::px(36.))
441 .background(self.background)
442 .hover_background(self.hover_background)
443 .maybe(self.enabled, |el| {
444 el.map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
445 })
446 .child(
447 label()
448 .text(self.day.to_string())
449 .color(self.color)
450 .font_size(14.),
451 )
452 }
453
454 fn render_key(&self) -> DiffKey {
455 self.key.clone().or(self.default_key())
456 }
457}