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