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