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