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