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