panel.rs

   1mod channel_modal;
   2mod contact_finder;
   3mod panel_settings;
   4
   5use anyhow::Result;
   6use call::ActiveCall;
   7use client::{proto::PeerId, Client, Contact, User, UserStore};
   8use contact_finder::build_contact_finder;
   9use context_menu::ContextMenu;
  10use db::kvp::KEY_VALUE_STORE;
  11use editor::{Cancel, Editor};
  12use futures::StreamExt;
  13use fuzzy::{match_strings, StringMatchCandidate};
  14use gpui::{
  15    actions,
  16    elements::{
  17        Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState,
  18        MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg,
  19    },
  20    geometry::{rect::RectF, vector::vec2f},
  21    platform::{CursorStyle, MouseButton, PromptLevel},
  22    serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle,
  23    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  24};
  25use menu::{Confirm, SelectNext, SelectPrev};
  26use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings};
  27use project::{Fs, Project};
  28use serde_derive::{Deserialize, Serialize};
  29use settings::SettingsStore;
  30use std::{mem, sync::Arc};
  31use theme::IconButton;
  32use util::{ResultExt, TryFutureExt};
  33use workspace::{
  34    dock::{DockPosition, Panel},
  35    Workspace,
  36};
  37
  38use self::channel_modal::ChannelModal;
  39
  40actions!(collab_panel, [ToggleFocus]);
  41
  42const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel";
  43
  44pub fn init(_client: Arc<Client>, cx: &mut AppContext) {
  45    settings::register::<panel_settings::ChannelsPanelSettings>(cx);
  46    contact_finder::init(cx);
  47    channel_modal::init(cx);
  48
  49    cx.add_action(CollabPanel::cancel);
  50    cx.add_action(CollabPanel::select_next);
  51    cx.add_action(CollabPanel::select_prev);
  52    cx.add_action(CollabPanel::confirm);
  53}
  54
  55pub struct CollabPanel {
  56    width: Option<f32>,
  57    fs: Arc<dyn Fs>,
  58    has_focus: bool,
  59    pending_serialization: Task<Option<()>>,
  60    context_menu: ViewHandle<ContextMenu>,
  61    filter_editor: ViewHandle<Editor>,
  62    entries: Vec<ContactEntry>,
  63    selection: Option<usize>,
  64    user_store: ModelHandle<UserStore>,
  65    project: ModelHandle<Project>,
  66    match_candidates: Vec<StringMatchCandidate>,
  67    list_state: ListState<Self>,
  68    subscriptions: Vec<Subscription>,
  69    collapsed_sections: Vec<Section>,
  70    workspace: WeakViewHandle<Workspace>,
  71}
  72
  73#[derive(Serialize, Deserialize)]
  74struct SerializedChannelsPanel {
  75    width: Option<f32>,
  76}
  77
  78#[derive(Debug)]
  79pub enum Event {
  80    DockPositionChanged,
  81    Focus,
  82    Dismissed,
  83}
  84
  85#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
  86enum Section {
  87    ActiveCall,
  88    Channels,
  89    Requests,
  90    Contacts,
  91    Online,
  92    Offline,
  93}
  94
  95#[derive(Clone)]
  96enum ContactEntry {
  97    Header(Section, usize),
  98    CallParticipant {
  99        user: Arc<User>,
 100        is_pending: bool,
 101    },
 102    ParticipantProject {
 103        project_id: u64,
 104        worktree_root_names: Vec<String>,
 105        host_user_id: u64,
 106        is_last: bool,
 107    },
 108    ParticipantScreen {
 109        peer_id: PeerId,
 110        is_last: bool,
 111    },
 112    IncomingRequest(Arc<User>),
 113    OutgoingRequest(Arc<User>),
 114    Contact {
 115        contact: Arc<Contact>,
 116        calling: bool,
 117    },
 118}
 119
 120impl Entity for CollabPanel {
 121    type Event = Event;
 122}
 123
 124impl CollabPanel {
 125    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
 126        cx.add_view::<Self, _>(|cx| {
 127            let view_id = cx.view_id();
 128
 129            let filter_editor = cx.add_view(|cx| {
 130                let mut editor = Editor::single_line(
 131                    Some(Arc::new(|theme| {
 132                        theme.collab_panel.user_query_editor.clone()
 133                    })),
 134                    cx,
 135                );
 136                editor.set_placeholder_text("Filter channels, contacts", cx);
 137                editor
 138            });
 139
 140            cx.subscribe(&filter_editor, |this, _, event, cx| {
 141                if let editor::Event::BufferEdited = event {
 142                    let query = this.filter_editor.read(cx).text(cx);
 143                    if !query.is_empty() {
 144                        this.selection.take();
 145                    }
 146                    this.update_entries(cx);
 147                    if !query.is_empty() {
 148                        this.selection = this
 149                            .entries
 150                            .iter()
 151                            .position(|entry| !matches!(entry, ContactEntry::Header(_, _)));
 152                    }
 153                }
 154            })
 155            .detach();
 156
 157            let list_state =
 158                ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
 159                    let theme = theme::current(cx).clone();
 160                    let is_selected = this.selection == Some(ix);
 161                    let current_project_id = this.project.read(cx).remote_id();
 162
 163                    match &this.entries[ix] {
 164                        ContactEntry::Header(section, depth) => {
 165                            let is_collapsed = this.collapsed_sections.contains(section);
 166                            Self::render_header(
 167                                *section,
 168                                &theme,
 169                                *depth,
 170                                is_selected,
 171                                is_collapsed,
 172                                cx,
 173                            )
 174                        }
 175                        ContactEntry::CallParticipant { user, is_pending } => {
 176                            Self::render_call_participant(
 177                                user,
 178                                *is_pending,
 179                                is_selected,
 180                                &theme.collab_panel,
 181                            )
 182                        }
 183                        ContactEntry::ParticipantProject {
 184                            project_id,
 185                            worktree_root_names,
 186                            host_user_id,
 187                            is_last,
 188                        } => Self::render_participant_project(
 189                            *project_id,
 190                            worktree_root_names,
 191                            *host_user_id,
 192                            Some(*project_id) == current_project_id,
 193                            *is_last,
 194                            is_selected,
 195                            &theme.collab_panel,
 196                            cx,
 197                        ),
 198                        ContactEntry::ParticipantScreen { peer_id, is_last } => {
 199                            Self::render_participant_screen(
 200                                *peer_id,
 201                                *is_last,
 202                                is_selected,
 203                                &theme.collab_panel,
 204                                cx,
 205                            )
 206                        }
 207                        ContactEntry::IncomingRequest(user) => Self::render_contact_request(
 208                            user.clone(),
 209                            this.user_store.clone(),
 210                            &theme.collab_panel,
 211                            true,
 212                            is_selected,
 213                            cx,
 214                        ),
 215                        ContactEntry::OutgoingRequest(user) => Self::render_contact_request(
 216                            user.clone(),
 217                            this.user_store.clone(),
 218                            &theme.collab_panel,
 219                            false,
 220                            is_selected,
 221                            cx,
 222                        ),
 223                        ContactEntry::Contact { contact, calling } => Self::render_contact(
 224                            contact,
 225                            *calling,
 226                            &this.project,
 227                            &theme.collab_panel,
 228                            is_selected,
 229                            cx,
 230                        ),
 231                    }
 232                });
 233
 234            let mut this = Self {
 235                width: None,
 236                has_focus: false,
 237                fs: workspace.app_state().fs.clone(),
 238                pending_serialization: Task::ready(None),
 239                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
 240                filter_editor,
 241                entries: Vec::default(),
 242                selection: None,
 243                user_store: workspace.user_store().clone(),
 244                project: workspace.project().clone(),
 245                subscriptions: Vec::default(),
 246                match_candidates: Vec::default(),
 247                collapsed_sections: Vec::default(),
 248                workspace: workspace.weak_handle(),
 249                list_state,
 250            };
 251            this.update_entries(cx);
 252
 253            // Update the dock position when the setting changes.
 254            let mut old_dock_position = this.position(cx);
 255            this.subscriptions
 256                .push(
 257                    cx.observe_global::<SettingsStore, _>(move |this: &mut CollabPanel, cx| {
 258                        let new_dock_position = this.position(cx);
 259                        if new_dock_position != old_dock_position {
 260                            old_dock_position = new_dock_position;
 261                            cx.emit(Event::DockPositionChanged);
 262                        }
 263                    }),
 264                );
 265
 266            let active_call = ActiveCall::global(cx);
 267            this.subscriptions
 268                .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx)));
 269            this.subscriptions
 270                .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
 271
 272            this
 273        })
 274    }
 275
 276    pub fn load(
 277        workspace: WeakViewHandle<Workspace>,
 278        cx: AsyncAppContext,
 279    ) -> Task<Result<ViewHandle<Self>>> {
 280        cx.spawn(|mut cx| async move {
 281            let serialized_panel = if let Some(panel) = cx
 282                .background()
 283                .spawn(async move { KEY_VALUE_STORE.read_kvp(CHANNELS_PANEL_KEY) })
 284                .await
 285                .log_err()
 286                .flatten()
 287            {
 288                Some(serde_json::from_str::<SerializedChannelsPanel>(&panel)?)
 289            } else {
 290                None
 291            };
 292
 293            workspace.update(&mut cx, |workspace, cx| {
 294                let panel = CollabPanel::new(workspace, cx);
 295                if let Some(serialized_panel) = serialized_panel {
 296                    panel.update(cx, |panel, cx| {
 297                        panel.width = serialized_panel.width;
 298                        cx.notify();
 299                    });
 300                }
 301                panel
 302            })
 303        })
 304    }
 305
 306    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
 307        let width = self.width;
 308        self.pending_serialization = cx.background().spawn(
 309            async move {
 310                KEY_VALUE_STORE
 311                    .write_kvp(
 312                        CHANNELS_PANEL_KEY.into(),
 313                        serde_json::to_string(&SerializedChannelsPanel { width })?,
 314                    )
 315                    .await?;
 316                anyhow::Ok(())
 317            }
 318            .log_err(),
 319        );
 320    }
 321
 322    fn update_entries(&mut self, cx: &mut ViewContext<Self>) {
 323        let user_store = self.user_store.read(cx);
 324        let query = self.filter_editor.read(cx).text(cx);
 325        let executor = cx.background().clone();
 326
 327        let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
 328        let old_entries = mem::take(&mut self.entries);
 329
 330        if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 331            let room = room.read(cx);
 332            let mut participant_entries = Vec::new();
 333
 334            // Populate the active user.
 335            if let Some(user) = user_store.current_user() {
 336                self.match_candidates.clear();
 337                self.match_candidates.push(StringMatchCandidate {
 338                    id: 0,
 339                    string: user.github_login.clone(),
 340                    char_bag: user.github_login.chars().collect(),
 341                });
 342                let matches = executor.block(match_strings(
 343                    &self.match_candidates,
 344                    &query,
 345                    true,
 346                    usize::MAX,
 347                    &Default::default(),
 348                    executor.clone(),
 349                ));
 350                if !matches.is_empty() {
 351                    let user_id = user.id;
 352                    participant_entries.push(ContactEntry::CallParticipant {
 353                        user,
 354                        is_pending: false,
 355                    });
 356                    let mut projects = room.local_participant().projects.iter().peekable();
 357                    while let Some(project) = projects.next() {
 358                        participant_entries.push(ContactEntry::ParticipantProject {
 359                            project_id: project.id,
 360                            worktree_root_names: project.worktree_root_names.clone(),
 361                            host_user_id: user_id,
 362                            is_last: projects.peek().is_none(),
 363                        });
 364                    }
 365                }
 366            }
 367
 368            // Populate remote participants.
 369            self.match_candidates.clear();
 370            self.match_candidates
 371                .extend(room.remote_participants().iter().map(|(_, participant)| {
 372                    StringMatchCandidate {
 373                        id: participant.user.id as usize,
 374                        string: participant.user.github_login.clone(),
 375                        char_bag: participant.user.github_login.chars().collect(),
 376                    }
 377                }));
 378            let matches = executor.block(match_strings(
 379                &self.match_candidates,
 380                &query,
 381                true,
 382                usize::MAX,
 383                &Default::default(),
 384                executor.clone(),
 385            ));
 386            for mat in matches {
 387                let user_id = mat.candidate_id as u64;
 388                let participant = &room.remote_participants()[&user_id];
 389                participant_entries.push(ContactEntry::CallParticipant {
 390                    user: participant.user.clone(),
 391                    is_pending: false,
 392                });
 393                let mut projects = participant.projects.iter().peekable();
 394                while let Some(project) = projects.next() {
 395                    participant_entries.push(ContactEntry::ParticipantProject {
 396                        project_id: project.id,
 397                        worktree_root_names: project.worktree_root_names.clone(),
 398                        host_user_id: participant.user.id,
 399                        is_last: projects.peek().is_none() && participant.video_tracks.is_empty(),
 400                    });
 401                }
 402                if !participant.video_tracks.is_empty() {
 403                    participant_entries.push(ContactEntry::ParticipantScreen {
 404                        peer_id: participant.peer_id,
 405                        is_last: true,
 406                    });
 407                }
 408            }
 409
 410            // Populate pending participants.
 411            self.match_candidates.clear();
 412            self.match_candidates
 413                .extend(
 414                    room.pending_participants()
 415                        .iter()
 416                        .enumerate()
 417                        .map(|(id, participant)| StringMatchCandidate {
 418                            id,
 419                            string: participant.github_login.clone(),
 420                            char_bag: participant.github_login.chars().collect(),
 421                        }),
 422                );
 423            let matches = executor.block(match_strings(
 424                &self.match_candidates,
 425                &query,
 426                true,
 427                usize::MAX,
 428                &Default::default(),
 429                executor.clone(),
 430            ));
 431            participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant {
 432                user: room.pending_participants()[mat.candidate_id].clone(),
 433                is_pending: true,
 434            }));
 435
 436            if !participant_entries.is_empty() {
 437                self.entries
 438                    .push(ContactEntry::Header(Section::ActiveCall, 0));
 439                if !self.collapsed_sections.contains(&Section::ActiveCall) {
 440                    self.entries.extend(participant_entries);
 441                }
 442            }
 443        }
 444
 445        self.entries
 446            .push(ContactEntry::Header(Section::Channels, 0));
 447
 448        self.entries
 449            .push(ContactEntry::Header(Section::Contacts, 0));
 450
 451        let mut request_entries = Vec::new();
 452        let incoming = user_store.incoming_contact_requests();
 453        if !incoming.is_empty() {
 454            self.match_candidates.clear();
 455            self.match_candidates
 456                .extend(
 457                    incoming
 458                        .iter()
 459                        .enumerate()
 460                        .map(|(ix, user)| StringMatchCandidate {
 461                            id: ix,
 462                            string: user.github_login.clone(),
 463                            char_bag: user.github_login.chars().collect(),
 464                        }),
 465                );
 466            let matches = executor.block(match_strings(
 467                &self.match_candidates,
 468                &query,
 469                true,
 470                usize::MAX,
 471                &Default::default(),
 472                executor.clone(),
 473            ));
 474            request_entries.extend(
 475                matches
 476                    .iter()
 477                    .map(|mat| ContactEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
 478            );
 479        }
 480
 481        let outgoing = user_store.outgoing_contact_requests();
 482        if !outgoing.is_empty() {
 483            self.match_candidates.clear();
 484            self.match_candidates
 485                .extend(
 486                    outgoing
 487                        .iter()
 488                        .enumerate()
 489                        .map(|(ix, user)| StringMatchCandidate {
 490                            id: ix,
 491                            string: user.github_login.clone(),
 492                            char_bag: user.github_login.chars().collect(),
 493                        }),
 494                );
 495            let matches = executor.block(match_strings(
 496                &self.match_candidates,
 497                &query,
 498                true,
 499                usize::MAX,
 500                &Default::default(),
 501                executor.clone(),
 502            ));
 503            request_entries.extend(
 504                matches
 505                    .iter()
 506                    .map(|mat| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
 507            );
 508        }
 509
 510        if !request_entries.is_empty() {
 511            self.entries
 512                .push(ContactEntry::Header(Section::Requests, 1));
 513            if !self.collapsed_sections.contains(&Section::Requests) {
 514                self.entries.append(&mut request_entries);
 515            }
 516        }
 517
 518        let contacts = user_store.contacts();
 519        if !contacts.is_empty() {
 520            self.match_candidates.clear();
 521            self.match_candidates
 522                .extend(
 523                    contacts
 524                        .iter()
 525                        .enumerate()
 526                        .map(|(ix, contact)| StringMatchCandidate {
 527                            id: ix,
 528                            string: contact.user.github_login.clone(),
 529                            char_bag: contact.user.github_login.chars().collect(),
 530                        }),
 531                );
 532
 533            let matches = executor.block(match_strings(
 534                &self.match_candidates,
 535                &query,
 536                true,
 537                usize::MAX,
 538                &Default::default(),
 539                executor.clone(),
 540            ));
 541
 542            let (mut online_contacts, offline_contacts) = matches
 543                .iter()
 544                .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
 545            if let Some(room) = ActiveCall::global(cx).read(cx).room() {
 546                let room = room.read(cx);
 547                online_contacts.retain(|contact| {
 548                    let contact = &contacts[contact.candidate_id];
 549                    !room.contains_participant(contact.user.id)
 550                });
 551            }
 552
 553            for (matches, section) in [
 554                (online_contacts, Section::Online),
 555                (offline_contacts, Section::Offline),
 556            ] {
 557                if !matches.is_empty() {
 558                    self.entries.push(ContactEntry::Header(section, 1));
 559                    if !self.collapsed_sections.contains(&section) {
 560                        let active_call = &ActiveCall::global(cx).read(cx);
 561                        for mat in matches {
 562                            let contact = &contacts[mat.candidate_id];
 563                            self.entries.push(ContactEntry::Contact {
 564                                contact: contact.clone(),
 565                                calling: active_call.pending_invites().contains(&contact.user.id),
 566                            });
 567                        }
 568                    }
 569                }
 570            }
 571        }
 572
 573        if let Some(prev_selected_entry) = prev_selected_entry {
 574            self.selection.take();
 575            for (ix, entry) in self.entries.iter().enumerate() {
 576                if *entry == prev_selected_entry {
 577                    self.selection = Some(ix);
 578                    break;
 579                }
 580            }
 581        }
 582
 583        let old_scroll_top = self.list_state.logical_scroll_top();
 584        self.list_state.reset(self.entries.len());
 585
 586        // Attempt to maintain the same scroll position.
 587        if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
 588            let new_scroll_top = self
 589                .entries
 590                .iter()
 591                .position(|entry| entry == old_top_entry)
 592                .map(|item_ix| ListOffset {
 593                    item_ix,
 594                    offset_in_item: old_scroll_top.offset_in_item,
 595                })
 596                .or_else(|| {
 597                    let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
 598                    let item_ix = self
 599                        .entries
 600                        .iter()
 601                        .position(|entry| entry == entry_after_old_top)?;
 602                    Some(ListOffset {
 603                        item_ix,
 604                        offset_in_item: 0.,
 605                    })
 606                })
 607                .or_else(|| {
 608                    let entry_before_old_top =
 609                        old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
 610                    let item_ix = self
 611                        .entries
 612                        .iter()
 613                        .position(|entry| entry == entry_before_old_top)?;
 614                    Some(ListOffset {
 615                        item_ix,
 616                        offset_in_item: 0.,
 617                    })
 618                });
 619
 620            self.list_state
 621                .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
 622        }
 623
 624        cx.notify();
 625    }
 626
 627    fn render_call_participant(
 628        user: &User,
 629        is_pending: bool,
 630        is_selected: bool,
 631        theme: &theme::CollabPanel,
 632    ) -> AnyElement<Self> {
 633        Flex::row()
 634            .with_children(user.avatar.clone().map(|avatar| {
 635                Image::from_data(avatar)
 636                    .with_style(theme.contact_avatar)
 637                    .aligned()
 638                    .left()
 639            }))
 640            .with_child(
 641                Label::new(
 642                    user.github_login.clone(),
 643                    theme.contact_username.text.clone(),
 644                )
 645                .contained()
 646                .with_style(theme.contact_username.container)
 647                .aligned()
 648                .left()
 649                .flex(1., true),
 650            )
 651            .with_children(if is_pending {
 652                Some(
 653                    Label::new("Calling", theme.calling_indicator.text.clone())
 654                        .contained()
 655                        .with_style(theme.calling_indicator.container)
 656                        .aligned(),
 657                )
 658            } else {
 659                None
 660            })
 661            .constrained()
 662            .with_height(theme.row_height)
 663            .contained()
 664            .with_style(
 665                *theme
 666                    .contact_row
 667                    .in_state(is_selected)
 668                    .style_for(&mut Default::default()),
 669            )
 670            .into_any()
 671    }
 672
 673    fn render_participant_project(
 674        project_id: u64,
 675        worktree_root_names: &[String],
 676        host_user_id: u64,
 677        is_current: bool,
 678        is_last: bool,
 679        is_selected: bool,
 680        theme: &theme::CollabPanel,
 681        cx: &mut ViewContext<Self>,
 682    ) -> AnyElement<Self> {
 683        enum JoinProject {}
 684
 685        let font_cache = cx.font_cache();
 686        let host_avatar_height = theme
 687            .contact_avatar
 688            .width
 689            .or(theme.contact_avatar.height)
 690            .unwrap_or(0.);
 691        let row = &theme.project_row.inactive_state().default;
 692        let tree_branch = theme.tree_branch;
 693        let line_height = row.name.text.line_height(font_cache);
 694        let cap_height = row.name.text.cap_height(font_cache);
 695        let baseline_offset =
 696            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 697        let project_name = if worktree_root_names.is_empty() {
 698            "untitled".to_string()
 699        } else {
 700            worktree_root_names.join(", ")
 701        };
 702
 703        MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
 704            let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 705            let row = theme
 706                .project_row
 707                .in_state(is_selected)
 708                .style_for(mouse_state);
 709
 710            Flex::row()
 711                .with_child(
 712                    Stack::new()
 713                        .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 714                            let start_x =
 715                                bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
 716                            let end_x = bounds.max_x();
 717                            let start_y = bounds.min_y();
 718                            let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 719
 720                            scene.push_quad(gpui::Quad {
 721                                bounds: RectF::from_points(
 722                                    vec2f(start_x, start_y),
 723                                    vec2f(
 724                                        start_x + tree_branch.width,
 725                                        if is_last { end_y } else { bounds.max_y() },
 726                                    ),
 727                                ),
 728                                background: Some(tree_branch.color),
 729                                border: gpui::Border::default(),
 730                                corner_radius: 0.,
 731                            });
 732                            scene.push_quad(gpui::Quad {
 733                                bounds: RectF::from_points(
 734                                    vec2f(start_x, end_y),
 735                                    vec2f(end_x, end_y + tree_branch.width),
 736                                ),
 737                                background: Some(tree_branch.color),
 738                                border: gpui::Border::default(),
 739                                corner_radius: 0.,
 740                            });
 741                        }))
 742                        .constrained()
 743                        .with_width(host_avatar_height),
 744                )
 745                .with_child(
 746                    Label::new(project_name, row.name.text.clone())
 747                        .aligned()
 748                        .left()
 749                        .contained()
 750                        .with_style(row.name.container)
 751                        .flex(1., false),
 752                )
 753                .constrained()
 754                .with_height(theme.row_height)
 755                .contained()
 756                .with_style(row.container)
 757        })
 758        .with_cursor_style(if !is_current {
 759            CursorStyle::PointingHand
 760        } else {
 761            CursorStyle::Arrow
 762        })
 763        .on_click(MouseButton::Left, move |_, this, cx| {
 764            if !is_current {
 765                if let Some(workspace) = this.workspace.upgrade(cx) {
 766                    let app_state = workspace.read(cx).app_state().clone();
 767                    workspace::join_remote_project(project_id, host_user_id, app_state, cx)
 768                        .detach_and_log_err(cx);
 769                }
 770            }
 771        })
 772        .into_any()
 773    }
 774
 775    fn render_participant_screen(
 776        peer_id: PeerId,
 777        is_last: bool,
 778        is_selected: bool,
 779        theme: &theme::CollabPanel,
 780        cx: &mut ViewContext<Self>,
 781    ) -> AnyElement<Self> {
 782        enum OpenSharedScreen {}
 783
 784        let font_cache = cx.font_cache();
 785        let host_avatar_height = theme
 786            .contact_avatar
 787            .width
 788            .or(theme.contact_avatar.height)
 789            .unwrap_or(0.);
 790        let row = &theme.project_row.inactive_state().default;
 791        let tree_branch = theme.tree_branch;
 792        let line_height = row.name.text.line_height(font_cache);
 793        let cap_height = row.name.text.cap_height(font_cache);
 794        let baseline_offset =
 795            row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
 796
 797        MouseEventHandler::<OpenSharedScreen, Self>::new(
 798            peer_id.as_u64() as usize,
 799            cx,
 800            |mouse_state, _| {
 801                let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
 802                let row = theme
 803                    .project_row
 804                    .in_state(is_selected)
 805                    .style_for(mouse_state);
 806
 807                Flex::row()
 808                    .with_child(
 809                        Stack::new()
 810                            .with_child(Canvas::new(move |scene, bounds, _, _, _| {
 811                                let start_x = bounds.min_x() + (bounds.width() / 2.)
 812                                    - (tree_branch.width / 2.);
 813                                let end_x = bounds.max_x();
 814                                let start_y = bounds.min_y();
 815                                let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
 816
 817                                scene.push_quad(gpui::Quad {
 818                                    bounds: RectF::from_points(
 819                                        vec2f(start_x, start_y),
 820                                        vec2f(
 821                                            start_x + tree_branch.width,
 822                                            if is_last { end_y } else { bounds.max_y() },
 823                                        ),
 824                                    ),
 825                                    background: Some(tree_branch.color),
 826                                    border: gpui::Border::default(),
 827                                    corner_radius: 0.,
 828                                });
 829                                scene.push_quad(gpui::Quad {
 830                                    bounds: RectF::from_points(
 831                                        vec2f(start_x, end_y),
 832                                        vec2f(end_x, end_y + tree_branch.width),
 833                                    ),
 834                                    background: Some(tree_branch.color),
 835                                    border: gpui::Border::default(),
 836                                    corner_radius: 0.,
 837                                });
 838                            }))
 839                            .constrained()
 840                            .with_width(host_avatar_height),
 841                    )
 842                    .with_child(
 843                        Svg::new("icons/disable_screen_sharing_12.svg")
 844                            .with_color(row.icon.color)
 845                            .constrained()
 846                            .with_width(row.icon.width)
 847                            .aligned()
 848                            .left()
 849                            .contained()
 850                            .with_style(row.icon.container),
 851                    )
 852                    .with_child(
 853                        Label::new("Screen", row.name.text.clone())
 854                            .aligned()
 855                            .left()
 856                            .contained()
 857                            .with_style(row.name.container)
 858                            .flex(1., false),
 859                    )
 860                    .constrained()
 861                    .with_height(theme.row_height)
 862                    .contained()
 863                    .with_style(row.container)
 864            },
 865        )
 866        .with_cursor_style(CursorStyle::PointingHand)
 867        .on_click(MouseButton::Left, move |_, this, cx| {
 868            if let Some(workspace) = this.workspace.upgrade(cx) {
 869                workspace.update(cx, |workspace, cx| {
 870                    workspace.open_shared_screen(peer_id, cx)
 871                });
 872            }
 873        })
 874        .into_any()
 875    }
 876
 877    fn render_header(
 878        section: Section,
 879        theme: &theme::Theme,
 880        depth: usize,
 881        is_selected: bool,
 882        is_collapsed: bool,
 883        cx: &mut ViewContext<Self>,
 884    ) -> AnyElement<Self> {
 885        enum Header {}
 886        enum LeaveCallContactList {}
 887        enum AddChannel {}
 888
 889        let tooltip_style = &theme.tooltip;
 890        let text = match section {
 891            Section::ActiveCall => "Current Call",
 892            Section::Requests => "Requests",
 893            Section::Contacts => "Contacts",
 894            Section::Channels => "Channels",
 895            Section::Online => "Online",
 896            Section::Offline => "Offline",
 897        };
 898
 899        enum AddContact {}
 900        let button = match section {
 901            Section::ActiveCall => Some(
 902                MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
 903                    render_icon_button(
 904                        &theme.collab_panel.leave_call_button,
 905                        "icons/radix/exit.svg",
 906                    )
 907                })
 908                .with_cursor_style(CursorStyle::PointingHand)
 909                .on_click(MouseButton::Left, |_, _, cx| {
 910                    ActiveCall::global(cx)
 911                        .update(cx, |call, cx| call.hang_up(cx))
 912                        .detach_and_log_err(cx);
 913                })
 914                .with_tooltip::<AddContact>(
 915                    0,
 916                    "Leave call".into(),
 917                    None,
 918                    tooltip_style.clone(),
 919                    cx,
 920                ),
 921            ),
 922            Section::Contacts => Some(
 923                MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |_, _| {
 924                    render_icon_button(
 925                        &theme.collab_panel.add_contact_button,
 926                        "icons/user_plus_16.svg",
 927                    )
 928                })
 929                .with_cursor_style(CursorStyle::PointingHand)
 930                .on_click(MouseButton::Left, |_, this, cx| {
 931                    this.toggle_contact_finder(cx);
 932                })
 933                .with_tooltip::<LeaveCallContactList>(
 934                    0,
 935                    "Search for new contact".into(),
 936                    None,
 937                    tooltip_style.clone(),
 938                    cx,
 939                ),
 940            ),
 941            Section::Channels => Some(
 942                MouseEventHandler::<AddChannel, Self>::new(0, cx, |_, _| {
 943                    render_icon_button(&theme.collab_panel.add_contact_button, "icons/plus_16.svg")
 944                })
 945                .with_cursor_style(CursorStyle::PointingHand)
 946                .on_click(MouseButton::Left, |_, this, cx| {
 947                    this.toggle_channel_finder(cx);
 948                })
 949                .with_tooltip::<AddChannel>(
 950                    0,
 951                    "Add or join a channel".into(),
 952                    None,
 953                    tooltip_style.clone(),
 954                    cx,
 955                ),
 956            ),
 957            _ => None,
 958        };
 959
 960        let can_collapse = depth > 0;
 961        let icon_size = (&theme.collab_panel).section_icon_size;
 962        MouseEventHandler::<Header, Self>::new(section as usize, cx, |state, _| {
 963            let header_style = if can_collapse {
 964                theme
 965                    .collab_panel
 966                    .subheader_row
 967                    .in_state(is_selected)
 968                    .style_for(state)
 969            } else {
 970                &theme.collab_panel.header_row
 971            };
 972
 973            Flex::row()
 974                .with_children(if can_collapse {
 975                    Some(
 976                        Svg::new(if is_collapsed {
 977                            "icons/chevron_right_8.svg"
 978                        } else {
 979                            "icons/chevron_down_8.svg"
 980                        })
 981                        .with_color(header_style.text.color)
 982                        .constrained()
 983                        .with_max_width(icon_size)
 984                        .with_max_height(icon_size)
 985                        .aligned()
 986                        .constrained()
 987                        .with_width(icon_size)
 988                        .contained()
 989                        .with_margin_right(
 990                            theme.collab_panel.contact_username.container.margin.left,
 991                        ),
 992                    )
 993                } else {
 994                    None
 995                })
 996                .with_child(
 997                    Label::new(text, header_style.text.clone())
 998                        .aligned()
 999                        .left()
1000                        .flex(1., true),
1001                )
1002                .with_children(button.map(|button| button.aligned().right()))
1003                .constrained()
1004                .with_height(theme.collab_panel.row_height)
1005                .contained()
1006                .with_style(header_style.container)
1007        })
1008        .with_cursor_style(CursorStyle::PointingHand)
1009        .on_click(MouseButton::Left, move |_, this, cx| {
1010            if can_collapse {
1011                this.toggle_expanded(section, cx);
1012            }
1013        })
1014        .into_any()
1015    }
1016
1017    fn render_contact(
1018        contact: &Contact,
1019        calling: bool,
1020        project: &ModelHandle<Project>,
1021        theme: &theme::CollabPanel,
1022        is_selected: bool,
1023        cx: &mut ViewContext<Self>,
1024    ) -> AnyElement<Self> {
1025        let online = contact.online;
1026        let busy = contact.busy || calling;
1027        let user_id = contact.user.id;
1028        let github_login = contact.user.github_login.clone();
1029        let initial_project = project.clone();
1030        let mut event_handler =
1031            MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |state, cx| {
1032                Flex::row()
1033                    .with_children(contact.user.avatar.clone().map(|avatar| {
1034                        let status_badge = if contact.online {
1035                            Some(
1036                                Empty::new()
1037                                    .collapsed()
1038                                    .contained()
1039                                    .with_style(if busy {
1040                                        theme.contact_status_busy
1041                                    } else {
1042                                        theme.contact_status_free
1043                                    })
1044                                    .aligned(),
1045                            )
1046                        } else {
1047                            None
1048                        };
1049                        Stack::new()
1050                            .with_child(
1051                                Image::from_data(avatar)
1052                                    .with_style(theme.contact_avatar)
1053                                    .aligned()
1054                                    .left(),
1055                            )
1056                            .with_children(status_badge)
1057                    }))
1058                    .with_child(
1059                        Label::new(
1060                            contact.user.github_login.clone(),
1061                            theme.contact_username.text.clone(),
1062                        )
1063                        .contained()
1064                        .with_style(theme.contact_username.container)
1065                        .aligned()
1066                        .left()
1067                        .flex(1., true),
1068                    )
1069                    .with_child(
1070                        MouseEventHandler::<Cancel, Self>::new(
1071                            contact.user.id as usize,
1072                            cx,
1073                            |mouse_state, _| {
1074                                let button_style = theme.contact_button.style_for(mouse_state);
1075                                render_icon_button(button_style, "icons/x_mark_8.svg")
1076                                    .aligned()
1077                                    .flex_float()
1078                            },
1079                        )
1080                        .with_padding(Padding::uniform(2.))
1081                        .with_cursor_style(CursorStyle::PointingHand)
1082                        .on_click(MouseButton::Left, move |_, this, cx| {
1083                            this.remove_contact(user_id, &github_login, cx);
1084                        })
1085                        .flex_float(),
1086                    )
1087                    .with_children(if calling {
1088                        Some(
1089                            Label::new("Calling", theme.calling_indicator.text.clone())
1090                                .contained()
1091                                .with_style(theme.calling_indicator.container)
1092                                .aligned(),
1093                        )
1094                    } else {
1095                        None
1096                    })
1097                    .constrained()
1098                    .with_height(theme.row_height)
1099                    .contained()
1100                    .with_style(*theme.contact_row.in_state(is_selected).style_for(state))
1101            })
1102            .on_click(MouseButton::Left, move |_, this, cx| {
1103                if online && !busy {
1104                    this.call(user_id, Some(initial_project.clone()), cx);
1105                }
1106            });
1107
1108        if online {
1109            event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
1110        }
1111
1112        event_handler.into_any()
1113    }
1114
1115    fn render_contact_request(
1116        user: Arc<User>,
1117        user_store: ModelHandle<UserStore>,
1118        theme: &theme::CollabPanel,
1119        is_incoming: bool,
1120        is_selected: bool,
1121        cx: &mut ViewContext<Self>,
1122    ) -> AnyElement<Self> {
1123        enum Decline {}
1124        enum Accept {}
1125        enum Cancel {}
1126
1127        let mut row = Flex::row()
1128            .with_children(user.avatar.clone().map(|avatar| {
1129                Image::from_data(avatar)
1130                    .with_style(theme.contact_avatar)
1131                    .aligned()
1132                    .left()
1133            }))
1134            .with_child(
1135                Label::new(
1136                    user.github_login.clone(),
1137                    theme.contact_username.text.clone(),
1138                )
1139                .contained()
1140                .with_style(theme.contact_username.container)
1141                .aligned()
1142                .left()
1143                .flex(1., true),
1144            );
1145
1146        let user_id = user.id;
1147        let github_login = user.github_login.clone();
1148        let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
1149        let button_spacing = theme.contact_button_spacing;
1150
1151        if is_incoming {
1152            row.add_child(
1153                MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
1154                    let button_style = if is_contact_request_pending {
1155                        &theme.disabled_button
1156                    } else {
1157                        theme.contact_button.style_for(mouse_state)
1158                    };
1159                    render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
1160                })
1161                .with_cursor_style(CursorStyle::PointingHand)
1162                .on_click(MouseButton::Left, move |_, this, cx| {
1163                    this.respond_to_contact_request(user_id, false, cx);
1164                })
1165                .contained()
1166                .with_margin_right(button_spacing),
1167            );
1168
1169            row.add_child(
1170                MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
1171                    let button_style = if is_contact_request_pending {
1172                        &theme.disabled_button
1173                    } else {
1174                        theme.contact_button.style_for(mouse_state)
1175                    };
1176                    render_icon_button(button_style, "icons/check_8.svg")
1177                        .aligned()
1178                        .flex_float()
1179                })
1180                .with_cursor_style(CursorStyle::PointingHand)
1181                .on_click(MouseButton::Left, move |_, this, cx| {
1182                    this.respond_to_contact_request(user_id, true, cx);
1183                }),
1184            );
1185        } else {
1186            row.add_child(
1187                MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
1188                    let button_style = if is_contact_request_pending {
1189                        &theme.disabled_button
1190                    } else {
1191                        theme.contact_button.style_for(mouse_state)
1192                    };
1193                    render_icon_button(button_style, "icons/x_mark_8.svg")
1194                        .aligned()
1195                        .flex_float()
1196                })
1197                .with_padding(Padding::uniform(2.))
1198                .with_cursor_style(CursorStyle::PointingHand)
1199                .on_click(MouseButton::Left, move |_, this, cx| {
1200                    this.remove_contact(user_id, &github_login, cx);
1201                })
1202                .flex_float(),
1203            );
1204        }
1205
1206        row.constrained()
1207            .with_height(theme.row_height)
1208            .contained()
1209            .with_style(
1210                *theme
1211                    .contact_row
1212                    .in_state(is_selected)
1213                    .style_for(&mut Default::default()),
1214            )
1215            .into_any()
1216    }
1217
1218    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
1219        let did_clear = self.filter_editor.update(cx, |editor, cx| {
1220            if editor.buffer().read(cx).len(cx) > 0 {
1221                editor.set_text("", cx);
1222                true
1223            } else {
1224                false
1225            }
1226        });
1227
1228        if !did_clear {
1229            cx.emit(Event::Dismissed);
1230        }
1231    }
1232
1233    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
1234        let mut ix = self.selection.map_or(0, |ix| ix + 1);
1235        while let Some(entry) = self.entries.get(ix) {
1236            if entry.is_selectable() {
1237                self.selection = Some(ix);
1238                break;
1239            }
1240            ix += 1;
1241        }
1242
1243        self.list_state.reset(self.entries.len());
1244        if let Some(ix) = self.selection {
1245            self.list_state.scroll_to(ListOffset {
1246                item_ix: ix,
1247                offset_in_item: 0.,
1248            });
1249        }
1250        cx.notify();
1251    }
1252
1253    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
1254        if let Some(mut ix) = self.selection.take() {
1255            while ix > 0 {
1256                ix -= 1;
1257                if let Some(entry) = self.entries.get(ix) {
1258                    if entry.is_selectable() {
1259                        self.selection = Some(ix);
1260                        break;
1261                    }
1262                }
1263            }
1264        }
1265
1266        self.list_state.reset(self.entries.len());
1267        if let Some(ix) = self.selection {
1268            self.list_state.scroll_to(ListOffset {
1269                item_ix: ix,
1270                offset_in_item: 0.,
1271            });
1272        }
1273        cx.notify();
1274    }
1275
1276    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
1277        if let Some(selection) = self.selection {
1278            if let Some(entry) = self.entries.get(selection) {
1279                match entry {
1280                    ContactEntry::Header(section, _) => {
1281                        self.toggle_expanded(*section, cx);
1282                    }
1283                    ContactEntry::Contact { contact, calling } => {
1284                        if contact.online && !contact.busy && !calling {
1285                            self.call(contact.user.id, Some(self.project.clone()), cx);
1286                        }
1287                    }
1288                    ContactEntry::ParticipantProject {
1289                        project_id,
1290                        host_user_id,
1291                        ..
1292                    } => {
1293                        if let Some(workspace) = self.workspace.upgrade(cx) {
1294                            let app_state = workspace.read(cx).app_state().clone();
1295                            workspace::join_remote_project(
1296                                *project_id,
1297                                *host_user_id,
1298                                app_state,
1299                                cx,
1300                            )
1301                            .detach_and_log_err(cx);
1302                        }
1303                    }
1304                    ContactEntry::ParticipantScreen { peer_id, .. } => {
1305                        if let Some(workspace) = self.workspace.upgrade(cx) {
1306                            workspace.update(cx, |workspace, cx| {
1307                                workspace.open_shared_screen(*peer_id, cx)
1308                            });
1309                        }
1310                    }
1311                    _ => {}
1312                }
1313            }
1314        }
1315    }
1316
1317    fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
1318        if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
1319            self.collapsed_sections.remove(ix);
1320        } else {
1321            self.collapsed_sections.push(section);
1322        }
1323        self.update_entries(cx);
1324    }
1325
1326    fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
1327        if let Some(workspace) = self.workspace.upgrade(cx) {
1328            workspace.update(cx, |workspace, cx| {
1329                workspace.toggle_modal(cx, |_, cx| {
1330                    cx.add_view(|cx| {
1331                        let finder = build_contact_finder(self.user_store.clone(), cx);
1332                        finder.set_query(self.filter_editor.read(cx).text(cx), cx);
1333                        finder
1334                    })
1335                });
1336            });
1337        }
1338    }
1339
1340    fn toggle_channel_finder(&mut self, cx: &mut ViewContext<Self>) {
1341        if let Some(workspace) = self.workspace.upgrade(cx) {
1342            workspace.update(cx, |workspace, cx| {
1343                workspace.toggle_modal(cx, |_, cx| cx.add_view(|cx| ChannelModal::new(cx)));
1344            });
1345        }
1346    }
1347
1348    fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext<Self>) {
1349        let user_store = self.user_store.clone();
1350        let prompt_message = format!(
1351            "Are you sure you want to remove \"{}\" from your contacts?",
1352            github_login
1353        );
1354        let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
1355        let window_id = cx.window_id();
1356        cx.spawn(|_, mut cx| async move {
1357            if answer.next().await == Some(0) {
1358                if let Err(e) = user_store
1359                    .update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
1360                    .await
1361                {
1362                    cx.prompt(
1363                        window_id,
1364                        PromptLevel::Info,
1365                        &format!("Failed to remove contact: {}", e),
1366                        &["Ok"],
1367                    );
1368                }
1369            }
1370        })
1371        .detach();
1372    }
1373
1374    fn respond_to_contact_request(
1375        &mut self,
1376        user_id: u64,
1377        accept: bool,
1378        cx: &mut ViewContext<Self>,
1379    ) {
1380        self.user_store
1381            .update(cx, |store, cx| {
1382                store.respond_to_contact_request(user_id, accept, cx)
1383            })
1384            .detach();
1385    }
1386
1387    fn call(
1388        &mut self,
1389        recipient_user_id: u64,
1390        initial_project: Option<ModelHandle<Project>>,
1391        cx: &mut ViewContext<Self>,
1392    ) {
1393        ActiveCall::global(cx)
1394            .update(cx, |call, cx| {
1395                call.invite(recipient_user_id, initial_project, cx)
1396            })
1397            .detach_and_log_err(cx);
1398    }
1399}
1400
1401impl View for CollabPanel {
1402    fn ui_name() -> &'static str {
1403        "CollabPanel"
1404    }
1405
1406    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1407        if !self.has_focus {
1408            self.has_focus = true;
1409            cx.emit(Event::Focus);
1410        }
1411    }
1412
1413    fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
1414        self.has_focus = false;
1415    }
1416
1417    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
1418        let theme = &theme::current(cx).collab_panel;
1419
1420        enum PanelFocus {}
1421        MouseEventHandler::<PanelFocus, _>::new(0, cx, |_, cx| {
1422            Stack::new()
1423                .with_child(
1424                    Flex::column()
1425                        .with_child(
1426                            Flex::row()
1427                                .with_child(
1428                                    ChildView::new(&self.filter_editor, cx)
1429                                        .contained()
1430                                        .with_style(theme.user_query_editor.container)
1431                                        .flex(1.0, true),
1432                                )
1433                                .constrained()
1434                                .with_width(self.size(cx)),
1435                        )
1436                        .with_child(
1437                            List::new(self.list_state.clone())
1438                                .constrained()
1439                                .with_width(self.size(cx))
1440                                .flex(1., true)
1441                                .into_any(),
1442                        )
1443                        .contained()
1444                        .with_style(theme.container)
1445                        .constrained()
1446                        .with_width(self.size(cx))
1447                        .into_any(),
1448                )
1449                .with_child(ChildView::new(&self.context_menu, cx))
1450                .into_any()
1451        })
1452        .on_click(MouseButton::Left, |_, _, cx| cx.focus_self())
1453        .into_any_named("channels panel")
1454    }
1455}
1456
1457impl Panel for CollabPanel {
1458    fn position(&self, cx: &gpui::WindowContext) -> DockPosition {
1459        match settings::get::<ChannelsPanelSettings>(cx).dock {
1460            ChannelsPanelDockPosition::Left => DockPosition::Left,
1461            ChannelsPanelDockPosition::Right => DockPosition::Right,
1462        }
1463    }
1464
1465    fn position_is_valid(&self, position: DockPosition) -> bool {
1466        matches!(position, DockPosition::Left | DockPosition::Right)
1467    }
1468
1469    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1470        settings::update_settings_file::<ChannelsPanelSettings>(
1471            self.fs.clone(),
1472            cx,
1473            move |settings| {
1474                let dock = match position {
1475                    DockPosition::Left | DockPosition::Bottom => ChannelsPanelDockPosition::Left,
1476                    DockPosition::Right => ChannelsPanelDockPosition::Right,
1477                };
1478                settings.dock = Some(dock);
1479            },
1480        );
1481    }
1482
1483    fn size(&self, cx: &gpui::WindowContext) -> f32 {
1484        self.width
1485            .unwrap_or_else(|| settings::get::<ChannelsPanelSettings>(cx).default_width)
1486    }
1487
1488    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
1489        self.width = Some(size);
1490        self.serialize(cx);
1491        cx.notify();
1492    }
1493
1494    fn icon_path(&self) -> &'static str {
1495        "icons/radix/person.svg"
1496    }
1497
1498    fn icon_tooltip(&self) -> (String, Option<Box<dyn gpui::Action>>) {
1499        ("Channels Panel".to_string(), Some(Box::new(ToggleFocus)))
1500    }
1501
1502    fn should_change_position_on_event(event: &Self::Event) -> bool {
1503        matches!(event, Event::DockPositionChanged)
1504    }
1505
1506    fn has_focus(&self, _cx: &gpui::WindowContext) -> bool {
1507        self.has_focus
1508    }
1509
1510    fn is_focus_event(event: &Self::Event) -> bool {
1511        matches!(event, Event::Focus)
1512    }
1513}
1514
1515impl ContactEntry {
1516    fn is_selectable(&self) -> bool {
1517        if let ContactEntry::Header(_, 0) = self {
1518            false
1519        } else {
1520            true
1521        }
1522    }
1523}
1524
1525impl PartialEq for ContactEntry {
1526    fn eq(&self, other: &Self) -> bool {
1527        match self {
1528            ContactEntry::Header(section_1, depth_1) => {
1529                if let ContactEntry::Header(section_2, depth_2) = other {
1530                    return section_1 == section_2 && depth_1 == depth_2;
1531                }
1532            }
1533            ContactEntry::CallParticipant { user: user_1, .. } => {
1534                if let ContactEntry::CallParticipant { user: user_2, .. } = other {
1535                    return user_1.id == user_2.id;
1536                }
1537            }
1538            ContactEntry::ParticipantProject {
1539                project_id: project_id_1,
1540                ..
1541            } => {
1542                if let ContactEntry::ParticipantProject {
1543                    project_id: project_id_2,
1544                    ..
1545                } = other
1546                {
1547                    return project_id_1 == project_id_2;
1548                }
1549            }
1550            ContactEntry::ParticipantScreen {
1551                peer_id: peer_id_1, ..
1552            } => {
1553                if let ContactEntry::ParticipantScreen {
1554                    peer_id: peer_id_2, ..
1555                } = other
1556                {
1557                    return peer_id_1 == peer_id_2;
1558                }
1559            }
1560            ContactEntry::IncomingRequest(user_1) => {
1561                if let ContactEntry::IncomingRequest(user_2) = other {
1562                    return user_1.id == user_2.id;
1563                }
1564            }
1565            ContactEntry::OutgoingRequest(user_1) => {
1566                if let ContactEntry::OutgoingRequest(user_2) = other {
1567                    return user_1.id == user_2.id;
1568                }
1569            }
1570            ContactEntry::Contact {
1571                contact: contact_1, ..
1572            } => {
1573                if let ContactEntry::Contact {
1574                    contact: contact_2, ..
1575                } = other
1576                {
1577                    return contact_1.user.id == contact_2.user.id;
1578                }
1579            }
1580        }
1581        false
1582    }
1583}
1584
1585fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
1586    Svg::new(svg_path)
1587        .with_color(style.color)
1588        .constrained()
1589        .with_width(style.icon_width)
1590        .aligned()
1591        .constrained()
1592        .with_width(style.button_width)
1593        .with_height(style.button_width)
1594        .contained()
1595        .with_style(style.container)
1596}