1use crate::{
2 contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
3 toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
4};
5use call::{ActiveCall, ParticipantLocation, Room};
6use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
7use clock::ReplicaId;
8use context_menu::{ContextMenu, ContextMenuItem};
9use gpui::{
10 actions,
11 color::Color,
12 elements::*,
13 geometry::{rect::RectF, vector::vec2f, PathBuilder},
14 json::{self, ToJson},
15 platform::{CursorStyle, MouseButton},
16 AppContext, Entity, ImageData, LayoutContext, ModelHandle, PaintContext, SceneBuilder,
17 Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
18};
19use picker::PickerEvent;
20use project::{Project, RepositoryEntry};
21use recent_projects::{build_recent_projects, RecentProjects};
22use std::{ops::Range, sync::Arc};
23use theme::{AvatarStyle, Theme};
24use util::ResultExt;
25use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
26use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
27
28const MAX_PROJECT_NAME_LENGTH: usize = 40;
29const MAX_BRANCH_NAME_LENGTH: usize = 40;
30
31actions!(
32 collab,
33 [
34 ToggleUserMenu,
35 ToggleProjectMenu,
36 SwitchBranch,
37 ShareProject,
38 UnshareProject,
39 ]
40);
41
42pub fn init(cx: &mut AppContext) {
43 cx.add_action(CollabTitlebarItem::share_project);
44 cx.add_action(CollabTitlebarItem::unshare_project);
45 cx.add_action(CollabTitlebarItem::toggle_user_menu);
46 cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
47 cx.add_action(CollabTitlebarItem::toggle_project_menu);
48}
49
50pub struct CollabTitlebarItem {
51 project: ModelHandle<Project>,
52 user_store: ModelHandle<UserStore>,
53 client: Arc<Client>,
54 workspace: WeakViewHandle<Workspace>,
55 branch_popover: Option<ViewHandle<BranchList>>,
56 project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
57 user_menu: ViewHandle<ContextMenu>,
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 ViewContext<Self>) -> AnyElement<Self> {
71 let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
72 workspace
73 } else {
74 return Empty::new().into_any();
75 };
76
77 let theme = theme::current(cx).clone();
78 let mut left_container = Flex::row();
79 let mut right_container = Flex::row().align_children_center();
80
81 left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
82
83 let user = self.user_store.read(cx).current_user();
84 let peer_id = self.client.peer_id();
85 if let Some(((user, peer_id), room)) = user
86 .as_ref()
87 .zip(peer_id)
88 .zip(ActiveCall::global(cx).read(cx).room().cloned())
89 {
90 right_container
91 .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
92 right_container.add_child(self.render_leave_call(&theme, cx));
93 let muted = room.read(cx).is_muted(cx);
94 let speaking = room.read(cx).is_speaking();
95 left_container.add_child(
96 self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
97 );
98 left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx));
99 right_container.add_child(self.render_toggle_mute(&theme, &room, cx));
100 right_container.add_child(self.render_toggle_deafen(&theme, &room, cx));
101 right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx));
102 }
103
104 let status = workspace.read(cx).client().status();
105 let status = &*status.borrow();
106 if matches!(status, client::Status::Connected { .. }) {
107 let avatar = user.as_ref().and_then(|user| user.avatar.clone());
108 right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
109 } else {
110 right_container.add_children(self.render_connection_status(status, cx));
111 right_container.add_child(self.render_sign_in_button(&theme, cx));
112 right_container.add_child(self.render_user_menu_button(&theme, None, cx));
113 }
114
115 Stack::new()
116 .with_child(left_container)
117 .with_child(
118 Flex::row()
119 .with_child(
120 right_container.contained().with_background_color(
121 theme
122 .titlebar
123 .container
124 .background_color
125 .unwrap_or_else(|| Color::transparent_black()),
126 ),
127 )
128 .aligned()
129 .right(),
130 )
131 .into_any()
132 }
133}
134
135impl CollabTitlebarItem {
136 pub fn new(
137 workspace: &Workspace,
138 workspace_handle: &ViewHandle<Workspace>,
139 cx: &mut ViewContext<Self>,
140 ) -> Self {
141 let project = workspace.project().clone();
142 let user_store = workspace.app_state().user_store.clone();
143 let client = workspace.app_state().client.clone();
144 let active_call = ActiveCall::global(cx);
145 let mut subscriptions = Vec::new();
146 subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
147 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
148 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
149 subscriptions.push(cx.observe_window_activation(|this, active, cx| {
150 this.window_activation_changed(active, cx)
151 }));
152 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
153 subscriptions.push(
154 cx.subscribe(&user_store, move |this, user_store, event, cx| {
155 if let Some(workspace) = this.workspace.upgrade(cx) {
156 workspace.update(cx, |workspace, cx| {
157 if let client::Event::Contact { user, kind } = event {
158 if let ContactEventKind::Requested | ContactEventKind::Accepted = kind {
159 workspace.show_notification(user.id as usize, cx, |cx| {
160 cx.add_view(|cx| {
161 ContactNotification::new(
162 user.clone(),
163 *kind,
164 user_store,
165 cx,
166 )
167 })
168 })
169 }
170 }
171 });
172 }
173 }),
174 );
175
176 Self {
177 workspace: workspace.weak_handle(),
178 project,
179 user_store,
180 client,
181 user_menu: cx.add_view(|cx| {
182 let view_id = cx.view_id();
183 let mut menu = ContextMenu::new(view_id, cx);
184 menu.set_position_mode(OverlayPositionMode::Local);
185 menu
186 }),
187 branch_popover: None,
188 project_popover: None,
189 _subscriptions: subscriptions,
190 }
191 }
192
193 fn collect_title_root_names(
194 &self,
195 theme: Arc<Theme>,
196 cx: &mut ViewContext<Self>,
197 ) -> AnyElement<Self> {
198 let project = self.project.read(cx);
199
200 let (name, entry) = {
201 let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
202 let worktree = worktree.read(cx);
203 (worktree.root_name(), worktree.root_git_entry())
204 });
205
206 names_and_branches.next().unwrap_or(("", None))
207 };
208
209 let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
210 let branch_prepended = entry
211 .as_ref()
212 .and_then(RepositoryEntry::branch)
213 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
214 let project_style = theme.titlebar.project_menu_button.clone();
215 let git_style = theme.titlebar.git_menu_button.clone();
216 let divider_style = theme.titlebar.project_name_divider.clone();
217 let item_spacing = theme.titlebar.item_spacing;
218
219 let mut ret = Flex::row().with_child(
220 Stack::new()
221 .with_child(
222 MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
223 let style = project_style
224 .in_state(self.project_popover.is_some())
225 .style_for(mouse_state);
226 enum RecentProjectsTooltip {}
227 Label::new(name, style.text.clone())
228 .contained()
229 .with_style(style.container)
230 .aligned()
231 .left()
232 .with_tooltip::<RecentProjectsTooltip>(
233 0,
234 "Recent projects",
235 Some(Box::new(recent_projects::OpenRecent)),
236 theme.tooltip.clone(),
237 cx,
238 )
239 .into_any_named("title-project-name")
240 })
241 .with_cursor_style(CursorStyle::PointingHand)
242 .on_down(MouseButton::Left, move |_, this, cx| {
243 this.toggle_project_menu(&Default::default(), cx)
244 })
245 .on_click(MouseButton::Left, move |_, _, _| {}),
246 )
247 .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
248 );
249 if let Some(git_branch) = branch_prepended {
250 ret = ret.with_child(
251 Flex::row()
252 .with_child(
253 Label::new("/", divider_style.text)
254 .contained()
255 .with_style(divider_style.container)
256 .aligned()
257 .left(),
258 )
259 .with_child(
260 Stack::new()
261 .with_child(
262 MouseEventHandler::new::<ToggleVcsMenu, _>(
263 0,
264 cx,
265 |mouse_state, cx| {
266 enum BranchPopoverTooltip {}
267 let style = git_style
268 .in_state(self.branch_popover.is_some())
269 .style_for(mouse_state);
270 Label::new(git_branch, style.text.clone())
271 .contained()
272 .with_style(style.container.clone())
273 .with_margin_right(item_spacing)
274 .aligned()
275 .left()
276 .with_tooltip::<BranchPopoverTooltip>(
277 0,
278 "Recent branches",
279 Some(Box::new(ToggleVcsMenu)),
280 theme.tooltip.clone(),
281 cx,
282 )
283 .into_any_named("title-project-branch")
284 },
285 )
286 .with_cursor_style(CursorStyle::PointingHand)
287 .on_down(MouseButton::Left, move |_, this, cx| {
288 this.toggle_vcs_menu(&Default::default(), cx)
289 })
290 .on_click(MouseButton::Left, move |_, _, _| {}),
291 )
292 .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
293 ),
294 )
295 }
296 ret.into_any()
297 }
298
299 fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
300 let project = if active {
301 Some(self.project.clone())
302 } else {
303 None
304 };
305 ActiveCall::global(cx)
306 .update(cx, |call, cx| call.set_location(project.as_ref(), cx))
307 .detach_and_log_err(cx);
308 }
309
310 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
311 cx.notify();
312 }
313
314 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
315 let active_call = ActiveCall::global(cx);
316 let project = self.project.clone();
317 active_call
318 .update(cx, |call, cx| call.share_project(project, cx))
319 .detach_and_log_err(cx);
320 }
321
322 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
323 let active_call = ActiveCall::global(cx);
324 let project = self.project.clone();
325 active_call
326 .update(cx, |call, cx| call.unshare_project(project, cx))
327 .log_err();
328 }
329
330 pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
331 self.user_menu.update(cx, |user_menu, cx| {
332 let items = if let Some(_) = self.user_store.read(cx).current_user() {
333 vec![
334 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
335 ContextMenuItem::action("Theme", theme_selector::Toggle),
336 ContextMenuItem::separator(),
337 ContextMenuItem::action(
338 "Share Feedback",
339 feedback::feedback_editor::GiveFeedback,
340 ),
341 ContextMenuItem::action("Sign Out", SignOut),
342 ]
343 } else {
344 vec![
345 ContextMenuItem::action("Settings", zed_actions::OpenSettings),
346 ContextMenuItem::action("Theme", theme_selector::Toggle),
347 ContextMenuItem::separator(),
348 ContextMenuItem::action(
349 "Share Feedback",
350 feedback::feedback_editor::GiveFeedback,
351 ),
352 ]
353 };
354 user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
355 });
356 }
357
358 fn render_branches_popover_host<'a>(
359 &'a self,
360 _theme: &'a theme::Titlebar,
361 cx: &'a mut ViewContext<Self>,
362 ) -> Option<AnyElement<Self>> {
363 self.branch_popover.as_ref().map(|child| {
364 let theme = theme::current(cx).clone();
365 let child = ChildView::new(child, cx);
366 let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
367 child
368 .flex(1., true)
369 .contained()
370 .constrained()
371 .with_width(theme.titlebar.menu.width)
372 .with_height(theme.titlebar.menu.height)
373 })
374 .on_click(MouseButton::Left, |_, _, _| {})
375 .on_down_out(MouseButton::Left, move |_, this, cx| {
376 this.branch_popover.take();
377 cx.emit(());
378 cx.notify();
379 })
380 .contained()
381 .into_any();
382
383 Overlay::new(child)
384 .with_fit_mode(OverlayFitMode::SwitchAnchor)
385 .with_anchor_corner(AnchorCorner::TopLeft)
386 .with_z_index(999)
387 .aligned()
388 .bottom()
389 .left()
390 .into_any()
391 })
392 }
393
394 fn render_project_popover_host<'a>(
395 &'a self,
396 _theme: &'a theme::Titlebar,
397 cx: &'a mut ViewContext<Self>,
398 ) -> Option<AnyElement<Self>> {
399 self.project_popover.as_ref().map(|child| {
400 let theme = theme::current(cx).clone();
401 let child = ChildView::new(child, cx);
402 let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
403 child
404 .flex(1., true)
405 .contained()
406 .constrained()
407 .with_width(theme.titlebar.menu.width)
408 .with_height(theme.titlebar.menu.height)
409 })
410 .on_click(MouseButton::Left, |_, _, _| {})
411 .on_down_out(MouseButton::Left, move |_, this, cx| {
412 this.project_popover.take();
413 cx.emit(());
414 cx.notify();
415 })
416 .into_any();
417
418 Overlay::new(child)
419 .with_fit_mode(OverlayFitMode::SwitchAnchor)
420 .with_anchor_corner(AnchorCorner::TopLeft)
421 .with_z_index(999)
422 .aligned()
423 .bottom()
424 .left()
425 .into_any()
426 })
427 }
428
429 pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
430 if self.branch_popover.take().is_none() {
431 if let Some(workspace) = self.workspace.upgrade(cx) {
432 let view = cx.add_view(|cx| build_branch_list(workspace, cx));
433 cx.subscribe(&view, |this, _, event, cx| {
434 match event {
435 PickerEvent::Dismiss => {
436 this.branch_popover = None;
437 }
438 }
439
440 cx.notify();
441 })
442 .detach();
443 self.project_popover.take();
444 cx.focus(&view);
445 self.branch_popover = Some(view);
446 }
447 }
448
449 cx.notify();
450 }
451
452 pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
453 let workspace = self.workspace.clone();
454 if self.project_popover.take().is_none() {
455 cx.spawn(|this, mut cx| async move {
456 let workspaces = WORKSPACE_DB
457 .recent_workspaces_on_disk()
458 .await
459 .unwrap_or_default()
460 .into_iter()
461 .map(|(_, location)| location)
462 .collect();
463
464 let workspace = workspace.clone();
465 this.update(&mut cx, move |this, cx| {
466 let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
467
468 cx.subscribe(&view, |this, _, event, cx| {
469 match event {
470 PickerEvent::Dismiss => {
471 this.project_popover = None;
472 }
473 }
474
475 cx.notify();
476 })
477 .detach();
478 cx.focus(&view);
479 this.branch_popover.take();
480 this.project_popover = Some(view);
481 cx.notify();
482 })
483 .log_err();
484 })
485 .detach();
486 }
487 cx.notify();
488 }
489
490 fn render_toggle_screen_sharing_button(
491 &self,
492 theme: &Theme,
493 room: &ModelHandle<Room>,
494 cx: &mut ViewContext<Self>,
495 ) -> AnyElement<Self> {
496 let icon;
497 let tooltip;
498 if room.read(cx).is_screen_sharing() {
499 icon = "icons/radix/desktop.svg";
500 tooltip = "Stop Sharing Screen"
501 } else {
502 icon = "icons/radix/desktop.svg";
503 tooltip = "Share Screen";
504 }
505
506 let active = room.read(cx).is_screen_sharing();
507 let titlebar = &theme.titlebar;
508 MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
509 let style = titlebar
510 .screen_share_button
511 .in_state(active)
512 .style_for(state);
513
514 Svg::new(icon)
515 .with_color(style.color)
516 .constrained()
517 .with_width(style.icon_width)
518 .aligned()
519 .constrained()
520 .with_width(style.button_width)
521 .with_height(style.button_width)
522 .contained()
523 .with_style(style.container)
524 })
525 .with_cursor_style(CursorStyle::PointingHand)
526 .on_click(MouseButton::Left, move |_, _, cx| {
527 toggle_screen_sharing(&Default::default(), cx)
528 })
529 .with_tooltip::<ToggleScreenSharing>(
530 0,
531 tooltip,
532 Some(Box::new(ToggleScreenSharing)),
533 theme.tooltip.clone(),
534 cx,
535 )
536 .aligned()
537 .into_any()
538 }
539 fn render_toggle_mute(
540 &self,
541 theme: &Theme,
542 room: &ModelHandle<Room>,
543 cx: &mut ViewContext<Self>,
544 ) -> AnyElement<Self> {
545 let icon;
546 let tooltip;
547 let is_muted = room.read(cx).is_muted(cx);
548 if is_muted {
549 icon = "icons/radix/mic-mute.svg";
550 tooltip = "Unmute microphone";
551 } else {
552 icon = "icons/radix/mic.svg";
553 tooltip = "Mute microphone";
554 }
555
556 let titlebar = &theme.titlebar;
557 MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
558 let style = titlebar
559 .toggle_microphone_button
560 .in_state(is_muted)
561 .style_for(state);
562 let image = Svg::new(icon)
563 .with_color(style.color)
564 .constrained()
565 .with_width(style.icon_width)
566 .aligned()
567 .constrained()
568 .with_width(style.button_width)
569 .with_height(style.button_width)
570 .contained()
571 .with_style(style.container);
572 if let Some(color) = style.container.background_color {
573 image.with_background_color(color)
574 } else {
575 image
576 }
577 })
578 .with_cursor_style(CursorStyle::PointingHand)
579 .on_click(MouseButton::Left, move |_, _, cx| {
580 toggle_mute(&Default::default(), cx)
581 })
582 .with_tooltip::<ToggleMute>(
583 0,
584 tooltip,
585 Some(Box::new(ToggleMute)),
586 theme.tooltip.clone(),
587 cx,
588 )
589 .aligned()
590 .into_any()
591 }
592 fn render_toggle_deafen(
593 &self,
594 theme: &Theme,
595 room: &ModelHandle<Room>,
596 cx: &mut ViewContext<Self>,
597 ) -> AnyElement<Self> {
598 let icon;
599 let tooltip;
600 let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
601 if is_deafened {
602 icon = "icons/radix/speaker-off.svg";
603 tooltip = "Unmute speakers";
604 } else {
605 icon = "icons/radix/speaker-loud.svg";
606 tooltip = "Mute speakers";
607 }
608
609 let titlebar = &theme.titlebar;
610 MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
611 let style = titlebar
612 .toggle_speakers_button
613 .in_state(is_deafened)
614 .style_for(state);
615 Svg::new(icon)
616 .with_color(style.color)
617 .constrained()
618 .with_width(style.icon_width)
619 .aligned()
620 .constrained()
621 .with_width(style.button_width)
622 .with_height(style.button_width)
623 .contained()
624 .with_style(style.container)
625 })
626 .with_cursor_style(CursorStyle::PointingHand)
627 .on_click(MouseButton::Left, move |_, _, cx| {
628 toggle_deafen(&Default::default(), cx)
629 })
630 .with_tooltip::<ToggleDeafen>(
631 0,
632 tooltip,
633 Some(Box::new(ToggleDeafen)),
634 theme.tooltip.clone(),
635 cx,
636 )
637 .aligned()
638 .into_any()
639 }
640 fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
641 let icon = "icons/radix/exit.svg";
642 let tooltip = "Leave call";
643
644 let titlebar = &theme.titlebar;
645 MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
646 let style = titlebar.leave_call_button.style_for(state);
647 Svg::new(icon)
648 .with_color(style.color)
649 .constrained()
650 .with_width(style.icon_width)
651 .aligned()
652 .constrained()
653 .with_width(style.button_width)
654 .with_height(style.button_width)
655 .contained()
656 .with_style(style.container)
657 })
658 .with_cursor_style(CursorStyle::PointingHand)
659 .on_click(MouseButton::Left, move |_, _, cx| {
660 ActiveCall::global(cx)
661 .update(cx, |call, cx| call.hang_up(cx))
662 .detach_and_log_err(cx);
663 })
664 .with_tooltip::<LeaveCall>(
665 0,
666 tooltip,
667 Some(Box::new(LeaveCall)),
668 theme.tooltip.clone(),
669 cx,
670 )
671 .aligned()
672 .into_any()
673 }
674 fn render_in_call_share_unshare_button(
675 &self,
676 workspace: &ViewHandle<Workspace>,
677 theme: &Theme,
678 cx: &mut ViewContext<Self>,
679 ) -> Option<AnyElement<Self>> {
680 let project = workspace.read(cx).project();
681 if project.read(cx).is_remote() {
682 return None;
683 }
684
685 let is_shared = project.read(cx).is_shared();
686 let label = if is_shared { "Stop Sharing" } else { "Share" };
687 let tooltip = if is_shared {
688 "Stop sharing project with call participants"
689 } else {
690 "Share project with call participants"
691 };
692
693 let titlebar = &theme.titlebar;
694
695 enum ShareUnshare {}
696 Some(
697 Stack::new()
698 .with_child(
699 MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
700 //TODO: Ensure this button has consistent width for both text variations
701 let style = titlebar.share_button.inactive_state().style_for(state);
702 Label::new(label, style.text.clone())
703 .contained()
704 .with_style(style.container)
705 })
706 .with_cursor_style(CursorStyle::PointingHand)
707 .on_click(MouseButton::Left, move |_, this, cx| {
708 if is_shared {
709 this.unshare_project(&Default::default(), cx);
710 } else {
711 this.share_project(&Default::default(), cx);
712 }
713 })
714 .with_tooltip::<ShareUnshare>(
715 0,
716 tooltip.to_owned(),
717 None,
718 theme.tooltip.clone(),
719 cx,
720 ),
721 )
722 .aligned()
723 .contained()
724 .with_margin_left(theme.titlebar.item_spacing)
725 .into_any(),
726 )
727 }
728
729 fn render_user_menu_button(
730 &self,
731 theme: &Theme,
732 avatar: Option<Arc<ImageData>>,
733 cx: &mut ViewContext<Self>,
734 ) -> AnyElement<Self> {
735 let tooltip = theme.tooltip.clone();
736 let user_menu_button_style = if avatar.is_some() {
737 &theme.titlebar.user_menu.user_menu_button_online
738 } else {
739 &theme.titlebar.user_menu.user_menu_button_offline
740 };
741
742 let avatar_style = &user_menu_button_style.avatar;
743 Stack::new()
744 .with_child(
745 MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
746 let style = user_menu_button_style
747 .user_menu
748 .inactive_state()
749 .style_for(state);
750
751 let mut dropdown = Flex::row().align_children_center();
752
753 if let Some(avatar_img) = avatar {
754 dropdown = dropdown.with_child(Self::render_face(
755 avatar_img,
756 *avatar_style,
757 Color::transparent_black(),
758 None,
759 ));
760 };
761
762 dropdown
763 .with_child(
764 Svg::new("icons/caret_down_8.svg")
765 .with_color(user_menu_button_style.icon.color)
766 .constrained()
767 .with_width(user_menu_button_style.icon.width)
768 .contained()
769 .into_any(),
770 )
771 .aligned()
772 .constrained()
773 .with_height(style.width)
774 .contained()
775 .with_style(style.container)
776 .into_any()
777 })
778 .with_cursor_style(CursorStyle::PointingHand)
779 .on_down(MouseButton::Left, move |_, this, cx| {
780 this.user_menu.update(cx, |menu, _| menu.delay_cancel());
781 })
782 .on_click(MouseButton::Left, move |_, this, cx| {
783 this.toggle_user_menu(&Default::default(), cx)
784 })
785 .with_tooltip::<ToggleUserMenu>(
786 0,
787 "Toggle user menu".to_owned(),
788 Some(Box::new(ToggleUserMenu)),
789 tooltip,
790 cx,
791 )
792 .contained(),
793 )
794 .with_child(
795 ChildView::new(&self.user_menu, cx)
796 .aligned()
797 .bottom()
798 .right(),
799 )
800 .into_any()
801 }
802
803 fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
804 let titlebar = &theme.titlebar;
805 MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
806 let style = titlebar.sign_in_button.inactive_state().style_for(state);
807 Label::new("Sign In", style.text.clone())
808 .contained()
809 .with_style(style.container)
810 })
811 .with_cursor_style(CursorStyle::PointingHand)
812 .on_click(MouseButton::Left, move |_, this, cx| {
813 let client = this.client.clone();
814 cx.app_context()
815 .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
816 .detach_and_log_err(cx);
817 })
818 .into_any()
819 }
820
821 fn render_collaborators(
822 &self,
823 workspace: &ViewHandle<Workspace>,
824 theme: &Theme,
825 room: &ModelHandle<Room>,
826 cx: &mut ViewContext<Self>,
827 ) -> Vec<Container<Self>> {
828 let mut participants = room
829 .read(cx)
830 .remote_participants()
831 .values()
832 .cloned()
833 .collect::<Vec<_>>();
834 participants.sort_by_cached_key(|p| p.user.github_login.clone());
835
836 participants
837 .into_iter()
838 .filter_map(|participant| {
839 let project = workspace.read(cx).project().read(cx);
840 let replica_id = project
841 .collaborators()
842 .get(&participant.peer_id)
843 .map(|collaborator| collaborator.replica_id);
844 let user = participant.user.clone();
845 Some(
846 Container::new(self.render_face_pile(
847 &user,
848 replica_id,
849 participant.peer_id,
850 Some(participant.location),
851 participant.muted,
852 participant.speaking,
853 workspace,
854 theme,
855 cx,
856 ))
857 .with_margin_right(theme.titlebar.face_pile_spacing),
858 )
859 })
860 .collect()
861 }
862
863 fn render_current_user(
864 &self,
865 workspace: &ViewHandle<Workspace>,
866 theme: &Theme,
867 user: &Arc<User>,
868 peer_id: PeerId,
869 muted: bool,
870 speaking: bool,
871 cx: &mut ViewContext<Self>,
872 ) -> AnyElement<Self> {
873 let replica_id = workspace.read(cx).project().read(cx).replica_id();
874
875 Container::new(self.render_face_pile(
876 user,
877 Some(replica_id),
878 peer_id,
879 None,
880 muted,
881 speaking,
882 workspace,
883 theme,
884 cx,
885 ))
886 .with_margin_right(theme.titlebar.item_spacing)
887 .into_any()
888 }
889
890 fn render_face_pile(
891 &self,
892 user: &User,
893 replica_id: Option<ReplicaId>,
894 peer_id: PeerId,
895 location: Option<ParticipantLocation>,
896 muted: bool,
897 speaking: bool,
898 workspace: &ViewHandle<Workspace>,
899 theme: &Theme,
900 cx: &mut ViewContext<Self>,
901 ) -> AnyElement<Self> {
902 let project_id = workspace.read(cx).project().read(cx).remote_id();
903 let room = ActiveCall::global(cx).read(cx).room();
904 let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
905 let followed_by_self = room
906 .and_then(|room| {
907 Some(
908 is_being_followed
909 && room
910 .read(cx)
911 .followers_for(peer_id, project_id?)
912 .iter()
913 .any(|&follower| {
914 Some(follower) == workspace.read(cx).client().peer_id()
915 }),
916 )
917 })
918 .unwrap_or(false);
919
920 let leader_style = theme.titlebar.leader_avatar;
921 let follower_style = theme.titlebar.follower_avatar;
922
923 let microphone_state = if muted {
924 Some(theme.titlebar.muted)
925 } else if speaking {
926 Some(theme.titlebar.speaking)
927 } else {
928 None
929 };
930
931 let mut background_color = theme
932 .titlebar
933 .container
934 .background_color
935 .unwrap_or_default();
936
937 if let Some(replica_id) = replica_id {
938 if followed_by_self {
939 let selection = theme.editor.replica_selection_style(replica_id).selection;
940 background_color = Color::blend(selection, background_color);
941 background_color.a = 255;
942 }
943 }
944
945 let mut content = Stack::new()
946 .with_children(user.avatar.as_ref().map(|avatar| {
947 let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
948 .with_child(Self::render_face(
949 avatar.clone(),
950 Self::location_style(workspace, location, leader_style, cx),
951 background_color,
952 microphone_state,
953 ))
954 .with_children(
955 (|| {
956 let project_id = project_id?;
957 let room = room?.read(cx);
958 let followers = room.followers_for(peer_id, project_id);
959
960 Some(followers.into_iter().flat_map(|&follower| {
961 let remote_participant =
962 room.remote_participant_for_peer_id(follower);
963
964 let avatar = remote_participant
965 .and_then(|p| p.user.avatar.clone())
966 .or_else(|| {
967 if follower == workspace.read(cx).client().peer_id()? {
968 workspace
969 .read(cx)
970 .user_store()
971 .read(cx)
972 .current_user()?
973 .avatar
974 .clone()
975 } else {
976 None
977 }
978 })?;
979
980 Some(Self::render_face(
981 avatar.clone(),
982 follower_style,
983 background_color,
984 None,
985 ))
986 }))
987 })()
988 .into_iter()
989 .flatten(),
990 );
991
992 let mut container = face_pile
993 .contained()
994 .with_style(theme.titlebar.leader_selection);
995
996 if let Some(replica_id) = replica_id {
997 if followed_by_self {
998 let color = theme.editor.replica_selection_style(replica_id).selection;
999 container = container.with_background_color(color);
1000 }
1001 }
1002
1003 container
1004 }))
1005 .with_children((|| {
1006 let replica_id = replica_id?;
1007 let color = theme.editor.replica_selection_style(replica_id).cursor;
1008 Some(
1009 AvatarRibbon::new(color)
1010 .constrained()
1011 .with_width(theme.titlebar.avatar_ribbon.width)
1012 .with_height(theme.titlebar.avatar_ribbon.height)
1013 .aligned()
1014 .bottom(),
1015 )
1016 })())
1017 .into_any();
1018
1019 if let Some(location) = location {
1020 if let Some(replica_id) = replica_id {
1021 enum ToggleFollow {}
1022
1023 content = MouseEventHandler::new::<ToggleFollow, _>(
1024 replica_id.into(),
1025 cx,
1026 move |_, _| content,
1027 )
1028 .with_cursor_style(CursorStyle::PointingHand)
1029 .on_click(MouseButton::Left, move |_, item, cx| {
1030 if let Some(workspace) = item.workspace.upgrade(cx) {
1031 if let Some(task) = workspace
1032 .update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
1033 {
1034 task.detach_and_log_err(cx);
1035 }
1036 }
1037 })
1038 .with_tooltip::<ToggleFollow>(
1039 peer_id.as_u64() as usize,
1040 if is_being_followed {
1041 format!("Unfollow {}", user.github_login)
1042 } else {
1043 format!("Follow {}", user.github_login)
1044 },
1045 Some(Box::new(FollowNextCollaborator)),
1046 theme.tooltip.clone(),
1047 cx,
1048 )
1049 .into_any();
1050 } else if let ParticipantLocation::SharedProject { project_id } = location {
1051 enum JoinProject {}
1052
1053 let user_id = user.id;
1054 content = MouseEventHandler::new::<JoinProject, _>(
1055 peer_id.as_u64() as usize,
1056 cx,
1057 move |_, _| content,
1058 )
1059 .with_cursor_style(CursorStyle::PointingHand)
1060 .on_click(MouseButton::Left, move |_, this, cx| {
1061 if let Some(workspace) = this.workspace.upgrade(cx) {
1062 let app_state = workspace.read(cx).app_state().clone();
1063 workspace::join_remote_project(project_id, user_id, app_state, cx)
1064 .detach_and_log_err(cx);
1065 }
1066 })
1067 .with_tooltip::<JoinProject>(
1068 peer_id.as_u64() as usize,
1069 format!("Follow {} into external project", user.github_login),
1070 Some(Box::new(FollowNextCollaborator)),
1071 theme.tooltip.clone(),
1072 cx,
1073 )
1074 .into_any();
1075 }
1076 }
1077 content
1078 }
1079
1080 fn location_style(
1081 workspace: &ViewHandle<Workspace>,
1082 location: Option<ParticipantLocation>,
1083 mut style: AvatarStyle,
1084 cx: &ViewContext<Self>,
1085 ) -> AvatarStyle {
1086 if let Some(location) = location {
1087 if let ParticipantLocation::SharedProject { project_id } = location {
1088 if Some(project_id) != workspace.read(cx).project().read(cx).remote_id() {
1089 style.image.grayscale = true;
1090 }
1091 } else {
1092 style.image.grayscale = true;
1093 }
1094 }
1095
1096 style
1097 }
1098
1099 fn render_face<V: View>(
1100 avatar: Arc<ImageData>,
1101 avatar_style: AvatarStyle,
1102 background_color: Color,
1103 microphone_state: Option<Color>,
1104 ) -> AnyElement<V> {
1105 Image::from_data(avatar)
1106 .with_style(avatar_style.image)
1107 .aligned()
1108 .contained()
1109 .with_background_color(microphone_state.unwrap_or(background_color))
1110 .with_corner_radius(avatar_style.outer_corner_radius)
1111 .constrained()
1112 .with_width(avatar_style.outer_width)
1113 .with_height(avatar_style.outer_width)
1114 .aligned()
1115 .into_any()
1116 }
1117
1118 fn render_connection_status(
1119 &self,
1120 status: &client::Status,
1121 cx: &mut ViewContext<Self>,
1122 ) -> Option<AnyElement<Self>> {
1123 enum ConnectionStatusButton {}
1124
1125 let theme = &theme::current(cx).clone();
1126 match status {
1127 client::Status::ConnectionError
1128 | client::Status::ConnectionLost
1129 | client::Status::Reauthenticating { .. }
1130 | client::Status::Reconnecting { .. }
1131 | client::Status::ReconnectionError { .. } => Some(
1132 Svg::new("icons/cloud_slash_12.svg")
1133 .with_color(theme.titlebar.offline_icon.color)
1134 .constrained()
1135 .with_width(theme.titlebar.offline_icon.width)
1136 .aligned()
1137 .contained()
1138 .with_style(theme.titlebar.offline_icon.container)
1139 .into_any(),
1140 ),
1141 client::Status::UpgradeRequired => Some(
1142 MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
1143 Label::new(
1144 "Please update Zed to collaborate",
1145 theme.titlebar.outdated_warning.text.clone(),
1146 )
1147 .contained()
1148 .with_style(theme.titlebar.outdated_warning.container)
1149 .aligned()
1150 })
1151 .with_cursor_style(CursorStyle::PointingHand)
1152 .on_click(MouseButton::Left, |_, _, cx| {
1153 auto_update::check(&Default::default(), cx);
1154 })
1155 .into_any(),
1156 ),
1157 _ => None,
1158 }
1159 }
1160}
1161
1162pub struct AvatarRibbon {
1163 color: Color,
1164}
1165
1166impl AvatarRibbon {
1167 pub fn new(color: Color) -> AvatarRibbon {
1168 AvatarRibbon { color }
1169 }
1170}
1171
1172impl Element<CollabTitlebarItem> for AvatarRibbon {
1173 type LayoutState = ();
1174
1175 type PaintState = ();
1176
1177 fn layout(
1178 &mut self,
1179 constraint: gpui::SizeConstraint,
1180 _: &mut CollabTitlebarItem,
1181 _: &mut LayoutContext<CollabTitlebarItem>,
1182 ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
1183 (constraint.max, ())
1184 }
1185
1186 fn paint(
1187 &mut self,
1188 scene: &mut SceneBuilder,
1189 bounds: RectF,
1190 _: RectF,
1191 _: &mut Self::LayoutState,
1192 _: &mut CollabTitlebarItem,
1193 _: &mut PaintContext<CollabTitlebarItem>,
1194 ) -> Self::PaintState {
1195 let mut path = PathBuilder::new();
1196 path.reset(bounds.lower_left());
1197 path.curve_to(
1198 bounds.origin() + vec2f(bounds.height(), 0.),
1199 bounds.origin(),
1200 );
1201 path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
1202 path.curve_to(bounds.lower_right(), bounds.upper_right());
1203 path.line_to(bounds.lower_left());
1204 scene.push_path(path.build(self.color, None));
1205 }
1206
1207 fn rect_for_text_range(
1208 &self,
1209 _: Range<usize>,
1210 _: RectF,
1211 _: RectF,
1212 _: &Self::LayoutState,
1213 _: &Self::PaintState,
1214 _: &CollabTitlebarItem,
1215 _: &ViewContext<CollabTitlebarItem>,
1216 ) -> Option<RectF> {
1217 None
1218 }
1219
1220 fn debug(
1221 &self,
1222 bounds: RectF,
1223 _: &Self::LayoutState,
1224 _: &Self::PaintState,
1225 _: &CollabTitlebarItem,
1226 _: &ViewContext<CollabTitlebarItem>,
1227 ) -> gpui::json::Value {
1228 json::json!({
1229 "type": "AvatarRibbon",
1230 "bounds": bounds.to_json(),
1231 "color": self.color.to_json(),
1232 })
1233 }
1234}