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