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