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
463 .share_button
464 .style_for(state, self.contacts_popover.is_some());
465 Label::new(label, style.text.clone())
466 .contained()
467 .with_style(style.container)
468 .boxed()
469 })
470 .with_cursor_style(CursorStyle::PointingHand)
471 .on_click(MouseButton::Left, move |_, cx| {
472 if is_shared {
473 cx.dispatch_action(UnshareProject);
474 } else {
475 cx.dispatch_action(ShareProject);
476 }
477 })
478 .with_tooltip::<ShareUnshare, _>(
479 0,
480 tooltip.to_owned(),
481 None,
482 theme.tooltip.clone(),
483 cx,
484 )
485 .boxed(),
486 )
487 .aligned()
488 .contained()
489 .with_margin_left(theme.workspace.titlebar.item_spacing)
490 .boxed(),
491 )
492 }
493
494 fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
495 let titlebar = &theme.workspace.titlebar;
496
497 Stack::new()
498 .with_child(
499 MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
500 let style = titlebar.call_control.style_for(state, false);
501 Svg::new("icons/ellipsis_14.svg")
502 .with_color(style.color)
503 .constrained()
504 .with_width(style.icon_width)
505 .aligned()
506 .constrained()
507 .with_width(style.button_width)
508 .with_height(style.button_width)
509 .contained()
510 .with_style(style.container)
511 .boxed()
512 })
513 .with_cursor_style(CursorStyle::PointingHand)
514 .on_click(MouseButton::Left, move |_, cx| {
515 cx.dispatch_action(ToggleUserMenu);
516 })
517 .with_tooltip::<ToggleUserMenu, _>(
518 0,
519 "Toggle user menu".to_owned(),
520 Some(Box::new(ToggleUserMenu)),
521 theme.tooltip.clone(),
522 cx,
523 )
524 .contained()
525 .with_margin_left(theme.workspace.titlebar.item_spacing)
526 .boxed(),
527 )
528 .with_child(
529 ChildView::new(&self.user_menu, cx)
530 .aligned()
531 .bottom()
532 .right()
533 .boxed(),
534 )
535 .boxed()
536 }
537
538 fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
539 let titlebar = &theme.workspace.titlebar;
540 MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
541 let style = titlebar.sign_in_prompt.style_for(state, false);
542 Label::new("Sign In", style.text.clone())
543 .contained()
544 .with_style(style.container)
545 .boxed()
546 })
547 .with_cursor_style(CursorStyle::PointingHand)
548 .on_click(MouseButton::Left, move |_, cx| {
549 cx.dispatch_action(SignIn);
550 })
551 .boxed()
552 }
553
554 fn render_contacts_popover_host<'a>(
555 &'a self,
556 _theme: &'a theme::Titlebar,
557 cx: &'a RenderContext<Self>,
558 ) -> Option<ElementBox> {
559 self.contacts_popover.as_ref().map(|popover| {
560 Overlay::new(ChildView::new(popover, cx).boxed())
561 .with_fit_mode(OverlayFitMode::SwitchAnchor)
562 .with_anchor_corner(AnchorCorner::TopRight)
563 .with_z_index(999)
564 .aligned()
565 .bottom()
566 .right()
567 .boxed()
568 })
569 }
570
571 fn render_collaborators(
572 &self,
573 workspace: &ViewHandle<Workspace>,
574 theme: &Theme,
575 room: &ModelHandle<Room>,
576 cx: &mut RenderContext<Self>,
577 ) -> Vec<ElementBox> {
578 let mut participants = room
579 .read(cx)
580 .remote_participants()
581 .values()
582 .cloned()
583 .collect::<Vec<_>>();
584 participants.sort_by_cached_key(|p| p.user.github_login.clone());
585
586 participants
587 .into_iter()
588 .filter_map(|participant| {
589 let project = workspace.read(cx).project().read(cx);
590 let replica_id = project
591 .collaborators()
592 .get(&participant.peer_id)
593 .map(|collaborator| collaborator.replica_id);
594 let user = participant.user.clone();
595 Some(
596 Container::new(self.render_face_pile(
597 &user,
598 replica_id,
599 participant.peer_id,
600 Some(participant.location),
601 workspace,
602 theme,
603 cx,
604 ))
605 .with_margin_right(theme.workspace.titlebar.face_pile_spacing)
606 .boxed(),
607 )
608 })
609 .collect()
610 }
611
612 fn render_current_user(
613 &self,
614 workspace: &ViewHandle<Workspace>,
615 theme: &Theme,
616 user: &Arc<User>,
617 peer_id: PeerId,
618 cx: &mut RenderContext<Self>,
619 ) -> ElementBox {
620 let replica_id = workspace.read(cx).project().read(cx).replica_id();
621 Container::new(self.render_face_pile(
622 user,
623 Some(replica_id),
624 peer_id,
625 None,
626 workspace,
627 theme,
628 cx,
629 ))
630 .with_margin_right(theme.workspace.titlebar.item_spacing)
631 .boxed()
632 }
633
634 fn render_face_pile(
635 &self,
636 user: &User,
637 replica_id: Option<ReplicaId>,
638 peer_id: PeerId,
639 location: Option<ParticipantLocation>,
640 workspace: &ViewHandle<Workspace>,
641 theme: &Theme,
642 cx: &mut RenderContext<Self>,
643 ) -> ElementBox {
644 let project_id = workspace.read(cx).project().read(cx).remote_id();
645 let room = ActiveCall::global(cx).read(cx).room();
646 let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
647 let followed_by_self = room
648 .and_then(|room| {
649 Some(
650 is_being_followed
651 && room
652 .read(cx)
653 .followers_for(peer_id, project_id?)
654 .iter()
655 .any(|&follower| {
656 Some(follower) == workspace.read(cx).client().peer_id()
657 }),
658 )
659 })
660 .unwrap_or(false);
661
662 let leader_style = theme.workspace.titlebar.leader_avatar;
663 let follower_style = theme.workspace.titlebar.follower_avatar;
664
665 let mut background_color = theme
666 .workspace
667 .titlebar
668 .container
669 .background_color
670 .unwrap_or_default();
671 if let Some(replica_id) = replica_id {
672 if followed_by_self {
673 let selection = theme.editor.replica_selection_style(replica_id).selection;
674 background_color = Color::blend(selection, background_color);
675 background_color.a = 255;
676 }
677 }
678
679 let mut content = Stack::new()
680 .with_children(user.avatar.as_ref().map(|avatar| {
681 let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap)
682 .with_child(Self::render_face(
683 avatar.clone(),
684 Self::location_style(workspace, location, leader_style, cx),
685 background_color,
686 ))
687 .with_children(
688 (|| {
689 let project_id = project_id?;
690 let room = room?.read(cx);
691 let followers = room.followers_for(peer_id, project_id);
692
693 Some(followers.into_iter().flat_map(|&follower| {
694 let remote_participant =
695 room.remote_participant_for_peer_id(follower);
696
697 let avatar = remote_participant
698 .and_then(|p| p.user.avatar.clone())
699 .or_else(|| {
700 if follower == workspace.read(cx).client().peer_id()? {
701 workspace
702 .read(cx)
703 .user_store()
704 .read(cx)
705 .current_user()?
706 .avatar
707 .clone()
708 } else {
709 None
710 }
711 })?;
712
713 let location = remote_participant.map(|p| p.location);
714
715 Some(Self::render_face(
716 avatar.clone(),
717 Self::location_style(workspace, location, follower_style, cx),
718 background_color,
719 ))
720 }))
721 })()
722 .into_iter()
723 .flatten(),
724 );
725
726 let mut container = face_pile
727 .contained()
728 .with_style(theme.workspace.titlebar.leader_selection);
729
730 if let Some(replica_id) = replica_id {
731 if followed_by_self {
732 let color = theme.editor.replica_selection_style(replica_id).selection;
733 container = container.with_background_color(color);
734 }
735 }
736
737 container.boxed()
738 }))
739 .with_children((|| {
740 let replica_id = replica_id?;
741 let color = theme.editor.replica_selection_style(replica_id).cursor;
742 Some(
743 AvatarRibbon::new(color)
744 .constrained()
745 .with_width(theme.workspace.titlebar.avatar_ribbon.width)
746 .with_height(theme.workspace.titlebar.avatar_ribbon.height)
747 .aligned()
748 .bottom()
749 .boxed(),
750 )
751 })())
752 .boxed();
753
754 if let Some(location) = location {
755 if let Some(replica_id) = replica_id {
756 content =
757 MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
758 content
759 })
760 .with_cursor_style(CursorStyle::PointingHand)
761 .on_click(MouseButton::Left, move |_, cx| {
762 cx.dispatch_action(ToggleFollow(peer_id))
763 })
764 .with_tooltip::<ToggleFollow, _>(
765 peer_id.as_u64() as usize,
766 if is_being_followed {
767 format!("Unfollow {}", user.github_login)
768 } else {
769 format!("Follow {}", user.github_login)
770 },
771 Some(Box::new(FollowNextCollaborator)),
772 theme.tooltip.clone(),
773 cx,
774 )
775 .boxed();
776 } else if let ParticipantLocation::SharedProject { project_id } = location {
777 let user_id = user.id;
778 content = MouseEventHandler::<JoinProject>::new(
779 peer_id.as_u64() as usize,
780 cx,
781 move |_, _| content,
782 )
783 .with_cursor_style(CursorStyle::PointingHand)
784 .on_click(MouseButton::Left, move |_, cx| {
785 cx.dispatch_action(JoinProject {
786 project_id,
787 follow_user_id: user_id,
788 })
789 })
790 .with_tooltip::<JoinProject, _>(
791 peer_id.as_u64() as usize,
792 format!("Follow {} into external project", user.github_login),
793 Some(Box::new(FollowNextCollaborator)),
794 theme.tooltip.clone(),
795 cx,
796 )
797 .boxed();
798 }
799 }
800 content
801 }
802
803 fn location_style(
804 workspace: &ViewHandle<Workspace>,
805 location: Option<ParticipantLocation>,
806 mut style: AvatarStyle,
807 cx: &RenderContext<Self>,
808 ) -> AvatarStyle {
809 if let Some(location) = location {
810 if let ParticipantLocation::SharedProject { project_id } = location {
811 if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
812 style.image.grayscale = true;
813 }
814 } else {
815 style.image.grayscale = true;
816 }
817 }
818
819 style
820 }
821
822 fn render_face(
823 avatar: Arc<ImageData>,
824 avatar_style: AvatarStyle,
825 background_color: Color,
826 ) -> ElementBox {
827 Image::from_data(avatar)
828 .with_style(avatar_style.image)
829 .aligned()
830 .contained()
831 .with_background_color(background_color)
832 .with_corner_radius(avatar_style.outer_corner_radius)
833 .constrained()
834 .with_width(avatar_style.outer_width)
835 .with_height(avatar_style.outer_width)
836 .aligned()
837 .boxed()
838 }
839
840 fn render_connection_status(
841 &self,
842 status: &client::Status,
843 cx: &mut RenderContext<Self>,
844 ) -> Option<ElementBox> {
845 enum ConnectionStatusButton {}
846
847 let theme = &cx.global::<Settings>().theme.clone();
848 match status {
849 client::Status::ConnectionError
850 | client::Status::ConnectionLost
851 | client::Status::Reauthenticating { .. }
852 | client::Status::Reconnecting { .. }
853 | client::Status::ReconnectionError { .. } => Some(
854 Container::new(
855 Align::new(
856 ConstrainedBox::new(
857 Svg::new("icons/cloud_slash_12.svg")
858 .with_color(theme.workspace.titlebar.offline_icon.color)
859 .boxed(),
860 )
861 .with_width(theme.workspace.titlebar.offline_icon.width)
862 .boxed(),
863 )
864 .boxed(),
865 )
866 .with_style(theme.workspace.titlebar.offline_icon.container)
867 .boxed(),
868 ),
869 client::Status::UpgradeRequired => Some(
870 MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
871 Label::new(
872 "Please update Zed to collaborate",
873 theme.workspace.titlebar.outdated_warning.text.clone(),
874 )
875 .contained()
876 .with_style(theme.workspace.titlebar.outdated_warning.container)
877 .aligned()
878 .boxed()
879 })
880 .with_cursor_style(CursorStyle::PointingHand)
881 .on_click(MouseButton::Left, |_, cx| {
882 cx.dispatch_action(auto_update::Check);
883 })
884 .boxed(),
885 ),
886 _ => None,
887 }
888 }
889}
890
891pub struct AvatarRibbon {
892 color: Color,
893}
894
895impl AvatarRibbon {
896 pub fn new(color: Color) -> AvatarRibbon {
897 AvatarRibbon { color }
898 }
899}
900
901impl Element for AvatarRibbon {
902 type LayoutState = ();
903
904 type PaintState = ();
905
906 fn layout(
907 &mut self,
908 constraint: gpui::SizeConstraint,
909 _: &mut gpui::LayoutContext,
910 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
911 (constraint.max, ())
912 }
913
914 fn paint(
915 &mut self,
916 bounds: gpui::geometry::rect::RectF,
917 _: gpui::geometry::rect::RectF,
918 _: &mut Self::LayoutState,
919 cx: &mut gpui::PaintContext,
920 ) -> Self::PaintState {
921 let mut path = PathBuilder::new();
922 path.reset(bounds.lower_left());
923 path.curve_to(
924 bounds.origin() + vec2f(bounds.height(), 0.),
925 bounds.origin(),
926 );
927 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
928 path.curve_to(bounds.lower_right(), bounds.upper_right());
929 path.line_to(bounds.lower_left());
930 cx.scene.push_path(path.build(self.color, None));
931 }
932
933 fn rect_for_text_range(
934 &self,
935 _: Range<usize>,
936 _: RectF,
937 _: RectF,
938 _: &Self::LayoutState,
939 _: &Self::PaintState,
940 _: &gpui::MeasurementContext,
941 ) -> Option<RectF> {
942 None
943 }
944
945 fn debug(
946 &self,
947 bounds: gpui::geometry::rect::RectF,
948 _: &Self::LayoutState,
949 _: &Self::PaintState,
950 _: &gpui::DebugContext,
951 ) -> gpui::json::Value {
952 json::json!({
953 "type": "AvatarRibbon",
954 "bounds": bounds.to_json(),
955 "color": self.color.to_json(),
956 })
957 }
958}