From 32028fbbb13f817aeb6193f2b28767510c06871c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 19 Oct 2023 20:04:21 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint=20=E2=80=93=20Notifications=20Panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ui2/src/components.rs | 6 +- crates/ui2/src/components/list.rs | 109 +++++++++++++++++- crates/ui2/src/components/notification.rs | 90 --------------- .../ui2/src/components/notification_toast.rs | 48 ++++++++ .../ui2/src/components/notifications_panel.rs | 80 +++++++++++++ crates/ui2/src/components/workspace.rs | 7 +- crates/ui2/src/elements/button.rs | 25 ++++ crates/ui2/src/elements/details.rs | 14 ++- crates/ui2/src/prelude.rs | 8 +- crates/ui2/src/static_data.rs | 38 +++++- 10 files changed, 318 insertions(+), 107 deletions(-) delete mode 100644 crates/ui2/src/components/notification.rs create mode 100644 crates/ui2/src/components/notification_toast.rs create mode 100644 crates/ui2/src/components/notifications_panel.rs diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index cbe5a2daa6..1e749f6934 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -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::*; diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index ab0c390110..f17480714b 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -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 { Entry(ListEntry), + Details(ListDetailsEntry), Separator(ListSeparator), Header(ListSubHeader), } @@ -211,6 +215,12 @@ impl From> for ListItem { } } +impl From> for ListItem { + fn from(entry: ListDetailsEntry) -> Self { + Self::Details(entry) + } +} + impl From> for ListItem { fn from(entry: ListSeparator) -> Self { Self::Separator(entry) @@ -229,6 +239,7 @@ impl ListItem { 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 { size: ListEntrySize, state: InteractionState, toggle: Option, + overflow: OverflowStyle, } impl ListEntry { @@ -270,6 +282,7 @@ impl ListEntry { // 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 ListEntry { } } +struct ListDetailsEntryHandlers { + click: Option>, +} + +impl Default for ListDetailsEntryHandlers { + fn default() -> Self { + Self { click: None } + } +} + +#[derive(Element)] +pub struct ListDetailsEntry { + label: SharedString, + meta: Option, + left_content: Option, + handlers: ListDetailsEntryHandlers, + actions: Option>>, + // TODO: make this more generic instead of + // specifically for notifications + seen: bool, +} + +impl ListDetailsEntry { + pub fn new(label: impl Into) -> 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) -> 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) -> Self { + self.handlers.click = Some(handler); + self + } + + pub fn actions(mut self, actions: Vec>) -> Self { + self.actions = Some(actions); + self + } + + fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + 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 { state_type: PhantomData, diff --git a/crates/ui2/src/components/notification.rs b/crates/ui2/src/components/notification.rs deleted file mode 100644 index 724571496f..0000000000 --- a/crates/ui2/src/components/notification.rs +++ /dev/null @@ -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 { - state_type: PhantomData, - left_icon: Option, - title: String, - message: String, - primary_action: Option>, - secondary_action: Option>, -} - -impl NotificationToast { - pub fn new( - // TODO: use a `SharedString` here - title: impl Into, - message: impl Into, - primary_action: Button, - ) -> 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) -> Self { - self.secondary_action = Some(action); - self - } - - fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { - 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) - } -} diff --git a/crates/ui2/src/components/notification_toast.rs b/crates/ui2/src/components/notification_toast.rs new file mode 100644 index 0000000000..351eed7e16 --- /dev/null +++ b/crates/ui2/src/components/notification_toast.rs @@ -0,0 +1,48 @@ +use std::marker::PhantomData; + +use gpui3::rems; + +use crate::{h_stack, prelude::*, Icon}; + +#[derive(Element)] +pub struct NotificationToast { + state_type: PhantomData, + label: SharedString, + icon: Option, +} + +impl NotificationToast { + pub fn new(label: SharedString) -> Self { + Self { + state_type: PhantomData, + label, + icon: None, + } + } + + pub fn icon(mut self, icon: I) -> Self + where + I: Into>, + { + self.icon = icon.into(); + self + } + + fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + 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())) + } +} diff --git a/crates/ui2/src/components/notifications_panel.rs b/crates/ui2/src/components/notifications_panel.rs new file mode 100644 index 0000000000..c502a20992 --- /dev/null +++ b/crates/ui2/src/components/notifications_panel.rs @@ -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 { + state_type: PhantomData, +} + +impl NotificationsPanel { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + 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 { + state_type: PhantomData, + } + + impl NotificationsPanelStory { + pub fn new() -> Self { + Self { + state_type: PhantomData, + } + } + + fn render( + &mut self, + _view: &mut S, + cx: &mut ViewContext, + ) -> impl Element { + Story::container(cx) + .child(Story::title_for::<_, NotificationsPanel>(cx)) + .child(Story::label(cx, "Default")) + .child(Panel::new(cx).child(NotificationsPanel::new())) + } + } +} diff --git a/crates/ui2/src/components/workspace.rs b/crates/ui2/src/components/workspace.rs index 055f05125d..bedec9434a 100644 --- a/crates/ui2/src/components/workspace.rs +++ b/crates/ui2/src/components/workspace.rs @@ -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()), ) diff --git a/crates/ui2/src/elements/button.rs b/crates/ui2/src/elements/button.rs index 7e474fb5af..ace3ada0cd 100644 --- a/crates/ui2/src/elements/button.rs +++ b/crates/ui2/src/elements/button.rs @@ -197,6 +197,31 @@ impl Button { } } +#[derive(Element)] +pub struct ButtonGroup { + state_type: PhantomData, + buttons: Vec>, +} + +impl ButtonGroup { + pub fn new(buttons: Vec>) -> Self { + Self { + state_type: PhantomData, + buttons, + } + } + + fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { + 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::*; diff --git a/crates/ui2/src/elements/details.rs b/crates/ui2/src/elements/details.rs index 30aa696c3b..61b793480f 100644 --- a/crates/ui2/src/elements/details.rs +++ b/crates/ui2/src/elements/details.rs @@ -1,12 +1,13 @@ use std::marker::PhantomData; -use crate::prelude::*; +use crate::{prelude::*, v_stack, ButtonGroup}; #[derive(Element)] pub struct Details { state_type: PhantomData, text: &'static str, meta: Option<&'static str>, + actions: Option>, } impl Details { @@ -15,6 +16,7 @@ impl Details { state_type: PhantomData, text, meta: None, + actions: None, } } @@ -23,18 +25,22 @@ impl Details { self } + pub fn actions(mut self, actions: ButtonGroup) -> Self { + self.actions = Some(actions); + self + } + fn render(&mut self, _view: &mut S, cx: &mut ViewContext) -> impl Element { 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)) } } diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 456d6cefe1..23d9e216c1 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -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] diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 96d8357bc4..9cc7396248 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -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() -> Vec> { vec![ @@ -324,6 +324,34 @@ pub fn static_players_with_call_status() -> Vec { ] } +pub fn static_new_notification_items() -> Vec> { + 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() -> Vec> { + 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() -> Vec> { vec![ ListEntry::new(Label::new("zed"))