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}
759
760#[cfg(test)]
761mod autotest_generated {
762 use std::{
763 collections::{BTreeMap, HashMap},
764 sync::{Arc, Mutex},
765 };
766
767 use azul_core::{
768 dom::{DomId, DomNodeId, EventFilter, HoverEventFilter, NodeId, NodeType},
769 geom::{LogicalRect, OptionLogicalPosition},
770 gl::OptionGlContextPtr,
771 hit_test::ScrollPosition,
772 resources::RendererResources,
773 styled_dom::{NodeHierarchyItemId, StyledDom},
774 window::{MonitorVec, RawWindowHandle},
775 };
776 use azul_css::props::basic::{length::SizeMetric, pixel::PixelValue};
777 use rust_fontconfig::FcFontCache;
778
779 use super::*;
780 #[cfg(feature = "icu")]
781 use crate::icu::IcuLocalizerHandle;
782 use crate::{
783 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
784 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
785 window::{DomLayoutResult, LayoutWindow},
786 window_state::FullWindowState,
787 };
788
789 const MAX_SAFE_WEEKDAY_YEAR: u32 = 1_717_986_916;
799
800 fn node(idx: usize) -> DomNodeId {
802 DomNodeId {
803 dom: DomId::ROOT_ID,
804 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx))),
805 }
806 }
807
808 fn node_none() -> DomNodeId {
812 DomNodeId {
813 dom: DomId::ROOT_ID,
814 node: NodeHierarchyItemId::NONE,
815 }
816 }
817
818 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
823 DomLayoutResult {
824 styled_dom,
825 layout_tree: LayoutTree {
826 nodes: Vec::new(),
827 warm: Vec::new(),
828 cold: Vec::new(),
829 root: 0,
830 dom_to_layout: BTreeMap::new(),
831 children_arena: Vec::new(),
832 children_offsets: Vec::new(),
833 subtree_needs_intrinsic: Vec::new(),
834 },
835 calculated_positions: Vec::new(),
836 viewport: LogicalRect::zero(),
837 display_list: DisplayList::default(),
838 scroll_ids: HashMap::new(),
839 scroll_id_to_node_id: HashMap::new(),
840 }
841 }
842
843 fn with_info<R>(
847 styled_dom: StyledDom,
848 hit: DomNodeId,
849 f: impl FnOnce(&mut CallbackInfo) -> R,
850 ) -> (R, Vec<CallbackChange>) {
851 let mut layout_window =
852 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
853 layout_window
854 .layout_results
855 .insert(DomId::ROOT_ID, layout_result(styled_dom));
856
857 let renderer_resources = RendererResources::default();
858 let previous_window_state: Option<FullWindowState> = None;
859 let current_window_state = FullWindowState::default();
860 let gl_context = OptionGlContextPtr::None;
861 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
862 BTreeMap::new();
863 let window_handle = RawWindowHandle::Unsupported;
864 let system_callbacks = ExternalSystemCallbacks::rust_internal();
865
866 let ref_data = CallbackInfoRefData {
867 layout_window: &layout_window,
868 renderer_resources: &renderer_resources,
869 previous_window_state: &previous_window_state,
870 current_window_state: ¤t_window_state,
871 gl_context: &gl_context,
872 current_scroll_manager: &scroll_states,
873 current_window_handle: &window_handle,
874 system_callbacks: &system_callbacks,
875 system_style: Arc::new(azul_css::system::SystemStyle::default()),
876 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
877 #[cfg(feature = "icu")]
878 icu_localizer: IcuLocalizerHandle::default(),
879 ctx: OptionRefAny::None,
880 };
881
882 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
883
884 let mut info = CallbackInfo::new(
885 &ref_data,
886 &changes,
887 hit,
888 OptionLogicalPosition::None,
889 OptionLogicalPosition::None,
890 );
891
892 let r = f(&mut info);
893 let pushed = info.take_changes();
894 (r, pushed)
895 }
896
897 fn sections(dom: &Dom) -> (&Dom, &Dom, &Dom) {
903 let c = dom.children.as_ref();
904 assert_eq!(
905 c.len(),
906 3,
907 "a date picker renders header + weekday row + grid, got {} children",
908 c.len(),
909 );
910 (&c[0], &c[1], &c[2])
911 }
912
913 fn grid_cells(grid: &Dom) -> Vec<&Dom> {
915 grid.children
916 .as_ref()
917 .iter()
918 .flat_map(|week| week.children.as_ref().iter())
919 .collect()
920 }
921
922 fn text_of(dom: &Dom) -> Option<String> {
924 dom.root.get_node_type().format()
925 }
926
927 fn day_numbers(grid: &Dom) -> Vec<Option<u32>> {
929 grid_cells(grid)
930 .into_iter()
931 .map(|c| text_of(c).map(|t| t.parse::<u32>().expect("a day cell is not a number")))
932 .collect()
933 }
934
935 fn classes(dom: &Dom) -> Vec<String> {
936 dom.root
937 .get_ids_and_classes()
938 .as_ref()
939 .iter()
940 .filter_map(|c| match c {
941 IdOrClass::Class(s) => Some(s.as_str().to_string()),
942 IdOrClass::Id(_) => None,
943 })
944 .collect()
945 }
946
947 fn descendants(dom: &Dom) -> usize {
951 dom.children
952 .as_ref()
953 .iter()
954 .map(|c| 1 + descendants(c))
955 .sum()
956 }
957
958 fn shared_state(sd: &StyledDom) -> RefAny {
961 for nd in sd.node_data.as_ref() {
962 for cb in nd.callbacks.as_ref() {
963 let matches = {
964 let mut r = cb.refany.clone();
965 let matches = r.downcast_ref::<DatePickerStateWrapper>().is_some();
966 matches
967 };
968 if matches {
969 return cb.refany.clone();
970 }
971 }
972 }
973 panic!("the rendered date picker carries no DatePickerStateWrapper");
974 }
975
976 fn day_cell(sd: &StyledDom, day: u32) -> (DomNodeId, RefAny) {
978 for (i, nd) in sd.node_data.as_ref().iter().enumerate() {
979 for cb in nd.callbacks.as_ref() {
980 let matches = {
981 let mut r = cb.refany.clone();
982 r.downcast_ref::<DayCellData>().is_some_and(|c| c.day == day)
983 };
984 if matches {
985 return (node(i), cb.refany.clone());
986 }
987 }
988 }
989 panic!("the rendered grid has no cell for day {day}");
990 }
991
992 fn nav_button(sd: &StyledDom, handler: usize) -> (DomNodeId, RefAny) {
994 for (i, nd) in sd.node_data.as_ref().iter().enumerate() {
995 for cb in nd.callbacks.as_ref() {
996 if cb.callback.cb == handler {
997 return (node(i), cb.refany.clone());
998 }
999 }
1000 }
1001 panic!("the rendered header has no button wired to that handler");
1002 }
1003
1004 fn read_state(shared: &RefAny) -> DatePickerState {
1005 let mut s = shared.clone();
1006 let w = s
1007 .downcast_ref::<DatePickerStateWrapper>()
1008 .expect("the widget state changed type");
1009 w.inner
1010 }
1011
1012 fn laid_out(picker: DatePicker) -> (StyledDom, RefAny) {
1016 let styled = StyledDom::create_from_dom(picker.dom());
1017 let shared = shared_state(&styled);
1018 (styled, shared)
1019 }
1020
1021 fn click(
1023 styled_dom: StyledDom,
1024 payload: &RefAny,
1025 hit: DomNodeId,
1026 ) -> (Update, Vec<CallbackChange>) {
1027 with_info(styled_dom, hit, |info| on_day_click(payload.clone(), *info))
1028 }
1029
1030 fn press_nav(
1034 inner: DatePickerState,
1035 next: bool,
1036 times: usize,
1037 ) -> (DatePickerState, Update, Vec<CallbackChange>) {
1038 let shared = RefAny::new(DatePickerStateWrapper {
1039 inner,
1040 on_change: None.into(),
1041 });
1042 let (update, changes) = with_info(StyledDom::default(), node(0), |info| {
1043 let mut last = Update::DoNothing;
1044 for _ in 0..times {
1045 last = if next {
1046 on_next_month(shared.clone(), *info)
1047 } else {
1048 on_prev_month(shared.clone(), *info)
1049 };
1050 }
1051 last
1052 });
1053 (read_state(&shared), update, changes)
1054 }
1055
1056 fn properties(v: &CssPropertyWithConditionsVec) -> Vec<CssProperty> {
1061 v.as_ref().iter().map(|p| p.property.clone()).collect()
1062 }
1063
1064 fn find<T>(
1065 v: &CssPropertyWithConditionsVec,
1066 f: impl Fn(&CssProperty) -> Option<T>,
1067 ) -> Option<T> {
1068 v.as_ref().iter().find_map(|p| f(&p.property))
1069 }
1070
1071 fn px(pv: &PixelValue) -> f32 {
1076 assert_eq!(
1077 pv.metric,
1078 SizeMetric::Px,
1079 "date-picker geometry must be absolute px, got {:?}",
1080 pv.metric,
1081 );
1082 pv.number.get()
1083 }
1084
1085 fn width_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
1086 find(v, |p| match p {
1087 CssProperty::Width(w) => match w.get_property() {
1088 Some(LayoutWidth::Px(pv)) => Some(px(pv)),
1089 _ => None,
1090 },
1091 _ => None,
1092 })
1093 }
1094
1095 fn height_px(v: &CssPropertyWithConditionsVec) -> Option<f32> {
1096 find(v, |p| match p {
1097 CssProperty::Height(h) => match h.get_property() {
1098 Some(LayoutHeight::Px(pv)) => Some(px(pv)),
1099 _ => None,
1100 },
1101 _ => None,
1102 })
1103 }
1104
1105 fn background(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
1107 find(v, |p| match p {
1108 CssProperty::BackgroundContent(b) => {
1109 b.get_property().and_then(|v| match v.as_ref().first() {
1110 Some(StyleBackgroundContent::Color(c)) => Some(*c),
1111 _ => None,
1112 })
1113 }
1114 _ => None,
1115 })
1116 }
1117
1118 fn text_colour(v: &CssPropertyWithConditionsVec) -> Option<ColorU> {
1119 find(v, |p| match p {
1120 CssProperty::TextColor(t) => t.get_property().map(|t| t.inner),
1121 _ => None,
1122 })
1123 }
1124
1125 fn rendered_background(dom: &Dom) -> Option<ColorU> {
1127 dom.root
1128 .style
1129 .iter_inline_properties()
1130 .find_map(|(p, _)| match p {
1131 CssProperty::BackgroundContent(b) => {
1132 b.get_property().and_then(|v| match v.as_ref().first() {
1133 Some(StyleBackgroundContent::Color(c)) => Some(*c),
1134 _ => None,
1135 })
1136 }
1137 _ => None,
1138 })
1139 }
1140
1141 fn pushed_backgrounds(changes: &[CallbackChange]) -> Vec<(NodeId, ColorU)> {
1143 changes
1144 .iter()
1145 .filter_map(|c| match c {
1146 CallbackChange::ChangeNodeCssProperties {
1147 node_id, properties, ..
1148 } => {
1149 let col = properties.as_ref().iter().find_map(|p| match p {
1150 CssProperty::BackgroundContent(b) => {
1151 b.get_property().and_then(|v| match v.as_ref().first() {
1152 Some(StyleBackgroundContent::Color(c)) => Some(*c),
1153 _ => None,
1154 })
1155 }
1156 _ => None,
1157 })?;
1158 Some((*node_id, col))
1159 }
1160 _ => None,
1161 })
1162 .collect()
1163 }
1164
1165 fn pushed_text_colours(changes: &[CallbackChange]) -> Vec<(NodeId, ColorU)> {
1167 changes
1168 .iter()
1169 .filter_map(|c| match c {
1170 CallbackChange::ChangeNodeCssProperties {
1171 node_id, properties, ..
1172 } => {
1173 let col = properties.as_ref().iter().find_map(|p| match p {
1174 CssProperty::TextColor(t) => t.get_property().map(|t| t.inner),
1175 _ => None,
1176 })?;
1177 Some((*node_id, col))
1178 }
1179 _ => None,
1180 })
1181 .collect()
1182 }
1183
1184 #[derive(Debug, Clone, PartialEq, Eq)]
1192 struct ChangeLog {
1193 seen: Vec<DatePickerState>,
1194 payload: u32,
1195 }
1196
1197 extern "C" fn record_change(
1198 mut data: RefAny,
1199 _info: CallbackInfo,
1200 state: DatePickerState,
1201 ) -> Update {
1202 if let Some(mut log) = data.downcast_mut::<ChangeLog>() {
1203 log.seen.push(state);
1204 }
1205 Update::RefreshDom
1206 }
1207
1208 extern "C" fn change_do_nothing(
1209 _data: RefAny,
1210 _info: CallbackInfo,
1211 _state: DatePickerState,
1212 ) -> Update {
1213 Update::DoNothing
1214 }
1215
1216 extern "C" fn change_refresh_all(
1217 _data: RefAny,
1218 _info: CallbackInfo,
1219 _state: DatePickerState,
1220 ) -> Update {
1221 Update::RefreshDomAllWindows
1222 }
1223
1224 extern "C" fn generic_shaped(_data: RefAny, _info: CallbackInfo) -> Update {
1228 Update::DoNothing
1229 }
1230
1231 fn log_refany() -> RefAny {
1232 RefAny::new(ChangeLog {
1233 seen: Vec::new(),
1234 payload: 0xDEAD_BEEF,
1235 })
1236 }
1237
1238 fn read_log(probe: &RefAny) -> ChangeLog {
1239 let mut probe = probe.clone();
1240 let log = probe
1241 .downcast_ref::<ChangeLog>()
1242 .expect("the user payload changed type");
1243 log.clone()
1244 }
1245
1246 #[test]
1251 fn is_leap_applies_all_three_gregorian_rules_including_the_century_exceptions() {
1252 for (year, expected) in [
1256 (1, false),
1257 (4, true),
1258 (100, false),
1259 (400, true),
1260 (1600, true),
1261 (1700, false),
1262 (1800, false),
1263 (1900, false),
1264 (2000, true),
1265 (2023, false),
1266 (2024, true),
1267 (2100, false),
1268 (2400, true),
1269 ] {
1270 assert_eq!(is_leap(year), expected, "is_leap({year}) is wrong");
1271 }
1272 }
1273
1274 #[test]
1275 fn is_leap_is_total_at_the_boundaries_of_u32() {
1276 assert!(is_leap(0), "year 0 is divisible by 400 and must be leap");
1280 assert!(
1281 !is_leap(u32::MAX),
1282 "u32::MAX (4294967295) is not divisible by 4",
1283 );
1284 for year in (u32::MAX - 500)..=u32::MAX {
1285 let _ = is_leap(year);
1288 }
1289 }
1290
1291 #[test]
1292 fn is_leap_repeats_with_the_400_year_gregorian_cycle() {
1293 for year in 0..1200u32 {
1296 assert_eq!(
1297 is_leap(year),
1298 is_leap(year + 400),
1299 "the leap rule is not 400-periodic at year {year}",
1300 );
1301 }
1302 let leaps = (0..400u32).filter(|y| is_leap(*y)).count();
1304 assert_eq!(leaps, 97, "a 400-year cycle must contain exactly 97 leap years");
1305 }
1306
1307 #[test]
1308 fn is_leap_agrees_with_the_length_of_february() {
1309 for year in 0..800u32 {
1312 assert_eq!(
1313 is_leap(year),
1314 days_in_month(year, 2) == 29,
1315 "is_leap and days_in_month disagree about February {year}",
1316 );
1317 }
1318 }
1319
1320 #[test]
1325 fn days_in_month_returns_the_calendar_length_of_every_real_month() {
1326 let expected = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1327 for (i, want) in expected.iter().enumerate() {
1328 let month = u32::try_from(i).unwrap() + 1;
1329 assert_eq!(
1330 days_in_month(2023, month),
1331 *want,
1332 "month {month} of the non-leap year 2023 has the wrong length",
1333 );
1334 }
1335 }
1336
1337 #[test]
1338 fn days_in_month_february_tracks_the_leap_rule_at_the_extremes() {
1339 for (year, want) in [
1340 (0u32, 29), (1900, 28), (2000, 29), (2023, 28),
1344 (2024, 29),
1345 (u32::MAX, 28), ] {
1347 assert_eq!(
1348 days_in_month(year, 2),
1349 want,
1350 "February {year} has the wrong length",
1351 );
1352 }
1353 }
1354
1355 #[test]
1356 fn days_in_month_falls_back_to_thirty_outside_one_to_twelve_without_panicking() {
1357 for month in [0u32, 13, 14, 99, 1000, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1360 assert_eq!(
1361 days_in_month(2024, month),
1362 30,
1363 "out-of-range month {month} did not take the 30-day fallback",
1364 );
1365 }
1366 }
1367
1368 #[test]
1369 fn days_in_month_never_leaves_the_28_to_31_band_for_any_input() {
1370 for year in [0u32, 1, 1900, 2000, 2023, 2024, u32::MAX - 1, u32::MAX] {
1373 for month in [0u32, 1, 2, 6, 12, 13, u32::MAX] {
1374 let dim = days_in_month(year, month);
1375 assert!(
1376 (28..=31).contains(&dim),
1377 "days_in_month({year}, {month}) = {dim} is outside 28..=31",
1378 );
1379 }
1380 }
1381 }
1382
1383 #[test]
1384 fn days_in_month_sums_to_a_real_year() {
1385 for year in [1900u32, 1999, 2000, 2023, 2024, 2100, 2400] {
1386 let total: u32 = (1..=12).map(|m| days_in_month(year, m)).sum();
1387 let want = if is_leap(year) { 366 } else { 365 };
1388 assert_eq!(total, want, "the twelve months of {year} do not add up to a year");
1389 }
1390 }
1391
1392 #[test]
1397 fn weekday_matches_independently_known_dates() {
1398 for (y, m, d, want) in [
1399 (1900u32, 1u32, 1u32, 1u32), (1970, 1, 1, 4), (2000, 1, 1, 6), (2000, 3, 1, 3), (2024, 2, 29, 4), (2024, 12, 31, 2), ] {
1406 assert_eq!(
1407 weekday(y, m, d),
1408 want,
1409 "weekday({y}, {m}, {d}) disagrees with the real calendar",
1410 );
1411 }
1412 }
1413
1414 #[test]
1415 fn weekday_is_always_a_valid_index_into_the_seven_weekday_names() {
1416 for year in [0u32, 1, 1899, 1900, 2000, 2024] {
1424 for month in [0u32, 1, 2, 3, 12, 13, 100, u32::MAX] {
1425 for day in [0u32, 1, 28, 31, 32, 999, 1_000_000_000, u32::MAX] {
1426 let w = weekday(year, month, day);
1427 assert!(
1428 w < 7,
1429 "weekday({year}, {month}, {day}) = {w} is not a weekday index",
1430 );
1431 }
1432 }
1433 }
1434 }
1435
1436 #[test]
1437 fn weekday_survives_a_saturated_year_by_wrapping_into_the_negative_range() {
1438 for month in 0..=13u32 {
1442 let w = weekday(u32::MAX, month, 1);
1443 assert!(w < 7, "weekday(u32::MAX, {month}, 1) = {w} is not a weekday index");
1444 }
1445 assert!(weekday(u32::MAX, 12, u32::MAX) < 7);
1446 assert!(weekday(MAX_SAFE_WEEKDAY_YEAR, 12, 1) < 7);
1447 }
1448
1449 #[test]
1450 fn weekday_advances_by_exactly_one_per_day_within_a_month() {
1451 for (year, month) in [(2024u32, 1u32), (2024, 2), (2023, 2), (2000, 12), (1900, 6)] {
1452 let dim = days_in_month(year, month);
1453 for day in 1..dim {
1454 assert_eq!(
1455 weekday(year, month, day + 1),
1456 (weekday(year, month, day) + 1) % 7,
1457 "{year}-{month}: the weekday jumps between day {day} and {}",
1458 day + 1,
1459 );
1460 }
1461 }
1462 }
1463
1464 #[test]
1465 fn weekday_of_the_first_chains_across_every_month_of_three_decades() {
1466 for year in 1995..2025u32 {
1472 for month in 1..=12u32 {
1473 let (ny, nm) = if month == 12 { (year + 1, 1) } else { (year, month + 1) };
1474 assert_eq!(
1475 weekday(ny, nm, 1),
1476 (weekday(year, month, 1) + days_in_month(year, month)) % 7,
1477 "the calendar breaks between {year}-{month} and {ny}-{nm}",
1478 );
1479 }
1480 }
1481 }
1482
1483 #[test]
1484 fn weekday_treats_day_zero_as_the_last_day_of_the_previous_month() {
1485 assert_eq!(
1488 weekday(2000, 1, 0),
1489 weekday(1999, 12, 31),
1490 "day 0 of January is not the same weekday as the preceding 31 December",
1491 );
1492 assert_eq!(weekday(2024, 3, 0), weekday(2024, 2, 29));
1493 }
1494
1495 #[test]
1496 fn weekday_collapses_every_month_above_twelve_onto_one_behaviour() {
1497 for day in [0u32, 1, 15, 31] {
1501 let thirteen = weekday(2024, 13, day);
1502 for month in [14u32, 99, 1000, u32::MAX] {
1503 assert_eq!(
1504 weekday(2024, month, day),
1505 thirteen,
1506 "month {month} is not handled like every other out-of-range month",
1507 );
1508 }
1509 }
1510 for day in [0u32, 1, 15, 31] {
1512 assert_eq!(
1513 weekday(2024, 0, day),
1514 weekday(2024, 1, day),
1515 "month 0 is not handled like January",
1516 );
1517 }
1518 }
1519
1520 #[test]
1525 fn month_name_maps_each_real_month_to_a_distinct_english_name() {
1526 let expected = [
1527 "January", "February", "March", "April", "May", "June",
1528 "July", "August", "September", "October", "November", "December",
1529 ];
1530 for (i, want) in expected.iter().enumerate() {
1531 let month = u32::try_from(i).unwrap() + 1;
1532 assert_eq!(month_name(month), *want, "month {month} has the wrong name");
1533 }
1534 let mut names: Vec<&str> = (1..=12).map(month_name).collect();
1535 names.sort_unstable();
1536 names.dedup();
1537 assert_eq!(names.len(), 12, "two months share a name");
1538 }
1539
1540 #[test]
1541 fn month_name_returns_empty_above_december_but_january_at_zero() {
1542 assert_eq!(month_name(0), "January", "the zero month stopped saturating to January");
1547 for month in [13u32, 14, 99, 1000, u32::MAX / 2, u32::MAX - 1, u32::MAX] {
1548 assert_eq!(
1549 month_name(month),
1550 "",
1551 "out-of-range month {month} produced a name",
1552 );
1553 }
1554 }
1555
1556 #[test]
1557 fn month_name_is_total_and_only_names_the_first_thirteen_indices() {
1558 for month in 0..2000u32 {
1559 let name = month_name(month);
1560 assert_eq!(
1561 !name.is_empty(),
1562 month <= 12,
1563 "month {month} named {name:?} outside the 0..=12 window",
1564 );
1565 }
1566 }
1567
1568 #[test]
1573 fn build_day_cell_style_differs_between_the_two_states_only_in_colour() {
1574 let sel = build_day_cell_style(true);
1577 let plain = build_day_cell_style(false);
1578 let a = properties(&sel);
1579 let b = properties(&plain);
1580
1581 assert_eq!(
1582 a.len(),
1583 b.len(),
1584 "the selected and unselected cell styles declare a different number of properties",
1585 );
1586 let differing: Vec<_> = a
1587 .iter()
1588 .zip(b.iter())
1589 .filter(|(x, y)| x != y)
1590 .map(|(x, _)| core::mem::discriminant(x))
1591 .collect();
1592 assert_eq!(
1593 differing,
1594 vec![
1595 core::mem::discriminant(&CssProperty::const_background_content(WHITE_BG_VEC)),
1596 core::mem::discriminant(&CssProperty::const_text_color(StyleTextColor {
1597 inner: WHITE
1598 })),
1599 ],
1600 "the two day-cell styles differ in something other than background + text colour",
1601 );
1602 }
1603
1604 #[test]
1605 fn build_day_cell_style_paints_the_selection_accent_on_white_and_the_rest_transparent() {
1606 assert_eq!(background(&build_day_cell_style(true)), Some(ACCENT_BG));
1609 assert_eq!(text_colour(&build_day_cell_style(true)), Some(WHITE));
1610 assert_eq!(background(&build_day_cell_style(false)), Some(TRANSPARENT));
1611 assert_eq!(text_colour(&build_day_cell_style(false)), Some(TEXT_COLOR));
1612 }
1613
1614 #[test]
1615 fn build_day_cell_style_keeps_the_cell_geometry_identical_and_absolute() {
1616 let blanks = CssPropertyWithConditionsVec::from_const_slice(BLANK_CELL_STYLE);
1620 let headers = CssPropertyWithConditionsVec::from_const_slice(WEEKDAY_CELL_STYLE);
1621 let want_w = Some(CELL_W as f32);
1622 let want_h = Some(CELL_H as f32);
1623
1624 for selected in [false, true] {
1625 let v = build_day_cell_style(selected);
1626 assert_eq!(width_px(&v), want_w, "selected={selected}: wrong cell width");
1627 assert_eq!(height_px(&v), want_h, "selected={selected}: wrong cell height");
1628 }
1629 assert_eq!(width_px(&blanks), want_w, "a blank cell is not a full column wide");
1630 assert_eq!(height_px(&blanks), want_h, "a blank cell is not a full row tall");
1631 assert_eq!(
1632 width_px(&headers),
1633 want_w,
1634 "the weekday header column is not the same width as a day cell",
1635 );
1636 }
1637
1638 #[test]
1639 fn build_day_cell_style_is_pure() {
1640 for selected in [false, true] {
1641 assert_eq!(
1642 properties(&build_day_cell_style(selected)),
1643 properties(&build_day_cell_style(selected)),
1644 "selected={selected}: two identical calls produced different styles",
1645 );
1646 }
1647 }
1648
1649 #[test]
1654 fn create_stores_the_date_it_was_given_and_installs_no_callback() {
1655 let p = DatePicker::create(2024, 7, 4);
1656 assert_eq!(
1657 p.state.inner,
1658 DatePickerState { year: 2024, month: 7, day: 4 },
1659 );
1660 assert!(
1661 p.state.on_change.as_ref().is_none(),
1662 "create invented a change callback out of nowhere",
1663 );
1664 }
1665
1666 #[test]
1667 fn create_clamps_the_month_into_one_to_twelve() {
1668 assert_eq!(DatePicker::create(2024, 0, 1).state.inner.month, 1);
1669 assert_eq!(DatePicker::create(2024, 13, 1).state.inner.month, 12);
1670 assert_eq!(DatePicker::create(2024, u32::MAX, 1).state.inner.month, 12);
1671 for month in 1..=12u32 {
1673 assert_eq!(DatePicker::create(2024, month, 1).state.inner.month, month);
1674 }
1675 }
1676
1677 #[test]
1678 fn create_clamps_the_day_into_the_real_length_of_the_month() {
1679 for (y, m, d, want) in [
1683 (2024u32, 2u32, 31u32, 29u32), (2023, 2, 31, 28), (2024, 2, 30, 29),
1686 (2024, 4, 31, 30), (2024, 1, 31, 31), (2024, 1, 0, 1), (2024, 6, u32::MAX, 30),
1690 (2024, 0, 99, 31), (2024, 13, 99, 31), ] {
1693 assert_eq!(
1694 DatePicker::create(y, m, d).state.inner.day,
1695 want,
1696 "create({y}, {m}, {d}) did not clamp the day correctly",
1697 );
1698 }
1699 }
1700
1701 #[test]
1702 fn create_never_leaves_an_impossible_date_for_any_input() {
1703 for year in [0u32, 1, 1899, 1900, 2000, 2023, 2024, u32::MAX] {
1704 for month in [0u32, 1, 2, 11, 12, 13, u32::MAX] {
1705 for day in [0u32, 1, 28, 29, 30, 31, 32, u32::MAX] {
1706 let s = DatePicker::create(year, month, day).state.inner;
1707 assert!(
1708 (1..=12).contains(&s.month),
1709 "create({year}, {month}, {day}) left month {}",
1710 s.month,
1711 );
1712 let dim = days_in_month(s.year, s.month);
1713 assert!(
1714 (1..=dim).contains(&s.day),
1715 "create({year}, {month}, {day}) left day {} in a {dim}-day month",
1716 s.day,
1717 );
1718 }
1719 }
1720 }
1721 }
1722
1723 #[test]
1724 fn create_passes_the_year_through_untouched() {
1725 for year in [0u32, 1, u32::MAX] {
1729 assert_eq!(DatePicker::create(year, 1, 1).state.inner.year, year);
1730 }
1731 }
1732
1733 #[test]
1734 fn create_is_pure_and_distinct_dates_stay_distinguishable() {
1735 assert_eq!(DatePicker::create(2024, 7, 4), DatePicker::create(2024, 7, 4));
1736 assert_ne!(DatePicker::create(2024, 7, 4), DatePicker::create(2024, 7, 5));
1737 assert_ne!(DatePicker::create(2024, 7, 4), DatePicker::create(2024, 8, 4));
1738 assert_ne!(DatePicker::create(2024, 7, 4), DatePicker::create(2025, 7, 4));
1739 assert_eq!(DatePicker::create(2023, 2, 31), DatePicker::create(2023, 2, 28));
1741 }
1742
1743 #[test]
1744 fn default_is_the_first_of_january_2000() {
1745 assert_eq!(DatePicker::default(), DatePicker::create(2000, 1, 1));
1746 assert_eq!(
1747 DatePickerState::default(),
1748 DatePickerState { year: 2000, month: 1, day: 1 },
1749 );
1750 assert_eq!(
1751 DatePickerStateWrapper::default().inner,
1752 DatePickerState::default(),
1753 );
1754 }
1755
1756 #[test]
1761 fn set_on_change_stores_the_function_pointer_and_the_payload_verbatim() {
1762 let mut p = DatePicker::create(2024, 1, 1);
1763 p.set_on_change(
1764 RefAny::new(0xDEAD_BEEF_u32),
1765 change_do_nothing as DatePickerOnChangeCallbackType,
1766 );
1767
1768 let c = p
1769 .state
1770 .on_change
1771 .as_ref()
1772 .expect("set_on_change did not store anything");
1773 assert_eq!(
1774 c.callback.cb as *const () as usize,
1775 change_do_nothing as *const () as usize,
1776 "the stored function pointer is not the one that was handed in",
1777 );
1778 let mut payload = c.refany.clone();
1779 assert_eq!(
1780 *payload.downcast_ref::<u32>().expect("the payload changed type"),
1781 0xDEAD_BEEF,
1782 );
1783 }
1784
1785 #[test]
1786 fn set_on_change_replaces_rather_than_accumulates() {
1787 let mut p = DatePicker::create(2024, 1, 1);
1788 p.set_on_change(RefAny::new(1u8), change_do_nothing as DatePickerOnChangeCallbackType);
1789 p.set_on_change(RefAny::new(2u8), change_refresh_all as DatePickerOnChangeCallbackType);
1790
1791 let c = p.state.on_change.as_ref().expect("the callback vanished");
1792 assert_eq!(
1793 c.callback.cb as *const () as usize,
1794 change_refresh_all as *const () as usize,
1795 "the second set_on_change did not win",
1796 );
1797 let mut payload = c.refany.clone();
1798 assert_eq!(*payload.downcast_ref::<u8>().expect("wrong payload type"), 2);
1799 }
1800
1801 #[test]
1802 fn set_on_change_does_not_disturb_the_date_or_the_container_style() {
1803 let before = DatePicker::create(2023, 2, 31);
1804 let mut after = DatePicker::create(2023, 2, 31);
1805 after.set_on_change(RefAny::new(0u8), change_do_nothing as DatePickerOnChangeCallbackType);
1806
1807 assert_eq!(after.state.inner, before.state.inner, "installing a callback moved the date");
1808 assert_eq!(
1809 properties(&after.container_style),
1810 properties(&before.container_style),
1811 "installing a callback restyled the container",
1812 );
1813 }
1814
1815 #[test]
1816 fn with_on_change_is_exactly_set_on_change_in_builder_form() {
1817 let built = DatePicker::create(2024, 5, 9)
1818 .with_on_change(RefAny::new(7u32), change_do_nothing as DatePickerOnChangeCallbackType);
1819 let mut set = DatePicker::create(2024, 5, 9);
1820 set.set_on_change(RefAny::new(7u32), change_do_nothing as DatePickerOnChangeCallbackType);
1821
1822 assert_eq!(built.state.inner, set.state.inner);
1823 let a = built.state.on_change.as_ref().expect("builder dropped the callback");
1824 let b = set.state.on_change.as_ref().expect("setter dropped the callback");
1825 assert_eq!(a.callback.cb as *const () as usize, b.callback.cb as *const () as usize);
1826
1827 let (mut pa, mut pb) = (a.refany.clone(), b.refany.clone());
1828 assert_eq!(
1829 *pa.downcast_ref::<u32>().expect("builder payload changed type"),
1830 *pb.downcast_ref::<u32>().expect("setter payload changed type"),
1831 );
1832 }
1833
1834 #[test]
1835 fn with_on_change_accepts_a_generic_callback_without_mangling_the_pointer() {
1836 let generic = Callback {
1840 cb: generic_shaped,
1841 ctx: OptionRefAny::None,
1842 };
1843 let expected = generic_shaped as *const () as usize;
1844
1845 let p = DatePicker::create(2024, 1, 1).with_on_change(RefAny::new(0u8), generic);
1846 let c = p.state.on_change.as_ref().expect("the generic callback was dropped");
1847 assert_eq!(
1848 c.callback.cb as *const () as usize,
1849 expected,
1850 "the Callback -> DatePickerOnChangeCallback transmute mangled the pointer",
1851 );
1852 }
1853
1854 #[test]
1859 fn swap_with_default_hands_out_the_original_and_leaves_a_default_behind() {
1860 let mut p = DatePicker::create(2024, 7, 4);
1861 let taken = p.swap_with_default();
1862
1863 assert_eq!(taken.state.inner, DatePickerState { year: 2024, month: 7, day: 4 });
1864 assert_eq!(p, DatePicker::default(), "the picker left behind is not a default one");
1865 }
1866
1867 #[test]
1868 fn swap_with_default_carries_the_callback_out_with_the_original() {
1869 let mut p = DatePicker::create(2024, 7, 4)
1872 .with_on_change(RefAny::new(3u8), change_do_nothing as DatePickerOnChangeCallbackType);
1873 let taken = p.swap_with_default();
1874
1875 assert!(taken.state.on_change.as_ref().is_some(), "the callback did not leave with the original");
1876 assert!(p.state.on_change.as_ref().is_none(), "the callback stayed behind on the default");
1877 }
1878
1879 #[test]
1880 fn swapping_a_default_twice_is_idempotent() {
1881 let mut p = DatePicker::default();
1882 let first = p.swap_with_default();
1883 let second = p.swap_with_default();
1884 assert_eq!(first, DatePicker::default());
1885 assert_eq!(second, DatePicker::default());
1886 assert_eq!(p, DatePicker::default());
1887 }
1888
1889 #[test]
1894 fn dom_builds_a_header_a_weekday_row_and_a_grid() {
1895 let dom = DatePicker::create(2024, 1, 15).dom();
1896
1897 assert!(matches!(dom.root.get_node_type(), NodeType::Div));
1898 assert_eq!(classes(&dom), vec!["__azul-native-date-picker".to_string()]);
1899
1900 let (header, weekdays, grid) = sections(&dom);
1901 assert_eq!(classes(header), vec!["__azul-native-date-picker-header".to_string()]);
1902 assert_eq!(classes(weekdays), vec!["__azul-native-date-picker-weekdays".to_string()]);
1903 assert_eq!(classes(grid), vec!["__azul-native-date-picker-grid".to_string()]);
1904 assert_eq!(
1905 header.children.as_ref().len(),
1906 3,
1907 "the header must be prev / label / next",
1908 );
1909 }
1910
1911 #[test]
1912 fn dom_labels_the_header_with_the_month_name_and_the_year() {
1913 for (y, m, want) in [
1914 (2024u32, 1u32, "January 2024"),
1915 (2024, 12, "December 2024"),
1916 (0, 6, "June 0"),
1917 (u32::MAX, 2, "February 4294967295"),
1918 ] {
1919 let dom = DatePicker::create(y, m, 1).dom();
1920 let (header, _, _) = sections(&dom);
1921 let kids = header.children.as_ref();
1922 assert_eq!(text_of(&kids[0]).as_deref(), Some("\u{2039}"), "wrong prev arrow");
1923 assert_eq!(text_of(&kids[1]).as_deref(), Some(want), "wrong header label");
1924 assert_eq!(text_of(&kids[2]).as_deref(), Some("\u{203A}"), "wrong next arrow");
1925 }
1926 }
1927
1928 #[test]
1929 fn dom_names_the_seven_weekdays_starting_at_sunday() {
1930 let dom = DatePicker::create(2024, 1, 1).dom();
1934 let (_, weekdays, _) = sections(&dom);
1935 let names: Vec<Option<String>> = weekdays.children.as_ref().iter().map(text_of).collect();
1936 assert_eq!(
1937 names,
1938 ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
1939 .iter()
1940 .map(|s| Some((*s).to_string()))
1941 .collect::<Vec<_>>(),
1942 );
1943 }
1944
1945 #[test]
1946 fn dom_opens_the_month_after_exactly_weekday_of_the_first_blank_cells() {
1947 for (y, m, want_leading) in [
1950 (2024u32, 1u32, 1usize),
1951 (2024, 2, 4),
1952 (2015, 2, 0),
1953 (2021, 5, 6), ] {
1955 let dom = DatePicker::create(y, m, 1).dom();
1956 let (_, _, grid) = sections(&dom);
1957 let days = day_numbers(grid);
1958 let leading = days.iter().take_while(|d| d.is_none()).count();
1959 assert_eq!(
1960 leading, want_leading,
1961 "{y}-{m}: wrong number of leading blank cells",
1962 );
1963 assert_eq!(
1964 u32::try_from(leading).unwrap(),
1965 weekday(y, m, 1),
1966 "{y}-{m}: the leading blanks disagree with the weekday of the 1st",
1967 );
1968 assert_eq!(days[leading], Some(1), "{y}-{m}: the 1st is not the first day cell");
1969 }
1970 }
1971
1972 #[test]
1973 fn dom_renders_every_day_of_the_month_exactly_once_and_in_order() {
1974 for year in [1900u32, 2000, 2023, 2024] {
1975 for month in 1..=12u32 {
1976 let dom = DatePicker::create(year, month, 1).dom();
1977 let (_, _, grid) = sections(&dom);
1978 let days: Vec<u32> = day_numbers(grid).into_iter().flatten().collect();
1979 let dim = days_in_month(year, month);
1980 assert_eq!(
1981 days,
1982 (1..=dim).collect::<Vec<_>>(),
1983 "{year}-{month}: the grid does not render 1..={dim} in order",
1984 );
1985 }
1986 }
1987 }
1988
1989 #[test]
1990 fn dom_grid_rows_are_always_full_weeks_and_never_wholly_blank() {
1991 for year in [1900u32, 2015, 2021, 2024] {
1994 for month in 1..=12u32 {
1995 let dom = DatePicker::create(year, month, 1).dom();
1996 let (_, _, grid) = sections(&dom);
1997 let rows = grid.children.as_ref();
1998 assert!(
1999 (4..=6).contains(&rows.len()),
2000 "{year}-{month}: {} week rows is not a possible month",
2001 rows.len(),
2002 );
2003 for (i, row) in rows.iter().enumerate() {
2004 assert_eq!(
2005 row.children.as_ref().len(),
2006 7,
2007 "{year}-{month}: week row {i} is not seven columns wide",
2008 );
2009 assert_eq!(classes(row), vec!["__azul-native-date-picker-week".to_string()]);
2010 }
2011 let last = &rows[rows.len() - 1];
2012 assert!(
2013 last.children.as_ref().iter().any(|c| text_of(c).is_some()),
2014 "{year}-{month}: the last week row is entirely blank",
2015 );
2016 }
2017 }
2018 }
2019
2020 #[test]
2021 fn dom_accents_exactly_the_selected_day_and_nothing_else() {
2022 for day in [1u32, 15, 29] {
2023 let dom = DatePicker::create(2024, 2, day).dom();
2024 let (_, _, grid) = sections(&dom);
2025 let accented: Vec<Option<u32>> = grid_cells(grid)
2026 .into_iter()
2027 .filter(|c| rendered_background(c) == Some(ACCENT_BG))
2028 .map(|c| text_of(c).map(|t| t.parse().unwrap()))
2029 .collect();
2030 assert_eq!(
2031 accented,
2032 vec![Some(day)],
2033 "selecting day {day} did not highlight exactly that one cell",
2034 );
2035 }
2036 }
2037
2038 #[test]
2039 fn dom_clamps_an_out_of_range_month_before_computing_the_grid() {
2040 for (raw, want_name, want_dim) in [(0u32, "January", 31u32), (13, "December", 31), (u32::MAX, "December", 31)] {
2044 let mut p = DatePicker::create(2024, 1, 1);
2045 p.state.inner.month = raw;
2046 let dom = p.dom();
2047
2048 let (header, _, grid) = sections(&dom);
2049 assert_eq!(
2050 text_of(&header.children.as_ref()[1]).as_deref(),
2051 Some(format!("{want_name} 2024").as_str()),
2052 "raw month {raw} produced a nonsense header",
2053 );
2054 let days: Vec<u32> = day_numbers(grid).into_iter().flatten().collect();
2055 assert_eq!(
2056 u32::try_from(days.len()).unwrap(),
2057 want_dim,
2058 "raw month {raw} produced a grid of the wrong length",
2059 );
2060 }
2061 }
2062
2063 #[test]
2064 fn dom_survives_a_saturated_or_zero_year_without_panicking() {
2065 for (y, m, d) in [(u32::MAX, u32::MAX, u32::MAX), (u32::MAX, 1, 1), (0, 0, 0), (1, 12, 31)] {
2068 let dom = DatePicker::create(y, m, d).dom();
2069 let (_, _, grid) = sections(&dom);
2070 let days: Vec<u32> = day_numbers(grid).into_iter().flatten().collect();
2071 let clamped_month = m.clamp(1, 12);
2072 assert_eq!(
2073 u32::try_from(days.len()).unwrap(),
2074 days_in_month(y, clamped_month),
2075 "create({y}, {m}, {d}).dom() rendered the wrong number of days",
2076 );
2077 }
2078 }
2079
2080 #[test]
2081 fn dom_gives_every_day_cell_a_mouse_up_handler_and_the_blanks_none() {
2082 let dom = DatePicker::create(2024, 2, 10).dom();
2083 let (_, _, grid) = sections(&dom);
2084
2085 let mut with_handler = 0;
2086 for cell in grid_cells(grid) {
2087 assert_eq!(
2088 classes(cell),
2089 vec!["__azul-native-date-picker-day".to_string()],
2090 "a grid cell lost the day class",
2091 );
2092 let cbs = cell.root.callbacks.as_ref();
2093 if text_of(cell).is_some() {
2094 assert_eq!(cbs.len(), 1, "a day cell must register exactly one handler");
2095 assert_eq!(cbs[0].event, EventFilter::Hover(HoverEventFilter::MouseUp));
2096 assert_eq!(cbs[0].callback.cb, on_day_click as usize);
2097 assert_eq!(
2098 cell.root.flags.get_tab_index(),
2099 Some(TabIndex::Auto),
2100 "a day cell is not keyboard-focusable",
2101 );
2102 with_handler += 1;
2103 } else {
2104 assert!(cbs.is_empty(), "a blank cell registered a click handler");
2105 }
2106 }
2107 assert_eq!(with_handler, days_in_month(2024, 2), "not every day is clickable");
2108 }
2109
2110 #[test]
2111 fn dom_bakes_a_distinct_day_number_into_every_cell_payload() {
2112 let styled = StyledDom::create_from_dom(DatePicker::create(2024, 2, 1).dom());
2116 let mut seen: Vec<u32> = Vec::new();
2117 for nd in styled.node_data.as_ref() {
2118 for cb in nd.callbacks.as_ref() {
2119 let mut r = cb.refany.clone();
2120 if let Some(cell) = r.downcast_ref::<DayCellData>() {
2121 seen.push(cell.day);
2122 };
2123 }
2124 }
2125 seen.sort_unstable();
2126 assert_eq!(
2127 seen,
2128 (1..=29).collect::<Vec<u32>>(),
2129 "the baked day numbers are not exactly 1..=29 of February 2024",
2130 );
2131 }
2132
2133 #[test]
2134 fn dom_keeps_its_cached_child_count_in_sync_with_the_tree() {
2135 for (y, m) in [(2024u32, 1u32), (2015, 2), (2021, 5), (u32::MAX, 12)] {
2138 let dom = DatePicker::create(y, m, 1).dom();
2139 assert_eq!(
2140 dom.estimated_total_children,
2141 descendants(&dom),
2142 "{y}-{m}: the cached descendant count is wrong",
2143 );
2144
2145 let rows = dom.children.as_ref()[2].children.as_ref().len();
2146 let styled = StyledDom::create_from_dom(dom);
2147 assert_eq!(
2148 styled.node_data.as_ref().len(),
2149 14 + 8 * rows,
2150 "{y}-{m}: container + header(3) + weekdays(7) + grid of {rows} weeks did not flatten as expected",
2151 );
2152 }
2153 }
2154
2155 #[test]
2156 fn from_datepicker_for_dom_is_the_dom_method() {
2157 let via_from: Dom = DatePicker::create(2024, 3, 7).into();
2158 let via_dom = DatePicker::create(2024, 3, 7).dom();
2159 assert_eq!(classes(&via_from), classes(&via_dom));
2160 assert_eq!(via_from.children.as_ref().len(), via_dom.children.as_ref().len());
2161 assert_eq!(via_from.estimated_total_children, via_dom.estimated_total_children);
2162 }
2163
2164 #[test]
2169 fn build_grid_uses_the_thirty_day_fallback_for_an_out_of_range_month() {
2170 for month in [0u32, 13, u32::MAX] {
2173 let grid = build_grid(2024, month, 1, RefAny::new(DatePickerStateWrapper::default()));
2174 let days: Vec<u32> = day_numbers(&grid).into_iter().flatten().collect();
2175 assert_eq!(
2176 days,
2177 (1..=30).collect::<Vec<_>>(),
2178 "month {month} did not fall back to a 30-day grid",
2179 );
2180 }
2181 }
2182
2183 #[test]
2184 fn build_grid_never_highlights_more_than_one_cell() {
2185 for sel in [0u32, 1, 29, 30, 31, 32, u32::MAX] {
2188 let grid = build_grid(2024, 2, sel, RefAny::new(DatePickerStateWrapper::default()));
2189 let accented = grid_cells(&grid)
2190 .into_iter()
2191 .filter(|c| rendered_background(c) == Some(ACCENT_BG))
2192 .count();
2193 let want = usize::from((1..=29).contains(&sel));
2194 assert_eq!(
2195 accented, want,
2196 "sel_day={sel} highlighted {accented} cells in a 29-day February",
2197 );
2198 }
2199 }
2200
2201 #[test]
2202 fn build_grid_is_total_across_extreme_years() {
2203 for year in [0u32, 1, 1900, 2024, MAX_SAFE_WEEKDAY_YEAR, u32::MAX] {
2204 for month in [0u32, 1, 2, 12, 13] {
2205 let grid = build_grid(year, month, 1, RefAny::new(DatePickerStateWrapper::default()));
2206 let rows = grid.children.as_ref();
2207 assert!(
2208 (4..=6).contains(&rows.len()),
2209 "build_grid({year}, {month}) produced {} week rows",
2210 rows.len(),
2211 );
2212 for row in rows {
2213 assert_eq!(row.children.as_ref().len(), 7);
2214 }
2215 }
2216 }
2217 }
2218
2219 #[test]
2220 fn build_header_is_total_for_out_of_range_months() {
2221 for (month, want) in [(0u32, "January 2024"), (12, "December 2024"), (13, " 2024"), (u32::MAX, " 2024")] {
2224 let header = build_header(2024, month, RefAny::new(DatePickerStateWrapper::default()));
2225 let kids = header.children.as_ref();
2226 assert_eq!(kids.len(), 3);
2227 assert_eq!(
2228 text_of(&kids[1]).as_deref(),
2229 Some(want),
2230 "build_header(2024, {month}) produced the wrong label",
2231 );
2232 }
2233 }
2234
2235 #[test]
2236 fn build_header_wires_prev_and_next_to_different_handlers() {
2237 let header = build_header(2024, 6, RefAny::new(DatePickerStateWrapper::default()));
2240 let kids = header.children.as_ref();
2241 let prev = kids[0].root.callbacks.as_ref();
2242 let next = kids[2].root.callbacks.as_ref();
2243
2244 assert_eq!(prev.len(), 1);
2245 assert_eq!(next.len(), 1);
2246 assert_eq!(prev[0].callback.cb, on_prev_month as usize);
2247 assert_eq!(next[0].callback.cb, on_next_month as usize);
2248 assert_ne!(prev[0].callback.cb, next[0].callback.cb);
2249 assert!(kids[1].root.callbacks.as_ref().is_empty(), "the header label is clickable");
2251 }
2252
2253 #[test]
2254 fn build_weekday_row_has_exactly_seven_inert_cells() {
2255 let row = build_weekday_row();
2256 let cells = row.children.as_ref();
2257 assert_eq!(cells.len(), 7);
2258 for cell in cells {
2259 assert!(cell.root.callbacks.as_ref().is_empty(), "a weekday header is clickable");
2260 assert_eq!(classes(cell), vec!["__azul-native-date-picker-weekday".to_string()]);
2261 }
2262 }
2263
2264 #[test]
2265 fn build_blank_cell_is_an_untexted_uncallbacked_column_placeholder() {
2266 let blank = build_blank_cell();
2267 assert!(matches!(blank.root.get_node_type(), NodeType::Div));
2268 assert_eq!(text_of(&blank), None, "a blank cell renders text");
2269 assert!(blank.root.callbacks.as_ref().is_empty(), "a blank cell is clickable");
2270 assert!(blank.children.as_ref().is_empty());
2271 assert_eq!(classes(&blank), vec!["__azul-native-date-picker-day".to_string()]);
2272 }
2273
2274 #[test]
2275 fn build_day_cell_renders_and_bakes_whatever_number_it_is_given() {
2276 for day in [0u32, 1, 31, 99, u32::MAX] {
2279 for selected in [false, true] {
2280 let cell = build_day_cell(day, selected, RefAny::new(DatePickerStateWrapper::default()));
2281 assert_eq!(
2282 text_of(&cell).as_deref(),
2283 Some(day.to_string().as_str()),
2284 "day {day} did not render as its own number",
2285 );
2286 assert_eq!(
2287 rendered_background(&cell),
2288 Some(if selected { ACCENT_BG } else { TRANSPARENT }),
2289 "day {day} selected={selected}: wrong background",
2290 );
2291
2292 let cbs = cell.root.callbacks.as_ref();
2293 assert_eq!(cbs.len(), 1);
2294 let mut r = cbs[0].refany.clone();
2295 let baked = r
2296 .downcast_ref::<DayCellData>()
2297 .expect("a day cell no longer carries a DayCellData")
2298 .day;
2299 assert_eq!(baked, day, "the rendered number and the baked one disagree");
2300 }
2301 }
2302 }
2303
2304 #[test]
2309 fn clicking_a_day_selects_it_and_restyles_the_whole_grid() {
2310 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2311 let (cell, payload) = day_cell(&styled, 17);
2312 let rows = 5; let (update, changes) = click(styled, &payload, cell);
2315
2316 assert_eq!(
2317 read_state(&shared),
2318 DatePickerState { year: 2024, month: 2, day: 17 },
2319 "the click did not move the selection to the clicked day",
2320 );
2321 assert_eq!(
2322 update,
2323 Update::DoNothing,
2324 "with no user callback installed the handler must report DoNothing",
2325 );
2326
2327 let bgs = pushed_backgrounds(&changes);
2328 assert_eq!(
2329 bgs.len(),
2330 rows * 7,
2331 "restyle_days must repaint every cell of every week row",
2332 );
2333 let accented: Vec<NodeId> = bgs
2334 .iter()
2335 .filter(|(_, c)| *c == ACCENT_BG)
2336 .map(|(n, _)| *n)
2337 .collect();
2338 assert_eq!(
2339 accented,
2340 vec![cell.node.into_crate_internal().unwrap()],
2341 "exactly the clicked cell must end up accented",
2342 );
2343 assert!(
2344 bgs.iter().filter(|(n, _)| *n != accented[0]).all(|(_, c)| *c == TRANSPARENT),
2345 "a cell other than the clicked one kept a background",
2346 );
2347
2348 let texts = pushed_text_colours(&changes);
2349 assert_eq!(texts.len(), rows * 7, "every cell needs its text colour resynced too");
2350 assert_eq!(
2351 texts.iter().filter(|(_, c)| *c == WHITE).map(|(n, _)| *n).collect::<Vec<_>>(),
2352 accented,
2353 "the white-on-accent text did not land on the accented cell",
2354 );
2355 }
2356
2357 #[test]
2358 fn clicking_a_foreign_payload_changes_nothing_at_all() {
2359 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2362 let (cell, _) = day_cell(&styled, 17);
2363 let before = read_state(&shared);
2364
2365 let (update, changes) = click(styled, &RefAny::new(0xBAD_u32), cell);
2366
2367 assert_eq!(update, Update::DoNothing);
2368 assert!(changes.is_empty(), "a foreign payload still restyled the grid");
2369 assert_eq!(read_state(&shared), before, "a foreign payload moved the selection");
2370 }
2371
2372 #[test]
2373 fn clicking_a_detached_node_updates_the_state_but_pushes_no_style() {
2374 for hit in [node_none(), node(9999)] {
2378 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2379 let (_, payload) = day_cell(&styled, 17);
2380
2381 let (update, changes) = click(styled, &payload, hit);
2382
2383 assert_eq!(update, Update::DoNothing, "{hit:?}: unexpected verdict");
2384 assert!(changes.is_empty(), "{hit:?}: a detached hit still pushed a style change");
2385 assert_eq!(read_state(&shared).day, 17, "{hit:?}: the selection was not committed");
2386 }
2387 }
2388
2389 #[test]
2390 fn clicking_a_node_without_a_grandparent_restyles_nothing() {
2391 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2395 let (_, payload) = day_cell(&styled, 3);
2396
2397 let (_, changes) = click(styled, &payload, node(0));
2398
2399 assert!(changes.is_empty(), "restyling from the root touched nodes anyway");
2400 assert_eq!(read_state(&shared).day, 3);
2401 }
2402
2403 #[test]
2404 fn the_change_callback_sees_the_clicked_day_and_its_verdict_is_forwarded() {
2405 let probe = log_refany();
2408 let (styled, shared) = laid_out(
2409 DatePicker::create(2024, 2, 1)
2410 .with_on_change(probe.clone(), record_change as DatePickerOnChangeCallbackType),
2411 );
2412 let (cell, payload) = day_cell(&styled, 29);
2413
2414 let (update, changes) = click(styled, &payload, cell);
2415
2416 let log = read_log(&probe);
2417 assert_eq!(
2418 log.seen,
2419 vec![DatePickerState { year: 2024, month: 2, day: 29 }],
2420 "the change callback was not called exactly once with the NEW state",
2421 );
2422 assert_eq!(
2423 log.payload, 0xDEAD_BEEF,
2424 "the callback was handed something other than the user's own RefAny",
2425 );
2426 assert_eq!(update, Update::RefreshDom, "the user callback's Update was swallowed");
2427 assert_eq!(read_state(&shared).day, 29);
2428 assert!(!changes.is_empty(), "a RefreshDom callback suppressed the restyle");
2430 }
2431
2432 #[test]
2433 fn a_change_callback_that_declines_the_update_still_gets_the_grid_restyled() {
2434 let (styled, shared) = laid_out(
2435 DatePicker::create(2024, 2, 1)
2436 .with_on_change(RefAny::new(0u8), change_do_nothing as DatePickerOnChangeCallbackType),
2437 );
2438 let (cell, payload) = day_cell(&styled, 12);
2439
2440 let (update, changes) = click(styled, &payload, cell);
2441
2442 assert_eq!(update, Update::DoNothing);
2443 assert_eq!(read_state(&shared).day, 12);
2444 assert!(
2445 !changes.is_empty(),
2446 "a DoNothing user callback suppressed the widget's own repaint",
2447 );
2448 }
2449
2450 #[test]
2451 fn clicking_every_day_of_the_month_keeps_the_state_and_the_accent_in_agreement() {
2452 for day in 1..=29u32 {
2456 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2457 let (cell, payload) = day_cell(&styled, day);
2458
2459 let (_, changes) = click(styled, &payload, cell);
2460
2461 assert_eq!(read_state(&shared).day, day, "click #{day}: the state drifted");
2462 let accented: Vec<NodeId> = pushed_backgrounds(&changes)
2463 .into_iter()
2464 .filter(|(_, c)| *c == ACCENT_BG)
2465 .map(|(n, _)| n)
2466 .collect();
2467 assert_eq!(
2468 accented,
2469 vec![cell.node.into_crate_internal().unwrap()],
2470 "click #{day}: the accent disagrees with the clicked cell",
2471 );
2472 }
2473 }
2474
2475 #[test]
2476 fn repeated_clicks_on_the_same_cell_are_idempotent() {
2477 let (styled, shared) = laid_out(DatePicker::create(2024, 2, 1));
2478 let (cell, payload) = day_cell(&styled, 20);
2479 let styled2 = StyledDom::create_from_dom(DatePicker::create(2024, 2, 1).dom());
2480
2481 let (_, first) = click(styled, &payload, cell);
2482 let (_, second) = click(styled2, &payload, cell);
2483
2484 assert_eq!(
2485 pushed_backgrounds(&first),
2486 pushed_backgrounds(&second),
2487 "clicking the same cell twice produced different repaints",
2488 );
2489 assert_eq!(read_state(&shared).day, 20);
2490 }
2491
2492 #[test]
2497 fn next_month_wraps_december_into_january_of_the_following_year() {
2498 let (s, _, _) = press_nav(DatePickerState { year: 2024, month: 12, day: 15 }, true, 1);
2499 assert_eq!(s, DatePickerState { year: 2025, month: 1, day: 15 });
2500 }
2501
2502 #[test]
2503 fn prev_month_wraps_january_into_december_of_the_preceding_year() {
2504 let (s, _, _) = press_nav(DatePickerState { year: 2024, month: 1, day: 15 }, false, 1);
2505 assert_eq!(s, DatePickerState { year: 2023, month: 12, day: 15 });
2506 }
2507
2508 #[test]
2509 fn twelve_presses_in_either_direction_land_on_the_same_month_one_year_away() {
2510 let start = DatePickerState { year: 2024, month: 5, day: 15 };
2511 let (fwd, _, _) = press_nav(start, true, 12);
2512 let (back, _, _) = press_nav(start, false, 12);
2513 assert_eq!(fwd, DatePickerState { year: 2025, month: 5, day: 15 });
2514 assert_eq!(back, DatePickerState { year: 2023, month: 5, day: 15 });
2515 }
2516
2517 #[test]
2518 fn month_nav_walks_every_month_in_order_across_a_year_boundary() {
2519 let mut state = DatePickerState { year: 2023, month: 11, day: 10 };
2520 let expected = [
2521 (2023u32, 12u32), (2024, 1), (2024, 2), (2024, 3), (2024, 4), (2024, 5),
2522 (2024, 6), (2024, 7), (2024, 8), (2024, 9), (2024, 10), (2024, 11),
2523 (2024, 12), (2025, 1),
2524 ];
2525 for (i, (y, m)) in expected.iter().enumerate() {
2526 let (next, _, _) = press_nav(state, true, 1);
2527 assert_eq!(
2528 (next.year, next.month),
2529 (*y, *m),
2530 "step {i}: the month walk went off the rails",
2531 );
2532 state = next;
2533 }
2534 }
2535
2536 #[test]
2537 fn month_nav_clamps_the_selected_day_into_the_shorter_month_and_never_grows_it_back() {
2538 let jan31 = DatePickerState { year: 2024, month: 1, day: 31 };
2543 let (feb, _, _) = press_nav(jan31, true, 1);
2544 assert_eq!(feb, DatePickerState { year: 2024, month: 2, day: 29 });
2545
2546 let (mar, _, _) = press_nav(feb, true, 1);
2547 assert_eq!(mar, DatePickerState { year: 2024, month: 3, day: 29 }, "the clamp grew back");
2548
2549 let (feb23, _, _) = press_nav(DatePickerState { year: 2023, month: 1, day: 31 }, true, 1);
2551 assert_eq!(feb23.day, 28);
2552 let (apr, _, _) = press_nav(DatePickerState { year: 2024, month: 3, day: 31 }, true, 1);
2554 assert_eq!(apr, DatePickerState { year: 2024, month: 4, day: 30 });
2555 }
2556
2557 #[test]
2558 fn month_nav_leaves_a_renderable_day_for_every_month_of_a_two_year_walk() {
2559 for start_day in [1u32, 28, 29, 30, 31] {
2563 let mut state = DatePickerState { year: 2023, month: 1, day: start_day };
2564 for step in 0..24 {
2565 let (next, _, _) = press_nav(state, true, 1);
2566 assert!(
2567 (1..=12).contains(&next.month),
2568 "step {step} from day {start_day}: month {} is out of range",
2569 next.month,
2570 );
2571 assert!(
2572 next.day <= days_in_month(next.year, next.month),
2573 "step {step} from day {start_day}: day {} does not exist in {}-{}",
2574 next.day,
2575 next.year,
2576 next.month,
2577 );
2578 state = next;
2579 }
2580 }
2581 }
2582
2583 #[test]
2584 fn month_nav_floors_the_year_at_one_instead_of_wrapping_below_zero() {
2585 let (from_one, _, _) = press_nav(DatePickerState { year: 1, month: 1, day: 1 }, false, 1);
2588 assert_eq!(from_one, DatePickerState { year: 1, month: 12, day: 1 });
2589
2590 let (from_zero, _, _) = press_nav(DatePickerState { year: 0, month: 1, day: 1 }, false, 1);
2591 assert_eq!(from_zero, DatePickerState { year: 1, month: 12, day: 1 });
2592
2593 let (deep, _, _) = press_nav(DatePickerState { year: 1, month: 1, day: 1 }, false, 20);
2595 assert!(deep.year >= 1, "the year floor leaked, got {}", deep.year);
2596 assert!((1..=12).contains(&deep.month));
2597 }
2598
2599 #[test]
2600 fn month_nav_collapses_a_saturated_year_to_the_floor_without_panicking() {
2601 for next in [false, true] {
2604 let (s, _, _) = press_nav(DatePickerState { year: u32::MAX, month: 6, day: 1 }, next, 1);
2605 assert_eq!(s.year, 1, "next={next}: a saturated year did not collapse to the floor");
2606 assert!((1..=12).contains(&s.month));
2607 }
2608 let (a, _, _) = press_nav(DatePickerState { year: u32::MAX, month: 12, day: 1 }, true, 1);
2611 assert_eq!(a, DatePickerState { year: 1, month: 1, day: 1 });
2612 let (b, _, _) = press_nav(DatePickerState { year: u32::MAX, month: 1, day: 1 }, false, 1);
2613 assert_eq!(b, DatePickerState { year: 1, month: 12, day: 1 });
2614 }
2615
2616 #[test]
2617 fn month_nav_repairs_an_out_of_range_month_rather_than_propagating_it() {
2618 for raw in [0u32, 13, 99, u32::MAX] {
2622 for next in [false, true] {
2623 let (s, _, _) = press_nav(DatePickerState { year: 2024, month: raw, day: 1 }, next, 1);
2624 assert!(
2625 (1..=12).contains(&s.month),
2626 "raw month {raw}, next={next}: nav left month {}",
2627 s.month,
2628 );
2629 assert!(s.year >= 1);
2630 }
2631 }
2632 }
2633
2634 #[test]
2635 fn month_nav_does_not_touch_the_grid() {
2636 for next in [false, true] {
2640 let (_, _, changes) = press_nav(DatePickerState { year: 2024, month: 3, day: 15 }, next, 1);
2641 assert!(
2642 changes.is_empty(),
2643 "next={next}: month navigation pushed {} DOM change(s)",
2644 changes.len(),
2645 );
2646 }
2647 }
2648
2649 #[test]
2650 fn month_nav_reports_the_new_month_so_the_host_can_rebuild() {
2651 let probe = log_refany();
2654 let shared = RefAny::new(DatePickerStateWrapper {
2655 inner: DatePickerState { year: 2024, month: 1, day: 31 },
2656 on_change: Some(DatePickerOnChange {
2657 callback: DatePickerOnChangeCallback::from(
2658 record_change as DatePickerOnChangeCallbackType,
2659 ),
2660 refany: probe.clone(),
2661 })
2662 .into(),
2663 });
2664
2665 let (update, _) = with_info(StyledDom::default(), node(0), |info| {
2666 on_next_month(shared.clone(), *info)
2667 });
2668
2669 assert_eq!(
2670 read_log(&probe).seen,
2671 vec![DatePickerState { year: 2024, month: 2, day: 29 }],
2672 "the host was not told the new (clamped) month",
2673 );
2674 assert_eq!(update, Update::RefreshDom, "the host's verdict was swallowed");
2675 }
2676
2677 #[test]
2678 fn month_nav_on_the_real_header_buttons_moves_the_real_widget_state() {
2679 for (handler, want) in [
2682 (on_prev_month as usize, DatePickerState { year: 2024, month: 5, day: 10 }),
2683 (on_next_month as usize, DatePickerState { year: 2024, month: 7, day: 10 }),
2684 ] {
2685 let (styled, shared) = laid_out(DatePicker::create(2024, 6, 10));
2686 let (hit, payload) = nav_button(&styled, handler);
2687
2688 let (_, changes) = with_info(styled, hit, |info| {
2689 if handler == on_prev_month as usize {
2690 on_prev_month(payload.clone(), *info)
2691 } else {
2692 on_next_month(payload.clone(), *info)
2693 }
2694 });
2695
2696 assert_eq!(read_state(&shared), want, "the header arrow did not move the state");
2697 assert!(changes.is_empty(), "the header arrow tried to restyle the stale grid");
2698 }
2699 }
2700
2701 #[test]
2702 fn month_nav_with_a_foreign_payload_is_a_no_op() {
2703 for next in [false, true] {
2704 let foreign = RefAny::new(0xBAD_u32);
2705 let (update, changes) = with_info(StyledDom::default(), node(0), |info| {
2706 if next {
2707 on_next_month(foreign.clone(), *info)
2708 } else {
2709 on_prev_month(foreign.clone(), *info)
2710 }
2711 });
2712 assert_eq!(update, Update::DoNothing, "next={next}: a foreign payload was acted on");
2713 assert!(changes.is_empty(), "next={next}: a foreign payload pushed a DOM change");
2714 }
2715 }
2716
2717 #[test]
2718 fn navigating_then_clicking_still_selects_within_the_stale_grid() {
2719 let (styled, shared) = laid_out(DatePicker::create(2024, 6, 10));
2723 let (nav_hit, nav_payload) = nav_button(&styled, on_next_month as usize);
2724 let (cell, cell_payload) = day_cell(&styled, 23);
2725
2726 let (_, _) = with_info(styled, nav_hit, |info| on_next_month(nav_payload.clone(), *info));
2727 assert_eq!(read_state(&shared), DatePickerState { year: 2024, month: 7, day: 10 });
2728
2729 let fresh = StyledDom::create_from_dom(DatePicker::create(2024, 6, 10).dom());
2730 let (_, changes) = click(fresh, &cell_payload, cell);
2731
2732 assert_eq!(
2733 read_state(&shared),
2734 DatePickerState { year: 2024, month: 7, day: 23 },
2735 "a click on the stale grid did not write the displayed day into the state",
2736 );
2737 assert!(!changes.is_empty(), "the stale grid was not restyled");
2738 }
2739}