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