1use gpui::{
11 App, AppContext as _, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
12 InteractiveElement, IntoElement, KeyDownEvent, ParentElement, Render, SharedString, Styled,
13 Subscription, Window, div, px,
14};
15use gpui_kit_assets::Icon;
16use gpui_kit_semantics::{NodeSpec, Role, Semantic};
17use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TypeScale};
18
19use crate::controls::button::IconButton;
20use crate::controls::field::{FieldState, field_shell};
21use crate::controls::input::{TextInput, TextInputEvent};
22use crate::datetime::adapter::{Day, SharedDateAdapter};
23use crate::datetime::calendar::{Calendar, CalendarEvent};
24use crate::foundation::{Disableable, Ident, Sizable, StyledExt, text as foundation_text};
25use crate::overlay::popover;
26use crate::strings::{ActiveStrings, StringKey};
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum DateInputEvent {
31 Changed(Day),
33 Unparsable {
36 text: SharedString,
37 message: SharedString,
38 },
39 Opened,
40 Closed,
41 Submit,
42}
43
44impl EventEmitter<DateInputEvent> for DateInput {}
45
46pub struct DateInput {
48 ident: Ident,
49 focus_handle: FocusHandle,
50 adapter: SharedDateAdapter,
51 field: Entity<TextInput>,
52 calendar: Entity<Calendar>,
53 value: Option<Day>,
54 message: Option<SharedString>,
56 open: bool,
57 size: ControlSize,
58 disabled: bool,
59 required: bool,
60 seeded: bool,
63 _subscriptions: Vec<Subscription>,
65}
66
67impl std::fmt::Debug for DateInput {
68 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 formatter
70 .debug_struct("DateInput")
71 .field("ident", &self.ident)
72 .field("value", &self.value)
73 .field("open", &self.open)
74 .field("disabled", &self.disabled)
75 .finish()
76 }
77}
78
79impl DateInput {
80 pub fn new(
81 ident: impl Into<Ident>,
82 adapter: SharedDateAdapter,
83 window: &mut Window,
84 cx: &mut Context<Self>,
85 ) -> Self {
86 let ident = ident.into();
87 let field = cx.new(|cx| TextInput::new(ident.child("field"), window, cx).bare(true));
88 let calendar =
89 cx.new(|cx| Calendar::new(ident.child("calendar"), adapter.clone(), window, cx));
90 let subscriptions = vec![
91 cx.subscribe(&field, |input, _field, event, cx| match event {
92 TextInputEvent::Change(text) => input.read_typed(text.clone(), cx),
93 TextInputEvent::Submit => cx.emit(DateInputEvent::Submit),
94 _ => {}
95 }),
96 cx.subscribe(&calendar, |input, _calendar, event, cx| {
97 if let CalendarEvent::Picked(day) = event {
98 input.take(*day, cx);
99 }
100 }),
101 ];
102
103 Self {
104 ident,
105 focus_handle: cx.focus_handle(),
106 adapter,
107 field,
108 calendar,
109 value: None,
110 message: None,
111 open: false,
112 size: ControlSize::Md,
113 disabled: false,
114 required: false,
115 seeded: false,
116 _subscriptions: subscriptions,
117 }
118 }
119
120 pub fn value(mut self, day: Day) -> Self {
122 self.value = Some(day);
123 self
124 }
125
126 pub fn required(mut self, required: bool) -> Self {
127 self.required = required;
128 self
129 }
130
131 pub fn set_value(&mut self, day: Option<Day>, cx: &mut Context<Self>) {
135 self.value = day;
136 self.message = None;
137 self.seeded = true;
138 let text = day
139 .map(|day| self.adapter.format_day(day))
140 .unwrap_or_default();
141 self.field
142 .update(cx, |field, cx| field.set_text_quietly(text, cx));
143 if let Some(day) = day {
144 self.calendar
145 .update(cx, |calendar, cx| calendar.set_selection(vec![day], cx));
146 }
147 cx.notify();
148 }
149
150 pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) {
151 self.disabled = disabled;
152 self.field
153 .update(cx, |field, cx| field.set_disabled(disabled, cx));
154 if disabled {
155 self.open = false;
156 }
157 cx.notify();
158 }
159
160 pub fn field(&self) -> &Entity<TextInput> {
161 &self.field
162 }
163
164 pub fn calendar(&self) -> &Entity<Calendar> {
165 &self.calendar
166 }
167
168 pub fn current(&self) -> Option<Day> {
169 self.value
170 }
171
172 pub fn message(&self) -> Option<&SharedString> {
174 self.message.as_ref()
175 }
176
177 pub fn is_open(&self) -> bool {
178 self.open
179 }
180
181 pub fn shown_text(&self, cx: &App) -> SharedString {
184 self.field.read(cx).value().clone()
185 }
186
187 pub fn is_invalid(&self) -> bool {
188 self.message.is_some()
189 }
190
191 pub fn open(&mut self, cx: &mut Context<Self>) {
192 if self.disabled || self.open {
193 return;
194 }
195 self.open = true;
196 cx.emit(DateInputEvent::Opened);
197 cx.notify();
198 }
199
200 pub fn close(&mut self, cx: &mut Context<Self>) {
201 if !self.open {
202 return;
203 }
204 self.open = false;
205 cx.emit(DateInputEvent::Closed);
206 cx.notify();
207 }
208
209 pub fn toggle(&mut self, cx: &mut Context<Self>) {
210 if self.open {
211 self.close(cx);
212 } else {
213 self.open(cx);
214 }
215 }
216
217 fn read_typed(&mut self, text: SharedString, cx: &mut Context<Self>) {
219 if text.trim().is_empty() {
220 self.message = None;
221 cx.notify();
222 return;
223 }
224 match self.adapter.parse_day(text.as_ref()) {
225 Ok(day) => {
226 self.message = None;
227 self.calendar
228 .update(cx, |calendar, cx| calendar.set_selection(vec![day], cx));
229 cx.emit(DateInputEvent::Changed(day));
230 }
231 Err(message) => {
232 self.message = Some(message.clone());
233 cx.emit(DateInputEvent::Unparsable { text, message });
234 }
235 }
236 cx.notify();
237 }
238
239 fn take(&mut self, day: Day, cx: &mut Context<Self>) {
242 let text = self.adapter.format_day(day);
243 self.field
244 .update(cx, |field, cx| field.set_text_quietly(text, cx));
245 self.message = None;
246 self.calendar
247 .update(cx, |calendar, cx| calendar.set_selection(vec![day], cx));
248 cx.emit(DateInputEvent::Changed(day));
249 self.close(cx);
250 }
251
252 fn on_key_down(&mut self, event: &KeyDownEvent, _window: &mut Window, cx: &mut Context<Self>) {
253 if self.disabled {
254 return;
255 }
256 match event.keystroke.key.as_str() {
257 "escape" if self.open => {
258 self.close(cx);
259 cx.stop_propagation();
260 }
261 "down" if !self.open => {
262 self.open(cx);
263 cx.stop_propagation();
264 }
265 _ => {}
266 }
267 }
268}
269
270impl Disableable for DateInput {
271 fn disabled(mut self, disabled: bool) -> Self {
272 self.disabled = disabled;
273 self
274 }
275}
276
277impl Sizable for DateInput {
278 fn control_size(mut self, size: ControlSize) -> Self {
279 self.size = size;
280 self
281 }
282}
283
284impl Focusable for DateInput {
285 fn focus_handle(&self, _cx: &App) -> FocusHandle {
286 self.focus_handle.clone()
287 }
288}
289
290impl Render for DateInput {
291 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
292 let theme = cx.theme().clone();
293 if !self.seeded {
294 self.seeded = true;
295 if let Some(day) = self.value {
296 let text = self.adapter.format_day(day);
297 self.field
298 .update(cx, |field, cx| field.set_text_quietly(text, cx));
299 self.calendar
300 .update(cx, |calendar, cx| calendar.set_selection(vec![day], cx));
301 }
302 }
303 if self.disabled != self.field.read(cx).is_disabled() {
304 let disabled = self.disabled;
305 self.field
306 .update(cx, |field, cx| field.set_disabled(disabled, cx));
307 }
308
309 let focused = self.field.read(cx).focus_handle(cx).is_focused(window);
310 let invalid = self.is_invalid();
311 let input = cx.entity().downgrade();
312
313 let trigger = IconButton::new(
314 self.ident.child("open"),
315 Icon::Checklist,
316 cx.strings().text(StringKey::DateInputOpen),
317 )
318 .ghost()
319 .control_size(self.size)
320 .semantic_parent(self.ident.semantic_id())
321 .disabled(self.disabled)
322 .on_click(move |_window, cx| {
323 input.update(cx, |input, cx| input.toggle(cx)).ok();
324 });
325
326 let surface = self.open.then(|| {
327 let card = popover::card(&theme)
328 .child(self.calendar.clone())
329 .into_any_element();
330 popover::anchored_below(
331 ElementId::from(self.ident.child("surface").semantic_id()),
332 &theme,
333 card,
334 )
335 });
336
337 let message = self.message.clone().map(|message| {
338 foundation_text(&theme, TypeScale::Caption, message.clone())
339 .text_color(theme.colors.danger)
340 .semantic_in(
341 cx,
342 NodeSpec::new(self.ident.child("message").semantic_id(), Role::Status)
343 .parent(self.ident.semantic_id())
344 .text(message),
345 )
346 });
347
348 div()
349 .id(self.ident.element_id())
350 .column()
351 .w_full()
352 .gap(px(theme.space(Space::Xs)))
353 .track_focus(&self.focus_handle)
354 .on_key_down(cx.listener(Self::on_key_down))
355 .child(
356 field_shell(
357 &theme,
358 self.size,
359 FieldState::default()
360 .focused(focused)
361 .invalid(invalid)
362 .disabled(self.disabled),
363 )
364 .child(div().flex_1().child(self.field.clone()))
365 .child(trigger),
366 )
367 .child(div().relative().children(surface))
368 .children(message)
369 .semantic_in(
370 cx,
371 NodeSpec::new(self.ident.semantic_id(), Role::Input)
372 .focus(&self.field.read(cx).focus_handle(cx))
373 .disabled(self.disabled)
374 .required(self.required)
375 .invalid(invalid)
376 .expanded(self.open)
377 .value(self.field.read(cx).value().clone())
378 .placeholder(cx.strings().text(StringKey::DateInputPlaceholder)),
379 )
380 }
381}