collab_panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3
   4use crate::{
   5    channel_view::{self, ChannelView},
   6    chat_panel::ChatPanel,
   7    face_pile::FacePile,
   8    panel_settings, CollaborationPanelSettings,
   9};
  10use anyhow::Result;
  11use call::ActiveCall;
  12use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
  13use channel_modal::ChannelModal;
  14use client::{proto::PeerId, Client, Contact, User, UserStore};
  15use contact_finder::ContactFinder;
  16use context_menu::{ContextMenu, ContextMenuItem};
  17use db::kvp::KEY_VALUE_STORE;
  18use drag_and_drop::{DragAndDrop, Draggable};
  19use editor::{Cancel, Editor};
  20use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
  21use futures::StreamExt;
  22use fuzzy::{match_strings, StringMatchCandidate};
  23use gpui::{
  24    actions,
  25    elements::{
  26        Canvas, ChildView, Component, ContainerStyle, Empty, Flex, Image, Label, List, ListOffset,
  27        ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement,
  28        SafeStylable, Stack, Svg,
  29    },
  30    fonts::TextStyle,
  31    geometry::{
  32        rect::RectF,
  33        vector::{vec2f, Vector2F},
  34    },
  35    impl_actions,
  36    platform::{CursorStyle, MouseButton, PromptLevel},
  37    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle,
  38    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  39};
  40use menu::{Confirm, SelectNext, SelectPrev};
  41use project::{Fs, Project};
  42use serde_derive::{Deserialize, Serialize};
  43use settings::SettingsStore;
  44use std::{borrow::Cow, hash::Hash, mem, sync::Arc};
  45use theme::{components::ComponentExt, IconButton, Interactive};
  46use util::{iife, ResultExt, TryFutureExt};
  47use workspace::{
  48    dock::{DockPosition, Panel},
  49    item::ItemHandle,
  50    Workspace,
  51};
  52
  53#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  54struct ToggleCollapse {
  55    location: ChannelPath,
  56}
  57
  58#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  59struct NewChannel {
  60    location: ChannelPath,
  61}
  62
  63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  64struct RenameChannel {
  65    location: ChannelPath,
  66}
  67
  68#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  69struct ToggleSelectedIx {
  70    ix: usize,
  71}
  72
  73#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  74struct RemoveChannel {
  75    channel_id: ChannelId,
  76}
  77
  78#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  79struct InviteMembers {
  80    channel_id: ChannelId,
  81}
  82
  83#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  84struct ManageMembers {
  85    channel_id: ChannelId,
  86}
  87
  88#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  89pub struct OpenChannelNotes {
  90    pub channel_id: ChannelId,
  91}
  92
  93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  94pub struct JoinChannelCall {
  95    pub channel_id: u64,
  96}
  97
  98#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
  99struct StartMoveChannelFor {
 100    channel_id: ChannelId,
 101    parent_id: Option<ChannelId>,
 102}
 103
 104#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 105struct StartLinkChannelFor {
 106    channel_id: ChannelId,
 107    parent_id: Option<ChannelId>,
 108}
 109
 110#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 111struct LinkChannel {
 112    to: ChannelId,
 113}
 114
 115#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 116struct MoveChannel {
 117    to: ChannelId,
 118}
 119
 120#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
 121struct UnlinkChannel {
 122    channel_id: ChannelId,
 123    parent_id: ChannelId,
 124}
 125
 126type DraggedChannel = (Channel, Option<ChannelId>);
 127
 128actions!(
 129    collab_panel,
 130    [
 131        ToggleFocus,
 132        Remove,
 133        Secondary,
 134        CollapseSelectedChannel,
 135        ExpandSelectedChannel,
 136        StartMoveChannel,
 137        StartLinkChannel,
 138        MoveOrLinkToSelected,
 139    ]
 140);
 141
 142impl_actions!(
 143    collab_panel,
 144    [
 145        RemoveChannel,
 146        NewChannel,
 147        InviteMembers,
 148        ManageMembers,
 149        RenameChannel,
 150        ToggleCollapse,
 151        OpenChannelNotes,
 152        JoinChannelCall,
 153        LinkChannel,
 154        StartMoveChannelFor,
 155        StartLinkChannelFor,
 156        MoveChannel,
 157        UnlinkChannel,
 158        ToggleSelectedIx
 159    ]
 160);
 161
 162#[derive(Debug, Copy, Clone, PartialEq, Eq)]
 163struct ChannelMoveClipboard {
 164    channel_id: ChannelId,
 165    parent_id: Option<ChannelId>,
 166    intent: ClipboardIntent,
 167}
 168
 169#[derive(Debug, Copy, Clone, PartialEq, Eq)]
 170enum ClipboardIntent {
 171    Move,
 172    Link,
 173}
 174
 175const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
 176
 177pub fn init(cx: &mut AppContext) {
 178    settings::register::<panel_settings::CollaborationPanelSettings>(cx);
 179    contact_finder::init(cx);
 180    channel_modal::init(cx);
 181    channel_view::init(cx);
 182
 183    cx.add_action(CollabPanel::cancel);
 184    cx.add_action(CollabPanel::select_next);
 185    cx.add_action(CollabPanel::select_prev);
 186    cx.add_action(CollabPanel::confirm);
 187    cx.add_action(CollabPanel::remove);
 188    cx.add_action(CollabPanel::remove_selected_channel);
 189    cx.add_action(CollabPanel::show_inline_context_menu);
 190    cx.add_action(CollabPanel::new_subchannel);
 191    cx.add_action(CollabPanel::invite_members);
 192    cx.add_action(CollabPanel::manage_members);
 193    cx.add_action(CollabPanel::rename_selected_channel);
 194    cx.add_action(CollabPanel::rename_channel);
 195    cx.add_action(CollabPanel::toggle_channel_collapsed_action);
 196    cx.add_action(CollabPanel::collapse_selected_channel);
 197    cx.add_action(CollabPanel::expand_selected_channel);
 198    cx.add_action(CollabPanel::open_channel_notes);
 199
 200    cx.add_action(
 201        |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext<CollabPanel>| {
 202            if panel.selection.take() != Some(action.ix) {
 203                panel.selection = Some(action.ix)
 204            }
 205
 206            cx.notify();
 207        },
 208    );
 209
 210    cx.add_action(
 211        |panel: &mut CollabPanel,
 212         action: &StartMoveChannelFor,
 213         _: &mut ViewContext<CollabPanel>| {
 214            panel.channel_clipboard = Some(ChannelMoveClipboard {
 215                channel_id: action.channel_id,
 216                parent_id: action.parent_id,
 217                intent: ClipboardIntent::Move,
 218            });
 219        },
 220    );
 221
 222    cx.add_action(
 223        |panel: &mut CollabPanel,
 224         action: &StartLinkChannelFor,
 225         _: &mut ViewContext<CollabPanel>| {
 226            panel.channel_clipboard = Some(ChannelMoveClipboard {
 227                channel_id: action.channel_id,
 228                parent_id: action.parent_id,
 229                intent: ClipboardIntent::Link,
 230            })
 231        },
 232    );
 233
 234    cx.add_action(
 235        |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext<CollabPanel>| {
 236            if let Some((_, path)) = panel.selected_channel() {
 237                panel.channel_clipboard = Some(ChannelMoveClipboard {
 238                    channel_id: path.channel_id(),
 239                    parent_id: path.parent_id(),
 240                    intent: ClipboardIntent::Move,
 241                })
 242            }
 243        },
 244    );
 245
 246    cx.add_action(
 247        |panel: &mut CollabPanel, _: &StartLinkChannel, _: &mut ViewContext<CollabPanel>| {
 248            if let Some((_, path)) = panel.selected_channel() {
 249                panel.channel_clipboard = Some(ChannelMoveClipboard {
 250                    channel_id: path.channel_id(),
 251                    parent_id: path.parent_id(),
 252                    intent: ClipboardIntent::Link,
 253                })
 254            }
 255        },
 256    );
 257
 258    cx.add_action(
 259        |panel: &mut CollabPanel, _: &MoveOrLinkToSelected, cx: &mut ViewContext<CollabPanel>| {
 260            let clipboard = panel.channel_clipboard.take();
 261            if let Some(((selected_channel, _), clipboard)) =
 262                panel.selected_channel().zip(clipboard)
 263            {
 264                match clipboard.intent {
 265                    ClipboardIntent::Move if clipboard.parent_id.is_some() => {
 266                        let parent_id = clipboard.parent_id.unwrap();
 267                        panel.channel_store.update(cx, |channel_store, cx| {
 268                            channel_store
 269                                .move_channel(
 270                                    clipboard.channel_id,
 271                                    parent_id,
 272                                    selected_channel.id,
 273                                    cx,
 274                                )
 275                                .detach_and_log_err(cx)
 276                        })
 277                    }
 278                    _ => panel.channel_store.update(cx, |channel_store, cx| {
 279                        channel_store
 280                            .link_channel(clipboard.channel_id, selected_channel.id, cx)
 281                            .detach_and_log_err(cx)
 282                    }),
 283                }
 284            }
 285        },
 286    );
 287
 288    cx.add_action(
 289        |panel: &mut CollabPanel, action: &LinkChannel, cx: &mut ViewContext<CollabPanel>| {
 290            if let Some(clipboard) = panel.channel_clipboard.take() {
 291                panel.channel_store.update(cx, |channel_store, cx| {
 292                    channel_store
 293                        .link_channel(clipboard.channel_id, action.to, cx)
 294                        .detach_and_log_err(cx)
 295                })
 296            }
 297        },
 298    );
 299
 300    cx.add_action(
 301        |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext<CollabPanel>| {
 302            if let Some(clipboard) = panel.channel_clipboard.take() {
 303                panel.channel_store.update(cx, |channel_store, cx| {
 304                    if let Some(parent) = clipboard.parent_id {
 305                        channel_store
 306                            .move_channel(clipboard.channel_id, parent, action.to, cx)
 307                            .detach_and_log_err(cx)
 308                    } else {
 309                        channel_store
 310                            .link_channel(clipboard.channel_id, action.to, cx)
 311                            .detach_and_log_err(cx)
 312                    }
 313                })
 314            }
 315        },
 316    );
 317
 318    cx.add_action(
 319        |panel: &mut CollabPanel, action: &UnlinkChannel, cx: &mut ViewContext<CollabPanel>| {
 320            panel.channel_store.update(cx, |channel_store, cx| {
 321                channel_store
 322                    .unlink_channel(action.channel_id, action.parent_id, cx)
 323                    .detach_and_log_err(cx)
 324            })
 325        },
 326    );
 327}
 328
 329#[derive(Debug)]
 330pub enum ChannelEditingState {
 331    Create {
 332        location: Option<ChannelPath>,
 333        pending_name: Option<String>,
 334    },
 335    Rename {
 336        location: ChannelPath,
 337        pending_name: Option<String>,
 338    },
 339}
 340
 341impl ChannelEditingState {
 342    fn pending_name(&self) -> Option<&str> {
 343        match self {
 344            ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
 345            ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
 346        }
 347    }
 348}
 349
 350pub struct CollabPanel {
 351    width: Option<f32>,
 352    fs: Arc<dyn Fs>,
 353    has_focus: bool,
 354    channel_clipboard: Option<ChannelMoveClipboard>,
 355    pending_serialization: Task<Option<()>>,
 356    context_menu: ViewHandle<ContextMenu>,
 357    filter_editor: ViewHandle<Editor>,
 358    channel_name_editor: ViewHandle<Editor>,
 359    channel_editing_state: Option<ChannelEditingState>,
 360    entries: Vec<ListEntry>,
 361    selection: Option<usize>,
 362    user_store: ModelHandle<UserStore>,
 363    client: Arc<Client>,
 364    channel_store: ModelHandle<ChannelStore>,
 365    project: ModelHandle<Project>,
 366    match_candidates: Vec<StringMatchCandidate>,
 367    list_state: ListState<Self>,
 368    subscriptions: Vec<Subscription>,
 369    collapsed_sections: Vec<Section>,
 370    collapsed_channels: Vec<ChannelPath>,
 371    drag_target_channel: Option<ChannelData>,
 372    workspace: WeakViewHandle<Workspace>,
 373    context_menu_on_selected: bool,
 374}
 375
 376#[derive(Serialize, Deserialize)]
 377struct SerializedCollabPanel {
 378    width: Option<f32>,
 379    collapsed_channels: Option<Vec<ChannelPath>>,
 380}
 381
 382#[derive(Debug)]
 383pub enum Event {
 384    DockPositionChanged,
 385    Focus,
 386    Dismissed,
 387}
 388
 389#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
 390enum Section {
 391    ActiveCall,
 392    Channels,
 393    ChannelInvites,
 394    ContactRequests,
 395    Contacts,
 396    Online,
 397    Offline,
 398}
 399
 400#[derive(Clone, Debug)]
 401enum ListEntry {
 402    Header(Section),
 403    CallParticipant {
 404        user: Arc<User>,
 405        is_pending: bool,
 406    },
 407    ParticipantProject {
 408        project_id: u64,
 409        worktree_root_names: Vec<String>,
 410        host_user_id: u64,
 411        is_last: bool,
 412    },
 413    ParticipantScreen {
 414        peer_id: PeerId,
 415        is_last: bool,
 416    },
 417    IncomingRequest(Arc<User>),
 418    OutgoingRequest(Arc<User>),
 419    ChannelInvite(Arc<Channel>),
 420    Channel {
 421        channel: Arc<Channel>,
 422        depth: usize,
 423        path: ChannelPath,
 424    },
 425    ChannelNotes {
 426        channel_id: ChannelId,
 427    },
 428    ChannelEditor {
 429        depth: usize,
 430    },
 431    Contact {
 432        contact: Arc<Contact>,
 433        calling: bool,
 434    },
 435    ContactPlaceholder,
 436}
 437
 438impl Entity for CollabPanel {
 439    type Event = Event;
 440}
 441
 442impl CollabPanel {
 443    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 444        cx.add_view::<Self, _>(|cx| {
 445            let view_id = cx.view_id();
 446
 447            let filter_editor = cx.add_view(|cx| {
 448                let mut editor = Editor::single_line(
 449                    Some(Arc::new(|theme| {
 450                        theme.collab_panel.user_query_editor.clone()
 451                    })),
 452                    cx,
 453                );
 454                editor.set_placeholder_text("Filter channels, contacts", cx);
 455                editor
 456            });
 457
 458            cx.subscribe(&filter_editor, |this, _, event, cx| {
 459                if let editor::Event::BufferEdited = event {
 460                    let query = this.filter_editor.read(cx).text(cx);
 461                    if !query.is_empty() {
 462                        this.selection.take();
 463                    }
 464                    this.update_entries(true, cx);
 465                    if !query.is_empty() {
 466                        this.selection = this
 467                            .entries
 468                            .iter()
 469                            .position(|entry| !matches!(entry, ListEntry::Header(_)));
 470                    }
 471                }
 472            })
 473            .detach();
 474
 475            let channel_name_editor = cx.add_view(|cx| {
 476                Editor::single_line(
 477                    Some(Arc::new(|theme| {
 478                        theme.collab_panel.user_query_editor.clone()
 479                    })),
 480                    cx,
 481                )
 482            });
 483
 484            cx.subscribe(&channel_name_editor, |this, _, event, cx| {
 485                if let editor::Event::Blurred = event {
 486                    if let Some(state) = &this.channel_editing_state {
 487                        if state.pending_name().is_some() {
 488                            return;
 489                        }
 490                    }
 491                    this.take_editing_state(cx);
 492                    this.update_entries(false, cx);
 493                    cx.notify();
 494                }
 495            })
 496            .detach();
 497
 498            let list_state =
 499                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 500                    let theme = theme::current(cx).clone();
 501                    let is_selected = this.selection == Some(ix);
 502                    let current_project_id = this.project.read(cx).remote_id();
 503
 504                    match &this.entries[ix] {
 505                        ListEntry::Header(section) => {
 506                            let is_collapsed = this.collapsed_sections.contains(section);
 507                            this.render_header(*section, &theme, is_selected, is_collapsed, cx)
 508                        }
 509                        ListEntry::CallParticipant { user, is_pending } => {
 510                            Self::render_call_participant(
 511                                user,
 512                                *is_pending,
 513                                is_selected,
 514                                &theme.collab_panel,
 515                            )
 516                        }
 517                        ListEntry::ParticipantProject {
 518                            project_id,
 519                            worktree_root_names,
 520                            host_user_id,
 521                            is_last,
 522                        } => Self::render_participant_project(
 523                            *project_id,
 524                            worktree_root_names,
 525                            *host_user_id,
 526                            Some(*project_id) == current_project_id,
 527                            *is_last,
 528                            is_selected,
 529                            &theme.collab_panel,
 530                            cx,
 531                        ),
 532                        ListEntry::ParticipantScreen { peer_id, is_last } => {
 533                            Self::render_participant_screen(
 534                                *peer_id,
 535                                *is_last,
 536                                is_selected,
 537                                &theme.collab_panel,
 538                                cx,
 539                            )
 540                        }
 541                        ListEntry::Channel {
 542                            channel,
 543                            depth,
 544                            path,
 545                        } => {
 546                            let channel_row = this.render_channel(
 547                                &*channel,
 548                                *depth,
 549                                path.to_owned(),
 550                                &theme.collab_panel,
 551                                is_selected,
 552                                ix,
 553                                cx,
 554                            );
 555
 556                            if is_selected && this.context_menu_on_selected {
 557                                Stack::new()
 558                                    .with_child(channel_row)
 559                                    .with_child(
 560                                        ChildView::new(&this.context_menu, cx)
 561                                            .aligned()
 562                                            .bottom()
 563                                            .right(),
 564                                    )
 565                                    .into_any()
 566                            } else {
 567                                return channel_row;
 568                            }
 569                        }
 570                        ListEntry::ChannelNotes { channel_id } => this.render_channel_notes(
 571                            *channel_id,
 572                            &theme.collab_panel,
 573                            is_selected,
 574                            cx,
 575                        ),
 576                        ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
 577                            channel.clone(),
 578                            this.channel_store.clone(),
 579                            &theme.collab_panel,
 580                            is_selected,
 581                            cx,
 582                        ),
 583                        ListEntry::IncomingRequest(user) => Self::render_contact_request(
 584                            user.clone(),
 585                            this.user_store.clone(),
 586                            &theme.collab_panel,
 587                            true,
 588                            is_selected,
 589                            cx,
 590                        ),
 591                        ListEntry::OutgoingRequest(user) => Self::render_contact_request(
 592                            user.clone(),
 593                            this.user_store.clone(),
 594                            &theme.collab_panel,
 595                            false,
 596                            is_selected,
 597                            cx,
 598                        ),
 599                        ListEntry::Contact { contact, calling } => Self::render_contact(
 600                            contact,
 601                            *calling,
 602                            &this.project,
 603                            &theme.collab_panel,
 604                            is_selected,
 605                            cx,
 606                        ),
 607                        ListEntry::ChannelEditor { depth } => {
 608                            this.render_channel_editor(&theme, *depth, cx)
 609                        }
 610                        ListEntry::ContactPlaceholder => {
 611                            this.render_contact_placeholder(&theme.collab_panel, is_selected, cx)
 612                        }
 613                    }
 614                });
 615
 616            let mut this = Self {
 617                width: None,
 618                has_focus: false,
 619                channel_clipboard: None,
 620                fs: workspace.app_state().fs.clone(),
 621                pending_serialization: Task::ready(None),
 622                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 623                channel_name_editor,
 624                filter_editor,
 625                entries: Vec::default(),
 626                channel_editing_state: None,
 627                selection: None,
 628                user_store: workspace.user_store().clone(),
 629                channel_store: workspace.app_state().channel_store.clone(),
 630                project: workspace.project().clone(),
 631                subscriptions: Vec::default(),
 632                match_candidates: Vec::default(),
 633                collapsed_sections: vec![Section::Offline],
 634                collapsed_channels: Vec::default(),
 635                workspace: workspace.weak_handle(),
 636                client: workspace.app_state().client.clone(),
 637                context_menu_on_selected: true,
 638                drag_target_channel: None,
 639                list_state,
 640            };
 641
 642            this.update_entries(false, cx);
 643
 644            // Update the dock position when the setting changes.
 645            let mut old_dock_position = this.position(cx);
 646            this.subscriptions
 647                .push(
 648                    cx.observe_global::<SettingsStore, _>(move |this: &mut Self, cx| {
 649                        let new_dock_position = this.position(cx);
 650                        if new_dock_position != old_dock_position {
 651                            old_dock_position = new_dock_position;
 652                            cx.emit(Event::DockPositionChanged);
 653                        }
 654                        cx.notify();
 655                    }),
 656                );
 657
 658            let active_call = ActiveCall::global(cx);
 659            this.subscriptions
 660                .push(cx.observe(&this.user_store, |this, _, cx| {
 661                    this.update_entries(true, cx)
 662                }));
 663            this.subscriptions
 664                .push(cx.observe(&this.channel_store, |this, _, cx| {
 665                    this.update_entries(true, cx)
 666                }));
 667            this.subscriptions
 668                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
 669            this.subscriptions
 670                .push(cx.observe_flag::<ChannelsAlpha, _>(move |_, this, cx| {
 671                    this.update_entries(true, cx)
 672                }));
 673            this.subscriptions.push(cx.subscribe(
 674                &this.channel_store,
 675                |this, _channel_store, e, cx| match e {
 676                    ChannelEvent::ChannelCreated(channel_id)
 677                    | ChannelEvent::ChannelRenamed(channel_id) => {
 678                        if this.take_editing_state(cx) {
 679                            this.update_entries(false, cx);
 680                            this.selection = this.entries.iter().position(|entry| {
 681                                if let ListEntry::Channel { channel, .. } = entry {
 682                                    channel.id == *channel_id
 683                                } else {
 684                                    false
 685                                }
 686                            });
 687                        }
 688                    }
 689                },
 690            ));
 691
 692            this
 693        })
 694    }
 695
 696    pub fn load(
 697        workspace: WeakViewHandle<Workspace>,
 698        cx: AsyncAppContext,
 699    ) -> Task<Result<ViewHandle<Self>>> {
 700        cx.spawn(|mut cx| async move {
 701            let serialized_panel = if let Some(panel) = cx
 702                .background()
 703                .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) })
 704                .await
 705                .log_err()
 706                .flatten()
 707            {
 708                match serde_json::from_str::<SerializedCollabPanel>(&panel) {
 709                    Ok(panel) => Some(panel),
 710                    Err(err) => {
 711                        log::error!("Failed to deserialize collaboration panel: {}", err);
 712                        None
 713                    }
 714                }
 715            } else {
 716                None
 717            };
 718
 719            workspace.update(&mut cx, |workspace, cx| {
 720                let panel = CollabPanel::new(workspace, cx);
 721                if let Some(serialized_panel) = serialized_panel {
 722                    panel.update(cx, |panel, cx| {
 723                        panel.width = serialized_panel.width;
 724                        panel.collapsed_channels = serialized_panel
 725                            .collapsed_channels
 726                            .unwrap_or_else(|| Vec::new());
 727                        cx.notify();
 728                    });
 729                }
 730                panel
 731            })
 732        })
 733    }
 734
 735    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 736        let width = self.width;
 737        let collapsed_channels = self.collapsed_channels.clone();
 738        self.pending_serialization = cx.background().spawn(
 739            async move {
 740                KEY_VALUE_STORE
 741                    .write_kvp(
 742                        COLLABORATION_PANEL_KEY.into(),
 743                        serde_json::to_string(&SerializedCollabPanel {
 744                            width,
 745                            collapsed_channels: Some(collapsed_channels),
 746                        })?,
 747                    )
 748                    .await?;
 749                anyhow::Ok(())
 750            }
 751            .log_err(),
 752        );
 753    }
 754
 755    fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
 756        let channel_store = self.channel_store.read(cx);
 757        let user_store = self.user_store.read(cx);
 758        let query = self.filter_editor.read(cx).text(cx);
 759        let executor = cx.background().clone();
 760
 761        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 762        let old_entries = mem::take(&mut self.entries);
 763
 764        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 765            self.entries.push(ListEntry::Header(Section::ActiveCall));
 766
 767            if !self.collapsed_sections.contains(&Section::ActiveCall) {
 768                let room = room.read(cx);
 769
 770                if let Some(channel_id) = room.channel_id() {
 771                    self.entries.push(ListEntry::ChannelNotes { channel_id })
 772                }
 773
 774                // Populate the active user.
 775                if let Some(user) = user_store.current_user() {
 776                    self.match_candidates.clear();
 777                    self.match_candidates.push(StringMatchCandidate {
 778                        id: 0,
 779                        string: user.github_login.clone(),
 780                        char_bag: user.github_login.chars().collect(),
 781                    });
 782                    let matches = executor.block(match_strings(
 783                        &self.match_candidates,
 784                        &query,
 785                        true,
 786                        usize::MAX,
 787                        &Default::default(),
 788                        executor.clone(),
 789                    ));
 790                    if !matches.is_empty() {
 791                        let user_id = user.id;
 792                        self.entries.push(ListEntry::CallParticipant {
 793                            user,
 794                            is_pending: false,
 795                        });
 796                        let mut projects = room.local_participant().projects.iter().peekable();
 797                        while let Some(project) = projects.next() {
 798                            self.entries.push(ListEntry::ParticipantProject {
 799                                project_id: project.id,
 800                                worktree_root_names: project.worktree_root_names.clone(),
 801                                host_user_id: user_id,
 802                                is_last: projects.peek().is_none(),
 803                            });
 804                        }
 805                    }
 806                }
 807
 808                // Populate remote participants.
 809                self.match_candidates.clear();
 810                self.match_candidates
 811                    .extend(room.remote_participants().iter().map(|(_, participant)| {
 812                        StringMatchCandidate {
 813                            id: participant.user.id as usize,
 814                            string: participant.user.github_login.clone(),
 815                            char_bag: participant.user.github_login.chars().collect(),
 816                        }
 817                    }));
 818                let matches = executor.block(match_strings(
 819                    &self.match_candidates,
 820                    &query,
 821                    true,
 822                    usize::MAX,
 823                    &Default::default(),
 824                    executor.clone(),
 825                ));
 826                for mat in matches {
 827                    let user_id = mat.candidate_id as u64;
 828                    let participant = &room.remote_participants()[&user_id];
 829                    self.entries.push(ListEntry::CallParticipant {
 830                        user: participant.user.clone(),
 831                        is_pending: false,
 832                    });
 833                    let mut projects = participant.projects.iter().peekable();
 834                    while let Some(project) = projects.next() {
 835                        self.entries.push(ListEntry::ParticipantProject {
 836                            project_id: project.id,
 837                            worktree_root_names: project.worktree_root_names.clone(),
 838                            host_user_id: participant.user.id,
 839                            is_last: projects.peek().is_none()
 840                                && participant.video_tracks.is_empty(),
 841                        });
 842                    }
 843                    if !participant.video_tracks.is_empty() {
 844                        self.entries.push(ListEntry::ParticipantScreen {
 845                            peer_id: participant.peer_id,
 846                            is_last: true,
 847                        });
 848                    }
 849                }
 850
 851                // Populate pending participants.
 852                self.match_candidates.clear();
 853                self.match_candidates
 854                    .extend(room.pending_participants().iter().enumerate().map(
 855                        |(id, participant)| StringMatchCandidate {
 856                            id,
 857                            string: participant.github_login.clone(),
 858                            char_bag: participant.github_login.chars().collect(),
 859                        },
 860                    ));
 861                let matches = executor.block(match_strings(
 862                    &self.match_candidates,
 863                    &query,
 864                    true,
 865                    usize::MAX,
 866                    &Default::default(),
 867                    executor.clone(),
 868                ));
 869                self.entries
 870                    .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
 871                        user: room.pending_participants()[mat.candidate_id].clone(),
 872                        is_pending: true,
 873                    }));
 874            }
 875        }
 876
 877        let mut request_entries = Vec::new();
 878
 879        if cx.has_flag::<ChannelsAlpha>() {
 880            self.entries.push(ListEntry::Header(Section::Channels));
 881
 882            if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
 883                self.match_candidates.clear();
 884                self.match_candidates
 885                    .extend(channel_store.channel_dag_entries().enumerate().map(
 886                        |(ix, (_, channel))| StringMatchCandidate {
 887                            id: ix,
 888                            string: channel.name.clone(),
 889                            char_bag: channel.name.chars().collect(),
 890                        },
 891                    ));
 892                let matches = executor.block(match_strings(
 893                    &self.match_candidates,
 894                    &query,
 895                    true,
 896                    usize::MAX,
 897                    &Default::default(),
 898                    executor.clone(),
 899                ));
 900                if let Some(state) = &self.channel_editing_state {
 901                    if matches!(state, ChannelEditingState::Create { location: None, .. }) {
 902                        self.entries.push(ListEntry::ChannelEditor { depth: 0 });
 903                    }
 904                }
 905                let mut collapse_depth = None;
 906                for mat in matches {
 907                    let (channel, path) = channel_store
 908                        .channel_dag_entry_at(mat.candidate_id)
 909                        .unwrap();
 910                    let depth = path.len() - 1;
 911
 912                    if collapse_depth.is_none() && self.is_channel_collapsed(path) {
 913                        collapse_depth = Some(depth);
 914                    } else if let Some(collapsed_depth) = collapse_depth {
 915                        if depth > collapsed_depth {
 916                            continue;
 917                        }
 918                        if self.is_channel_collapsed(path) {
 919                            collapse_depth = Some(depth);
 920                        } else {
 921                            collapse_depth = None;
 922                        }
 923                    }
 924
 925                    match &self.channel_editing_state {
 926                        Some(ChannelEditingState::Create {
 927                            location: parent_path,
 928                            ..
 929                        }) if parent_path.as_ref() == Some(path) => {
 930                            self.entries.push(ListEntry::Channel {
 931                                channel: channel.clone(),
 932                                depth,
 933                                path: path.clone(),
 934                            });
 935                            self.entries
 936                                .push(ListEntry::ChannelEditor { depth: depth + 1 });
 937                        }
 938                        Some(ChannelEditingState::Rename {
 939                            location: parent_path,
 940                            ..
 941                        }) if parent_path == path => {
 942                            self.entries.push(ListEntry::ChannelEditor { depth });
 943                        }
 944                        _ => {
 945                            self.entries.push(ListEntry::Channel {
 946                                channel: channel.clone(),
 947                                depth,
 948                                path: path.clone(),
 949                            });
 950                        }
 951                    }
 952                }
 953            }
 954
 955            let channel_invites = channel_store.channel_invitations();
 956            if !channel_invites.is_empty() {
 957                self.match_candidates.clear();
 958                self.match_candidates
 959                    .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
 960                        StringMatchCandidate {
 961                            id: ix,
 962                            string: channel.name.clone(),
 963                            char_bag: channel.name.chars().collect(),
 964                        }
 965                    }));
 966                let matches = executor.block(match_strings(
 967                    &self.match_candidates,
 968                    &query,
 969                    true,
 970                    usize::MAX,
 971                    &Default::default(),
 972                    executor.clone(),
 973                ));
 974                request_entries.extend(matches.iter().map(|mat| {
 975                    ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
 976                }));
 977
 978                if !request_entries.is_empty() {
 979                    self.entries
 980                        .push(ListEntry::Header(Section::ChannelInvites));
 981                    if !self.collapsed_sections.contains(&Section::ChannelInvites) {
 982                        self.entries.append(&mut request_entries);
 983                    }
 984                }
 985            }
 986        }
 987
 988        self.entries.push(ListEntry::Header(Section::Contacts));
 989
 990        request_entries.clear();
 991        let incoming = user_store.incoming_contact_requests();
 992        if !incoming.is_empty() {
 993            self.match_candidates.clear();
 994            self.match_candidates
 995                .extend(
 996                    incoming
 997                        .iter()
 998                        .enumerate()
 999                        .map(|(ix, user)| StringMatchCandidate {
1000                            id: ix,
1001                            string: user.github_login.clone(),
1002                            char_bag: user.github_login.chars().collect(),
1003                        }),
1004                );
1005            let matches = executor.block(match_strings(
1006                &self.match_candidates,
1007                &query,
1008                true,
1009                usize::MAX,
1010                &Default::default(),
1011                executor.clone(),
1012            ));
1013            request_entries.extend(
1014                matches
1015                    .iter()
1016                    .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
1017            );
1018        }
1019
1020        let outgoing = user_store.outgoing_contact_requests();
1021        if !outgoing.is_empty() {
1022            self.match_candidates.clear();
1023            self.match_candidates
1024                .extend(
1025                    outgoing
1026                        .iter()
1027                        .enumerate()
1028                        .map(|(ix, user)| StringMatchCandidate {
1029                            id: ix,
1030                            string: user.github_login.clone(),
1031                            char_bag: user.github_login.chars().collect(),
1032                        }),
1033                );
1034            let matches = executor.block(match_strings(
1035                &self.match_candidates,
1036                &query,
1037                true,
1038                usize::MAX,
1039                &Default::default(),
1040                executor.clone(),
1041            ));
1042            request_entries.extend(
1043                matches
1044                    .iter()
1045                    .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
1046            );
1047        }
1048
1049        if !request_entries.is_empty() {
1050            self.entries
1051                .push(ListEntry::Header(Section::ContactRequests));
1052            if !self.collapsed_sections.contains(&Section::ContactRequests) {
1053                self.entries.append(&mut request_entries);
1054            }
1055        }
1056
1057        let contacts = user_store.contacts();
1058        if !contacts.is_empty() {
1059            self.match_candidates.clear();
1060            self.match_candidates
1061                .extend(
1062                    contacts
1063                        .iter()
1064                        .enumerate()
1065                        .map(|(ix, contact)| StringMatchCandidate {
1066                            id: ix,
1067                            string: contact.user.github_login.clone(),
1068                            char_bag: contact.user.github_login.chars().collect(),
1069                        }),
1070                );
1071
1072            let matches = executor.block(match_strings(
1073                &self.match_candidates,
1074                &query,
1075                true,
1076                usize::MAX,
1077                &Default::default(),
1078                executor.clone(),
1079            ));
1080
1081            let (online_contacts, offline_contacts) = matches
1082                .iter()
1083                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
1084
1085            for (matches, section) in [
1086                (online_contacts, Section::Online),
1087                (offline_contacts, Section::Offline),
1088            ] {
1089                if !matches.is_empty() {
1090                    self.entries.push(ListEntry::Header(section));
1091                    if !self.collapsed_sections.contains(&section) {
1092                        let active_call = &ActiveCall::global(cx).read(cx);
1093                        for mat in matches {
1094                            let contact = &contacts[mat.candidate_id];
1095                            self.entries.push(ListEntry::Contact {
1096                                contact: contact.clone(),
1097                                calling: active_call.pending_invites().contains(&contact.user.id),
1098                            });
1099                        }
1100                    }
1101                }
1102            }
1103        }
1104
1105        if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
1106            self.entries.push(ListEntry::ContactPlaceholder);
1107        }
1108
1109        if select_same_item {
1110            if let Some(prev_selected_entry) = prev_selected_entry {
1111                self.selection.take();
1112                for (ix, entry) in self.entries.iter().enumerate() {
1113                    if *entry == prev_selected_entry {
1114                        self.selection = Some(ix);
1115                        break;
1116                    }
1117                }
1118            }
1119        } else {
1120            self.selection = self.selection.and_then(|prev_selection| {
1121                if self.entries.is_empty() {
1122                    None
1123                } else {
1124                    Some(prev_selection.min(self.entries.len() - 1))
1125                }
1126            });
1127        }
1128
1129        let old_scroll_top = self.list_state.logical_scroll_top();
1130        self.list_state.reset(self.entries.len());
1131
1132        // Attempt to maintain the same scroll position.
1133        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
1134            let new_scroll_top = self
1135                .entries
1136                .iter()
1137                .position(|entry| entry == old_top_entry)
1138                .map(|item_ix| ListOffset {
1139                    item_ix,
1140                    offset_in_item: old_scroll_top.offset_in_item,
1141                })
1142                .or_else(|| {
1143                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
1144                    let item_ix = self
1145                        .entries
1146                        .iter()
1147                        .position(|entry| entry == entry_after_old_top)?;
1148                    Some(ListOffset {
1149                        item_ix,
1150                        offset_in_item: 0.,
1151                    })
1152                })
1153                .or_else(|| {
1154                    let entry_before_old_top =
1155                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
1156                    let item_ix = self
1157                        .entries
1158                        .iter()
1159                        .position(|entry| entry == entry_before_old_top)?;
1160                    Some(ListOffset {
1161                        item_ix,
1162                        offset_in_item: 0.,
1163                    })
1164                });
1165
1166            self.list_state
1167                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
1168        }
1169
1170        cx.notify();
1171    }
1172
1173    fn render_call_participant(
1174        user: &User,
1175        is_pending: bool,
1176        is_selected: bool,
1177        theme: &theme::CollabPanel,
1178    ) -> AnyElement<Self> {
1179        Flex::row()
1180            .with_children(user.avatar.clone().map(|avatar| {
1181                Image::from_data(avatar)
1182                    .with_style(theme.contact_avatar)
1183                    .aligned()
1184                    .left()
1185            }))
1186            .with_child(
1187                Label::new(
1188                    user.github_login.clone(),
1189                    theme.contact_username.text.clone(),
1190                )
1191                .contained()
1192                .with_style(theme.contact_username.container)
1193                .aligned()
1194                .left()
1195                .flex(1., true),
1196            )
1197            .with_children(if is_pending {
1198                Some(
1199                    Label::new("Calling", theme.calling_indicator.text.clone())
1200                        .contained()
1201                        .with_style(theme.calling_indicator.container)
1202                        .aligned(),
1203                )
1204            } else {
1205                None
1206            })
1207            .constrained()
1208            .with_height(theme.row_height)
1209            .contained()
1210            .with_style(
1211                *theme
1212                    .contact_row
1213                    .in_state(is_selected)
1214                    .style_for(&mut Default::default()),
1215            )
1216            .into_any()
1217    }
1218
1219    fn render_participant_project(
1220        project_id: u64,
1221        worktree_root_names: &[String],
1222        host_user_id: u64,
1223        is_current: bool,
1224        is_last: bool,
1225        is_selected: bool,
1226        theme: &theme::CollabPanel,
1227        cx: &mut ViewContext<Self>,
1228    ) -> AnyElement<Self> {
1229        enum JoinProject {}
1230
1231        let host_avatar_width = theme
1232            .contact_avatar
1233            .width
1234            .or(theme.contact_avatar.height)
1235            .unwrap_or(0.);
1236        let tree_branch = theme.tree_branch;
1237        let project_name = if worktree_root_names.is_empty() {
1238            "untitled".to_string()
1239        } else {
1240            worktree_root_names.join(", ")
1241        };
1242
1243        MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
1244            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1245            let row = theme
1246                .project_row
1247                .in_state(is_selected)
1248                .style_for(mouse_state);
1249
1250            Flex::row()
1251                .with_child(render_tree_branch(
1252                    tree_branch,
1253                    &row.name.text,
1254                    is_last,
1255                    vec2f(host_avatar_width, theme.row_height),
1256                    cx.font_cache(),
1257                ))
1258                .with_child(
1259                    Svg::new("icons/file_icons/folder.svg")
1260                        .with_color(theme.channel_hash.color)
1261                        .constrained()
1262                        .with_width(theme.channel_hash.width)
1263                        .aligned()
1264                        .left(),
1265                )
1266                .with_child(
1267                    Label::new(project_name, row.name.text.clone())
1268                        .aligned()
1269                        .left()
1270                        .contained()
1271                        .with_style(row.name.container)
1272                        .flex(1., false),
1273                )
1274                .constrained()
1275                .with_height(theme.row_height)
1276                .contained()
1277                .with_style(row.container)
1278        })
1279        .with_cursor_style(if !is_current {
1280            CursorStyle::PointingHand
1281        } else {
1282            CursorStyle::Arrow
1283        })
1284        .on_click(MouseButton::Left, move |_, this, cx| {
1285            if !is_current {
1286                if let Some(workspace) = this.workspace.upgrade(cx) {
1287                    let app_state = workspace.read(cx).app_state().clone();
1288                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
1289                        .detach_and_log_err(cx);
1290                }
1291            }
1292        })
1293        .into_any()
1294    }
1295
1296    fn render_participant_screen(
1297        peer_id: PeerId,
1298        is_last: bool,
1299        is_selected: bool,
1300        theme: &theme::CollabPanel,
1301        cx: &mut ViewContext<Self>,
1302    ) -> AnyElement<Self> {
1303        enum OpenSharedScreen {}
1304
1305        let host_avatar_width = theme
1306            .contact_avatar
1307            .width
1308            .or(theme.contact_avatar.height)
1309            .unwrap_or(0.);
1310        let tree_branch = theme.tree_branch;
1311
1312        MouseEventHandler::new::<OpenSharedScreen, _>(
1313            peer_id.as_u64() as usize,
1314            cx,
1315            |mouse_state, cx| {
1316                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
1317                let row = theme
1318                    .project_row
1319                    .in_state(is_selected)
1320                    .style_for(mouse_state);
1321
1322                Flex::row()
1323                    .with_child(render_tree_branch(
1324                        tree_branch,
1325                        &row.name.text,
1326                        is_last,
1327                        vec2f(host_avatar_width, theme.row_height),
1328                        cx.font_cache(),
1329                    ))
1330                    .with_child(
1331                        Svg::new("icons/desktop.svg")
1332                            .with_color(theme.channel_hash.color)
1333                            .constrained()
1334                            .with_width(theme.channel_hash.width)
1335                            .aligned()
1336                            .left(),
1337                    )
1338                    .with_child(
1339                        Label::new("Screen", row.name.text.clone())
1340                            .aligned()
1341                            .left()
1342                            .contained()
1343                            .with_style(row.name.container)
1344                            .flex(1., false),
1345                    )
1346                    .constrained()
1347                    .with_height(theme.row_height)
1348                    .contained()
1349                    .with_style(row.container)
1350            },
1351        )
1352        .with_cursor_style(CursorStyle::PointingHand)
1353        .on_click(MouseButton::Left, move |_, this, cx| {
1354            if let Some(workspace) = this.workspace.upgrade(cx) {
1355                workspace.update(cx, |workspace, cx| {
1356                    workspace.open_shared_screen(peer_id, cx)
1357                });
1358            }
1359        })
1360        .into_any()
1361    }
1362
1363    fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
1364        if let Some(_) = self.channel_editing_state.take() {
1365            self.channel_name_editor.update(cx, |editor, cx| {
1366                editor.set_text("", cx);
1367            });
1368            true
1369        } else {
1370            false
1371        }
1372    }
1373
1374    fn render_header(
1375        &self,
1376        section: Section,
1377        theme: &theme::Theme,
1378        is_selected: bool,
1379        is_collapsed: bool,
1380        cx: &mut ViewContext<Self>,
1381    ) -> AnyElement<Self> {
1382        enum Header {}
1383        enum LeaveCallContactList {}
1384        enum AddChannel {}
1385
1386        let tooltip_style = &theme.tooltip;
1387        let text = match section {
1388            Section::ActiveCall => {
1389                let channel_name = iife!({
1390                    let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
1391
1392                    let name = self
1393                        .channel_store
1394                        .read(cx)
1395                        .channel_for_id(channel_id)?
1396                        .name
1397                        .as_str();
1398
1399                    Some(name)
1400                });
1401
1402                if let Some(name) = channel_name {
1403                    Cow::Owned(format!("#{}", name))
1404                } else {
1405                    Cow::Borrowed("Current Call")
1406                }
1407            }
1408            Section::ContactRequests => Cow::Borrowed("Requests"),
1409            Section::Contacts => Cow::Borrowed("Contacts"),
1410            Section::Channels => Cow::Borrowed("Channels"),
1411            Section::ChannelInvites => Cow::Borrowed("Invites"),
1412            Section::Online => Cow::Borrowed("Online"),
1413            Section::Offline => Cow::Borrowed("Offline"),
1414        };
1415
1416        enum AddContact {}
1417        let button = match section {
1418            Section::ActiveCall => Some(
1419                MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
1420                    render_icon_button(
1421                        theme
1422                            .collab_panel
1423                            .leave_call_button
1424                            .style_for(is_selected, state),
1425                        "icons/exit.svg",
1426                    )
1427                })
1428                .with_cursor_style(CursorStyle::PointingHand)
1429                .on_click(MouseButton::Left, |_, _, cx| {
1430                    Self::leave_call(cx);
1431                })
1432                .with_tooltip::<AddContact>(
1433                    0,
1434                    "Leave call",
1435                    None,
1436                    tooltip_style.clone(),
1437                    cx,
1438                ),
1439            ),
1440            Section::Contacts => Some(
1441                MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
1442                    render_icon_button(
1443                        theme
1444                            .collab_panel
1445                            .add_contact_button
1446                            .style_for(is_selected, state),
1447                        "icons/plus.svg",
1448                    )
1449                })
1450                .with_cursor_style(CursorStyle::PointingHand)
1451                .on_click(MouseButton::Left, |_, this, cx| {
1452                    this.toggle_contact_finder(cx);
1453                })
1454                .with_tooltip::<LeaveCallContactList>(
1455                    0,
1456                    "Search for new contact",
1457                    None,
1458                    tooltip_style.clone(),
1459                    cx,
1460                ),
1461            ),
1462            Section::Channels => Some(
1463                MouseEventHandler::new::<AddChannel, _>(0, cx, |state, _| {
1464                    render_icon_button(
1465                        theme
1466                            .collab_panel
1467                            .add_contact_button
1468                            .style_for(is_selected, state),
1469                        "icons/plus.svg",
1470                    )
1471                })
1472                .with_cursor_style(CursorStyle::PointingHand)
1473                .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx))
1474                .with_tooltip::<AddChannel>(
1475                    0,
1476                    "Create a channel",
1477                    None,
1478                    tooltip_style.clone(),
1479                    cx,
1480                ),
1481            ),
1482            _ => None,
1483        };
1484
1485        let can_collapse = match section {
1486            Section::ActiveCall | Section::Channels | Section::Contacts => false,
1487            Section::ChannelInvites
1488            | Section::ContactRequests
1489            | Section::Online
1490            | Section::Offline => true,
1491        };
1492        let icon_size = (&theme.collab_panel).section_icon_size;
1493        let mut result = MouseEventHandler::new::<Header, _>(section as usize, cx, |state, _| {
1494            let header_style = if can_collapse {
1495                theme
1496                    .collab_panel
1497                    .subheader_row
1498                    .in_state(is_selected)
1499                    .style_for(state)
1500            } else {
1501                &theme.collab_panel.header_row
1502            };
1503
1504            Flex::row()
1505                .with_children(if can_collapse {
1506                    Some(
1507                        Svg::new(if is_collapsed {
1508                            "icons/chevron_right.svg"
1509                        } else {
1510                            "icons/chevron_down.svg"
1511                        })
1512                        .with_color(header_style.text.color)
1513                        .constrained()
1514                        .with_max_width(icon_size)
1515                        .with_max_height(icon_size)
1516                        .aligned()
1517                        .constrained()
1518                        .with_width(icon_size)
1519                        .contained()
1520                        .with_margin_right(
1521                            theme.collab_panel.contact_username.container.margin.left,
1522                        ),
1523                    )
1524                } else {
1525                    None
1526                })
1527                .with_child(
1528                    Label::new(text, header_style.text.clone())
1529                        .aligned()
1530                        .left()
1531                        .flex(1., true),
1532                )
1533                .with_children(button.map(|button| button.aligned().right()))
1534                .constrained()
1535                .with_height(theme.collab_panel.row_height)
1536                .contained()
1537                .with_style(header_style.container)
1538        });
1539
1540        if can_collapse {
1541            result = result
1542                .with_cursor_style(CursorStyle::PointingHand)
1543                .on_click(MouseButton::Left, move |_, this, cx| {
1544                    if can_collapse {
1545                        this.toggle_section_expanded(section, cx);
1546                    }
1547                })
1548        }
1549
1550        result.into_any()
1551    }
1552
1553    fn render_contact(
1554        contact: &Contact,
1555        calling: bool,
1556        project: &ModelHandle<Project>,
1557        theme: &theme::CollabPanel,
1558        is_selected: bool,
1559        cx: &mut ViewContext<Self>,
1560    ) -> AnyElement<Self> {
1561        let online = contact.online;
1562        let busy = contact.busy || calling;
1563        let user_id = contact.user.id;
1564        let github_login = contact.user.github_login.clone();
1565        let initial_project = project.clone();
1566        let mut event_handler =
1567            MouseEventHandler::new::<Contact, _>(contact.user.id as usize, cx, |state, cx| {
1568                Flex::row()
1569                    .with_children(contact.user.avatar.clone().map(|avatar| {
1570                        let status_badge = if contact.online {
1571                            Some(
1572                                Empty::new()
1573                                    .collapsed()
1574                                    .contained()
1575                                    .with_style(if busy {
1576                                        theme.contact_status_busy
1577                                    } else {
1578                                        theme.contact_status_free
1579                                    })
1580                                    .aligned(),
1581                            )
1582                        } else {
1583                            None
1584                        };
1585                        Stack::new()
1586                            .with_child(
1587                                Image::from_data(avatar)
1588                                    .with_style(theme.contact_avatar)
1589                                    .aligned()
1590                                    .left(),
1591                            )
1592                            .with_children(status_badge)
1593                    }))
1594                    .with_child(
1595                        Label::new(
1596                            contact.user.github_login.clone(),
1597                            theme.contact_username.text.clone(),
1598                        )
1599                        .contained()
1600                        .with_style(theme.contact_username.container)
1601                        .aligned()
1602                        .left()
1603                        .flex(1., true),
1604                    )
1605                    .with_child(
1606                        MouseEventHandler::new::<Cancel, _>(
1607                            contact.user.id as usize,
1608                            cx,
1609                            |mouse_state, _| {
1610                                let button_style = theme.contact_button.style_for(mouse_state);
1611                                render_icon_button(button_style, "icons/x.svg")
1612                                    .aligned()
1613                                    .flex_float()
1614                            },
1615                        )
1616                        .with_padding(Padding::uniform(2.))
1617                        .with_cursor_style(CursorStyle::PointingHand)
1618                        .on_click(MouseButton::Left, move |_, this, cx| {
1619                            this.remove_contact(user_id, &github_login, cx);
1620                        })
1621                        .flex_float(),
1622                    )
1623                    .with_children(if calling {
1624                        Some(
1625                            Label::new("Calling", theme.calling_indicator.text.clone())
1626                                .contained()
1627                                .with_style(theme.calling_indicator.container)
1628                                .aligned(),
1629                        )
1630                    } else {
1631                        None
1632                    })
1633                    .constrained()
1634                    .with_height(theme.row_height)
1635                    .contained()
1636                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1637            })
1638            .on_click(MouseButton::Left, move |_, this, cx| {
1639                if online && !busy {
1640                    this.call(user_id, Some(initial_project.clone()), cx);
1641                }
1642            });
1643
1644        if online {
1645            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1646        }
1647
1648        event_handler.into_any()
1649    }
1650
1651    fn render_contact_placeholder(
1652        &self,
1653        theme: &theme::CollabPanel,
1654        is_selected: bool,
1655        cx: &mut ViewContext<Self>,
1656    ) -> AnyElement<Self> {
1657        enum AddContacts {}
1658        MouseEventHandler::new::<AddContacts, _>(0, cx, |state, _| {
1659            let style = theme.list_empty_state.style_for(is_selected, state);
1660            Flex::row()
1661                .with_child(
1662                    Svg::new("icons/plus.svg")
1663                        .with_color(theme.list_empty_icon.color)
1664                        .constrained()
1665                        .with_width(theme.list_empty_icon.width)
1666                        .aligned()
1667                        .left(),
1668                )
1669                .with_child(
1670                    Label::new("Add a contact", style.text.clone())
1671                        .contained()
1672                        .with_style(theme.list_empty_label_container),
1673                )
1674                .align_children_center()
1675                .contained()
1676                .with_style(style.container)
1677                .into_any()
1678        })
1679        .on_click(MouseButton::Left, |_, this, cx| {
1680            this.toggle_contact_finder(cx);
1681        })
1682        .into_any()
1683    }
1684
1685    fn render_channel_editor(
1686        &self,
1687        theme: &theme::Theme,
1688        depth: usize,
1689        cx: &AppContext,
1690    ) -> AnyElement<Self> {
1691        Flex::row()
1692            .with_child(
1693                Empty::new()
1694                    .constrained()
1695                    .with_width(theme.collab_panel.disclosure.button_space()),
1696            )
1697            .with_child(
1698                Svg::new("icons/hash.svg")
1699                    .with_color(theme.collab_panel.channel_hash.color)
1700                    .constrained()
1701                    .with_width(theme.collab_panel.channel_hash.width)
1702                    .aligned()
1703                    .left(),
1704            )
1705            .with_child(
1706                if let Some(pending_name) = self
1707                    .channel_editing_state
1708                    .as_ref()
1709                    .and_then(|state| state.pending_name())
1710                {
1711                    Label::new(
1712                        pending_name.to_string(),
1713                        theme.collab_panel.contact_username.text.clone(),
1714                    )
1715                    .contained()
1716                    .with_style(theme.collab_panel.contact_username.container)
1717                    .aligned()
1718                    .left()
1719                    .flex(1., true)
1720                    .into_any()
1721                } else {
1722                    ChildView::new(&self.channel_name_editor, cx)
1723                        .aligned()
1724                        .left()
1725                        .contained()
1726                        .with_style(theme.collab_panel.channel_editor)
1727                        .flex(1.0, true)
1728                        .into_any()
1729                },
1730            )
1731            .align_children_center()
1732            .constrained()
1733            .with_height(theme.collab_panel.row_height)
1734            .contained()
1735            .with_style(ContainerStyle {
1736                background_color: Some(theme.editor.background),
1737                ..*theme.collab_panel.contact_row.default_style()
1738            })
1739            .with_padding_left(
1740                theme.collab_panel.contact_row.default_style().padding.left
1741                    + theme.collab_panel.channel_indent * depth as f32,
1742            )
1743            .into_any()
1744    }
1745
1746    fn render_channel(
1747        &self,
1748        channel: &Channel,
1749        depth: usize,
1750        path: ChannelPath,
1751        theme: &theme::CollabPanel,
1752        is_selected: bool,
1753        ix: usize,
1754        cx: &mut ViewContext<Self>,
1755    ) -> AnyElement<Self> {
1756        let channel_id = channel.id;
1757        let has_children = self.channel_store.read(cx).has_children(channel_id);
1758        let other_selected =
1759            self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
1760        let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
1761
1762        let is_active = iife!({
1763            let call_channel = ActiveCall::global(cx)
1764                .read(cx)
1765                .room()?
1766                .read(cx)
1767                .channel_id()?;
1768            Some(call_channel == channel_id)
1769        })
1770        .unwrap_or(false);
1771
1772        const FACEPILE_LIMIT: usize = 3;
1773
1774        enum ChannelCall {}
1775
1776        let mut is_dragged_over = false;
1777        if cx
1778            .global::<DragAndDrop<Workspace>>()
1779            .currently_dragged::<DraggedChannel>(cx.window())
1780            .is_some()
1781            && self
1782                .drag_target_channel
1783                .as_ref()
1784                .filter(|(_, dragged_path)| path.starts_with(dragged_path))
1785                .is_some()
1786        {
1787            is_dragged_over = true;
1788        }
1789
1790        MouseEventHandler::new::<Channel, _>(ix, cx, |state, cx| {
1791            let row_hovered = state.hovered();
1792
1793            let mut select_state = |interactive: &Interactive<ContainerStyle>| {
1794                if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() {
1795                    interactive.clicked.as_ref().unwrap().clone()
1796                } else if state.hovered() || other_selected {
1797                    interactive
1798                        .hovered
1799                        .as_ref()
1800                        .unwrap_or(&interactive.default)
1801                        .clone()
1802                } else {
1803                    interactive.default.clone()
1804                }
1805            };
1806
1807            Flex::<Self>::row()
1808                .with_child(
1809                    Svg::new("icons/hash.svg")
1810                        .with_color(theme.channel_hash.color)
1811                        .constrained()
1812                        .with_width(theme.channel_hash.width)
1813                        .aligned()
1814                        .left(),
1815                )
1816                .with_child(
1817                    Label::new(channel.name.clone(), theme.channel_name.text.clone())
1818                        .contained()
1819                        .with_style(theme.channel_name.container)
1820                        .aligned()
1821                        .left()
1822                        .flex(1., true),
1823                )
1824                .with_child(
1825                    MouseEventHandler::new::<ChannelCall, _>(ix, cx, move |_, cx| {
1826                        let participants =
1827                            self.channel_store.read(cx).channel_participants(channel_id);
1828                        if !participants.is_empty() {
1829                            let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT);
1830
1831                            FacePile::new(theme.face_overlap)
1832                                .with_children(
1833                                    participants
1834                                        .iter()
1835                                        .filter_map(|user| {
1836                                            Some(
1837                                                Image::from_data(user.avatar.clone()?)
1838                                                    .with_style(theme.channel_avatar),
1839                                            )
1840                                        })
1841                                        .take(FACEPILE_LIMIT),
1842                                )
1843                                .with_children((extra_count > 0).then(|| {
1844                                    Label::new(
1845                                        format!("+{}", extra_count),
1846                                        theme.extra_participant_label.text.clone(),
1847                                    )
1848                                    .contained()
1849                                    .with_style(theme.extra_participant_label.container)
1850                                }))
1851                                .into_any()
1852                        } else if row_hovered {
1853                            Svg::new("icons/speaker-loud.svg")
1854                                .with_color(theme.channel_hash.color)
1855                                .constrained()
1856                                .with_width(theme.channel_hash.width)
1857                                .into_any()
1858                        } else {
1859                            Empty::new().into_any()
1860                        }
1861                    })
1862                    .on_click(MouseButton::Left, move |_, this, cx| {
1863                        this.join_channel_call(channel_id, cx);
1864                    }),
1865                )
1866                .align_children_center()
1867                .styleable_component()
1868                .disclosable(
1869                    disclosed,
1870                    Box::new(ToggleCollapse {
1871                        location: path.clone(),
1872                    }),
1873                )
1874                .with_id(ix)
1875                .with_style(theme.disclosure.clone())
1876                .element()
1877                .constrained()
1878                .with_height(theme.row_height)
1879                .contained()
1880                .with_style(select_state(
1881                    theme
1882                        .channel_row
1883                        .in_state(is_selected || is_active || is_dragged_over),
1884                ))
1885                .with_padding_left(
1886                    theme.channel_row.default_style().padding.left
1887                        + theme.channel_indent * depth as f32,
1888                )
1889        })
1890        .on_click(MouseButton::Left, move |_, this, cx| {
1891            if this.drag_target_channel.take().is_none() {
1892                this.join_channel_chat(channel_id, cx);
1893            }
1894        })
1895        .on_click(MouseButton::Right, {
1896            let path = path.clone();
1897            move |e, this, cx| {
1898                this.deploy_channel_context_menu(Some(e.position), &path, ix, cx);
1899            }
1900        })
1901        .on_up(MouseButton::Left, move |e, this, cx| {
1902            if let Some((_, dragged_channel)) = cx
1903                .global::<DragAndDrop<Workspace>>()
1904                .currently_dragged::<DraggedChannel>(cx.window())
1905            {
1906                if e.modifiers.alt {
1907                    this.channel_store.update(cx, |channel_store, cx| {
1908                        channel_store
1909                            .link_channel(dragged_channel.0.id, channel_id, cx)
1910                            .detach_and_log_err(cx)
1911                    })
1912                } else {
1913                    this.channel_store.update(cx, |channel_store, cx| {
1914                        match dragged_channel.1 {
1915                            Some(parent_id) => channel_store.move_channel(
1916                                dragged_channel.0.id,
1917                                parent_id,
1918                                channel_id,
1919                                cx,
1920                            ),
1921                            None => {
1922                                channel_store.link_channel(dragged_channel.0.id, channel_id, cx)
1923                            }
1924                        }
1925                        .detach_and_log_err(cx)
1926                    })
1927                }
1928            }
1929        })
1930        .on_move({
1931            let channel = channel.clone();
1932            let path = path.clone();
1933            move |_, this, cx| {
1934                if let Some((_, _dragged_channel)) =
1935                    cx.global::<DragAndDrop<Workspace>>()
1936                        .currently_dragged::<DraggedChannel>(cx.window())
1937                {
1938                    match &this.drag_target_channel {
1939                        Some(current_target)
1940                            if current_target.0 == channel && current_target.1 == path =>
1941                        {
1942                            return
1943                        }
1944                        _ => {
1945                            this.drag_target_channel = Some((channel.clone(), path.clone()));
1946                            cx.notify();
1947                        }
1948                    }
1949                }
1950            }
1951        })
1952        .as_draggable(
1953            (channel.clone(), path.parent_id()),
1954            move |modifiers, (channel, _), cx: &mut ViewContext<Workspace>| {
1955                let theme = &theme::current(cx).collab_panel;
1956
1957                Flex::<Workspace>::row()
1958                    .with_children(modifiers.alt.then(|| {
1959                        Svg::new("icons/plus.svg")
1960                            .with_color(theme.channel_hash.color)
1961                            .constrained()
1962                            .with_width(theme.channel_hash.width)
1963                            .aligned()
1964                            .left()
1965                    }))
1966                    .with_child(
1967                        Svg::new("icons/hash.svg")
1968                            .with_color(theme.channel_hash.color)
1969                            .constrained()
1970                            .with_width(theme.channel_hash.width)
1971                            .aligned()
1972                            .left(),
1973                    )
1974                    .with_child(
1975                        Label::new(channel.name.clone(), theme.channel_name.text.clone())
1976                            .contained()
1977                            .with_style(theme.channel_name.container)
1978                            .aligned()
1979                            .left(),
1980                    )
1981                    .align_children_center()
1982                    .contained()
1983                    .with_background_color(
1984                        theme
1985                            .container
1986                            .background_color
1987                            .unwrap_or(gpui::color::Color::transparent_black()),
1988                    )
1989                    .contained()
1990                    .with_padding_left(
1991                        theme.channel_row.default_style().padding.left
1992                            + theme.channel_indent * depth as f32,
1993                    )
1994                    .into_any()
1995            },
1996        )
1997        .with_cursor_style(CursorStyle::PointingHand)
1998        .into_any()
1999    }
2000
2001    fn render_channel_notes(
2002        &self,
2003        channel_id: ChannelId,
2004        theme: &theme::CollabPanel,
2005        is_selected: bool,
2006        cx: &mut ViewContext<Self>,
2007    ) -> AnyElement<Self> {
2008        enum ChannelNotes {}
2009        let host_avatar_width = theme
2010            .contact_avatar
2011            .width
2012            .or(theme.contact_avatar.height)
2013            .unwrap_or(0.);
2014
2015        MouseEventHandler::new::<ChannelNotes, _>(channel_id as usize, cx, |state, cx| {
2016            let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
2017            let row = theme.project_row.in_state(is_selected).style_for(state);
2018
2019            Flex::<Self>::row()
2020                .with_child(render_tree_branch(
2021                    tree_branch,
2022                    &row.name.text,
2023                    true,
2024                    vec2f(host_avatar_width, theme.row_height),
2025                    cx.font_cache(),
2026                ))
2027                .with_child(
2028                    Svg::new("icons/file.svg")
2029                        .with_color(theme.channel_hash.color)
2030                        .constrained()
2031                        .with_width(theme.channel_hash.width)
2032                        .aligned()
2033                        .left(),
2034                )
2035                .with_child(
2036                    Label::new("notes", theme.channel_name.text.clone())
2037                        .contained()
2038                        .with_style(theme.channel_name.container)
2039                        .aligned()
2040                        .left()
2041                        .flex(1., true),
2042                )
2043                .constrained()
2044                .with_height(theme.row_height)
2045                .contained()
2046                .with_style(*theme.channel_row.style_for(is_selected, state))
2047                .with_padding_left(theme.channel_row.default_style().padding.left)
2048        })
2049        .on_click(MouseButton::Left, move |_, this, cx| {
2050            this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
2051        })
2052        .with_cursor_style(CursorStyle::PointingHand)
2053        .into_any()
2054    }
2055
2056    fn render_channel_invite(
2057        channel: Arc<Channel>,
2058        channel_store: ModelHandle<ChannelStore>,
2059        theme: &theme::CollabPanel,
2060        is_selected: bool,
2061        cx: &mut ViewContext<Self>,
2062    ) -> AnyElement<Self> {
2063        enum Decline {}
2064        enum Accept {}
2065
2066        let channel_id = channel.id;
2067        let is_invite_pending = channel_store
2068            .read(cx)
2069            .has_pending_channel_invite_response(&channel);
2070        let button_spacing = theme.contact_button_spacing;
2071
2072        Flex::row()
2073            .with_child(
2074                Svg::new("icons/hash.svg")
2075                    .with_color(theme.channel_hash.color)
2076                    .constrained()
2077                    .with_width(theme.channel_hash.width)
2078                    .aligned()
2079                    .left(),
2080            )
2081            .with_child(
2082                Label::new(channel.name.clone(), theme.contact_username.text.clone())
2083                    .contained()
2084                    .with_style(theme.contact_username.container)
2085                    .aligned()
2086                    .left()
2087                    .flex(1., true),
2088            )
2089            .with_child(
2090                MouseEventHandler::new::<Decline, _>(channel.id as usize, cx, |mouse_state, _| {
2091                    let button_style = if is_invite_pending {
2092                        &theme.disabled_button
2093                    } else {
2094                        theme.contact_button.style_for(mouse_state)
2095                    };
2096                    render_icon_button(button_style, "icons/x.svg").aligned()
2097                })
2098                .with_cursor_style(CursorStyle::PointingHand)
2099                .on_click(MouseButton::Left, move |_, this, cx| {
2100                    this.respond_to_channel_invite(channel_id, false, cx);
2101                })
2102                .contained()
2103                .with_margin_right(button_spacing),
2104            )
2105            .with_child(
2106                MouseEventHandler::new::<Accept, _>(channel.id as usize, cx, |mouse_state, _| {
2107                    let button_style = if is_invite_pending {
2108                        &theme.disabled_button
2109                    } else {
2110                        theme.contact_button.style_for(mouse_state)
2111                    };
2112                    render_icon_button(button_style, "icons/check.svg")
2113                        .aligned()
2114                        .flex_float()
2115                })
2116                .with_cursor_style(CursorStyle::PointingHand)
2117                .on_click(MouseButton::Left, move |_, this, cx| {
2118                    this.respond_to_channel_invite(channel_id, true, cx);
2119                }),
2120            )
2121            .constrained()
2122            .with_height(theme.row_height)
2123            .contained()
2124            .with_style(
2125                *theme
2126                    .contact_row
2127                    .in_state(is_selected)
2128                    .style_for(&mut Default::default()),
2129            )
2130            .with_padding_left(
2131                theme.contact_row.default_style().padding.left + theme.channel_indent,
2132            )
2133            .into_any()
2134    }
2135
2136    fn render_contact_request(
2137        user: Arc<User>,
2138        user_store: ModelHandle<UserStore>,
2139        theme: &theme::CollabPanel,
2140        is_incoming: bool,
2141        is_selected: bool,
2142        cx: &mut ViewContext<Self>,
2143    ) -> AnyElement<Self> {
2144        enum Decline {}
2145        enum Accept {}
2146        enum Cancel {}
2147
2148        let mut row = Flex::row()
2149            .with_children(user.avatar.clone().map(|avatar| {
2150                Image::from_data(avatar)
2151                    .with_style(theme.contact_avatar)
2152                    .aligned()
2153                    .left()
2154            }))
2155            .with_child(
2156                Label::new(
2157                    user.github_login.clone(),
2158                    theme.contact_username.text.clone(),
2159                )
2160                .contained()
2161                .with_style(theme.contact_username.container)
2162                .aligned()
2163                .left()
2164                .flex(1., true),
2165            );
2166
2167        let user_id = user.id;
2168        let github_login = user.github_login.clone();
2169        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
2170        let button_spacing = theme.contact_button_spacing;
2171
2172        if is_incoming {
2173            row.add_child(
2174                MouseEventHandler::new::<Decline, _>(user.id as usize, cx, |mouse_state, _| {
2175                    let button_style = if is_contact_request_pending {
2176                        &theme.disabled_button
2177                    } else {
2178                        theme.contact_button.style_for(mouse_state)
2179                    };
2180                    render_icon_button(button_style, "icons/x.svg").aligned()
2181                })
2182                .with_cursor_style(CursorStyle::PointingHand)
2183                .on_click(MouseButton::Left, move |_, this, cx| {
2184                    this.respond_to_contact_request(user_id, false, cx);
2185                })
2186                .contained()
2187                .with_margin_right(button_spacing),
2188            );
2189
2190            row.add_child(
2191                MouseEventHandler::new::<Accept, _>(user.id as usize, cx, |mouse_state, _| {
2192                    let button_style = if is_contact_request_pending {
2193                        &theme.disabled_button
2194                    } else {
2195                        theme.contact_button.style_for(mouse_state)
2196                    };
2197                    render_icon_button(button_style, "icons/check.svg")
2198                        .aligned()
2199                        .flex_float()
2200                })
2201                .with_cursor_style(CursorStyle::PointingHand)
2202                .on_click(MouseButton::Left, move |_, this, cx| {
2203                    this.respond_to_contact_request(user_id, true, cx);
2204                }),
2205            );
2206        } else {
2207            row.add_child(
2208                MouseEventHandler::new::<Cancel, _>(user.id as usize, cx, |mouse_state, _| {
2209                    let button_style = if is_contact_request_pending {
2210                        &theme.disabled_button
2211                    } else {
2212                        theme.contact_button.style_for(mouse_state)
2213                    };
2214                    render_icon_button(button_style, "icons/x.svg")
2215                        .aligned()
2216                        .flex_float()
2217                })
2218                .with_padding(Padding::uniform(2.))
2219                .with_cursor_style(CursorStyle::PointingHand)
2220                .on_click(MouseButton::Left, move |_, this, cx| {
2221                    this.remove_contact(user_id, &github_login, cx);
2222                })
2223                .flex_float(),
2224            );
2225        }
2226
2227        row.constrained()
2228            .with_height(theme.row_height)
2229            .contained()
2230            .with_style(
2231                *theme
2232                    .contact_row
2233                    .in_state(is_selected)
2234                    .style_for(&mut Default::default()),
2235            )
2236            .into_any()
2237    }
2238
2239    fn has_subchannels(&self, ix: usize) -> bool {
2240        self.entries
2241            .get(ix)
2242            .zip(self.entries.get(ix + 1))
2243            .map(|entries| match entries {
2244                (
2245                    ListEntry::Channel {
2246                        path: this_path, ..
2247                    },
2248                    ListEntry::Channel {
2249                        path: next_path, ..
2250                    },
2251                ) => next_path.starts_with(this_path),
2252                _ => false,
2253            })
2254            .unwrap_or(false)
2255    }
2256
2257    fn deploy_channel_context_menu(
2258        &mut self,
2259        position: Option<Vector2F>,
2260        path: &ChannelPath,
2261        ix: usize,
2262        cx: &mut ViewContext<Self>,
2263    ) {
2264        self.context_menu_on_selected = position.is_none();
2265
2266        let channel_name = self.channel_clipboard.as_ref().and_then(|channel| {
2267            let channel_name = self
2268                .channel_store
2269                .read(cx)
2270                .channel_for_id(channel.channel_id)
2271                .map(|channel| channel.name.clone())?;
2272            Some(channel_name)
2273        });
2274
2275        self.context_menu.update(cx, |context_menu, cx| {
2276            context_menu.set_position_mode(if self.context_menu_on_selected {
2277                OverlayPositionMode::Local
2278            } else {
2279                OverlayPositionMode::Window
2280            });
2281
2282            let mut items = Vec::new();
2283
2284            let select_action_name = if self.selection == Some(ix) {
2285                "Unselect"
2286            } else {
2287                "Select"
2288            };
2289
2290            items.push(ContextMenuItem::action(
2291                select_action_name,
2292                ToggleSelectedIx { ix },
2293            ));
2294
2295            if self.has_subchannels(ix) {
2296                let expand_action_name = if self.is_channel_collapsed(&path) {
2297                    "Expand Subchannels"
2298                } else {
2299                    "Collapse Subchannels"
2300                };
2301                items.push(ContextMenuItem::action(
2302                    expand_action_name,
2303                    ToggleCollapse {
2304                        location: path.clone(),
2305                    },
2306                ));
2307            }
2308
2309            items.push(ContextMenuItem::action(
2310                "Open Notes",
2311                OpenChannelNotes {
2312                    channel_id: path.channel_id(),
2313                },
2314            ));
2315
2316            if self.channel_store.read(cx).is_user_admin(path.channel_id()) {
2317                let parent_id = path.parent_id();
2318
2319                items.extend([
2320                    ContextMenuItem::Separator,
2321                    ContextMenuItem::action(
2322                        "New Subchannel",
2323                        NewChannel {
2324                            location: path.clone(),
2325                        },
2326                    ),
2327                    ContextMenuItem::action(
2328                        "Rename",
2329                        RenameChannel {
2330                            location: path.clone(),
2331                        },
2332                    ),
2333                    ContextMenuItem::Separator,
2334                ]);
2335
2336                if let Some(parent_id) = parent_id {
2337                    items.push(ContextMenuItem::action(
2338                        "Unlink from parent",
2339                        UnlinkChannel {
2340                            channel_id: path.channel_id(),
2341                            parent_id,
2342                        },
2343                    ));
2344                }
2345
2346                items.extend([
2347                    ContextMenuItem::action(
2348                        "Move this channel",
2349                        StartMoveChannelFor {
2350                            channel_id: path.channel_id(),
2351                            parent_id,
2352                        },
2353                    ),
2354                    ContextMenuItem::action(
2355                        "Link this channel",
2356                        StartLinkChannelFor {
2357                            channel_id: path.channel_id(),
2358                            parent_id,
2359                        },
2360                    ),
2361                ]);
2362
2363                if let Some(channel_name) = channel_name {
2364                    items.push(ContextMenuItem::Separator);
2365                    items.push(ContextMenuItem::action(
2366                        format!("Move '#{}' here", channel_name),
2367                        MoveChannel {
2368                            to: path.channel_id(),
2369                        },
2370                    ));
2371                    items.push(ContextMenuItem::action(
2372                        format!("Link '#{}' here", channel_name),
2373                        LinkChannel {
2374                            to: path.channel_id(),
2375                        },
2376                    ));
2377                }
2378
2379                items.extend([
2380                    ContextMenuItem::Separator,
2381                    ContextMenuItem::action(
2382                        "Invite Members",
2383                        InviteMembers {
2384                            channel_id: path.channel_id(),
2385                        },
2386                    ),
2387                    ContextMenuItem::action(
2388                        "Manage Members",
2389                        ManageMembers {
2390                            channel_id: path.channel_id(),
2391                        },
2392                    ),
2393                    ContextMenuItem::Separator,
2394                    ContextMenuItem::action(
2395                        "Delete",
2396                        RemoveChannel {
2397                            channel_id: path.channel_id(),
2398                        },
2399                    ),
2400                ]);
2401            }
2402
2403            context_menu.show(
2404                position.unwrap_or_default(),
2405                if self.context_menu_on_selected {
2406                    gpui::elements::AnchorCorner::TopRight
2407                } else {
2408                    gpui::elements::AnchorCorner::BottomLeft
2409                },
2410                items,
2411                cx,
2412            );
2413        });
2414
2415        cx.notify();
2416    }
2417
2418    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
2419        if self.take_editing_state(cx) {
2420            cx.focus(&self.filter_editor);
2421        } else {
2422            self.filter_editor.update(cx, |editor, cx| {
2423                if editor.buffer().read(cx).len(cx) > 0 {
2424                    editor.set_text("", cx);
2425                }
2426            });
2427        }
2428
2429        self.update_entries(false, cx);
2430    }
2431
2432    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
2433        let ix = self.selection.map_or(0, |ix| ix + 1);
2434        if ix < self.entries.len() {
2435            self.selection = Some(ix);
2436        }
2437
2438        self.list_state.reset(self.entries.len());
2439        if let Some(ix) = self.selection {
2440            self.list_state.scroll_to(ListOffset {
2441                item_ix: ix,
2442                offset_in_item: 0.,
2443            });
2444        }
2445        cx.notify();
2446    }
2447
2448    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
2449        let ix = self.selection.take().unwrap_or(0);
2450        if ix > 0 {
2451            self.selection = Some(ix - 1);
2452        }
2453
2454        self.list_state.reset(self.entries.len());
2455        if let Some(ix) = self.selection {
2456            self.list_state.scroll_to(ListOffset {
2457                item_ix: ix,
2458                offset_in_item: 0.,
2459            });
2460        }
2461        cx.notify();
2462    }
2463
2464    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
2465        if self.confirm_channel_edit(cx) {
2466            return;
2467        }
2468
2469        if let Some(selection) = self.selection {
2470            if let Some(entry) = self.entries.get(selection) {
2471                match entry {
2472                    ListEntry::Header(section) => match section {
2473                        Section::ActiveCall => Self::leave_call(cx),
2474                        Section::Channels => self.new_root_channel(cx),
2475                        Section::Contacts => self.toggle_contact_finder(cx),
2476                        Section::ContactRequests
2477                        | Section::Online
2478                        | Section::Offline
2479                        | Section::ChannelInvites => {
2480                            self.toggle_section_expanded(*section, cx);
2481                        }
2482                    },
2483                    ListEntry::Contact { contact, calling } => {
2484                        if contact.online && !contact.busy && !calling {
2485                            self.call(contact.user.id, Some(self.project.clone()), cx);
2486                        }
2487                    }
2488                    ListEntry::ParticipantProject {
2489                        project_id,
2490                        host_user_id,
2491                        ..
2492                    } => {
2493                        if let Some(workspace) = self.workspace.upgrade(cx) {
2494                            let app_state = workspace.read(cx).app_state().clone();
2495                            workspace::join_remote_project(
2496                                *project_id,
2497                                *host_user_id,
2498                                app_state,
2499                                cx,
2500                            )
2501                            .detach_and_log_err(cx);
2502                        }
2503                    }
2504                    ListEntry::ParticipantScreen { peer_id, .. } => {
2505                        if let Some(workspace) = self.workspace.upgrade(cx) {
2506                            workspace.update(cx, |workspace, cx| {
2507                                workspace.open_shared_screen(*peer_id, cx)
2508                            });
2509                        }
2510                    }
2511                    ListEntry::Channel { channel, .. } => {
2512                        self.join_channel_chat(channel.id, cx);
2513                    }
2514                    ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
2515                    _ => {}
2516                }
2517            }
2518        }
2519    }
2520
2521    fn confirm_channel_edit(&mut self, cx: &mut ViewContext<CollabPanel>) -> bool {
2522        if let Some(editing_state) = &mut self.channel_editing_state {
2523            match editing_state {
2524                ChannelEditingState::Create {
2525                    location,
2526                    pending_name,
2527                    ..
2528                } => {
2529                    if pending_name.is_some() {
2530                        return false;
2531                    }
2532                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2533
2534                    *pending_name = Some(channel_name.clone());
2535
2536                    self.channel_store
2537                        .update(cx, |channel_store, cx| {
2538                            channel_store.create_channel(
2539                                &channel_name,
2540                                location.as_ref().map(|location| location.channel_id()),
2541                                cx,
2542                            )
2543                        })
2544                        .detach();
2545                    cx.notify();
2546                }
2547                ChannelEditingState::Rename {
2548                    location,
2549                    pending_name,
2550                } => {
2551                    if pending_name.is_some() {
2552                        return false;
2553                    }
2554                    let channel_name = self.channel_name_editor.read(cx).text(cx);
2555                    *pending_name = Some(channel_name.clone());
2556
2557                    self.channel_store
2558                        .update(cx, |channel_store, cx| {
2559                            channel_store.rename(location.channel_id(), &channel_name, cx)
2560                        })
2561                        .detach();
2562                    cx.notify();
2563                }
2564            }
2565            cx.focus_self();
2566            true
2567        } else {
2568            false
2569        }
2570    }
2571
2572    fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
2573        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
2574            self.collapsed_sections.remove(ix);
2575        } else {
2576            self.collapsed_sections.push(section);
2577        }
2578        self.update_entries(false, cx);
2579    }
2580
2581    fn collapse_selected_channel(
2582        &mut self,
2583        _: &CollapseSelectedChannel,
2584        cx: &mut ViewContext<Self>,
2585    ) {
2586        let Some((_, path)) = self
2587            .selected_channel()
2588            .map(|(channel, parent)| (channel.id, parent))
2589        else {
2590            return;
2591        };
2592
2593        if self.is_channel_collapsed(&path) {
2594            return;
2595        }
2596
2597        self.toggle_channel_collapsed(&path.clone(), cx);
2598    }
2599
2600    fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
2601        let Some((_, path)) = self
2602            .selected_channel()
2603            .map(|(channel, parent)| (channel.id, parent))
2604        else {
2605            return;
2606        };
2607
2608        if !self.is_channel_collapsed(&path) {
2609            return;
2610        }
2611
2612        self.toggle_channel_collapsed(path.to_owned(), cx)
2613    }
2614
2615    fn toggle_channel_collapsed_action(
2616        &mut self,
2617        action: &ToggleCollapse,
2618        cx: &mut ViewContext<Self>,
2619    ) {
2620        self.toggle_channel_collapsed(&action.location, cx);
2621    }
2622
2623    fn toggle_channel_collapsed<'a>(
2624        &mut self,
2625        path: impl Into<Cow<'a, ChannelPath>>,
2626        cx: &mut ViewContext<Self>,
2627    ) {
2628        let path = path.into();
2629        match self.collapsed_channels.binary_search(&path) {
2630            Ok(ix) => {
2631                self.collapsed_channels.remove(ix);
2632            }
2633            Err(ix) => {
2634                self.collapsed_channels.insert(ix, path.into_owned());
2635            }
2636        };
2637        self.serialize(cx);
2638        self.update_entries(true, cx);
2639        cx.notify();
2640        cx.focus_self();
2641    }
2642
2643    fn is_channel_collapsed(&self, path: &ChannelPath) -> bool {
2644        self.collapsed_channels.binary_search(path).is_ok()
2645    }
2646
2647    fn leave_call(cx: &mut ViewContext<Self>) {
2648        ActiveCall::global(cx)
2649            .update(cx, |call, cx| call.hang_up(cx))
2650            .detach_and_log_err(cx);
2651    }
2652
2653    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
2654        if let Some(workspace) = self.workspace.upgrade(cx) {
2655            workspace.update(cx, |workspace, cx| {
2656                workspace.toggle_modal(cx, |_, cx| {
2657                    cx.add_view(|cx| {
2658                        let mut finder = ContactFinder::new(self.user_store.clone(), cx);
2659                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
2660                        finder
2661                    })
2662                });
2663            });
2664        }
2665    }
2666
2667    fn new_root_channel(&mut self, cx: &mut ViewContext<Self>) {
2668        self.channel_editing_state = Some(ChannelEditingState::Create {
2669            location: None,
2670            pending_name: None,
2671        });
2672        self.update_entries(false, cx);
2673        self.select_channel_editor();
2674        cx.focus(self.channel_name_editor.as_any());
2675        cx.notify();
2676    }
2677
2678    fn select_channel_editor(&mut self) {
2679        self.selection = self.entries.iter().position(|entry| match entry {
2680            ListEntry::ChannelEditor { .. } => true,
2681            _ => false,
2682        });
2683    }
2684
2685    fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext<Self>) {
2686        self.collapsed_channels
2687            .retain(|channel| *channel != action.location);
2688        self.channel_editing_state = Some(ChannelEditingState::Create {
2689            location: Some(action.location.to_owned()),
2690            pending_name: None,
2691        });
2692        self.update_entries(false, cx);
2693        self.select_channel_editor();
2694        cx.focus(self.channel_name_editor.as_any());
2695        cx.notify();
2696    }
2697
2698    fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext<Self>) {
2699        self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx);
2700    }
2701
2702    fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext<Self>) {
2703        self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx);
2704    }
2705
2706    fn remove(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
2707        if let Some((channel, _)) = self.selected_channel() {
2708            self.remove_channel(channel.id, cx)
2709        }
2710    }
2711
2712    fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
2713        if let Some((_, parent)) = self.selected_channel() {
2714            self.rename_channel(
2715                &RenameChannel {
2716                    location: parent.to_owned(),
2717                },
2718                cx,
2719            );
2720        }
2721    }
2722
2723    fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext<Self>) {
2724        let channel_store = self.channel_store.read(cx);
2725        if !channel_store.is_user_admin(action.location.channel_id()) {
2726            return;
2727        }
2728        if let Some(channel) = channel_store
2729            .channel_for_id(action.location.channel_id())
2730            .cloned()
2731        {
2732            self.channel_editing_state = Some(ChannelEditingState::Rename {
2733                location: action.location.to_owned(),
2734                pending_name: None,
2735            });
2736            self.channel_name_editor.update(cx, |editor, cx| {
2737                editor.set_text(channel.name.clone(), cx);
2738                editor.select_all(&Default::default(), cx);
2739            });
2740            cx.focus(self.channel_name_editor.as_any());
2741            self.update_entries(false, cx);
2742            self.select_channel_editor();
2743        }
2744    }
2745
2746    fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext<Self>) {
2747        if let Some(workspace) = self.workspace.upgrade(cx) {
2748            ChannelView::deploy(action.channel_id, workspace, cx);
2749        }
2750    }
2751
2752    fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
2753        let Some((_, path)) = self.selected_channel() else {
2754            return;
2755        };
2756
2757        self.deploy_channel_context_menu(None, &path.to_owned(), self.selection.unwrap(), cx);
2758    }
2759
2760    fn selected_channel(&self) -> Option<(&Arc<Channel>, &ChannelPath)> {
2761        self.selection
2762            .and_then(|ix| self.entries.get(ix))
2763            .and_then(|entry| match entry {
2764                ListEntry::Channel {
2765                    channel,
2766                    path: parent,
2767                    ..
2768                } => Some((channel, parent)),
2769                _ => None,
2770            })
2771    }
2772
2773    fn show_channel_modal(
2774        &mut self,
2775        channel_id: ChannelId,
2776        mode: channel_modal::Mode,
2777        cx: &mut ViewContext<Self>,
2778    ) {
2779        let workspace = self.workspace.clone();
2780        let user_store = self.user_store.clone();
2781        let channel_store = self.channel_store.clone();
2782        let members = self.channel_store.update(cx, |channel_store, cx| {
2783            channel_store.get_channel_member_details(channel_id, cx)
2784        });
2785
2786        cx.spawn(|_, mut cx| async move {
2787            let members = members.await?;
2788            workspace.update(&mut cx, |workspace, cx| {
2789                workspace.toggle_modal(cx, |_, cx| {
2790                    cx.add_view(|cx| {
2791                        ChannelModal::new(
2792                            user_store.clone(),
2793                            channel_store.clone(),
2794                            channel_id,
2795                            mode,
2796                            members,
2797                            cx,
2798                        )
2799                    })
2800                });
2801            })
2802        })
2803        .detach();
2804    }
2805
2806    fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
2807        self.remove_channel(action.channel_id, cx)
2808    }
2809
2810    fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
2811        let channel_store = self.channel_store.clone();
2812        if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) {
2813            let prompt_message = format!(
2814                "Are you sure you want to remove the channel \"{}\"?",
2815                channel.name
2816            );
2817            let mut answer =
2818                cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2819            let window = cx.window();
2820            cx.spawn(|this, mut cx| async move {
2821                if answer.next().await == Some(0) {
2822                    if let Err(e) = channel_store
2823                        .update(&mut cx, |channels, _| channels.remove_channel(channel_id))
2824                        .await
2825                    {
2826                        window.prompt(
2827                            PromptLevel::Info,
2828                            &format!("Failed to remove channel: {}", e),
2829                            &["Ok"],
2830                            &mut cx,
2831                        );
2832                    }
2833                    this.update(&mut cx, |_, cx| cx.focus_self()).ok();
2834                }
2835            })
2836            .detach();
2837        }
2838    }
2839
2840    // Should move to the filter editor if clicking on it
2841    // Should move selection to the channel editor if activating it
2842
2843    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
2844        let user_store = self.user_store.clone();
2845        let prompt_message = format!(
2846            "Are you sure you want to remove \"{}\" from your contacts?",
2847            github_login
2848        );
2849        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
2850        let window = cx.window();
2851        cx.spawn(|_, mut cx| async move {
2852            if answer.next().await == Some(0) {
2853                if let Err(e) = user_store
2854                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
2855                    .await
2856                {
2857                    window.prompt(
2858                        PromptLevel::Info,
2859                        &format!("Failed to remove contact: {}", e),
2860                        &["Ok"],
2861                        &mut cx,
2862                    );
2863                }
2864            }
2865        })
2866        .detach();
2867    }
2868
2869    fn respond_to_contact_request(
2870        &mut self,
2871        user_id: u64,
2872        accept: bool,
2873        cx: &mut ViewContext<Self>,
2874    ) {
2875        self.user_store
2876            .update(cx, |store, cx| {
2877                store.respond_to_contact_request(user_id, accept, cx)
2878            })
2879            .detach();
2880    }
2881
2882    fn respond_to_channel_invite(
2883        &mut self,
2884        channel_id: u64,
2885        accept: bool,
2886        cx: &mut ViewContext<Self>,
2887    ) {
2888        let respond = self.channel_store.update(cx, |store, _| {
2889            store.respond_to_channel_invite(channel_id, accept)
2890        });
2891        cx.foreground().spawn(respond).detach();
2892    }
2893
2894    fn call(
2895        &mut self,
2896        recipient_user_id: u64,
2897        initial_project: Option<ModelHandle<Project>>,
2898        cx: &mut ViewContext<Self>,
2899    ) {
2900        ActiveCall::global(cx)
2901            .update(cx, |call, cx| {
2902                call.invite(recipient_user_id, initial_project, cx)
2903            })
2904            .detach_and_log_err(cx);
2905    }
2906
2907    fn join_channel_call(&self, channel: u64, cx: &mut ViewContext<Self>) {
2908        ActiveCall::global(cx)
2909            .update(cx, |call, cx| call.join_channel(channel, cx))
2910            .detach_and_log_err(cx);
2911    }
2912
2913    fn join_channel_chat(&mut self, channel_id: u64, cx: &mut ViewContext<Self>) {
2914        if let Some(workspace) = self.workspace.upgrade(cx) {
2915            cx.app_context().defer(move |cx| {
2916                workspace.update(cx, |workspace, cx| {
2917                    if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
2918                        panel.update(cx, |panel, cx| {
2919                            panel.select_channel(channel_id, cx).detach_and_log_err(cx);
2920                        });
2921                    }
2922                });
2923            });
2924        }
2925    }
2926}
2927
2928fn render_tree_branch(
2929    branch_style: theme::TreeBranch,
2930    row_style: &TextStyle,
2931    is_last: bool,
2932    size: Vector2F,
2933    font_cache: &FontCache,
2934) -> gpui::elements::ConstrainedBox<CollabPanel> {
2935    let line_height = row_style.line_height(font_cache);
2936    let cap_height = row_style.cap_height(font_cache);
2937    let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.;
2938
2939    Canvas::new(move |bounds, _, _, cx| {
2940        cx.paint_layer(None, |cx| {
2941            let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.);
2942            let end_x = bounds.max_x();
2943            let start_y = bounds.min_y();
2944            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
2945
2946            cx.scene().push_quad(gpui::Quad {
2947                bounds: RectF::from_points(
2948                    vec2f(start_x, start_y),
2949                    vec2f(
2950                        start_x + branch_style.width,
2951                        if is_last { end_y } else { bounds.max_y() },
2952                    ),
2953                ),
2954                background: Some(branch_style.color),
2955                border: gpui::Border::default(),
2956                corner_radii: (0.).into(),
2957            });
2958            cx.scene().push_quad(gpui::Quad {
2959                bounds: RectF::from_points(
2960                    vec2f(start_x, end_y),
2961                    vec2f(end_x, end_y + branch_style.width),
2962                ),
2963                background: Some(branch_style.color),
2964                border: gpui::Border::default(),
2965                corner_radii: (0.).into(),
2966            });
2967        })
2968    })
2969    .constrained()
2970    .with_width(size.x())
2971}
2972
2973impl View for CollabPanel {
2974    fn ui_name() -> &'static str {
2975        "CollabPanel"
2976    }
2977
2978    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
2979        if !self.has_focus {
2980            self.has_focus = true;
2981            if !self.context_menu.is_focused(cx) {
2982                if let Some(editing_state) = &self.channel_editing_state {
2983                    if editing_state.pending_name().is_none() {
2984                        cx.focus(&self.channel_name_editor);
2985                    } else {
2986                        cx.focus(&self.filter_editor);
2987                    }
2988                } else {
2989                    cx.focus(&self.filter_editor);
2990                }
2991            }
2992            cx.emit(Event::Focus);
2993        }
2994    }
2995
2996    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
2997        self.has_focus = false;
2998    }
2999
3000    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
3001        let theme = &theme::current(cx).collab_panel;
3002
3003        if self.user_store.read(cx).current_user().is_none() {
3004            enum LogInButton {}
3005
3006            return Flex::column()
3007                .with_child(
3008                    MouseEventHandler::new::<LogInButton, _>(0, cx, |state, _| {
3009                        let button = theme.log_in_button.style_for(state);
3010                        Label::new("Sign in to collaborate", button.text.clone())
3011                            .aligned()
3012                            .left()
3013                            .contained()
3014                            .with_style(button.container)
3015                    })
3016                    .on_click(MouseButton::Left, |_, this, cx| {
3017                        let client = this.client.clone();
3018                        cx.spawn(|_, cx| async move {
3019                            client.authenticate_and_connect(true, &cx).await.log_err();
3020                        })
3021                        .detach();
3022                    })
3023                    .with_cursor_style(CursorStyle::PointingHand),
3024                )
3025                .contained()
3026                .with_style(theme.container)
3027                .into_any();
3028        }
3029
3030        enum PanelFocus {}
3031        MouseEventHandler::new::<PanelFocus, _>(0, cx, |_, cx| {
3032            Stack::new()
3033                .with_child(
3034                    Flex::column()
3035                        .with_child(
3036                            Flex::row().with_child(
3037                                ChildView::new(&self.filter_editor, cx)
3038                                    .contained()
3039                                    .with_style(theme.user_query_editor.container)
3040                                    .flex(1.0, true),
3041                            ),
3042                        )
3043                        .with_child(List::new(self.list_state.clone()).flex(1., true).into_any())
3044                        .contained()
3045                        .with_style(theme.container)
3046                        .into_any(),
3047                )
3048                .with_children(
3049                    (!self.context_menu_on_selected)
3050                        .then(|| ChildView::new(&self.context_menu, cx)),
3051                )
3052                .into_any()
3053        })
3054        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
3055        .into_any_named("collab panel")
3056    }
3057}
3058
3059impl Panel for CollabPanel {
3060    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
3061        settings::get::<CollaborationPanelSettings>(cx).dock
3062    }
3063
3064    fn position_is_valid(&self, position: DockPosition) -> bool {
3065        matches!(position, DockPosition::Left | DockPosition::Right)
3066    }
3067
3068    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3069        settings::update_settings_file::<CollaborationPanelSettings>(
3070            self.fs.clone(),
3071            cx,
3072            move |settings| settings.dock = Some(position),
3073        );
3074    }
3075
3076    fn size(&self, cx: &gpui::WindowContext) -> f32 {
3077        self.width
3078            .unwrap_or_else(|| settings::get::<CollaborationPanelSettings>(cx).default_width)
3079    }
3080
3081    fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
3082        self.width = size;
3083        self.serialize(cx);
3084        cx.notify();
3085    }
3086
3087    fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> {
3088        settings::get::<CollaborationPanelSettings>(cx)
3089            .button
3090            .then(|| "icons/user_group_16.svg")
3091    }
3092
3093    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
3094        (
3095            "Collaboration Panel".to_string(),
3096            Some(Box::new(ToggleFocus)),
3097        )
3098    }
3099
3100    fn should_change_position_on_event(event: &Self::Event) -> bool {
3101        matches!(event, Event::DockPositionChanged)
3102    }
3103
3104    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
3105        self.has_focus
3106    }
3107
3108    fn is_focus_event(event: &Self::Event) -> bool {
3109        matches!(event, Event::Focus)
3110    }
3111}
3112
3113impl PartialEq for ListEntry {
3114    fn eq(&self, other: &Self) -> bool {
3115        match self {
3116            ListEntry::Header(section_1) => {
3117                if let ListEntry::Header(section_2) = other {
3118                    return section_1 == section_2;
3119                }
3120            }
3121            ListEntry::CallParticipant { user: user_1, .. } => {
3122                if let ListEntry::CallParticipant { user: user_2, .. } = other {
3123                    return user_1.id == user_2.id;
3124                }
3125            }
3126            ListEntry::ParticipantProject {
3127                project_id: project_id_1,
3128                ..
3129            } => {
3130                if let ListEntry::ParticipantProject {
3131                    project_id: project_id_2,
3132                    ..
3133                } = other
3134                {
3135                    return project_id_1 == project_id_2;
3136                }
3137            }
3138            ListEntry::ParticipantScreen {
3139                peer_id: peer_id_1, ..
3140            } => {
3141                if let ListEntry::ParticipantScreen {
3142                    peer_id: peer_id_2, ..
3143                } = other
3144                {
3145                    return peer_id_1 == peer_id_2;
3146                }
3147            }
3148            ListEntry::Channel {
3149                channel: channel_1,
3150                depth: depth_1,
3151                path: parent_1,
3152            } => {
3153                if let ListEntry::Channel {
3154                    channel: channel_2,
3155                    depth: depth_2,
3156                    path: parent_2,
3157                } = other
3158                {
3159                    return channel_1.id == channel_2.id
3160                        && depth_1 == depth_2
3161                        && parent_1 == parent_2;
3162                }
3163            }
3164            ListEntry::ChannelNotes { channel_id } => {
3165                if let ListEntry::ChannelNotes {
3166                    channel_id: other_id,
3167                } = other
3168                {
3169                    return channel_id == other_id;
3170                }
3171            }
3172            ListEntry::ChannelInvite(channel_1) => {
3173                if let ListEntry::ChannelInvite(channel_2) = other {
3174                    return channel_1.id == channel_2.id;
3175                }
3176            }
3177            ListEntry::IncomingRequest(user_1) => {
3178                if let ListEntry::IncomingRequest(user_2) = other {
3179                    return user_1.id == user_2.id;
3180                }
3181            }
3182            ListEntry::OutgoingRequest(user_1) => {
3183                if let ListEntry::OutgoingRequest(user_2) = other {
3184                    return user_1.id == user_2.id;
3185                }
3186            }
3187            ListEntry::Contact {
3188                contact: contact_1, ..
3189            } => {
3190                if let ListEntry::Contact {
3191                    contact: contact_2, ..
3192                } = other
3193                {
3194                    return contact_1.user.id == contact_2.user.id;
3195                }
3196            }
3197            ListEntry::ChannelEditor { depth } => {
3198                if let ListEntry::ChannelEditor { depth: other_depth } = other {
3199                    return depth == other_depth;
3200                }
3201            }
3202            ListEntry::ContactPlaceholder => {
3203                if let ListEntry::ContactPlaceholder = other {
3204                    return true;
3205                }
3206            }
3207        }
3208        false
3209    }
3210}
3211
3212fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
3213    Svg::new(svg_path)
3214        .with_color(style.color)
3215        .constrained()
3216        .with_width(style.icon_width)
3217        .aligned()
3218        .constrained()
3219        .with_width(style.button_width)
3220        .with_height(style.button_width)
3221        .contained()
3222        .with_style(style.container)
3223}