1use jag_draw::{Brush, Color, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 ElementState, EventHandler, EventResult, KeyCode, KeyboardEvent, MouseButton, MouseClickEvent,
8 MouseMoveEvent, ScrollEvent,
9};
10use crate::focus::FocusId;
11
12use super::Element;
13
14pub struct DatePicker {
20 pub rect: Rect,
22 pub label_size: f32,
24 pub label_color: ColorLinPremul,
26 pub focused: bool,
28 pub selected_date: Option<(u32, u32, u32)>,
30 pub bg_color: ColorLinPremul,
32 pub border_color: ColorLinPremul,
34 pub border_width: f32,
36 pub radius: f32,
38 pub padding: [f32; 4],
40 pub validation_error: Option<String>,
42 pub focus_id: FocusId,
44}
45
46impl DatePicker {
47 pub fn new() -> Self {
49 Self {
50 rect: Rect {
51 x: 0.0,
52 y: 0.0,
53 w: 180.0,
54 h: 36.0,
55 },
56 label_size: 14.0,
57 label_color: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
58 focused: false,
59 selected_date: None,
60 bg_color: ColorLinPremul::from_srgba_u8([40, 40, 40, 255]),
61 border_color: ColorLinPremul::from_srgba_u8([80, 80, 80, 255]),
62 border_width: 1.0,
63 radius: 4.0,
64 padding: [8.0, 12.0, 8.0, 12.0],
65 validation_error: None,
66 focus_id: FocusId(0),
67 }
68 }
69
70 pub fn date_string(&self) -> String {
72 match self.selected_date {
73 Some((y, m, d)) => format!("{y:04}-{m:02}-{d:02}"),
74 None => String::new(),
75 }
76 }
77
78 pub fn set_date(&mut self, year: u32, month: u32, day: u32) {
80 self.selected_date = Some((year, month, day));
81 }
82
83 pub fn clear_date(&mut self) {
85 self.selected_date = None;
86 }
87
88 pub fn hit_test(&self, x: f32, y: f32) -> bool {
90 x >= self.rect.x
91 && x <= self.rect.x + self.rect.w
92 && y >= self.rect.y
93 && y <= self.rect.y + self.rect.h
94 }
95}
96
97impl Default for DatePicker {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Element for DatePicker {
108 fn rect(&self) -> Rect {
109 self.rect
110 }
111
112 fn set_rect(&mut self, rect: Rect) {
113 self.rect = rect;
114 }
115
116 fn render(&self, canvas: &mut Canvas, z: i32) {
117 let rrect = RoundedRect {
118 rect: self.rect,
119 radii: RoundedRadii {
120 tl: self.radius,
121 tr: self.radius,
122 br: self.radius,
123 bl: self.radius,
124 },
125 };
126
127 let has_error = self.validation_error.is_some();
128 let border_color = if has_error {
129 Color::rgba(220, 38, 38, 255)
130 } else if self.focused {
131 Color::rgba(63, 130, 246, 255)
132 } else {
133 self.border_color
134 };
135 let border_width = if has_error {
136 self.border_width.max(2.0)
137 } else if self.focused {
138 (self.border_width + 1.0).max(2.0)
139 } else {
140 self.border_width
141 };
142
143 jag_surface::shapes::draw_snapped_rounded_rectangle(
144 canvas,
145 rrect,
146 Some(Brush::Solid(self.bg_color)),
147 Some(border_width),
148 Some(Brush::Solid(border_color)),
149 z,
150 );
151
152 let pad_top = self.padding[0];
154 let pad_left = self.padding[3];
155 let pad_bottom = self.padding[2];
156 let content_h = (self.rect.h - pad_top - pad_bottom).max(0.0);
157 let baseline_y = self.rect.y + pad_top + content_h * 0.5 + self.label_size * 0.35;
158 let text_x = self.rect.x + pad_left;
159
160 let date_str = self.date_string();
161 if date_str.is_empty() {
162 let ph_color = ColorLinPremul::from_srgba_u8([160, 160, 160, 255]);
163 canvas.draw_text_run_weighted(
164 [text_x, baseline_y],
165 "YYYY-MM-DD".to_string(),
166 self.label_size,
167 400.0,
168 ph_color,
169 z + 1,
170 );
171 } else {
172 canvas.draw_text_run_weighted(
173 [text_x, baseline_y],
174 date_str,
175 self.label_size,
176 400.0,
177 self.label_color,
178 z + 1,
179 );
180 }
181
182 let pad_right = self.padding[1];
184 let icon_x = self.rect.x + self.rect.w - pad_right - 14.0;
185 canvas.draw_text_run_weighted(
186 [icon_x, baseline_y],
187 "\u{1F4C5}".to_string(),
188 self.label_size,
189 400.0,
190 self.label_color,
191 z + 2,
192 );
193
194 if let Some(ref error_msg) = self.validation_error {
196 let error_size = (self.label_size * 0.85).max(12.0);
197 let baseline_offset = error_size * 0.8;
198 let top_gap = 3.0;
199 let error_y = self.rect.y + self.rect.h + top_gap + baseline_offset;
200 let error_color = ColorLinPremul::from_srgba_u8([220, 38, 38, 255]);
201
202 canvas.draw_text_run_weighted(
203 [self.rect.x + pad_left, error_y],
204 error_msg.clone(),
205 error_size,
206 400.0,
207 error_color,
208 z + 3,
209 );
210 }
211 }
212
213 fn focus_id(&self) -> Option<FocusId> {
214 Some(self.focus_id)
215 }
216}
217
218impl EventHandler for DatePicker {
223 fn handle_mouse_click(&mut self, event: &MouseClickEvent) -> EventResult {
224 if event.button != MouseButton::Left || event.state != ElementState::Pressed {
225 return EventResult::Ignored;
226 }
227 if self.hit_test(event.x, event.y) {
228 EventResult::Handled
229 } else {
230 EventResult::Ignored
231 }
232 }
233
234 fn handle_keyboard(&mut self, event: &KeyboardEvent) -> EventResult {
235 if event.state != ElementState::Pressed || !self.focused {
236 return EventResult::Ignored;
237 }
238 match event.key {
240 KeyCode::Escape => EventResult::Handled,
241 _ => EventResult::Ignored,
242 }
243 }
244
245 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
246 EventResult::Ignored
247 }
248
249 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
250 EventResult::Ignored
251 }
252
253 fn is_focused(&self) -> bool {
254 self.focused
255 }
256
257 fn set_focused(&mut self, focused: bool) {
258 self.focused = focused;
259 }
260
261 fn contains_point(&self, x: f32, y: f32) -> bool {
262 self.hit_test(x, y)
263 }
264}
265
266#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn date_picker_defaults() {
276 let dp = DatePicker::new();
277 assert!(dp.selected_date.is_none());
278 assert!(!dp.focused);
279 assert_eq!(dp.date_string(), "");
280 }
281
282 #[test]
283 fn date_picker_set_and_clear() {
284 let mut dp = DatePicker::new();
285 dp.set_date(2026, 3, 7);
286 assert_eq!(dp.date_string(), "2026-03-07");
287 assert_eq!(dp.selected_date, Some((2026, 3, 7)));
288
289 dp.clear_date();
290 assert!(dp.selected_date.is_none());
291 assert_eq!(dp.date_string(), "");
292 }
293
294 #[test]
295 fn date_picker_hit_test() {
296 let mut dp = DatePicker::new();
297 dp.rect = Rect {
298 x: 10.0,
299 y: 10.0,
300 w: 180.0,
301 h: 36.0,
302 };
303 assert!(dp.hit_test(50.0, 25.0));
304 assert!(!dp.hit_test(0.0, 0.0));
305 }
306
307 #[test]
308 fn date_picker_focus() {
309 let mut dp = DatePicker::new();
310 assert!(!dp.is_focused());
311 dp.set_focused(true);
312 assert!(dp.is_focused());
313 }
314}