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