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