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

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