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