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