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