Render other tab icons in the start slot (#14683)

Marshall Bowers created

This PR reworks the rendering for tab icons to allow us to render all of
the tab icons—not just file icons—in the tab's start slot.

The `Item` trait now has a separate `tab_icon` method that can be used
to indicate what icon should be shown for the tab.

Release Notes:

- N/A

Change summary

Cargo.lock                                           |  2 
crates/editor/Cargo.toml                             |  1 
crates/editor/src/items.rs                           | 15 ++++++
crates/markdown_preview/src/markdown_preview_view.rs | 35 ++++++--------
crates/search/src/project_search.rs                  | 30 +++++------
crates/terminal_view/src/terminal_view.rs            | 14 ++--
crates/workspace/Cargo.toml                          |  1 
crates/workspace/src/item.rs                         | 12 ++++
crates/workspace/src/pane.rs                         | 24 ++------
crates/workspace/src/shared_screen.rs                | 26 ++++------
10 files changed, 80 insertions(+), 80 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3601,6 +3601,7 @@ dependencies = [
  "db",
  "emojis",
  "env_logger",
+ "file_icons",
  "futures 0.3.28",
  "fuzzy",
  "git",
@@ -13336,7 +13337,6 @@ dependencies = [
  "derive_more",
  "dev_server_projects",
  "env_logger",
- "file_icons",
  "fs",
  "futures 0.3.28",
  "gpui",

crates/editor/Cargo.toml 🔗

@@ -37,6 +37,7 @@ collections.workspace = true
 convert_case = "0.6.0"
 db.workspace = true
 emojis.workspace = true
+file_icons.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 git.workspace = true

crates/editor/src/items.rs 🔗

@@ -5,6 +5,7 @@ use crate::{
 };
 use anyhow::{anyhow, Context as _, Result};
 use collections::HashSet;
+use file_icons::FileIcons;
 use futures::future::try_join_all;
 use git::repository::GitFileStatus;
 use gpui::{
@@ -590,6 +591,20 @@ impl Item for Editor {
         Some(path.to_string_lossy().to_string().into())
     }
 
+    fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
+        ItemSettings::get_global(cx)
+            .file_icons
+            .then(|| {
+                self.buffer
+                    .read(cx)
+                    .as_singleton()
+                    .and_then(|buffer| buffer.read(cx).project_path(cx))
+                    .and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
+            })
+            .flatten()
+            .map(|icon| Icon::from_path(icon))
+    }
+
     fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
         let label_color = if ItemSettings::get_global(cx).git_status {
             self.buffer()

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -452,27 +452,22 @@ impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
 impl Item for MarkdownPreviewView {
     type Event = PreviewEvent;
 
+    fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
+        Some(Icon::new(IconName::FileDoc))
+    }
+
     fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement {
-        h_flex()
-            .gap_2()
-            .child(Icon::new(IconName::FileDoc).color(if params.selected {
-                Color::Default
-            } else {
-                Color::Muted
-            }))
-            .child(
-                Label::new(if let Some(description) = &self.tab_description {
-                    description.clone().into()
-                } else {
-                    self.fallback_tab_description.clone()
-                })
-                .color(if params.selected {
-                    Color::Default
-                } else {
-                    Color::Muted
-                }),
-            )
-            .into_any()
+        Label::new(if let Some(description) = &self.tab_description {
+            description.clone().into()
+        } else {
+            self.fallback_tab_description.clone()
+        })
+        .color(if params.selected {
+            Color::Default
+        } else {
+            Color::Muted
+        })
+        .into_any_element()
     }
 
     fn telemetry_event_text(&self) -> Option<&'static str> {

crates/search/src/project_search.rs 🔗

@@ -12,11 +12,11 @@ use editor::{
     MAX_TAB_TITLE_LEN,
 };
 use gpui::{
-    actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
-    EventEmitter, FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement,
-    IntoElement, KeyContext, Model, ModelContext, ParentElement, Point, Render, SharedString,
-    Styled, Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext,
-    WeakModel, WhiteSpace, WindowContext,
+    actions, div, Action, AnyElement, AnyView, AppContext, Context as _, EntityId, EventEmitter,
+    FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement, IntoElement,
+    KeyContext, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled,
+    Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel,
+    WhiteSpace, WindowContext,
 };
 use menu::Confirm;
 use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
@@ -370,6 +370,10 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.deactivated(cx));
     }
 
+    fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
+        Some(Icon::new(IconName::MagnifyingGlass))
+    }
+
     fn tab_content(&self, params: TabContentParams, cx: &WindowContext<'_>) -> AnyElement {
         let last_query: Option<SharedString> = self
             .model
@@ -384,21 +388,13 @@ impl Item for ProjectSearchView {
         let tab_name = last_query
             .filter(|query| !query.is_empty())
             .unwrap_or_else(|| "Project Search".into());
-        h_flex()
-            .gap_2()
-            .child(
-                Icon::new(IconName::MagnifyingGlass).color(if params.selected {
-                    Color::Default
-                } else {
-                    Color::Muted
-                }),
-            )
-            .child(Label::new(tab_name).color(if params.selected {
+        Label::new(tab_name)
+            .color(if params.selected {
                 Color::Default
             } else {
                 Color::Muted
-            }))
-            .into_any()
+            })
+            .into_any_element()
     }
 
     fn telemetry_event_text(&self) -> Option<&'static str> {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -943,13 +943,13 @@ impl Item for TerminalView {
         let terminal = self.terminal().read(cx);
         let title = terminal.title(true);
 
-        let (icon, icon_color, rerun_btn) = match terminal.task() {
+        let (icon, icon_color, rerun_button) = match terminal.task() {
             Some(terminal_task) => match &terminal_task.status {
                 TaskStatus::Unknown => (IconName::ExclamationTriangle, Color::Warning, None),
                 TaskStatus::Running => (IconName::Play, Color::Disabled, None),
                 TaskStatus::Completed { success } => {
                     let task_id = terminal_task.id.clone();
-                    let rerun_btn = IconButton::new("rerun-icon", IconName::Rerun)
+                    let rerun_button = IconButton::new("rerun-icon", IconName::Rerun)
                         .icon_size(IconSize::Small)
                         .size(ButtonSize::Compact)
                         .icon_color(Color::Default)
@@ -963,9 +963,9 @@ impl Item for TerminalView {
                         });
 
                     if *success {
-                        (IconName::Check, Color::Success, Some(rerun_btn))
+                        (IconName::Check, Color::Success, Some(rerun_button))
                     } else {
-                        (IconName::XCircle, Color::Error, Some(rerun_btn))
+                        (IconName::XCircle, Color::Error, Some(rerun_button))
                     }
                 }
             },
@@ -980,17 +980,17 @@ impl Item for TerminalView {
                     .group("term-tab-icon")
                     .child(
                         div()
-                            .when(rerun_btn.is_some(), |this| {
+                            .when(rerun_button.is_some(), |this| {
                                 this.hover(|style| style.invisible().w_0())
                             })
                             .child(Icon::new(icon).color(icon_color)),
                     )
-                    .when_some(rerun_btn, |this, rerun_btn| {
+                    .when_some(rerun_button, |this, rerun_button| {
                         this.child(
                             div()
                                 .absolute()
                                 .visible_on_hover("term-tab-icon")
-                                .child(rerun_btn),
+                                .child(rerun_button),
                         )
                     }),
             )

crates/workspace/Cargo.toml 🔗

@@ -36,7 +36,6 @@ clock.workspace = true
 collections.workspace = true
 db.workspace = true
 derive_more.workspace = true
-file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true

crates/workspace/src/item.rs 🔗

@@ -31,7 +31,7 @@ use std::{
     time::Duration,
 };
 use theme::Theme;
-use ui::Element as _;
+use ui::{Element as _, Icon};
 use util::ResultExt;
 
 pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
@@ -147,6 +147,11 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
     fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement {
         gpui::Empty.into_any()
     }
+
+    fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
+        None
+    }
+
     fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
 
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
@@ -330,6 +335,7 @@ pub trait ItemHandle: 'static + Send {
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
     fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
     fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
+    fn tab_icon(&self, cx: &WindowContext) -> Option<Icon>;
     fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>;
     fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
     fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
@@ -441,6 +447,10 @@ impl<T: Item> ItemHandle for View<T> {
         self.read(cx).tab_content(params, cx)
     }
 
+    fn tab_icon(&self, cx: &WindowContext) -> Option<Icon> {
+        self.read(cx).tab_icon(cx)
+    }
+
     fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
         self.read(cx).tab_content(
             TabContentParams {

crates/workspace/src/pane.rs 🔗

@@ -10,7 +10,6 @@ use crate::{
 };
 use anyhow::Result;
 use collections::{BTreeSet, HashMap, HashSet, VecDeque};
-use file_icons::FileIcons;
 use futures::{stream::FuturesUnordered, StreamExt};
 use gpui::{
     actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
@@ -1590,6 +1589,7 @@ impl Pane {
             },
             cx,
         );
+        let icon = item.tab_icon(cx);
         let close_side = &ItemSettings::get_global(cx).close_position;
         let indicator = render_item_indicator(item.boxed_clone(), cx);
         let item_id = item.item_id();
@@ -1597,14 +1597,6 @@ impl Pane {
         let is_last_item = ix == self.items.len() - 1;
         let position_relative_to_active_item = ix.cmp(&self.active_item_index);
 
-        let file_icon = ItemSettings::get_global(cx)
-            .file_icons
-            .then(|| {
-                item.project_path(cx)
-                    .and_then(|path| FileIcons::get_icon(path.path.as_ref(), cx))
-            })
-            .flatten();
-
         let tab = Tab::new(ix)
             .position(if is_first_item {
                 TabPosition::First
@@ -1675,14 +1667,12 @@ impl Pane {
             })
             .map(|tab| match indicator {
                 Some(indicator) => tab.start_slot(indicator),
-                None => tab.start_slot::<Icon>(file_icon.map(|icon| {
-                    Icon::from_path(icon.to_string())
-                        .size(IconSize::XSmall)
-                        .color(if is_active {
-                            Color::Default
-                        } else {
-                            Color::Muted
-                        })
+                None => tab.start_slot::<Icon>(icon.map(|icon| {
+                    icon.size(IconSize::XSmall).color(if is_active {
+                        Color::Default
+                    } else {
+                        Color::Muted
+                    })
                 })),
             })
             .end_slot(

crates/workspace/src/shared_screen.rs 🔗

@@ -7,12 +7,12 @@ use call::participant::{Frame, RemoteVideoTrack};
 use client::{proto::PeerId, User};
 use futures::StreamExt;
 use gpui::{
-    div, img, AppContext, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    div, img, AppContext, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
     ParentElement, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
     WindowContext,
 };
 use std::sync::{Arc, Weak};
-use ui::{h_flex, prelude::*, Icon, IconName, Label};
+use ui::{prelude::*, Icon, IconName, Label};
 
 pub enum Event {
     Close,
@@ -93,24 +93,18 @@ impl Item for SharedScreen {
         }
     }
 
+    fn tab_icon(&self, _cx: &WindowContext) -> Option<Icon> {
+        Some(Icon::new(IconName::Screen))
+    }
+
     fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> gpui::AnyElement {
-        h_flex()
-            .gap_1()
-            .child(Icon::new(IconName::Screen).color(if params.selected {
+        Label::new(format!("{}'s screen", self.user.github_login))
+            .color(if params.selected {
                 Color::Default
             } else {
                 Color::Muted
-            }))
-            .child(
-                Label::new(format!("{}'s screen", self.user.github_login)).color(
-                    if params.selected {
-                        Color::Default
-                    } else {
-                        Color::Muted
-                    },
-                ),
-            )
-            .into_any()
+            })
+            .into_any_element()
     }
 
     fn telemetry_event_text(&self) -> Option<&'static str> {