Work on default theme, Notifications, Add ListHeader meta (#3203)

Work on default theme, Notifications, Add ListHeader `meta`

Also adds `naive_format_distance` & `naive_format_distance_from_now`

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2023-11-02 11:19:48 -04:00 committed by GitHub
commit 5e12b48ae0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1130 additions and 302 deletions

1
assets/icons/at-sign.svg Normal file
View 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-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>

After

Width:  |  Height:  |  Size: 300 B

View 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-bell-off"><path d="M8.7 3A6 6 0 0 1 18 8a21.3 21.3 0 0 0 .6 5"/><path d="M17 17H3s3-2 3-9a4.67 4.67 0 0 1 .3-1.7"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="m2 2 20 20"/></svg>

After

Width:  |  Height:  |  Size: 387 B

View 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-bell-ring"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M4 2C2.8 3.7 2 5.7 2 8"/><path d="M22 8c0-2.3-.8-4.3-2-6"/></svg>

After

Width:  |  Height:  |  Size: 382 B

View file

@ -1,8 +1 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.60124 1.25086C8.60124 1.75459 8.26278 2.17927 7.80087 2.30989C10.1459 2.4647 12 4.41582 12 6.79999V10.25C12 11.0563 12.0329 11.7074 12.7236 12.0528C12.931 12.1565 13.0399 12.3892 12.9866 12.6149C12.9333 12.8406 12.7319 13 12.5 13H8.16144C8.36904 13.1832 8.49997 13.4513 8.49997 13.75C8.49997 14.3023 8.05226 14.75 7.49997 14.75C6.94769 14.75 6.49997 14.3023 6.49997 13.75C6.49997 13.4513 6.63091 13.1832 6.83851 13H2.49999C2.2681 13 2.06664 12.8406 2.01336 12.6149C1.96009 12.3892 2.06897 12.1565 2.27638 12.0528C2.96708 11.7074 2.99999 11.0563 2.99999 10.25V6.79999C2.99999 4.41537 4.85481 2.46396 7.20042 2.3098C6.73867 2.17908 6.40036 1.75448 6.40036 1.25086C6.40036 0.643104 6.89304 0.150421 7.5008 0.150421C8.10855 0.150421 8.60124 0.643104 8.60124 1.25086ZM7.49999 3.29999C5.56699 3.29999 3.99999 4.86699 3.99999 6.79999V10.25L4.00002 10.3009C4.0005 10.7463 4.00121 11.4084 3.69929 12H11.3007C10.9988 11.4084 10.9995 10.7463 11 10.3009L11 10.25V6.79999C11 4.86699 9.43299 3.29999 7.49999 3.29999Z"
fill="currentColor"
/>
</svg>
<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-bell"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 309 B

Before After
Before After

View 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-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>

After

Width:  |  Height:  |  Size: 390 B

View file

@ -77,7 +77,7 @@ fn main() {
WindowOptions {
bounds: WindowBounds::Fixed(Bounds {
origin: Default::default(),
size: size(px(1700.), px(980.)).into(),
size: size(px(1500.), px(780.)).into(),
}),
..Default::default()
},

View file

@ -64,6 +64,7 @@ pub struct ThemeColors {
pub element_selected: Hsla,
pub element_disabled: Hsla,
pub element_placeholder: Hsla,
pub element_drop_target: Hsla,
pub ghost_element: Hsla,
pub ghost_element_hover: Hsla,
pub ghost_element_active: Hsla,
@ -83,6 +84,8 @@ pub struct ThemeColors {
pub title_bar: Hsla,
pub toolbar: Hsla,
pub tab_bar: Hsla,
pub tab_inactive: Hsla,
pub tab_active: Hsla,
pub editor: Hsla,
pub editor_subheader: Hsla,
pub editor_active_line: Hsla,

View file

@ -9,6 +9,10 @@ use crate::{
ColorScale,
};
fn neutral() -> ColorScaleSet {
slate()
}
impl Default for SystemColors {
fn default() -> Self {
Self {
@ -24,16 +28,16 @@ impl Default for StatusColors {
fn default() -> Self {
Self {
conflict: red().dark().step_11(),
created: gpui2::black(),
deleted: gpui2::black(),
error: gpui2::black(),
hidden: gpui2::black(),
ignored: gpui2::black(),
info: gpui2::black(),
modified: gpui2::black(),
renamed: gpui2::black(),
success: gpui2::black(),
warning: gpui2::black(),
created: grass().dark().step_11(),
deleted: red().dark().step_11(),
error: red().dark().step_11(),
hidden: neutral().dark().step_11(),
ignored: neutral().dark().step_11(),
info: blue().dark().step_11(),
modified: yellow().dark().step_11(),
renamed: blue().dark().step_11(),
success: grass().dark().step_11(),
warning: yellow().dark().step_11(),
}
}
}
@ -41,12 +45,12 @@ impl Default for StatusColors {
impl Default for GitStatusColors {
fn default() -> Self {
Self {
conflict: gpui2::rgba(0xdec184ff).into(),
created: gpui2::rgba(0xa1c181ff).into(),
deleted: gpui2::rgba(0xd07277ff).into(),
ignored: gpui2::rgba(0x555a63ff).into(),
modified: gpui2::rgba(0x74ade8ff).into(),
renamed: gpui2::rgba(0xdec184ff).into(),
conflict: orange().dark().step_11(),
created: grass().dark().step_11(),
deleted: red().dark().step_11(),
ignored: neutral().dark().step_11(),
modified: yellow().dark().step_11(),
renamed: blue().dark().step_11(),
}
}
}
@ -82,54 +86,57 @@ impl SyntaxTheme {
pub fn default_light() -> Self {
Self {
highlights: vec![
("attribute".into(), cyan().light().step_11().into()),
("boolean".into(), tomato().light().step_11().into()),
("comment".into(), neutral().light().step_11().into()),
("comment.doc".into(), iris().light().step_12().into()),
("constant".into(), red().light().step_7().into()),
("constructor".into(), red().light().step_7().into()),
("embedded".into(), red().light().step_7().into()),
("emphasis".into(), red().light().step_7().into()),
("emphasis.strong".into(), red().light().step_7().into()),
("enum".into(), red().light().step_7().into()),
("function".into(), red().light().step_7().into()),
("hint".into(), red().light().step_7().into()),
("keyword".into(), orange().light().step_11().into()),
("label".into(), red().light().step_7().into()),
("link_text".into(), red().light().step_7().into()),
("link_uri".into(), red().light().step_7().into()),
("number".into(), red().light().step_7().into()),
("operator".into(), red().light().step_7().into()),
("predictive".into(), red().light().step_7().into()),
("preproc".into(), red().light().step_7().into()),
("primary".into(), red().light().step_7().into()),
("property".into(), red().light().step_7().into()),
("punctuation".into(), neutral().light().step_11().into()),
(
"string.special.symbol".into(),
gpui2::rgba(0xad6e26ff).into(),
"punctuation.bracket".into(),
neutral().light().step_11().into(),
),
("hint".into(), gpui2::rgba(0x9294beff).into()),
("link_uri".into(), gpui2::rgba(0x3882b7ff).into()),
("type".into(), gpui2::rgba(0x3882b7ff).into()),
("string.regex".into(), gpui2::rgba(0xad6e26ff).into()),
("constant".into(), gpui2::rgba(0x669f59ff).into()),
("function".into(), gpui2::rgba(0x5b79e3ff).into()),
("string.special".into(), gpui2::rgba(0xad6e26ff).into()),
("punctuation.bracket".into(), gpui2::rgba(0x4d4f52ff).into()),
("variable".into(), gpui2::rgba(0x383a41ff).into()),
("punctuation".into(), gpui2::rgba(0x383a41ff).into()),
("property".into(), gpui2::rgba(0xd3604fff).into()),
("string".into(), gpui2::rgba(0x649f57ff).into()),
("predictive".into(), gpui2::rgba(0x9b9ec6ff).into()),
("attribute".into(), gpui2::rgba(0x5c78e2ff).into()),
("number".into(), gpui2::rgba(0xad6e25ff).into()),
("constructor".into(), gpui2::rgba(0x5c78e2ff).into()),
("embedded".into(), gpui2::rgba(0x383a41ff).into()),
("title".into(), gpui2::rgba(0xd3604fff).into()),
("tag".into(), gpui2::rgba(0x5c78e2ff).into()),
("boolean".into(), gpui2::rgba(0xad6e25ff).into()),
(
"punctuation.list_marker".into(),
gpui2::rgba(0xd3604fff).into(),
),
("variant".into(), gpui2::rgba(0x5b79e3ff).into()),
("emphasis".into(), gpui2::rgba(0x5c78e2ff).into()),
("link_text".into(), gpui2::rgba(0x5b79e3ff).into()),
("comment".into(), gpui2::rgba(0xa2a3a7ff).into()),
("punctuation.special".into(), gpui2::rgba(0xb92b46ff).into()),
("emphasis.strong".into(), gpui2::rgba(0xad6e25ff).into()),
("primary".into(), gpui2::rgba(0x383a41ff).into()),
(
"punctuation.delimiter".into(),
gpui2::rgba(0x4d4f52ff).into(),
neutral().light().step_11().into(),
),
("label".into(), gpui2::rgba(0x5c78e2ff).into()),
("keyword".into(), gpui2::rgba(0xa449abff).into()),
("string.escape".into(), gpui2::rgba(0x7c7e86ff).into()),
("text.literal".into(), gpui2::rgba(0x649f57ff).into()),
("variable.special".into(), gpui2::rgba(0xad6e25ff).into()),
("comment.doc".into(), gpui2::rgba(0x7c7e86ff).into()),
("enum".into(), gpui2::rgba(0xd3604fff).into()),
("operator".into(), gpui2::rgba(0x3882b7ff).into()),
("preproc".into(), gpui2::rgba(0x383a41ff).into()),
(
"punctuation.list_marker".into(),
blue().light().step_11().into(),
),
("punctuation.special".into(), red().light().step_7().into()),
("string".into(), jade().light().step_11().into()),
("string.escape".into(), red().light().step_7().into()),
("string.regex".into(), tomato().light().step_11().into()),
("string.special".into(), red().light().step_7().into()),
(
"string.special.symbol".into(),
red().light().step_7().into(),
),
("tag".into(), red().light().step_7().into()),
("text.literal".into(), red().light().step_7().into()),
("title".into(), red().light().step_7().into()),
("type".into(), red().light().step_7().into()),
("variable".into(), red().light().step_7().into()),
("variable.special".into(), red().light().step_7().into()),
("variant".into(), red().light().step_7().into()),
],
}
}
@ -137,54 +144,54 @@ impl SyntaxTheme {
pub fn default_dark() -> Self {
Self {
highlights: vec![
("keyword".into(), gpui2::rgba(0xb477cfff).into()),
("comment.doc".into(), gpui2::rgba(0x878e98ff).into()),
("variant".into(), gpui2::rgba(0x73ade9ff).into()),
("property".into(), gpui2::rgba(0xd07277ff).into()),
("function".into(), gpui2::rgba(0x73ade9ff).into()),
("type".into(), gpui2::rgba(0x6eb4bfff).into()),
("tag".into(), gpui2::rgba(0x74ade8ff).into()),
("string.escape".into(), gpui2::rgba(0x878e98ff).into()),
("punctuation.bracket".into(), gpui2::rgba(0xb2b9c6ff).into()),
("hint".into(), gpui2::rgba(0x5a6f89ff).into()),
("punctuation".into(), gpui2::rgba(0xacb2beff).into()),
("comment".into(), gpui2::rgba(0x5d636fff).into()),
("emphasis".into(), gpui2::rgba(0x74ade8ff).into()),
("punctuation.special".into(), gpui2::rgba(0xb1574bff).into()),
("link_uri".into(), gpui2::rgba(0x6eb4bfff).into()),
("string.regex".into(), gpui2::rgba(0xbf956aff).into()),
("constructor".into(), gpui2::rgba(0x73ade9ff).into()),
("operator".into(), gpui2::rgba(0x6eb4bfff).into()),
("constant".into(), gpui2::rgba(0xdfc184ff).into()),
("string.special".into(), gpui2::rgba(0xbf956aff).into()),
("emphasis.strong".into(), gpui2::rgba(0xbf956aff).into()),
("attribute".into(), cyan().dark().step_11().into()),
("boolean".into(), tomato().dark().step_11().into()),
("comment".into(), neutral().dark().step_11().into()),
("comment.doc".into(), iris().dark().step_12().into()),
("constant".into(), red().dark().step_7().into()),
("constructor".into(), red().dark().step_7().into()),
("embedded".into(), red().dark().step_7().into()),
("emphasis".into(), red().dark().step_7().into()),
("emphasis.strong".into(), red().dark().step_7().into()),
("enum".into(), red().dark().step_7().into()),
("function".into(), red().dark().step_7().into()),
("hint".into(), red().dark().step_7().into()),
("keyword".into(), orange().dark().step_11().into()),
("label".into(), red().dark().step_7().into()),
("link_text".into(), red().dark().step_7().into()),
("link_uri".into(), red().dark().step_7().into()),
("number".into(), red().dark().step_7().into()),
("operator".into(), red().dark().step_7().into()),
("predictive".into(), red().dark().step_7().into()),
("preproc".into(), red().dark().step_7().into()),
("primary".into(), red().dark().step_7().into()),
("property".into(), red().dark().step_7().into()),
("punctuation".into(), neutral().dark().step_11().into()),
(
"string.special.symbol".into(),
gpui2::rgba(0xbf956aff).into(),
"punctuation.bracket".into(),
neutral().dark().step_11().into(),
),
("primary".into(), gpui2::rgba(0xacb2beff).into()),
("preproc".into(), gpui2::rgba(0xc8ccd4ff).into()),
("string".into(), gpui2::rgba(0xa1c181ff).into()),
(
"punctuation.delimiter".into(),
gpui2::rgba(0xb2b9c6ff).into(),
neutral().dark().step_11().into(),
),
("embedded".into(), gpui2::rgba(0xc8ccd4ff).into()),
("enum".into(), gpui2::rgba(0xd07277ff).into()),
("variable.special".into(), gpui2::rgba(0xbf956aff).into()),
("text.literal".into(), gpui2::rgba(0xa1c181ff).into()),
("attribute".into(), gpui2::rgba(0x74ade8ff).into()),
("link_text".into(), gpui2::rgba(0x73ade9ff).into()),
("title".into(), gpui2::rgba(0xd07277ff).into()),
("predictive".into(), gpui2::rgba(0x5a6a87ff).into()),
("number".into(), gpui2::rgba(0xbf956aff).into()),
("label".into(), gpui2::rgba(0x74ade8ff).into()),
("variable".into(), gpui2::rgba(0xc8ccd4ff).into()),
("boolean".into(), gpui2::rgba(0xbf956aff).into()),
(
"punctuation.list_marker".into(),
gpui2::rgba(0xd07277ff).into(),
blue().dark().step_11().into(),
),
("punctuation.special".into(), red().dark().step_7().into()),
("string".into(), jade().dark().step_11().into()),
("string.escape".into(), red().dark().step_7().into()),
("string.regex".into(), tomato().dark().step_11().into()),
("string.special".into(), red().dark().step_7().into()),
("string.special.symbol".into(), red().dark().step_7().into()),
("tag".into(), red().dark().step_7().into()),
("text.literal".into(), red().dark().step_7().into()),
("title".into(), red().dark().step_7().into()),
("type".into(), red().dark().step_7().into()),
("variable".into(), red().dark().step_7().into()),
("variable.special".into(), red().dark().step_7().into()),
("variant".into(), red().dark().step_7().into()),
],
}
}
@ -192,82 +199,92 @@ impl SyntaxTheme {
impl ThemeColors {
pub fn default_light() -> Self {
let system = SystemColors::default();
Self {
border: gpui2::white(),
border_variant: gpui2::white(),
border_focused: gpui2::white(),
border_transparent: gpui2::white(),
elevated_surface: gpui2::white(),
surface: gpui2::white(),
background: gpui2::white(),
element: gpui2::white(),
element_hover: gpui2::white(),
element_active: gpui2::white(),
element_selected: gpui2::white(),
element_disabled: gpui2::white(),
element_placeholder: gpui2::white(),
ghost_element: gpui2::white(),
ghost_element_hover: gpui2::white(),
ghost_element_active: gpui2::white(),
ghost_element_selected: gpui2::white(),
ghost_element_disabled: gpui2::white(),
text: gpui2::white(),
text_muted: gpui2::white(),
text_placeholder: gpui2::white(),
text_disabled: gpui2::white(),
text_accent: gpui2::white(),
icon: gpui2::white(),
icon_muted: gpui2::white(),
icon_disabled: gpui2::white(),
icon_placeholder: gpui2::white(),
icon_accent: gpui2::white(),
status_bar: gpui2::white(),
title_bar: gpui2::white(),
toolbar: gpui2::white(),
tab_bar: gpui2::white(),
editor: gpui2::white(),
editor_subheader: gpui2::white(),
editor_active_line: gpui2::white(),
border: neutral().light().step_6(),
border_variant: neutral().light().step_5(),
border_focused: blue().light().step_5(),
border_transparent: system.transparent,
elevated_surface: neutral().light().step_2(),
surface: neutral().light().step_2(),
background: neutral().light().step_1(),
element: neutral().light().step_3(),
element_hover: neutral().light().step_4(),
element_active: neutral().light().step_5(),
element_selected: neutral().light().step_5(),
element_disabled: neutral().light_alpha().step_3(),
element_placeholder: neutral().light().step_11(),
element_drop_target: blue().light_alpha().step_2(),
ghost_element: system.transparent,
ghost_element_hover: neutral().light().step_4(),
ghost_element_active: neutral().light().step_5(),
ghost_element_selected: neutral().light().step_5(),
ghost_element_disabled: neutral().light_alpha().step_3(),
text: neutral().light().step_12(),
text_muted: neutral().light().step_11(),
text_placeholder: neutral().light().step_10(),
text_disabled: neutral().light().step_9(),
text_accent: blue().light().step_11(),
icon: neutral().light().step_11(),
icon_muted: neutral().light().step_10(),
icon_disabled: neutral().light().step_9(),
icon_placeholder: neutral().light().step_10(),
icon_accent: blue().light().step_11(),
status_bar: neutral().light().step_2(),
title_bar: neutral().light().step_2(),
toolbar: neutral().light().step_1(),
tab_bar: neutral().light().step_2(),
tab_active: neutral().light().step_1(),
tab_inactive: neutral().light().step_2(),
editor: neutral().light().step_1(),
editor_subheader: neutral().light().step_2(),
editor_active_line: neutral().light_alpha().step_3(),
}
}
pub fn default_dark() -> Self {
let system = SystemColors::default();
Self {
border: gpui2::rgba(0x464b57ff).into(),
border_variant: gpui2::rgba(0x464b57ff).into(),
border_focused: gpui2::rgba(0x293b5bff).into(),
border_transparent: gpui2::rgba(0x00000000).into(),
elevated_surface: gpui2::rgba(0x3b414dff).into(),
surface: gpui2::rgba(0x2f343eff).into(),
background: gpui2::rgba(0x3b414dff).into(),
element: gpui2::rgba(0x3b414dff).into(),
element_hover: gpui2::rgba(0xffffff1e).into(),
element_active: gpui2::rgba(0xffffff28).into(),
element_selected: gpui2::rgba(0x18243dff).into(),
element_disabled: gpui2::rgba(0x00000000).into(),
element_placeholder: gpui2::black(),
ghost_element: gpui2::rgba(0x00000000).into(),
ghost_element_hover: gpui2::rgba(0xffffff14).into(),
ghost_element_active: gpui2::rgba(0xffffff1e).into(),
ghost_element_selected: gpui2::rgba(0x18243dff).into(),
ghost_element_disabled: gpui2::rgba(0x00000000).into(),
text: gpui2::rgba(0xc8ccd4ff).into(),
text_muted: gpui2::rgba(0x838994ff).into(),
text_placeholder: gpui2::rgba(0xd07277ff).into(),
text_disabled: gpui2::rgba(0x555a63ff).into(),
text_accent: gpui2::rgba(0x74ade8ff).into(),
icon: gpui2::black(),
icon_muted: gpui2::rgba(0x838994ff).into(),
icon_disabled: gpui2::black(),
icon_placeholder: gpui2::black(),
icon_accent: gpui2::black(),
status_bar: gpui2::rgba(0x3b414dff).into(),
title_bar: gpui2::rgba(0x3b414dff).into(),
toolbar: gpui2::rgba(0x282c33ff).into(),
tab_bar: gpui2::rgba(0x2f343eff).into(),
editor: gpui2::rgba(0x282c33ff).into(),
editor_subheader: gpui2::rgba(0x2f343eff).into(),
editor_active_line: gpui2::rgba(0x2f343eff).into(),
border: neutral().dark().step_6(),
border_variant: neutral().dark().step_5(),
border_focused: blue().dark().step_5(),
border_transparent: system.transparent,
elevated_surface: neutral().dark().step_2(),
surface: neutral().dark().step_2(),
background: neutral().dark().step_1(),
element: neutral().dark().step_3(),
element_hover: neutral().dark().step_4(),
element_active: neutral().dark().step_5(),
element_selected: neutral().dark().step_5(),
element_disabled: neutral().dark_alpha().step_3(),
element_placeholder: neutral().dark().step_11(),
element_drop_target: blue().dark_alpha().step_2(),
ghost_element: system.transparent,
ghost_element_hover: neutral().dark().step_4(),
ghost_element_active: neutral().dark().step_5(),
ghost_element_selected: neutral().dark().step_5(),
ghost_element_disabled: neutral().dark_alpha().step_3(),
text: neutral().dark().step_12(),
text_muted: neutral().dark().step_11(),
text_placeholder: neutral().dark().step_10(),
text_disabled: neutral().dark().step_9(),
text_accent: blue().dark().step_11(),
icon: neutral().dark().step_11(),
icon_muted: neutral().dark().step_10(),
icon_disabled: neutral().dark().step_9(),
icon_placeholder: neutral().dark().step_10(),
icon_accent: blue().dark().step_11(),
status_bar: neutral().dark().step_2(),
title_bar: neutral().dark().step_2(),
toolbar: neutral().dark().step_1(),
tab_bar: neutral().dark().step_2(),
tab_active: neutral().dark().step_1(),
tab_inactive: neutral().dark().step_2(),
editor: neutral().dark().step_1(),
editor_subheader: neutral().dark().step_2(),
editor_active_line: neutral().dark_alpha().step_3(),
}
}
}

View file

@ -70,6 +70,18 @@ impl ThemeVariant {
&self.styles.syntax
}
/// Returns the [`StatusColors`] for the theme.
#[inline(always)]
pub fn status(&self) -> &StatusColors {
&self.styles.status
}
/// Returns the [`GitStatusColors`] for the theme.
#[inline(always)]
pub fn git(&self) -> &GitStatusColors {
&self.styles.git
}
/// Returns the color for the syntax node with the given name.
#[inline(always)]
pub fn syntax_color(&self, name: &str) -> Hsla {

View file

@ -1,4 +1,4 @@
use gpui2::{div, relative, Div};
use gpui2::{div, px, relative, Div};
use crate::settings::user_settings;
use crate::{
@ -15,12 +15,20 @@ pub enum ListItemVariant {
Inset,
}
pub enum ListHeaderMeta {
// TODO: These should be IconButtons
Tools(Vec<Icon>),
// TODO: This should be a button
Button(Label),
Text(Label),
}
#[derive(Component)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
meta: Option<ListHeaderMeta>,
variant: ListItemVariant,
state: InteractionState,
toggleable: Toggleable,
}
@ -29,9 +37,9 @@ impl ListHeader {
Self {
label: label.into(),
left_icon: None,
meta: None,
variant: ListItemVariant::default(),
state: InteractionState::default(),
toggleable: Toggleable::Toggleable(ToggleState::Toggled),
toggleable: Toggleable::NotToggleable,
}
}
@ -50,8 +58,8 @@ impl ListHeader {
self
}
pub fn state(mut self, state: InteractionState) -> Self {
self.state = state;
pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
self.meta = meta;
self
}
@ -74,34 +82,36 @@ impl ListHeader {
}
}
fn label_color(&self) -> LabelColor {
match self.state {
InteractionState::Disabled => LabelColor::Disabled,
_ => Default::default(),
}
}
fn icon_color(&self) -> IconColor {
match self.state {
InteractionState::Disabled => IconColor::Disabled,
_ => Default::default(),
}
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let is_toggleable = self.toggleable != Toggleable::NotToggleable;
let is_toggled = self.toggleable.is_toggled();
let disclosure_control = self.disclosure_control();
let meta = match self.meta {
Some(ListHeaderMeta::Tools(icons)) => div().child(
h_stack()
.gap_2()
.items_center()
.children(icons.into_iter().map(|i| {
IconElement::new(i)
.color(IconColor::Muted)
.size(IconSize::Small)
})),
),
Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label),
None => div(),
};
h_stack()
.flex_1()
.w_full()
.bg(cx.theme().colors().surface)
.when(self.state == InteractionState::Focused, |this| {
this.border()
.border_color(cx.theme().colors().border_focused)
})
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
// .border_color(cx.theme().colors().border_focused)
// })
.relative()
.child(
div()
@ -109,22 +119,28 @@ impl ListHeader {
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
.flex()
.flex_1()
.items_center()
.justify_between()
.w_full()
.gap_1()
.items_center()
.child(
div()
.flex()
h_stack()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(IconColor::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(LabelColor::Muted)),
.child(
div()
.flex()
.gap_1()
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
.color(IconColor::Muted)
.size(IconSize::Small)
}))
.child(Label::new(self.label.clone()).color(LabelColor::Muted)),
)
.child(disclosure_control),
)
.child(disclosure_control),
.child(meta),
)
}
}
@ -473,42 +489,63 @@ impl<V: 'static> ListDetailsEntry<V> {
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
let settings = user_settings(cx);
let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
true => (
cx.theme().colors().ghost_element,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
false => (
cx.theme().colors().element,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
};
let (item_bg, item_bg_hover, item_bg_active) = (
cx.theme().colors().ghost_element,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
);
let label_color = match self.seen {
true => LabelColor::Muted,
false => LabelColor::Default,
};
v_stack()
div()
.relative()
.group("")
.bg(item_bg)
.px_1()
.py_1_5()
.px_2()
.py_1p5()
.w_full()
.line_height(relative(1.2))
.child(Label::new(self.label.clone()).color(label_color))
.children(
self.meta
.map(|meta| Label::new(meta).color(LabelColor::Muted)),
)
.z_index(1)
.when(!self.seen, |this| {
this.child(
div()
.absolute()
.left(px(3.0))
.top_3()
.rounded_full()
.border_2()
.border_color(cx.theme().colors().surface)
.w(px(9.0))
.h(px(9.0))
.z_index(2)
.bg(cx.theme().status().info),
)
})
.child(
h_stack()
v_stack()
.w_full()
.line_height(relative(1.2))
.gap_1()
.justify_end()
.children(self.actions.unwrap_or_default()),
.child(
div()
.w_5()
.h_5()
.rounded_full()
.bg(cx.theme().colors().icon_accent),
)
.child(Label::new(self.label.clone()).color(label_color))
.children(
self.meta
.map(|meta| Label::new(meta).color(LabelColor::Muted)),
)
.child(
h_stack()
.gap_1()
.justify_end()
.children(self.actions.unwrap_or_default()),
),
)
}
}
@ -522,7 +559,7 @@ impl ListSeparator {
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div().h_px().w_full().bg(cx.theme().colors().border)
div().h_px().w_full().bg(cx.theme().colors().border_variant)
}
}
@ -564,14 +601,15 @@ impl<V: 'static> List<V> {
let is_toggled = Toggleable::is_toggled(&self.toggleable);
let list_content = match (self.items.is_empty(), is_toggled) {
(_, false) => div(),
(false, _) => div().children(self.items),
(true, _) => {
(true, false) => div(),
(true, true) => {
div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
}
};
v_stack()
.w_full()
.py_1()
.children(self.header.map(|header| header.toggleable(self.toggleable)))
.child(list_content)

View file

@ -1,5 +1,10 @@
use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
use crate::{List, ListHeader};
use crate::utils::naive_format_distance_from_now;
use crate::{
h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, Button, Icon,
IconButton, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
UnreadIndicator,
};
use crate::{ClickHandler, ListHeader};
#[derive(Component)]
pub struct NotificationsPanel {
@ -16,31 +21,348 @@ impl NotificationsPanel {
.id(self.id.clone())
.flex()
.flex_col()
.w_full()
.h_full()
.size_full()
.bg(cx.theme().colors().surface)
.child(
div()
.id("header")
.w_full()
.flex()
.flex_col()
ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
Icon::AtSign,
Icon::BellOff,
Icon::MailOpen,
]))),
)
.child(ListSeparator::new())
.child(
v_stack()
.id("notifications-panel-scroll-view")
.py_1()
.overflow_y_scroll()
.flex_1()
.child(
List::new(static_new_notification_items())
.header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
.toggle(ToggleState::Toggled),
div()
.mx_2()
.p_1()
// TODO: Add cursor style
// .cursor(Cursor::IBeam)
.bg(cx.theme().colors().element)
.border()
.border_color(cx.theme().colors().border_variant)
.child(
Label::new("Search...")
.color(LabelColor::Placeholder)
.line_height_style(LineHeightStyle::UILabel),
),
)
.child(v_stack().px_1().children(static_new_notification_items_2())),
)
}
}
pub enum ButtonOrIconButton<V: 'static> {
Button(Button<V>),
IconButton(IconButton<V>),
}
impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
fn from(value: Button<V>) -> Self {
Self::Button(value)
}
}
impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
fn from(value: IconButton<V>) -> Self {
Self::IconButton(value)
}
}
pub struct NotificationAction<V: 'static> {
button: ButtonOrIconButton<V>,
tooltip: SharedString,
/// Shows after action is chosen
///
/// For example, if the action is "Accept" the taken message could be:
///
/// - `(None,"Accepted")` - "Accepted"
///
/// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
taken_message: (Option<Icon>, SharedString),
}
impl<V: 'static> NotificationAction<V> {
pub fn new(
button: impl Into<ButtonOrIconButton<V>>,
tooltip: impl Into<SharedString>,
(icon, taken_message): (Option<Icon>, impl Into<SharedString>),
) -> Self {
Self {
button: button.into(),
tooltip: tooltip.into(),
taken_message: (icon, taken_message.into()),
}
}
}
pub enum ActorOrIcon {
Actor(PublicActor),
Icon(Icon),
}
pub struct NotificationMeta<V: 'static> {
items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
}
struct NotificationHandlers<V: 'static> {
click: Option<ClickHandler<V>>,
}
impl<V: 'static> Default for NotificationHandlers<V> {
fn default() -> Self {
Self { click: None }
}
}
#[derive(Component)]
pub struct Notification<V: 'static> {
id: ElementId,
slot: ActorOrIcon,
message: SharedString,
date_received: NaiveDateTime,
meta: Option<NotificationMeta<V>>,
actions: Option<[NotificationAction<V>; 2]>,
unread: bool,
new: bool,
action_taken: Option<NotificationAction<V>>,
handlers: NotificationHandlers<V>,
}
impl<V> Notification<V> {
fn new(
id: ElementId,
message: SharedString,
date_received: NaiveDateTime,
slot: ActorOrIcon,
click_action: Option<ClickHandler<V>>,
) -> Self {
let handlers = if click_action.is_some() {
NotificationHandlers {
click: click_action,
}
} else {
NotificationHandlers::default()
};
Self {
id,
date_received,
message,
meta: None,
slot,
actions: None,
unread: true,
new: false,
action_taken: None,
handlers,
}
}
/// Creates a new notification with an actor slot.
///
/// Requires a click action.
pub fn new_actor_message(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicActor,
click_action: ClickHandler<V>,
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Actor(actor),
Some(click_action),
)
}
/// Creates a new notification with an icon slot.
///
/// Requires a click action.
pub fn new_icon_message(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
icon: Icon,
click_action: ClickHandler<V>,
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Icon(icon),
Some(click_action),
)
}
/// Creates a new notification with an actor slot
/// and a Call To Action row.
///
/// Cannot take a click action due to required actions.
pub fn new_actor_with_actions(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
actor: PublicActor,
actions: [NotificationAction<V>; 2],
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Actor(actor),
None,
)
.actions(actions)
}
/// Creates a new notification with an icon slot
/// and a Call To Action row.
///
/// Cannot take a click action due to required actions.
pub fn new_icon_with_actions(
id: impl Into<ElementId>,
message: impl Into<SharedString>,
date_received: NaiveDateTime,
icon: Icon,
actions: [NotificationAction<V>; 2],
) -> Self {
Self::new(
id.into(),
message.into(),
date_received,
ActorOrIcon::Icon(icon),
None,
)
.actions(actions)
}
fn on_click(mut self, handler: ClickHandler<V>) -> Self {
self.handlers.click = Some(handler);
self
}
pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
self.actions = Some(actions);
self
}
pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
self.meta = Some(meta);
self
}
fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
if let Some(meta) = &self.meta {
h_stack().children(
meta.items
.iter()
.map(|(icon, text, _)| {
let mut meta_el = div();
if let Some(icon) = icon {
meta_el = meta_el.child(IconElement::new(icon.clone()));
}
meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
})
.collect::<Vec<_>>(),
)
} else {
div()
}
}
fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
match &self.slot {
ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
}
}
fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.relative()
.id(self.id.clone())
.p_1()
.flex()
.flex_col()
.w_full()
.children(
Some(
div()
.absolute()
.left(px(3.0))
.top_3()
.z_index(2)
.child(UnreadIndicator::new()),
)
.filter(|_| self.unread),
)
.child(
v_stack()
.z_index(1)
.gap_1()
.w_full()
.child(
h_stack()
.w_full()
.gap_2()
.child(self.render_slot(cx))
.child(div().flex_1().child(Label::new(self.message.clone()))),
)
.child(
List::new(static_read_notification_items())
.header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
.empty_message("No new notifications")
.toggle(ToggleState::Toggled),
h_stack()
.justify_between()
.child(
h_stack()
.gap_1()
.child(
Label::new(naive_format_distance_from_now(
self.date_received,
true,
true,
))
.color(LabelColor::Muted),
)
.child(self.render_meta_items(cx)),
)
.child(match (self.actions, self.action_taken) {
// Show nothing
(None, _) => div(),
// Show the taken_message
(Some(_), Some(action_taken)) => h_stack()
.children(action_taken.taken_message.0.map(|icon| {
IconElement::new(icon).color(crate::IconColor::Muted)
}))
.child(
Label::new(action_taken.taken_message.1.clone())
.color(LabelColor::Muted),
),
// Show the actions
(Some(actions), None) => {
h_stack().children(actions.map(|action| match action.button {
ButtonOrIconButton::Button(button) => {
Component::render(button)
}
ButtonOrIconButton::IconButton(icon_button) => {
Component::render(icon_button)
}
}))
}
}),
),
)
}
}
use chrono::NaiveDateTime;
use gpui2::{px, Styled};
#[cfg(feature = "stories")]
pub use stories::*;

View file

@ -1,6 +1,6 @@
use crate::prelude::*;
use crate::{Icon, IconColor, IconElement, Label, LabelColor};
use gpui2::{black, red, Div, ElementId, Render, View, VisualContext};
use gpui2::{red, Div, ElementId, Render, View, VisualContext};
#[derive(Component, Clone)]
pub struct Tab {
@ -108,13 +108,13 @@ impl Tab {
let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
true => (
cx.theme().colors().ghost_element,
false => (
cx.theme().colors().tab_inactive,
cx.theme().colors().ghost_element_hover,
cx.theme().colors().ghost_element_active,
),
false => (
cx.theme().colors().element,
true => (
cx.theme().colors().tab_active,
cx.theme().colors().element_hover,
cx.theme().colors().element_active,
),
@ -127,7 +127,7 @@ impl Tab {
div()
.id(self.id.clone())
.on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
.drag_over::<TabDragState>(|d| d.bg(black()))
.drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().element_drop_target))
.on_drop(|_view, state: View<TabDragState>, cx| {
eprintln!("{:?}", state.read(cx));
})
@ -144,7 +144,7 @@ impl Tab {
.px_1()
.flex()
.items_center()
.gap_1()
.gap_1p5()
.children(has_fs_conflict.then(|| {
IconElement::new(Icon::ExclamationTriangle)
.size(crate::IconSize::Small)

View file

@ -27,6 +27,7 @@ impl TabBar {
let (can_navigate_back, can_navigate_forward) = self.can_navigate;
div()
.group("tab_bar")
.id(self.id.clone())
.w_full()
.flex()
@ -34,6 +35,7 @@ impl TabBar {
// Left Side
.child(
div()
.relative()
.px_1()
.flex()
.flex_none()
@ -41,6 +43,7 @@ impl TabBar {
// Nav Buttons
.child(
div()
.right_0()
.flex()
.items_center()
.gap_px()
@ -67,10 +70,15 @@ impl TabBar {
// Right Side
.child(
div()
// We only use absolute here since we don't
// have opacity or `hidden()` yet
.absolute()
.neg_top_7()
.px_1()
.flex()
.flex_none()
.gap_2()
.group_hover("tab_bar", |this| this.top_0())
// Nav Buttons
.child(
div()

View file

@ -2,6 +2,7 @@ mod avatar;
mod button;
mod details;
mod icon;
mod indicator;
mod input;
mod label;
mod player;
@ -12,6 +13,7 @@ pub use avatar::*;
pub use button::*;
pub use details::*;
pub use icon::*;
pub use indicator::*;
pub use input::*;
pub use label::*;
pub use player::*;

View file

@ -26,23 +26,21 @@ pub enum IconColor {
impl IconColor {
pub fn color(self, cx: &WindowContext) -> Hsla {
let theme_colors = cx.theme().colors();
match self {
IconColor::Default => theme_colors.icon,
IconColor::Muted => theme_colors.icon_muted,
IconColor::Disabled => theme_colors.icon_disabled,
IconColor::Placeholder => theme_colors.icon_placeholder,
IconColor::Accent => theme_colors.icon_accent,
IconColor::Error => gpui2::red(),
IconColor::Warning => gpui2::red(),
IconColor::Success => gpui2::red(),
IconColor::Info => gpui2::red(),
IconColor::Default => cx.theme().colors().icon,
IconColor::Muted => cx.theme().colors().icon_muted,
IconColor::Disabled => cx.theme().colors().icon_disabled,
IconColor::Placeholder => cx.theme().colors().icon_placeholder,
IconColor::Accent => cx.theme().colors().icon_accent,
IconColor::Error => cx.theme().status().error,
IconColor::Warning => cx.theme().status().warning,
IconColor::Success => cx.theme().status().success,
IconColor::Info => cx.theme().status().info,
}
}
}
#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
pub enum Icon {
Ai,
ArrowLeft,
@ -51,6 +49,7 @@ pub enum Icon {
AudioOff,
AudioOn,
Bolt,
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
@ -69,7 +68,6 @@ pub enum Icon {
Folder,
FolderOpen,
FolderX,
#[default]
Hash,
InlayHint,
MagicWand,
@ -91,6 +89,11 @@ pub enum Icon {
XCircle,
Copilot,
Envelope,
Bell,
BellOff,
BellRing,
MailOpen,
AtSign,
}
impl Icon {
@ -103,6 +106,7 @@ impl Icon {
Icon::AudioOff => "icons/speaker-off.svg",
Icon::AudioOn => "icons/speaker-loud.svg",
Icon::Bolt => "icons/bolt.svg",
Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
Icon::ChevronRight => "icons/chevron_right.svg",
@ -142,6 +146,11 @@ impl Icon {
Icon::XCircle => "icons/error.svg",
Icon::Copilot => "icons/copilot.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::Bell => "icons/bell.svg",
Icon::BellOff => "icons/bell-off.svg",
Icon::BellRing => "icons/bell-ring.svg",
Icon::MailOpen => "icons/mail-open.svg",
Icon::AtSign => "icons/at-sign.svg",
}
}
}

View file

@ -0,0 +1,23 @@
use gpui2::px;
use crate::prelude::*;
#[derive(Component)]
pub struct UnreadIndicator;
impl UnreadIndicator {
pub fn new() -> Self {
Self
}
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
div()
.rounded_full()
.border_2()
.border_color(cx.theme().colors().surface)
.w(px(9.0))
.h(px(9.0))
.z_index(2)
.bg(cx.theme().status().info)
}
}

View file

@ -21,11 +21,11 @@ impl LabelColor {
match self {
Self::Default => cx.theme().colors().text,
Self::Muted => cx.theme().colors().text_muted,
Self::Created => gpui2::red(),
Self::Modified => gpui2::red(),
Self::Deleted => gpui2::red(),
Self::Created => cx.theme().status().created,
Self::Modified => cx.theme().status().modified,
Self::Deleted => cx.theme().status().deleted,
Self::Disabled => cx.theme().colors().text_disabled,
Self::Hidden => gpui2::red(),
Self::Hidden => cx.theme().status().hidden,
Self::Placeholder => cx.theme().colors().text_placeholder,
Self::Accent => cx.theme().colors().text_accent,
}
@ -79,8 +79,7 @@ impl Label {
this.relative().child(
div()
.absolute()
.top_px()
.my_auto()
.top_1_2()
.w_full()
.h_px()
.bg(LabelColor::Hidden.hsla(cx)),

View file

@ -23,6 +23,7 @@ mod elevation;
pub mod prelude;
pub mod settings;
mod static_data;
pub mod utils;
pub use components::*;
pub use elements::*;

View file

@ -10,6 +10,24 @@ pub use theme2::ActiveTheme;
use gpui2::Hsla;
use strum::EnumIter;
/// Represents a person with a Zed account's public profile.
/// All data in this struct should be considered public.
pub struct PublicActor {
pub username: SharedString,
pub avatar: SharedString,
pub is_contact: bool,
}
impl PublicActor {
pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
Self {
username: username.into(),
avatar: avatar.into(),
is_contact: false,
}
}
}
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum FileSystemStatus {
#[default]

View file

@ -1,17 +1,20 @@
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use chrono::DateTime;
use gpui2::{AppContext, ViewContext};
use rand::Rng;
use theme2::ActiveTheme;
use crate::{
Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem,
Livestream, MicStatus, ModifierKeys, PaletteItem, Player, PlayerCallStatus,
PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListSubHeader,
Livestream, MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus,
PlayerWithCallStatus, PublicActor, ScreenShareStatus, Symbol, Tab, ToggleState, VideoStatus,
};
use crate::{HighlightedText, ListDetailsEntry};
use crate::{ListItem, NotificationAction};
pub fn static_tabs_example() -> Vec<Tab> {
vec![
@ -325,27 +328,227 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
]
}
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
vec![
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
.meta("4 people in stream."),
ListDetailsEntry::new("nathansobo accepted your contact request."),
Notification::new_icon_message(
"notif-1",
"You were mentioned in a note.",
DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z")
.unwrap()
.naive_local(),
Icon::AtSign,
Arc::new(|_, _| {}),
),
Notification::new_actor_with_actions(
"notif-2",
"as-cii sent you a contact request.",
DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
"Decline Request",
(Some(Icon::XCircle), "Declined"),
),
NotificationAction::new(
Button::new("Accept").variant(crate::ButtonVariant::Filled),
"Accept Request",
(Some(Icon::Check), "Accepted"),
),
],
),
Notification::new_icon_message(
"notif-3",
"You were mentioned #design.",
DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
.unwrap()
.naive_local(),
Icon::MessageBubbles,
Arc::new(|_, _| {}),
),
Notification::new_actor_with_actions(
"notif-4",
"as-cii sent you a contact request.",
DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
"Decline Request",
(Some(Icon::XCircle), "Declined"),
),
NotificationAction::new(
Button::new("Accept").variant(crate::ButtonVariant::Filled),
"Accept Request",
(Some(Icon::Check), "Accepted"),
),
],
),
Notification::new_icon_message(
"notif-5",
"You were mentioned in a note.",
DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z")
.unwrap()
.naive_local(),
Icon::AtSign,
Arc::new(|_, _| {}),
),
Notification::new_actor_with_actions(
"notif-6",
"as-cii sent you a contact request.",
DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
"Decline Request",
(Some(Icon::XCircle), "Declined"),
),
NotificationAction::new(
Button::new("Accept").variant(crate::ButtonVariant::Filled),
"Accept Request",
(Some(Icon::Check), "Accepted"),
),
],
),
Notification::new_icon_message(
"notif-7",
"You were mentioned in a note.",
DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z")
.unwrap()
.naive_local(),
Icon::AtSign,
Arc::new(|_, _| {}),
),
Notification::new_actor_with_actions(
"notif-8",
"as-cii sent you a contact request.",
DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
"Decline Request",
(Some(Icon::XCircle), "Declined"),
),
NotificationAction::new(
Button::new("Accept").variant(crate::ButtonVariant::Filled),
"Accept Request",
(Some(Icon::Check), "Accepted"),
),
],
),
Notification::new_icon_message(
"notif-9",
"You were mentioned in a note.",
DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z")
.unwrap()
.naive_local(),
Icon::AtSign,
Arc::new(|_, _| {}),
),
Notification::new_actor_with_actions(
"notif-10",
"as-cii sent you a contact request.",
DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
.unwrap()
.naive_local(),
PublicActor::new("as-cii", "http://github.com/as-cii.png?s=50"),
[
NotificationAction::new(
Button::new("Decline"),
"Decline Request",
(Some(Icon::XCircle), "Declined"),
),
NotificationAction::new(
Button::new("Accept").variant(crate::ButtonVariant::Filled),
"Accept Request",
(Some(Icon::Check), "Accepted"),
),
],
),
]
.into_iter()
.map(From::from)
.collect()
}
pub fn static_read_notification_items<V: 'static>() -> Vec<ListItem<V>> {
pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<V>> {
vec![
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
]),
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
.seen(true)
.meta("This stream has ended."),
ListDetailsEntry::new("as-cii accepted your contact request."),
ListItem::Header(ListSubHeader::new("New")),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to join a stream in #design.")
.meta("4 people in stream."),
),
ListItem::Details(ListDetailsEntry::new(
"nathansobo accepted your contact request.",
)),
ListItem::Header(ListSubHeader::new("Earlier")),
ListItem::Details(
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
]),
),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"as-cii accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
),
ListItem::Details(ListDetailsEntry::new(
"osiewicz accepted your contact request.",
)),
ListItem::Details(ListDetailsEntry::new(
"ConradIrwin accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"nathansobo accepted your contact request.",
)),
ListItem::Header(ListSubHeader::new("Earlier")),
ListItem::Details(
ListDetailsEntry::new("mikaylamaki added you as a contact.").actions(vec![
Button::new("Decline"),
Button::new("Accept").variant(crate::ButtonVariant::Filled),
]),
),
ListItem::Details(
ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
.seen(true)
.meta("This stream has ended."),
),
ListItem::Details(ListDetailsEntry::new(
"as-cii accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("You were added as an admin on the #gpui2 channel.").seen(true),
),
ListItem::Details(ListDetailsEntry::new(
"osiewicz accepted your contact request.",
)),
ListItem::Details(ListDetailsEntry::new(
"ConradIrwin accepted your contact request.",
)),
ListItem::Details(
ListDetailsEntry::new("nathansobo invited you to a stream in #gpui2.")
.seen(true)
.meta("This stream has ended."),
),
]
.into_iter()
.map(From::from)

3
crates/ui2/src/utils.rs Normal file
View file

@ -0,0 +1,3 @@
mod format_distance;
pub use format_distance::*;

View file

@ -0,0 +1,173 @@
use chrono::NaiveDateTime;
fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
let duration = date.signed_duration_since(base_date);
-duration.num_seconds()
}
fn distance_string(distance: i64, include_seconds: bool, add_suffix: bool) -> String {
let suffix = if distance < 0 { " from now" } else { " ago" };
let d = distance.abs();
let minutes = d / 60;
let hours = d / 3600;
let days = d / 86400;
let months = d / 2592000;
let years = d / 31536000;
let string = if d < 5 && include_seconds {
"less than 5 seconds".to_string()
} else if d < 10 && include_seconds {
"less than 10 seconds".to_string()
} else if d < 20 && include_seconds {
"less than 20 seconds".to_string()
} else if d < 40 && include_seconds {
"half a minute".to_string()
} else if d < 60 && include_seconds {
"less than a minute".to_string()
} else if d < 90 && include_seconds {
"1 minute".to_string()
} else if d < 30 {
"less than a minute".to_string()
} else if d < 90 {
"1 minute".to_string()
} else if d < 2700 {
format!("{} minutes", minutes)
} else if d < 5400 {
"about 1 hour".to_string()
} else if d < 86400 {
format!("about {} hours", hours)
} else if d < 172800 {
"1 day".to_string()
} else if d < 2592000 {
format!("{} days", days)
} else if d < 5184000 {
"about 1 month".to_string()
} else if d < 7776000 {
"about 2 months".to_string()
} else if d < 31540000 {
format!("{} months", months)
} else if d < 39425000 {
"about 1 year".to_string()
} else if d < 55195000 {
"over 1 year".to_string()
} else if d < 63080000 {
"almost 2 years".to_string()
} else {
let years = d / 31536000;
let remaining_months = (d % 31536000) / 2592000;
if remaining_months < 3 {
format!("about {} years", years)
} else if remaining_months < 9 {
format!("over {} years", years)
} else {
format!("almost {} years", years + 1)
}
};
if add_suffix {
return format!("{}{}", string, suffix);
} else {
string
}
}
pub fn naive_format_distance(
date: NaiveDateTime,
base_date: NaiveDateTime,
include_seconds: bool,
add_suffix: bool,
) -> String {
let distance = distance_in_seconds(date, base_date);
distance_string(distance, include_seconds, add_suffix)
}
pub fn naive_format_distance_from_now(
datetime: NaiveDateTime,
include_seconds: bool,
add_suffix: bool,
) -> String {
let now = chrono::offset::Local::now().naive_local();
naive_format_distance(datetime, now, include_seconds, add_suffix)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDateTime;
#[test]
fn test_naive_format_distance() {
let date =
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
let base_date =
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
assert_eq!(
"about 2 hours",
naive_format_distance(date, base_date, false, false)
);
}
#[test]
fn test_naive_format_distance_with_suffix() {
let date =
NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date");
let base_date =
NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date");
assert_eq!(
"about 2 hours from now",
naive_format_distance(date, base_date, false, true)
);
}
#[test]
fn test_naive_format_distance_from_now() {
let date = NaiveDateTime::parse_from_str("1969-07-20T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ")
.expect("Invalid NaiveDateTime for date");
assert_eq!(
"over 54 years ago",
naive_format_distance_from_now(date, false, true)
);
}
#[test]
fn test_naive_format_distance_string() {
assert_eq!(distance_string(3, false, false), "less than a minute");
assert_eq!(distance_string(7, false, false), "less than a minute");
assert_eq!(distance_string(13, false, false), "less than a minute");
assert_eq!(distance_string(21, false, false), "less than a minute");
assert_eq!(distance_string(45, false, false), "1 minute");
assert_eq!(distance_string(61, false, false), "1 minute");
assert_eq!(distance_string(1920, false, false), "32 minutes");
assert_eq!(distance_string(3902, false, false), "about 1 hour");
assert_eq!(distance_string(18002, false, false), "about 5 hours");
assert_eq!(distance_string(86470, false, false), "1 day");
assert_eq!(distance_string(345880, false, false), "4 days");
assert_eq!(distance_string(2764800, false, false), "about 1 month");
assert_eq!(distance_string(5184000, false, false), "about 2 months");
assert_eq!(distance_string(10368000, false, false), "4 months");
assert_eq!(distance_string(34694000, false, false), "about 1 year");
assert_eq!(distance_string(47310000, false, false), "over 1 year");
assert_eq!(distance_string(61503000, false, false), "almost 2 years");
assert_eq!(distance_string(160854000, false, false), "about 5 years");
assert_eq!(distance_string(236550000, false, false), "over 7 years");
assert_eq!(distance_string(249166000, false, false), "almost 8 years");
}
#[test]
fn test_naive_format_distance_string_include_seconds() {
assert_eq!(distance_string(3, true, false), "less than 5 seconds");
assert_eq!(distance_string(7, true, false), "less than 10 seconds");
assert_eq!(distance_string(13, true, false), "less than 20 seconds");
assert_eq!(distance_string(21, true, false), "half a minute");
assert_eq!(distance_string(45, true, false), "less than a minute");
assert_eq!(distance_string(61, true, false), "1 minute");
}
}