Skip to main content

frus_widgets/
datetimepicker.rs

1//! [`DateTimePicker`]: a **date-and-time flow** in a single widget — the
2//! [`crate::DatePicker`] calendar above the [`crate::TimePicker`] time picker, topped by
3//! a **summary** of the selection.
4//!
5//! Purely composite and controlled: it only combines the two sub-pickers and relays
6//! their messages; the state, date and time, lives in the application.
7
8use frus_core::{Rect, Scene};
9use frus_layout::{Dimension, FlexDirection, Style};
10
11use crate::datepicker::DatePicker;
12use crate::interaction::Status;
13use crate::text::Text;
14use crate::theme::Theme;
15use crate::timepicker::TimePicker;
16use crate::widget::Widget;
17
18/// A date-and-time picker.
19pub struct DateTimePicker<Msg = crate::callback::Callback> {
20    children: Vec<Box<dyn Widget<Msg>>>,
21}
22
23impl<Msg: Clone + 'static> DateTimePicker<Msg> {
24    /// Creates a date-and-time flow. The **date** part shows `year`/`month` (1–12)
25    /// with `day` selected if there is one — `on_day(day)` on click, `on_nav(±1)` to
26    /// change month — while the **time** part shows `hour`/`minute`
27    /// (`on_hour`/`on_minute`). A "Month day, year  HH:MM" summary tops it all once a
28    /// day is chosen.
29    #[allow(clippy::too_many_arguments)]
30    pub fn new(
31        year: i32,
32        month: u32,
33        day: Option<u32>,
34        hour: u32,
35        minute: u32,
36        on_day: impl Fn(u32) -> Msg + 'static,
37        on_nav: impl Fn(i32) -> Msg + 'static,
38        on_hour: impl Fn(u32) -> Msg + 'static,
39        on_minute: impl Fn(u32) -> Msg + 'static,
40    ) -> Self {
41        let month = month.clamp(1, 12);
42        let (hour, minute) = (hour.min(23), minute.min(59));
43
44        let date = DatePicker::new(year, month, day, on_day, on_nav);
45        let time = TimePicker::new(hour, minute, on_hour, on_minute);
46
47        let mut children: Vec<Box<dyn Widget<Msg>>> = Vec::new();
48        if let Some(day) = day {
49            let summary = format!(
50                "{} {day}, {year}  {hour:02}:{minute:02}",
51                crate::localizations::of().months()[(month - 1) as usize]
52            );
53            children.push(Box::new(Text::new(summary).size(16.0)));
54        }
55        children.push(Box::new(date));
56        children.push(Box::new(time));
57
58        Self { children }
59    }
60}
61
62impl<Msg: Clone> Widget<Msg> for DateTimePicker<Msg> {
63    fn style(&self) -> Style {
64        Style {
65            width: Dimension::Auto,
66            flex_direction: FlexDirection::Column,
67            gap: 16.0,
68            ..Default::default()
69        }
70    }
71
72    fn children(&self) -> &[Box<dyn Widget<Msg>>] {
73        &self.children
74    }
75
76    fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
77
78    fn on_click(&self) -> Option<Msg> {
79        None
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::{build_ui, Runtime, Size};
87    use frus_core::Primitive;
88
89    #[derive(Clone, Debug, PartialEq)]
90    enum Msg {
91        Day(u32),
92        Nav(i32),
93        Hour(u32),
94        Minute(u32),
95    }
96
97    #[test]
98    fn summary_appears_only_when_a_day_is_picked() {
99        // With no day: [date, time]. With one: [summary, date, time].
100        let without = DateTimePicker::new(
101            2026,
102            7,
103            None,
104            9,
105            30,
106            Msg::Day,
107            Msg::Nav,
108            Msg::Hour,
109            Msg::Minute,
110        );
111        assert_eq!(Widget::<Msg>::children(&without).len(), 2);
112
113        let with = DateTimePicker::new(
114            2026,
115            7,
116            Some(11),
117            9,
118            30,
119            Msg::Day,
120            Msg::Nav,
121            Msg::Hour,
122            Msg::Minute,
123        );
124        assert_eq!(Widget::<Msg>::children(&with).len(), 3);
125    }
126
127    #[test]
128    fn renders_the_combined_summary() {
129        let dt = DateTimePicker::new(
130            2026,
131            7,
132            Some(11),
133            9,
134            30,
135            Msg::Day,
136            Msg::Nav,
137            Msg::Hour,
138            Msg::Minute,
139        );
140        let ui = build_ui(
141            &dt,
142            Size::new(280.0, 520.0),
143            &Runtime::default(),
144            &Theme::default(),
145        );
146        let has_summary =
147            ui.scene().primitives().iter().any(
148                |p| matches!(p, Primitive::Text { text, .. } if text == "July 11, 2026  09:30"),
149            );
150        assert!(has_summary, "the date-and-time summary is shown");
151    }
152}