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