1use std::vec::Vec;
36
37use azul_core::{
38 callbacks::{CoreCallback, CoreCallbackData, Update},
39 dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
40 refany::{OptionRefAny, RefAny},
41};
42use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
43use azul_css::{
44 props::{
45 basic::{color::ColorU, StyleFontSize},
46 layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignSelf, LayoutFlexGrow, LayoutPaddingTop, LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutAlignItems, LayoutWidth, LayoutHeight},
47 property::{CssProperty, *},
48 style::{StyleBackgroundContent, StyleBackgroundContentVec, LayoutBorderTopWidth, LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth, StyleBorderTopStyle, BorderStyle, StyleBorderBottomStyle, StyleBorderLeftStyle, StyleBorderRightStyle, StyleBorderTopColor, StyleBorderBottomColor, StyleBorderLeftColor, StyleBorderRightColor, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleTextAlign, StyleCursor, StyleUserSelect, StyleTextColor},
49 },
50 impl_option_inner, AzString,
51};
52
53use crate::callbacks::{Callback, CallbackInfo};
54
55static DATE_PICKER_CLASS: &[IdOrClass] =
57 &[Class(AzString::from_const_str("__azul-native-date-picker"))];
58static HEADER_CLASS: &[IdOrClass] =
59 &[Class(AzString::from_const_str("__azul-native-date-picker-header"))];
60static HEADER_LABEL_CLASS: &[IdOrClass] =
61 &[Class(AzString::from_const_str("__azul-native-date-picker-label"))];
62static NAV_BTN_CLASS: &[IdOrClass] =
63 &[Class(AzString::from_const_str("__azul-native-date-picker-nav"))];
64static WEEKDAY_ROW_CLASS: &[IdOrClass] =
65 &[Class(AzString::from_const_str("__azul-native-date-picker-weekdays"))];
66static WEEKDAY_CELL_CLASS: &[IdOrClass] =
67 &[Class(AzString::from_const_str("__azul-native-date-picker-weekday"))];
68static GRID_CLASS: &[IdOrClass] =
69 &[Class(AzString::from_const_str("__azul-native-date-picker-grid"))];
70static WEEK_ROW_CLASS: &[IdOrClass] =
71 &[Class(AzString::from_const_str("__azul-native-date-picker-week"))];
72static DAY_CELL_CLASS: &[IdOrClass] =
73 &[Class(AzString::from_const_str("__azul-native-date-picker-day"))];
74
75const PREV_ARROW: AzString = AzString::from_const_str("\u{2039}"); const NEXT_ARROW: AzString = AzString::from_const_str("\u{203A}"); const WEEKDAY_NAMES: [AzString; 7] = [
79 AzString::from_const_str("Su"),
80 AzString::from_const_str("Mo"),
81 AzString::from_const_str("Tu"),
82 AzString::from_const_str("We"),
83 AzString::from_const_str("Th"),
84 AzString::from_const_str("Fr"),
85 AzString::from_const_str("Sa"),
86];
87
88pub type DatePickerOnChangeCallbackType =
90 extern "C" fn(RefAny, CallbackInfo, DatePickerState) -> Update;
91impl_widget_callback!(
92 DatePickerOnChange,
93 OptionDatePickerOnChange,
94 DatePickerOnChangeCallback,
95 DatePickerOnChangeCallbackType
96);
97
98azul_core::impl_managed_callback! {
99 wrapper: DatePickerOnChangeCallback,
100 info_ty: CallbackInfo,
101 return_ty: Update,
102 default_ret: Update::DoNothing,
103 invoker_static: DATE_PICKER_ON_CHANGE_INVOKER,
104 invoker_ty: AzDatePickerOnChangeCallbackInvoker,
105 thunk_fn: az_date_picker_on_change_callback_thunk,
106 setter_fn: AzApp_setDatePickerOnChangeCallbackInvoker,
107 from_handle_fn: AzDatePickerOnChangeCallback_createFromHostHandle,
108 extra_args: [ state: DatePickerState ],
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113#[repr(C)]
114pub struct DatePicker {
115 pub state: DatePickerStateWrapper,
116 pub container_style: CssPropertyWithConditionsVec,
118}
119
120#[derive(Debug, Default, Clone, PartialEq, Eq)]
122#[repr(C)]
123pub struct DatePickerStateWrapper {
124 pub inner: DatePickerState,
125 pub on_change: OptionDatePickerOnChange,
126}
127
128#[derive(Debug, Copy, Clone, PartialEq, Eq)]
130#[repr(C)]
131pub struct DatePickerState {
132 pub year: u32,
134 pub month: u32,
136 pub day: u32,
138}
139
140impl Default for DatePickerState {
141 fn default() -> Self {
142 Self {
143 year: 2000,
144 month: 1,
145 day: 1,
146 }
147 }
148}
149
150const fn is_leap(year: u32) -> bool {
156 (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
157}
158
159#[allow(clippy::match_same_arms)] const fn days_in_month(year: u32, month: u32) -> u32 {
162 match month {
163 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
164 4 | 6 | 9 | 11 => 30,
165 2 => {
166 if is_leap(year) {
167 29
168 } else {
169 28
170 }
171 }
172 _ => 30, }
174}
175
176#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn weekday(year: u32, month: u32, day: u32) -> u32 {
180 const T: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
181 let mut y = year as i32;
182 if month < 3 {
183 y -= 1;
184 }
185 let idx = if (1..=12).contains(&month) {
186 (month - 1) as usize
187 } else {
188 0
189 };
190 let w = (y + y / 4 - y / 100 + y / 400 + T[idx] + day as i32) % 7;
191 (((w % 7) + 7) % 7) as u32
192}
193
194const fn month_name(month: u32) -> &'static str {
196 const NAMES: [&str; 12] = [
197 "January",
198 "February",
199 "March",
200 "April",
201 "May",
202 "June",
203 "July",
204 "August",
205 "September",
206 "October",
207 "November",
208 "December",
209 ];
210 let idx = month.saturating_sub(1) as usize;
211 if idx < 12 {
212 NAMES[idx]
213 } else {
214 ""
215 }
216}
217
218const BORDER_COLOR: ColorU = ColorU { r: 206, g: 212, b: 218, a: 255 };
220const TEXT_COLOR: ColorU = ColorU { r: 33, g: 37, b: 41, a: 255 };
221const MUTED_COLOR: ColorU = ColorU { r: 108, g: 117, b: 125, a: 255 };
222const ACCENT_BG: ColorU = ColorU { r: 13, g: 110, b: 253, a: 255 };
223const WHITE: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 };
224const TRANSPARENT: ColorU = ColorU { r: 0, g: 0, b: 0, a: 0 };
225
226const DAY_SELECTED_BG_ITEMS: &[StyleBackgroundContent] =
227 &[StyleBackgroundContent::Color(ACCENT_BG)];
228const DAY_SELECTED_BG_VEC: StyleBackgroundContentVec =
229 StyleBackgroundContentVec::from_const_slice(DAY_SELECTED_BG_ITEMS);
230const TRANSPARENT_BG_ITEMS: &[StyleBackgroundContent] =
231 &[StyleBackgroundContent::Color(TRANSPARENT)];
232const TRANSPARENT_BG_VEC: StyleBackgroundContentVec =
233 StyleBackgroundContentVec::from_const_slice(TRANSPARENT_BG_ITEMS);
234const WHITE_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(WHITE)];
235const WHITE_BG_VEC: StyleBackgroundContentVec =
236 StyleBackgroundContentVec::from_const_slice(WHITE_BG_ITEMS);
237
238const CELL_W: isize = 32;
239const CELL_H: isize = 28;
240
241static CONTAINER_STYLE: &[CssPropertyWithConditions] = &[
243 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
244 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
245 CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Start)),
246 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
247 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(8))),
248 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
249 LayoutPaddingBottom::const_px(8),
250 )),
251 CssPropertyWithConditions::simple(CssProperty::const_padding_left(LayoutPaddingLeft::const_px(
252 8,
253 ))),
254 CssPropertyWithConditions::simple(CssProperty::const_padding_right(
255 LayoutPaddingRight::const_px(8),
256 )),
257 CssPropertyWithConditions::simple(CssProperty::const_background_content(WHITE_BG_VEC)),
258 CssPropertyWithConditions::simple(CssProperty::const_border_top_width(
259 LayoutBorderTopWidth::const_px(1),
260 )),
261 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_width(
262 LayoutBorderBottomWidth::const_px(1),
263 )),
264 CssPropertyWithConditions::simple(CssProperty::const_border_left_width(
265 LayoutBorderLeftWidth::const_px(1),
266 )),
267 CssPropertyWithConditions::simple(CssProperty::const_border_right_width(
268 LayoutBorderRightWidth::const_px(1),
269 )),
270 CssPropertyWithConditions::simple(CssProperty::const_border_top_style(StyleBorderTopStyle {
271 inner: BorderStyle::Solid,
272 })),
273 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_style(
274 StyleBorderBottomStyle {
275 inner: BorderStyle::Solid,
276 },
277 )),
278 CssPropertyWithConditions::simple(CssProperty::const_border_left_style(StyleBorderLeftStyle {
279 inner: BorderStyle::Solid,
280 })),
281 CssPropertyWithConditions::simple(CssProperty::const_border_right_style(
282 StyleBorderRightStyle {
283 inner: BorderStyle::Solid,
284 },
285 )),
286 CssPropertyWithConditions::simple(CssProperty::const_border_top_color(StyleBorderTopColor {
287 inner: BORDER_COLOR,
288 })),
289 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_color(
290 StyleBorderBottomColor { inner: BORDER_COLOR },
291 )),
292 CssPropertyWithConditions::simple(CssProperty::const_border_left_color(StyleBorderLeftColor {
293 inner: BORDER_COLOR,
294 })),
295 CssPropertyWithConditions::simple(CssProperty::const_border_right_color(
296 StyleBorderRightColor { inner: BORDER_COLOR },
297 )),
298 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
299 StyleBorderTopLeftRadius::const_px(6),
300 )),
301 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
302 StyleBorderTopRightRadius::const_px(6),
303 )),
304 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
305 StyleBorderBottomLeftRadius::const_px(6),
306 )),
307 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
308 StyleBorderBottomRightRadius::const_px(6),
309 )),
310];
311
312static HEADER_STYLE: &[CssPropertyWithConditions] = &[
314 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
315 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
316 CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
317 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
318 LayoutPaddingBottom::const_px(6),
319 )),
320];
321
322static NAV_BTN_STYLE: &[CssPropertyWithConditions] = &[
324 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(24))),
325 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(18))),
326 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
327 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
328 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
329 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
330 inner: TEXT_COLOR,
331 })),
332 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
333];
334
335static HEADER_LABEL_STYLE: &[CssPropertyWithConditions] = &[
337 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(1))),
338 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(14))),
339 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
340 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
341 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
342 inner: TEXT_COLOR,
343 })),
344];
345
346static ROW_STYLE: &[CssPropertyWithConditions] = &[
348 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
349 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
350 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
351];
352
353static WEEKDAY_CELL_STYLE: &[CssPropertyWithConditions] = &[
354 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(CELL_W))),
355 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(11))),
356 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
357 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
358 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
359 inner: MUTED_COLOR,
360 })),
361 CssPropertyWithConditions::simple(CssProperty::const_padding_bottom(
362 LayoutPaddingBottom::const_px(4),
363 )),
364];
365
366static GRID_STYLE: &[CssPropertyWithConditions] = &[
368 CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
369 CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Column)),
370 CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
371];
372
373static BLANK_CELL_STYLE: &[CssPropertyWithConditions] = &[
375 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(CELL_W))),
376 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(CELL_H))),
377];
378
379fn build_day_cell_style(selected: bool) -> CssPropertyWithConditionsVec {
383 let (bg, text) = if selected {
384 (DAY_SELECTED_BG_VEC, WHITE)
385 } else {
386 (TRANSPARENT_BG_VEC, TEXT_COLOR)
387 };
388 CssPropertyWithConditionsVec::from_vec(alloc::vec![
389 CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(CELL_W))),
390 CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(CELL_H))),
391 CssPropertyWithConditions::simple(CssProperty::const_font_size(StyleFontSize::const_px(13))),
392 CssPropertyWithConditions::simple(CssProperty::const_text_align(StyleTextAlign::Center)),
393 CssPropertyWithConditions::simple(CssProperty::const_padding_top(LayoutPaddingTop::const_px(
394 5,
395 ))),
396 CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
397 CssPropertyWithConditions::simple(CssProperty::user_select(StyleUserSelect::None)),
398 CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
399 StyleBorderTopLeftRadius::const_px(4),
400 )),
401 CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
402 StyleBorderTopRightRadius::const_px(4),
403 )),
404 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
405 StyleBorderBottomLeftRadius::const_px(4),
406 )),
407 CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
408 StyleBorderBottomRightRadius::const_px(4),
409 )),
410 CssPropertyWithConditions::simple(CssProperty::const_background_content(bg)),
411 CssPropertyWithConditions::simple(CssProperty::const_text_color(StyleTextColor {
412 inner: text,
413 })),
414 ])
415}
416
417struct DayCellData {
420 day: u32,
421 state: RefAny,
422}
423
424impl DatePicker {
425 #[must_use] pub fn create(year: u32, month: u32, day: u32) -> Self {
428 let month = month.clamp(1, 12);
429 let dim = days_in_month(year, month);
430 let day = day.clamp(1, dim);
431 Self {
432 state: DatePickerStateWrapper {
433 inner: DatePickerState { year, month, day },
434 on_change: None.into(),
435 },
436 container_style: CssPropertyWithConditionsVec::from_const_slice(CONTAINER_STYLE),
437 }
438 }
439
440 pub fn set_on_change<C: Into<DatePickerOnChangeCallback>>(&mut self, data: RefAny, callback: C) {
442 self.state.on_change = Some(DatePickerOnChange {
443 callback: callback.into(),
444 refany: data,
445 })
446 .into();
447 }
448
449 #[must_use] pub fn with_on_change<C: Into<DatePickerOnChangeCallback>>(
451 mut self,
452 data: RefAny,
453 callback: C,
454 ) -> Self {
455 self.set_on_change(data, callback);
456 self
457 }
458
459 #[must_use] pub fn swap_with_default(&mut self) -> Self {
461 let mut s = Self::create(2000, 1, 1);
462 core::mem::swap(&mut s, self);
463 s
464 }
465
466 #[must_use] pub fn dom(self) -> Dom {
467 let inner = self.state.inner;
468 let year = inner.year;
469 let month = inner.month.clamp(1, 12);
470 let sel_day = inner.day;
471 let container_style = self.container_style.clone();
472
473 let shared = RefAny::new(self.state);
474
475 let header = build_header(year, month, shared.clone());
476 let weekday_row = build_weekday_row();
477 let grid = build_grid(year, month, sel_day, shared);
478
479 Dom::create_div()
480 .with_ids_and_classes(IdOrClassVec::from_const_slice(DATE_PICKER_CLASS))
481 .with_css_props(container_style)
482 .with_children(alloc::vec![header, weekday_row, grid].into())
483 }
484}
485
486impl Default for DatePicker {
487 fn default() -> Self {
488 Self::create(2000, 1, 1)
489 }
490}
491
492fn build_header(year: u32, month: u32, shared: RefAny) -> Dom {
493 use azul_core::dom::{EventFilter, HoverEventFilter};
494
495 let nav = |arrow: AzString, cb: usize, refany: RefAny| -> Dom {
496 Dom::create_text(arrow)
497 .with_ids_and_classes(IdOrClassVec::from_const_slice(NAV_BTN_CLASS))
498 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(NAV_BTN_STYLE))
499 .with_callbacks(
500 alloc::vec![CoreCallbackData {
501 event: EventFilter::Hover(HoverEventFilter::MouseUp),
502 callback: CoreCallback {
503 cb,
504 ctx: OptionRefAny::None,
505 },
506 refany,
507 }]
508 .into(),
509 )
510 .with_tab_index(TabIndex::Auto)
511 };
512
513 let label = AzString::from(format!("{} {}", month_name(month), year));
514
515 Dom::create_div()
516 .with_ids_and_classes(IdOrClassVec::from_const_slice(HEADER_CLASS))
517 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(HEADER_STYLE))
518 .with_children(
519 alloc::vec![
520 nav(PREV_ARROW, on_prev_month as usize, shared.clone()),
521 Dom::create_text(label)
522 .with_ids_and_classes(IdOrClassVec::from_const_slice(HEADER_LABEL_CLASS))
523 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
524 HEADER_LABEL_STYLE,
525 )),
526 nav(NEXT_ARROW, on_next_month as usize, shared),
527 ]
528 .into(),
529 )
530}
531
532fn build_weekday_row() -> Dom {
533 let cells: Vec<Dom> = WEEKDAY_NAMES
534 .iter()
535 .map(|n| {
536 Dom::create_text(n.clone())
537 .with_ids_and_classes(IdOrClassVec::from_const_slice(WEEKDAY_CELL_CLASS))
538 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(
539 WEEKDAY_CELL_STYLE,
540 ))
541 })
542 .collect();
543
544 Dom::create_div()
545 .with_ids_and_classes(IdOrClassVec::from_const_slice(WEEKDAY_ROW_CLASS))
546 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ROW_STYLE))
547 .with_children(cells.into())
548}
549
550#[allow(clippy::needless_pass_by_value)] fn build_grid(year: u32, month: u32, sel_day: u32, shared: RefAny) -> Dom {
552 let leading = weekday(year, month, 1);
553 let dim = days_in_month(year, month);
554 let total = leading + dim;
555 let rows = total.div_ceil(7);
556
557 let mut week_rows: Vec<Dom> = Vec::with_capacity(rows as usize);
558 for r in 0..rows {
559 let mut cells: Vec<Dom> = Vec::with_capacity(7);
560 for c in 0..7 {
561 let i = r * 7 + c;
562 if i < leading || i >= leading + dim {
563 cells.push(build_blank_cell());
564 } else {
565 let day = i - leading + 1;
566 cells.push(build_day_cell(day, day == sel_day, shared.clone()));
567 }
568 }
569 week_rows.push(
570 Dom::create_div()
571 .with_ids_and_classes(IdOrClassVec::from_const_slice(WEEK_ROW_CLASS))
572 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(ROW_STYLE))
573 .with_children(cells.into()),
574 );
575 }
576
577 Dom::create_div()
578 .with_ids_and_classes(IdOrClassVec::from_const_slice(GRID_CLASS))
579 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(GRID_STYLE))
580 .with_children(week_rows.into())
581}
582
583fn build_blank_cell() -> Dom {
584 Dom::create_div()
585 .with_ids_and_classes(IdOrClassVec::from_const_slice(DAY_CELL_CLASS))
586 .with_css_props(CssPropertyWithConditionsVec::from_const_slice(BLANK_CELL_STYLE))
587}
588
589fn build_day_cell(day: u32, selected: bool, shared: RefAny) -> Dom {
590 use azul_core::dom::{EventFilter, HoverEventFilter};
591
592 Dom::create_text(AzString::from(format!("{day}")))
593 .with_ids_and_classes(IdOrClassVec::from_const_slice(DAY_CELL_CLASS))
594 .with_css_props(build_day_cell_style(selected))
595 .with_callbacks(
596 alloc::vec![CoreCallbackData {
597 event: EventFilter::Hover(HoverEventFilter::MouseUp),
598 callback: CoreCallback {
599 cb: on_day_click as usize,
600 ctx: OptionRefAny::None,
601 },
602 refany: RefAny::new(DayCellData { day, state: shared }),
603 }]
604 .into(),
605 )
606 .with_tab_index(TabIndex::Auto)
607}
608
609extern "C" fn on_day_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
612 let clicked = info.get_hit_node();
613
614 let (day, mut shared) = {
616 let Some(cell) = data.downcast_ref::<DayCellData>() else {
617 return Update::DoNothing;
618 };
619 (cell.day, cell.state.clone())
620 };
621
622 let update = {
623 let Some(mut w) = shared.downcast_mut::<DatePickerStateWrapper>() else {
624 return Update::DoNothing;
625 };
626 w.inner.day = day;
627 let inner = w.inner;
628 let w = &mut *w;
629 match w.on_change.as_mut() {
630 Some(DatePickerOnChange { callback, refany }) => {
631 (callback.cb)(refany.clone(), info, inner)
632 }
633 None => Update::DoNothing,
634 }
635 };
636
637 restyle_days(&mut info, clicked);
638
639 update
640}
641
642fn restyle_days(info: &mut CallbackInfo, clicked: azul_core::dom::DomNodeId) {
645 let Some(row) = info.get_parent(clicked) else {
646 return;
647 };
648 let Some(grid) = info.get_parent(row) else {
649 return;
650 };
651
652 let mut week = info.get_first_child(grid);
653 while let Some(w) = week {
654 let mut cellopt = info.get_first_child(w);
655 while let Some(cell) = cellopt {
656 if cell == clicked {
657 info.set_css_property(
658 cell,
659 CssProperty::const_background_content(DAY_SELECTED_BG_VEC),
660 );
661 info.set_css_property(
662 cell,
663 CssProperty::const_text_color(StyleTextColor { inner: WHITE }),
664 );
665 } else {
666 info.set_css_property(
667 cell,
668 CssProperty::const_background_content(TRANSPARENT_BG_VEC),
669 );
670 info.set_css_property(
671 cell,
672 CssProperty::const_text_color(StyleTextColor { inner: TEXT_COLOR }),
673 );
674 }
675 cellopt = info.get_next_sibling(cell);
676 }
677 week = info.get_next_sibling(w);
678 }
679}
680
681extern "C" fn on_prev_month(data: RefAny, info: CallbackInfo) -> Update {
682 month_nav(data, info, -1)
683}
684
685extern "C" fn on_next_month(data: RefAny, info: CallbackInfo) -> Update {
686 month_nav(data, info, 1)
687}
688
689#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn month_nav(mut data: RefAny, info: CallbackInfo, delta: i32) -> Update {
695 let Some(mut w) = data.downcast_mut::<DatePickerStateWrapper>() else {
696 return Update::DoNothing;
697 };
698
699 let mut month = w.inner.month as i32 + delta;
700 let mut year = w.inner.year as i32;
701 if month < 1 {
702 month = 12;
703 year -= 1;
704 } else if month > 12 {
705 month = 1;
706 year += 1;
707 }
708 w.inner.year = year.max(1) as u32;
709 w.inner.month = month as u32;
710 let dim = days_in_month(w.inner.year, w.inner.month);
711 if w.inner.day > dim {
712 w.inner.day = dim;
713 }
714
715 let inner = w.inner;
716 let w = &mut *w;
717 match w.on_change.as_mut() {
718 Some(DatePickerOnChange { callback, refany }) => (callback.cb)(refany.clone(), info, inner),
719 None => Update::DoNothing,
720 }
721}
722
723impl From<DatePicker> for Dom {
724 fn from(d: DatePicker) -> Self {
725 d.dom()
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732
733 #[test]
734 fn leap_years() {
735 assert!(is_leap(2000));
736 assert!(is_leap(2024));
737 assert!(!is_leap(1900));
738 assert!(!is_leap(2023));
739 }
740
741 #[test]
742 fn days_per_month() {
743 assert_eq!(days_in_month(2023, 2), 28);
744 assert_eq!(days_in_month(2024, 2), 29);
745 assert_eq!(days_in_month(2024, 4), 30);
746 assert_eq!(days_in_month(2024, 1), 31);
747 }
748
749 #[test]
750 fn weekday_known_dates() {
751 assert_eq!(weekday(2000, 1, 1), 6);
753 assert_eq!(weekday(2026, 6, 1), 1);
755 assert_eq!(weekday(1970, 1, 1), 4);
757 }
758}