use super::*;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct DatePickerRenderState {
pub is_disabled: bool,
pub is_invalid: bool,
pub is_read_only: bool,
pub is_required: bool,
pub is_focus_within: bool,
pub is_focus_visible: bool,
pub is_open: bool,
}
#[derive(IntoElement)]
pub struct DatePicker {
locale: Option<SharedString>,
content: Option<std::sync::Arc<dyn Fn(DatePickerRenderState) -> gpui::AnyElement + 'static>>,
name: Option<SharedString>,
default_value: Option<Date>,
value: Option<Option<Date>>,
constraints: DateConstraints,
is_disabled: bool,
is_read_only: bool,
is_required: bool,
validation_behavior: crate::form::ValidationBehavior,
validate: Option<crate::validation::Validator<Option<Date>>>,
validation_errors: Vec<SharedString>,
is_invalid: bool,
auto_focus: bool,
on_open_change: Option<std::sync::Arc<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
state: Entity<CalendarState>,
is_open: Option<bool>,
default_open: bool,
should_close_on_select: bool,
placement: herogpui_core::Placement,
label: Option<SharedString>,
trigger_indicator: Option<gpui::AnyElement>,
on_change: Option<OnChange>,
form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
form_is_disabled: Rc<Cell<bool>>,
form_default: Rc<RefCell<Option<Date>>>,
form_on_change: Rc<RefCell<Option<OnChange>>>,
form_field_state: Rc<RefCell<Option<Entity<crate::input::InputState>>>>,
sx: Option<Box<gpui::StyleRefinement>>,
}
impl DatePicker {
pub fn locale(mut self, tag: impl Into<SharedString>) -> Self {
self.locale = Some(tag.into());
self
}
pub fn value(mut self, date: Option<Date>) -> Self {
self.value = Some(date);
self
}
pub fn min_value(mut self, date: Date) -> Self {
self.constraints.min_value = Some(date);
self
}
pub fn max_value(mut self, date: Date) -> Self {
self.constraints.max_value = Some(date);
self
}
pub fn is_date_unavailable(mut self, f: impl Fn(Date) -> bool + 'static) -> Self {
self.constraints.is_date_unavailable = Some(std::sync::Arc::new(f));
self
}
pub fn first_day_of_week(mut self, day: Weekday) -> Self {
self.constraints.first_day_of_week = day;
self
}
pub fn constraints(mut self, constraints: DateConstraints) -> Self {
self.constraints = constraints;
self
}
pub fn is_disabled(mut self, v: bool) -> Self {
self.is_disabled = v;
self
}
pub fn is_read_only(mut self, v: bool) -> Self {
self.is_read_only = v;
self
}
pub fn is_required(mut self, v: bool) -> Self {
self.is_required = v;
self
}
pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
self.validation_behavior = behavior;
self
}
pub fn validate(mut self, f: impl Fn(&Option<Date>) -> Option<SharedString> + 'static) -> Self {
self.validate = Some(std::sync::Arc::new(f));
self
}
pub fn validation_errors(
mut self,
errors: impl IntoIterator<Item = impl Into<SharedString>>,
) -> Self {
self.validation_errors = errors.into_iter().map(Into::into).collect();
self
}
pub fn is_invalid(mut self, v: bool) -> Self {
self.is_invalid = v;
self
}
pub fn auto_focus(mut self, v: bool) -> Self {
self.auto_focus = v;
self
}
pub fn on_open_change(mut self, f: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
self.on_open_change = Some(std::sync::Arc::new(f));
self
}
pub fn new(state: Entity<CalendarState>) -> Self {
let form_state = date_picker_form_state(state.entity_id().as_u64());
let form_is_disabled = Rc::new(Cell::new(false));
let form_default = Rc::new(RefCell::new(None));
let form_on_change: Rc<RefCell<Option<OnChange>>> = Rc::new(RefCell::new(None));
let form_field_state = Rc::new(RefCell::new(None::<Entity<crate::input::InputState>>));
let restore_form_state = Rc::downgrade(&form_state);
let restore_is_disabled = form_is_disabled.clone();
let restore_default = form_default.clone();
let restore_callback = form_on_change.clone();
let restore_state = state.clone();
let restore_field_state = form_field_state.clone();
#[allow(clippy::arc_with_non_send_sync)]
let restore: std::sync::Arc<dyn Fn(&mut Window, &mut App)> =
std::sync::Arc::new(move |window, cx| {
let date = *restore_default.borrow();
restore_state.update(&mut *cx, |state, cx| {
state.selected = date;
state.selected_dates = date.into_iter().collect();
if let Some(date) = date {
state.view_year = date.year;
state.view_month = date.month;
state.view_day = date.day;
}
cx.notify();
});
if let Some(field_state) = restore_field_state.borrow().clone() {
let text = date.map(|date| date.format_iso()).unwrap_or_default();
field_state.update(cx, |state, cx| {
state.set_value(text);
cx.notify();
});
}
if let Some(state) = restore_form_state.upgrade() {
let mut state = state.borrow_mut();
state.value = crate::form::FormValue::Text(
date.map(|date| date.format_iso())
.unwrap_or_default()
.into(),
);
state.is_invalid = false;
state.is_successful = !restore_is_disabled.get();
}
if let Some(callback) = restore_callback.borrow().as_ref() {
callback(&date, window, cx);
}
});
form_state.borrow_mut().restore = Some(restore);
Self {
locale: None,
content: None,
name: None,
default_value: None,
value: None,
constraints: DateConstraints::new(),
is_disabled: false,
is_read_only: false,
is_required: false,
validation_behavior: crate::form::ValidationBehavior::Native,
validate: None,
validation_errors: Vec::new(),
is_invalid: false,
auto_focus: false,
on_open_change: None,
state,
is_open: None,
default_open: false,
should_close_on_select: true,
placement: herogpui_core::Placement::Bottom,
label: None,
trigger_indicator: None,
on_change: None,
form_state,
form_is_disabled,
form_default,
form_on_change,
form_field_state,
sx: None,
}
}
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
self.name = Some(name.into());
self
}
pub fn form_field(&self, cx: &App) -> Option<crate::form::FormField> {
let name = self.name.clone()?;
self.form_is_disabled.set(self.is_disabled);
let selected = self.state.read(cx).selected();
let text = selected.map(|d| d.format_iso()).unwrap_or_default();
if self.form_default.borrow().is_none() {
*self.form_default.borrow_mut() = self.state.read(cx).selected;
}
let mut form_state = self.form_state.borrow_mut();
form_state.value = crate::form::FormValue::Text(text.into());
form_state.is_successful = !self.is_disabled;
let constraint_error = selected
.is_some_and(|date| !self.constraints.allows(date))
.then(|| SharedString::from("That date is unavailable."));
form_state.is_invalid = crate::validation::resolve(
self.is_invalid,
&self.validation_errors,
self.validate
.as_ref()
.and_then(|validate| validate(&selected)),
constraint_error,
)
.is_invalid;
Some(
crate::form::FormField::live(name, self.form_state.clone())
.is_required(self.is_required)
.validation_behavior(self.validation_behavior),
)
}
pub fn default_value(mut self, value: Date) -> Self {
self.default_value = Some(value);
*self.form_default.borrow_mut() = Some(value);
self
}
pub fn is_open(mut self, v: bool) -> Self {
self.is_open = Some(v);
self
}
pub fn default_open(mut self, v: bool) -> Self {
self.default_open = v;
self
}
pub fn should_close_on_select(mut self, v: bool) -> Self {
self.should_close_on_select = v;
self
}
pub fn placement(mut self, placement: herogpui_core::Placement) -> Self {
self.placement = placement;
self
}
pub fn label(mut self, l: impl Into<SharedString>) -> Self {
self.label = Some(l.into());
self
}
pub fn trigger_indicator(mut self, indicator: impl IntoElement) -> Self {
self.trigger_indicator = Some(indicator.into_any_element());
self
}
pub fn content(
mut self,
render: impl Fn(DatePickerRenderState) -> gpui::AnyElement + 'static,
) -> Self {
self.content = Some(std::sync::Arc::new(render));
self
}
pub fn on_change(mut self, f: impl Fn(&Option<Date>, &mut Window, &mut App) + 'static) -> Self {
let callback = std::sync::Arc::new(f);
*self.form_on_change.borrow_mut() = Some(callback.clone());
self.on_change = Some(callback);
self
}
pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
self.sx = Some(crate::util::capture_sx(style));
self
}
}
impl RenderOnce for DatePicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
self.form_is_disabled.set(self.is_disabled);
let base_id = gpui::ElementId::named_usize("dp", self.state.entity_id().as_u64() as usize);
if let Some(date) = self.value {
let state = self.state.clone();
crate::util::seed_once(
window,
cx,
gpui::ElementId::named_usize(
"datepicker-default",
self.state.entity_id().as_u64() as usize,
),
move |cx| {
state.update(cx, |s, cx| {
s.selected = date;
s.selected_dates = date.into_iter().collect();
if let Some(date) = date {
s.view_year = date.year;
s.view_month = date.month;
s.view_day = date.day;
}
cx.notify();
});
},
);
} else if let Some(value) = self.default_value {
let state = self.state.clone();
crate::util::seed_once(
window,
cx,
gpui::ElementId::named_usize(
"datepicker-default",
self.state.entity_id().as_u64() as usize,
),
move |cx| {
state.update(cx, |s, cx| {
s.selected = Some(value);
s.selected_dates = vec![value];
s.view_year = value.year;
s.view_month = value.month;
s.view_day = value.day;
cx.notify();
});
},
);
}
let (is_open, open_own) = crate::util::controlled(
window,
cx,
element_id::scoped(&base_id, "open"),
self.is_open,
self.default_open,
);
let open = is_open && !self.is_disabled;
let selected = self.state.read(cx).selected;
let selected_text = selected.map(|date| date.format_iso()).unwrap_or_default();
self.form_state.borrow_mut().value =
crate::form::FormValue::Text(selected_text.clone().into());
let initial_text = selected_text.clone();
let field_state = window
.use_keyed_state(
element_id::scoped(&base_id, "field-state"),
cx,
move |_, cx| cx.new(|cx| crate::input::InputState::with_value(cx, initial_text)),
)
.read(cx)
.clone();
*self.form_field_state.borrow_mut() = Some(field_state.clone());
let field_sync = window.use_keyed_state(element_id::scoped(&base_id, "field-sync"), cx, {
let selected_text = selected_text.clone();
move |_, _| selected_text
});
let field_value = field_state.read(cx).value().to_owned();
let last_field_value = field_sync.read(cx).clone();
let field_follows_selection =
field_value == last_field_value && field_value != selected_text;
if field_follows_selection {
field_state.update(cx, |state, _| state.set_value(selected_text.clone()));
}
let live_field_value = if field_follows_selection {
selected_text.clone()
} else {
field_value
};
if live_field_value == selected_text {
field_sync.update(cx, |value, _| *value = selected_text.clone());
}
let (parsed, constraint_error) = if live_field_value.trim().is_empty() {
(None, None)
} else {
let (parsed, _) = parse_value(&live_field_value);
let error = match parsed {
None => Some(SharedString::from("Enter a valid date.")),
Some(date) if !self.constraints.allows(date) => {
Some(SharedString::from("That date is unavailable."))
}
Some(_) => None,
};
(parsed, error)
};
let field_invalid = crate::validation::resolve(
self.is_invalid,
&self.validation_errors,
self.validate
.as_ref()
.and_then(|validate| validate(&parsed)),
constraint_error,
)
.is_invalid;
{
let mut state = self.form_state.borrow_mut();
state.value = crate::form::FormValue::Text(live_field_value.into());
state.is_successful = !self.is_disabled;
state.is_invalid = field_invalid;
}
let field_focus = field_state.read(cx).focus_handle(cx);
self.form_state.borrow_mut().focus = Some(field_focus.clone());
if let Some(render) = self.content.clone() {
let content_scope = window
.use_keyed_state(
element_id::scoped(&base_id, "content-focus"),
cx,
|_, cx| cx.focus_handle().tab_stop(false),
)
.read(cx)
.clone();
let is_focus_within = content_scope.contains_focused(window, cx);
let content_root = gpui::div()
.relative()
.max_w(px(320.))
.track_focus(&content_scope)
.child(render(DatePickerRenderState {
is_disabled: self.is_disabled,
is_invalid: field_invalid,
is_read_only: self.is_read_only,
is_required: self.is_required,
is_focus_within,
is_focus_visible: is_focus_within && crate::util::focus_visible(cx),
is_open: open,
}));
return crate::util::apply_sx(content_root, &self.sx).into_any_element();
}
let (overlay_phase, dismissal_token) = crate::util::overlay_scope(
window,
cx,
element_id::scoped(&base_id, "overlay"),
open,
true,
);
let panel_visible = overlay_phase != crate::util::OverlayPhase::Closed;
let panel_open = overlay_phase == crate::util::OverlayPhase::Open;
let requested_placement = window.use_keyed_state(
element_id::scoped(&base_id, "requested-placement"),
cx,
|_, _| self.placement,
);
let resolved_placement = window.use_keyed_state(
element_id::scoped(&base_id, "resolved-placement"),
cx,
|_, _| Rc::new(Cell::new(None::<crate::popover::PopoverPlacement>)),
);
if *requested_placement.read(cx) != self.placement {
requested_placement.update(cx, |placement, _| *placement = self.placement);
resolved_placement.read(cx).set(None);
}
let resolved_placement = resolved_placement.read(cx).clone();
let entry_placement = resolved_placement.get().unwrap_or(self.placement);
let blur_open_own = open_own.clone();
let blur_open_change = self.on_open_change.clone();
let blur_scope =
crate::util::close_on_blur(window, cx, &base_id, open, move |window, cx| {
if let Some(held) = &blur_open_own {
held.update(cx, |value, cx| {
*value = false;
cx.notify();
});
}
if let Some(callback) = &blur_open_change {
callback(&false, window, cx);
}
});
let trigger_focus =
crate::util::tab_stop_handle(element_id::scoped(&base_id, "trigger-focus"), window, cx);
let initiator =
window.use_keyed_state(element_id::scoped(&base_id, "initiator"), cx, |_, _| 0usize);
let trigger_pressed = Rc::new(Cell::new(false));
let open_own_keys = open_own.clone();
let open_cb_keys = self.on_open_change.clone();
let open_picker = crate::util::shared(move |window: &mut Window, cx: &mut App| {
if is_open {
return;
}
if let Some(held) = &open_own_keys {
held.update(cx, |open, cx| {
*open = true;
cx.notify();
});
}
if let Some(cb) = &open_cb_keys {
cb(&true, window, cx);
}
});
let selected_state = self.state.clone();
let user_change = self.on_change.clone();
let form_state = self.form_state.clone();
let field_forced_invalid = self.is_invalid;
let was_open = is_open;
let trigger_pressed_for_capture = trigger_pressed.clone();
let trigger_open_own = open_own.clone();
let trigger_open_cb = self.on_open_change.clone();
let trigger_indicator = self.trigger_indicator.unwrap_or_else(|| {
gpui::svg()
.size(px(16.))
.path(icons::CALENDAR)
.text_color(cx.colors().muted)
.into_any_element()
});
let mut trigger = gpui::div()
.id(element_id::scoped(&base_id, "trigger"))
.flex()
.items_center()
.justify_center()
.size(px(24.))
.child(
gpui::div()
.flex()
.items_center()
.justify_center()
.size(px(16.))
.child(trigger_indicator),
);
trigger = crate::util::ring_overlay_if_focused(
trigger,
&trigger_focus,
true,
px(0.),
Vec::new(),
window,
cx,
);
trigger = trigger
.a11y_named(a11y::Role::Button, &a11y::Name::labelled("Calendar"))
.a11y_expanded(is_open);
if !self.is_disabled && !self.is_read_only {
let focus_on_press = trigger_focus.clone();
let trigger_initiator = initiator.clone();
trigger = trigger
.capture_any_mouse_down(move |_, _, cx| {
trigger_pressed_for_capture.set(true);
let pressed = trigger_pressed_for_capture.clone();
cx.defer(move |_| pressed.set(false));
})
.track_focus(&trigger_focus)
.cursor(crate::util::interactive_cursor(cx))
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
window.focus(&focus_on_press, cx);
cx.stop_propagation();
})
.on_click(move |_, window, cx| {
trigger_initiator.update(cx, |part, _| *part = 1);
if let Some(held) = &trigger_open_own {
held.update(cx, |open, cx| {
*open = !was_open;
cx.notify();
});
}
if let Some(cb) = &trigger_open_cb {
cb(&!was_open, window, cx);
}
});
}
let mut date_field = DateField::new(field_state)
.embedded(false)
.full_width(true)
.is_disabled(self.is_disabled)
.is_read_only(self.is_read_only)
.is_required(self.is_required)
.validation_behavior(self.validation_behavior)
.is_invalid(field_invalid)
.auto_focus(self.auto_focus)
.constraints(self.constraints.clone())
.report_invalid_changes()
.when_some(self.locale.clone(), |field, tag| field.locale(tag))
.suffix(trigger);
let open_from_field = open_picker.clone();
let field_initiator = initiator.clone();
let field_constraints = self.constraints.clone();
let field_validate = self.validate.clone();
let field_validation_errors = self.validation_errors.clone();
date_field = date_field
.on_picker_open(move |window, cx| {
field_initiator.update(cx, |part, _| *part = 0);
open_from_field(window, cx);
})
.on_change(move |date, window, cx| {
let valid_date = date.filter(|date| field_constraints.allows(*date));
let constraint_error = (date.is_some() && valid_date.is_none())
.then(|| SharedString::from("That date is unavailable."));
form_state.borrow_mut().is_invalid = crate::validation::resolve(
field_forced_invalid,
&field_validation_errors,
field_validate
.as_ref()
.and_then(|validate| validate(&valid_date)),
constraint_error,
)
.is_invalid;
selected_state.update(cx, |state, cx| {
if date.is_none() || valid_date.is_some() {
state.selected = valid_date;
state.selected_dates = valid_date.into_iter().collect();
}
if let Some(date) = valid_date {
state.view_year = date.year;
state.view_month = date.month;
state.view_day = date.day;
}
cx.notify();
});
if date.is_none() || valid_date.is_some() {
form_state.borrow_mut().value = crate::form::FormValue::Text(
valid_date
.map(|date| date.format_iso())
.unwrap_or_default()
.into(),
);
}
if date.is_none() || valid_date.is_some() {
if let Some(cb) = &user_change {
cb(&valid_date, window, cx);
}
}
});
let field = gpui::div()
.id(base_id.clone())
.a11y_named(a11y::Role::Group, &a11y::Name::maybe(self.label.clone()))
.w_full()
.child(date_field);
let anchor_bounds = window
.use_keyed_state(element_id::scoped(&base_id, "anchor-bounds"), cx, |_, _| {
Rc::new(Cell::new(None::<gpui::Bounds<Pixels>>))
})
.read(cx)
.clone();
let field = crate::popover::PopoverTriggerMeasure::new(field, anchor_bounds.clone());
let mut root = gpui::div().relative().max_w(px(320.));
let mut wrapper = gpui::div().flex().flex_col().gap(px(4.)).w_full();
if let Some(label) = &self.label {
wrapper = wrapper.child(
crate::field::Label::new(label.clone())
.is_required(self.is_required)
.is_disabled(self.is_disabled)
.is_invalid(self.is_invalid),
);
}
wrapper = wrapper.child(field);
root = root.child(wrapper);
if panel_visible {
let close_own = open_own;
let close_cb = self.on_open_change.clone();
let restore_field = field_focus;
let restore_trigger = trigger_focus;
let restore_part = initiator;
let close = crate::util::shared(move |window: &mut Window, cx: &mut App| {
if let Some(held) = &close_own {
held.update(cx, |v, cx| {
*v = false;
cx.notify();
});
}
if let Some(cb) = &close_cb {
cb(&false, window, cx);
}
if *restore_part.read(cx) == 1 {
window.focus(&restore_trigger, cx);
} else {
window.focus(&restore_field, cx);
}
});
let mut cal = Calendar::new(self.state.clone())
.constraints(self.constraints.clone())
.when_some(self.locale.clone(), |cal, tag| cal.locale(tag))
.is_disabled(self.is_disabled)
.is_read_only(self.is_read_only)
.inert(!panel_open)
.autofocus_grid(panel_open)
.is_invalid(field_invalid);
let pick_close = close.clone();
let user_change = self.on_change.clone();
let form_state = self.form_state.clone();
let calendar_forced_invalid = self.is_invalid;
let calendar_validate = self.validate.clone();
let calendar_validation_errors = self.validation_errors.clone();
let should_close_on_select = self.should_close_on_select;
let panel_open_for_change = panel_open;
cal = cal.on_change(move |d, window, cx| {
if !panel_open_for_change {
return;
}
let mut form_state = form_state.borrow_mut();
form_state.value = crate::form::FormValue::Text(
d.map(|date| date.format_iso()).unwrap_or_default().into(),
);
form_state.is_invalid = crate::validation::resolve(
calendar_forced_invalid,
&calendar_validation_errors,
calendar_validate.as_ref().and_then(|validate| validate(d)),
None,
)
.is_invalid;
if let Some(cb) = &user_change {
cb(d, window, cx);
}
if should_close_on_select {
pick_close(window, cx);
}
});
let esc = close.clone();
root = crate::util::dismiss_on_escape_with_token(
root,
dismissal_token.clone(),
move |window, cx| {
esc(window, cx);
crate::util::DismissResult::Handled
},
);
let outside_close = close.clone();
let panel_open_for_dismissal = panel_open;
let panel = crate::util::dismiss_on_press_outside_with_token(
picker_panel(
cx,
"date-picker-popover",
element_id::scoped(&base_id, "popover"),
)
.child(cal),
dismissal_token,
move |window, cx| {
if !panel_open_for_dismissal {
return crate::util::DismissResult::Declined;
}
if trigger_pressed.get() {
return crate::util::DismissResult::Declined;
}
outside_close(window, cx);
crate::util::DismissResult::Handled
},
);
let (slide_x, slide_y) = placement_entry_offset(entry_placement);
let zoom = crate::anim::ZoomBox::panel(px(12.), px(20.)).padding_x(px(12.));
let zoom = crate::anim::ZoomBox {
slide_x: (slide_x != 0.0).then(|| px(slide_x)),
slide_y: (slide_y != 0.0).then(|| px(slide_y)),
..zoom
};
let panel = if overlay_phase == crate::util::OverlayPhase::Exiting {
crate::anim::exiting(
panel,
element_id::scoped(&base_id, "panel-out"),
zoom,
crate::anim::Motion::LIST_OUT,
cx,
)
} else {
crate::anim::entering_zoom(
panel,
element_id::scoped(&base_id, "panel"),
zoom,
crate::anim::Motion::LIST_IN,
cx,
)
};
root = root.child(crate::util::floating(
crate::popover::popover_with_resolved_placement(
anchor_bounds,
self.placement,
px(PICKER_POPOVER_OFFSET),
Some(resolved_placement),
panel,
),
));
}
crate::util::apply_sx(root.track_focus(&blur_scope), &self.sx).into_any_element()
}
}
pub(super) fn picker_panel(
cx: &App,
selector: &'static str,
id: gpui::ElementId,
) -> gpui::Stateful<gpui::Div> {
let colors = cx.colors();
let layout = cx.layout();
gpui::div()
.id(id)
.p(px(12.))
.rounded(px(20.))
.max_h_full()
.overflow_y_scroll()
.restrict_scroll_to_axis()
.debug_selector(move || selector.to_owned())
.bg(colors.overlay.background)
.text_color(colors.overlay.foreground)
.when(!layout.overlay_shadow.is_empty(), |e| {
e.shadow(layout.overlay_shadow.clone())
})
}