Add channels panel with stubbed out information

Mikayla Maki and nate created

co-authored-by: nate <nate@zed.dev>

Change summary

Cargo.lock                                     |  26 +
Cargo.toml                                     |   1 
assets/settings/default.json                   |   6 
crates/channels/Cargo.toml                     |  38 ++
crates/channels/src/channels.rs                | 103 +++++
crates/channels/src/channels_panel.rs          | 369 ++++++++++++++++++++
crates/channels/src/channels_panel_settings.rs |  37 ++
crates/gpui/src/elements/flex.rs               |   7 
crates/project_panel/src/project_panel.rs      |  24 -
crates/theme/src/theme.rs                      |  80 ++++
crates/theme/src/ui.rs                         |  10 
crates/workspace/src/dock.rs                   |  28 +
crates/zed/Cargo.toml                          |   1 
crates/zed/src/main.rs                         |   1 
crates/zed/src/zed.rs                          |  17 
styles/src/style_tree/app.ts                   |   2 
styles/src/style_tree/channels_panel.ts        |  68 +++
17 files changed, 784 insertions(+), 34 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1254,6 +1254,31 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
 
+[[package]]
+name = "channels"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "collections",
+ "context_menu",
+ "db",
+ "editor",
+ "futures 0.3.28",
+ "gpui",
+ "log",
+ "menu",
+ "project",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings",
+ "theme",
+ "util",
+ "workspace",
+]
+
 [[package]]
 name = "chrono"
 version = "0.4.26"
@@ -9857,6 +9882,7 @@ dependencies = [
  "backtrace",
  "breadcrumbs",
  "call",
+ "channels",
  "chrono",
  "cli",
  "client",

Cargo.toml 🔗

@@ -6,6 +6,7 @@ members = [
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
+    "crates/channels",
     "crates/cli",
     "crates/client",
     "crates/clock",

assets/settings/default.json 🔗

@@ -122,6 +122,12 @@
     // Amount of indentation for nested items.
     "indent_size": 20
   },
+  "channels_panel": {
+    // Where to dock channels panel. Can be 'left' or 'right'.
+    "dock": "left",
+    // Default width of the channels panel.
+    "default_width": 240
+  },
   "assistant": {
     // Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
     "dock": "right",

crates/channels/Cargo.toml 🔗

@@ -0,0 +1,38 @@
+[package]
+name = "channels"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/channels.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+client = { path = "../client" }
+db = { path = "../db" }
+editor = { path = "../editor" }
+gpui = { path = "../gpui" }
+project = { path = "../project" }
+theme = { path = "../theme" }
+settings = { path = "../settings" }
+workspace = { path = "../workspace" }
+menu = { path = "../menu" }
+util = { path = "../util" }
+
+log.workspace = true
+anyhow.workspace = true
+schemars.workspace = true
+serde_json.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+futures.workspace = true
+
+[dev-dependencies]
+client = { path = "../client", features = ["test-support"] }
+editor = { path = "../editor", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+serde_json.workspace = true

crates/channels/src/channels.rs 🔗

@@ -0,0 +1,103 @@
+mod channels_panel;
+mod channels_panel_settings;
+
+pub use channels_panel::*;
+use gpui::{AppContext, Entity};
+
+use std::sync::Arc;
+
+use client::Client;
+
+pub fn init(client: Arc<Client>, cx: &mut AppContext) {
+    let channels = cx.add_model(|cx| Channels::new(client, cx));
+    cx.set_global(channels);
+    channels_panel::init(cx);
+}
+
+#[derive(Debug, Clone)]
+struct Channel {
+    id: u64,
+    name: String,
+    sub_channels: Vec<Channel>,
+    _room: Option<()>,
+}
+
+impl Channel {
+    fn new(id: u64, name: impl AsRef<str>, members: Vec<Channel>) -> Channel {
+        Channel {
+            name: name.as_ref().to_string(),
+            id,
+            sub_channels: members,
+            _room: None,
+        }
+    }
+
+    fn members(&self) -> &[Channel] {
+        &self.sub_channels
+    }
+
+    fn name(&self) -> &str {
+        &self.name
+    }
+}
+
+struct Channels {
+    channels: Vec<Channel>,
+}
+
+impl Channels {
+    fn channels(&self) -> Vec<Channel> {
+        self.channels.clone()
+    }
+}
+
+enum ChannelEvents {}
+
+impl Entity for Channels {
+    type Event = ChannelEvents;
+}
+
+impl Channels {
+    fn new(_client: Arc<Client>, _cx: &mut AppContext) -> Self {
+        //TODO: Subscribe to channel updates from the server
+        Channels {
+            channels: vec![Channel::new(
+                0,
+                "Zed Industries",
+                vec![
+                    Channel::new(1, "#general", Vec::new()),
+                    Channel::new(2, "#admiral", Vec::new()),
+                    Channel::new(3, "#livestreaming", vec![]),
+                    Channel::new(4, "#crdb", Vec::new()),
+                    Channel::new(5, "#crdb-1", Vec::new()),
+                    Channel::new(6, "#crdb-2", Vec::new()),
+                    Channel::new(7, "#crdb-3", vec![]),
+                    Channel::new(8, "#crdb-4", Vec::new()),
+                    Channel::new(9, "#crdb-1", Vec::new()),
+                    Channel::new(10, "#crdb-1", Vec::new()),
+                    Channel::new(11, "#crdb-1", Vec::new()),
+                    Channel::new(12, "#crdb-1", vec![]),
+                    Channel::new(13, "#crdb-1", Vec::new()),
+                    Channel::new(14, "#crdb-1", Vec::new()),
+                    Channel::new(15, "#crdb-1", Vec::new()),
+                    Channel::new(16, "#crdb-1", Vec::new()),
+                    Channel::new(17, "#crdb", vec![]),
+                ],
+            ),
+            Channel::new(
+                18,
+                "CRDB Consulting",
+                vec![
+                    Channel::new(19, "#crdb 😭", Vec::new()),
+                    Channel::new(20, "#crdb 😌", Vec::new()),
+                    Channel::new(21, "#crdb 🦀", vec![]),
+                    Channel::new(22, "#crdb 😤", Vec::new()),
+                    Channel::new(23, "#crdb 😤", Vec::new()),
+                    Channel::new(24, "#crdb 😤", Vec::new()),
+                    Channel::new(25, "#crdb 😤", vec![]),
+                    Channel::new(26, "#crdb 😤", Vec::new()),
+                ],
+            )],
+        }
+    }
+}

crates/channels/src/channels_panel.rs 🔗

@@ -0,0 +1,369 @@
+use std::sync::Arc;
+
+use crate::{
+    channels_panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings},
+    Channel, Channels,
+};
+use anyhow::Result;
+use collections::HashMap;
+use context_menu::ContextMenu;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    actions,
+    elements::{ChildView, Empty, Flex, Label, MouseEventHandler, ParentElement, Stack},
+    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::Fs;
+use serde_derive::{Deserialize, Serialize};
+use settings::SettingsStore;
+use theme::ChannelTreeStyle;
+use util::{ResultExt, TryFutureExt};
+use workspace::{
+    dock::{DockPosition, Panel},
+    Workspace,
+};
+
+actions!(channels, [ToggleFocus]);
+
+const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
+
+pub fn init(cx: &mut AppContext) {
+    settings::register::<ChannelsPanelSettings>(cx);
+}
+
+pub struct ChannelsPanel {
+    width: Option<f32>,
+    fs: Arc<dyn Fs>,
+    has_focus: bool,
+    pending_serialization: Task<Option<()>>,
+    channels: ModelHandle<Channels>,
+    context_menu: ViewHandle<ContextMenu>,
+    collapsed_channels: HashMap<u64, bool>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct SerializedChannelsPanel {
+    width: Option<f32>,
+    collapsed_channels: Option<HashMap<u64, bool>>,
+}
+
+#[derive(Debug)]
+pub enum Event {
+    DockPositionChanged,
+    Focus,
+}
+
+impl Entity for ChannelsPanel {
+    type Event = Event;
+}
+
+impl ChannelsPanel {
+    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+        cx.add_view(|cx| {
+            let view_id = cx.view_id();
+            let this = Self {
+                width: None,
+                has_focus: false,
+                fs: workspace.app_state().fs.clone(),
+                pending_serialization: Task::ready(None),
+                channels: cx.global::<ModelHandle<Channels>>().clone(),
+                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
+                collapsed_channels: HashMap::default(),
+            };
+
+            // Update the dock position when the setting changes.
+            let mut old_dock_position = this.position(cx);
+            cx.observe_global::<SettingsStore, _>(move |this: &mut ChannelsPanel, cx| {
+                let new_dock_position = this.position(cx);
+                if new_dock_position != old_dock_position {
+                    old_dock_position = new_dock_position;
+                    cx.emit(Event::DockPositionChanged);
+                }
+            })
+            .detach();
+
+            this
+        })
+    }
+
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
+            } else {
+                None
+            };
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = ChannelsPanel::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        panel.collapsed_channels =
+                            serialized_panel.collapsed_channels.unwrap_or_default();
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        let collapsed_channels = self.collapsed_channels.clone();
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        CHANNELS_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedChannelsPanel {
+                            width,
+                            collapsed_channels: Some(collapsed_channels),
+                        })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
+    fn render_channel(
+        &mut self,
+        depth: usize,
+        channel: &Channel,
+        style: &ChannelTreeStyle,
+        root: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> AnyElement<Self> {
+        let has_chilren = !channel.members().is_empty();
+
+        let sub_channel_details = has_chilren.then(|| {
+            let mut sub_channels = Flex::column();
+            let collapsed = self
+                .collapsed_channels
+                .get(&channel.id)
+                .copied()
+                .unwrap_or_default();
+            if !collapsed {
+                for sub_channel in channel.members() {
+                    sub_channels = sub_channels.with_child(self.render_channel(
+                        depth + 1,
+                        sub_channel,
+                        style,
+                        false,
+                        cx,
+                    ));
+                }
+            }
+            (sub_channels, collapsed)
+        });
+
+        let channel_id = channel.id;
+
+        enum ChannelCollapser {}
+        Flex::row()
+            .with_child(
+                Empty::new()
+                    .constrained()
+                    .with_width(depth as f32 * style.channel_indent),
+            )
+            .with_child(
+                Flex::column()
+                    .with_child(
+                        Flex::row()
+                            .with_child(
+                                sub_channel_details
+                                    .as_ref()
+                                    .map(|(_, expanded)| {
+                                        MouseEventHandler::<ChannelCollapser, _>::new(
+                                            channel.id as usize,
+                                            cx,
+                                            |state, _cx| {
+                                                let icon =
+                                                    style.channel_icon.style_for(!*expanded, state);
+                                                theme::ui::icon(icon)
+                                            },
+                                        )
+                                        .on_click(
+                                            gpui::platform::MouseButton::Left,
+                                            move |_, v, cx| {
+                                                let entry = v
+                                                    .collapsed_channels
+                                                    .entry(channel_id)
+                                                    .or_default();
+                                                *entry = !*entry;
+                                                v.serialize(cx);
+                                                cx.notify();
+                                            },
+                                        )
+                                        .into_any()
+                                    })
+                                    .unwrap_or_else(|| {
+                                        Empty::new()
+                                            .constrained()
+                                            .with_width(style.channel_icon.default_style().width())
+                                            .into_any()
+                                    }),
+                            )
+                            .with_child(
+                                Label::new(
+                                    channel.name().to_string(),
+                                    if root {
+                                        style.root_name.clone()
+                                    } else {
+                                        style.channel_name.clone()
+                                    },
+                                )
+                                .into_any(),
+                            ),
+                    )
+                    .with_children(sub_channel_details.map(|(elements, _)| elements)),
+            )
+            .into_any()
+    }
+}
+
+impl View for ChannelsPanel {
+    fn ui_name() -> &'static str {
+        "ChannelsPanel"
+    }
+
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+        if !self.has_focus {
+            self.has_focus = true;
+            cx.emit(Event::Focus);
+        }
+    }
+
+    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+        self.has_focus = false;
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+        let theme = theme::current(cx).clone();
+
+        let mut channels_column = Flex::column();
+        for channel in self.channels.read(cx).channels() {
+            channels_column = channels_column.with_child(self.render_channel(
+                0,
+                &channel,
+                &theme.channels_panel.channel_tree,
+                true,
+                cx,
+            ));
+        }
+
+        let spacing = theme.channels_panel.spacing;
+
+        enum ChannelsPanelScrollTag {}
+        Stack::new()
+            .with_child(
+                // Full panel column
+                Flex::column()
+                    .with_spacing(spacing)
+                    .with_child(
+                        // Channels section column
+                        Flex::column()
+                            .with_child(
+                                Flex::row().with_child(
+                                    Label::new(
+                                        "Active Channels",
+                                        theme.editor.invalid_information_diagnostic.message.clone(),
+                                    )
+                                    .into_any(),
+                                ),
+                            )
+                            // Channels list column
+                            .with_child(channels_column),
+                    )
+                    // TODO: Replace with spacing implementation
+                    .with_child(Empty::new().constrained().with_height(spacing))
+                    .with_child(
+                        Flex::column().with_child(
+                            Flex::row().with_child(
+                                Label::new(
+                                    "Contacts",
+                                    theme.editor.invalid_information_diagnostic.message.clone(),
+                                )
+                                .into_any(),
+                            ),
+                        ),
+                    )
+                    .scrollable::<ChannelsPanelScrollTag>(0, None, cx)
+                    .expanded(),
+            )
+            .with_child(ChildView::new(&self.context_menu, cx))
+            .into_any_named("channels panel")
+            .into_any()
+    }
+}
+
+impl Panel for ChannelsPanel {
+    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
+        match settings::get::<ChannelsPanelSettings>(cx).dock {
+            ChannelsPanelDockPosition::Left => DockPosition::Left,
+            ChannelsPanelDockPosition::Right => DockPosition::Right,
+        }
+    }
+
+    fn position_is_valid(&self, position: DockPosition) -> bool {
+        matches!(position, DockPosition::Left | DockPosition::Right)
+    }
+
+    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
+        settings::update_settings_file::<ChannelsPanelSettings>(
+            self.fs.clone(),
+            cx,
+            move |settings| {
+                let dock = match position {
+                    DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
+                    DockPosition::Right => ChannelsPanelDockPosition::Right,
+                };
+                settings.dock = Some(dock);
+            },
+        );
+    }
+
+    fn size(&self, cx: &gpui::WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+        self.width = Some(size);
+        self.serialize(cx);
+        cx.notify();
+    }
+
+    fn icon_path(&self) -> &'static str {
+        "icons/bolt_16.svg"
+    }
+
+    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
+        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
+    }
+
+    fn should_change_position_on_event(event: &Self::Event) -> bool {
+        matches!(event, Event::DockPositionChanged)
+    }
+
+    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
+        self.has_focus
+    }
+
+    fn is_focus_event(event: &Self::Event) -> bool {
+        matches!(event, Event::Focus)
+    }
+}

crates/channels/src/channels_panel_settings.rs 🔗

@@ -0,0 +1,37 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde_derive::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ChannelsPanelDockPosition {
+    Left,
+    Right,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ChannelsPanelSettings {
+    pub dock: ChannelsPanelDockPosition,
+    pub default_width: f32,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct ChannelsPanelSettingsContent {
+    pub dock: Option<ChannelsPanelDockPosition>,
+    pub default_width: Option<f32>,
+}
+
+impl Setting for ChannelsPanelSettings {
+    const KEY: Option<&'static str> = Some("channels_panel");
+
+    type FileContent = ChannelsPanelSettingsContent;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self> {
+        Self::load_via_json_merge(default_value, user_values)
+    }
+}

crates/gpui/src/elements/flex.rs 🔗

@@ -22,6 +22,7 @@ pub struct Flex<V: View> {
     children: Vec<AnyElement<V>>,
     scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
     child_alignment: f32,
+    spacing: f32,
 }
 
 impl<V: View> Flex<V> {
@@ -31,6 +32,7 @@ impl<V: View> Flex<V> {
             children: Default::default(),
             scroll_state: None,
             child_alignment: -1.,
+            spacing: 0.,
         }
     }
 
@@ -42,6 +44,11 @@ impl<V: View> Flex<V> {
         Self::new(Axis::Vertical)
     }
 
+    pub fn with_spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
     /// Render children centered relative to the cross-axis of the parent flex.
     ///
     /// If this is a flex row, children will be centered vertically. If this is a

crates/project_panel/src/project_panel.rs 🔗

@@ -1649,22 +1649,6 @@ impl workspace::dock::Panel for ProjectPanel {
         cx.notify();
     }
 
-    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn is_zoomed(&self, _: &WindowContext) -> bool {
-        false
-    }
-
-    fn set_zoomed(&mut self, _: bool, _: &mut ViewContext<Self>) {}
-
-    fn set_active(&mut self, _: bool, _: &mut ViewContext<Self>) {}
-
     fn icon_path(&self) -> &'static str {
         "icons/folder_tree_16.svg"
     }
@@ -1677,14 +1661,6 @@ impl workspace::dock::Panel for ProjectPanel {
         matches!(event, Event::DockPositionChanged)
     }
 
-    fn should_activate_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
-    fn should_close_on_event(_: &Self::Event) -> bool {
-        false
-    }
-
     fn has_focus(&self, _: &WindowContext) -> bool {
         self.has_focus
     }

crates/theme/src/theme.rs 🔗

@@ -49,6 +49,7 @@ pub struct Theme {
     pub copilot: Copilot,
     pub contact_finder: ContactFinder,
     pub project_panel: ProjectPanel,
+    pub channels_panel: ChanelsPanelStyle,
     pub command_palette: CommandPalette,
     pub picker: Picker,
     pub editor: Editor,
@@ -880,6 +881,16 @@ impl<T> Interactive<T> {
     }
 }
 
+impl<T> Toggleable<Interactive<T>> {
+    pub fn style_for(&self, active: bool, state: &mut MouseState) -> &T {
+        self.in_state(active).style_for(state)
+    }
+
+    pub fn default_style(&self) -> &T {
+        &self.inactive.default
+    }
+}
+
 impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive<T> {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -1045,6 +1056,75 @@ pub struct AssistantStyle {
     pub saved_conversation: SavedConversation,
 }
 
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct Contained<T> {
+    container: ContainerStyle,
+    contained: T,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct FlexStyle {
+    // Between item spacing
+    item_spacing: f32,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ChannelProjectStyle {
+    // TODO: Implement Contained Flex
+    // ContainerStyle + Spacing between elements
+    // Negative spacing overlaps elements instead of spacing them out
+    pub container: Contained<FlexStyle>,
+    pub host: ImageStyle,
+    pub title: ContainedText,
+    pub members: Contained<FlexStyle>,
+    pub member: ImageStyle
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ChanneltemStyle {
+    pub icon: IconStyle,
+    pub title: TextStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ChannelListStyle {
+    pub section_title: ContainedText,
+    pub channel: Toggleable<Contained<ChanneltemStyle>>,
+    pub project: ChannelProjectStyle
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ContactItemStyle {
+    pub container: Contained<FlexStyle>,
+    pub avatar: IconStyle,
+    pub name: TextStyle,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ContactsListStyle {
+    pub section_title: ContainedText,
+    pub contact: ContactItemStyle,
+}
+
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ChannelTreeStyle {
+    pub channel_indent: f32,
+    pub channel_name: TextStyle,
+    pub root_name: TextStyle,
+    pub channel_icon: Toggleable<Interactive<IconStyle>>,
+}
+
+#[derive(Clone, Deserialize, Default, JsonSchema)]
+pub struct ChanelsPanelStyle {
+    pub channel_tree: ChannelTreeStyle,
+    pub spacing: f32,
+    // TODO: Uncomment:
+    // pub container: ContainerStyle,
+    // pub channel_list: ChannelListStyle,
+    // pub contacts_list: ContactsListStyle
+}
+
 #[derive(Clone, Deserialize, Default, JsonSchema)]
 pub struct SavedConversation {
     pub container: Interactive<ContainerStyle>,

crates/theme/src/ui.rs 🔗

@@ -107,6 +107,16 @@ pub struct IconStyle {
     pub container: ContainerStyle,
 }
 
+impl IconStyle {
+    pub fn width(&self) -> f32 {
+        self.icon.dimensions.width
+            + self.container.padding.left
+            + self.container.padding.right
+            + self.container.margin.left
+            + self.container.margin.right
+    }
+}
+
 pub fn icon<V: View>(style: &IconStyle) -> Container<V> {
     svg(&style.icon).contained().with_style(style.container)
 }

crates/workspace/src/dock.rs 🔗

@@ -20,13 +20,27 @@ pub trait Panel: View {
         None
     }
     fn should_change_position_on_event(_: &Self::Event) -> bool;
-    fn should_zoom_in_on_event(_: &Self::Event) -> bool;
-    fn should_zoom_out_on_event(_: &Self::Event) -> bool;
-    fn is_zoomed(&self, cx: &WindowContext) -> bool;
-    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>);
-    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>);
-    fn should_activate_on_event(_: &Self::Event) -> bool;
-    fn should_close_on_event(_: &Self::Event) -> bool;
+    fn should_zoom_in_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn should_zoom_out_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn is_zoomed(&self, _cx: &WindowContext) -> bool {
+        false
+    }
+    fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {
+
+    }
+    fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {
+
+    }
+    fn should_activate_on_event(_: &Self::Event) -> bool {
+        false
+    }
+    fn should_close_on_event(_: &Self::Event) -> bool {
+        false
+    }
     fn has_focus(&self, cx: &WindowContext) -> bool;
     fn is_focus_event(_: &Self::Event) -> bool;
 }

crates/zed/Cargo.toml 🔗

@@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 call = { path = "../call" }
+channels = { path = "../channels" }
 cli = { path = "../cli" }
 collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }

crates/zed/src/main.rs 🔗

@@ -155,6 +155,7 @@ fn main() {
         outline::init(cx);
         project_symbols::init(cx);
         project_panel::init(Assets, cx);
+        channels::init(client.clone(), cx);
         diagnostics::init(cx);
         search::init(cx);
         semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);

crates/zed/src/zed.rs 🔗

@@ -9,6 +9,7 @@ use ai::AssistantPanel;
 use anyhow::Context;
 use assets::Assets;
 use breadcrumbs::Breadcrumbs;
+use channels::ChannelsPanel;
 pub use client;
 use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
 use collections::VecDeque;
@@ -221,6 +222,11 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
             workspace.toggle_panel_focus::<ProjectPanel>(cx);
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace, _: &channels::ToggleFocus, cx: &mut ViewContext<Workspace>| {
+            workspace.toggle_panel_focus::<channels::ChannelsPanel>(cx);
+        },
+    );
     cx.add_action(
         |workspace: &mut Workspace,
          _: &terminal_panel::ToggleFocus,
@@ -339,9 +345,13 @@ pub fn initialize_workspace(
         let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
         let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
         let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-        let (project_panel, terminal_panel, assistant_panel) =
-            futures::try_join!(project_panel, terminal_panel, assistant_panel)?;
-
+        let channels_panel = ChannelsPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel, assistant_panel, channels_panel) = futures::try_join!(
+            project_panel,
+            terminal_panel,
+            assistant_panel,
+            channels_panel
+        )?;
         workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
             workspace.add_panel_with_extra_event_handler(
@@ -359,6 +369,7 @@ pub fn initialize_workspace(
             );
             workspace.add_panel(terminal_panel, cx);
             workspace.add_panel(assistant_panel, cx);
+            workspace.add_panel(channels_panel, cx);
 
             if !was_deserialized
                 && workspace

styles/src/style_tree/app.ts 🔗

@@ -24,6 +24,7 @@ import { titlebar } from "./titlebar"
 import editor from "./editor"
 import feedback from "./feedback"
 import { useTheme } from "../common"
+import channels_panel from "./channels_panel"
 
 export default function app(): any {
     const theme = useTheme()
@@ -46,6 +47,7 @@ export default function app(): any {
         editor: editor(),
         project_diagnostics: project_diagnostics(),
         project_panel: project_panel(),
+        channels_panel: channels_panel(),
         contacts_popover: contacts_popover(),
         contact_finder: contact_finder(),
         contact_list: contact_list(),

styles/src/style_tree/channels_panel.ts 🔗

@@ -0,0 +1,68 @@
+// import { with_opacity } from "../theme/color"
+import {
+    //     Border,
+    //     TextStyle,
+    // background,
+    //     border,
+    foreground,
+    text,
+} from "./components"
+import { interactive, toggleable } from "../element"
+// import merge from "ts-deepmerge"
+import { useTheme } from "../theme"
+export default function channels_panel(): any {
+    const theme = useTheme()
+
+    // const { is_light } = theme
+
+    return {
+        spacing: 10,
+        channel_tree: {
+            channel_indent: 10,
+            channel_name: text(theme.middle, "sans", "variant", { size: "md" }),
+            root_name: text(theme.middle, "sans", "variant", { size: "lg", weight: "bold" }),
+            channel_icon: (() => {
+                const base_icon = (asset: any, color: any) => {
+                    return {
+                        icon: {
+                            color,
+                            asset,
+                            dimensions: {
+                                width: 12,
+                                height: 12,
+                            }
+                        },
+                        container: {
+                            corner_radius: 4,
+                            padding: {
+                                top: 4, bottom: 4, left: 4, right: 4
+                            },
+                            margin: {
+                                right: 4,
+                            },
+                        }
+                    }
+                }
+
+                return toggleable({
+                    state: {
+                        inactive: interactive({
+                            state: {
+                                default: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "variant")),
+                                hovered: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "hovered")),
+                                clicked: base_icon("icons/chevron_right_8.svg", foreground(theme.middle, "active")),
+                            },
+                        }),
+                        active: interactive({
+                            state: {
+                                default: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "variant")),
+                                hovered: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "hovered")),
+                                clicked: base_icon("icons/chevron_down_8.svg", foreground(theme.highest, "active")),
+                            },
+                        }),
+                    },
+                })
+            })(),
+        }
+    }
+}