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