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