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