Detailed changes
@@ -13,7 +13,8 @@ mod keybinding;
mod language_selector;
mod list;
mod multi_buffer;
-mod notification;
+mod notification_toast;
+mod notifications_panel;
mod palette;
mod panel;
mod panes;
@@ -46,7 +47,8 @@ pub use keybinding::*;
pub use language_selector::*;
pub use list::*;
pub use multi_buffer::*;
-pub use notification::*;
+pub use notification_toast::*;
+pub use notifications_panel::*;
pub use palette::*;
pub use panel::*;
pub use panes::*;
@@ -1,10 +1,13 @@
use std::marker::PhantomData;
-use gpui3::{div, Div};
+use gpui3::{div, relative, Div};
-use crate::prelude::*;
use crate::settings::user_settings;
-use crate::{h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, LabelColor};
+use crate::{
+ h_stack, v_stack, Avatar, ClickHandler, Icon, IconColor, IconElement, IconSize, Label,
+ LabelColor,
+};
+use crate::{prelude::*, Button};
#[derive(Clone, Copy, Default, Debug, PartialEq)]
pub enum ListItemVariant {
@@ -201,6 +204,7 @@ pub enum ListEntrySize {
#[derive(Element)]
pub enum ListItem<S: 'static + Send + Sync> {
Entry(ListEntry<S>),
+ Details(ListDetailsEntry<S>),
Separator(ListSeparator<S>),
Header(ListSubHeader<S>),
}
@@ -211,6 +215,12 @@ impl<S: 'static + Send + Sync> From<ListEntry<S>> for ListItem<S> {
}
}
+impl<S: 'static + Send + Sync> From<ListDetailsEntry<S>> for ListItem<S> {
+ fn from(entry: ListDetailsEntry<S>) -> Self {
+ Self::Details(entry)
+ }
+}
+
impl<S: 'static + Send + Sync> From<ListSeparator<S>> for ListItem<S> {
fn from(entry: ListSeparator<S>) -> Self {
Self::Separator(entry)
@@ -229,6 +239,7 @@ impl<S: 'static + Send + Sync> ListItem<S> {
ListItem::Entry(entry) => div().child(entry.render(view, cx)),
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
ListItem::Header(header) => div().child(header.render(view, cx)),
+ ListItem::Details(details) => div().child(details.render(view, cx)),
}
}
@@ -255,6 +266,7 @@ pub struct ListEntry<S: 'static + Send + Sync> {
size: ListEntrySize,
state: InteractionState,
toggle: Option<ToggleState>,
+ overflow: OverflowStyle,
}
impl<S: 'static + Send + Sync> ListEntry<S> {
@@ -270,6 +282,7 @@ impl<S: 'static + Send + Sync> ListEntry<S> {
// TODO: Should use Toggleable::NotToggleable
// or remove Toggleable::NotToggleable from the system
toggle: None,
+ overflow: OverflowStyle::Hidden,
}
}
pub fn set_variant(mut self, variant: ListItemVariant) -> Self {
@@ -416,6 +429,96 @@ impl<S: 'static + Send + Sync> ListEntry<S> {
}
}
+struct ListDetailsEntryHandlers<S: 'static + Send + Sync> {
+ click: Option<ClickHandler<S>>,
+}
+
+impl<S: 'static + Send + Sync> Default for ListDetailsEntryHandlers<S> {
+ fn default() -> Self {
+ Self { click: None }
+ }
+}
+
+#[derive(Element)]
+pub struct ListDetailsEntry<S: 'static + Send + Sync> {
+ label: SharedString,
+ meta: Option<SharedString>,
+ left_content: Option<LeftContent>,
+ handlers: ListDetailsEntryHandlers<S>,
+ actions: Option<Vec<Button<S>>>,
+ // TODO: make this more generic instead of
+ // specifically for notifications
+ seen: bool,
+}
+
+impl<S: 'static + Send + Sync> ListDetailsEntry<S> {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ meta: None,
+ left_content: None,
+ handlers: ListDetailsEntryHandlers::default(),
+ actions: None,
+ seen: false,
+ }
+ }
+
+ pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
+ self.meta = Some(meta.into());
+ self
+ }
+
+ pub fn seen(mut self, seen: bool) -> Self {
+ self.seen = seen;
+ self
+ }
+
+ pub fn on_click(mut self, handler: ClickHandler<S>) -> Self {
+ self.handlers.click = Some(handler);
+ self
+ }
+
+ pub fn actions(mut self, actions: Vec<Button<S>>) -> Self {
+ self.actions = Some(actions);
+ self
+ }
+
+ fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+ let color = ThemeColor::new(cx);
+ let settings = user_settings(cx);
+
+ let (item_bg, item_bg_hover, item_bg_active) = match self.seen {
+ true => (
+ color.ghost_element,
+ color.ghost_element_hover,
+ color.ghost_element_active,
+ ),
+ false => (
+ color.filled_element,
+ color.filled_element_hover,
+ color.filled_element_active,
+ ),
+ };
+
+ let label_color = match self.seen {
+ true => LabelColor::Muted,
+ false => LabelColor::Default,
+ };
+
+ v_stack()
+ .relative()
+ .group("")
+ .bg(item_bg)
+ .p_1()
+ .w_full()
+ .line_height(relative(1.2))
+ .child(Label::new(self.label.clone()).color(label_color))
+ .when(self.meta.is_some(), |this| {
+ this.child(Label::new(self.meta.clone().unwrap()).color(LabelColor::Muted))
+ })
+ }
+}
+
#[derive(Clone, Element)]
pub struct ListSeparator<S: 'static + Send + Sync> {
state_type: PhantomData<S>,
@@ -1,90 +0,0 @@
-use std::marker::PhantomData;
-
-use gpui3::{Element, ParentElement, Styled, ViewContext};
-
-use crate::{
- h_stack, v_stack, Button, Icon, IconButton, IconElement, Label, ThemeColor, Toast, ToastOrigin,
-};
-
-/// Notification toasts are used to display a message
-/// that requires them to take action.
-///
-/// You must provide a primary action for the user to take.
-///
-/// To simply convey information, use a `StatusToast`.
-#[derive(Element)]
-pub struct NotificationToast<S: 'static + Send + Sync + Clone> {
- state_type: PhantomData<S>,
- left_icon: Option<Icon>,
- title: String,
- message: String,
- primary_action: Option<Button<S>>,
- secondary_action: Option<Button<S>>,
-}
-
-impl<S: 'static + Send + Sync + Clone> NotificationToast<S> {
- pub fn new(
- // TODO: use a `SharedString` here
- title: impl Into<String>,
- message: impl Into<String>,
- primary_action: Button<S>,
- ) -> Self {
- Self {
- state_type: PhantomData,
- left_icon: None,
- title: title.into(),
- message: message.into(),
- primary_action: Some(primary_action),
- secondary_action: None,
- }
- }
-
- pub fn left_icon(mut self, icon: Icon) -> Self {
- self.left_icon = Some(icon);
- self
- }
-
- pub fn secondary_action(mut self, action: Button<S>) -> Self {
- self.secondary_action = Some(action);
- self
- }
-
- fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
- let color = ThemeColor::new(cx);
-
- let notification = h_stack()
- .min_w_64()
- .max_w_96()
- .gap_1()
- .items_start()
- .p_1()
- .children(self.left_icon.map(|i| IconElement::new(i)))
- .child(
- v_stack()
- .flex_1()
- .w_full()
- .gap_1()
- .child(
- h_stack()
- .justify_between()
- .child(Label::new(self.title.clone()))
- .child(IconButton::new(Icon::Close).color(crate::IconColor::Muted)),
- )
- .child(
- v_stack()
- .overflow_hidden_x()
- .gap_1()
- .child(Label::new(self.message.clone()))
- .child(
- h_stack()
- .gap_1()
- .justify_end()
- .children(self.secondary_action.take())
- .children(self.primary_action.take()),
- ),
- ),
- );
-
- Toast::new(ToastOrigin::BottomRight).child(notification)
- }
-}
@@ -0,0 +1,48 @@
+use std::marker::PhantomData;
+
+use gpui3::rems;
+
+use crate::{h_stack, prelude::*, Icon};
+
+#[derive(Element)]
+pub struct NotificationToast<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ label: SharedString,
+ icon: Option<Icon>,
+}
+
+impl<S: 'static + Send + Sync + Clone> NotificationToast<S> {
+ pub fn new(label: SharedString) -> Self {
+ Self {
+ state_type: PhantomData,
+ label,
+ icon: None,
+ }
+ }
+
+ pub fn icon<I>(mut self, icon: I) -> Self
+ where
+ I: Into<Option<Icon>>,
+ {
+ self.icon = icon.into();
+ self
+ }
+
+ fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+ let color = ThemeColor::new(cx);
+
+ h_stack()
+ .z_index(5)
+ .absolute()
+ .top_1()
+ .right_1()
+ .w(rems(9999.))
+ .max_w_56()
+ .py_1()
+ .px_1p5()
+ .rounded_lg()
+ .shadow_md()
+ .bg(color.elevated_surface)
+ .child(div().size_full().child(self.label.clone()))
+ }
+}
@@ -0,0 +1,80 @@
+use std::marker::PhantomData;
+
+use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
+use crate::{List, ListHeader};
+
+#[derive(Element)]
+pub struct NotificationsPanel<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> NotificationsPanel<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+ let color = ThemeColor::new(cx);
+
+ div()
+ .flex()
+ .flex_col()
+ .w_full()
+ .h_full()
+ .bg(color.surface)
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_col()
+ .overflow_y_scroll(ScrollState::default())
+ .child(
+ List::new(static_new_notification_items())
+ .header(ListHeader::new("NEW").set_toggle(ToggleState::Toggled))
+ .set_toggle(ToggleState::Toggled),
+ )
+ .child(
+ List::new(static_read_notification_items())
+ .header(ListHeader::new("EARLIER").set_toggle(ToggleState::Toggled))
+ .empty_message("No new notifications")
+ .set_toggle(ToggleState::Toggled),
+ ),
+ )
+ }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+ use crate::{Panel, Story};
+
+ use super::*;
+
+ #[derive(Element)]
+ pub struct NotificationsPanelStory<S: 'static + Send + Sync + Clone> {
+ state_type: PhantomData<S>,
+ }
+
+ impl<S: 'static + Send + Sync + Clone> NotificationsPanelStory<S> {
+ pub fn new() -> Self {
+ Self {
+ state_type: PhantomData,
+ }
+ }
+
+ fn render(
+ &mut self,
+ _view: &mut S,
+ cx: &mut ViewContext<S>,
+ ) -> impl Element<ViewState = S> {
+ Story::container(cx)
+ .child(Story::title_for::<_, NotificationsPanel<S>>(cx))
+ .child(Story::label(cx, "Default"))
+ .child(Panel::new(cx).child(NotificationsPanel::new()))
+ }
+ }
+}
@@ -3,7 +3,7 @@ use std::sync::Arc;
use chrono::DateTime;
use gpui3::{px, relative, rems, view, Context, Size, View};
-use crate::prelude::*;
+use crate::{prelude::*, NotificationToast, NotificationsPanel};
use crate::{
static_livestream, theme, user_settings_mut, v_stack, AssistantPanel, Button, ChatMessage,
ChatPanel, CollabPanel, EditorPane, FakeSettings, Label, LanguageSelector, Pane, PaneGroup,
@@ -249,6 +249,9 @@ impl Workspace {
)
.filter(|_| self.is_collab_panel_open()),
)
+ // .child(NotificationToast::new(
+ // "maxbrunsfeld has requested to add you as a contact.".into(),
+ // ))
.child(
v_stack()
.flex_1()
@@ -289,7 +292,7 @@ impl Workspace {
Some(
Panel::new(cx)
.side(PanelSide::Right)
- .child(div().w_96().h_full().child("Notifications")),
+ .child(NotificationsPanel::new()),
)
.filter(|_| self.is_notifications_panel_open()),
)
@@ -197,6 +197,31 @@ impl<S: 'static + Send + Sync> Button<S> {
}
}
+#[derive(Element)]
+pub struct ButtonGroup<S: 'static + Send + Sync> {
+ state_type: PhantomData<S>,
+ buttons: Vec<Button<S>>,
+}
+
+impl<S: 'static + Send + Sync> ButtonGroup<S> {
+ pub fn new(buttons: Vec<Button<S>>) -> Self {
+ Self {
+ state_type: PhantomData,
+ buttons,
+ }
+ }
+
+ fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+ let mut el = h_stack().text_size(ui_size(cx, 1.));
+
+ for button in &mut self.buttons {
+ el = el.child(button.render(_view, cx));
+ }
+
+ el
+ }
+}
+
#[cfg(feature = "stories")]
pub use stories::*;
@@ -1,12 +1,13 @@
use std::marker::PhantomData;
-use crate::prelude::*;
+use crate::{prelude::*, v_stack, ButtonGroup};
#[derive(Element)]
pub struct Details<S: 'static + Send + Sync> {
state_type: PhantomData<S>,
text: &'static str,
meta: Option<&'static str>,
+ actions: Option<ButtonGroup<S>>,
}
impl<S: 'static + Send + Sync> Details<S> {
@@ -15,6 +16,7 @@ impl<S: 'static + Send + Sync> Details<S> {
state_type: PhantomData,
text,
meta: None,
+ actions: None,
}
}
@@ -23,18 +25,22 @@ impl<S: 'static + Send + Sync> Details<S> {
self
}
+ pub fn actions(mut self, actions: ButtonGroup<S>) -> Self {
+ self.actions = Some(actions);
+ self
+ }
+
fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
let color = ThemeColor::new(cx);
- div()
- // .flex()
- // .w_full()
+ v_stack()
.p_1()
.gap_0p5()
.text_xs()
.text_color(color.text)
.child(self.text)
.children(self.meta.map(|m| m))
+ .children(self.actions.take().map(|a| a))
}
}
@@ -190,7 +190,7 @@ impl ThemeColor {
border_variant: theme.lowest.variant.default.border,
border_focused: theme.lowest.accent.default.border,
border_transparent: system_color.transparent,
- elevated_surface: theme.middle.base.default.background,
+ elevated_surface: theme.lowest.base.default.background,
surface: theme.middle.base.default.background,
background: theme.lowest.base.default.background,
filled_element: theme.lowest.base.default.background,
@@ -397,6 +397,12 @@ pub enum DisclosureControlStyle {
None,
}
+#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
+pub enum OverflowStyle {
+ Hidden,
+ Wrap,
+}
+
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
pub enum InteractionState {
#[default]
@@ -4,13 +4,13 @@ use std::str::FromStr;
use gpui3::WindowContext;
use rand::Rng;
-use crate::HighlightedText;
use crate::{
- Buffer, BufferRow, BufferRows, EditorPane, FileSystemStatus, GitStatus, HighlightedLine, Icon,
- Keybinding, Label, LabelColor, ListEntry, ListEntrySize, ListItem, Livestream, MicStatus,
- ModifierKeys, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus,
- Symbol, Tab, ThemeColor, ToggleState, VideoStatus,
+ 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, ThemeColor, ToggleState, VideoStatus,
};
+use crate::{HighlightedText, ListDetailsEntry};
pub fn static_tabs_example<S: 'static + Send + Sync + Clone>() -> Vec<Tab<S>> {
vec![
@@ -324,6 +324,34 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
]
}
+pub fn static_new_notification_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+ vec![
+ ListEntry::new(Label::new(
+ "maxdeviant invited you to join a stream in #design.",
+ ))
+ .set_left_icon(Icon::FileLock.into()),
+ ListEntry::new(Label::new("nathansobo accepted your contact request."))
+ .set_left_icon(Icon::FileToml.into()),
+ ]
+ .into_iter()
+ .map(From::from)
+ .collect()
+}
+
+pub fn static_read_notification_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
+ vec![
+ ListDetailsEntry::new("mikaylamaki added you as a contact.")
+ .actions(vec![Button::new("Decline"), Button::new("Accept")]),
+ ListDetailsEntry::new("maxdeviant invited you to a stream in #design.")
+ .seen(true)
+ .meta("This stream has ended."),
+ ListDetailsEntry::new("nathansobo accepted your contact request."),
+ ]
+ .into_iter()
+ .map(From::from)
+ .collect()
+}
+
pub fn static_project_panel_project_items<S: 'static + Send + Sync + Clone>() -> Vec<ListItem<S>> {
vec![
ListEntry::new(Label::new("zed"))