gpui-component/crates/ui/src/time/date_picker.rs

228 lines
7.5 KiB
Rust

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<NaiveDate>),
}
pub struct DatePicker {
id: ElementId,
focus_handle: FocusHandle,
date: Option<NaiveDate>,
cleanable: bool,
placeholder: Option<SharedString>,
open: bool,
size: Size,
width: Length,
date_format: SharedString,
calendar: View<Calendar>,
}
impl DatePicker {
pub fn new(id: impl Into<ElementId>, cx: &mut ViewContext<Self>) -> 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<SharedString>) -> Self {
self.date_format = format.into();
self
}
/// Set the placeholder of the date picker, default: "".
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> 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<Length>) -> 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<NaiveDate> {
self.date
}
/// Set the date of the date picker.
pub fn set_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
self.update_date(date, cx);
}
fn update_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
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>) {
self.open = false;
cx.notify();
}
fn clean(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
self.update_date(None, cx);
}
fn toggle_calendar(&mut self, _: &gpui::ClickEvent, cx: &mut ViewContext<Self>) {
self.open = !self.open;
cx.notify();
}
}
impl EventEmitter<DatePickerEvent> for DatePicker {}
impl Sizable for DatePicker {
fn with_size(mut self, size: impl Into<Size>) -> 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<Self>) -> 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),
)
})
}
}