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