collab_titlebar_item.rs

   1use crate::{
   2    contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
   3    toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
   4    ToggleScreenSharing,
   5};
   6use call::{ActiveCall, ParticipantLocation, Room};
   7use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
   8use clock::ReplicaId;
   9use contacts_popover::ContactsPopover;
  10use context_menu::{ContextMenu, ContextMenuItem};
  11use gpui::{
  12    actions,
  13    color::Color,
  14    elements::*,
  15    geometry::{rect::RectF, vector::vec2f, PathBuilder},
  16    json::{self, ToJson},
  17    platform::{CursorStyle, MouseButton},
  18    AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
  19    ViewContext, ViewHandle, WeakViewHandle,
  20};
  21use project::{Project, RepositoryEntry};
  22use std::{ops::Range, sync::Arc};
  23use theme::{AvatarStyle, Theme};
  24use util::ResultExt;
  25use workspace::{FollowNextCollaborator, Workspace};
  26
  27// const MAX_TITLE_LENGTH: usize = 75;
  28
  29actions!(
  30    collab,
  31    [
  32        ToggleContactsMenu,
  33        ToggleUserMenu,
  34        ShareProject,
  35        UnshareProject,
  36    ]
  37);
  38
  39pub fn init(cx: &mut AppContext) {
  40    cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
  41    cx.add_action(CollabTitlebarItem::share_project);
  42    cx.add_action(CollabTitlebarItem::unshare_project);
  43    cx.add_action(CollabTitlebarItem::toggle_user_menu);
  44}
  45
  46pub struct CollabTitlebarItem {
  47    project: ModelHandle<Project>,
  48    user_store: ModelHandle<UserStore>,
  49    client: Arc<Client>,
  50    workspace: WeakViewHandle<Workspace>,
  51    contacts_popover: Option<ViewHandle<ContactsPopover>>,
  52    user_menu: ViewHandle<ContextMenu>,
  53    _subscriptions: Vec<Subscription>,
  54}
  55
  56impl Entity for CollabTitlebarItem {
  57    type Event = ();
  58}
  59
  60impl View for CollabTitlebarItem {
  61    fn ui_name() -> &'static str {
  62        "CollabTitlebarItem"
  63    }
  64
  65    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
  66        let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
  67            workspace
  68        } else {
  69            return Empty::new().into_any();
  70        };
  71
  72        let project = self.project.read(cx);
  73        let theme = theme::current(cx).clone();
  74        let mut left_container = Flex::row();
  75        let mut right_container = Flex::row().align_children_center();
  76
  77        left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
  78
  79        let user = self.user_store.read(cx).current_user();
  80        let peer_id = self.client.peer_id();
  81        if let Some(((user, peer_id), room)) = user
  82            .as_ref()
  83            .zip(peer_id)
  84            .zip(ActiveCall::global(cx).read(cx).room().cloned())
  85        {
  86            right_container
  87                .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
  88            right_container.add_child(self.render_leave_call(&theme, cx));
  89            let muted = room.read(cx).is_muted();
  90            let speaking = room.read(cx).is_speaking();
  91            left_container.add_child(
  92                self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
  93            );
  94            left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
  95            right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
  96            right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
  97            right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
  98        }
  99
 100        let status = workspace.read(cx).client().status();
 101        let status = &*status.borrow();
 102        if matches!(status, client::Status::Connected { .. }) {
 103            right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
 104            let avatar = user.as_ref().and_then(|user| user.avatar.clone());
 105            right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
 106        } else {
 107            right_container.add_children(self.render_connection_status(status, cx));
 108            right_container.add_child(self.render_sign_in_button(&theme, cx));
 109            right_container.add_child(self.render_user_menu_button(&theme, None, cx));
 110        }
 111
 112        Stack::new()
 113            .with_child(left_container)
 114            .with_child(
 115                Flex::row()
 116                    .with_child(
 117                        right_container.contained().with_background_color(
 118                            theme
 119                                .titlebar
 120                                .container
 121                                .background_color
 122                                .unwrap_or_else(|| Color::transparent_black()),
 123                        ),
 124                    )
 125                    .aligned()
 126                    .right(),
 127            )
 128            .into_any()
 129    }
 130}
 131
 132impl CollabTitlebarItem {
 133    pub fn new(
 134        workspace: &Workspace,
 135        workspace_handle: &ViewHandle<Workspace>,
 136        cx: &mut ViewContext<Self>,
 137    ) -> Self {
 138        let project = workspace.project().clone();
 139        let user_store = workspace.app_state().user_store.clone();
 140        let client = workspace.app_state().client.clone();
 141        let active_call = ActiveCall::global(cx);
 142        let mut subscriptions = Vec::new();
 143        subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
 144        subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
 145        subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
 146        subscriptions.push(cx.observe_window_activation(|this, active, cx| {
 147            this.window_activation_changed(active, cx)
 148        }));
 149        subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
 150        subscriptions.push(
 151            cx.subscribe(&user_store, move |this, user_store, event, cx| {
 152                if let Some(workspace) = this.workspace.upgrade(cx) {
 153                    workspace.update(cx, |workspace, cx| {
 154                        if let client::Event::Contact { user, kind } = event {
 155                            if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
 156                                workspace.show_notification(user.id as usize, cx, |cx| {
 157                                    cx.add_view(|cx| {
 158                                        ContactNotification::new(
 159                                            user.clone(),
 160                                            *kind,
 161                                            user_store,
 162                                            cx,
 163                                        )
 164                                    })
 165                                })
 166                            }
 167                        }
 168                    });
 169                }
 170            }),
 171        );
 172
 173        Self {
 174            workspace: workspace.weak_handle(),
 175            project,
 176            user_store,
 177            client,
 178            contacts_popover: None,
 179            user_menu: cx.add_view(|cx| {
 180                let view_id = cx.view_id();
 181                let mut menu = ContextMenu::new(view_id, cx);
 182                menu.set_position_mode(OverlayPositionMode::Local);
 183                menu
 184            }),
 185            _subscriptions: subscriptions,
 186        }
 187    }
 188
 189    fn collect_title_root_names(
 190        &self,
 191        project: &Project,
 192        theme: Arc<Theme>,
 193        cx: &ViewContext<Self>,
 194    ) -> AnyElement<Self> {
 195        let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
 196            let worktree = worktree.read(cx);
 197            (worktree.root_name(), worktree.root_git_entry())
 198        });
 199
 200        let (name, entry) = names_and_branches.next().unwrap_or(("", None));
 201        let branch_prepended = entry
 202            .as_ref()
 203            .and_then(RepositoryEntry::branch)
 204            .map(|branch| format!("/{branch}"));
 205        let text_style = theme.titlebar.title.clone();
 206        let item_spacing = theme.titlebar.item_spacing;
 207
 208        let mut highlight = text_style.clone();
 209        highlight.color = theme.titlebar.highlight_color;
 210
 211        let style = LabelStyle {
 212            text: text_style,
 213            highlight_text: Some(highlight),
 214        };
 215        let mut ret = Flex::row().with_child(
 216            Label::new(name.to_owned(), style.clone())
 217                .with_highlights((0..name.len()).into_iter().collect())
 218                .contained()
 219                .aligned()
 220                .left()
 221                .into_any_named("title-project-name"),
 222        );
 223        if let Some(git_branch) = branch_prepended {
 224            ret = ret.with_child(
 225                Label::new(git_branch, style)
 226                    .contained()
 227                    .with_margin_right(item_spacing)
 228                    .aligned()
 229                    .left()
 230                    .into_any_named("title-project-branch"),
 231            )
 232        }
 233        ret.into_any()
 234    }
 235
 236    fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
 237        let project = if active {
 238            Some(self.project.clone())
 239        } else {
 240            None
 241        };
 242        ActiveCall::global(cx)
 243            .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
 244            .detach_and_log_err(cx);
 245    }
 246
 247    fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
 248        if ActiveCall::global(cx).read(cx).room().is_none() {
 249            self.contacts_popover = None;
 250        }
 251        cx.notify();
 252    }
 253
 254    fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
 255        let active_call = ActiveCall::global(cx);
 256        let project = self.project.clone();
 257        active_call
 258            .update(cx, |call, cx| call.share_project(project, cx))
 259            .detach_and_log_err(cx);
 260    }
 261
 262    fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
 263        let active_call = ActiveCall::global(cx);
 264        let project = self.project.clone();
 265        active_call
 266            .update(cx, |call, cx| call.unshare_project(project, cx))
 267            .log_err();
 268    }
 269
 270    pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
 271        if self.contacts_popover.take().is_none() {
 272            let view = cx.add_view(|cx| {
 273                ContactsPopover::new(
 274                    self.project.clone(),
 275                    self.user_store.clone(),
 276                    self.workspace.clone(),
 277                    cx,
 278                )
 279            });
 280            cx.subscribe(&view, |this, _, event, cx| {
 281                match event {
 282                    contacts_popover::Event::Dismissed => {
 283                        this.contacts_popover = None;
 284                    }
 285                }
 286
 287                cx.notify();
 288            })
 289            .detach();
 290            self.contacts_popover = Some(view);
 291        }
 292
 293        cx.notify();
 294    }
 295
 296    pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
 297        self.user_menu.update(cx, |user_menu, cx| {
 298            let items = if let Some(_) = self.user_store.read(cx).current_user() {
 299                vec![
 300                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
 301                    ContextMenuItem::action("Theme", theme_selector::Toggle),
 302                    ContextMenuItem::separator(),
 303                    ContextMenuItem::action(
 304                        "Share Feedback",
 305                        feedback::feedback_editor::GiveFeedback,
 306                    ),
 307                    ContextMenuItem::action("Sign out", SignOut),
 308                ]
 309            } else {
 310                vec![
 311                    ContextMenuItem::action("Settings", zed_actions::OpenSettings),
 312                    ContextMenuItem::action("Theme", theme_selector::Toggle),
 313                    ContextMenuItem::separator(),
 314                    ContextMenuItem::action(
 315                        "Share Feedback",
 316                        feedback::feedback_editor::GiveFeedback,
 317                    ),
 318                ]
 319            };
 320            user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
 321        });
 322    }
 323
 324    fn render_toggle_contacts_button(
 325        &self,
 326        theme: &Theme,
 327        cx: &mut ViewContext<Self>,
 328    ) -> AnyElement<Self> {
 329        let titlebar = &theme.titlebar;
 330
 331        let badge = if self
 332            .user_store
 333            .read(cx)
 334            .incoming_contact_requests()
 335            .is_empty()
 336        {
 337            None
 338        } else {
 339            Some(
 340                Empty::new()
 341                    .collapsed()
 342                    .contained()
 343                    .with_style(titlebar.toggle_contacts_badge)
 344                    .contained()
 345                    .with_margin_left(
 346                        titlebar
 347                            .toggle_contacts_button
 348                            .inactive_state()
 349                            .default
 350                            .icon_width,
 351                    )
 352                    .with_margin_top(
 353                        titlebar
 354                            .toggle_contacts_button
 355                            .inactive_state()
 356                            .default
 357                            .icon_width,
 358                    )
 359                    .aligned(),
 360            )
 361        };
 362
 363        Stack::new()
 364            .with_child(
 365                MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
 366                    let style = titlebar
 367                        .toggle_contacts_button
 368                        .in_state(self.contacts_popover.is_some())
 369                        .style_for(state);
 370                    Svg::new("icons/radix/person.svg")
 371                        .with_color(style.color)
 372                        .constrained()
 373                        .with_width(style.icon_width)
 374                        .aligned()
 375                        .constrained()
 376                        .with_width(style.button_width)
 377                        .with_height(style.button_width)
 378                        .contained()
 379                        .with_style(style.container)
 380                })
 381                .with_cursor_style(CursorStyle::PointingHand)
 382                .on_click(MouseButton::Left, move |_, this, cx| {
 383                    this.toggle_contacts_popover(&Default::default(), cx)
 384                })
 385                .with_tooltip::<ToggleContactsMenu>(
 386                    0,
 387                    "Show contacts menu".into(),
 388                    Some(Box::new(ToggleContactsMenu)),
 389                    theme.tooltip.clone(),
 390                    cx,
 391                ),
 392            )
 393            .with_children(badge)
 394            .with_children(self.render_contacts_popover_host(titlebar, cx))
 395            .into_any()
 396    }
 397    fn render_toggle_screen_sharing_button(
 398        &self,
 399        theme: &Theme,
 400        room: &ModelHandle<Room>,
 401        cx: &mut ViewContext<Self>,
 402    ) -> AnyElement<Self> {
 403        let icon;
 404        let tooltip;
 405        if room.read(cx).is_screen_sharing() {
 406            icon = "icons/radix/desktop.svg";
 407            tooltip = "Stop Sharing Screen"
 408        } else {
 409            icon = "icons/radix/desktop.svg";
 410            tooltip = "Share Screen";
 411        }
 412
 413        let active = room.read(cx).is_screen_sharing();
 414        let titlebar = &theme.titlebar;
 415        MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
 416            let style = titlebar
 417                .screen_share_button
 418                .in_state(active)
 419                .style_for(state);
 420
 421            Svg::new(icon)
 422                .with_color(style.color)
 423                .constrained()
 424                .with_width(style.icon_width)
 425                .aligned()
 426                .constrained()
 427                .with_width(style.button_width)
 428                .with_height(style.button_width)
 429                .contained()
 430                .with_style(style.container)
 431        })
 432        .with_cursor_style(CursorStyle::PointingHand)
 433        .on_click(MouseButton::Left, move |_, _, cx| {
 434            toggle_screen_sharing(&Default::default(), cx)
 435        })
 436        .with_tooltip::<ToggleScreenSharing>(
 437            0,
 438            tooltip.into(),
 439            Some(Box::new(ToggleScreenSharing)),
 440            theme.tooltip.clone(),
 441            cx,
 442        )
 443        .aligned()
 444        .into_any()
 445    }
 446    fn render_toggle_mute(
 447        &self,
 448        theme: &Theme,
 449        room: &ModelHandle<Room>,
 450        cx: &mut ViewContext<Self>,
 451    ) -> AnyElement<Self> {
 452        let icon;
 453        let tooltip;
 454        let is_muted = room.read(cx).is_muted();
 455        if is_muted {
 456            icon = "icons/radix/mic-mute.svg";
 457            tooltip = "Unmute microphone\nRight click for options";
 458        } else {
 459            icon = "icons/radix/mic.svg";
 460            tooltip = "Mute microphone\nRight click for options";
 461        }
 462
 463        let titlebar = &theme.titlebar;
 464        MouseEventHandler::<ToggleMute, Self>::new(0, cx, |state, _| {
 465            let style = titlebar
 466                .toggle_microphone_button
 467                .in_state(is_muted)
 468                .style_for(state);
 469            let image = Svg::new(icon)
 470                .with_color(style.color)
 471                .constrained()
 472                .with_width(style.icon_width)
 473                .aligned()
 474                .constrained()
 475                .with_width(style.button_width)
 476                .with_height(style.button_width)
 477                .contained()
 478                .with_style(style.container);
 479            if let Some(color) = style.container.background_color {
 480                image.with_background_color(color)
 481            } else {
 482                image
 483            }
 484        })
 485        .with_cursor_style(CursorStyle::PointingHand)
 486        .on_click(MouseButton::Left, move |_, _, cx| {
 487            toggle_mute(&Default::default(), cx)
 488        })
 489        .with_tooltip::<ToggleMute>(
 490            0,
 491            tooltip.into(),
 492            Some(Box::new(ToggleMute)),
 493            theme.tooltip.clone(),
 494            cx,
 495        )
 496        .aligned()
 497        .into_any()
 498    }
 499    fn render_toggle_deafen(
 500        &self,
 501        theme: &Theme,
 502        room: &ModelHandle<Room>,
 503        cx: &mut ViewContext<Self>,
 504    ) -> AnyElement<Self> {
 505        let icon;
 506        let tooltip;
 507        let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
 508        if is_deafened {
 509            icon = "icons/radix/speaker-off.svg";
 510            tooltip = "Unmute speakers\nRight click for options";
 511        } else {
 512            icon = "icons/radix/speaker-loud.svg";
 513            tooltip = "Mute speakers\nRight click for options";
 514        }
 515
 516        let titlebar = &theme.titlebar;
 517        MouseEventHandler::<ToggleDeafen, Self>::new(0, cx, |state, _| {
 518            let style = titlebar
 519                .toggle_speakers_button
 520                .in_state(is_deafened)
 521                .style_for(state);
 522            Svg::new(icon)
 523                .with_color(style.color)
 524                .constrained()
 525                .with_width(style.icon_width)
 526                .aligned()
 527                .constrained()
 528                .with_width(style.button_width)
 529                .with_height(style.button_width)
 530                .contained()
 531                .with_style(style.container)
 532        })
 533        .with_cursor_style(CursorStyle::PointingHand)
 534        .on_click(MouseButton::Left, move |_, _, cx| {
 535            toggle_deafen(&Default::default(), cx)
 536        })
 537        .with_tooltip::<ToggleDeafen>(
 538            0,
 539            tooltip.into(),
 540            Some(Box::new(ToggleDeafen)),
 541            theme.tooltip.clone(),
 542            cx,
 543        )
 544        .aligned()
 545        .into_any()
 546    }
 547    fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 548        let icon = "icons/radix/exit.svg";
 549        let tooltip = "Leave call";
 550
 551        let titlebar = &theme.titlebar;
 552        MouseEventHandler::<LeaveCall, Self>::new(0, cx, |state, _| {
 553            let style = titlebar.leave_call_button.style_for(state);
 554            Svg::new(icon)
 555                .with_color(style.color)
 556                .constrained()
 557                .with_width(style.icon_width)
 558                .aligned()
 559                .constrained()
 560                .with_width(style.button_width)
 561                .with_height(style.button_width)
 562                .contained()
 563                .with_style(style.container)
 564        })
 565        .with_cursor_style(CursorStyle::PointingHand)
 566        .on_click(MouseButton::Left, move |_, _, cx| {
 567            ActiveCall::global(cx)
 568                .update(cx, |call, cx| call.hang_up(cx))
 569                .detach_and_log_err(cx);
 570        })
 571        .with_tooltip::<LeaveCall>(
 572            0,
 573            tooltip.into(),
 574            Some(Box::new(LeaveCall)),
 575            theme.tooltip.clone(),
 576            cx,
 577        )
 578        .aligned()
 579        .into_any()
 580    }
 581    fn render_in_call_share_unshare_button(
 582        &self,
 583        workspace: &ViewHandle<Workspace>,
 584        theme: &Theme,
 585        cx: &mut ViewContext<Self>,
 586    ) -> Option<AnyElement<Self>> {
 587        let project = workspace.read(cx).project();
 588        if project.read(cx).is_remote() {
 589            return None;
 590        }
 591
 592        let is_shared = project.read(cx).is_shared();
 593        let label = if is_shared { "Stop Sharing" } else { "Share" };
 594        let tooltip = if is_shared {
 595            "Stop sharing project with call participants"
 596        } else {
 597            "Share project with call participants"
 598        };
 599
 600        let titlebar = &theme.titlebar;
 601
 602        enum ShareUnshare {}
 603        Some(
 604            Stack::new()
 605                .with_child(
 606                    MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
 607                        //TODO: Ensure this button has consistent width for both text variations
 608                        let style = titlebar.share_button.inactive_state().style_for(state);
 609                        Label::new(label, style.text.clone())
 610                            .contained()
 611                            .with_style(style.container)
 612                    })
 613                    .with_cursor_style(CursorStyle::PointingHand)
 614                    .on_click(MouseButton::Left, move |_, this, cx| {
 615                        if is_shared {
 616                            this.unshare_project(&Default::default(), cx);
 617                        } else {
 618                            this.share_project(&Default::default(), cx);
 619                        }
 620                    })
 621                    .with_tooltip::<ShareUnshare>(
 622                        0,
 623                        tooltip.to_owned(),
 624                        None,
 625                        theme.tooltip.clone(),
 626                        cx,
 627                    ),
 628                )
 629                .aligned()
 630                .contained()
 631                .with_margin_left(theme.titlebar.item_spacing)
 632                .into_any(),
 633        )
 634    }
 635
 636    fn render_user_menu_button(
 637        &self,
 638        theme: &Theme,
 639        avatar: Option<Arc<ImageData>>,
 640        cx: &mut ViewContext<Self>,
 641    ) -> AnyElement<Self> {
 642        let tooltip = theme.tooltip.clone();
 643        let user_menu_button_style = if avatar.is_some() {
 644            &theme.titlebar.user_menu.user_menu_button_online
 645        } else {
 646            &theme.titlebar.user_menu.user_menu_button_offline
 647        };
 648
 649        let avatar_style = &user_menu_button_style.avatar;
 650        Stack::new()
 651            .with_child(
 652                MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
 653                    let style = user_menu_button_style
 654                        .user_menu
 655                        .inactive_state()
 656                        .style_for(state);
 657
 658                    let mut dropdown = Flex::row().align_children_center();
 659
 660                    if let Some(avatar_img) = avatar {
 661                        dropdown = dropdown.with_child(Self::render_face(
 662                            avatar_img,
 663                            *avatar_style,
 664                            Color::transparent_black(),
 665                            None,
 666                        ));
 667                    };
 668
 669                    dropdown
 670                        .with_child(
 671                            Svg::new("icons/caret_down_8.svg")
 672                                .with_color(user_menu_button_style.icon.color)
 673                                .constrained()
 674                                .with_width(user_menu_button_style.icon.width)
 675                                .contained()
 676                                .into_any(),
 677                        )
 678                        .aligned()
 679                        .constrained()
 680                        .with_height(style.width)
 681                        .contained()
 682                        .with_style(style.container)
 683                        .into_any()
 684                })
 685                .with_cursor_style(CursorStyle::PointingHand)
 686                .on_click(MouseButton::Left, move |_, this, cx| {
 687                    this.toggle_user_menu(&Default::default(), cx)
 688                })
 689                .with_tooltip::<ToggleUserMenu>(
 690                    0,
 691                    "Toggle user menu".to_owned(),
 692                    Some(Box::new(ToggleUserMenu)),
 693                    tooltip,
 694                    cx,
 695                )
 696                .contained(),
 697            )
 698            .with_child(
 699                ChildView::new(&self.user_menu, cx)
 700                    .aligned()
 701                    .bottom()
 702                    .right(),
 703            )
 704            .into_any()
 705    }
 706
 707    fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 708        let titlebar = &theme.titlebar;
 709        MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
 710            let style = titlebar.sign_in_button.inactive_state().style_for(state);
 711            Label::new("Sign In", style.text.clone())
 712                .contained()
 713                .with_style(style.container)
 714        })
 715        .with_cursor_style(CursorStyle::PointingHand)
 716        .on_click(MouseButton::Left, move |_, this, cx| {
 717            let client = this.client.clone();
 718            cx.app_context()
 719                .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
 720                .detach_and_log_err(cx);
 721        })
 722        .into_any()
 723    }
 724
 725    fn render_contacts_popover_host<'a>(
 726        &'a self,
 727        _theme: &'a theme::Titlebar,
 728        cx: &'a ViewContext<Self>,
 729    ) -> Option<AnyElement<Self>> {
 730        self.contacts_popover.as_ref().map(|popover| {
 731            Overlay::new(ChildView::new(popover, cx))
 732                .with_fit_mode(OverlayFitMode::SwitchAnchor)
 733                .with_anchor_corner(AnchorCorner::TopRight)
 734                .with_z_index(999)
 735                .aligned()
 736                .bottom()
 737                .right()
 738                .into_any()
 739        })
 740    }
 741
 742    fn render_collaborators(
 743        &self,
 744        workspace: &ViewHandle<Workspace>,
 745        theme: &Theme,
 746        room: &ModelHandle<Room>,
 747        cx: &mut ViewContext<Self>,
 748    ) -> Vec<Container<Self>> {
 749        let mut participants = room
 750            .read(cx)
 751            .remote_participants()
 752            .values()
 753            .cloned()
 754            .collect::<Vec<_>>();
 755        participants.sort_by_cached_key(|p| p.user.github_login.clone());
 756
 757        participants
 758            .into_iter()
 759            .filter_map(|participant| {
 760                let project = workspace.read(cx).project().read(cx);
 761                let replica_id = project
 762                    .collaborators()
 763                    .get(&participant.peer_id)
 764                    .map(|collaborator| collaborator.replica_id);
 765                let user = participant.user.clone();
 766                Some(
 767                    Container::new(self.render_face_pile(
 768                        &user,
 769                        replica_id,
 770                        participant.peer_id,
 771                        Some(participant.location),
 772                        participant.muted,
 773                        participant.speaking,
 774                        workspace,
 775                        theme,
 776                        cx,
 777                    ))
 778                    .with_margin_right(theme.titlebar.face_pile_spacing),
 779                )
 780            })
 781            .collect()
 782    }
 783
 784    fn render_current_user(
 785        &self,
 786        workspace: &ViewHandle<Workspace>,
 787        theme: &Theme,
 788        user: &Arc<User>,
 789        peer_id: PeerId,
 790        muted: bool,
 791        speaking: bool,
 792        cx: &mut ViewContext<Self>,
 793    ) -> AnyElement<Self> {
 794        let replica_id = workspace.read(cx).project().read(cx).replica_id();
 795
 796        Container::new(self.render_face_pile(
 797            user,
 798            Some(replica_id),
 799            peer_id,
 800            None,
 801            muted,
 802            speaking,
 803            workspace,
 804            theme,
 805            cx,
 806        ))
 807        .with_margin_right(theme.titlebar.item_spacing)
 808        .into_any()
 809    }
 810
 811    fn render_face_pile(
 812        &self,
 813        user: &User,
 814        replica_id: Option<ReplicaId>,
 815        peer_id: PeerId,
 816        location: Option<ParticipantLocation>,
 817        muted: bool,
 818        speaking: bool,
 819        workspace: &ViewHandle<Workspace>,
 820        theme: &Theme,
 821        cx: &mut ViewContext<Self>,
 822    ) -> AnyElement<Self> {
 823        let project_id = workspace.read(cx).project().read(cx).remote_id();
 824        let room = ActiveCall::global(cx).read(cx).room();
 825        let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
 826        let followed_by_self = room
 827            .and_then(|room| {
 828                Some(
 829                    is_being_followed
 830                        && room
 831                            .read(cx)
 832                            .followers_for(peer_id, project_id?)
 833                            .iter()
 834                            .any(|&follower| {
 835                                Some(follower) == workspace.read(cx).client().peer_id()
 836                            }),
 837                )
 838            })
 839            .unwrap_or(false);
 840
 841        let leader_style = theme.titlebar.leader_avatar;
 842        let follower_style = theme.titlebar.follower_avatar;
 843
 844        let microphone_state = if muted {
 845            Some(theme.titlebar.muted)
 846        } else if speaking {
 847            Some(theme.titlebar.speaking)
 848        } else {
 849            None
 850        };
 851
 852        let mut background_color = theme
 853            .titlebar
 854            .container
 855            .background_color
 856            .unwrap_or_default();
 857
 858        if let Some(replica_id) = replica_id {
 859            if followed_by_self {
 860                let selection = theme.editor.replica_selection_style(replica_id).selection;
 861                background_color = Color::blend(selection, background_color);
 862                background_color.a = 255;
 863            }
 864        }
 865
 866        let mut content = Stack::new()
 867            .with_children(user.avatar.as_ref().map(|avatar| {
 868                let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
 869                    .with_child(Self::render_face(
 870                        avatar.clone(),
 871                        Self::location_style(workspace, location, leader_style, cx),
 872                        background_color,
 873                        microphone_state,
 874                    ))
 875                    .with_children(
 876                        (|| {
 877                            let project_id = project_id?;
 878                            let room = room?.read(cx);
 879                            let followers = room.followers_for(peer_id, project_id);
 880
 881                            Some(followers.into_iter().flat_map(|&follower| {
 882                                let remote_participant =
 883                                    room.remote_participant_for_peer_id(follower);
 884
 885                                let avatar = remote_participant
 886                                    .and_then(|p| p.user.avatar.clone())
 887                                    .or_else(|| {
 888                                        if follower == workspace.read(cx).client().peer_id()? {
 889                                            workspace
 890                                                .read(cx)
 891                                                .user_store()
 892                                                .read(cx)
 893                                                .current_user()?
 894                                                .avatar
 895                                                .clone()
 896                                        } else {
 897                                            None
 898                                        }
 899                                    })?;
 900
 901                                Some(Self::render_face(
 902                                    avatar.clone(),
 903                                    follower_style,
 904                                    background_color,
 905                                    None,
 906                                ))
 907                            }))
 908                        })()
 909                        .into_iter()
 910                        .flatten(),
 911                    );
 912
 913                let mut container = face_pile
 914                    .contained()
 915                    .with_style(theme.titlebar.leader_selection);
 916
 917                if let Some(replica_id) = replica_id {
 918                    if followed_by_self {
 919                        let color = theme.editor.replica_selection_style(replica_id).selection;
 920                        container = container.with_background_color(color);
 921                    }
 922                }
 923
 924                container
 925            }))
 926            .with_children((|| {
 927                let replica_id = replica_id?;
 928                let color = theme.editor.replica_selection_style(replica_id).cursor;
 929                Some(
 930                    AvatarRibbon::new(color)
 931                        .constrained()
 932                        .with_width(theme.titlebar.avatar_ribbon.width)
 933                        .with_height(theme.titlebar.avatar_ribbon.height)
 934                        .aligned()
 935                        .bottom(),
 936                )
 937            })())
 938            .into_any();
 939
 940        if let Some(location) = location {
 941            if let Some(replica_id) = replica_id {
 942                enum ToggleFollow {}
 943
 944                content = MouseEventHandler::<ToggleFollow, Self>::new(
 945                    replica_id.into(),
 946                    cx,
 947                    move |_, _| content,
 948                )
 949                .with_cursor_style(CursorStyle::PointingHand)
 950                .on_click(MouseButton::Left, move |_, item, cx| {
 951                    if let Some(workspace) = item.workspace.upgrade(cx) {
 952                        if let Some(task) = workspace
 953                            .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
 954                        {
 955                            task.detach_and_log_err(cx);
 956                        }
 957                    }
 958                })
 959                .with_tooltip::<ToggleFollow>(
 960                    peer_id.as_u64() as usize,
 961                    if is_being_followed {
 962                        format!("Unfollow {}", user.github_login)
 963                    } else {
 964                        format!("Follow {}", user.github_login)
 965                    },
 966                    Some(Box::new(FollowNextCollaborator)),
 967                    theme.tooltip.clone(),
 968                    cx,
 969                )
 970                .into_any();
 971            } else if let ParticipantLocation::SharedProject { project_id } = location {
 972                enum JoinProject {}
 973
 974                let user_id = user.id;
 975                content = MouseEventHandler::<JoinProject, Self>::new(
 976                    peer_id.as_u64() as usize,
 977                    cx,
 978                    move |_, _| content,
 979                )
 980                .with_cursor_style(CursorStyle::PointingHand)
 981                .on_click(MouseButton::Left, move |_, this, cx| {
 982                    if let Some(workspace) = this.workspace.upgrade(cx) {
 983                        let app_state = workspace.read(cx).app_state().clone();
 984                        workspace::join_remote_project(project_id, user_id, app_state, cx)
 985                            .detach_and_log_err(cx);
 986                    }
 987                })
 988                .with_tooltip::<JoinProject>(
 989                    peer_id.as_u64() as usize,
 990                    format!("Follow {} into external project", user.github_login),
 991                    Some(Box::new(FollowNextCollaborator)),
 992                    theme.tooltip.clone(),
 993                    cx,
 994                )
 995                .into_any();
 996            }
 997        }
 998        content
 999    }
1000
1001    fn location_style(
1002        workspace: &ViewHandle<Workspace>,
1003        location: Option<ParticipantLocation>,
1004        mut style: AvatarStyle,
1005        cx: &ViewContext<Self>,
1006    ) -> AvatarStyle {
1007        if let Some(location) = location {
1008            if let ParticipantLocation::SharedProject { project_id } = location {
1009                if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
1010                    style.image.grayscale = true;
1011                }
1012            } else {
1013                style.image.grayscale = true;
1014            }
1015        }
1016
1017        style
1018    }
1019
1020    fn render_face<V: View>(
1021        avatar: Arc<ImageData>,
1022        avatar_style: AvatarStyle,
1023        background_color: Color,
1024        microphone_state: Option<Color>,
1025    ) -> AnyElement<V> {
1026        Image::from_data(avatar)
1027            .with_style(avatar_style.image)
1028            .aligned()
1029            .contained()
1030            .with_background_color(microphone_state.unwrap_or(background_color))
1031            .with_corner_radius(avatar_style.outer_corner_radius)
1032            .constrained()
1033            .with_width(avatar_style.outer_width)
1034            .with_height(avatar_style.outer_width)
1035            .aligned()
1036            .into_any()
1037    }
1038
1039    fn render_connection_status(
1040        &self,
1041        status: &client::Status,
1042        cx: &mut ViewContext<Self>,
1043    ) -> Option<AnyElement<Self>> {
1044        enum ConnectionStatusButton {}
1045
1046        let theme = &theme::current(cx).clone();
1047        match status {
1048            client::Status::ConnectionError
1049            | client::Status::ConnectionLost
1050            | client::Status::Reauthenticating { .. }
1051            | client::Status::Reconnecting { .. }
1052            | client::Status::ReconnectionError { .. } => Some(
1053                Svg::new("icons/cloud_slash_12.svg")
1054                    .with_color(theme.titlebar.offline_icon.color)
1055                    .constrained()
1056                    .with_width(theme.titlebar.offline_icon.width)
1057                    .aligned()
1058                    .contained()
1059                    .with_style(theme.titlebar.offline_icon.container)
1060                    .into_any(),
1061            ),
1062            client::Status::UpgradeRequired => Some(
1063                MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
1064                    Label::new(
1065                        "Please update Zed to collaborate",
1066                        theme.titlebar.outdated_warning.text.clone(),
1067                    )
1068                    .contained()
1069                    .with_style(theme.titlebar.outdated_warning.container)
1070                    .aligned()
1071                })
1072                .with_cursor_style(CursorStyle::PointingHand)
1073                .on_click(MouseButton::Left, |_, _, cx| {
1074                    auto_update::check(&Default::default(), cx);
1075                })
1076                .into_any(),
1077            ),
1078            _ => None,
1079        }
1080    }
1081}
1082
1083pub struct AvatarRibbon {
1084    color: Color,
1085}
1086
1087impl AvatarRibbon {
1088    pub fn new(color: Color) -> AvatarRibbon {
1089        AvatarRibbon { color }
1090    }
1091}
1092
1093impl Element<CollabTitlebarItem> for AvatarRibbon {
1094    type LayoutState = ();
1095
1096    type PaintState = ();
1097
1098    fn layout(
1099        &mut self,
1100        constraint: gpui::SizeConstraint,
1101        _: &mut CollabTitlebarItem,
1102        _: &mut LayoutContext<CollabTitlebarItem>,
1103    ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
1104        (constraint.max, ())
1105    }
1106
1107    fn paint(
1108        &mut self,
1109        scene: &mut SceneBuilder,
1110        bounds: RectF,
1111        _: RectF,
1112        _: &mut Self::LayoutState,
1113        _: &mut CollabTitlebarItem,
1114        _: &mut ViewContext<CollabTitlebarItem>,
1115    ) -> Self::PaintState {
1116        let mut path = PathBuilder::new();
1117        path.reset(bounds.lower_left());
1118        path.curve_to(
1119            bounds.origin() + vec2f(bounds.height(), 0.),
1120            bounds.origin(),
1121        );
1122        path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
1123        path.curve_to(bounds.lower_right(), bounds.upper_right());
1124        path.line_to(bounds.lower_left());
1125        scene.push_path(path.build(self.color, None));
1126    }
1127
1128    fn rect_for_text_range(
1129        &self,
1130        _: Range<usize>,
1131        _: RectF,
1132        _: RectF,
1133        _: &Self::LayoutState,
1134        _: &Self::PaintState,
1135        _: &CollabTitlebarItem,
1136        _: &ViewContext<CollabTitlebarItem>,
1137    ) -> Option<RectF> {
1138        None
1139    }
1140
1141    fn debug(
1142        &self,
1143        bounds: RectF,
1144        _: &Self::LayoutState,
1145        _: &Self::PaintState,
1146        _: &CollabTitlebarItem,
1147        _: &ViewContext<CollabTitlebarItem>,
1148    ) -> gpui::json::Value {
1149        json::json!({
1150            "type": "AvatarRibbon",
1151            "bounds": bounds.to_json(),
1152            "color": self.color.to_json(),
1153        })
1154    }
1155}