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