Allow ListHeader to take a `meta`

Nate Butler created

Change summary

assets/icons/at-sign.svg                         |  1 
assets/icons/bell-off.svg                        |  1 
assets/icons/bell-ring.svg                       |  1 
assets/icons/bell.svg                            |  4 
assets/icons/mail-open.svg                       |  1 
crates/ui2/src/components/list.rs                | 84 ++++++++++-------
crates/ui2/src/components/notifications_panel.rs | 20 ++-
crates/ui2/src/elements/icon.rs                  | 13 ++
crates/ui2/src/static_data.rs                    | 49 +++++----
9 files changed, 104 insertions(+), 70 deletions(-)

Detailed changes

assets/icons/at-sign.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-at-sign"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/></svg>

assets/icons/bell-off.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-off"><path d="M8.7 3A6 6 0 0 1 18 8a21.3 21.3 0 0 0 .6 5"/><path d="M17 17H3s3-2 3-9a4.67 4.67 0 0 1 .3-1.7"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="m2 2 20 20"/></svg>

assets/icons/bell-ring.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-ring"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/><path d="M4 2C2.8 3.7 2 5.7 2 8"/><path d="M22 8c0-2.3-.8-4.3-2-6"/></svg>

assets/icons/bell.svg 🔗

@@ -1,8 +1 @@
-<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
-  <path
-    fill-rule="evenodd"
-    clip-rule="evenodd"

assets/icons/mail-open.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail-open"><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0l8 6Z"/><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10"/></svg>

crates/ui2/src/components/list.rs 🔗

@@ -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,8 +37,8 @@ impl ListHeader {
         Self {
             label: label.into(),
             left_icon: None,
+            meta: None,
             variant: ListItemVariant::default(),
-            state: InteractionState::default(),
             toggleable: Toggleable::Toggleable(ToggleState::Toggled),
         }
     }
@@ -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,37 @@ 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 +120,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),
             )
     }
 }
@@ -593,6 +610,7 @@ impl<V: 'static> List<V> {
         };
 
         v_stack()
+            .w_full()
             .py_1()
             .children(self.header.map(|header| header.toggleable(self.toggleable)))
             .child(list_content)

crates/ui2/src/components/notifications_panel.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{prelude::*, static_new_notification_items, static_read_notification_items};
+use crate::{prelude::*, static_new_notification_items, Icon, ListHeaderMeta};
 use crate::{List, ListHeader};
 
 #[derive(Component)]
@@ -28,14 +28,16 @@ impl NotificationsPanel {
                     .overflow_y_scroll()
                     .child(
                         List::new(static_new_notification_items())
-                            .header(ListHeader::new("NEW").toggle(ToggleState::Toggled))
-                            .toggle(ToggleState::Toggled),
-                    )
-                    .child(
-                        List::new(static_read_notification_items())
-                            .header(ListHeader::new("EARLIER").toggle(ToggleState::Toggled))
-                            .empty_message("No new notifications")
-                            .toggle(ToggleState::Toggled),
+                            .toggle(ToggleState::Toggled)
+                            .header(
+                                ListHeader::new("Notifications")
+                                    .toggle(ToggleState::Toggled)
+                                    .meta(Some(ListHeaderMeta::Tools(vec![
+                                        Icon::AtSign,
+                                        Icon::BellOff,
+                                        Icon::MailOpen,
+                                    ]))),
+                            ),
                     ),
             )
     }

crates/ui2/src/elements/icon.rs 🔗

@@ -40,7 +40,7 @@ impl IconColor {
     }
 }
 
-#[derive(Debug, Default, PartialEq, Copy, Clone, EnumIter)]
+#[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 pub enum Icon {
     Ai,
     ArrowLeft,
@@ -67,7 +67,6 @@ pub enum Icon {
     Folder,
     FolderOpen,
     FolderX,
-    #[default]
     Hash,
     InlayHint,
     MagicWand,
@@ -89,6 +88,11 @@ pub enum Icon {
     XCircle,
     Copilot,
     Envelope,
+    Bell,
+    BellOff,
+    BellRing,
+    MailOpen,
+    AtSign,
 }
 
 impl Icon {
@@ -140,6 +144,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",
         }
     }
 }

crates/ui2/src/static_data.rs 🔗

@@ -7,9 +7,10 @@ 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, ListHeaderMeta,
+    ListItem, ListSubHeader, Livestream, MicStatus, ModifierKeys, PaletteItem, Player,
+    PlayerCallStatus, PlayerWithCallStatus, ScreenShareStatus, Symbol, Tab, ToggleState,
+    VideoStatus,
 };
 use crate::{HighlightedText, ListDetailsEntry};
 
@@ -327,25 +328,29 @@ pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
 
 pub fn static_new_notification_items<V: 'static>() -> Vec<ListItem<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."),
-    ]
-    .into_iter()
-    .map(From::from)
-    .collect()
-}
-
-pub fn static_read_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.",
+        )),
     ]
     .into_iter()
     .map(From::from)