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_recent_projects_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 fn render_recent_projects_popover(
806 &self,
807 display_name: String,
808 is_project_selected: bool,
809 cx: &mut Context<Self>,
810 ) -> impl IntoElement {
811 let workspace = self.workspace.clone();
812
813 let focus_handle = workspace
814 .upgrade()
815 .map(|w| w.read(cx).focus_handle(cx))
816 .unwrap_or_else(|| cx.focus_handle());
817
818 let sibling_workspace_ids: HashSet<WorkspaceId> = self
819 .multi_workspace
820 .as_ref()
821 .and_then(|mw| mw.upgrade())
822 .map(|mw| {
823 mw.read(cx)
824 .workspaces()
825 .iter()
826 .filter_map(|ws| ws.read(cx).database_id())
827 .collect()
828 })
829 .unwrap_or_default();
830
831 PopoverMenu::new("sidebar-title-recent-projects-menu")
832 .menu(move |window, cx| {
833 Some(recent_projects::RecentProjects::popover(
834 workspace.clone(),
835 sibling_workspace_ids.clone(),
836 false,
837 focus_handle.clone(),
838 window,
839 cx,
840 ))
841 })
842 .trigger_with_tooltip(
843 Button::new("project_name_trigger", display_name)
844 .label_size(LabelSize::Small)
845 .when(self.worktree_count(cx) > 1, |this| {
846 this.end_icon(
847 Icon::new(IconName::ChevronDown)
848 .size(IconSize::XSmall)
849 .color(Color::Muted),
850 )
851 })
852 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
853 .when(!is_project_selected, |s| s.color(Color::Muted)),
854 move |_window, cx| {
855 Tooltip::for_action(
856 "Recent Projects",
857 &zed_actions::OpenRecent {
858 create_new_window: false,
859 },
860 cx,
861 )
862 },
863 )
864 .anchor(gpui::Corner::TopLeft)
865 }
866
867 fn render_project_branch(
868 &self,
869 repository: Entity<project::git_store::Repository>,
870 linked_worktree_name: Option<SharedString>,
871 cx: &mut Context<Self>,
872 ) -> Option<impl IntoElement> {
873 let workspace = self.workspace.upgrade()?;
874
875 let (branch_name, icon_info) = {
876 let repo = repository.read(cx);
877
878 let branch_name = repo
879 .branch
880 .as_ref()
881 .map(|branch| branch.name())
882 .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
883 .or_else(|| {
884 repo.head_commit.as_ref().map(|commit| {
885 commit
886 .sha
887 .chars()
888 .take(MAX_SHORT_SHA_LENGTH)
889 .collect::<String>()
890 })
891 });
892
893 let status = repo.status_summary();
894 let tracked = status.index + status.worktree;
895 let icon_info = if status.conflict > 0 {
896 (IconName::Warning, Color::VersionControlConflict)
897 } else if tracked.modified > 0 {
898 (IconName::SquareDot, Color::VersionControlModified)
899 } else if tracked.added > 0 || status.untracked > 0 {
900 (IconName::SquarePlus, Color::VersionControlAdded)
901 } else if tracked.deleted > 0 {
902 (IconName::SquareMinus, Color::VersionControlDeleted)
903 } else {
904 (IconName::GitBranch, Color::Muted)
905 };
906
907 (branch_name, icon_info)
908 };
909
910 let branch_name = branch_name?;
911 let settings = TitleBarSettings::get_global(cx);
912 let effective_repository = Some(repository);
913
914 Some(
915 PopoverMenu::new("branch-menu")
916 .menu(move |window, cx| {
917 Some(git_ui::git_picker::popover(
918 workspace.downgrade(),
919 effective_repository.clone(),
920 git_ui::git_picker::GitPickerTab::Branches,
921 gpui::rems(34.),
922 window,
923 cx,
924 ))
925 })
926 .trigger_with_tooltip(
927 ButtonLike::new("project_branch_trigger")
928 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
929 .child(
930 h_flex()
931 .gap_0p5()
932 .when(settings.show_branch_icon, |this| {
933 let (icon, icon_color) = icon_info;
934 this.child(
935 Icon::new(icon).size(IconSize::XSmall).color(icon_color),
936 )
937 })
938 .when_some(linked_worktree_name.as_ref(), |this, worktree_name| {
939 this.child(
940 Label::new(worktree_name)
941 .size(LabelSize::Small)
942 .color(Color::Muted),
943 )
944 .child(
945 Label::new("/").size(LabelSize::Small).color(
946 Color::Custom(
947 cx.theme().colors().text_muted.opacity(0.4),
948 ),
949 ),
950 )
951 })
952 .child(
953 Label::new(branch_name)
954 .size(LabelSize::Small)
955 .color(Color::Muted),
956 ),
957 ),
958 move |_window, cx| {
959 Tooltip::with_meta(
960 "Git Switcher",
961 Some(&zed_actions::git::Branch),
962 "Worktrees, Branches, and Stashes",
963 cx,
964 )
965 },
966 )
967 .anchor(gpui::Corner::TopLeft),
968 )
969 }
970
971 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
972 if window.is_window_active() {
973 ActiveCall::global(cx)
974 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
975 .detach_and_log_err(cx);
976 } else if cx.active_window().is_none() {
977 ActiveCall::global(cx)
978 .update(cx, |call, cx| call.set_location(None, cx))
979 .detach_and_log_err(cx);
980 }
981 self.workspace
982 .update(cx, |workspace, cx| {
983 workspace.update_active_view_for_followers(window, cx);
984 })
985 .ok();
986 }
987
988 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
989 self.observe_diagnostics(cx);
990 cx.notify();
991 }
992
993 fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
994 let diagnostics = ActiveCall::global(cx)
995 .read(cx)
996 .room()
997 .and_then(|room| room.read(cx).diagnostics().cloned());
998
999 if let Some(diagnostics) = diagnostics {
1000 self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
1001 } else {
1002 self._diagnostics_subscription = None;
1003 }
1004 }
1005
1006 fn share_project(&mut self, cx: &mut Context<Self>) {
1007 let active_call = ActiveCall::global(cx);
1008 let project = self.project.clone();
1009 active_call
1010 .update(cx, |call, cx| call.share_project(project, cx))
1011 .detach_and_log_err(cx);
1012 }
1013
1014 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1015 let active_call = ActiveCall::global(cx);
1016 let project = self.project.clone();
1017 active_call
1018 .update(cx, |call, cx| call.unshare_project(project, cx))
1019 .log_err();
1020 }
1021
1022 fn render_connection_status(
1023 &self,
1024 status: &client::Status,
1025 cx: &mut Context<Self>,
1026 ) -> Option<AnyElement> {
1027 match status {
1028 client::Status::ConnectionError
1029 | client::Status::ConnectionLost
1030 | client::Status::Reauthenticating
1031 | client::Status::Reconnecting
1032 | client::Status::ReconnectionError { .. } => Some(
1033 div()
1034 .id("disconnected")
1035 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
1036 .tooltip(Tooltip::text("Disconnected"))
1037 .into_any_element(),
1038 ),
1039 client::Status::UpgradeRequired => {
1040 let auto_updater = auto_update::AutoUpdater::get(cx);
1041 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
1042 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
1043 Some(AutoUpdateStatus::Installing { .. })
1044 | Some(AutoUpdateStatus::Downloading { .. })
1045 | Some(AutoUpdateStatus::Checking) => "Updating...",
1046 Some(AutoUpdateStatus::Idle)
1047 | Some(AutoUpdateStatus::Errored { .. })
1048 | None => "Please update Zed to Collaborate",
1049 };
1050
1051 Some(
1052 Button::new("connection-status", label)
1053 .label_size(LabelSize::Small)
1054 .on_click(|_, window, cx| {
1055 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1056 && auto_updater.read(cx).status().is_updated()
1057 {
1058 workspace::reload(cx);
1059 return;
1060 }
1061 auto_update::check(&Default::default(), window, cx);
1062 })
1063 .into_any_element(),
1064 )
1065 }
1066 _ => None,
1067 }
1068 }
1069
1070 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1071 let client = self.client.clone();
1072 let workspace = self.workspace.clone();
1073 Button::new("sign_in", "Sign In")
1074 .label_size(LabelSize::Small)
1075 .on_click(move |_, window, cx| {
1076 let client = client.clone();
1077 let workspace = workspace.clone();
1078 window
1079 .spawn(cx, async move |mut cx| {
1080 client
1081 .sign_in_with_optional_connect(true, cx)
1082 .await
1083 .notify_workspace_async_err(workspace, &mut cx);
1084 })
1085 .detach();
1086 })
1087 }
1088
1089 pub fn render_organization_menu_button(&mut self, cx: &mut Context<Self>) -> AnyElement {
1090 let Some(organization) = self.user_store.read(cx).current_organization() else {
1091 return Empty.into_any_element();
1092 };
1093
1094 PopoverMenu::new("organization-menu")
1095 .anchor(Corner::TopRight)
1096 .menu({
1097 let user_store = self.user_store.clone();
1098 move |window, cx| {
1099 ContextMenu::build(window, cx, |mut menu, _window, cx| {
1100 menu = menu.header("Organizations").separator();
1101
1102 let current_organization = user_store.read(cx).current_organization();
1103
1104 for organization in user_store.read(cx).organizations() {
1105 let organization = organization.clone();
1106 let plan = user_store.read(cx).plan_for_organization(&organization.id);
1107
1108 let is_current =
1109 current_organization
1110 .as_ref()
1111 .is_some_and(|current_organization| {
1112 current_organization.id == organization.id
1113 });
1114
1115 menu = menu.custom_entry(
1116 {
1117 let organization = organization.clone();
1118 move |_window, _cx| {
1119 h_flex()
1120 .w_full()
1121 .gap_1()
1122 .child(
1123 div()
1124 .flex_none()
1125 .when(!is_current, |parent| parent.invisible())
1126 .child(Icon::new(IconName::Check)),
1127 )
1128 .child(
1129 h_flex()
1130 .w_full()
1131 .gap_3()
1132 .justify_between()
1133 .child(Label::new(&organization.name))
1134 .child(PlanChip::new(
1135 plan.unwrap_or(Plan::ZedFree),
1136 )),
1137 )
1138 .into_any_element()
1139 }
1140 },
1141 {
1142 let user_store = user_store.clone();
1143 let organization = organization.clone();
1144 move |_window, cx| {
1145 user_store.update(cx, |user_store, cx| {
1146 user_store
1147 .set_current_organization(organization.clone(), cx);
1148 });
1149 }
1150 },
1151 );
1152 }
1153
1154 menu
1155 })
1156 .into()
1157 }
1158 })
1159 .trigger_with_tooltip(
1160 Button::new("organization-menu", &organization.name)
1161 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1162 .label_size(LabelSize::Small),
1163 Tooltip::text("Toggle Organization Menu"),
1164 )
1165 .anchor(gpui::Corner::TopRight)
1166 .into_any_element()
1167 }
1168
1169 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1170 let show_update_badge = self.update_version.read(cx).show_update_in_menu_bar();
1171
1172 let user_store = self.user_store.read(cx);
1173 let user = user_store.current_user();
1174
1175 let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1176 let user_login = user.as_ref().map(|u| u.github_login.clone());
1177
1178 let is_signed_in = user.is_some();
1179
1180 let has_subscription_period = user_store.subscription_period().is_some();
1181 let plan = user_store.plan().filter(|_| {
1182 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
1183 has_subscription_period
1184 });
1185
1186 PopoverMenu::new("user-menu")
1187 .anchor(Corner::TopRight)
1188 .menu(move |window, cx| {
1189 ContextMenu::build(window, cx, |menu, _, _cx| {
1190 let user_login = user_login.clone();
1191
1192 menu.when(is_signed_in, |this| {
1193 this.custom_entry(
1194 move |_window, _cx| {
1195 let user_login = user_login.clone().unwrap_or_default();
1196
1197 h_flex()
1198 .w_full()
1199 .justify_between()
1200 .child(Label::new(user_login))
1201 .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1202 .into_any_element()
1203 },
1204 move |_, cx| {
1205 cx.open_url(&zed_urls::account_url(cx));
1206 },
1207 )
1208 .separator()
1209 })
1210 .when(show_update_badge, |this| {
1211 this.custom_entry(
1212 move |_window, _cx| {
1213 h_flex()
1214 .w_full()
1215 .gap_1()
1216 .justify_between()
1217 .child(Label::new("Restart to update Zed").color(Color::Accent))
1218 .child(
1219 Icon::new(IconName::Download)
1220 .size(IconSize::Small)
1221 .color(Color::Accent),
1222 )
1223 .into_any_element()
1224 },
1225 move |_, cx| {
1226 workspace::reload(cx);
1227 },
1228 )
1229 .separator()
1230 })
1231 .action("Settings", zed_actions::OpenSettings.boxed_clone())
1232 .action("Keymap", Box::new(zed_actions::OpenKeymap))
1233 .action(
1234 "Themes…",
1235 zed_actions::theme_selector::Toggle::default().boxed_clone(),
1236 )
1237 .action(
1238 "Icon Themes…",
1239 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1240 )
1241 .action(
1242 "Extensions",
1243 zed_actions::Extensions::default().boxed_clone(),
1244 )
1245 .when(is_signed_in, |this| {
1246 this.separator()
1247 .action("Sign Out", client::SignOut.boxed_clone())
1248 })
1249 })
1250 .into()
1251 })
1252 .map(|this| {
1253 if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
1254 let avatar =
1255 user_avatar
1256 .clone()
1257 .map(|avatar| Avatar::new(avatar))
1258 .map(|avatar| {
1259 if show_update_badge {
1260 avatar.indicator(
1261 div()
1262 .absolute()
1263 .bottom_0()
1264 .right_0()
1265 .child(Indicator::dot().color(Color::Accent)),
1266 )
1267 } else {
1268 avatar
1269 }
1270 });
1271 this.trigger_with_tooltip(
1272 ButtonLike::new("user-menu").children(avatar),
1273 Tooltip::text("Toggle User Menu"),
1274 )
1275 } else {
1276 this.trigger_with_tooltip(
1277 IconButton::new("user-menu", IconName::ChevronDown)
1278 .icon_size(IconSize::Small),
1279 Tooltip::text("Toggle User Menu"),
1280 )
1281 }
1282 })
1283 .anchor(gpui::Corner::TopRight)
1284 }
1285}