Add Calendar and DatePicker. (#117)
https://github.com/user-attachments/assets/dda87fa7-f080-418a-8dd7-ab8581eb3beb
This commit is contained in:
parent
e07f551317
commit
ebdcf244e2
22 changed files with 1130 additions and 34 deletions
219
Cargo.lock
generated
219
Cargo.lock
generated
|
|
@ -54,6 +54,21 @@ version = "0.2.18"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.15"
|
||||
|
|
@ -115,6 +130,12 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||
|
||||
[[package]]
|
||||
name = "arg_enum_proc_macro"
|
||||
version = "0.3.4"
|
||||
|
|
@ -860,6 +881,20 @@ dependencies = [
|
|||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
|
|
@ -2122,6 +2157,17 @@ dependencies = [
|
|||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "globwalk"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.13.1"
|
||||
|
|
@ -2270,6 +2316,7 @@ dependencies = [
|
|||
"gpui",
|
||||
"log",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"story",
|
||||
"ui",
|
||||
"workspace",
|
||||
|
|
@ -2502,6 +2549,29 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
|
|
@ -2512,6 +2582,22 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.1"
|
||||
|
|
@ -2878,6 +2964,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.27"
|
||||
|
|
@ -3174,6 +3266,15 @@ version = "0.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "normpath"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8911957c4b1549ac0dc74e30db9c8b0e66ddcd6d7acc33098f4c63a64a6d7ed"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
|
|
@ -4205,6 +4306,57 @@ dependencies = [
|
|||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dcd94370631e5658a0a23635f7f47e43d06a00ad948e0bb5de79b00d85b880c"
|
||||
dependencies = [
|
||||
"globwalk",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"rust-i18n-macro",
|
||||
"rust-i18n-support",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-macro"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "355763801dcf287e777e42def7c578410783477b804b1107852119e0b2518396"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-i18n-support",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"syn 2.0.71",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-support"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "399801f4d955abf1c3ce3ce2215dc76bd40beb4ae39e3a84936b21a79ce2caa5"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"globwalk",
|
||||
"lazy_static",
|
||||
"normpath",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"toml 0.7.8",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
|
|
@ -4477,6 +4629,18 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
|
||||
dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"ryu",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "servo_arc"
|
||||
version = "0.1.1"
|
||||
|
|
@ -4731,6 +4895,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"charts-rs",
|
||||
"chrono",
|
||||
"fake",
|
||||
"gpui",
|
||||
"regex",
|
||||
|
|
@ -5142,6 +5307,18 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.15"
|
||||
|
|
@ -5170,6 +5347,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
|
@ -5251,6 +5430,17 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.21.1"
|
||||
|
|
@ -5285,12 +5475,14 @@ name = "ui"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"gpui",
|
||||
"image",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"regex",
|
||||
"resvg",
|
||||
"rust-i18n",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
|
|
@ -5869,6 +6061,15 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
|
|
@ -5993,6 +6194,15 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
|
|
@ -6392,6 +6602,15 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yazi"
|
||||
version = "0.1.6"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ A UI components for building desktop application by using [GPUI](https://gpui.rs
|
|||
- [x] Slider
|
||||
- [ ] Skeleton
|
||||
- [ ] DatePicker
|
||||
- [ ] DateTimePicker
|
||||
- [x] DatePicker
|
||||
- [x] Calendar
|
||||
- [ ] TimePicker
|
||||
- [ ] DateRangePicker
|
||||
- [ ] ColorPicker
|
||||
|
|
|
|||
1
assets/icons/calendar.svg
Normal file
1
assets/icons/calendar.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/></svg>
|
||||
|
After Width: | Height: | Size: 325 B |
1
assets/icons/globe.svg
Normal file
1
assets/icons/globe.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
|
||||
|
After Width: | Height: | Size: 327 B |
|
|
@ -11,7 +11,7 @@ log.workspace = true
|
|||
workspace.workspace = true
|
||||
ui.workspace = true
|
||||
story.workspace = true
|
||||
|
||||
serde.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
use private::serde::Deserialize;
|
||||
use story::{
|
||||
ButtonStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory, PickerStory,
|
||||
PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory,
|
||||
TableStory, TextStory, TooltipStory,
|
||||
ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory,
|
||||
PickerStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
|
||||
SwitchStory, TableStory, TextStory, TooltipStory,
|
||||
};
|
||||
use workspace::{TitleBar, Workspace};
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::{Button, ButtonStyle},
|
||||
popover::Popover,
|
||||
popup_menu::PopupMenu,
|
||||
theme::{ActiveTheme, Theme},
|
||||
Clickable as _, IconName, Sizable,
|
||||
};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize)]
|
||||
struct SelectLocale(SharedString);
|
||||
|
||||
impl_actions!(locale_switcher, [SelectLocale]);
|
||||
|
||||
actions!(workspace, [Open, CloseWindow]);
|
||||
|
||||
pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
|
|
@ -28,6 +36,7 @@ pub fn init(_app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||
|
||||
pub struct StoryWorkspace {
|
||||
workspace: View<Workspace>,
|
||||
locale_selector: View<LocaleSelector>,
|
||||
}
|
||||
|
||||
impl StoryWorkspace {
|
||||
|
|
@ -184,7 +193,20 @@ impl StoryWorkspace {
|
|||
)
|
||||
.detach();
|
||||
|
||||
Self { workspace }
|
||||
StoryContainer::add_pane(
|
||||
"Calendar",
|
||||
"A calendar component.",
|
||||
CalendarStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
||||
let locale_selector = cx.new_view(LocaleSelector::new);
|
||||
Self {
|
||||
workspace,
|
||||
locale_selector,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_local(
|
||||
|
|
@ -276,6 +298,7 @@ impl Render for StoryWorkspace {
|
|||
.px_2()
|
||||
.mr_3()
|
||||
.gap_2()
|
||||
.child(self.locale_selector.clone())
|
||||
.child(
|
||||
Button::new("theme-mode", cx)
|
||||
.map(|this| {
|
||||
|
|
@ -310,3 +333,53 @@ impl Render for StoryWorkspace {
|
|||
.child(self.workspace.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct LocaleSelector {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl LocaleSelector {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_select_locale(&mut self, locale: &SelectLocale, cx: &mut ViewContext<Self>) {
|
||||
ui::set_locale(&locale.0);
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LocaleSelector {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let locale = ui::locale().to_string();
|
||||
|
||||
div()
|
||||
.id("locale-selector")
|
||||
.track_focus(&focus_handle)
|
||||
.on_action(cx.listener(Self::on_select_locale))
|
||||
.child(
|
||||
Popover::new("locale-selector")
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.trigger(Button::new("btn", cx).small().ghost().icon(IconName::Globe))
|
||||
.content(move |cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
PopupMenu::build(cx, |this, _cx| {
|
||||
this.track_focus(focus_handle)
|
||||
.menu_with_check(
|
||||
"English",
|
||||
locale == "en",
|
||||
Box::new(SelectLocale("en".into())),
|
||||
)
|
||||
.menu_with_check(
|
||||
"简体中文",
|
||||
locale == "zh-CN",
|
||||
Box::new(SelectLocale("zh-CN".into())),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ anyhow = "1"
|
|||
workspace.workspace = true
|
||||
charts-rs = "0.3"
|
||||
regex = "1"
|
||||
chrono = "0.4"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
67
crates/story/src/calendar_story.rs
Normal file
67
crates/story/src/calendar_story.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use gpui::{
|
||||
px, IntoElement, ParentElement as _, Render, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use ui::{
|
||||
date_picker::{DatePicker, DatePickerEvent},
|
||||
v_flex, Sizable as _,
|
||||
};
|
||||
|
||||
pub struct CalendarStory {
|
||||
date_picker: View<DatePicker>,
|
||||
date_picker_small: View<DatePicker>,
|
||||
date_picker_large: View<DatePicker>,
|
||||
date_picker_value: Option<String>,
|
||||
}
|
||||
|
||||
impl CalendarStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let date_picker = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_medium", cx)
|
||||
.default_now()
|
||||
.cleanable(true)
|
||||
.width(px(220.))
|
||||
});
|
||||
let date_picker_large = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_large", cx)
|
||||
.large()
|
||||
.date_format("%Y-%m-%d")
|
||||
.width(px(300.))
|
||||
});
|
||||
let date_picker_small = cx.new_view(|cx| {
|
||||
DatePicker::new("date_picker_small", cx)
|
||||
.default_now()
|
||||
.small()
|
||||
.width(px(180.))
|
||||
});
|
||||
|
||||
cx.subscribe(&date_picker, |this, _, ev, _| match ev {
|
||||
DatePickerEvent::Change(date) => {
|
||||
this.date_picker_value = date.map(|d| d.to_string());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
date_picker,
|
||||
date_picker_large,
|
||||
date_picker_small,
|
||||
date_picker_value: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CalendarStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(self.date_picker.clone())
|
||||
.child(self.date_picker_small.clone())
|
||||
.child(self.date_picker_large.clone())
|
||||
.child(format!("Date picker value: {:?}", self.date_picker_value).into_element())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod button_story;
|
||||
mod calendar_story;
|
||||
mod dropdown_story;
|
||||
mod icon_story;
|
||||
mod image_story;
|
||||
|
|
@ -16,6 +17,7 @@ mod tooltip_story;
|
|||
mod webview_story;
|
||||
|
||||
pub use button_story::ButtonStory;
|
||||
pub use calendar_story::CalendarStory;
|
||||
pub use dropdown_story::DropdownStory;
|
||||
pub use icon_story::IconStory;
|
||||
pub use image_story::ImageStory;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ once_cell = "1.19.0"
|
|||
wry = "0"
|
||||
smol = "1"
|
||||
regex = "1"
|
||||
rust-i18n = "3"
|
||||
|
||||
# Calendar
|
||||
chrono = "0.4.38"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
83
crates/ui/locales/ui.yml
Normal file
83
crates/ui/locales/ui.yml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
_version: 2
|
||||
Calendar:
|
||||
week.0:
|
||||
en: Su
|
||||
zh-CN: 日
|
||||
zh-HK: 日
|
||||
week.1:
|
||||
en: Mo
|
||||
zh-CN: 一
|
||||
zh-HK: 一
|
||||
week.2:
|
||||
en: Tu
|
||||
zh-CN: 二
|
||||
zh-HK: 二
|
||||
week.3:
|
||||
en: We
|
||||
zh-CN: 三
|
||||
zh-HK: 三
|
||||
week.4:
|
||||
en: Th
|
||||
zh-CN: 四
|
||||
zh-HK: 四
|
||||
week.5:
|
||||
en: Fr
|
||||
zh-CN: 五
|
||||
zh-HK: 五
|
||||
week.6:
|
||||
en: Sa
|
||||
zh-CN: 六
|
||||
zh-HK: 六
|
||||
month.January:
|
||||
en: January
|
||||
zh-CN: 一月
|
||||
zh-HK: 一月
|
||||
month.February:
|
||||
en: February
|
||||
zh-CN: 二月
|
||||
zh-HK: 二月
|
||||
month.March:
|
||||
en: March
|
||||
zh-CN: 三月
|
||||
zh-HK: 三月
|
||||
month.April:
|
||||
en: April
|
||||
zh-CN: 四月
|
||||
zh-HK: 四月
|
||||
month.May:
|
||||
en: May
|
||||
zh-CN: 五月
|
||||
zh-HK: 五月
|
||||
month.June:
|
||||
en: June
|
||||
zh-CN: 六月
|
||||
zh-HK: 六月
|
||||
month.July:
|
||||
en: July
|
||||
zh-CN: 七月
|
||||
zh-HK: 七月
|
||||
month.August:
|
||||
en: August
|
||||
zh-CN: 八月
|
||||
zh-HK: 八月
|
||||
month.September:
|
||||
en: September
|
||||
zh-CN: 九月
|
||||
zh-HK: 九月
|
||||
month.October:
|
||||
en: October
|
||||
zh-CN: 十月
|
||||
zh-HK: 十月
|
||||
month.November:
|
||||
en: November
|
||||
zh-CN: 十一月
|
||||
zh-HK: 十一月
|
||||
month.December:
|
||||
en: December
|
||||
zh-CN: 十二月
|
||||
zh-HK: 十二月
|
||||
DatePicker:
|
||||
placeholder:
|
||||
en: Select date
|
||||
zh-CN: 选择日期
|
||||
zh-HK: 選擇日期
|
||||
|
|
@ -7,8 +7,8 @@ use gpui::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
button::{Button, ButtonStyle},
|
||||
h_flex,
|
||||
input::ClearButton,
|
||||
list::{self, List, ListDelegate, ListItem},
|
||||
styled_ext::StyleSized,
|
||||
theme::ActiveTheme,
|
||||
|
|
@ -591,14 +591,7 @@ where
|
|||
.child(self.display_title(cx)),
|
||||
)
|
||||
.when(show_clean, |this| {
|
||||
this.child(
|
||||
Button::new("clean", cx)
|
||||
.icon(IconName::CircleX)
|
||||
.style(ButtonStyle::Ghost)
|
||||
.with_size(px(14.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean)))
|
||||
})
|
||||
.when(!show_clean, |this| {
|
||||
let icon = match self.icon.clone() {
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ pub enum IconName {
|
|||
ArrowRight,
|
||||
ArrowUp,
|
||||
Asterisk,
|
||||
Calendar,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronsUpDown,
|
||||
ChevronUp,
|
||||
ChevronsUpDown,
|
||||
CircleX,
|
||||
Close,
|
||||
Copy,
|
||||
Dash,
|
||||
Delete,
|
||||
Ellipsis,
|
||||
|
|
@ -26,6 +28,7 @@ pub enum IconName {
|
|||
Eye,
|
||||
EyeOff,
|
||||
GitHub,
|
||||
Globe,
|
||||
Heart,
|
||||
HeartOff,
|
||||
Inbox,
|
||||
|
|
@ -33,6 +36,7 @@ pub enum IconName {
|
|||
Loader,
|
||||
LoaderCircle,
|
||||
Maximize,
|
||||
Menu,
|
||||
Minimize,
|
||||
Minus,
|
||||
Moon,
|
||||
|
|
@ -45,8 +49,6 @@ pub enum IconName {
|
|||
Sun,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
Menu,
|
||||
Copy,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
|
|
@ -57,14 +59,16 @@ impl IconName {
|
|||
IconName::ArrowRight => "icons/arrow-right.svg",
|
||||
IconName::ArrowUp => "icons/arrow-up.svg",
|
||||
IconName::Asterisk => "icons/asterisk.svg",
|
||||
IconName::Calendar => "icons/calendar.svg",
|
||||
IconName::Check => "icons/check.svg",
|
||||
IconName::ChevronDown => "icons/chevron-down.svg",
|
||||
IconName::ChevronLeft => "icons/chevron-left.svg",
|
||||
IconName::ChevronRight => "icons/chevron-right.svg",
|
||||
IconName::ChevronsUpDown => "icons/chevrons-up-down.svg",
|
||||
IconName::ChevronUp => "icons/chevron-up.svg",
|
||||
IconName::ChevronsUpDown => "icons/chevrons-up-down.svg",
|
||||
IconName::CircleX => "icons/circle-x.svg",
|
||||
IconName::Close => "icons/close.svg",
|
||||
IconName::Copy => "icons/copy.svg",
|
||||
IconName::Dash => "icons/dash.svg",
|
||||
IconName::Delete => "icons/delete.svg",
|
||||
IconName::Ellipsis => "icons/ellipsis.svg",
|
||||
|
|
@ -72,6 +76,7 @@ impl IconName {
|
|||
IconName::Eye => "icons/eye.svg",
|
||||
IconName::EyeOff => "icons/eye-off.svg",
|
||||
IconName::GitHub => "icons/github.svg",
|
||||
IconName::Globe => "icons/globe.svg",
|
||||
IconName::Heart => "icons/heart.svg",
|
||||
IconName::HeartOff => "icons/heart-off.svg",
|
||||
IconName::Inbox => "icons/inbox.svg",
|
||||
|
|
@ -79,6 +84,7 @@ impl IconName {
|
|||
IconName::Loader => "icons/loader.svg",
|
||||
IconName::LoaderCircle => "icons/loader-circle.svg",
|
||||
IconName::Maximize => "icons/maximize.svg",
|
||||
IconName::Menu => "icons/menu.svg",
|
||||
IconName::Minimize => "icons/minimize.svg",
|
||||
IconName::Minus => "icons/minus.svg",
|
||||
IconName::Moon => "icons/moon.svg",
|
||||
|
|
@ -91,8 +97,6 @@ impl IconName {
|
|||
IconName::Sun => "icons/sun.svg",
|
||||
IconName::ThumbsDown => "icons/thumbs-down.svg",
|
||||
IconName::ThumbsUp => "icons/thumbs-up.svg",
|
||||
IconName::Menu => "icons/menu.svg",
|
||||
IconName::Copy => "icons/copy.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
|
|||
15
crates/ui/src/input/clear_button.rs
Normal file
15
crates/ui/src/input/clear_button.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use gpui::{px, Styled as _, WindowContext};
|
||||
|
||||
use crate::{button::Button, IconName, Sizable as _};
|
||||
|
||||
pub(crate) struct ClearButton {}
|
||||
|
||||
impl ClearButton {
|
||||
pub fn new(cx: &mut WindowContext) -> Button {
|
||||
Button::new("clean", cx)
|
||||
.icon(IconName::CircleX)
|
||||
.ghost()
|
||||
.with_size(px(14.))
|
||||
.cursor_pointer()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,12 +7,12 @@ use std::ops::Range;
|
|||
|
||||
use super::blink_cursor::BlinkCursor;
|
||||
use super::history::History;
|
||||
use crate::button::{Button, ButtonStyle};
|
||||
use super::ClearButton;
|
||||
use crate::indicator::Indicator;
|
||||
use crate::styled_ext::{Sizable, StyleSized};
|
||||
use crate::theme::ActiveTheme;
|
||||
use crate::{event::InteractiveElementExt as _, Size};
|
||||
use crate::{Clickable as _, IconName, StyledExt as _};
|
||||
use crate::{Clickable as _, StyledExt as _};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
actions, div, fill, point, px, relative, rems, size, AnyElement, AppContext, Bounds,
|
||||
|
|
@ -1066,16 +1066,7 @@ impl Render for TextInput {
|
|||
.when(self.loading, |this| this.child(Indicator::new()))
|
||||
.when(
|
||||
self.cleanable && !self.loading && !self.text.is_empty(),
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("clean-text", cx)
|
||||
.icon(IconName::Close)
|
||||
.style(ButtonStyle::Ghost)
|
||||
.with_size(px(15.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
},
|
||||
|this| this.child(ClearButton::new(cx).on_click(cx.listener(Self::clean))),
|
||||
)
|
||||
.children(suffix)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
mod blink_cursor;
|
||||
mod clear_button;
|
||||
mod history;
|
||||
mod input;
|
||||
mod otp_input;
|
||||
|
||||
pub(crate) use clear_button::*;
|
||||
pub use input::*;
|
||||
pub use otp_input::*;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mod selectable;
|
|||
mod stack;
|
||||
mod styled_ext;
|
||||
mod svg_img;
|
||||
mod time;
|
||||
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
|
|
@ -36,6 +37,8 @@ pub mod theme;
|
|||
pub mod tooltip;
|
||||
pub mod webview;
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
// re-export
|
||||
pub use wry;
|
||||
|
||||
|
|
@ -45,20 +48,32 @@ pub use event::InteractiveElementExt;
|
|||
pub use focusable::FocusableCycle;
|
||||
pub use selectable::{Selectable, Selection};
|
||||
pub use styled_ext::{Sizable, Size, StyledExt};
|
||||
pub use time::*;
|
||||
|
||||
pub use colors::*;
|
||||
pub use icon::*;
|
||||
pub use stack::*;
|
||||
pub use svg_img::*;
|
||||
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
|
||||
/// Initialize the UI module.
|
||||
pub fn init(cx: &mut gpui::AppContext) {
|
||||
input::init(cx);
|
||||
list::init(cx);
|
||||
dropdown::init(cx);
|
||||
date_picker::init(cx);
|
||||
popover::init(cx);
|
||||
popup_menu::init(cx);
|
||||
context_menu::init(cx);
|
||||
table::init(cx);
|
||||
webview::init(cx)
|
||||
}
|
||||
|
||||
pub fn locale() -> impl Deref<Target = str> {
|
||||
rust_i18n::locale()
|
||||
}
|
||||
|
||||
pub fn set_locale(locale: &str) {
|
||||
rust_i18n::set_locale(locale)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,11 +179,12 @@ pub trait StyledExt: Styled + Sized {
|
|||
impl<E: Styled> StyledExt for E {}
|
||||
|
||||
/// A size for elements.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
#[derive(Clone, Default, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Size {
|
||||
Size(Pixels),
|
||||
XSmall,
|
||||
Small,
|
||||
#[default]
|
||||
Medium,
|
||||
Large,
|
||||
}
|
||||
|
|
|
|||
222
crates/ui/src/time/calendar.rs
Normal file
222
crates/ui/src/time/calendar.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use gpui::{
|
||||
prelude::FluentBuilder as _, ClickEvent, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled as _, ViewContext,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
|
||||
use crate::{
|
||||
button::Button,
|
||||
h_flex,
|
||||
theme::{ActiveTheme, Colorize},
|
||||
v_flex, Clickable, IconName, StyledExt,
|
||||
};
|
||||
|
||||
use super::utils::days_in_month;
|
||||
|
||||
pub enum CalendarEvent {
|
||||
/// The user selected a date.
|
||||
Selected(NaiveDate),
|
||||
}
|
||||
|
||||
pub struct Calendar {
|
||||
date: Option<NaiveDate>,
|
||||
current_year: i32,
|
||||
current_month: u8,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
pub fn new(_cx: &mut ViewContext<Self>) -> Self {
|
||||
let today = Local::now().naive_local().date();
|
||||
Self {
|
||||
date: None,
|
||||
current_month: today.month() as u8,
|
||||
current_year: today.year(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the date of the calendar.
|
||||
pub fn set_date(&mut self, date: Option<NaiveDate>, cx: &mut ViewContext<Self>) {
|
||||
self.date = date;
|
||||
if let Some(date) = date {
|
||||
self.current_month = date.month() as u8;
|
||||
self.current_year = date.year();
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
/// Get the date of the calendar.
|
||||
pub fn date(&self) -> Option<NaiveDate> {
|
||||
self.date
|
||||
}
|
||||
|
||||
/// Returns the days of the month in a 2D vector to render on calendar.
|
||||
fn days(&self) -> Vec<Vec<NaiveDate>> {
|
||||
days_in_month(self.current_year, self.current_month as u32)
|
||||
}
|
||||
|
||||
fn prev_month(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.current_month = if self.current_month == 1 {
|
||||
12
|
||||
} else {
|
||||
self.current_month - 1
|
||||
};
|
||||
self.current_year = if self.current_month == 12 {
|
||||
self.current_year - 1
|
||||
} else {
|
||||
self.current_year
|
||||
};
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn next_month(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.current_month = if self.current_month == 12 {
|
||||
1
|
||||
} else {
|
||||
self.current_month + 1
|
||||
};
|
||||
self.current_year = if self.current_month == 1 {
|
||||
self.current_year + 1
|
||||
} else {
|
||||
self.current_year
|
||||
};
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn month_name(&self) -> SharedString {
|
||||
match self.current_month {
|
||||
1 => t!("Calendar.month.January"),
|
||||
2 => t!("Calendar.month.February"),
|
||||
3 => t!("Calendar.month.March"),
|
||||
4 => t!("Calendar.month.April"),
|
||||
5 => t!("Calendar.month.May"),
|
||||
6 => t!("Calendar.month.June"),
|
||||
7 => t!("Calendar.month.July"),
|
||||
8 => t!("Calendar.month.August"),
|
||||
9 => t!("Calendar.month.September"),
|
||||
10 => t!("Calendar.month.October"),
|
||||
11 => t!("Calendar.month.November"),
|
||||
12 => t!("Calendar.month.December"),
|
||||
_ => Cow::Borrowed(""),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_week(
|
||||
&self,
|
||||
week: impl Into<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
h_flex()
|
||||
.w_9()
|
||||
.h_9()
|
||||
.rounded_md()
|
||||
.justify_center()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.text_sm()
|
||||
.child(week.into())
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
ix: usize,
|
||||
d: &NaiveDate,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let day = d.day();
|
||||
let is_current_month = d.month() == self.current_month as u32;
|
||||
let is_active = match self.date {
|
||||
Some(date) => date == *d,
|
||||
None => false,
|
||||
};
|
||||
|
||||
let date = *d;
|
||||
|
||||
h_flex()
|
||||
.id(ix)
|
||||
.w_9()
|
||||
.h_9()
|
||||
.rounded_lg()
|
||||
.justify_center()
|
||||
.cursor_pointer()
|
||||
.when(!is_current_month, |this| {
|
||||
this.text_color(cx.theme().muted_foreground.opacity(0.3))
|
||||
})
|
||||
.when(!is_active, |this| {
|
||||
this.hover(|this| {
|
||||
this.bg(cx.theme().accent)
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
})
|
||||
})
|
||||
.when(is_active, |this| {
|
||||
this.bg(cx.theme().primary)
|
||||
.text_color(cx.theme().primary_foreground)
|
||||
})
|
||||
.on_click(cx.listener(move |view, _: &ClickEvent, cx| {
|
||||
view.set_date(Some(date), cx);
|
||||
cx.emit(CalendarEvent::Selected(date))
|
||||
}))
|
||||
.child(day.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<CalendarEvent> for Calendar {}
|
||||
|
||||
impl Render for Calendar {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
let weeks: [SharedString; 7] = [
|
||||
t!("Calendar.week.0").into(),
|
||||
t!("Calendar.week.1").into(),
|
||||
t!("Calendar.week.2").into(),
|
||||
t!("Calendar.week.3").into(),
|
||||
t!("Calendar.week.4").into(),
|
||||
t!("Calendar.week.5").into(),
|
||||
t!("Calendar.week.6").into(),
|
||||
];
|
||||
|
||||
v_flex().gap_0p5().text_sm().child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
Button::new("prev", cx)
|
||||
.icon(IconName::ArrowLeft)
|
||||
.ghost()
|
||||
.on_click(cx.listener(Self::prev_month)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.font_semibold()
|
||||
.gap_3()
|
||||
// TODO: Add menu to select month and year
|
||||
.child(self.current_year.to_string())
|
||||
.child(self.month_name()),
|
||||
)
|
||||
.child(
|
||||
Button::new("next", cx)
|
||||
.icon(IconName::ArrowRight)
|
||||
.ghost()
|
||||
.on_click(cx.listener(Self::next_month)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.children(weeks.iter().map(|week| self.render_week(week.clone(), cx))),
|
||||
)
|
||||
.children(self.days().iter().map(|week| {
|
||||
h_flex().gap_0p5().children(
|
||||
week.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, d)| self.render_item(ix, d, cx)),
|
||||
)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
228
crates/ui/src/time/date_picker.rs
Normal file
228
crates/ui/src/time/date_picker.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
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),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
3
crates/ui/src/time/mod.rs
Normal file
3
crates/ui/src/time/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod calendar;
|
||||
pub mod date_picker;
|
||||
mod utils;
|
||||
170
crates/ui/src/time/utils.rs
Normal file
170
crates/ui/src/time/utils.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use chrono::{Datelike, Duration, NaiveDate};
|
||||
|
||||
trait NaiveDateExt {
|
||||
fn days_in_month(&self) -> i32;
|
||||
fn is_leap_year(&self) -> bool;
|
||||
}
|
||||
|
||||
impl NaiveDateExt for chrono::NaiveDate {
|
||||
fn days_in_month(&self) -> i32 {
|
||||
let month = self.month();
|
||||
match month {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
2 => {
|
||||
if self.is_leap_year() {
|
||||
29
|
||||
} else {
|
||||
28
|
||||
}
|
||||
}
|
||||
_ => panic!("Invalid month: {}", month),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_leap_year(&self) -> bool {
|
||||
let year = self.year();
|
||||
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn days_in_month(year: i32, month: u32) -> Vec<Vec<NaiveDate>> {
|
||||
let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
|
||||
let num_days = date.days_in_month();
|
||||
let start_weekday = date.weekday().num_days_from_sunday();
|
||||
|
||||
// Get the days in the month, 2023-02 will returns
|
||||
// "29|30|31| 1| 2| 3| 4",
|
||||
// " 5| 6| 7| 8| 9|10|11",
|
||||
// "12|13|14|15|16|17|18",
|
||||
// "19|20|21|22|23|24|25",
|
||||
// "26|27|28| 1| 2| 3| 4",
|
||||
let mut days = vec![];
|
||||
for n in 0..5 {
|
||||
let mut week_days = vec![];
|
||||
for weekday in 0..7 {
|
||||
let (mut y, mut m) = (year, month);
|
||||
|
||||
// If the day is less than the start weekday, we need to go back to the previous month.
|
||||
if n == 0 && weekday < start_weekday {
|
||||
m = if m == 1 { 12 } else { m - 1 };
|
||||
y = if m == 1 { year - 1 } else { y };
|
||||
}
|
||||
|
||||
// If start_weekday is 3, and n is 0 and weekday is 3, then day is 1.
|
||||
// If start_weekday is 3, and n is 1 and weekday is 4, then day is 9.
|
||||
let day = n * 7 + weekday as i32 - start_weekday as i32;
|
||||
|
||||
// If the day is greater than the number of days in the month, we need to go to the next month.
|
||||
if day > num_days {
|
||||
m = if m == 12 { 1 } else { m + 1 };
|
||||
y = if m == 1 { year + 1 } else { y };
|
||||
}
|
||||
|
||||
#[allow(clippy::expect_fun_call)]
|
||||
let date = date
|
||||
.checked_add_signed(Duration::days(day as i64))
|
||||
.expect(&format!("invalid date {}-{} days {}", y, m, day));
|
||||
week_days.push(date);
|
||||
}
|
||||
|
||||
days.push(week_days);
|
||||
}
|
||||
|
||||
days
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
|
||||
use super::{days_in_month, NaiveDateExt};
|
||||
|
||||
#[test]
|
||||
fn test_days_in_month() {
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap().days_in_month(),
|
||||
29
|
||||
);
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd_opt(2023, 2, 1).unwrap().days_in_month(),
|
||||
28
|
||||
);
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd_opt(2023, 1, 1).unwrap().days_in_month(),
|
||||
31
|
||||
);
|
||||
assert_eq!(
|
||||
NaiveDate::from_ymd_opt(2023, 4, 1).unwrap().days_in_month(),
|
||||
30
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days() {
|
||||
#[track_caller]
|
||||
fn assert_case(date: NaiveDate, expected: Vec<&str>) {
|
||||
let out = days_in_month(date.year(), date.month())
|
||||
.iter()
|
||||
.map(|week| {
|
||||
week.iter()
|
||||
.map(|d| {
|
||||
if d.year() == date.year() && d.month() == date.month() {
|
||||
format!("{:2}", d.day())
|
||||
} else if d.year() == date.year() {
|
||||
format!("{}-{}", d.month(), d.day())
|
||||
} else {
|
||||
format!("{}-{}-{}", d.year(), d.month(), d.day())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(out, expected);
|
||||
}
|
||||
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2024, 8, 1).unwrap(),
|
||||
vec![
|
||||
"7-28|7-29|7-30|7-31| 1| 2| 3",
|
||||
" 4| 5| 6| 7| 8| 9|10",
|
||||
"11|12|13|14|15|16|17",
|
||||
"18|19|20|21|22|23|24",
|
||||
"25|26|27|28|29|30|31",
|
||||
],
|
||||
);
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(),
|
||||
vec![
|
||||
"2024-12-29|2024-12-30|2024-12-31| 1| 2| 3| 4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
"12|13|14|15|16|17|18",
|
||||
"19|20|21|22|23|24|25",
|
||||
"26|27|28|29|30|31|2-1",
|
||||
],
|
||||
);
|
||||
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2024, 2, 1).unwrap(),
|
||||
vec![
|
||||
"1-28|1-29|1-30|1-31| 1| 2| 3",
|
||||
" 4| 5| 6| 7| 8| 9|10",
|
||||
"11|12|13|14|15|16|17",
|
||||
"18|19|20|21|22|23|24",
|
||||
"25|26|27|28|29|3-1|3-2",
|
||||
],
|
||||
);
|
||||
assert_case(
|
||||
NaiveDate::from_ymd_opt(2023, 2, 20).unwrap(),
|
||||
vec![
|
||||
"1-29|1-30|1-31| 1| 2| 3| 4",
|
||||
" 5| 6| 7| 8| 9|10|11",
|
||||
"12|13|14|15|16|17|18",
|
||||
"19|20|21|22|23|24|25",
|
||||
"26|27|28|3-1|3-2|3-3|3-4",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue