sidebar: Improve keyboard navigation (#50938)

Anthony Eid created

We know outline the focus sidebar entry similar to the project panel.
This allows users to see what they have selected vs active

Before you mark this PR as ready for review, make sure that you have:
- [ ] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A

Change summary

crates/sidebar/src/sidebar.rs              | 19 ++++++++++---
crates/ui/src/components/ai/thread_item.rs | 33 ++++++++++++++++++++++++
crates/ui/src/components/list/list_item.rs | 14 ++++++++++
3 files changed, 61 insertions(+), 5 deletions(-)

Detailed changes

crates/sidebar/src/sidebar.rs 🔗

@@ -677,9 +677,15 @@ impl Sidebar {
                 label,
                 workspace,
                 highlight_positions,
-            } => {
-                self.render_project_header(ix, path_list, label, workspace, highlight_positions, cx)
-            }
+            } => self.render_project_header(
+                ix,
+                path_list,
+                label,
+                workspace,
+                highlight_positions,
+                is_selected,
+                cx,
+            ),
             ListEntry::Thread {
                 session_info,
                 icon,
@@ -730,6 +736,7 @@ impl Sidebar {
         label: &SharedString,
         workspace: &Entity<Workspace>,
         highlight_positions: &[usize],
+        is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let id = SharedString::from(format!("project-header-{}", ix));
@@ -770,6 +777,7 @@ impl Sidebar {
         // TODO: if is_selected, draw a blue border around the item.
 
         ListItem::new(id)
+            .selection_outlined(is_selected)
             .group_name(&group)
             .toggle_state(is_active_workspace)
             .child(
@@ -1092,7 +1100,7 @@ impl Sidebar {
         status: AgentThreadStatus,
         workspace: &Entity<Workspace>,
         highlight_positions: &[usize],
-        _is_selected: bool,
+        is_selected: bool,
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let has_notification = self.contents.is_thread_notified(&session_info.session_id);
@@ -1114,6 +1122,7 @@ impl Sidebar {
             .status(status)
             .notified(has_notification)
             .selected(self.focused_thread.as_ref() == Some(&session_info.session_id))
+            .outlined(is_selected)
             .on_click(cx.listener(move |this, _, window, cx| {
                 this.selection = None;
                 this.activate_thread(session_info.clone(), &workspace, window, cx);
@@ -1159,7 +1168,7 @@ impl Sidebar {
         let count = format!("({})", remaining_count);
 
         ListItem::new(id)
-            .toggle_state(is_selected)
+            .selection_outlined(is_selected)
             .child(
                 h_flex()
                     .px_1()

crates/ui/src/components/ai/thread_item.rs 🔗

@@ -24,6 +24,7 @@ pub struct ThreadItem {
     notified: bool,
     status: AgentThreadStatus,
     selected: bool,
+    outlined: bool,
     hovered: bool,
     added: Option<usize>,
     removed: Option<usize>,
@@ -47,6 +48,7 @@ impl ThreadItem {
             notified: false,
             status: AgentThreadStatus::default(),
             selected: false,
+            outlined: false,
             hovered: false,
             added: None,
             removed: None,
@@ -90,6 +92,11 @@ impl ThreadItem {
         self
     }
 
+    pub fn outlined(mut self, outlined: bool) -> Self {
+        self.outlined = outlined;
+        self
+    }
+
     pub fn added(mut self, added: usize) -> Self {
         self.added = Some(added);
         self
@@ -221,6 +228,9 @@ impl RenderOnce for ThreadItem {
                 }
             })
             .when(self.selected, |s| s.bg(clr.element_active))
+            .border_1()
+            .border_color(gpui::transparent_black())
+            .when(self.outlined, |s| s.border_color(clr.panel_focused_border))
             .hover(|s| s.bg(clr.element_hover))
             .on_hover(self.on_hover)
             .child(
@@ -409,6 +419,29 @@ impl Component for ThreadItem {
                     )
                     .into_any_element(),
             ),
+            single_example(
+                "Outlined Item (Keyboard Selection)",
+                container()
+                    .child(
+                        ThreadItem::new("ti-7", "Implement keyboard navigation")
+                            .icon(IconName::AiClaude)
+                            .timestamp("4:00 PM")
+                            .outlined(true),
+                    )
+                    .into_any_element(),
+            ),
+            single_example(
+                "Selected + Outlined",
+                container()
+                    .child(
+                        ThreadItem::new("ti-8", "Active and keyboard-focused thread")
+                            .icon(IconName::AiGemini)
+                            .timestamp("5:00 PM")
+                            .selected(true)
+                            .outlined(true),
+                    )
+                    .into_any_element(),
+            ),
         ];
 
         Some(

crates/ui/src/components/list/list_item.rs 🔗

@@ -42,6 +42,7 @@ pub struct ListItem {
     selectable: bool,
     always_show_disclosure_icon: bool,
     outlined: bool,
+    selection_outlined: Option<bool>,
     rounded: bool,
     overflow_x: bool,
     focused: Option<bool>,
@@ -71,6 +72,7 @@ impl ListItem {
             selectable: true,
             always_show_disclosure_icon: false,
             outlined: false,
+            selection_outlined: None,
             rounded: false,
             overflow_x: false,
             focused: None,
@@ -171,6 +173,11 @@ impl ListItem {
         self
     }
 
+    pub fn selection_outlined(mut self, outlined: bool) -> Self {
+        self.selection_outlined = Some(outlined);
+        self
+    }
+
     pub fn rounded(mut self) -> Self {
         self.rounded = true;
         self
@@ -241,6 +248,13 @@ impl RenderOnce for ListItem {
                     })
             })
             .when(self.rounded, |this| this.rounded_sm())
+            .when_some(self.selection_outlined, |this, outlined| {
+                this.border_1()
+                    .border_color(gpui::transparent_black())
+                    .when(outlined, |this| {
+                        this.border_color(cx.theme().colors().panel_focused_border)
+                    })
+            })
             .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover))
             .child(
                 h_flex()