1use gpui::prelude::*;
8use gpui::{
9 deferred, div, px, App, Context, Entity, EventEmitter, FocusHandle, IntoElement, SharedString,
10 Window,
11};
12
13use super::calendar::Calendar;
14use super::control_metrics;
15use super::date::{Date, Weekday};
16use crate::devtools::Probed;
17use crate::icon::{Icon, IconName};
18use crate::reactive::Signal;
19use crate::theme::{theme, Size};
20
21#[derive(Debug, Clone)]
23pub enum DatePickerEvent {
24 Selected(Date),
26 Range(Date, Date),
28}
29
30pub struct DatePicker {
32 open: bool,
33 focus: FocusHandle,
34 view_year: i32,
35 view_month: u32,
36 selected: Option<Date>,
37 range_mode: bool,
38 range_start: Option<Date>,
39 range_end: Option<Date>,
40 min: Option<Date>,
41 max: Option<Date>,
42 week_start: Weekday,
43 format: SharedString,
44 placeholder: SharedString,
45 label: Option<SharedString>,
46 size: Size,
47 disabled: bool,
48}
49
50impl EventEmitter<DatePickerEvent> for DatePicker {}
51
52impl DatePicker {
53 pub fn new(cx: &mut Context<Self>) -> Self {
54 let today = Date::today();
55 DatePicker {
56 open: false,
57 focus: cx.focus_handle(),
58 view_year: today.year(),
59 view_month: today.month(),
60 selected: None,
61 range_mode: false,
62 range_start: None,
63 range_end: None,
64 min: None,
65 max: None,
66 week_start: Weekday::Sunday,
67 format: SharedString::new_static("MMM D, YYYY"),
68 placeholder: SharedString::new_static("Pick a date"),
69 label: None,
70 size: Size::Sm,
71 disabled: false,
72 }
73 }
74
75 pub fn range_mode(mut self) -> Self {
77 self.range_mode = true;
78 self.placeholder = SharedString::new_static("Pick a range");
79 self
80 }
81
82 pub fn value(mut self, value: Date) -> Self {
83 self.selected = Some(value);
84 self.view_year = value.year();
85 self.view_month = value.month();
86 self
87 }
88
89 pub fn min(mut self, min: Date) -> Self {
90 self.min = Some(min);
91 self
92 }
93
94 pub fn max(mut self, max: Date) -> Self {
95 self.max = Some(max);
96 self
97 }
98
99 pub fn week_start(mut self, week_start: Weekday) -> Self {
100 self.week_start = week_start;
101 self
102 }
103
104 pub fn format(mut self, pattern: impl Into<SharedString>) -> Self {
106 self.format = pattern.into();
107 self
108 }
109
110 pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
111 self.placeholder = placeholder.into();
112 self
113 }
114
115 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
116 self.label = Some(label.into());
117 self
118 }
119
120 pub fn size(mut self, size: Size) -> Self {
121 self.size = size;
122 self
123 }
124
125 pub fn disabled(mut self, disabled: bool) -> Self {
126 self.disabled = disabled;
127 self
128 }
129
130 pub fn selected_date(&self) -> Option<Date> {
131 self.selected
132 }
133
134 pub fn selected_range(&self) -> Option<(Date, Date)> {
135 Some((self.range_start?, self.range_end?))
136 }
137
138 pub fn bind(entity: &Entity<DatePicker>, signal: &Signal<Option<Date>>, cx: &mut App) {
142 let initial = signal.get(cx);
143 entity.update(cx, |this, cx| this.sync_selected(initial, cx));
144 let sink = signal.clone();
145 cx.subscribe(entity, move |_picker, event: &DatePickerEvent, cx| {
146 if let DatePickerEvent::Selected(date) = event {
147 sink.set_if_changed(cx, Some(*date));
148 }
149 })
150 .detach();
151 let picker = entity.downgrade();
152 cx.observe(signal.entity(), move |observed, cx| {
153 let date = *observed.read(cx);
154 picker
155 .update(cx, |this, cx| this.sync_selected(date, cx))
156 .ok();
157 })
158 .detach();
159 }
160
161 fn sync_selected(&mut self, date: Option<Date>, cx: &mut Context<Self>) {
163 if self.selected != date {
164 self.selected = date;
165 if let Some(date) = date {
166 self.view_year = date.year();
167 self.view_month = date.month();
168 }
169 cx.notify();
170 }
171 }
172
173 fn pick(&mut self, date: Date, cx: &mut Context<Self>) {
174 if self.range_mode {
175 match (self.range_start, self.range_end) {
176 (Some(start), None) => {
177 let (lo, hi) = if date < start {
178 (date, start)
179 } else {
180 (start, date)
181 };
182 self.range_start = Some(lo);
183 self.range_end = Some(hi);
184 self.open = false;
185 cx.emit(DatePickerEvent::Range(lo, hi));
186 }
187 _ => {
188 self.range_start = Some(date);
189 self.range_end = None;
190 }
191 }
192 } else {
193 self.selected = Some(date);
194 self.open = false;
195 cx.emit(DatePickerEvent::Selected(date));
196 }
197 cx.notify();
198 }
199
200 fn show_month(&mut self, year: i32, month: u32, cx: &mut Context<Self>) {
201 self.view_year = year;
202 self.view_month = month;
203 cx.notify();
204 }
205
206 fn trigger_text(&self) -> Option<SharedString> {
207 if self.range_mode {
208 let start = self.range_start?;
209 let text = match self.range_end {
210 Some(end) => format!(
211 "{} – {}",
212 start.format(&self.format),
213 end.format(&self.format)
214 ),
215 None => format!("{} – …", start.format(&self.format)),
216 };
217 Some(text.into())
218 } else {
219 Some(self.selected?.format(&self.format).into())
220 }
221 }
222}
223
224impl Render for DatePicker {
225 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
226 let t = theme(cx);
227 let (height, pad_x, font) = control_metrics(self.size);
228 let radius = t.radius(t.default_radius);
229 let surface = t.surface().hsla();
230 let border = t.border().hsla();
231 let text_color = t.text().hsla();
232 let dimmed = t.dimmed().hsla();
233 let font_sm = t.font_size(Size::Sm);
234
235 let value_text = self.trigger_text();
236 let has_value = value_text.is_some();
237 let shown: SharedString = value_text.unwrap_or_else(|| self.placeholder.clone());
238
239 let trigger = div()
240 .id("guise-datepicker-trigger")
241 .track_focus(&self.focus)
242 .flex()
243 .items_center()
244 .justify_between()
245 .gap(px(8.0))
246 .h(px(height))
247 .px(px(pad_x))
248 .rounded(px(radius))
249 .border_1()
250 .border_color(border)
251 .bg(surface)
252 .text_size(px(font))
253 .text_color(if has_value { text_color } else { dimmed })
254 .child(shown)
255 .child(
256 div()
257 .text_color(dimmed)
258 .child(Icon::new(IconName::CalendarDays).size(Size::Sm)),
259 )
260 .on_click(cx.listener(|this, _ev, _window, cx| {
261 if !this.disabled {
262 this.open = !this.open;
263 cx.notify();
264 }
265 }));
266
267 let mut wrap = div().relative().child(trigger);
268
269 if self.open && !self.disabled {
270 let this = cx.entity().downgrade();
271 let month_target = this.clone();
272 let mut calendar = Calendar::new("guise-datepicker-calendar")
273 .month(self.view_year, self.view_month)
274 .week_start(self.week_start)
275 .size(self.size)
276 .on_select(move |date, _window, cx| {
277 this.update(cx, |picker, cx| picker.pick(date, cx)).ok();
278 })
279 .on_month_change(move |year, month, _window, cx| {
280 month_target
281 .update(cx, |picker, cx| picker.show_month(year, month, cx))
282 .ok();
283 });
284 if self.range_mode {
285 if let Some(start) = self.range_start {
286 calendar = calendar.range(start, self.range_end);
287 }
288 } else {
289 calendar = calendar.value(self.selected);
290 }
291 if let Some(min) = self.min {
292 calendar = calendar.min(min);
293 }
294 if let Some(max) = self.max {
295 calendar = calendar.max(max);
296 }
297
298 let panel = div()
299 .absolute()
300 .top(px(height + 6.0))
301 .left(px(0.0))
302 .p(px(10.0))
303 .rounded(px(radius))
304 .border_1()
305 .border_color(border)
306 .bg(surface)
307 .shadow_md()
308 .occlude()
309 .child(calendar);
310 wrap = wrap.child(deferred(panel));
311 }
312
313 let mut column = div().flex().flex_col().gap(px(4.0));
314 if let Some(label) = self.label.clone() {
315 column = column.child(
316 div()
317 .text_size(px(font_sm))
318 .text_color(text_color)
319 .child(label),
320 );
321 }
322 column = column.child(wrap);
323
324 let element = if self.disabled {
325 column.opacity(0.6)
326 } else {
327 column
328 };
329
330 element.probe("DatePicker")
331 }
332}