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 for repo in git_store.repositories().values() {
516 let repo_path = &repo.read(cx).work_directory_abs_path;
517 if worktree_path == *repo_path || worktree_path.starts_with(repo_path.as_ref()) {
518 return Some(repo.clone());
519 }
520 }
521
522 None
523 }
524
525 fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
526 let workspace = self.workspace.clone();
527
528 let options = self.project.read(cx).remote_connection_options(cx)?;
529 let host: SharedString = options.display_name().into();
530
531 let (nickname, tooltip_title, icon) = match options {
532 RemoteConnectionOptions::Ssh(options) => (
533 options.nickname.map(|nick| nick.into()),
534 "Remote Project",
535 IconName::Server,
536 ),
537 RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
538 RemoteConnectionOptions::Docker(_dev_container_connection) => {
539 (None, "Dev Container", IconName::Box)
540 }
541 #[cfg(any(test, feature = "test-support"))]
542 RemoteConnectionOptions::Mock(_) => (None, "Mock Remote Project", IconName::Server),
543 };
544
545 let nickname = nickname.unwrap_or_else(|| host.clone());
546
547 let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
548 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
549 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
550 remote::ConnectionState::HeartbeatMissed => (
551 Color::Warning,
552 format!("Connection attempt to {host} missed. Retrying..."),
553 ),
554 remote::ConnectionState::Reconnecting => (
555 Color::Warning,
556 format!("Lost connection to {host}. Reconnecting..."),
557 ),
558 remote::ConnectionState::Disconnected => {
559 (Color::Error, format!("Disconnected from {host}"))
560 }
561 };
562
563 let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
564 remote::ConnectionState::Connecting => Color::Info,
565 remote::ConnectionState::Connected => Color::Default,
566 remote::ConnectionState::HeartbeatMissed => Color::Warning,
567 remote::ConnectionState::Reconnecting => Color::Warning,
568 remote::ConnectionState::Disconnected => Color::Error,
569 };
570
571 let meta = SharedString::from(meta);
572
573 Some(
574 PopoverMenu::new("remote-project-menu")
575 .menu(move |window, cx| {
576 let workspace_entity = workspace.upgrade()?;
577 let fs = workspace_entity.read(cx).project().read(cx).fs().clone();
578 Some(recent_projects::RemoteServerProjects::popover(
579 fs,
580 workspace.clone(),
581 false,
582 window,
583 cx,
584 ))
585 })
586 .trigger_with_tooltip(
587 ButtonLike::new("remote_project")
588 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
589 .child(
590 h_flex()
591 .gap_2()
592 .max_w_32()
593 .child(
594 IconWithIndicator::new(
595 Icon::new(icon).size(IconSize::Small).color(icon_color),
596 Some(Indicator::dot().color(indicator_color)),
597 )
598 .indicator_border_color(Some(
599 cx.theme().colors().title_bar_background,
600 ))
601 .into_any_element(),
602 )
603 .child(Label::new(nickname).size(LabelSize::Small).truncate()),
604 ),
605 move |_window, cx| {
606 Tooltip::with_meta(
607 tooltip_title,
608 Some(&OpenRemote {
609 from_existing_connection: false,
610 create_new_window: false,
611 }),
612 meta.clone(),
613 cx,
614 )
615 },
616 )
617 .anchor(gpui::Corner::TopLeft)
618 .into_any_element(),
619 )
620 }
621
622 pub fn render_restricted_mode(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
623 let has_restricted_worktrees = TrustedWorktrees::try_get_global(cx)
624 .map(|trusted_worktrees| {
625 trusted_worktrees
626 .read(cx)
627 .has_restricted_worktrees(&self.project.read(cx).worktree_store(), cx)
628 })
629 .unwrap_or(false);
630 if !has_restricted_worktrees {
631 return None;
632 }
633
634 let button = Button::new("restricted_mode_trigger", "Restricted Mode")
635 .style(ButtonStyle::Tinted(TintColor::Warning))
636 .label_size(LabelSize::Small)
637 .color(Color::Warning)
638 .start_icon(
639 Icon::new(IconName::Warning)
640 .size(IconSize::Small)
641 .color(Color::Warning),
642 )
643 .tooltip(|_, cx| {
644 Tooltip::with_meta(
645 "You're in Restricted Mode",
646 Some(&ToggleWorktreeSecurity),
647 "Mark this project as trusted and unlock all features",
648 cx,
649 )
650 })
651 .on_click({
652 cx.listener(move |this, _, window, cx| {
653 this.workspace
654 .update(cx, |workspace, cx| {
655 workspace.show_worktree_trust_security_modal(true, window, cx)
656 })
657 .log_err();
658 })
659 });
660
661 if cfg!(macos_sdk_26) {
662 // Make up for Tahoe's traffic light buttons having less spacing around them
663 Some(div().child(button).ml_0p5().into_any_element())
664 } else {
665 Some(button.into_any_element())
666 }
667 }
668
669 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
670 if self.project.read(cx).is_via_remote_server() {
671 return self.render_remote_project_connection(cx);
672 }
673
674 if self.project.read(cx).is_disconnected(cx) {
675 return Some(
676 Button::new("disconnected", "Disconnected")
677 .disabled(true)
678 .color(Color::Disabled)
679 .label_size(LabelSize::Small)
680 .into_any_element(),
681 );
682 }
683
684 let host = self.project.read(cx).host()?;
685 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
686 let participant_index = self
687 .user_store
688 .read(cx)
689 .participant_indices()
690 .get(&host_user.id)?;
691
692 Some(
693 Button::new("project_owner_trigger", host_user.github_login.clone())
694 .color(Color::Player(participant_index.0))
695 .label_size(LabelSize::Small)
696 .tooltip(move |_, cx| {
697 let tooltip_title = format!(
698 "{} is sharing this project. Click to follow.",
699 host_user.github_login
700 );
701
702 Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
703 })
704 .on_click({
705 let host_peer_id = host.peer_id;
706 cx.listener(move |this, _, window, cx| {
707 this.workspace
708 .update(cx, |workspace, cx| {
709 workspace.follow(host_peer_id, window, cx);
710 })
711 .log_err();
712 })
713 })
714 .into_any_element(),
715 )
716 }
717
718 fn render_project_name(
719 &self,
720 name: Option<SharedString>,
721 _: &mut Window,
722 cx: &mut Context<Self>,
723 ) -> impl IntoElement {
724 let workspace = self.workspace.clone();
725
726 let is_project_selected = name.is_some();
727
728 let display_name = if let Some(ref name) = name {
729 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
730 } else {
731 "Open Recent Project".to_string()
732 };
733
734 let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
735
736 let is_threads_list_view_active = self
737 .multi_workspace
738 .as_ref()
739 .and_then(|mw| mw.upgrade())
740 .map(|mw| mw.read(cx).is_threads_list_view_active(cx))
741 .unwrap_or(false);
742
743 if is_sidebar_open && is_threads_list_view_active {
744 return self
745 .render_project_name_with_sidebar_popover(display_name, is_project_selected, cx)
746 .into_any_element();
747 }
748
749 let focus_handle = workspace
750 .upgrade()
751 .map(|w| w.read(cx).focus_handle(cx))
752 .unwrap_or_else(|| cx.focus_handle());
753
754 let sibling_workspace_ids: HashSet<WorkspaceId> = self
755 .multi_workspace
756 .as_ref()
757 .and_then(|mw| mw.upgrade())
758 .map(|mw| {
759 mw.read(cx)
760 .workspaces()
761 .iter()
762 .filter_map(|ws| ws.read(cx).database_id())
763 .collect()
764 })
765 .unwrap_or_default();
766
767 PopoverMenu::new("recent-projects-menu")
768 .menu(move |window, cx| {
769 Some(recent_projects::RecentProjects::popover(
770 workspace.clone(),
771 sibling_workspace_ids.clone(),
772 false,
773 focus_handle.clone(),
774 window,
775 cx,
776 ))
777 })
778 .trigger_with_tooltip(
779 Button::new("project_name_trigger", display_name)
780 .label_size(LabelSize::Small)
781 .when(self.worktree_count(cx) > 1, |this| {
782 this.end_icon(
783 Icon::new(IconName::ChevronDown)
784 .size(IconSize::XSmall)
785 .color(Color::Muted),
786 )
787 })
788 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
789 .when(!is_project_selected, |s| s.color(Color::Muted)),
790 move |_window, cx| {
791 Tooltip::for_action(
792 "Recent Projects",
793 &zed_actions::OpenRecent {
794 create_new_window: false,
795 },
796 cx,
797 )
798 },
799 )
800 .anchor(gpui::Corner::TopLeft)
801 .into_any_element()
802 }
803
804 /// When the sidebar is open, the title bar's project name button becomes a
805 /// plain button that toggles the sidebar's popover (so the popover is always
806 /// anchored to the sidebar). Both buttons show their selected state together.
807 fn render_project_name_with_sidebar_popover(
808 &self,
809 display_name: String,
810 is_project_selected: bool,
811 cx: &mut Context<Self>,
812 ) -> impl IntoElement {
813 let multi_workspace = self.multi_workspace.clone();
814
815 let is_popover_deployed = multi_workspace
816 .as_ref()
817 .and_then(|mw| mw.upgrade())
818 .map(|mw| mw.read(cx).is_recent_projects_popover_deployed(cx))
819 .unwrap_or(false);
820
821 Button::new("project_name_trigger", display_name)
822 .label_size(LabelSize::Small)
823 .when(self.worktree_count(cx) > 1, |this| {
824 this.end_icon(
825 Icon::new(IconName::ChevronDown)
826 .size(IconSize::XSmall)
827 .color(Color::Muted),
828 )
829 })
830 .toggle_state(is_popover_deployed)
831 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
832 .when(!is_project_selected, |s| s.color(Color::Muted))
833 .tooltip(move |_window, cx| {
834 Tooltip::for_action(
835 "Recent Projects",
836 &zed_actions::OpenRecent {
837 create_new_window: false,
838 },
839 cx,
840 )
841 })
842 .on_click(move |_, window, cx| {
843 if let Some(mw) = multi_workspace.as_ref().and_then(|mw| mw.upgrade()) {
844 mw.update(cx, |mw, cx| {
845 mw.toggle_recent_projects_popover(window, cx);
846 });
847 }
848 })
849 }
850
851 fn render_project_branch(
852 &self,
853 repository: Entity<project::git_store::Repository>,
854 linked_worktree_name: Option<SharedString>,
855 cx: &mut Context<Self>,
856 ) -> Option<impl IntoElement> {
857 let workspace = self.workspace.upgrade()?;
858
859 let (branch_name, icon_info) = {
860 let repo = repository.read(cx);
861
862 let branch_name = repo
863 .branch
864 .as_ref()
865 .map(|branch| branch.name())
866 .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
867 .or_else(|| {
868 repo.head_commit.as_ref().map(|commit| {
869 commit
870 .sha
871 .chars()
872 .take(MAX_SHORT_SHA_LENGTH)
873 .collect::<String>()
874 })
875 });
876
877 let status = repo.status_summary();
878 let tracked = status.index + status.worktree;
879 let icon_info = if status.conflict > 0 {
880 (IconName::Warning, Color::VersionControlConflict)
881 } else if tracked.modified > 0 {
882 (IconName::SquareDot, Color::VersionControlModified)
883 } else if tracked.added > 0 || status.untracked > 0 {
884 (IconName::SquarePlus, Color::VersionControlAdded)
885 } else if tracked.deleted > 0 {
886 (IconName::SquareMinus, Color::VersionControlDeleted)
887 } else {
888 (IconName::GitBranch, Color::Muted)
889 };
890
891 (branch_name, icon_info)
892 };
893
894 let branch_name = branch_name?;
895 let button_text = if let Some(worktree_name) = linked_worktree_name {
896 format!("{}/{}", worktree_name, branch_name)
897 } else {
898 branch_name
899 };
900
901 let settings = TitleBarSettings::get_global(cx);
902
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 Button::new("project_branch_trigger", button_text)
919 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
920 .label_size(LabelSize::Small)
921 .color(Color::Muted)
922 .when(settings.show_branch_icon, |branch_button| {
923 let (icon, icon_color) = icon_info;
924 branch_button.start_icon(
925 Icon::new(icon).size(IconSize::Indicator).color(icon_color),
926 )
927 }),
928 move |_window, cx| {
929 Tooltip::with_meta(
930 "Recent Branches",
931 Some(&zed_actions::git::Branch),
932 "Local branches only",
933 cx,
934 )
935 },
936 )
937 .anchor(gpui::Corner::TopLeft),
938 )
939 }
940
941 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
942 if window.is_window_active() {
943 ActiveCall::global(cx)
944 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
945 .detach_and_log_err(cx);
946 } else if cx.active_window().is_none() {
947 ActiveCall::global(cx)
948 .update(cx, |call, cx| call.set_location(None, cx))
949 .detach_and_log_err(cx);
950 }
951 self.workspace
952 .update(cx, |workspace, cx| {
953 workspace.update_active_view_for_followers(window, cx);
954 })
955 .ok();
956 }
957
958 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
959 self.observe_diagnostics(cx);
960 cx.notify();
961 }
962
963 fn observe_diagnostics(&mut self, cx: &mut Context<Self>) {
964 let diagnostics = ActiveCall::global(cx)
965 .read(cx)
966 .room()
967 .and_then(|room| room.read(cx).diagnostics().cloned());
968
969 if let Some(diagnostics) = diagnostics {
970 self._diagnostics_subscription = Some(cx.observe(&diagnostics, |_, _, cx| cx.notify()));
971 } else {
972 self._diagnostics_subscription = None;
973 }
974 }
975
976 fn share_project(&mut self, cx: &mut Context<Self>) {
977 let active_call = ActiveCall::global(cx);
978 let project = self.project.clone();
979 active_call
980 .update(cx, |call, cx| call.share_project(project, cx))
981 .detach_and_log_err(cx);
982 }
983
984 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
985 let active_call = ActiveCall::global(cx);
986 let project = self.project.clone();
987 active_call
988 .update(cx, |call, cx| call.unshare_project(project, cx))
989 .log_err();
990 }
991
992 fn render_connection_status(
993 &self,
994 status: &client::Status,
995 cx: &mut Context<Self>,
996 ) -> Option<AnyElement> {
997 match status {
998 client::Status::ConnectionError
999 | client::Status::ConnectionLost
1000 | client::Status::Reauthenticating
1001 | client::Status::Reconnecting
1002 | client::Status::ReconnectionError { .. } => Some(
1003 div()
1004 .id("disconnected")
1005 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
1006 .tooltip(Tooltip::text("Disconnected"))
1007 .into_any_element(),
1008 ),
1009 client::Status::UpgradeRequired => {
1010 let auto_updater = auto_update::AutoUpdater::get(cx);
1011 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
1012 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
1013 Some(AutoUpdateStatus::Installing { .. })
1014 | Some(AutoUpdateStatus::Downloading { .. })
1015 | Some(AutoUpdateStatus::Checking) => "Updating...",
1016 Some(AutoUpdateStatus::Idle)
1017 | Some(AutoUpdateStatus::Errored { .. })
1018 | None => "Please update Zed to Collaborate",
1019 };
1020
1021 Some(
1022 Button::new("connection-status", label)
1023 .label_size(LabelSize::Small)
1024 .on_click(|_, window, cx| {
1025 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
1026 && auto_updater.read(cx).status().is_updated()
1027 {
1028 workspace::reload(cx);
1029 return;
1030 }
1031 auto_update::check(&Default::default(), window, cx);
1032 })
1033 .into_any_element(),
1034 )
1035 }
1036 _ => None,
1037 }
1038 }
1039
1040 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
1041 let client = self.client.clone();
1042 let workspace = self.workspace.clone();
1043 Button::new("sign_in", "Sign In")
1044 .label_size(LabelSize::Small)
1045 .on_click(move |_, window, cx| {
1046 let client = client.clone();
1047 let workspace = workspace.clone();
1048 window
1049 .spawn(cx, async move |mut cx| {
1050 client
1051 .sign_in_with_optional_connect(true, cx)
1052 .await
1053 .notify_workspace_async_err(workspace, &mut cx);
1054 })
1055 .detach();
1056 })
1057 }
1058
1059 pub fn render_organization_menu_button(&mut self, cx: &mut Context<Self>) -> AnyElement {
1060 let Some(organization) = self.user_store.read(cx).current_organization() else {
1061 return Empty.into_any_element();
1062 };
1063
1064 PopoverMenu::new("organization-menu")
1065 .anchor(Corner::TopRight)
1066 .menu({
1067 let user_store = self.user_store.clone();
1068 move |window, cx| {
1069 ContextMenu::build(window, cx, |mut menu, _window, cx| {
1070 menu = menu.header("Organizations").separator();
1071
1072 let current_organization = user_store.read(cx).current_organization();
1073
1074 for organization in user_store.read(cx).organizations() {
1075 let organization = organization.clone();
1076 let plan = user_store.read(cx).plan_for_organization(&organization.id);
1077
1078 let is_current =
1079 current_organization
1080 .as_ref()
1081 .is_some_and(|current_organization| {
1082 current_organization.id == organization.id
1083 });
1084
1085 menu = menu.custom_entry(
1086 {
1087 let organization = organization.clone();
1088 move |_window, _cx| {
1089 h_flex()
1090 .w_full()
1091 .gap_1()
1092 .child(
1093 div()
1094 .flex_none()
1095 .when(!is_current, |parent| parent.invisible())
1096 .child(Icon::new(IconName::Check)),
1097 )
1098 .child(
1099 h_flex()
1100 .w_full()
1101 .gap_3()
1102 .justify_between()
1103 .child(Label::new(&organization.name))
1104 .child(PlanChip::new(
1105 plan.unwrap_or(Plan::ZedFree),
1106 )),
1107 )
1108 .into_any_element()
1109 }
1110 },
1111 {
1112 let user_store = user_store.clone();
1113 let organization = organization.clone();
1114 move |_window, cx| {
1115 user_store.update(cx, |user_store, cx| {
1116 user_store
1117 .set_current_organization(organization.clone(), cx);
1118 });
1119 }
1120 },
1121 );
1122 }
1123
1124 menu
1125 })
1126 .into()
1127 }
1128 })
1129 .trigger_with_tooltip(
1130 Button::new("organization-menu", &organization.name)
1131 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
1132 .label_size(LabelSize::Small),
1133 Tooltip::text("Toggle Organization Menu"),
1134 )
1135 .anchor(gpui::Corner::TopRight)
1136 .into_any_element()
1137 }
1138
1139 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
1140 let show_update_badge = self.update_version.read(cx).show_update_in_menu_bar();
1141
1142 let user_store = self.user_store.read(cx);
1143 let user = user_store.current_user();
1144
1145 let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
1146 let user_login = user.as_ref().map(|u| u.github_login.clone());
1147
1148 let is_signed_in = user.is_some();
1149
1150 let has_subscription_period = user_store.subscription_period().is_some();
1151 let plan = user_store.plan().filter(|_| {
1152 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
1153 has_subscription_period
1154 });
1155
1156 PopoverMenu::new("user-menu")
1157 .anchor(Corner::TopRight)
1158 .menu(move |window, cx| {
1159 ContextMenu::build(window, cx, |menu, _, _cx| {
1160 let user_login = user_login.clone();
1161
1162 menu.when(is_signed_in, |this| {
1163 this.custom_entry(
1164 move |_window, _cx| {
1165 let user_login = user_login.clone().unwrap_or_default();
1166
1167 h_flex()
1168 .w_full()
1169 .justify_between()
1170 .child(Label::new(user_login))
1171 .child(PlanChip::new(plan.unwrap_or(Plan::ZedFree)))
1172 .into_any_element()
1173 },
1174 move |_, cx| {
1175 cx.open_url(&zed_urls::account_url(cx));
1176 },
1177 )
1178 .separator()
1179 })
1180 .when(show_update_badge, |this| {
1181 this.custom_entry(
1182 move |_window, _cx| {
1183 h_flex()
1184 .w_full()
1185 .gap_1()
1186 .justify_between()
1187 .child(Label::new("Restart to update Zed").color(Color::Accent))
1188 .child(
1189 Icon::new(IconName::Download)
1190 .size(IconSize::Small)
1191 .color(Color::Accent),
1192 )
1193 .into_any_element()
1194 },
1195 move |_, cx| {
1196 workspace::reload(cx);
1197 },
1198 )
1199 .separator()
1200 })
1201 .action("Settings", zed_actions::OpenSettings.boxed_clone())
1202 .action("Keymap", Box::new(zed_actions::OpenKeymap))
1203 .action(
1204 "Themes…",
1205 zed_actions::theme_selector::Toggle::default().boxed_clone(),
1206 )
1207 .action(
1208 "Icon Themes…",
1209 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
1210 )
1211 .action(
1212 "Extensions",
1213 zed_actions::Extensions::default().boxed_clone(),
1214 )
1215 .when(is_signed_in, |this| {
1216 this.separator()
1217 .action("Sign Out", client::SignOut.boxed_clone())
1218 })
1219 })
1220 .into()
1221 })
1222 .map(|this| {
1223 if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
1224 let avatar =
1225 user_avatar
1226 .clone()
1227 .map(|avatar| Avatar::new(avatar))
1228 .map(|avatar| {
1229 if show_update_badge {
1230 avatar.indicator(
1231 div()
1232 .absolute()
1233 .bottom_0()
1234 .right_0()
1235 .child(Indicator::dot().color(Color::Accent)),
1236 )
1237 } else {
1238 avatar
1239 }
1240 });
1241 this.trigger_with_tooltip(
1242 ButtonLike::new("user-menu").children(avatar),
1243 Tooltip::text("Toggle User Menu"),
1244 )
1245 } else {
1246 this.trigger_with_tooltip(
1247 IconButton::new("user-menu", IconName::ChevronDown)
1248 .icon_size(IconSize::Small),
1249 Tooltip::text("Toggle User Menu"),
1250 )
1251 }
1252 })
1253 .anchor(gpui::Corner::TopRight)
1254 }
1255}