1mod application_menu;
2pub mod collab;
3mod onboarding_banner;
4mod plan_chip;
5mod title_bar_settings;
6mod update_version;
7
8use crate::application_menu::{ApplicationMenu, show_menus};
9use crate::plan_chip::PlanChip;
10pub use platform_title_bar::{
11 self, DraggedWindowTab, MergeAllWindows, MoveTabToNewWindow, PlatformTitleBar,
12 ShowNextWindowTab, ShowPreviousWindowTab,
13};
14use project::linked_worktree_short_name;
15
16#[cfg(not(target_os = "macos"))]
17use crate::application_menu::{
18 ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
19};
20
21use auto_update::AutoUpdateStatus;
22use call::ActiveCall;
23use client::{Client, UserStore, zed_urls};
24use cloud_api_types::Plan;
25
26use gpui::{
27 Action, Animation, AnimationExt, AnyElement, App, Context, Corner, Element, Entity, Focusable,
28 InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
29 StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
30 pulsating_between,
31};
32use onboarding_banner::OnboardingBanner;
33use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
34use remote::RemoteConnectionOptions;
35use settings::Settings;
36
37use std::sync::Arc;
38use std::time::Duration;
39use theme::ActiveTheme;
40use title_bar_settings::TitleBarSettings;
41use ui::{
42 Avatar, ButtonLike, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle,
43 TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
44};
45use update_version::UpdateVersion;
46use util::ResultExt;
47use workspace::{
48 MultiWorkspace, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt,
49};
50
51use zed_actions::OpenRemote;
52
53pub use onboarding_banner::restore_banner;
54
55const MAX_PROJECT_NAME_LENGTH: usize = 40;
56const MAX_BRANCH_NAME_LENGTH: usize = 40;
57const MAX_SHORT_SHA_LENGTH: usize = 8;
58
59actions!(
60 collab,
61 [
62 /// Toggles the user menu dropdown.
63 ToggleUserMenu,
64 /// Toggles the project menu dropdown.
65 ToggleProjectMenu,
66 /// Switches to a different git branch.
67 SwitchBranch,
68 /// A debug action to simulate an update being available to test the update banner UI.
69 SimulateUpdateAvailable
70 ]
71);
72
73pub fn init(cx: &mut App) {
74 platform_title_bar::PlatformTitleBar::init(cx);
75
76 cx.observe_new(|workspace: &mut Workspace, window, cx| {
77 let Some(window) = window else {
78 return;
79 };
80 let multi_workspace = workspace.multi_workspace().cloned();
81 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, multi_workspace, window, cx));
82 workspace.set_titlebar_item(item.into(), window, cx);
83
84 workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| {
85 if let Some(titlebar) = workspace
86 .titlebar_item()
87 .and_then(|item| item.downcast::<TitleBar>().ok())
88 {
89 titlebar.update(cx, |titlebar, cx| {
90 titlebar.toggle_update_simulation(cx);
91 });
92 }
93 });
94
95 #[cfg(not(target_os = "macos"))]
96 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
97 if let Some(titlebar) = workspace
98 .titlebar_item()
99 .and_then(|item| item.downcast::<TitleBar>().ok())
100 {
101 titlebar.update(cx, |titlebar, cx| {
102 if let Some(ref menu) = titlebar.application_menu {
103 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
104 }
105 });
106 }
107 });
108
109 #[cfg(not(target_os = "macos"))]
110 workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
111 if let Some(titlebar) = workspace
112 .titlebar_item()
113 .and_then(|item| item.downcast::<TitleBar>().ok())
114 {
115 titlebar.update(cx, |titlebar, cx| {
116 if let Some(ref menu) = titlebar.application_menu {
117 menu.update(cx, |menu, cx| {
118 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
119 });
120 }
121 });
122 }
123 });
124
125 #[cfg(not(target_os = "macos"))]
126 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
127 if let Some(titlebar) = workspace
128 .titlebar_item()
129 .and_then(|item| item.downcast::<TitleBar>().ok())
130 {
131 titlebar.update(cx, |titlebar, cx| {
132 if let Some(ref menu) = titlebar.application_menu {
133 menu.update(cx, |menu, cx| {
134 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
135 });
136 }
137 });
138 }
139 });
140 })
141 .detach();
142}
143
144pub struct TitleBar {
145 platform_titlebar: Entity<PlatformTitleBar>,
146 project: Entity<Project>,
147 user_store: Entity<UserStore>,
148 client: Arc<Client>,
149 workspace: WeakEntity<Workspace>,
150 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
151 application_menu: Option<Entity<ApplicationMenu>>,
152 _subscriptions: Vec<Subscription>,
153 banner: Option<Entity<OnboardingBanner>>,
154 update_version: Entity<UpdateVersion>,
155 screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
156 _diagnostics_subscription: Option<gpui::Subscription>,
157}
158
159impl Render for TitleBar {
160 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
161 if self.multi_workspace.is_none() {
162 if let Some(mw) = self
163 .workspace
164 .upgrade()
165 .and_then(|ws| ws.read(cx).multi_workspace().cloned())
166 {
167 self.multi_workspace = Some(mw.clone());
168 self.platform_titlebar.update(cx, |titlebar, _cx| {
169 titlebar.set_multi_workspace(mw);
170 });
171 }
172 }
173
174 let title_bar_settings = *TitleBarSettings::get_global(cx);
175 let button_layout = title_bar_settings.button_layout;
176
177 let show_menus = show_menus(cx);
178
179 let mut children = Vec::new();
180
181 let mut project_name = None;
182 let mut repository = None;
183 let mut linked_worktree_name = None;
184 if let Some(worktree) = self.effective_active_worktree(cx) {
185 repository = self.get_repository_for_worktree(&worktree, cx);
186 let worktree = worktree.read(cx);
187 project_name = worktree
188 .root_name()
189 .file_name()
190 .map(|name| SharedString::from(name.to_string()));
191 linked_worktree_name = repository.as_ref().and_then(|repo| {
192 let repo = repo.read(cx);
193 linked_worktree_short_name(
194 repo.original_repo_abs_path.as_ref(),
195 repo.work_directory_abs_path.as_ref(),
196 )
197 .filter(|name| Some(name) != project_name.as_ref())
198 });
199 }
200
201 children.push(
202 h_flex()
203 .h_full()
204 .gap_0p5()
205 .map(|title_bar| {
206 let mut render_project_items = title_bar_settings.show_branch_name
207 || title_bar_settings.show_project_items;
208 title_bar
209 .when_some(
210 self.application_menu.clone().filter(|_| !show_menus),
211 |title_bar, menu| {
212 render_project_items &=
213 !menu.update(cx, |menu, cx| menu.all_menus_shown(cx));
214 title_bar.child(menu)
215 },
216 )
217 .children(self.render_restricted_mode(cx))
218 .when(render_project_items, |title_bar| {
219 title_bar
220 .when(title_bar_settings.show_project_items, |title_bar| {
221 title_bar
222 .children(self.render_project_host(cx))
223 .child(self.render_project_name(project_name, window, cx))
224 })
225 .when_some(
226 repository.filter(|_| title_bar_settings.show_branch_name),
227 |title_bar, repository| {
228 title_bar.children(self.render_project_branch(
229 repository,
230 linked_worktree_name,
231 cx,
232 ))
233 },
234 )
235 })
236 })
237 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
238 .into_any_element(),
239 );
240
241 children.push(self.render_collaborator_list(window, cx).into_any_element());
242
243 if title_bar_settings.show_onboarding_banner {
244 if let Some(banner) = &self.banner {
245 children.push(banner.clone().into_any_element())
246 }
247 }
248
249 let status = self.client.status();
250 let status = &*status.borrow();
251 let user = self.user_store.read(cx).current_user();
252
253 let signed_in = user.is_some();
254 let is_signing_in = user.is_none()
255 && matches!(
256 status,
257 client::Status::Authenticating
258 | client::Status::Authenticated
259 | client::Status::Connecting
260 );
261 let is_signed_out_or_auth_error = user.is_none()
262 && matches!(
263 status,
264 client::Status::SignedOut | client::Status::AuthenticationError
265 );
266
267 children.push(
268 h_flex()
269 .map(|this| {
270 if signed_in {
271 this.pr_1p5()
272 } else {
273 this.pr_1()
274 }
275 })
276 .gap_1()
277 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
278 .children(self.render_call_controls(window, cx))
279 .children(self.render_connection_status(status, cx))
280 .child(self.update_version.clone())
281 .when(
282 user.is_none()
283 && is_signed_out_or_auth_error
284 && TitleBarSettings::get_global(cx).show_sign_in,
285 |this| this.child(self.render_sign_in_button(cx)),
286 )
287 .when(is_signing_in, |this| {
288 this.child(
289 Label::new("Signing in…")
290 .size(LabelSize::Small)
291 .color(Color::Muted)
292 .with_animation(
293 "signing-in",
294 Animation::new(Duration::from_secs(2))
295 .repeat()
296 .with_easing(pulsating_between(0.4, 0.8)),
297 |label, delta| label.alpha(delta),
298 ),
299 )
300 })
301 .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
302 this.child(self.render_user_menu_button(cx))
303 })
304 .into_any_element(),
305 );
306
307 if show_menus {
308 self.platform_titlebar.update(cx, |this, _| {
309 this.set_button_layout(button_layout);
310 this.set_children(
311 self.application_menu
312 .clone()
313 .map(|menu| menu.into_any_element()),
314 );
315 });
316
317 let height = platform_title_bar_height(window);
318 let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
319 platform_titlebar.title_bar_color(window, cx)
320 });
321
322 v_flex()
323 .w_full()
324 .child(self.platform_titlebar.clone().into_any_element())
325 .child(
326 h_flex()
327 .bg(title_bar_color)
328 .h(height)
329 .pl_2()
330 .justify_between()
331 .w_full()
332 .children(children),
333 )
334 .into_any_element()
335 } else {
336 self.platform_titlebar.update(cx, |this, _| {
337 this.set_button_layout(button_layout);
338 this.set_children(children);
339 });
340 self.platform_titlebar.clone().into_any_element()
341 }
342 }
343}
344
345impl TitleBar {
346 pub fn new(
347 id: impl Into<ElementId>,
348 workspace: &Workspace,
349 multi_workspace: Option<WeakEntity<MultiWorkspace>>,
350 window: &mut Window,
351 cx: &mut Context<Self>,
352 ) -> Self {
353 let project = workspace.project().clone();
354 let git_store = project.read(cx).git_store().clone();
355 let user_store = workspace.app_state().user_store.clone();
356 let client = workspace.app_state().client.clone();
357 let active_call = ActiveCall::global(cx);
358
359 let platform_style = PlatformStyle::platform();
360 let application_menu = match platform_style {
361 PlatformStyle::Mac => {
362 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
363 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
364 } else {
365 None
366 }
367 }
368 PlatformStyle::Linux | PlatformStyle::Windows => {
369 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
370 }
371 };
372
373 let mut subscriptions = Vec::new();
374 subscriptions.push(
375 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
376 cx.notify()
377 }),
378 );
379
380 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
381 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
382 subscriptions.push(
383 cx.subscribe(&git_store, move |_, _, event, cx| match event {
384 GitStoreEvent::ActiveRepositoryChanged(_)
385 | GitStoreEvent::RepositoryUpdated(_, _, true) => {
386 cx.notify();
387 }
388 _ => {}
389 }),
390 );
391 subscriptions.push(cx.observe(&user_store, |_a, _, cx| cx.notify()));
392 subscriptions.push(cx.observe_button_layout_changed(window, |_, _, cx| cx.notify()));
393 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
394 subscriptions.push(cx.subscribe(&trusted_worktrees, |_, _, _, cx| {
395 cx.notify();
396 }));
397 }
398
399 let update_version = cx.new(|cx| UpdateVersion::new(cx));
400 let platform_titlebar = cx.new(|cx| {
401 let mut titlebar = PlatformTitleBar::new(id, cx);
402 if let Some(mw) = multi_workspace.clone() {
403 titlebar = titlebar.with_multi_workspace(mw);
404 }
405 titlebar
406 });
407
408 let mut this = Self {
409 platform_titlebar,
410 application_menu,
411 workspace: workspace.weak_handle(),
412 multi_workspace,
413 project,
414 user_store,
415 client,
416 _subscriptions: subscriptions,
417 banner: None,
418 update_version,
419 screen_share_popover_handle: PopoverMenuHandle::default(),
420 _diagnostics_subscription: None,
421 };
422
423 this.observe_diagnostics(cx);
424
425 this
426 }
427
428 fn worktree_count(&self, cx: &App) -> usize {
429 self.project.read(cx).visible_worktrees(cx).count()
430 }
431
432 fn toggle_update_simulation(&mut self, cx: &mut Context<Self>) {
433 self.update_version
434 .update(cx, |banner, cx| banner.update_simulation(cx));
435 cx.notify();
436 }
437
438 /// Returns the worktree to display in the title bar.
439 /// - Prefer the worktree owning the project's active repository
440 /// - Fall back to the first visible worktree
441 pub fn effective_active_worktree(&self, cx: &App) -> Option<Entity<project::Worktree>> {
442 let project = self.project.read(cx);
443
444 if let Some(repo) = project.active_repository(cx) {
445 let repo = repo.read(cx);
446 let repo_path = &repo.work_directory_abs_path;
447
448 for worktree in project.visible_worktrees(cx) {
449 let worktree_path = worktree.read(cx).abs_path();
450 if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
451 return Some(worktree);
452 }
453 }
454 }
455
456 project.visible_worktrees(cx).next()
457 }
458
459 fn get_repository_for_worktree(
460 &self,
461 worktree: &Entity<project::Worktree>,
462 cx: &App,
463 ) -> Option<Entity<project::git_store::Repository>> {
464 let project = self.project.read(cx);
465 let git_store = project.git_store().read(cx);
466 let worktree_path = worktree.read(cx).abs_path();
467
468 git_store
469 .repositories()
470 .values()
471 .filter(|repo| {
472 let repo_path = &repo.read(cx).work_directory_abs_path;
473 worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref())
474 })
475 .max_by_key(|repo| repo.read(cx).work_directory_abs_path.as_os_str().len())
476 .cloned()
477 }
478
479 fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
480 let workspace = self.workspace.clone();
481
482 let options = self.project.read(cx).remote_connection_options(cx)?;
483 let host: SharedString = options.display_name().into();
484
485 let (nickname, tooltip_title, icon) = match options {
486 RemoteConnectionOptions::Ssh(options) => (
487 options.nickname.map(|nick| nick.into()),
488 "Remote Project",
489 IconName::Server,
490 ),
491 RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
492 RemoteConnectionOptions::Docker(_dev_container_connection) => {
493 (None, "Dev Container", IconName::Box)
494 }
495 #[cfg(any(test, feature = "test-support"))]
496 RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
497 };
498
499 let nickname = nickname.unwrap_or_else(|| host.clone());
500
501 let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
502 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
503 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
504 remote::ConnectionState::HeartbeatMissed => (
505 Color::Warning,
506 format!("Connection attempt to {host} missed. Retrying..."),
507 ),
508 remote::ConnectionState::Reconnecting => (
509 Color::Warning,
510 format!("Lost connection to {host}. Reconnecting..."),
511 ),
512 remote::ConnectionState::Disconnected => {
513 (Color::Error, format!("Disconnected from {host}"))
514 }
515 };
516
517 let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
518 remote::ConnectionState::Connecting => Color::Info,
519 remote::ConnectionState::Connected => Color::Default,
520 remote::ConnectionState::HeartbeatMissed => Color::Warning,
521 remote::ConnectionState::Reconnecting => Color::Warning,
522 remote::ConnectionState::Disconnected => Color::Error,
523 };
524
525 let meta = SharedString::from(meta);
526
527 Some(
528 PopoverMenu::new("remote-project-menu")
529 .menu(move |window, cx| {
530 let workspace_entity = workspace.upgrade()?;
531 let fs = workspace_entity.read(cx).project().read(cx).fs().clone();
532 Some(recent_projects::RemoteServerProjects::popover(
533 fs,
534 workspace.clone(),
535 false,
536 window,
537 cx,
538 ))
539 })
540 .trigger_with_tooltip(
541 ButtonLike::new("remote_project")
542 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
543 .child(
544 h_flex()
545 .gap_2()
546 .max_w_32()
547 .child(
548 IconWithIndicator::new(
549 Icon::new(icon).size(IconSize::Small).color(icon_color),
550 Some(Indicator::dot().color(indicator_color)),
551 )
552 .indicator_border_color(Some(
553 cx.theme().colors().title_bar_background,
554 ))
555 .into_any_element(),
556 )
557 .child(Label::new(nickname).size(LabelSize::Small).truncate()),
558 ),
559 move |_window, cx| {
560 Tooltip::with_meta(
561 tooltip_title,
562 Some(&OpenRemote {
563 from_existing_connection: false,
564 create_new_window: false,
565 }),
566 meta.clone(),
567 cx,
568 )
569 },
570 )
571 .anchor(gpui::Corner::TopLeft)
572 .into_any_element(),
573 )
574 }
575
576 pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
577 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
578 .map(|trusted_worktrees| {
579 trusted_worktrees
580 .read(cx)
581 .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
582 })
583 .unwrap_or(false);
584 if !has_restricted_worktrees {
585 return None;
586 }
587
588 let button = Button::new("restricted_mode_trigger", "Restricted Mode")
589 .style(ButtonStyle::Tinted(TintColor::Warning))
590 .label_size(LabelSize::Small)
591 .color(Color::Warning)
592 .start_icon(
593 Icon::new(IconName::Warning)
594 .size(IconSize::Small)
595 .color(Color::Warning),
596 )
597 .tooltip(|_, cx| {
598 Tooltip::with_meta(
599 "You're in Restricted Mode",
600 Some(&ToggleWorktreeSecurity),
601 "Mark this project as trusted and unlock all features",
602 cx,
603 )
604 })
605 .on_click({
606 cx.listener(move |this, _, window, cx| {
607 this.workspace
608 .update(cx, |workspace, cx| {
609 workspace.show_worktree_trust_security_modal(true, window, cx)
610 })
611 .log_err();
612 })
613 });
614
615 if cfg!(macos_sdk_26) {
616 // Make up for Tahoe's traffic light buttons having less spacing around them
617 Some(div().child(button).ml_0p5().into_any_element())
618 } else {
619 Some(button.into_any_element())
620 }
621 }
622
623 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
624 if self.project.read(cx).is_via_remote_server() {
625 return self.render_remote_project_connection(cx);
626 }
627
628 if self.project.read(cx).is_disconnected(cx) {
629 return Some(
630 Button::new("disconnected", "Disconnected")
631 .disabled(true)
632 .color(Color::Disabled)
633 .label_size(LabelSize::Small)
634 .into_any_element(),
635 );
636 }
637
638 let host = self.project.read(cx).host()?;
639 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
640 let participant_index = self
641 .user_store
642 .read(cx)
643 .participant_indices()
644 .get(&host_user.id)?;
645
646 Some(
647 Button::new("project_owner_trigger", host_user.github_login.clone())
648 .color(Color::Player(participant_index.0))
649 .label_size(LabelSize::Small)
650 .tooltip(move |_, cx| {
651 let tooltip_title = format!(
652 "{} is sharing this project. Click to follow.",
653 host_user.github_login
654 );
655
656 Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
657 })
658 .on_click({
659 let host_peer_id = host.peer_id;
660 cx.listener(move |this, _, window, cx| {
661 this.workspace
662 .update(cx, |workspace, cx| {
663 workspace.follow(host_peer_id, window, cx);
664 })
665 .log_err();
666 })
667 })
668 .into_any_element(),
669 )
670 }
671
672 fn render_project_name(
673 &self,
674 name: Option<SharedString>,
675 _: &mut Window,
676 cx: &mut Context<Self>,
677 ) -> impl IntoElement {
678 let workspace = self.workspace.clone();
679
680 let is_project_selected = name.is_some();
681
682 let display_name = if let Some(ref name) = name {
683 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
684 } else {
685 "Open Recent Project".to_string()
686 };
687
688 let is_sidebar_open = self
689 .multi_workspace
690 .as_ref()
691 .and_then(|mw| mw.upgrade())
692 .map(|mw| mw.read(cx).sidebar_open())
693 .unwrap_or(false)
694 && PlatformTitleBar::is_multi_workspace_enabled(cx);
695
696 let is_threads_list_view_active = self
697 .multi_workspace
698 .as_ref()
699 .and_then(|mw| mw.upgrade())
700 .map(|mw| mw.read(cx).is_threads_list_view_active(cx))
701 .unwrap_or(false);
702
703 if is_sidebar_open && is_threads_list_view_active {
704 return self
705 .render_recent_projects_popover(display_name, is_project_selected, cx)
706 .into_any_element();
707 }
708
709 let focus_handle = workspace
710 .upgrade()
711 .map(|w| w.read(cx).focus_handle(cx))
712 .unwrap_or_else(|| cx.focus_handle());
713
714 let window_project_groups: Vec<_> = self
715 .multi_workspace
716 .as_ref()
717 .and_then(|mw| mw.upgrade())
718 .map(|mw| mw.read(cx).project_group_keys(cx))
719 .unwrap_or_default();
720
721 PopoverMenu::new("recent-projects-menu")
722 .menu(move |window, cx| {
723 Some(recent_projects::RecentProjects::popover(
724 workspace.clone(),
725 window_project_groups.clone(),
726 false,
727 focus_handle.clone(),
728 window,
729 cx,
730 ))
731 })
732 .trigger_with_tooltip(
733 Button::new("project_name_trigger", display_name)
734 .label_size(LabelSize::Small)
735 .when(self.worktree_count(cx) > 1, |this| {
736 this.end_icon(
737 Icon::new(IconName::ChevronDown)
738 .size(IconSize::XSmall)
739 .color(Color::Muted),
740 )
741 })
742 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
743 .when(!is_project_selected, |s| s.color(Color::Muted)),
744 move |_window, cx| {
745 Tooltip::for_action(
746 "Recent Projects",
747 &zed_actions::OpenRecent {
748 create_new_window: false,
749 },
750 cx,
751 )
752 },
753 )
754 .anchor(gpui::Corner::TopLeft)
755 .into_any_element()
756 }
757
758 fn render_recent_projects_popover(
759 &self,
760 display_name: String,
761 is_project_selected: bool,
762 cx: &mut Context<Self>,
763 ) -> impl IntoElement {
764 let workspace = self.workspace.clone();
765
766 let focus_handle = workspace
767 .upgrade()
768 .map(|w| w.read(cx).focus_handle(cx))
769 .unwrap_or_else(|| cx.focus_handle());
770
771 let window_project_groups: Vec<_> = self
772 .multi_workspace
773 .as_ref()
774 .and_then(|mw| mw.upgrade())
775 .map(|mw| mw.read(cx).project_group_keys(cx))
776 .unwrap_or_default();
777
778 PopoverMenu::new("sidebar-title-recent-projects-menu")
779 .menu(move |window, cx| {
780 Some(recent_projects::RecentProjects::popover(
781 workspace.clone(),
782 window_project_groups.clone(),
783 false,
784 focus_handle.clone(),
785 window,
786 cx,
787 ))
788 })
789 .trigger_with_tooltip(
790 Button::new("project_name_trigger", display_name)
791 .label_size(LabelSize::Small)
792 .when(self.worktree_count(cx) > 1, |this| {
793 this.end_icon(
794 Icon::new(IconName::ChevronDown)
795 .size(IconSize::XSmall)
796 .color(Color::Muted),
797 )
798 })
799 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
800 .when(!is_project_selected, |s| s.color(Color::Muted)),
801 move |_window, cx| {
802 Tooltip::for_action(
803 "Recent Projects",
804 &zed_actions::OpenRecent {
805 create_new_window: false,
806 },
807 cx,
808 )
809 },
810 )
811 .anchor(gpui::Corner::TopLeft)
812 }
813
814 fn render_project_branch(
815 &self,
816 repository: Entity<project::git_store::Repository>,
817 linked_worktree_name: Option<SharedString>,
818 cx: &mut Context<Self>,
819 ) -> Option<impl IntoElement> {
820 let workspace = self.workspace.upgrade()?;
821
822 let (branch_name, icon_info) = {
823 let repo = repository.read(cx);
824
825 let branch_name = repo
826 .branch
827 .as_ref()
828 .map(|branch| branch.name())
829 .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
830 .or_else(|| {
831 repo.head_commit.as_ref().map(|commit| {
832 commit
833 .sha
834 .chars()
835 .take(MAX_SHORT_SHA_LENGTH)
836 .collect::<String>()
837 })
838 });
839
840 let status = repo.status_summary();
841 let tracked = status.index + status.worktree;
842 let icon_info = if status.conflict > 0 {
843 (IconName::Warning, Color::VersionControlConflict)
844 } else if tracked.modified > 0 {
845 (IconName::SquareDot, Color::VersionControlModified)
846 } else if tracked.added > 0 || status.untracked > 0 {
847 (IconName::SquarePlus, Color::VersionControlAdded)
848 } else if tracked.deleted > 0 {
849 (IconName::SquareMinus, Color::VersionControlDeleted)
850 } else {
851 (IconName::GitBranch, Color::Muted)
852 };
853
854 (branch_name, icon_info)
855 };
856
857 let branch_name = branch_name?;
858 let settings = TitleBarSettings::get_global(cx);
859 let effective_repository = Some(repository);
860
861 Some(
862 PopoverMenu::new("branch-menu")
863 .menu(move |window, cx| {
864 Some(git_ui::git_picker::popover(
865 workspace.downgrade(),
866 effective_repository.clone(),
867 git_ui::git_picker::GitPickerTab::Branches,
868 gpui::rems(34.),
869 window,
870 cx,
871 ))
872 })
873 .trigger_with_tooltip(
874 ButtonLike::new("project_branch_trigger")
875 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
876 .child(
877 h_flex()
878 .gap_0p5()
879 .when(settings.show_branch_icon, |this| {
880 let (icon, icon_color) = icon_info;
881 this.child(
882 Icon::new(icon).size(IconSize::XSmall).color(icon_color),
883 )
884 })
885 .when_some(linked_worktree_name.as_ref(), |this, worktree_name| {
886 this.child(
887 Label::new(worktree_name)
888 .size(LabelSize::Small)
889 .color(Color::Muted),
890 )
891 .child(
892 Label::new("/").size(LabelSize::Small).color(
893 Color::Custom(
894 cx.theme().colors().text_muted.opacity(0.4),
895 ),
896 ),
897 )
898 })
899 .child(
900 Label::new(branch_name)
901 .size(LabelSize::Small)
902 .color(Color::Muted),
903 ),
904 ),
905 move |_window, cx| {
906 Tooltip::with_meta(
907 "Git Switcher",
908 Some(&zed_actions::git::Branch),
909 "Worktrees, Branches, and Stashes",
910 cx,
911 )
912 },
913 )
914 .anchor(gpui::Corner::TopLeft),
915 )
916 }
917
918 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
919 if window.is_window_active() {
920 ActiveCall::global(cx)
921 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
922 .detach_and_log_err(cx);
923 } else if cx.active_window().is_none() {
924 ActiveCall::global(cx)
925 .update(cx, |call, cx| call.set_location(None, cx))
926 .detach_and_log_err(cx);
927 }
928 self.workspace
929 .update(cx, |workspace, cx| {
930 workspace.update_active_view_for_followers(window, cx);
931 })
932 .ok();
933 }
934
935 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
936 self.observe_diagnostics(cx);
937 cx.notify();
938 }
939
940 fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
941 let diagnostics = ActiveCall::global(cx)
942 .read(cx)
943 .room()
944 .and_then(|room| room.read(cx).diagnostics().cloned());
945
946 if let Some(diagnostics) = diagnostics {
947 self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
948 } else {
949 self._diagnostics_subscription = None;
950 }
951 }
952
953 fn share_project(&mut self, cx: &mut Context<Self>) {
954 let active_call = ActiveCall::global(cx);
955 let project = self.project.clone();
956 active_call
957 .update(cx, |call, cx| call.share_project(project, cx))
958 .detach_and_log_err(cx);
959 }
960
961 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
962 let active_call = ActiveCall::global(cx);
963 let project = self.project.clone();
964 active_call
965 .update(cx, |call, cx| call.unshare_project(project, cx))
966 .log_err();
967 }
968
969 fn render_connection_status(
970 &self,
971 status: &client::Status,
972 cx: &mut Context<Self>,
973 ) -> Option<AnyElement> {
974 match status {
975 client::Status::ConnectionError
976 | client::Status::ConnectionLost
977 | client::Status::Reauthenticating
978 | client::Status::Reconnecting
979 | client::Status::ReconnectionError { .. } => Some(
980 div()
981 .id("disconnected")
982 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
983 .tooltip(Tooltip::text("Disconnected"))
984 .into_any_element(),
985 ),
986 client::Status::UpgradeRequired => {
987 let auto_updater = auto_update::AutoUpdater::get(cx);
988 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
989 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
990 Some(AutoUpdateStatus::Installing { .. })
991 | Some(AutoUpdateStatus::Downloading { .. })
992 | Some(AutoUpdateStatus::Checking) => "Updating...",
993 Some(AutoUpdateStatus::Idle)
994 | Some(AutoUpdateStatus::Errored { .. })
995 | None => "Please update Zed to Collaborate",
996 };
997
998 Some(
999 Button::new("connection-status", label)
1000 .label_size(LabelSize::Small)
1001 .on_click(|_, window, cx| {
1002 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1003 && auto_updater.read(cx).status().is_updated()
1004 {
1005 workspace::reload(cx);
1006 return;
1007 }
1008 auto_update::check(&Default::default(), window, cx);
1009 })
1010 .into_any_element(),
1011 )
1012 }
1013 _ => None,
1014 }
1015 }
1016
1017 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1018 let client = self.client.clone();
1019 let workspace = self.workspace.clone();
1020 Button::new("sign_in", "Sign In")
1021 .label_size(LabelSize::Small)
1022 .on_click(move |_, window, cx| {
1023 let client = client.clone();
1024 let workspace = workspace.clone();
1025 window
1026 .spawn(cx, async move |mut cx| {
1027 client
1028 .sign_in_with_optional_connect(true, cx)
1029 .await
1030 .notify_workspace_async_err(workspace, &mut cx);
1031 })
1032 .detach();
1033 })
1034 }
1035
1036 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1037 let show_update_button = self.update_version.read(cx).show_update_in_menu_bar();
1038
1039 let user_store = self.user_store.clone();
1040 let user_store_read = user_store.read(cx);
1041 let user = user_store_read.current_user();
1042
1043 let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1044 let user_login = user.as_ref().map(|u| u.github_login.clone());
1045
1046 let is_signed_in = user.is_some();
1047
1048 let has_subscription_period = user_store_read.subscription_period().is_some();
1049 let plan = user_store_read.plan().filter(|_| {
1050 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
1051 has_subscription_period
1052 });
1053
1054 let has_organization = user_store_read.current_organization().is_some();
1055
1056 let current_organization = user_store_read.current_organization();
1057 let business_organization = current_organization
1058 .as_ref()
1059 .filter(|organization| !organization.is_personal);
1060 let organizations: Vec<_> = user_store_read
1061 .organizations()
1062 .iter()
1063 .map(|org| {
1064 let plan = user_store_read.plan_for_organization(&org.id);
1065 (org.clone(), plan)
1066 })
1067 .collect();
1068
1069 let show_user_picture = TitleBarSettings::get_global(cx).show_user_picture;
1070
1071 let trigger = if is_signed_in && show_user_picture {
1072 let avatar = user_avatar.map(|avatar| Avatar::new(avatar)).map(|avatar| {
1073 if show_update_button {
1074 avatar.indicator(
1075 div()
1076 .absolute()
1077 .bottom_0()
1078 .right_0()
1079 .child(Indicator::dot().color(Color::Accent)),
1080 )
1081 } else {
1082 avatar
1083 }
1084 });
1085
1086 ButtonLike::new("user-menu").child(
1087 h_flex()
1088 .when_some(business_organization, |this, organization| {
1089 this.gap_2()
1090 .child(Label::new(&organization.name).size(LabelSize::Small))
1091 })
1092 .children(avatar),
1093 )
1094 } else {
1095 ButtonLike::new("user-menu")
1096 .child(Icon::new(IconName::ChevronDown).size(IconSize::Small))
1097 };
1098
1099 PopoverMenu::new("user-menu")
1100 .trigger(trigger)
1101 .menu(move |window, cx| {
1102 let user_login = user_login.clone();
1103 let current_organization = current_organization.clone();
1104 let organizations = organizations.clone();
1105 let user_store = user_store.clone();
1106
1107 ContextMenu::build(window, cx, |menu, _, _cx| {
1108 menu.when(is_signed_in, |this| {
1109 let user_login = user_login.clone();
1110 this.custom_entry(
1111 move |_window, _cx| {
1112 let user_login = user_login.clone().unwrap_or_default();
1113
1114 h_flex()
1115 .w_full()
1116 .justify_between()
1117 .child(Label::new(user_login))
1118 .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1119 .into_any_element()
1120 },
1121 move |_, cx| {
1122 cx.open_url(&zed_urls::account_url(cx));
1123 },
1124 )
1125 .separator()
1126 })
1127 .when(show_update_button, |this| {
1128 this.custom_entry(
1129 move |_window, _cx| {
1130 h_flex()
1131 .w_full()
1132 .gap_1()
1133 .justify_between()
1134 .child(Label::new("Restart to update Zed").color(Color::Accent))
1135 .child(
1136 Icon::new(IconName::Download)
1137 .size(IconSize::Small)
1138 .color(Color::Accent),
1139 )
1140 .into_any_element()
1141 },
1142 move |_, cx| {
1143 workspace::reload(cx);
1144 },
1145 )
1146 .separator()
1147 })
1148 .when(has_organization, |this| {
1149 let mut this = this.header("Organization");
1150
1151 for (organization, plan) in &organizations {
1152 let organization = organization.clone();
1153 let plan = *plan;
1154
1155 let is_current =
1156 current_organization
1157 .as_ref()
1158 .is_some_and(|current_organization| {
1159 current_organization.id == organization.id
1160 });
1161
1162 this = this.custom_entry(
1163 {
1164 let organization = organization.clone();
1165 move |_window, _cx| {
1166 h_flex()
1167 .w_full()
1168 .gap_4()
1169 .justify_between()
1170 .child(
1171 h_flex()
1172 .gap_1()
1173 .child(Label::new(&organization.name))
1174 .when(is_current, |this| {
1175 this.child(
1176 Icon::new(IconName::Check)
1177 .color(Color::Accent),
1178 )
1179 }),
1180 )
1181 .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1182 .into_any_element()
1183 }
1184 },
1185 {
1186 let user_store = user_store.clone();
1187 let organization = organization.clone();
1188 move |_window, cx| {
1189 user_store.update(cx, |user_store, cx| {
1190 user_store
1191 .set_current_organization(organization.clone(), cx);
1192 });
1193 }
1194 },
1195 );
1196 }
1197
1198 this.separator()
1199 })
1200 .action("Settings", zed_actions::OpenSettings.boxed_clone())
1201 .action("Keymap", Box::new(zed_actions::OpenKeymap))
1202 .action(
1203 "Themes…",
1204 zed_actions::theme_selector::Toggle::default().boxed_clone(),
1205 )
1206 .action(
1207 "Icon Themes…",
1208 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1209 )
1210 .action(
1211 "Extensions",
1212 zed_actions::Extensions::default().boxed_clone(),
1213 )
1214 .when(is_signed_in, |this| {
1215 this.separator()
1216 .action("Sign Out", client::SignOut.boxed_clone())
1217 })
1218 })
1219 .into()
1220 })
1221 .anchor(Corner::TopRight)
1222 }
1223}