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