calendar: Add Matcher for disabled dates (#623)
| Day Of Week | Interval | Range | |--------------|---------|-------| | <img width="420" alt="image" src="https://github.com/user-attachments/assets/e23f69b8-3191-4370-8adf-266362680e5c" /> | <img width="264" alt="image" src="https://github.com/user-attachments/assets/c211d7c3-9878-402e-a6cc-e3ac1b50e588" /> | <img width="337" alt="image" src="https://github.com/user-attachments/assets/2b894ef1-14d0-4fc3-89e5-4e38b5e8572c" /> |
This commit is contained in:
parent
83375147fe
commit
c7257d36cf
3 changed files with 198 additions and 39 deletions
|
|
@ -5,6 +5,7 @@ use gpui::{
|
|||
};
|
||||
use gpui_component::{
|
||||
button::Button,
|
||||
calendar,
|
||||
date_picker::{DatePicker, DatePickerEvent, DateRangePreset},
|
||||
v_flex, Sizable as _, Size,
|
||||
};
|
||||
|
|
@ -82,18 +83,31 @@ impl CalendarStory {
|
|||
.width(px(220.))
|
||||
.presets(presets);
|
||||
picker.set_date(now, window, cx);
|
||||
picker.set_disabled(vec![0, 6], window, cx);
|
||||
picker
|
||||
});
|
||||
let date_picker_large = cx.new(|cx| {
|
||||
DatePicker::new("date_picker_large", window, cx)
|
||||
let mut picker = DatePicker::new("date_picker_large", window, cx)
|
||||
.large()
|
||||
.date_format("%Y-%m-%d")
|
||||
.width(px(300.))
|
||||
.width(px(300.));
|
||||
picker.set_disabled(
|
||||
calendar::Matcher::range(Some(now), now.checked_add_days(Days::new(7))),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
picker.set_date(now, window, cx);
|
||||
picker
|
||||
});
|
||||
let date_picker_small = cx.new(|cx| {
|
||||
let mut picker = DatePicker::new("date_picker_small", window, cx)
|
||||
.small()
|
||||
.width(px(180.));
|
||||
picker.set_disabled(
|
||||
calendar::Matcher::interval(Some(now), now.checked_add_days(Days::new(5))),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
picker.set_date(now, window, cx);
|
||||
picker
|
||||
});
|
||||
|
|
|
|||
|
|
@ -147,6 +147,80 @@ impl ViewMode {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct IntervalMatcher {
|
||||
before: Option<NaiveDate>,
|
||||
after: Option<NaiveDate>,
|
||||
}
|
||||
|
||||
pub struct RangeMatcher {
|
||||
from: Option<NaiveDate>,
|
||||
to: Option<NaiveDate>,
|
||||
}
|
||||
|
||||
pub enum Matcher {
|
||||
/// Match declare days of the week.
|
||||
///
|
||||
/// Matcher::DayOfWeek(vec![0, 6])
|
||||
/// @ill match the days of the week that are Sunday and Saturday.
|
||||
DayOfWeek(Vec<u32>),
|
||||
/// Match the included days, except for those before and after the interval.
|
||||
///
|
||||
/// Matcher::Interval(IntervalMatcher {
|
||||
/// before: Some(NaiveDate::from_ymd(2020, 1, 2)),
|
||||
/// after: Some(NaiveDate::from_ymd(2020, 1, 3)),
|
||||
/// })
|
||||
/// Will match the days that are not between 2020-01-02 and 2020-01-03.
|
||||
Interval(IntervalMatcher),
|
||||
/// Match the days within the range.
|
||||
///
|
||||
/// Matcher::Range(RangeMatcher {
|
||||
/// from: Some(NaiveDate::from_ymd(2020, 1, 1)),
|
||||
/// to: Some(NaiveDate::from_ymd(2020, 1, 3)),
|
||||
/// })
|
||||
/// Will match the days that are between 2020-01-01 and 2020-01-03.
|
||||
Range(RangeMatcher),
|
||||
}
|
||||
|
||||
impl From<Vec<u32>> for Matcher {
|
||||
fn from(days: Vec<u32>) -> Self {
|
||||
Matcher::DayOfWeek(days)
|
||||
}
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub fn interval(before: Option<NaiveDate>, after: Option<NaiveDate>) -> Self {
|
||||
Matcher::Interval(IntervalMatcher { before, after })
|
||||
}
|
||||
|
||||
pub fn range(from: Option<NaiveDate>, to: Option<NaiveDate>) -> Self {
|
||||
Matcher::Range(RangeMatcher { from, to })
|
||||
}
|
||||
|
||||
fn matched(&self, date: &NaiveDate) -> bool {
|
||||
match self {
|
||||
Matcher::DayOfWeek(days) => days.contains(&date.weekday().num_days_from_sunday()),
|
||||
Matcher::Interval(interval) => {
|
||||
let before_check = interval.before.map_or(false, |before| date < &before);
|
||||
let after_check = interval.after.map_or(false, |after| date > &after);
|
||||
before_check || after_check
|
||||
}
|
||||
Matcher::Range(range) => {
|
||||
let from_check = range.from.map_or(false, |from| date < &from);
|
||||
let to_check = range.to.map_or(false, |to| date > &to);
|
||||
!from_check && !to_check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn date_matched(&self, date: &Date) -> bool {
|
||||
match date {
|
||||
Date::Single(Some(date)) => self.matched(date),
|
||||
Date::Range(Some(start), Some(end)) => self.matched(start) || self.matched(end),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Calendar {
|
||||
focus_handle: FocusHandle,
|
||||
size: Size,
|
||||
|
|
@ -159,6 +233,7 @@ pub struct Calendar {
|
|||
/// Number of the months view to show.
|
||||
number_of_months: usize,
|
||||
today: NaiveDate,
|
||||
disabled: Option<Matcher>,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
|
|
@ -175,6 +250,7 @@ impl Calendar {
|
|||
year_page: 0,
|
||||
number_of_months: 1,
|
||||
today,
|
||||
disabled: None,
|
||||
}
|
||||
.year_range((today.year() - 50, today.year() + 50))
|
||||
}
|
||||
|
|
@ -183,8 +259,18 @@ impl Calendar {
|
|||
///
|
||||
/// When you set a range date, the mode will be automatically set to `Mode::Range`.
|
||||
pub fn set_date(&mut self, date: impl Into<Date>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.date = date.into();
|
||||
let date = date.into();
|
||||
|
||||
let invalid = self
|
||||
.disabled
|
||||
.as_ref()
|
||||
.map_or(false, |disabled| disabled.date_matched(&date));
|
||||
|
||||
if invalid {
|
||||
return;
|
||||
}
|
||||
|
||||
self.date = date;
|
||||
match self.date {
|
||||
Date::Single(Some(date)) => {
|
||||
self.current_month = date.month() as u8;
|
||||
|
|
@ -243,6 +329,13 @@ impl Calendar {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the disabled matcher of the calendar.
|
||||
///
|
||||
/// The disabled matcher will be used to disable the days that match the matcher.
|
||||
pub fn set_disabled(&mut self, disabled: Matcher, _: &mut Window, _: &mut Context<Self>) {
|
||||
self.disabled = Some(disabled);
|
||||
}
|
||||
|
||||
/// Get year and month by offset month.
|
||||
fn offset_year_month(&self, offset_month: usize) -> (i32, u32) {
|
||||
let mut month = self.current_month as i32 + offset_month as i32;
|
||||
|
|
@ -368,6 +461,7 @@ impl Calendar {
|
|||
active: bool,
|
||||
secondary_active: bool,
|
||||
muted: bool,
|
||||
disabled: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + Styled + StatefulInteractiveElement {
|
||||
|
|
@ -379,7 +473,13 @@ impl Calendar {
|
|||
_ => this.size_9().rounded(cx.theme().radius * 2.),
|
||||
})
|
||||
.justify_center()
|
||||
.cursor_pointer()
|
||||
.map(|this| {
|
||||
if disabled {
|
||||
this.cursor_not_allowed()
|
||||
} else {
|
||||
this.cursor_pointer()
|
||||
}
|
||||
})
|
||||
.when(muted, |this| {
|
||||
this.text_color(cx.theme().muted_foreground.opacity(0.3))
|
||||
})
|
||||
|
|
@ -391,7 +491,7 @@ impl Calendar {
|
|||
})
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
})
|
||||
.when(!active, |this| {
|
||||
.when(!active && !disabled, |this| {
|
||||
this.hover(|this| {
|
||||
this.bg(cx.theme().accent)
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
|
|
@ -420,44 +520,55 @@ impl Calendar {
|
|||
|
||||
let date = *d;
|
||||
let is_today = *d == self.today;
|
||||
let disabled = self
|
||||
.disabled
|
||||
.as_ref()
|
||||
.map_or(false, |disabled| disabled.matched(&date));
|
||||
|
||||
self.item_button(
|
||||
ix,
|
||||
day.to_string(),
|
||||
is_active,
|
||||
is_in_range,
|
||||
!is_current_month,
|
||||
!is_current_month || disabled,
|
||||
disabled,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.when(is_today && !is_active, |this| {
|
||||
this.border_1().border_color(cx.theme().border)
|
||||
}) // Add border for today
|
||||
.on_click(cx.listener(move |view, _: &ClickEvent, window, cx| {
|
||||
if view.date.is_single() {
|
||||
view.set_date(date, window, cx);
|
||||
cx.emit(CalendarEvent::Selected(view.date()));
|
||||
} else {
|
||||
let start = view.date.start();
|
||||
let end = view.date.end();
|
||||
|
||||
if start.is_none() && end.is_none() {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
} else if start.is_some() && end.is_none() {
|
||||
if date < start.unwrap() {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
} else {
|
||||
view.set_date(Date::Range(Some(start.unwrap()), Some(date)), window, cx);
|
||||
}
|
||||
} else {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
}
|
||||
|
||||
if view.date.is_complete() {
|
||||
.when(!disabled, |this| {
|
||||
this.on_click(cx.listener(move |view, _: &ClickEvent, window, cx| {
|
||||
if view.date.is_single() {
|
||||
view.set_date(date, window, cx);
|
||||
cx.emit(CalendarEvent::Selected(view.date()));
|
||||
} else {
|
||||
let start = view.date.start();
|
||||
let end = view.date.end();
|
||||
|
||||
if start.is_none() && end.is_none() {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
} else if start.is_some() && end.is_none() {
|
||||
if date < start.unwrap() {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
} else {
|
||||
view.set_date(
|
||||
Date::Range(Some(start.unwrap()), Some(date)),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
view.set_date(Date::Range(Some(date), None), window, cx);
|
||||
}
|
||||
|
||||
if view.date.is_complete() {
|
||||
cx.emit(CalendarEvent::Selected(view.date()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn set_view_mode(&mut self, mode: ViewMode, _: &mut Window, cx: &mut Context<Self>) {
|
||||
|
|
@ -647,14 +758,25 @@ impl Calendar {
|
|||
.map(|(ix, month)| {
|
||||
let active = (ix + 1) as u8 == self.current_month;
|
||||
|
||||
self.item_button(ix, month.to_string(), active, false, false, window, cx)
|
||||
.w(relative(0.3))
|
||||
.text_sm()
|
||||
.on_click(cx.listener(move |view, _, window, cx| {
|
||||
self.item_button(
|
||||
ix,
|
||||
month.to_string(),
|
||||
active,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.w(relative(0.3))
|
||||
.text_sm()
|
||||
.on_click(cx.listener(
|
||||
move |view, _, window, cx| {
|
||||
view.current_month = (ix + 1) as u8;
|
||||
view.set_view_mode(ViewMode::Day, window, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
|
|
@ -681,13 +803,24 @@ impl Calendar {
|
|||
let year = *year;
|
||||
let active = year == self.current_year;
|
||||
|
||||
self.item_button(ix, year.to_string(), active, false, false, window, cx)
|
||||
.w(relative(0.2))
|
||||
.on_click(cx.listener(move |view, _, window, cx| {
|
||||
self.item_button(
|
||||
ix,
|
||||
year.to_string(),
|
||||
active,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.w(relative(0.2))
|
||||
.on_click(cx.listener(
|
||||
move |view, _, window, cx| {
|
||||
view.current_year = year;
|
||||
view.set_view_mode(ViewMode::Day, window, cx);
|
||||
cx.notify();
|
||||
}))
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::{
|
|||
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _,
|
||||
};
|
||||
|
||||
use super::calendar::{Calendar, CalendarEvent, Date};
|
||||
use super::calendar::{Calendar, CalendarEvent, Date, Matcher};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let context = Some("DatePicker");
|
||||
|
|
@ -190,6 +190,18 @@ impl DatePicker {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the disabled matcher of the date picker.
|
||||
pub fn set_disabled(
|
||||
&mut self,
|
||||
disabled: impl Into<Matcher>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.calendar.update(cx, |view, cx| {
|
||||
view.set_disabled(disabled.into(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Set size of the date picker.
|
||||
pub fn set_size(&mut self, size: Size, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.size = size;
|
||||
|
|
|
|||
Loading…
Reference in a new issue