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