Merge remote-tracking branch 'origin/main' into gpui2-image-reborrow

This commit is contained in:
Nathan Sobo 2023-11-02 12:25:06 -06:00
commit 1dd20d4c0a
29 changed files with 1489 additions and 530 deletions

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

@ -98,16 +98,14 @@ impl<V: 'static> Panel<V> {
v_stack()
.id(self.id.clone())
.flex_initial()
.when(
self.current_side == PanelSide::Left || self.current_side == PanelSide::Right,
|this| this.h_full().w(current_size),
)
.when(self.current_side == PanelSide::Left, |this| this.border_r())
.when(self.current_side == PanelSide::Right, |this| {
this.border_l()
.map(|this| match self.current_side {
PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
PanelSide::Bottom => this,
})
.when(self.current_side == PanelSide::Bottom, |this| {
this.border_b().w_full().h(current_size)
.map(|this| match self.current_side {
PanelSide::Left => this.border_r(),
PanelSide::Right => this.border_l(),
PanelSide::Bottom => this.border_b().w_full().h(current_size),
})
.bg(cx.theme().colors().surface)
.border_color(cx.theme().colors().border)

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

@ -94,14 +94,13 @@ impl Input {
.active(|style| style.bg(input_active_bg))
.flex()
.items_center()
.child(
div()
.flex()
.items_center()
.text_sm()
.when(self.value.is_empty(), |this| this.child(placeholder_label))
.when(!self.value.is_empty(), |this| this.child(label)),
)
.child(div().flex().items_center().text_sm().map(|this| {
if self.value.is_empty() {
this.child(placeholder_label)
} else {
this.child(label)
}
}))
}
}

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,231 @@
use chrono::NaiveDateTime;
/// Calculates the distance in seconds between two NaiveDateTime objects.
/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative.
///
/// ## Arguments
///
/// * `date` - A NaiveDateTime object representing the date of interest
/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made
fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 {
let duration = date.signed_duration_since(base_date);
-duration.num_seconds()
}
/// Generates a string describing the time distance between two dates in a human-readable way.
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
}
}
/// Get the time difference between two dates into a relative human readable string.
///
/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
///
/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now.
///
/// # Arguments
///
/// * `date` - The NaiveDateTime to compare.
/// * `base_date` - The NaiveDateTime to compare against.
/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
///
/// # Example
///
/// ```rust
/// use chrono::DateTime;
/// use ui2::utils::naive_format_distance;
///
/// fn time_between_moon_landings() -> String {
/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local();
/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false))
/// }
/// ```
///
/// Output: `"There was about 3 years between the first and last crewed moon landings."`
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)
}
/// Get the time difference between a date and now as relative human readable string.
///
/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc.
///
/// # Arguments
///
/// * `datetime` - The NaiveDateTime to compare with the current time.
/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed
/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future
///
/// # Example
///
/// ```rust
/// use chrono::DateTime;
/// use ui2::utils::naive_format_distance_from_now;
///
/// fn time_since_first_moon_landing() -> String {
/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local();
/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false))
/// }
/// ```
///
/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.`
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");
}
}