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