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