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:
Floyd Wang 2025-02-13 17:43:19 +08:00 committed by GitHub
parent 83375147fe
commit c7257d36cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 198 additions and 39 deletions

View file

@ -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
});

View file

@ -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<_>>(),
)

View file

@ -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;