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