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