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