use chrono::{Local, NaiveDate}; use gpui::{ deferred, div, prelude::FluentBuilder as _, px, AppContext, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement as _, KeyBinding, Length, MouseButton, ParentElement as _, Render, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext as _, }; use rust_i18n::t; use crate::{ dropdown::Escape, h_flex, input::ClearButton, styled_ext::StyleSized as _, theme::ActiveTheme as _, Clickable, Icon, IconName, Sizable, Size, StyledExt as _, }; use super::calendar::{Calendar, CalendarEvent}; pub fn init(cx: &mut AppContext) { let context = Some("DatePicker"); cx.bind_keys([KeyBinding::new("escape", Escape, context)]) } #[derive(Clone)] pub enum DatePickerEvent { Change(Option), } pub struct DatePicker { id: ElementId, focus_handle: FocusHandle, date: Option, cleanable: bool, placeholder: Option, open: bool, size: Size, width: Length, date_format: SharedString, calendar: View, } impl DatePicker { pub fn new(id: impl Into, cx: &mut ViewContext) -> Self { let calendar = cx.new_view(Calendar::new); cx.subscribe(&calendar, |this, _, ev: &CalendarEvent, cx| match ev { CalendarEvent::Selected(date) => { this.update_date(Some(*date), cx); } }) .detach(); Self { id: id.into(), focus_handle: cx.focus_handle(), date: None, calendar, open: false, size: Size::default(), width: Length::Auto, date_format: "%Y/%m/%d".into(), cleanable: false, placeholder: None, } } /// Set the date format of the date picker to display in Input, default: "%Y/%m/%d". pub fn date_format(mut self, format: impl Into) -> Self { self.date_format = format.into(); self } /// Set the placeholder of the date picker, default: "". pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = Some(placeholder.into()); self } /// Set true to show the clear button when the input field is not empty. pub fn cleanable(mut self, cleanable: bool) -> Self { self.cleanable = cleanable; self } /// Set width of the date picker input field, default is `Length::Auto`. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); self } /// Set the default date of the date picker. pub fn default_now(mut self) -> Self { self.date = Some(Local::now().naive_local().date()); self } /// Get the date of the date picker. pub fn date(&self) -> Option { self.date } /// Set the date of the date picker. pub fn set_date(&mut self, date: Option, cx: &mut ViewContext) { self.update_date(date, cx); } fn update_date(&mut self, date: Option, cx: &mut ViewContext) { self.date = date; self.calendar.update(cx, |view, cx| { view.set_date(date, cx); }); self.open = false; cx.emit(DatePickerEvent::Change(date)); cx.notify(); } fn escape(&mut self, _: &Escape, cx: &mut ViewContext) { self.open = false; cx.notify(); } fn clean(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext) { self.update_date(None, cx); } fn toggle_calendar(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext) { self.open = !self.open; cx.notify(); } } impl EventEmitter for DatePicker {} impl Sizable for DatePicker { fn with_size(mut self, size: impl Into) -> Self { self.size = size.into(); self } } impl FocusableView for DatePicker { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() } } impl Render for DatePicker { fn render(&mut self, cx: &mut ViewContext) -> impl gpui::IntoElement { let is_focused = self.focus_handle.is_focused(cx); let show_clean = self.cleanable && self.date.is_some(); let placeholder = self .placeholder .clone() .unwrap_or_else(|| t!("DatePicker.placeholder").into()); let display_title = self .date .map(|date| date.format(&self.date_format).to_string()) .unwrap_or(placeholder.to_string()); div() .id(self.id.clone()) .key_context("DatePicker") .track_focus(&self.focus_handle) .on_action(cx.listener(Self::escape)) .w_full() .relative() .map(|this| match self.width { Length::Definite(l) => this.flex_none().w(l), Length::Auto => this.w_full(), }) .input_text_size(self.size) .child( div() .id("date-picker-input") .relative() .flex() .items_center() .justify_between() .bg(cx.theme().background) .border_1() .border_color(cx.theme().input) .rounded(px(cx.theme().radius)) .shadow_sm() .cursor_pointer() .overflow_hidden() .input_text_size(self.size) .when(is_focused, |this| this.outline(cx)) .input_size(self.size) .when(!self.open, |this| { this.on_click(cx.listener(Self::toggle_calendar)) }) .child( h_flex() .w_full() .items_center() .justify_between() .gap_1() .child(div().w_full().overflow_hidden().child(display_title)) .when(show_clean, |this| { this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean))) }) .when(!show_clean, |this| { this.child( Icon::new(IconName::Calendar) .text_color(cx.theme().muted_foreground), ) }), ), ) .when(self.open, |this| { this.child( deferred( div() .track_focus(&self.focus_handle) .absolute() .mt_1() .overflow_hidden() .rounded_lg() .p_3() .w(px(300.)) .elevation_2(cx) .on_mouse_up_out( MouseButton::Left, cx.listener(|view, _, cx| view.escape(&Escape, cx)), ) .child(self.calendar.clone()), ) .with_priority(1), ) }) } }