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