Skip to main content

ui/components/
date_picker.rs

1//! Date picker composed from a popover trigger and [`Calendar`].
2
3use std::cell::Cell;
4use std::rc::Rc;
5
6use chrono::{Datelike, Local, NaiveDate};
7use gpui::{Bounds, Context, Entity, Pixels, Render, anchored, canvas, deferred, point};
8
9use crate::components::calendar::Calendar;
10use crate::prelude::*;
11
12fn format_date(date: NaiveDate) -> SharedString {
13    format!("{}/{}/{}", date.month(), date.day(), date.year()).into()
14}
15
16/// Popover date picker: trigger button + floating [`Calendar`].
17///
18/// Create with `cx.new(|cx| DatePicker::new(cx))`.
19pub struct DatePicker {
20    selected: Option<NaiveDate>,
21    open: bool,
22    placeholder: SharedString,
23    calendar: Entity<Calendar>,
24    trigger_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
25}
26
27impl DatePicker {
28    pub fn new(cx: &mut Context<Self>) -> Self {
29        let calendar = cx.new(|_| Calendar::new());
30        cx.observe(&calendar, |this, cal, cx| {
31            if let Some(date) = cal.read(cx).selection() {
32                this.selected = Some(date);
33                this.open = false;
34                cx.notify();
35            }
36        })
37        .detach();
38
39        Self {
40            selected: None,
41            open: false,
42            placeholder: "Pick a date".into(),
43            calendar,
44            trigger_bounds: Rc::new(Cell::new(None)),
45        }
46    }
47
48    pub fn selected(mut self, date: NaiveDate, cx: &mut Context<Self>) -> Self {
49        self.selected = Some(date);
50        self.calendar.update(cx, |cal, _| {
51            *cal = Calendar::new().selected(date);
52        });
53        self
54    }
55
56    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
57        self.placeholder = placeholder.into();
58        self
59    }
60
61    pub fn value(&self) -> Option<NaiveDate> {
62        self.selected
63    }
64}
65
66impl Render for DatePicker {
67    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
68        let label = self
69            .selected
70            .map(format_date)
71            .unwrap_or_else(|| self.placeholder.clone());
72        let has_value = self.selected.is_some();
73        let open = self.open;
74
75        let trigger = h_flex()
76            .id("date-picker-trigger")
77            // Test-only, no-op in release builds (mirrors the
78            // `Select`/`Combobox`/`MultiSelect` trigger `debug_selector`
79            // precedent) — lets integration tests open the popover via a
80            // genuine `simulate_click` at real rendered bounds.
81            .debug_selector(|| "DATE-PICKER-TRIGGER".into())
82            .w(px(240.))
83            .items_center()
84            .justify_between()
85            .px_3()
86            .py_2()
87            .rounded_md()
88            .bg(semantic::surface(cx))
89            .border_1()
90            .border_color(if open {
91                palette::primary(500)
92            } else {
93                semantic::border(cx)
94            })
95            .cursor_pointer()
96            .on_click(cx.listener(|this, _, _, cx| {
97                this.open = !this.open;
98                cx.notify();
99            }))
100            .child(Label::new(label).color(if has_value {
101                Color::Default
102            } else {
103                Color::Placeholder
104            }))
105            .child(Icon::new(IconName::Clock).size(IconSize::Small))
106            .child({
107                let trigger_bounds = self.trigger_bounds.clone();
108                canvas(
109                    move |bounds, _, _| trigger_bounds.set(Some(bounds)),
110                    |_, _, _, _| {},
111                )
112                .absolute()
113                .top_0()
114                .left_0()
115                .size_full()
116            });
117
118        v_flex().gap_1().child(trigger).when(open, |this| {
119            let mut anchor = anchored().snap_to_window_with_margin(px(8.));
120            if let Some(bounds) = self.trigger_bounds.get() {
121                anchor = anchor.position(point(
122                    bounds.origin.x,
123                    bounds.origin.y + bounds.size.height + px(4.),
124                ));
125            }
126
127            let floating = deferred(
128                anchor.child(
129                    div()
130                        .occlude()
131                        .id("date-picker-popover")
132                        // Test-only, no-op in release builds — lets
133                        // integration tests assert the popover
134                        // opened/closed (its `debug_bounds` resolving or
135                        // not) without reaching into private state.
136                        .debug_selector(|| "DATE-PICKER-POPOVER".into())
137                        .child(self.calendar.clone())
138                        .on_mouse_down_out(cx.listener(|this, _, _, cx| {
139                            this.open = false;
140                            cx.notify();
141                        })),
142                ),
143            )
144            .with_priority(1);
145
146            this.child(floating)
147        })
148    }
149}
150
151/// Gallery catalog entry for [`DatePicker`].
152#[derive(IntoElement, RegisterComponent)]
153pub struct DatePickerPreview;
154
155impl RenderOnce for DatePickerPreview {
156    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
157        cx.new(|cx| {
158            let mut picker = DatePicker::new(cx);
159            picker = picker.selected(Local::now().date_naive(), cx);
160            picker
161        })
162    }
163}
164
165impl Component for DatePickerPreview {
166    fn scope() -> ComponentScope {
167        ComponentScope::Input
168    }
169
170    fn description() -> Option<&'static str> {
171        Some("Popover trigger + calendar for choosing a date.")
172    }
173
174    fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
175        DatePickerPreview
176            .render(window, cx)
177            .into_any_element()
178            .into()
179    }
180}