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