1use frus_core::{Rect, Scene};
11use frus_layout::{Dimension, FlexDirection, Style};
12
13use crate::datepicker::DatePicker;
14use crate::interaction::Status;
15use crate::text::Text;
16use crate::theme::Theme;
17use crate::timepicker::{Endpoint, TimeField, TimeRange};
18use crate::widget::Widget;
19
20pub struct DateTimeRange<Msg = crate::callback::Callback> {
22 children: Vec<Box<dyn Widget<Msg>>>,
23}
24
25impl<Msg: Clone + 'static> DateTimeRange<Msg> {
26 #[allow(clippy::too_many_arguments)]
32 pub fn new(
33 year: i32,
34 month: u32,
35 start_date: Option<(i32, u32, u32)>,
36 end_date: Option<(i32, u32, u32)>,
37 start_time: (u32, u32),
38 end_time: (u32, u32),
39 on_date: impl Fn((i32, u32, u32)) -> Msg + 'static,
40 on_nav: impl Fn(i32) -> Msg + 'static,
41 on_time: impl Fn(Endpoint, TimeField, u32) -> Msg + 'static,
42 ) -> Self {
43 let calendar = DatePicker::range_dual(year, month, start_date, end_date, on_date, on_nav);
44 let times = TimeRange::new(start_time, end_time, on_time);
45
46 let mut children: Vec<Box<dyn Widget<Msg>>> = Vec::new();
47 if let (Some(s), Some(e)) = (start_date, end_date) {
48 let l10n = crate::localizations::of();
49 let stamp = |d: (i32, u32, u32), t: (u32, u32)| {
50 format!(
51 "{} {}, {} {:02}:{:02}",
52 l10n.months()[(d.1 - 1) as usize],
53 d.2,
54 d.0,
55 t.0,
56 t.1
57 )
58 };
59 let summary = format!("{} → {}", stamp(s, start_time), stamp(e, end_time));
60 children.push(Box::new(Text::new(summary).size(16.0)));
61 }
62 children.push(Box::new(calendar));
63 children.push(Box::new(times));
64
65 Self { children }
66 }
67}
68
69impl<Msg: Clone> Widget<Msg> for DateTimeRange<Msg> {
70 fn style(&self) -> Style {
71 Style {
72 width: Dimension::Auto,
73 flex_direction: FlexDirection::Column,
74 gap: 16.0,
75 ..Default::default()
76 }
77 }
78
79 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
80 &self.children
81 }
82
83 fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
84
85 fn on_click(&self) -> Option<Msg> {
86 None
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::{build_ui, Runtime, Size};
94 use frus_core::Primitive;
95
96 #[derive(Clone, Debug, PartialEq)]
97 enum Msg {
98 Date(i32, u32, u32),
99 Nav(i32),
100 Time(Endpoint, TimeField, u32),
101 }
102
103 fn build(start: Option<(i32, u32, u32)>, end: Option<(i32, u32, u32)>) -> DateTimeRange<Msg> {
104 DateTimeRange::new(
105 2026,
106 7,
107 start,
108 end,
109 (9, 0),
110 (17, 30),
111 |(y, m, d)| Msg::Date(y, m, d),
112 Msg::Nav,
113 Msg::Time,
114 )
115 }
116
117 #[test]
118 fn summary_appears_only_with_both_dates() {
119 assert_eq!(Widget::<Msg>::children(&build(None, None)).len(), 2);
121 assert_eq!(
122 Widget::<Msg>::children(&build(Some((2026, 7, 28)), None)).len(),
123 2
124 );
125 assert_eq!(
126 Widget::<Msg>::children(&build(Some((2026, 7, 28)), Some((2026, 8, 3)))).len(),
127 3,
128 );
129 }
130
131 #[test]
132 fn renders_the_combined_summary() {
133 let dtr = build(Some((2026, 7, 28)), Some((2026, 8, 3)));
134 let ui = build_ui(
135 &dtr,
136 Size::new(560.0, 700.0),
137 &Runtime::default(),
138 &Theme::default(),
139 );
140 let has = ui.scene().primitives().iter().any(|p| {
141 matches!(p, Primitive::Text { text, .. }
142 if text == "July 28, 2026 09:00 → August 3, 2026 17:30")
143 });
144 assert!(has, "the start → end summary is shown");
145 }
146}