1use crate::face_pile::FacePile;
2use auto_update::AutoUpdateStatus;
3use call::{ActiveCall, ParticipantLocation, Room};
4use client::{proto::PeerId, Client, User, UserStore};
5use gpui::{
6 actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla,
7 InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
8 StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
9};
10use project::{Project, RepositoryEntry};
11use recent_projects::RecentProjects;
12use rpc::proto::{self, DevServerStatus};
13use settings::Settings;
14use std::sync::Arc;
15use theme::{ActiveTheme, ThemeSettings};
16use ui::{
17 h_flex, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, ButtonStyle,
18 ContextMenu, Icon, IconButton, IconName, Indicator, PopoverMenu, TintColor, TitleBar, Tooltip,
19};
20use util::ResultExt;
21use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
22use workspace::{notifications::NotifyResultExt, Workspace};
23
24const MAX_PROJECT_NAME_LENGTH: usize = 40;
25const MAX_BRANCH_NAME_LENGTH: usize = 40;
26
27actions!(
28 collab,
29 [
30 ShareProject,
31 UnshareProject,
32 ToggleUserMenu,
33 ToggleProjectMenu,
34 SwitchBranch
35 ]
36);
37
38pub fn init(cx: &mut AppContext) {
39 cx.observe_new_views(|workspace: &mut Workspace, cx| {
40 let titlebar_item = cx.new_view(|cx| CollabTitlebarItem::new(workspace, cx));
41 workspace.set_titlebar_item(titlebar_item.into(), cx)
42 })
43 .detach();
44}
45
46pub struct CollabTitlebarItem {
47 project: Model<Project>,
48 user_store: Model<UserStore>,
49 client: Arc<Client>,
50 workspace: WeakView<Workspace>,
51 _subscriptions: Vec<Subscription>,
52}
53
54impl Render for CollabTitlebarItem {
55 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
56 let room = ActiveCall::global(cx).read(cx).room().cloned();
57 let current_user = self.user_store.read(cx).current_user();
58 let client = self.client.clone();
59 let project_id = self.project.read(cx).remote_id();
60 let workspace = self.workspace.upgrade();
61
62 let platform_supported = cfg!(target_os = "macos");
63
64 TitleBar::new("collab-titlebar", Box::new(workspace::CloseWindow))
65 // note: on windows titlebar behaviour is handled by the platform implementation
66 .when(cfg!(not(windows)), |this| {
67 this.on_click(|event, cx| {
68 if event.up.click_count == 2 {
69 cx.zoom_window();
70 }
71 })
72 })
73 // left side
74 .child(
75 h_flex()
76 .gap_1()
77 .children(self.render_application_menu(cx))
78 .children(self.render_project_host(cx))
79 .child(self.render_project_name(cx))
80 .children(self.render_project_branch(cx))
81 .on_mouse_move(|_, cx| cx.stop_propagation()),
82 )
83 .child(
84 h_flex()
85 .id("collaborator-list")
86 .w_full()
87 .gap_1()
88 .overflow_x_scroll()
89 .when_some(
90 current_user.clone().zip(client.peer_id()).zip(room.clone()),
91 |this, ((current_user, peer_id), room)| {
92 let player_colors = cx.theme().players();
93 let room = room.read(cx);
94 let mut remote_participants =
95 room.remote_participants().values().collect::<Vec<_>>();
96 remote_participants.sort_by_key(|p| p.participant_index.0);
97
98 let current_user_face_pile = self.render_collaborator(
99 ¤t_user,
100 peer_id,
101 true,
102 room.is_speaking(),
103 room.is_muted(),
104 None,
105 &room,
106 project_id,
107 ¤t_user,
108 cx,
109 );
110
111 this.children(current_user_face_pile.map(|face_pile| {
112 v_flex()
113 .on_mouse_move(|_, cx| cx.stop_propagation())
114 .child(face_pile)
115 .child(render_color_ribbon(player_colors.local().cursor))
116 }))
117 .children(
118 remote_participants.iter().filter_map(|collaborator| {
119 let player_color = player_colors
120 .color_for_participant(collaborator.participant_index.0);
121 let is_following = workspace
122 .as_ref()?
123 .read(cx)
124 .is_being_followed(collaborator.peer_id);
125 let is_present = project_id.map_or(false, |project_id| {
126 collaborator.location
127 == ParticipantLocation::SharedProject { project_id }
128 });
129
130 let face_pile = self.render_collaborator(
131 &collaborator.user,
132 collaborator.peer_id,
133 is_present,
134 collaborator.speaking,
135 collaborator.muted,
136 is_following.then_some(player_color.selection),
137 &room,
138 project_id,
139 ¤t_user,
140 cx,
141 )?;
142
143 Some(
144 v_flex()
145 .id(("collaborator", collaborator.user.id))
146 .child(face_pile)
147 .child(render_color_ribbon(player_color.cursor))
148 .cursor_pointer()
149 .on_click({
150 let peer_id = collaborator.peer_id;
151 cx.listener(move |this, _, cx| {
152 this.workspace
153 .update(cx, |workspace, cx| {
154 workspace.follow(peer_id, cx);
155 })
156 .ok();
157 })
158 })
159 .tooltip({
160 let login = collaborator.user.github_login.clone();
161 move |cx| {
162 Tooltip::text(format!("Follow {login}"), cx)
163 }
164 }),
165 )
166 }),
167 )
168 },
169 ),
170 )
171 // right side
172 .child(
173 h_flex()
174 .gap_1()
175 .pr_1()
176 .on_mouse_move(|_, cx| cx.stop_propagation())
177 .when_some(room, |this, room| {
178 let room = room.read(cx);
179 let project = self.project.read(cx);
180 let is_local = project.is_local();
181 let is_dev_server_project = project.dev_server_project_id().is_some();
182 let is_shared = (is_local || is_dev_server_project) && project.is_shared();
183 let is_muted = room.is_muted();
184 let is_deafened = room.is_deafened().unwrap_or(false);
185 let is_screen_sharing = room.is_screen_sharing();
186 let can_use_microphone = room.can_use_microphone();
187 let can_share_projects = room.can_share_projects();
188
189 this.when(
190 (is_local || is_dev_server_project) && can_share_projects,
191 |this| {
192 this.child(
193 Button::new(
194 "toggle_sharing",
195 if is_shared { "Unshare" } else { "Share" },
196 )
197 .tooltip(move |cx| {
198 Tooltip::text(
199 if is_shared {
200 "Stop sharing project with call participants"
201 } else {
202 "Share project with call participants"
203 },
204 cx,
205 )
206 })
207 .style(ButtonStyle::Subtle)
208 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
209 .selected(is_shared)
210 .label_size(LabelSize::Small)
211 .on_click(cx.listener(
212 move |this, _, cx| {
213 if is_shared {
214 this.unshare_project(&Default::default(), cx);
215 } else {
216 this.share_project(&Default::default(), cx);
217 }
218 },
219 )),
220 )
221 },
222 )
223 .child(
224 div()
225 .child(
226 IconButton::new("leave-call", ui::IconName::Exit)
227 .style(ButtonStyle::Subtle)
228 .tooltip(|cx| Tooltip::text("Leave call", cx))
229 .icon_size(IconSize::Small)
230 .on_click(move |_, cx| {
231 ActiveCall::global(cx)
232 .update(cx, |call, cx| call.hang_up(cx))
233 .detach_and_log_err(cx);
234 }),
235 )
236 .pr_2(),
237 )
238 .when(can_use_microphone, |this| {
239 this.child(
240 IconButton::new(
241 "mute-microphone",
242 if is_muted {
243 ui::IconName::MicMute
244 } else {
245 ui::IconName::Mic
246 },
247 )
248 .tooltip(move |cx| {
249 Tooltip::text(
250 if !platform_supported {
251 "Cannot share microphone"
252 } else if is_muted {
253 "Unmute microphone"
254 } else {
255 "Mute microphone"
256 },
257 cx,
258 )
259 })
260 .style(ButtonStyle::Subtle)
261 .icon_size(IconSize::Small)
262 .selected(platform_supported && is_muted)
263 .disabled(!platform_supported)
264 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
265 .on_click(move |_, cx| crate::toggle_mute(&Default::default(), cx)),
266 )
267 })
268 .child(
269 IconButton::new(
270 "mute-sound",
271 if is_deafened {
272 ui::IconName::AudioOff
273 } else {
274 ui::IconName::AudioOn
275 },
276 )
277 .style(ButtonStyle::Subtle)
278 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
279 .icon_size(IconSize::Small)
280 .selected(is_deafened)
281 .disabled(!platform_supported)
282 .tooltip(move |cx| {
283 if !platform_supported {
284 Tooltip::text("Cannot share microphone", cx)
285 } else if can_use_microphone {
286 Tooltip::with_meta(
287 "Deafen Audio",
288 None,
289 "Mic will be muted",
290 cx,
291 )
292 } else {
293 Tooltip::text("Deafen Audio", cx)
294 }
295 })
296 .on_click(move |_, cx| crate::toggle_deafen(&Default::default(), cx)),
297 )
298 .when(can_share_projects, |this| {
299 this.child(
300 IconButton::new("screen-share", ui::IconName::Screen)
301 .style(ButtonStyle::Subtle)
302 .icon_size(IconSize::Small)
303 .selected(is_screen_sharing)
304 .disabled(!platform_supported)
305 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
306 .tooltip(move |cx| {
307 Tooltip::text(
308 if !platform_supported {
309 "Cannot share screen"
310 } else if is_screen_sharing {
311 "Stop Sharing Screen"
312 } else {
313 "Share Screen"
314 },
315 cx,
316 )
317 })
318 .on_click(move |_, cx| {
319 crate::toggle_screen_sharing(&Default::default(), cx)
320 }),
321 )
322 })
323 .child(div().pr_2())
324 })
325 .map(|el| {
326 let status = self.client.status();
327 let status = &*status.borrow();
328 if matches!(status, client::Status::Connected { .. }) {
329 el.child(self.render_user_menu_button(cx))
330 } else {
331 el.children(self.render_connection_status(status, cx))
332 .child(self.render_sign_in_button(cx))
333 .child(self.render_user_menu_button(cx))
334 }
335 }),
336 )
337 }
338}
339
340fn render_color_ribbon(color: Hsla) -> impl Element {
341 canvas(
342 move |_, _| {},
343 move |bounds, _, cx| {
344 let height = bounds.size.height;
345 let horizontal_offset = height;
346 let vertical_offset = px(height.0 / 2.0);
347 let mut path = Path::new(bounds.lower_left());
348 path.curve_to(
349 bounds.origin + point(horizontal_offset, vertical_offset),
350 bounds.origin + point(px(0.0), vertical_offset),
351 );
352 path.line_to(bounds.upper_right() + point(-horizontal_offset, vertical_offset));
353 path.curve_to(
354 bounds.lower_right(),
355 bounds.upper_right() + point(px(0.0), vertical_offset),
356 );
357 path.line_to(bounds.lower_left());
358 cx.paint_path(path, color);
359 },
360 )
361 .h_1()
362 .w_full()
363}
364
365impl CollabTitlebarItem {
366 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
367 let project = workspace.project().clone();
368 let user_store = workspace.app_state().user_store.clone();
369 let client = workspace.app_state().client.clone();
370 let active_call = ActiveCall::global(cx);
371 let mut subscriptions = Vec::new();
372 subscriptions.push(
373 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
374 cx.notify()
375 }),
376 );
377 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
378 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
379 subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
380 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
381
382 Self {
383 workspace: workspace.weak_handle(),
384 project,
385 user_store,
386 client,
387 _subscriptions: subscriptions,
388 }
389 }
390
391 pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
392 cfg!(not(target_os = "macos")).then(|| {
393 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
394 let font = cx.text_style().font();
395 let font_id = cx.text_system().resolve_font(&font);
396 let width = cx
397 .text_system()
398 .typographic_bounds(font_id, ui_font_size, 'm')
399 .unwrap()
400 .size
401 .width
402 * 3.0;
403
404 PopoverMenu::new("application-menu")
405 .menu(move |cx| {
406 let width = width;
407 ContextMenu::build(cx, move |menu, _cx| {
408 let width = width;
409 menu.header("Workspace")
410 .action("Open Command Palette", Box::new(command_palette::Toggle))
411 .custom_row(move |cx| {
412 div()
413 .w_full()
414 .flex()
415 .flex_row()
416 .justify_between()
417 .cursor(gpui::CursorStyle::Arrow)
418 .child(Label::new("Buffer Font Size"))
419 .child(
420 div()
421 .flex()
422 .flex_row()
423 .child(div().w(px(16.0)))
424 .child(
425 IconButton::new(
426 "reset-buffer-zoom",
427 IconName::RotateCcw,
428 )
429 .on_click(|_, cx| {
430 cx.dispatch_action(Box::new(
431 zed_actions::ResetBufferFontSize,
432 ))
433 }),
434 )
435 .child(
436 IconButton::new("--buffer-zoom", IconName::Dash)
437 .on_click(|_, cx| {
438 cx.dispatch_action(Box::new(
439 zed_actions::DecreaseBufferFontSize,
440 ))
441 }),
442 )
443 .child(
444 div()
445 .w(width)
446 .flex()
447 .flex_row()
448 .justify_around()
449 .child(Label::new(
450 theme::get_buffer_font_size(cx).to_string(),
451 )),
452 )
453 .child(
454 IconButton::new("+-buffer-zoom", IconName::Plus)
455 .on_click(|_, cx| {
456 cx.dispatch_action(Box::new(
457 zed_actions::IncreaseBufferFontSize,
458 ))
459 }),
460 ),
461 )
462 .into_any_element()
463 })
464 .custom_row(move |cx| {
465 div()
466 .w_full()
467 .flex()
468 .flex_row()
469 .justify_between()
470 .cursor(gpui::CursorStyle::Arrow)
471 .child(Label::new("UI Font Size"))
472 .child(
473 div()
474 .flex()
475 .flex_row()
476 .child(
477 IconButton::new(
478 "reset-ui-zoom",
479 IconName::RotateCcw,
480 )
481 .on_click(|_, cx| {
482 cx.dispatch_action(Box::new(
483 zed_actions::ResetUiFontSize,
484 ))
485 }),
486 )
487 .child(
488 IconButton::new("--ui-zoom", IconName::Dash)
489 .on_click(|_, cx| {
490 cx.dispatch_action(Box::new(
491 zed_actions::DecreaseUiFontSize,
492 ))
493 }),
494 )
495 .child(
496 div()
497 .w(width)
498 .flex()
499 .flex_row()
500 .justify_around()
501 .child(Label::new(
502 theme::get_ui_font_size(cx).to_string(),
503 )),
504 )
505 .child(
506 IconButton::new("+-ui-zoom", IconName::Plus)
507 .on_click(|_, cx| {
508 cx.dispatch_action(Box::new(
509 zed_actions::IncreaseUiFontSize,
510 ))
511 }),
512 ),
513 )
514 .into_any_element()
515 })
516 .header("Project")
517 .action(
518 "Add Folder to Project...",
519 Box::new(workspace::AddFolderToProject),
520 )
521 .action("Open a new Project...", Box::new(workspace::Open))
522 .action(
523 "Open Recent Projects...",
524 Box::new(recent_projects::OpenRecent {
525 create_new_window: false,
526 }),
527 )
528 .header("Help")
529 .action("About Zed", Box::new(zed_actions::About))
530 .action("Welcome", Box::new(workspace::Welcome))
531 .link(
532 "Documentation",
533 Box::new(zed_actions::OpenBrowser {
534 url: "https://zed.dev/docs".into(),
535 }),
536 )
537 .action("Give Feedback", Box::new(feedback::GiveFeedback))
538 .action("Check for Updates", Box::new(auto_update::Check))
539 .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog))
540 .action(
541 "View Dependency Licenses",
542 Box::new(zed_actions::OpenLicenses),
543 )
544 .separator()
545 .action("Quit", Box::new(zed_actions::Quit))
546 })
547 .into()
548 })
549 .trigger(
550 IconButton::new("application-menu", ui::IconName::Menu)
551 .style(ButtonStyle::Subtle)
552 .tooltip(|cx| Tooltip::text("Open Application Menu", cx))
553 .icon_size(IconSize::Small),
554 )
555 .into_any_element()
556 })
557 }
558
559 pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
560 if let Some(dev_server) =
561 self.project
562 .read(cx)
563 .dev_server_project_id()
564 .and_then(|dev_server_project_id| {
565 dev_server_projects::Store::global(cx)
566 .read(cx)
567 .dev_server_for_project(dev_server_project_id)
568 })
569 {
570 return Some(
571 ButtonLike::new("dev_server_trigger")
572 .child(Indicator::dot().color(
573 if dev_server.status == DevServerStatus::Online {
574 Color::Created
575 } else {
576 Color::Disabled
577 },
578 ))
579 .child(
580 Label::new(dev_server.name.clone())
581 .size(LabelSize::Small)
582 .line_height_style(LineHeightStyle::UiLabel),
583 )
584 .tooltip(move |cx| Tooltip::text("Project is hosted on a dev server", cx))
585 .on_click(cx.listener(|this, _, cx| {
586 if let Some(workspace) = this.workspace.upgrade() {
587 recent_projects::DevServerProjects::open(workspace, cx)
588 }
589 }))
590 .into_any_element(),
591 );
592 }
593
594 if self.project.read(cx).is_disconnected() {
595 return Some(
596 Button::new("disconnected", "Disconnected")
597 .disabled(true)
598 .color(Color::Disabled)
599 .style(ButtonStyle::Subtle)
600 .label_size(LabelSize::Small)
601 .into_any_element(),
602 );
603 }
604
605 let host = self.project.read(cx).host()?;
606 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
607 let participant_index = self
608 .user_store
609 .read(cx)
610 .participant_indices()
611 .get(&host_user.id)?;
612 Some(
613 Button::new("project_owner_trigger", host_user.github_login.clone())
614 .color(Color::Player(participant_index.0))
615 .style(ButtonStyle::Subtle)
616 .label_size(LabelSize::Small)
617 .tooltip(move |cx| {
618 Tooltip::text(
619 format!(
620 "{} is sharing this project. Click to follow.",
621 host_user.github_login.clone()
622 ),
623 cx,
624 )
625 })
626 .on_click({
627 let host_peer_id = host.peer_id;
628 cx.listener(move |this, _, cx| {
629 this.workspace
630 .update(cx, |workspace, cx| {
631 workspace.follow(host_peer_id, cx);
632 })
633 .log_err();
634 })
635 })
636 .into_any_element(),
637 )
638 }
639
640 pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
641 let name = {
642 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
643 let worktree = worktree.read(cx);
644 worktree.root_name()
645 });
646
647 names.next()
648 };
649 let is_project_selected = name.is_some();
650 let name = if let Some(name) = name {
651 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
652 } else {
653 "Open recent project".to_string()
654 };
655
656 let workspace = self.workspace.clone();
657 Button::new("project_name_trigger", name)
658 .when(!is_project_selected, |b| b.color(Color::Muted))
659 .style(ButtonStyle::Subtle)
660 .label_size(LabelSize::Small)
661 .tooltip(move |cx| {
662 Tooltip::for_action(
663 "Recent Projects",
664 &recent_projects::OpenRecent {
665 create_new_window: false,
666 },
667 cx,
668 )
669 })
670 .on_click(cx.listener(move |_, _, cx| {
671 if let Some(workspace) = workspace.upgrade() {
672 workspace.update(cx, |workspace, cx| {
673 RecentProjects::open(workspace, false, cx);
674 })
675 }
676 }))
677 }
678
679 pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
680 let entry = {
681 let mut names_and_branches =
682 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
683 let worktree = worktree.read(cx);
684 worktree.root_git_entry()
685 });
686
687 names_and_branches.next().flatten()
688 };
689 let workspace = self.workspace.upgrade()?;
690 let branch_name = entry
691 .as_ref()
692 .and_then(RepositoryEntry::branch)
693 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
694 Some(
695 Button::new("project_branch_trigger", branch_name)
696 .color(Color::Muted)
697 .style(ButtonStyle::Subtle)
698 .label_size(LabelSize::Small)
699 .tooltip(move |cx| {
700 Tooltip::with_meta(
701 "Recent Branches",
702 Some(&ToggleVcsMenu),
703 "Local branches only",
704 cx,
705 )
706 })
707 .on_click(move |_, cx| {
708 let _ = workspace.update(cx, |this, cx| {
709 BranchList::open(this, &Default::default(), cx)
710 });
711 }),
712 )
713 }
714
715 #[allow(clippy::too_many_arguments)]
716 fn render_collaborator(
717 &self,
718 user: &Arc<User>,
719 peer_id: PeerId,
720 is_present: bool,
721 is_speaking: bool,
722 is_muted: bool,
723 leader_selection_color: Option<Hsla>,
724 room: &Room,
725 project_id: Option<u64>,
726 current_user: &Arc<User>,
727 cx: &ViewContext<Self>,
728 ) -> Option<Div> {
729 if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) {
730 return None;
731 }
732
733 const FACEPILE_LIMIT: usize = 3;
734 let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id));
735 let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT);
736
737 Some(
738 div()
739 .m_0p5()
740 .p_0p5()
741 // When the collaborator is not followed, still draw this wrapper div, but leave
742 // it transparent, so that it does not shift the layout when following.
743 .when_some(leader_selection_color, |div, color| {
744 div.rounded_md().bg(color)
745 })
746 .child(
747 FacePile::empty()
748 .child(
749 Avatar::new(user.avatar_uri.clone())
750 .grayscale(!is_present)
751 .border_color(if is_speaking {
752 cx.theme().status().info
753 } else {
754 // We draw the border in a transparent color rather to avoid
755 // the layout shift that would come with adding/removing the border.
756 gpui::transparent_black()
757 })
758 .when(is_muted, |avatar| {
759 avatar.indicator(
760 AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted)
761 .tooltip({
762 let github_login = user.github_login.clone();
763 move |cx| {
764 Tooltip::text(
765 format!("{} is muted", github_login),
766 cx,
767 )
768 }
769 }),
770 )
771 }),
772 )
773 .children(followers.iter().take(FACEPILE_LIMIT).filter_map(
774 |follower_peer_id| {
775 let follower = room
776 .remote_participants()
777 .values()
778 .find_map(|p| {
779 (p.peer_id == *follower_peer_id).then_some(&p.user)
780 })
781 .or_else(|| {
782 (self.client.peer_id() == Some(*follower_peer_id))
783 .then_some(current_user)
784 })?
785 .clone();
786
787 Some(div().mt(-px(4.)).child(
788 Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)),
789 ))
790 },
791 ))
792 .children(if extra_count > 0 {
793 Some(
794 div()
795 .ml_1()
796 .child(Label::new(format!("+{extra_count}")))
797 .into_any_element(),
798 )
799 } else {
800 None
801 }),
802 ),
803 )
804 }
805
806 fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
807 if cx.is_window_active() {
808 ActiveCall::global(cx)
809 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
810 .detach_and_log_err(cx);
811 } else if cx.active_window().is_none() {
812 ActiveCall::global(cx)
813 .update(cx, |call, cx| call.set_location(None, cx))
814 .detach_and_log_err(cx);
815 }
816 self.workspace
817 .update(cx, |workspace, cx| {
818 workspace.update_active_view_for_followers(cx);
819 })
820 .ok();
821 }
822
823 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
824 cx.notify();
825 }
826
827 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
828 let active_call = ActiveCall::global(cx);
829 let project = self.project.clone();
830 active_call
831 .update(cx, |call, cx| call.share_project(project, cx))
832 .detach_and_log_err(cx);
833 }
834
835 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
836 let active_call = ActiveCall::global(cx);
837 let project = self.project.clone();
838 active_call
839 .update(cx, |call, cx| call.unshare_project(project, cx))
840 .log_err();
841 }
842
843 fn render_connection_status(
844 &self,
845 status: &client::Status,
846 cx: &mut ViewContext<Self>,
847 ) -> Option<AnyElement> {
848 match status {
849 client::Status::ConnectionError
850 | client::Status::ConnectionLost
851 | client::Status::Reauthenticating { .. }
852 | client::Status::Reconnecting { .. }
853 | client::Status::ReconnectionError { .. } => Some(
854 div()
855 .id("disconnected")
856 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
857 .tooltip(|cx| Tooltip::text("Disconnected", cx))
858 .into_any_element(),
859 ),
860 client::Status::UpgradeRequired => {
861 let auto_updater = auto_update::AutoUpdater::get(cx);
862 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
863 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
864 Some(AutoUpdateStatus::Installing)
865 | Some(AutoUpdateStatus::Downloading)
866 | Some(AutoUpdateStatus::Checking) => "Updating...",
867 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
868 "Please update Zed to Collaborate"
869 }
870 };
871
872 Some(
873 Button::new("connection-status", label)
874 .label_size(LabelSize::Small)
875 .on_click(|_, cx| {
876 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
877 if auto_updater.read(cx).status().is_updated() {
878 workspace::reload(&Default::default(), cx);
879 return;
880 }
881 }
882 auto_update::check(&Default::default(), cx);
883 })
884 .into_any_element(),
885 )
886 }
887 _ => None,
888 }
889 }
890
891 pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
892 let client = self.client.clone();
893 Button::new("sign_in", "Sign in")
894 .label_size(LabelSize::Small)
895 .on_click(move |_, cx| {
896 let client = client.clone();
897 cx.spawn(move |mut cx| async move {
898 client
899 .authenticate_and_connect(true, &cx)
900 .await
901 .notify_async_err(&mut cx);
902 })
903 .detach();
904 })
905 }
906
907 pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
908 if let Some(user) = self.user_store.read(cx).current_user() {
909 PopoverMenu::new("user-menu")
910 .menu(|cx| {
911 ContextMenu::build(cx, |menu, _| {
912 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
913 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
914 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
915 .action("Extensions", extensions_ui::Extensions.boxed_clone())
916 .separator()
917 .action("Sign Out", client::SignOut.boxed_clone())
918 })
919 .into()
920 })
921 .trigger(
922 ButtonLike::new("user-menu")
923 .child(
924 h_flex()
925 .gap_0p5()
926 .child(Avatar::new(user.avatar_uri.clone()))
927 .child(
928 Icon::new(IconName::ChevronDown)
929 .size(IconSize::Small)
930 .color(Color::Muted),
931 ),
932 )
933 .style(ButtonStyle::Subtle)
934 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
935 )
936 .anchor(gpui::AnchorCorner::TopRight)
937 } else {
938 PopoverMenu::new("user-menu")
939 .menu(|cx| {
940 ContextMenu::build(cx, |menu, _| {
941 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
942 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
943 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
944 .action("Extensions", extensions_ui::Extensions.boxed_clone())
945 })
946 .into()
947 })
948 .trigger(
949 ButtonLike::new("user-menu")
950 .child(
951 h_flex().gap_0p5().child(
952 Icon::new(IconName::ChevronDown)
953 .size(IconSize::Small)
954 .color(Color::Muted),
955 ),
956 )
957 .style(ButtonStyle::Subtle)
958 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
959 )
960 }
961 }
962}