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