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.context_menu.item.disabled_style().clone();
303 self.user_menu.update(cx, |user_menu, cx| {
304 let items = if let Some(user) = self.user_store.read(cx).current_user() {
305 vec![
306 ContextMenuItem::Static(Box::new(move |_| {
307 Flex::row()
308 .with_children(user.avatar.clone().map(|avatar| {
309 Self::render_face(
310 avatar,
311 avatar_style.clone(),
312 Color::transparent_black(),
313 )
314 }))
315 .with_child(Label::new(
316 user.github_login.clone(),
317 item_style.label.clone(),
318 ))
319 .contained()
320 .with_style(item_style.container)
321 .into_any()
322 })),
323 ContextMenuItem::action("Sign out", SignOut),
324 ContextMenuItem::action(
325 "Send Feedback",
326 feedback::feedback_editor::GiveFeedback,
327 ),
328 ]
329 } else {
330 vec![
331 ContextMenuItem::action("Sign in", SignIn),
332 ContextMenuItem::action(
333 "Send Feedback",
334 feedback::feedback_editor::GiveFeedback,
335 ),
336 ]
337 };
338
339 user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
340 });
341 }
342
343 fn render_toggle_contacts_button(
344 &self,
345 theme: &Theme,
346 cx: &mut ViewContext<Self>,
347 ) -> AnyElement<Self> {
348 let titlebar = &theme.workspace.titlebar;
349
350 let badge = if self
351 .user_store
352 .read(cx)
353 .incoming_contact_requests()
354 .is_empty()
355 {
356 None
357 } else {
358 Some(
359 Empty::new()
360 .collapsed()
361 .contained()
362 .with_style(titlebar.toggle_contacts_badge)
363 .contained()
364 .with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
365 .with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
366 .aligned(),
367 )
368 };
369
370 Stack::new()
371 .with_child(
372 MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
373 let style = titlebar
374 .toggle_contacts_button
375 .style_for(state, self.contacts_popover.is_some());
376 Svg::new("icons/user_plus_16.svg")
377 .with_color(style.color)
378 .constrained()
379 .with_width(style.icon_width)
380 .aligned()
381 .constrained()
382 .with_width(style.button_width)
383 .with_height(style.button_width)
384 .contained()
385 .with_style(style.container)
386 })
387 .with_cursor_style(CursorStyle::PointingHand)
388 .on_click(MouseButton::Left, move |_, this, cx| {
389 this.toggle_contacts_popover(&Default::default(), cx)
390 })
391 .with_tooltip::<ToggleContactsMenu>(
392 0,
393 "Show contacts menu".into(),
394 Some(Box::new(ToggleContactsMenu)),
395 theme.tooltip.clone(),
396 cx,
397 ),
398 )
399 .with_children(badge)
400 .with_children(self.render_contacts_popover_host(titlebar, cx))
401 .into_any()
402 }
403
404 fn render_toggle_screen_sharing_button(
405 &self,
406 theme: &Theme,
407 room: &ModelHandle<Room>,
408 cx: &mut ViewContext<Self>,
409 ) -> AnyElement<Self> {
410 let icon;
411 let tooltip;
412 if room.read(cx).is_screen_sharing() {
413 icon = "icons/enable_screen_sharing_12.svg";
414 tooltip = "Stop Sharing Screen"
415 } else {
416 icon = "icons/disable_screen_sharing_12.svg";
417 tooltip = "Share Screen";
418 }
419
420 let titlebar = &theme.workspace.titlebar;
421 MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
422 let style = titlebar.call_control.style_for(state, false);
423 Svg::new(icon)
424 .with_color(style.color)
425 .constrained()
426 .with_width(style.icon_width)
427 .aligned()
428 .constrained()
429 .with_width(style.button_width)
430 .with_height(style.button_width)
431 .contained()
432 .with_style(style.container)
433 })
434 .with_cursor_style(CursorStyle::PointingHand)
435 .on_click(MouseButton::Left, move |_, _, cx| {
436 toggle_screen_sharing(&Default::default(), cx)
437 })
438 .with_tooltip::<ToggleScreenSharing>(
439 0,
440 tooltip.into(),
441 Some(Box::new(ToggleScreenSharing)),
442 theme.tooltip.clone(),
443 cx,
444 )
445 .aligned()
446 .into_any()
447 }
448
449 fn render_in_call_share_unshare_button(
450 &self,
451 workspace: &ViewHandle<Workspace>,
452 theme: &Theme,
453 cx: &mut ViewContext<Self>,
454 ) -> Option<AnyElement<Self>> {
455 let project = workspace.read(cx).project();
456 if project.read(cx).is_remote() {
457 return None;
458 }
459
460 let is_shared = project.read(cx).is_shared();
461 let label = if is_shared { "Unshare" } else { "Share" };
462 let tooltip = if is_shared {
463 "Unshare project from call participants"
464 } else {
465 "Share project with call participants"
466 };
467
468 let titlebar = &theme.workspace.titlebar;
469
470 enum ShareUnshare {}
471 Some(
472 Stack::new()
473 .with_child(
474 MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
475 //TODO: Ensure this button has consistent width for both text variations
476 let style = titlebar.share_button.style_for(state, false);
477 Label::new(label, style.text.clone())
478 .contained()
479 .with_style(style.container)
480 })
481 .with_cursor_style(CursorStyle::PointingHand)
482 .on_click(MouseButton::Left, move |_, this, cx| {
483 if is_shared {
484 this.unshare_project(&Default::default(), cx);
485 } else {
486 this.share_project(&Default::default(), cx);
487 }
488 })
489 .with_tooltip::<ShareUnshare>(
490 0,
491 tooltip.to_owned(),
492 None,
493 theme.tooltip.clone(),
494 cx,
495 ),
496 )
497 .aligned()
498 .contained()
499 .with_margin_left(theme.workspace.titlebar.item_spacing)
500 .into_any(),
501 )
502 }
503
504 fn render_user_menu_button(
505 &self,
506 theme: &Theme,
507 cx: &mut ViewContext<Self>,
508 ) -> AnyElement<Self> {
509 let titlebar = &theme.workspace.titlebar;
510
511 Stack::new()
512 .with_child(
513 MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
514 let style = titlebar.call_control.style_for(state, false);
515 Svg::new("icons/ellipsis_14.svg")
516 .with_color(style.color)
517 .constrained()
518 .with_width(style.icon_width)
519 .aligned()
520 .constrained()
521 .with_width(style.button_width)
522 .with_height(style.button_width)
523 .contained()
524 .with_style(style.container)
525 })
526 .with_cursor_style(CursorStyle::PointingHand)
527 .on_click(MouseButton::Left, move |_, this, cx| {
528 this.toggle_user_menu(&Default::default(), cx)
529 })
530 .with_tooltip::<ToggleUserMenu>(
531 0,
532 "Toggle user menu".to_owned(),
533 Some(Box::new(ToggleUserMenu)),
534 theme.tooltip.clone(),
535 cx,
536 )
537 .contained()
538 .with_margin_left(theme.workspace.titlebar.item_spacing),
539 )
540 .with_child(
541 ChildView::new(&self.user_menu, cx)
542 .aligned()
543 .bottom()
544 .right(),
545 )
546 .into_any()
547 }
548
549 fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
550 let titlebar = &theme.workspace.titlebar;
551 MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
552 let style = titlebar.sign_in_prompt.style_for(state, false);
553 Label::new("Sign In", style.text.clone())
554 .contained()
555 .with_style(style.container)
556 })
557 .with_cursor_style(CursorStyle::PointingHand)
558 .on_click(MouseButton::Left, move |_, this, cx| {
559 let client = this.client.clone();
560 cx.app_context()
561 .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
562 .detach_and_log_err(cx);
563 })
564 .into_any()
565 }
566
567 fn render_contacts_popover_host<'a>(
568 &'a self,
569 _theme: &'a theme::Titlebar,
570 cx: &'a ViewContext<Self>,
571 ) -> Option<AnyElement<Self>> {
572 self.contacts_popover.as_ref().map(|popover| {
573 Overlay::new(ChildView::new(popover, cx))
574 .with_fit_mode(OverlayFitMode::SwitchAnchor)
575 .with_anchor_corner(AnchorCorner::TopRight)
576 .with_z_index(999)
577 .aligned()
578 .bottom()
579 .right()
580 .into_any()
581 })
582 }
583
584 fn render_collaborators(
585 &self,
586 workspace: &ViewHandle<Workspace>,
587 theme: &Theme,
588 room: &ModelHandle<Room>,
589 cx: &mut ViewContext<Self>,
590 ) -> Vec<Container<Self>> {
591 let mut participants = room
592 .read(cx)
593 .remote_participants()
594 .values()
595 .cloned()
596 .collect::<Vec<_>>();
597 participants.sort_by_cached_key(|p| p.user.github_login.clone());
598
599 participants
600 .into_iter()
601 .filter_map(|participant| {
602 let project = workspace.read(cx).project().read(cx);
603 let replica_id = project
604 .collaborators()
605 .get(&participant.peer_id)
606 .map(|collaborator| collaborator.replica_id);
607 let user = participant.user.clone();
608 Some(
609 Container::new(self.render_face_pile(
610 &user,
611 replica_id,
612 participant.peer_id,
613 Some(participant.location),
614 workspace,
615 theme,
616 cx,
617 ))
618 .with_margin_right(theme.workspace.titlebar.face_pile_spacing),
619 )
620 })
621 .collect()
622 }
623
624 fn render_current_user(
625 &self,
626 workspace: &ViewHandle<Workspace>,
627 theme: &Theme,
628 user: &Arc<User>,
629 peer_id: PeerId,
630 cx: &mut ViewContext<Self>,
631 ) -> AnyElement<Self> {
632 let replica_id = workspace.read(cx).project().read(cx).replica_id();
633 Container::new(self.render_face_pile(
634 user,
635 Some(replica_id),
636 peer_id,
637 None,
638 workspace,
639 theme,
640 cx,
641 ))
642 .with_margin_right(theme.workspace.titlebar.item_spacing)
643 .into_any()
644 }
645
646 fn render_face_pile(
647 &self,
648 user: &User,
649 replica_id: Option<ReplicaId>,
650 peer_id: PeerId,
651 location: Option<ParticipantLocation>,
652 workspace: &ViewHandle<Workspace>,
653 theme: &Theme,
654 cx: &mut ViewContext<Self>,
655 ) -> AnyElement<Self> {
656 let project_id = workspace.read(cx).project().read(cx).remote_id();
657 let room = ActiveCall::global(cx).read(cx).room();
658 let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
659 let followed_by_self = room
660 .and_then(|room| {
661 Some(
662 is_being_followed
663 && room
664 .read(cx)
665 .followers_for(peer_id, project_id?)
666 .iter()
667 .any(|&follower| {
668 Some(follower) == workspace.read(cx).client().peer_id()
669 }),
670 )
671 })
672 .unwrap_or(false);
673
674 let leader_style = theme.workspace.titlebar.leader_avatar;
675 let follower_style = theme.workspace.titlebar.follower_avatar;
676
677 let mut background_color = theme
678 .workspace
679 .titlebar
680 .container
681 .background_color
682 .unwrap_or_default();
683 if let Some(replica_id) = replica_id {
684 if followed_by_self {
685 let selection = theme.editor.replica_selection_style(replica_id).selection;
686 background_color = Color::blend(selection, background_color);
687 background_color.a = 255;
688 }
689 }
690
691 let mut content = Stack::new()
692 .with_children(user.avatar.as_ref().map(|avatar| {
693 let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
694 .with_child(Self::render_face(
695 avatar.clone(),
696 Self::location_style(workspace, location, leader_style, cx),
697 background_color,
698 ))
699 .with_children(
700 (|| {
701 let project_id = project_id?;
702 let room = room?.read(cx);
703 let followers = room.followers_for(peer_id, project_id);
704
705 Some(followers.into_iter().flat_map(|&follower| {
706 let remote_participant =
707 room.remote_participant_for_peer_id(follower);
708
709 let avatar = remote_participant
710 .and_then(|p| p.user.avatar.clone())
711 .or_else(|| {
712 if follower == workspace.read(cx).client().peer_id()? {
713 workspace
714 .read(cx)
715 .user_store()
716 .read(cx)
717 .current_user()?
718 .avatar
719 .clone()
720 } else {
721 None
722 }
723 })?;
724
725 Some(Self::render_face(
726 avatar.clone(),
727 follower_style,
728 background_color,
729 ))
730 }))
731 })()
732 .into_iter()
733 .flatten(),
734 );
735
736 let mut container = face_pile
737 .contained()
738 .with_style(theme.workspace.titlebar.leader_selection);
739
740 if let Some(replica_id) = replica_id {
741 if followed_by_self {
742 let color = theme.editor.replica_selection_style(replica_id).selection;
743 container = container.with_background_color(color);
744 }
745 }
746
747 container
748 }))
749 .with_children((|| {
750 let replica_id = replica_id?;
751 let color = theme.editor.replica_selection_style(replica_id).cursor;
752 Some(
753 AvatarRibbon::new(color)
754 .constrained()
755 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
756 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
757 .aligned()
758 .bottom(),
759 )
760 })())
761 .into_any();
762
763 if let Some(location) = location {
764 if let Some(replica_id) = replica_id {
765 enum ToggleFollow {}
766
767 content = MouseEventHandler::<ToggleFollow, Self>::new(
768 replica_id.into(),
769 cx,
770 move |_, _| content,
771 )
772 .with_cursor_style(CursorStyle::PointingHand)
773 .on_click(MouseButton::Left, move |_, item, cx| {
774 if let Some(workspace) = item.workspace.upgrade(cx) {
775 if let Some(task) = workspace
776 .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
777 {
778 task.detach_and_log_err(cx);
779 }
780 }
781 })
782 .with_tooltip::<ToggleFollow>(
783 peer_id.as_u64() as usize,
784 if is_being_followed {
785 format!("Unfollow {}", user.github_login)
786 } else {
787 format!("Follow {}", user.github_login)
788 },
789 Some(Box::new(FollowNextCollaborator)),
790 theme.tooltip.clone(),
791 cx,
792 )
793 .into_any();
794 } else if let ParticipantLocation::SharedProject { project_id } = location {
795 enum JoinProject {}
796
797 let user_id = user.id;
798 content = MouseEventHandler::<JoinProject, Self>::new(
799 peer_id.as_u64() as usize,
800 cx,
801 move |_, _| content,
802 )
803 .with_cursor_style(CursorStyle::PointingHand)
804 .on_click(MouseButton::Left, move |_, this, cx| {
805 if let Some(workspace) = this.workspace.upgrade(cx) {
806 let app_state = workspace.read(cx).app_state().clone();
807 workspace::join_remote_project(project_id, user_id, app_state, cx)
808 .detach_and_log_err(cx);
809 }
810 })
811 .with_tooltip::<JoinProject>(
812 peer_id.as_u64() as usize,
813 format!("Follow {} into external project", user.github_login),
814 Some(Box::new(FollowNextCollaborator)),
815 theme.tooltip.clone(),
816 cx,
817 )
818 .into_any();
819 }
820 }
821 content
822 }
823
824 fn location_style(
825 workspace: &ViewHandle<Workspace>,
826 location: Option<ParticipantLocation>,
827 mut style: AvatarStyle,
828 cx: &ViewContext<Self>,
829 ) -> AvatarStyle {
830 if let Some(location) = location {
831 if let ParticipantLocation::SharedProject { project_id } = location {
832 if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
833 style.image.grayscale = true;
834 }
835 } else {
836 style.image.grayscale = true;
837 }
838 }
839
840 style
841 }
842
843 fn render_face<V: View>(
844 avatar: Arc<ImageData>,
845 avatar_style: AvatarStyle,
846 background_color: Color,
847 ) -> AnyElement<V> {
848 Image::from_data(avatar)
849 .with_style(avatar_style.image)
850 .aligned()
851 .contained()
852 .with_background_color(background_color)
853 .with_corner_radius(avatar_style.outer_corner_radius)
854 .constrained()
855 .with_width(avatar_style.outer_width)
856 .with_height(avatar_style.outer_width)
857 .aligned()
858 .into_any()
859 }
860
861 fn render_connection_status(
862 &self,
863 status: &client::Status,
864 cx: &mut ViewContext<Self>,
865 ) -> Option<AnyElement<Self>> {
866 enum ConnectionStatusButton {}
867
868 let theme = &theme::current(cx).clone();
869 match status {
870 client::Status::ConnectionError
871 | client::Status::ConnectionLost
872 | client::Status::Reauthenticating { .. }
873 | client::Status::Reconnecting { .. }
874 | client::Status::ReconnectionError { .. } => Some(
875 Svg::new("icons/cloud_slash_12.svg")
876 .with_color(theme.workspace.titlebar.offline_icon.color)
877 .constrained()
878 .with_width(theme.workspace.titlebar.offline_icon.width)
879 .aligned()
880 .contained()
881 .with_style(theme.workspace.titlebar.offline_icon.container)
882 .into_any(),
883 ),
884 client::Status::UpgradeRequired => Some(
885 MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
886 Label::new(
887 "Please update Zed to collaborate",
888 theme.workspace.titlebar.outdated_warning.text.clone(),
889 )
890 .contained()
891 .with_style(theme.workspace.titlebar.outdated_warning.container)
892 .aligned()
893 })
894 .with_cursor_style(CursorStyle::PointingHand)
895 .on_click(MouseButton::Left, |_, _, cx| {
896 auto_update::check(&Default::default(), cx);
897 })
898 .into_any(),
899 ),
900 _ => None,
901 }
902 }
903}
904
905pub struct AvatarRibbon {
906 color: Color,
907}
908
909impl AvatarRibbon {
910 pub fn new(color: Color) -> AvatarRibbon {
911 AvatarRibbon { color }
912 }
913}
914
915impl Element<CollabTitlebarItem> for AvatarRibbon {
916 type LayoutState = ();
917
918 type PaintState = ();
919
920 fn layout(
921 &mut self,
922 constraint: gpui::SizeConstraint,
923 _: &mut CollabTitlebarItem,
924 _: &mut LayoutContext<CollabTitlebarItem>,
925 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
926 (constraint.max, ())
927 }
928
929 fn paint(
930 &mut self,
931 scene: &mut SceneBuilder,
932 bounds: RectF,
933 _: RectF,
934 _: &mut Self::LayoutState,
935 _: &mut CollabTitlebarItem,
936 _: &mut ViewContext<CollabTitlebarItem>,
937 ) -> Self::PaintState {
938 let mut path = PathBuilder::new();
939 path.reset(bounds.lower_left());
940 path.curve_to(
941 bounds.origin() + vec2f(bounds.height(), 0.),
942 bounds.origin(),
943 );
944 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
945 path.curve_to(bounds.lower_right(), bounds.upper_right());
946 path.line_to(bounds.lower_left());
947 scene.push_path(path.build(self.color, None));
948 }
949
950 fn rect_for_text_range(
951 &self,
952 _: Range<usize>,
953 _: RectF,
954 _: RectF,
955 _: &Self::LayoutState,
956 _: &Self::PaintState,
957 _: &CollabTitlebarItem,
958 _: &ViewContext<CollabTitlebarItem>,
959 ) -> Option<RectF> {
960 None
961 }
962
963 fn debug(
964 &self,
965 bounds: RectF,
966 _: &Self::LayoutState,
967 _: &Self::PaintState,
968 _: &CollabTitlebarItem,
969 _: &ViewContext<CollabTitlebarItem>,
970 ) -> gpui::json::Value {
971 json::json!({
972 "type": "AvatarRibbon",
973 "bounds": bounds.to_json(),
974 "color": self.color.to_json(),
975 })
976 }
977}