Skip to main content

azul_layout/widgets/
date_picker.rs

1//! Calendar date picker widget.
2//!
3//! Renders a month header (‹ prev / `Month YYYY` / next ›) above a weekday
4//! header row (`Su Mo Tu We Th Fr Sa`) and a 7-column grid of day cells laid
5//! out as week rows. The grid is computed from real calendar math — days in
6//! month (with leap-year February) and the weekday offset of the 1st (Sakamoto's
7//! algorithm) — so the leading blank cells and day count are correct for the
8//! given month.
9//!
10//! Clicking a day cell selects it: the handler reads the cell's baked day
11//! number, updates `state.day`, fires the optional `on_change(state)`, and
12//! live-restyles the grid (accent fill on the selected cell, neutral on the
13//! rest) exactly like `segmented.rs` restyles its active segment. The day number
14//! is carried per-cell (like `drop_down.rs`'s per-item data) alongside a clone of
15//! the shared-state handle, so selection never depends on re-deriving the grid
16//! offset at click time.
17//!
18//! TODO2 — MONTH NAVIGATION CANNOT REBUILD THE GRID IN-WIDGET.
19//! Clicking ‹ / › changes a *different month*, which has a different day count
20//! and weekday offset, i.e. a different *number of day-cell nodes*. A widget
21//! callback can only `set_css_property` / `change_node_text` on the EXISTING
22//! nodes — it cannot add/remove/relayout day cells (the same limitation
23//! `combobox`'s type-to-filter hit). Therefore the ‹ / › buttons DO update the
24//! month/year in the state and fire `on_change(state)` so host code can rebuild
25//! the widget (a fresh `DatePicker::create(...)` with the new month), but the
26//! in-widget grid does NOT change, and the header is deliberately NOT re-texted
27//! either — showing a new month name over the old day grid would be a misleading
28//! half-switch. Day-selection (the restyle) is fully functional for the
29//! displayed month; after a ‹ / › without a host rebuild the grid is stale (the
30//! documented limitation). Computing the initial grid from calendar math is NOT
31//! faked behaviour — only the live month rebuild is the limitation.
32//!
33//! Key types: [`DatePicker`], [`DatePickerState`], [`DatePickerOnChange`].
34
35use 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
55// ---- classes ----
56static 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}"); // ‹
76const NEXT_ARROW: AzString = AzString::from_const_str("\u{203A}"); // ›
77
78const 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
88/// Callback type invoked when the selected day or displayed month/year changes.
89pub 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/// A calendar date picker.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[repr(C)]
114pub struct DatePicker {
115    pub state: DatePickerStateWrapper,
116    /// Style for the outer container.
117    pub container_style: CssPropertyWithConditionsVec,
118}
119
120/// Wraps [`DatePickerState`] together with its change callback.
121#[derive(Debug, Default, Clone, PartialEq, Eq)]
122#[repr(C)]
123pub struct DatePickerStateWrapper {
124    pub inner: DatePickerState,
125    pub on_change: OptionDatePickerOnChange,
126}
127
128/// State of a [`DatePicker`]: the displayed month/year and the selected day.
129#[derive(Debug, Copy, Clone, PartialEq, Eq)]
130#[repr(C)]
131pub struct DatePickerState {
132    /// The displayed (and selected) year.
133    pub year: u32,
134    /// The displayed (and selected) month, `1..=12`.
135    pub month: u32,
136    /// The selected day of the month, `1..=31`.
137    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
150// ---------------------------------------------------------------------------
151// Pure calendar math (standard, well-known formulas — not faked behaviour).
152// ---------------------------------------------------------------------------
153
154/// Gregorian leap-year test.
155const fn is_leap(year: u32) -> bool {
156    (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
157}
158
159/// Number of days in the given (1-based) month of the given year.
160#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
161const 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, // defensive (month is clamped to 1..=12 elsewhere)
173    }
174}
175
176/// Sakamoto's algorithm: weekday of `(year, month, day)`, returned as
177/// `0 = Sunday .. 6 = Saturday`. Verified: 2000-01-01 -> 6 (Saturday).
178#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded layout/render numeric cast
179fn 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
194/// English month name for a 1-based month index.
195const 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
218// ---- colours ----
219const 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
241/// Outer container: a column that hugs its content, bordered + white.
242static 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
312/// Header row: prev button / centred label / next button.
313static 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
322/// The ‹ / › nav buttons.
323static 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
335/// The `Month YYYY` label (centred, fills the header).
336static 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
346/// Weekday header row + cells.
347static 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
366/// Grid: column of week rows.
367static 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
373/// Blank (offset / trailing) cell — keeps the column width, no text/callback.
374static 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
379/// Builds the per-day-cell style. Only the background + text colour depend on
380/// the selected flag (the rest is shared), so the style is built at runtime
381/// (mirrors `segmented::build_segment_style`).
382fn 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
417/// Per-day-cell callback payload: the cell's day number + a clone of the shared
418/// state handle (so the handler can update `state.day` + fire `on_change`).
419struct DayCellData {
420    day: u32,
421    state: RefAny,
422}
423
424impl DatePicker {
425    /// Creates a new `DatePicker` showing `year`/`month` with `day` selected.
426    /// `month` is clamped to `1..=12` and `day` to `1..=days_in_month`.
427    #[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    /// Sets the callback invoked when the selection or month changes.
441    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    /// Builder variant of [`Self::set_on_change`].
450    #[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    /// Replaces `self` with the default value and returns the original.
460    #[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)] // shared RefAny handle cloned per day cell
551fn 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
609/// Day-cell click handler. Reads the cell's baked day, updates the shared
610/// `state.day`, fires `on_change`, and live-restyles the whole grid.
611extern "C" fn on_day_click(mut data: RefAny, mut info: CallbackInfo) -> Update {
612    let clicked = info.get_hit_node();
613
614    // Read the baked day + clone the shared-state handle.
615    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
642/// Accents the clicked cell and neutralises every other grid cell (blanks
643/// included — for them transparent bg + a colour on empty text is a no-op).
644fn 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/// Month navigation. Updates month/year (wrapping across year boundaries),
690/// clamps the selected day into the new month, and fires `on_change` so host
691/// code can rebuild the widget. TODO2: the in-widget grid is NOT rebuilt (see
692/// module docs) — only the reported state changes.
693#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] // bounded layout/render numeric cast
694fn 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        // 2000-01-01 was a Saturday (6).
752        assert_eq!(weekday(2000, 1, 1), 6);
753        // 2026-06-01 is a Monday (1).
754        assert_eq!(weekday(2026, 6, 1), 1);
755        // 1970-01-01 was a Thursday (4).
756        assert_eq!(weekday(1970, 1, 1), 4);
757    }
758}