1use crate::{
2 collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
3 contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
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 impl_internal_actions,
17 json::{self, ToJson},
18 platform::{CursorStyle, MouseButton},
19 AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
20 ViewHandle, WeakViewHandle,
21};
22use project::Project;
23use settings::Settings;
24use std::{ops::Range, sync::Arc};
25use theme::{AvatarStyle, Theme};
26use util::ResultExt;
27use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
28
29actions!(
30 collab,
31 [
32 ToggleCollaboratorList,
33 ToggleContactsMenu,
34 ToggleUserMenu,
35 ShareProject,
36 UnshareProject,
37 ]
38);
39
40impl_internal_actions!(collab, [LeaveCall]);
41
42#[derive(Copy, Clone, PartialEq)]
43pub(crate) struct LeaveCall;
44
45pub fn init(cx: &mut AppContext) {
46 cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
47 cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
48 cx.add_action(CollabTitlebarItem::share_project);
49 cx.add_action(CollabTitlebarItem::unshare_project);
50 cx.add_action(CollabTitlebarItem::leave_call);
51 cx.add_action(CollabTitlebarItem::toggle_user_menu);
52}
53
54pub struct CollabTitlebarItem {
55 project: ModelHandle<Project>,
56 user_store: ModelHandle<UserStore>,
57 client: Arc<Client>,
58 workspace: WeakViewHandle<Workspace>,
59 contacts_popover: Option<ViewHandle<ContactsPopover>>,
60 user_menu: ViewHandle<ContextMenu>,
61 collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
62 _subscriptions: Vec<Subscription>,
63}
64
65impl Entity for CollabTitlebarItem {
66 type Event = ();
67}
68
69impl View for CollabTitlebarItem {
70 fn ui_name() -> &'static str {
71 "CollabTitlebarItem"
72 }
73
74 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
75 let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
76 workspace
77 } else {
78 return Empty::new().into_any();
79 };
80
81 let project = self.project.read(cx);
82 let mut project_title = String::new();
83 for (i, name) in project.worktree_root_names(cx).enumerate() {
84 if i > 0 {
85 project_title.push_str(", ");
86 }
87 project_title.push_str(name);
88 }
89 if project_title.is_empty() {
90 project_title = "empty project".to_owned();
91 }
92
93 let theme = cx.global::<Settings>().theme.clone();
94
95 let mut left_container = Flex::row();
96 let mut right_container = Flex::row().align_children_center();
97
98 left_container.add_child(
99 Label::new(project_title, theme.workspace.titlebar.title.clone())
100 .contained()
101 .with_margin_right(theme.workspace.titlebar.item_spacing)
102 .aligned()
103 .left(),
104 );
105
106 let user = self.user_store.read(cx).current_user();
107 let peer_id = self.client.peer_id();
108 if let Some(((user, peer_id), room)) = user
109 .zip(peer_id)
110 .zip(ActiveCall::global(cx).read(cx).room().cloned())
111 {
112 left_container
113 .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
114
115 right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
116 right_container
117 .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx));
118 right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
119 }
120
121 let status = workspace.read(cx).client().status();
122 let status = &*status.borrow();
123
124 if matches!(status, client::Status::Connected { .. }) {
125 right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
126 right_container.add_child(self.render_user_menu_button(&theme, cx));
127 } else {
128 right_container.add_children(self.render_connection_status(status, cx));
129 right_container.add_child(self.render_sign_in_button(&theme, cx));
130 }
131
132 Stack::new()
133 .with_child(left_container)
134 .with_child(right_container.aligned().right())
135 .into_any()
136 }
137}
138
139impl CollabTitlebarItem {
140 pub fn new(
141 workspace: &Workspace,
142 workspace_handle: &ViewHandle<Workspace>,
143 cx: &mut ViewContext<Self>,
144 ) -> Self {
145 let project = workspace.project().clone();
146 let user_store = workspace.user_store().clone();
147 let client = workspace.client().clone();
148 let active_call = ActiveCall::global(cx);
149 let mut subscriptions = Vec::new();
150 subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
151 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
152 subscriptions.push(cx.observe_window_activation(|this, active, cx| {
153 this.window_activation_changed(active, cx)
154 }));
155 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
156 subscriptions.push(
157 cx.subscribe(&user_store, move |this, user_store, event, cx| {
158 if let Some(workspace) = this.workspace.upgrade(cx) {
159 workspace.update(cx, |workspace, cx| {
160 if let client::Event::Contact { user, kind } = event {
161 if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
162 workspace.show_notification(user.id as usize, cx, |cx| {
163 cx.add_view(|cx| {
164 ContactNotification::new(
165 user.clone(),
166 *kind,
167 user_store,
168 cx,
169 )
170 })
171 })
172 }
173 }
174 });
175 }
176 }),
177 );
178
179 Self {
180 workspace: workspace.weak_handle(),
181 project,
182 user_store,
183 client,
184 contacts_popover: None,
185 user_menu: cx.add_view(|cx| {
186 let mut menu = ContextMenu::new(cx);
187 menu.set_position_mode(OverlayPositionMode::Local);
188 menu
189 }),
190 collaborator_list_popover: None,
191 _subscriptions: subscriptions,
192 }
193 }
194
195 fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
196 let project = if active {
197 Some(self.project.clone())
198 } else {
199 None
200 };
201 ActiveCall::global(cx)
202 .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
203 .detach_and_log_err(cx);
204 }
205
206 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
207 if ActiveCall::global(cx).read(cx).room().is_none() {
208 self.contacts_popover = None;
209 }
210 cx.notify();
211 }
212
213 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
214 let active_call = ActiveCall::global(cx);
215 let project = self.project.clone();
216 active_call
217 .update(cx, |call, cx| call.share_project(project, cx))
218 .detach_and_log_err(cx);
219 }
220
221 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
222 let active_call = ActiveCall::global(cx);
223 let project = self.project.clone();
224 active_call
225 .update(cx, |call, cx| call.unshare_project(project, cx))
226 .log_err();
227 }
228
229 pub fn toggle_collaborator_list_popover(
230 &mut self,
231 _: &ToggleCollaboratorList,
232 cx: &mut ViewContext<Self>,
233 ) {
234 match self.collaborator_list_popover.take() {
235 Some(_) => {}
236 None => {
237 if let Some(workspace) = self.workspace.upgrade(cx) {
238 let user_store = workspace.read(cx).user_store().clone();
239 let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
240
241 cx.subscribe(&view, |this, _, event, cx| {
242 match event {
243 collaborator_list_popover::Event::Dismissed => {
244 this.collaborator_list_popover = None;
245 }
246 }
247
248 cx.notify();
249 })
250 .detach();
251
252 self.collaborator_list_popover = Some(view);
253 }
254 }
255 }
256 cx.notify();
257 }
258
259 pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
260 if self.contacts_popover.take().is_none() {
261 let view = cx.add_view(|cx| {
262 ContactsPopover::new(self.project.clone(), self.user_store.clone(), cx)
263 });
264 cx.subscribe(&view, |this, _, event, cx| {
265 match event {
266 contacts_popover::Event::Dismissed => {
267 this.contacts_popover = None;
268 }
269 }
270
271 cx.notify();
272 })
273 .detach();
274 self.contacts_popover = Some(view);
275 }
276
277 cx.notify();
278 }
279
280 pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
281 let theme = cx.global::<Settings>().theme.clone();
282 let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
283 let item_style = theme.context_menu.item.disabled_style().clone();
284 self.user_menu.update(cx, |user_menu, cx| {
285 let items = if let Some(user) = self.user_store.read(cx).current_user() {
286 vec![
287 ContextMenuItem::Static(Box::new(move |_| {
288 Flex::row()
289 .with_children(user.avatar.clone().map(|avatar| {
290 Self::render_face(
291 avatar,
292 avatar_style.clone(),
293 Color::transparent_black(),
294 )
295 }))
296 .with_child(Label::new(
297 user.github_login.clone(),
298 item_style.label.clone(),
299 ))
300 .contained()
301 .with_style(item_style.container)
302 .into_any()
303 })),
304 ContextMenuItem::item("Sign out", SignOut),
305 ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
306 ]
307 } else {
308 vec![
309 ContextMenuItem::item("Sign in", SignIn),
310 ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
311 ]
312 };
313
314 user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
315 });
316 }
317
318 fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
319 ActiveCall::global(cx)
320 .update(cx, |call, cx| call.hang_up(cx))
321 .detach_and_log_err(cx);
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.workspace.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(titlebar.toggle_contacts_button.default.icon_width)
346 .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
347 .aligned(),
348 )
349 };
350
351 Stack::new()
352 .with_child(
353 MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
354 let style = titlebar
355 .toggle_contacts_button
356 .style_for(state, self.contacts_popover.is_some());
357 Svg::new("icons/user_plus_16.svg")
358 .with_color(style.color)
359 .constrained()
360 .with_width(style.icon_width)
361 .aligned()
362 .constrained()
363 .with_width(style.button_width)
364 .with_height(style.button_width)
365 .contained()
366 .with_style(style.container)
367 })
368 .with_cursor_style(CursorStyle::PointingHand)
369 .on_click(MouseButton::Left, move |_, _, cx| {
370 cx.dispatch_action(ToggleContactsMenu);
371 })
372 .with_tooltip::<ToggleContactsMenu>(
373 0,
374 "Show contacts menu".into(),
375 Some(Box::new(ToggleContactsMenu)),
376 theme.tooltip.clone(),
377 cx,
378 ),
379 )
380 .with_children(badge)
381 .with_children(self.render_contacts_popover_host(titlebar, cx))
382 .into_any()
383 }
384
385 fn render_toggle_screen_sharing_button(
386 &self,
387 theme: &Theme,
388 room: &ModelHandle<Room>,
389 cx: &mut ViewContext<Self>,
390 ) -> AnyElement<Self> {
391 let icon;
392 let tooltip;
393 if room.read(cx).is_screen_sharing() {
394 icon = "icons/enable_screen_sharing_12.svg";
395 tooltip = "Stop Sharing Screen"
396 } else {
397 icon = "icons/disable_screen_sharing_12.svg";
398 tooltip = "Share Screen";
399 }
400
401 let titlebar = &theme.workspace.titlebar;
402 MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
403 let style = titlebar.call_control.style_for(state, false);
404 Svg::new(icon)
405 .with_color(style.color)
406 .constrained()
407 .with_width(style.icon_width)
408 .aligned()
409 .constrained()
410 .with_width(style.button_width)
411 .with_height(style.button_width)
412 .contained()
413 .with_style(style.container)
414 })
415 .with_cursor_style(CursorStyle::PointingHand)
416 .on_click(MouseButton::Left, move |_, _, cx| {
417 cx.dispatch_action(ToggleScreenSharing);
418 })
419 .with_tooltip::<ToggleScreenSharing>(
420 0,
421 tooltip.into(),
422 Some(Box::new(ToggleScreenSharing)),
423 theme.tooltip.clone(),
424 cx,
425 )
426 .aligned()
427 .into_any()
428 }
429
430 fn render_in_call_share_unshare_button(
431 &self,
432 workspace: &ViewHandle<Workspace>,
433 theme: &Theme,
434 cx: &mut ViewContext<Self>,
435 ) -> Option<AnyElement<Self>> {
436 let project = workspace.read(cx).project();
437 if project.read(cx).is_remote() {
438 return None;
439 }
440
441 let is_shared = project.read(cx).is_shared();
442 let label = if is_shared { "Unshare" } else { "Share" };
443 let tooltip = if is_shared {
444 "Unshare project from call participants"
445 } else {
446 "Share project with call participants"
447 };
448
449 let titlebar = &theme.workspace.titlebar;
450
451 enum ShareUnshare {}
452 Some(
453 Stack::new()
454 .with_child(
455 MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
456 //TODO: Ensure this button has consistant width for both text variations
457 let style = titlebar.share_button.style_for(state, false);
458 Label::new(label, style.text.clone())
459 .contained()
460 .with_style(style.container)
461 })
462 .with_cursor_style(CursorStyle::PointingHand)
463 .on_click(MouseButton::Left, move |_, _, cx| {
464 if is_shared {
465 cx.dispatch_action(UnshareProject);
466 } else {
467 cx.dispatch_action(ShareProject);
468 }
469 })
470 .with_tooltip::<ShareUnshare>(
471 0,
472 tooltip.to_owned(),
473 None,
474 theme.tooltip.clone(),
475 cx,
476 ),
477 )
478 .aligned()
479 .contained()
480 .with_margin_left(theme.workspace.titlebar.item_spacing)
481 .into_any(),
482 )
483 }
484
485 fn render_user_menu_button(
486 &self,
487 theme: &Theme,
488 cx: &mut ViewContext<Self>,
489 ) -> AnyElement<Self> {
490 let titlebar = &theme.workspace.titlebar;
491
492 Stack::new()
493 .with_child(
494 MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
495 let style = titlebar.call_control.style_for(state, false);
496 Svg::new("icons/ellipsis_14.svg")
497 .with_color(style.color)
498 .constrained()
499 .with_width(style.icon_width)
500 .aligned()
501 .constrained()
502 .with_width(style.button_width)
503 .with_height(style.button_width)
504 .contained()
505 .with_style(style.container)
506 })
507 .with_cursor_style(CursorStyle::PointingHand)
508 .on_click(MouseButton::Left, move |_, _, cx| {
509 cx.dispatch_action(ToggleUserMenu);
510 })
511 .with_tooltip::<ToggleUserMenu>(
512 0,
513 "Toggle user menu".to_owned(),
514 Some(Box::new(ToggleUserMenu)),
515 theme.tooltip.clone(),
516 cx,
517 )
518 .contained()
519 .with_margin_left(theme.workspace.titlebar.item_spacing),
520 )
521 .with_child(
522 ChildView::new(&self.user_menu, cx)
523 .aligned()
524 .bottom()
525 .right(),
526 )
527 .into_any()
528 }
529
530 fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
531 let titlebar = &theme.workspace.titlebar;
532 MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
533 let style = titlebar.sign_in_prompt.style_for(state, false);
534 Label::new("Sign In", style.text.clone())
535 .contained()
536 .with_style(style.container)
537 })
538 .with_cursor_style(CursorStyle::PointingHand)
539 .on_click(MouseButton::Left, move |_, _, cx| {
540 cx.dispatch_action(SignIn);
541 })
542 .into_any()
543 }
544
545 fn render_contacts_popover_host<'a>(
546 &'a self,
547 _theme: &'a theme::Titlebar,
548 cx: &'a ViewContext<Self>,
549 ) -> Option<AnyElement<Self>> {
550 self.contacts_popover.as_ref().map(|popover| {
551 Overlay::new(ChildView::new(popover, cx))
552 .with_fit_mode(OverlayFitMode::SwitchAnchor)
553 .with_anchor_corner(AnchorCorner::TopRight)
554 .with_z_index(999)
555 .aligned()
556 .bottom()
557 .right()
558 .into_any()
559 })
560 }
561
562 fn render_collaborators(
563 &self,
564 workspace: &ViewHandle<Workspace>,
565 theme: &Theme,
566 room: &ModelHandle<Room>,
567 cx: &mut ViewContext<Self>,
568 ) -> Vec<Container<Self>> {
569 let mut participants = room
570 .read(cx)
571 .remote_participants()
572 .values()
573 .cloned()
574 .collect::<Vec<_>>();
575 participants.sort_by_cached_key(|p| p.user.github_login.clone());
576
577 participants
578 .into_iter()
579 .filter_map(|participant| {
580 let project = workspace.read(cx).project().read(cx);
581 let replica_id = project
582 .collaborators()
583 .get(&participant.peer_id)
584 .map(|collaborator| collaborator.replica_id);
585 let user = participant.user.clone();
586 Some(
587 Container::new(self.render_face_pile(
588 &user,
589 replica_id,
590 participant.peer_id,
591 Some(participant.location),
592 workspace,
593 theme,
594 cx,
595 ))
596 .with_margin_right(theme.workspace.titlebar.face_pile_spacing),
597 )
598 })
599 .collect()
600 }
601
602 fn render_current_user(
603 &self,
604 workspace: &ViewHandle<Workspace>,
605 theme: &Theme,
606 user: &Arc<User>,
607 peer_id: PeerId,
608 cx: &mut ViewContext<Self>,
609 ) -> AnyElement<Self> {
610 let replica_id = workspace.read(cx).project().read(cx).replica_id();
611 Container::new(self.render_face_pile(
612 user,
613 Some(replica_id),
614 peer_id,
615 None,
616 workspace,
617 theme,
618 cx,
619 ))
620 .with_margin_right(theme.workspace.titlebar.item_spacing)
621 .into_any()
622 }
623
624 fn render_face_pile(
625 &self,
626 user: &User,
627 replica_id: Option<ReplicaId>,
628 peer_id: PeerId,
629 location: Option<ParticipantLocation>,
630 workspace: &ViewHandle<Workspace>,
631 theme: &Theme,
632 cx: &mut ViewContext<Self>,
633 ) -> AnyElement<Self> {
634 let project_id = workspace.read(cx).project().read(cx).remote_id();
635 let room = ActiveCall::global(cx).read(cx).room();
636 let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
637 let followed_by_self = room
638 .and_then(|room| {
639 Some(
640 is_being_followed
641 && room
642 .read(cx)
643 .followers_for(peer_id, project_id?)
644 .iter()
645 .any(|&follower| {
646 Some(follower) == workspace.read(cx).client().peer_id()
647 }),
648 )
649 })
650 .unwrap_or(false);
651
652 let leader_style = theme.workspace.titlebar.leader_avatar;
653 let follower_style = theme.workspace.titlebar.follower_avatar;
654
655 let mut background_color = theme
656 .workspace
657 .titlebar
658 .container
659 .background_color
660 .unwrap_or_default();
661 if let Some(replica_id) = replica_id {
662 if followed_by_self {
663 let selection = theme.editor.replica_selection_style(replica_id).selection;
664 background_color = Color::blend(selection, background_color);
665 background_color.a = 255;
666 }
667 }
668
669 let mut content = Stack::new()
670 .with_children(user.avatar.as_ref().map(|avatar| {
671 let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
672 .with_child(Self::render_face(
673 avatar.clone(),
674 Self::location_style(workspace, location, leader_style, cx),
675 background_color,
676 ))
677 .with_children(
678 (|| {
679 let project_id = project_id?;
680 let room = room?.read(cx);
681 let followers = room.followers_for(peer_id, project_id);
682
683 Some(followers.into_iter().flat_map(|&follower| {
684 let remote_participant =
685 room.remote_participant_for_peer_id(follower);
686
687 let avatar = remote_participant
688 .and_then(|p| p.user.avatar.clone())
689 .or_else(|| {
690 if follower == workspace.read(cx).client().peer_id()? {
691 workspace
692 .read(cx)
693 .user_store()
694 .read(cx)
695 .current_user()?
696 .avatar
697 .clone()
698 } else {
699 None
700 }
701 })?;
702
703 Some(Self::render_face(
704 avatar.clone(),
705 follower_style,
706 background_color,
707 ))
708 }))
709 })()
710 .into_iter()
711 .flatten(),
712 );
713
714 let mut container = face_pile
715 .contained()
716 .with_style(theme.workspace.titlebar.leader_selection);
717
718 if let Some(replica_id) = replica_id {
719 if followed_by_self {
720 let color = theme.editor.replica_selection_style(replica_id).selection;
721 container = container.with_background_color(color);
722 }
723 }
724
725 container
726 }))
727 .with_children((|| {
728 let replica_id = replica_id?;
729 let color = theme.editor.replica_selection_style(replica_id).cursor;
730 Some(
731 AvatarRibbon::new(color)
732 .constrained()
733 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
734 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
735 .aligned()
736 .bottom(),
737 )
738 })())
739 .into_any();
740
741 if let Some(location) = location {
742 if let Some(replica_id) = replica_id {
743 content = MouseEventHandler::<ToggleFollow, Self>::new(
744 replica_id.into(),
745 cx,
746 move |_, _| content,
747 )
748 .with_cursor_style(CursorStyle::PointingHand)
749 .on_click(MouseButton::Left, move |_, _, cx| {
750 cx.dispatch_action(ToggleFollow(peer_id))
751 })
752 .with_tooltip::<ToggleFollow>(
753 peer_id.as_u64() as usize,
754 if is_being_followed {
755 format!("Unfollow {}", user.github_login)
756 } else {
757 format!("Follow {}", user.github_login)
758 },
759 Some(Box::new(FollowNextCollaborator)),
760 theme.tooltip.clone(),
761 cx,
762 )
763 .into_any();
764 } else if let ParticipantLocation::SharedProject { project_id } = location {
765 let user_id = user.id;
766 content = MouseEventHandler::<JoinProject, Self>::new(
767 peer_id.as_u64() as usize,
768 cx,
769 move |_, _| content,
770 )
771 .with_cursor_style(CursorStyle::PointingHand)
772 .on_click(MouseButton::Left, move |_, _, cx| {
773 cx.dispatch_action(JoinProject {
774 project_id,
775 follow_user_id: user_id,
776 })
777 })
778 .with_tooltip::<JoinProject>(
779 peer_id.as_u64() as usize,
780 format!("Follow {} into external project", user.github_login),
781 Some(Box::new(FollowNextCollaborator)),
782 theme.tooltip.clone(),
783 cx,
784 )
785 .into_any();
786 }
787 }
788 content
789 }
790
791 fn location_style(
792 workspace: &ViewHandle<Workspace>,
793 location: Option<ParticipantLocation>,
794 mut style: AvatarStyle,
795 cx: &ViewContext<Self>,
796 ) -> AvatarStyle {
797 if let Some(location) = location {
798 if let ParticipantLocation::SharedProject { project_id } = location {
799 if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
800 style.image.grayscale = true;
801 }
802 } else {
803 style.image.grayscale = true;
804 }
805 }
806
807 style
808 }
809
810 fn render_face<V: View>(
811 avatar: Arc<ImageData>,
812 avatar_style: AvatarStyle,
813 background_color: Color,
814 ) -> AnyElement<V> {
815 Image::from_data(avatar)
816 .with_style(avatar_style.image)
817 .aligned()
818 .contained()
819 .with_background_color(background_color)
820 .with_corner_radius(avatar_style.outer_corner_radius)
821 .constrained()
822 .with_width(avatar_style.outer_width)
823 .with_height(avatar_style.outer_width)
824 .aligned()
825 .into_any()
826 }
827
828 fn render_connection_status(
829 &self,
830 status: &client::Status,
831 cx: &mut ViewContext<Self>,
832 ) -> Option<AnyElement<Self>> {
833 enum ConnectionStatusButton {}
834
835 let theme = &cx.global::<Settings>().theme.clone();
836 match status {
837 client::Status::ConnectionError
838 | client::Status::ConnectionLost
839 | client::Status::Reauthenticating { .. }
840 | client::Status::Reconnecting { .. }
841 | client::Status::ReconnectionError { .. } => Some(
842 Svg::new("icons/cloud_slash_12.svg")
843 .with_color(theme.workspace.titlebar.offline_icon.color)
844 .constrained()
845 .with_width(theme.workspace.titlebar.offline_icon.width)
846 .aligned()
847 .contained()
848 .with_style(theme.workspace.titlebar.offline_icon.container)
849 .into_any(),
850 ),
851 client::Status::UpgradeRequired => Some(
852 MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
853 Label::new(
854 "Please update Zed to collaborate",
855 theme.workspace.titlebar.outdated_warning.text.clone(),
856 )
857 .contained()
858 .with_style(theme.workspace.titlebar.outdated_warning.container)
859 .aligned()
860 })
861 .with_cursor_style(CursorStyle::PointingHand)
862 .on_click(MouseButton::Left, |_, _, cx| {
863 cx.dispatch_action(auto_update::Check);
864 })
865 .into_any(),
866 ),
867 _ => None,
868 }
869 }
870}
871
872pub struct AvatarRibbon {
873 color: Color,
874}
875
876impl AvatarRibbon {
877 pub fn new(color: Color) -> AvatarRibbon {
878 AvatarRibbon { color }
879 }
880}
881
882impl Element<CollabTitlebarItem> for AvatarRibbon {
883 type LayoutState = ();
884
885 type PaintState = ();
886
887 fn layout(
888 &mut self,
889 constraint: gpui::SizeConstraint,
890 _: &mut CollabTitlebarItem,
891 _: &mut ViewContext<CollabTitlebarItem>,
892 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
893 (constraint.max, ())
894 }
895
896 fn paint(
897 &mut self,
898 scene: &mut SceneBuilder,
899 bounds: RectF,
900 _: RectF,
901 _: &mut Self::LayoutState,
902 _: &mut CollabTitlebarItem,
903 _: &mut ViewContext<CollabTitlebarItem>,
904 ) -> Self::PaintState {
905 let mut path = PathBuilder::new();
906 path.reset(bounds.lower_left());
907 path.curve_to(
908 bounds.origin() + vec2f(bounds.height(), 0.),
909 bounds.origin(),
910 );
911 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
912 path.curve_to(bounds.lower_right(), bounds.upper_right());
913 path.line_to(bounds.lower_left());
914 scene.push_path(path.build(self.color, None));
915 }
916
917 fn rect_for_text_range(
918 &self,
919 _: Range<usize>,
920 _: RectF,
921 _: RectF,
922 _: &Self::LayoutState,
923 _: &Self::PaintState,
924 _: &CollabTitlebarItem,
925 _: &ViewContext<CollabTitlebarItem>,
926 ) -> Option<RectF> {
927 None
928 }
929
930 fn debug(
931 &self,
932 bounds: RectF,
933 _: &Self::LayoutState,
934 _: &Self::PaintState,
935 _: &CollabTitlebarItem,
936 _: &ViewContext<CollabTitlebarItem>,
937 ) -> gpui::json::Value {
938 json::json!({
939 "type": "AvatarRibbon",
940 "bounds": bounds.to_json(),
941 "color": self.color.to_json(),
942 })
943 }
944}