Skip to main content

jag_ui/elements/
date_picker.rs

1//! Simplified date picker element with text input.
2
3use 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
14/// A simplified date picker rendered as a text input with a date value.
15///
16/// For now this is a text-input style element that stores a date as
17/// an optional `(year, month, day)` tuple.  A full calendar popup can
18/// be layered on top in a future iteration.
19pub struct DatePicker {
20    /// Bounding rect.
21    pub rect: Rect,
22    /// Font size.
23    pub label_size: f32,
24    /// Text color.
25    pub label_color: ColorLinPremul,
26    /// Whether the picker is focused.
27    pub focused: bool,
28    /// Selected date as (year, month, day).
29    pub selected_date: Option<(u32, u32, u32)>,
30    /// Background color.
31    pub bg_color: ColorLinPremul,
32    /// Border color.
33    pub border_color: ColorLinPremul,
34    /// Border width.
35    pub border_width: f32,
36    /// Corner radius.
37    pub radius: f32,
38    /// Padding [top, right, bottom, left].
39    pub padding: [f32; 4],
40    /// Validation error message.
41    pub validation_error: Option<String>,
42    /// Focus identifier.
43    pub focus_id: FocusId,
44}
45
46impl DatePicker {
47    /// Create a date picker with default styling and no date selected.
48    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    /// Get the selected date as a formatted string.
71    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    /// Set the date.
79    pub fn set_date(&mut self, year: u32, month: u32, day: u32) {
80        self.selected_date = Some((year, month, day));
81    }
82
83    /// Clear the date.
84    pub fn clear_date(&mut self) {
85        self.selected_date = None;
86    }
87
88    /// Hit-test the field.
89    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
103// ---------------------------------------------------------------------------
104// Element trait
105// ---------------------------------------------------------------------------
106
107impl 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        // Date text or placeholder
153        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        // Calendar icon (simple text glyph)
183        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        // Validation error
195        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
218// ---------------------------------------------------------------------------
219// EventHandler trait
220// ---------------------------------------------------------------------------
221
222impl 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        // Date pickers are primarily mouse-driven; minimal keyboard support.
239        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// ---------------------------------------------------------------------------
267// Tests
268// ---------------------------------------------------------------------------
269
270#[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}