frus_widgets/
datetimepicker.rs1use 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
18pub struct DateTimePicker<Msg = crate::callback::Callback> {
20 children: Vec<Box<dyn Widget<Msg>>>,
21}
22
23impl<Msg: Clone + 'static> DateTimePicker<Msg> {
24 #[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 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}