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