1mod application_menu;
2pub mod collab;
3mod onboarding_banner;
4pub mod platform_title_bar;
5mod platforms;
6mod system_window_tabs;
7mod title_bar_settings;
8
9#[cfg(feature = "stories")]
10mod stories;
11
12use crate::{
13 application_menu::{ApplicationMenu, show_menus},
14 platform_title_bar::PlatformTitleBar,
15 system_window_tabs::SystemWindowTabs,
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_llm_client::{Plan, PlanV1, PlanV2};
27use gpui::{
28 Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
29 IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
30 Subscription, WeakEntity, Window, actions, div,
31};
32use onboarding_banner::OnboardingBanner;
33use project::{Project, WorktreeSettings, git_store::GitStoreEvent};
34use remote::RemoteConnectionOptions;
35use settings::{Settings, SettingsLocation};
36use std::sync::Arc;
37use theme::ActiveTheme;
38use title_bar_settings::TitleBarSettings;
39use ui::{
40 Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize,
41 IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*,
42};
43use util::{ResultExt, rel_path::RelPath};
44use workspace::{Workspace, notifications::NotifyResultExt};
45use zed_actions::{OpenRecent, OpenRemote};
46
47pub use onboarding_banner::restore_banner;
48
49#[cfg(feature = "stories")]
50pub use stories::*;
51
52const MAX_PROJECT_NAME_LENGTH: usize = 40;
53const MAX_BRANCH_NAME_LENGTH: usize = 40;
54const MAX_SHORT_SHA_LENGTH: usize = 8;
55
56actions!(
57 collab,
58 [
59 /// Toggles the user menu dropdown.
60 ToggleUserMenu,
61 /// Toggles the project menu dropdown.
62 ToggleProjectMenu,
63 /// Switches to a different git branch.
64 SwitchBranch
65 ]
66);
67
68pub fn init(cx: &mut App) {
69 SystemWindowTabs::init(cx);
70
71 cx.observe_new(|workspace: &mut Workspace, window, cx| {
72 let Some(window) = window else {
73 return;
74 };
75 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
76 workspace.set_titlebar_item(item.into(), window, cx);
77
78 #[cfg(not(target_os = "macos"))]
79 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
80 if let Some(titlebar) = workspace
81 .titlebar_item()
82 .and_then(|item| item.downcast::<TitleBar>().ok())
83 {
84 titlebar.update(cx, |titlebar, cx| {
85 if let Some(ref menu) = titlebar.application_menu {
86 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
87 }
88 });
89 }
90 });
91
92 #[cfg(not(target_os = "macos"))]
93 workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| {
94 if let Some(titlebar) = workspace
95 .titlebar_item()
96 .and_then(|item| item.downcast::<TitleBar>().ok())
97 {
98 titlebar.update(cx, |titlebar, cx| {
99 if let Some(ref menu) = titlebar.application_menu {
100 menu.update(cx, |menu, cx| {
101 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
102 });
103 }
104 });
105 }
106 });
107
108 #[cfg(not(target_os = "macos"))]
109 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
110 if let Some(titlebar) = workspace
111 .titlebar_item()
112 .and_then(|item| item.downcast::<TitleBar>().ok())
113 {
114 titlebar.update(cx, |titlebar, cx| {
115 if let Some(ref menu) = titlebar.application_menu {
116 menu.update(cx, |menu, cx| {
117 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
118 });
119 }
120 });
121 }
122 });
123 })
124 .detach();
125}
126
127pub struct TitleBar {
128 platform_titlebar: Entity<PlatformTitleBar>,
129 project: Entity<Project>,
130 user_store: Entity<UserStore>,
131 client: Arc<Client>,
132 workspace: WeakEntity<Workspace>,
133 application_menu: Option<Entity<ApplicationMenu>>,
134 _subscriptions: Vec<Subscription>,
135 banner: Entity<OnboardingBanner>,
136 screen_share_popover_handle: PopoverMenuHandle<ContextMenu>,
137}
138
139impl Render for TitleBar {
140 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
141 let title_bar_settings = *TitleBarSettings::get_global(cx);
142
143 let show_menus = show_menus(cx);
144
145 let mut children = Vec::new();
146
147 children.push(
148 h_flex()
149 .gap_1()
150 .map(|title_bar| {
151 let mut render_project_items = title_bar_settings.show_branch_name
152 || title_bar_settings.show_project_items;
153 title_bar
154 .when_some(
155 self.application_menu.clone().filter(|_| !show_menus),
156 |title_bar, menu| {
157 render_project_items &=
158 !menu.update(cx, |menu, cx| menu.all_menus_shown(cx));
159 title_bar.child(menu)
160 },
161 )
162 .when(render_project_items, |title_bar| {
163 title_bar
164 .when(title_bar_settings.show_project_items, |title_bar| {
165 title_bar
166 .children(self.render_project_host(cx))
167 .child(self.render_project_name(cx))
168 })
169 .when(title_bar_settings.show_branch_name, |title_bar| {
170 title_bar.children(self.render_project_repo(cx))
171 })
172 })
173 })
174 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
175 .into_any_element(),
176 );
177
178 children.push(self.render_collaborator_list(window, cx).into_any_element());
179
180 if title_bar_settings.show_onboarding_banner {
181 children.push(self.banner.clone().into_any_element())
182 }
183
184 let status = self.client.status();
185 let status = &*status.borrow();
186 let user = self.user_store.read(cx).current_user();
187
188 let signed_in = user.is_some();
189
190 children.push(
191 h_flex()
192 .map(|this| {
193 if signed_in {
194 this.pr_1p5()
195 } else {
196 this.pr_1()
197 }
198 })
199 .gap_1()
200 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
201 .children(self.render_call_controls(window, cx))
202 .children(self.render_connection_status(status, cx))
203 .when(
204 user.is_none() && TitleBarSettings::get_global(cx).show_sign_in,
205 |el| el.child(self.render_sign_in_button(cx)),
206 )
207 .child(self.render_app_menu_button(cx))
208 .into_any_element(),
209 );
210
211 if show_menus {
212 self.platform_titlebar.update(cx, |this, _| {
213 this.set_children(
214 self.application_menu
215 .clone()
216 .map(|menu| menu.into_any_element()),
217 );
218 });
219
220 let height = PlatformTitleBar::height(window);
221 let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
222 platform_titlebar.title_bar_color(window, cx)
223 });
224
225 v_flex()
226 .w_full()
227 .child(self.platform_titlebar.clone().into_any_element())
228 .child(
229 h_flex()
230 .bg(title_bar_color)
231 .h(height)
232 .pl_2()
233 .justify_between()
234 .w_full()
235 .children(children),
236 )
237 .into_any_element()
238 } else {
239 self.platform_titlebar.update(cx, |this, _| {
240 this.set_children(children);
241 });
242 self.platform_titlebar.clone().into_any_element()
243 }
244 }
245}
246
247impl TitleBar {
248 pub fn new(
249 id: impl Into<ElementId>,
250 workspace: &Workspace,
251 window: &mut Window,
252 cx: &mut Context<Self>,
253 ) -> Self {
254 let project = workspace.project().clone();
255 let git_store = project.read(cx).git_store().clone();
256 let user_store = workspace.app_state().user_store.clone();
257 let client = workspace.app_state().client.clone();
258 let active_call = ActiveCall::global(cx);
259
260 let platform_style = PlatformStyle::platform();
261 let application_menu = match platform_style {
262 PlatformStyle::Mac => {
263 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
264 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
265 } else {
266 None
267 }
268 }
269 PlatformStyle::Linux | PlatformStyle::Windows => {
270 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
271 }
272 };
273
274 let mut subscriptions = Vec::new();
275 subscriptions.push(
276 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
277 cx.notify()
278 }),
279 );
280 subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
281 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
282 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
283 subscriptions.push(
284 cx.subscribe(&git_store, move |_, _, event, cx| match event {
285 GitStoreEvent::ActiveRepositoryChanged(_)
286 | GitStoreEvent::RepositoryUpdated(_, _, true) => {
287 cx.notify();
288 }
289 _ => {}
290 }),
291 );
292 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
293
294 let banner = cx.new(|cx| {
295 OnboardingBanner::new(
296 "ACP Claude Code Onboarding",
297 IconName::AiClaude,
298 "Claude Code",
299 Some("Introducing:".into()),
300 zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(),
301 cx,
302 )
303 // When updating this to a non-AI feature release, remove this line.
304 .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai)
305 });
306
307 let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
308
309 Self {
310 platform_titlebar,
311 application_menu,
312 workspace: workspace.weak_handle(),
313 project,
314 user_store,
315 client,
316 _subscriptions: subscriptions,
317 banner,
318 screen_share_popover_handle: Default::default(),
319 }
320 }
321
322 fn project_name(&self, cx: &Context<Self>) -> Option<SharedString> {
323 self.project
324 .read(cx)
325 .visible_worktrees(cx)
326 .map(|worktree| {
327 let worktree = worktree.read(cx);
328 let settings_location = SettingsLocation {
329 worktree_id: worktree.id(),
330 path: RelPath::empty(),
331 };
332
333 let settings = WorktreeSettings::get(Some(settings_location), cx);
334 let name = match &settings.project_name {
335 Some(name) => name.as_str(),
336 None => worktree.root_name_str(),
337 };
338 SharedString::new(name)
339 })
340 .next()
341 }
342
343 fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
344 let options = self.project.read(cx).remote_connection_options(cx)?;
345 let host: SharedString = options.display_name().into();
346
347 let (nickname, tooltip_title, icon) = match options {
348 RemoteConnectionOptions::Ssh(options) => (
349 options.nickname.map(|nick| nick.into()),
350 "Remote Project",
351 IconName::Server,
352 ),
353 RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
354 RemoteConnectionOptions::Docker(_dev_container_connection) => {
355 (None, "Dev Container", IconName::Box)
356 }
357 };
358
359 let nickname = nickname.unwrap_or_else(|| host.clone());
360
361 let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
362 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
363 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
364 remote::ConnectionState::HeartbeatMissed => (
365 Color::Warning,
366 format!("Connection attempt to {host} missed. Retrying..."),
367 ),
368 remote::ConnectionState::Reconnecting => (
369 Color::Warning,
370 format!("Lost connection to {host}. Reconnecting..."),
371 ),
372 remote::ConnectionState::Disconnected => {
373 (Color::Error, format!("Disconnected from {host}"))
374 }
375 };
376
377 let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
378 remote::ConnectionState::Connecting => Color::Info,
379 remote::ConnectionState::Connected => Color::Default,
380 remote::ConnectionState::HeartbeatMissed => Color::Warning,
381 remote::ConnectionState::Reconnecting => Color::Warning,
382 remote::ConnectionState::Disconnected => Color::Error,
383 };
384
385 let meta = SharedString::from(meta);
386
387 Some(
388 ButtonLike::new("ssh-server-icon")
389 .child(
390 h_flex()
391 .gap_2()
392 .max_w_32()
393 .child(
394 IconWithIndicator::new(
395 Icon::new(icon).size(IconSize::Small).color(icon_color),
396 Some(Indicator::dot().color(indicator_color)),
397 )
398 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
399 .into_any_element(),
400 )
401 .child(Label::new(nickname).size(LabelSize::Small).truncate()),
402 )
403 .tooltip(move |_window, cx| {
404 Tooltip::with_meta(
405 tooltip_title,
406 Some(&OpenRemote {
407 from_existing_connection: false,
408 create_new_window: false,
409 }),
410 meta.clone(),
411 cx,
412 )
413 })
414 .on_click(|_, window, cx| {
415 window.dispatch_action(
416 OpenRemote {
417 from_existing_connection: false,
418 create_new_window: false,
419 }
420 .boxed_clone(),
421 cx,
422 );
423 })
424 .into_any_element(),
425 )
426 }
427
428 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
429 if self.project.read(cx).is_via_remote_server() {
430 return self.render_remote_project_connection(cx);
431 }
432
433 if self.project.read(cx).is_disconnected(cx) {
434 return Some(
435 Button::new("disconnected", "Disconnected")
436 .disabled(true)
437 .color(Color::Disabled)
438 .style(ButtonStyle::Subtle)
439 .label_size(LabelSize::Small)
440 .into_any_element(),
441 );
442 }
443
444 let host = self.project.read(cx).host()?;
445 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
446 let participant_index = self
447 .user_store
448 .read(cx)
449 .participant_indices()
450 .get(&host_user.id)?;
451 Some(
452 Button::new("project_owner_trigger", host_user.github_login.clone())
453 .color(Color::Player(participant_index.0))
454 .style(ButtonStyle::Subtle)
455 .label_size(LabelSize::Small)
456 .tooltip(Tooltip::text(format!(
457 "{} is sharing this project. Click to follow.",
458 host_user.github_login
459 )))
460 .on_click({
461 let host_peer_id = host.peer_id;
462 cx.listener(move |this, _, window, cx| {
463 this.workspace
464 .update(cx, |workspace, cx| {
465 workspace.follow(host_peer_id, window, cx);
466 })
467 .log_err();
468 })
469 })
470 .into_any_element(),
471 )
472 }
473
474 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
475 let name = self.project_name(cx);
476 let is_project_selected = name.is_some();
477 let name = if let Some(name) = name {
478 util::truncate_and_trailoff(&name, MAX_PROJECT_NAME_LENGTH)
479 } else {
480 "Open recent project".to_string()
481 };
482
483 Button::new("project_name_trigger", name)
484 .when(!is_project_selected, |b| b.color(Color::Muted))
485 .style(ButtonStyle::Subtle)
486 .label_size(LabelSize::Small)
487 .tooltip(move |_window, cx| {
488 Tooltip::for_action(
489 "Recent Projects",
490 &zed_actions::OpenRecent {
491 create_new_window: false,
492 },
493 cx,
494 )
495 })
496 .on_click(cx.listener(move |_, _, window, cx| {
497 window.dispatch_action(
498 OpenRecent {
499 create_new_window: false,
500 }
501 .boxed_clone(),
502 cx,
503 );
504 }))
505 }
506
507 pub fn render_project_repo(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
508 let settings = TitleBarSettings::get_global(cx);
509 let repository = self.project.read(cx).active_repository(cx)?;
510 let repository_count = self.project.read(cx).repositories(cx).len();
511 let workspace = self.workspace.upgrade()?;
512 let repo = repository.read(cx);
513 let branch_name = repo
514 .branch
515 .as_ref()
516 .map(|branch| branch.name())
517 .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
518 .or_else(|| {
519 repo.head_commit.as_ref().map(|commit| {
520 commit
521 .sha
522 .chars()
523 .take(MAX_SHORT_SHA_LENGTH)
524 .collect::<String>()
525 })
526 })?;
527 let project_name = self.project_name(cx);
528 let repo_name = repo
529 .work_directory_abs_path
530 .file_name()
531 .and_then(|name| name.to_str())
532 .map(SharedString::new);
533 let show_repo_name =
534 repository_count > 1 && repo.branch.is_some() && repo_name != project_name;
535 let branch_name = if let Some(repo_name) = repo_name.filter(|_| show_repo_name) {
536 format!("{repo_name}/{branch_name}")
537 } else {
538 branch_name
539 };
540
541 Some(
542 Button::new("project_branch_trigger", branch_name)
543 .color(Color::Muted)
544 .style(ButtonStyle::Subtle)
545 .label_size(LabelSize::Small)
546 .tooltip(move |_window, cx| {
547 Tooltip::with_meta(
548 "Recent Branches",
549 Some(&zed_actions::git::Branch),
550 "Local branches only",
551 cx,
552 )
553 })
554 .on_click(move |_, window, cx| {
555 let _ = workspace.update(cx, |this, cx| {
556 window.focus(&this.active_pane().focus_handle(cx));
557 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
558 });
559 })
560 .when(settings.show_branch_icon, |branch_button| {
561 let (icon, icon_color) = {
562 let status = repo.status_summary();
563 let tracked = status.index + status.worktree;
564 if status.conflict > 0 {
565 (IconName::Warning, Color::VersionControlConflict)
566 } else if tracked.modified > 0 {
567 (IconName::SquareDot, Color::VersionControlModified)
568 } else if tracked.added > 0 || status.untracked > 0 {
569 (IconName::SquarePlus, Color::VersionControlAdded)
570 } else if tracked.deleted > 0 {
571 (IconName::SquareMinus, Color::VersionControlDeleted)
572 } else {
573 (IconName::GitBranch, Color::Muted)
574 }
575 };
576
577 branch_button
578 .icon(icon)
579 .icon_position(IconPosition::Start)
580 .icon_color(icon_color)
581 .icon_size(IconSize::Indicator)
582 }),
583 )
584 }
585
586 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
587 if window.is_window_active() {
588 ActiveCall::global(cx)
589 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
590 .detach_and_log_err(cx);
591 } else if cx.active_window().is_none() {
592 ActiveCall::global(cx)
593 .update(cx, |call, cx| call.set_location(None, cx))
594 .detach_and_log_err(cx);
595 }
596 self.workspace
597 .update(cx, |workspace, cx| {
598 workspace.update_active_view_for_followers(window, cx);
599 })
600 .ok();
601 }
602
603 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
604 cx.notify();
605 }
606
607 fn share_project(&mut self, cx: &mut Context<Self>) {
608 let active_call = ActiveCall::global(cx);
609 let project = self.project.clone();
610 active_call
611 .update(cx, |call, cx| call.share_project(project, cx))
612 .detach_and_log_err(cx);
613 }
614
615 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
616 let active_call = ActiveCall::global(cx);
617 let project = self.project.clone();
618 active_call
619 .update(cx, |call, cx| call.unshare_project(project, cx))
620 .log_err();
621 }
622
623 fn render_connection_status(
624 &self,
625 status: &client::Status,
626 cx: &mut Context<Self>,
627 ) -> Option<AnyElement> {
628 match status {
629 client::Status::ConnectionError
630 | client::Status::ConnectionLost
631 | client::Status::Reauthenticating
632 | client::Status::Reconnecting
633 | client::Status::ReconnectionError { .. } => Some(
634 div()
635 .id("disconnected")
636 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
637 .tooltip(Tooltip::text("Disconnected"))
638 .into_any_element(),
639 ),
640 client::Status::UpgradeRequired => {
641 let auto_updater = auto_update::AutoUpdater::get(cx);
642 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
643 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
644 Some(AutoUpdateStatus::Installing { .. })
645 | Some(AutoUpdateStatus::Downloading { .. })
646 | Some(AutoUpdateStatus::Checking) => "Updating...",
647 Some(AutoUpdateStatus::Idle)
648 | Some(AutoUpdateStatus::Errored { .. })
649 | None => "Please update Zed to Collaborate",
650 };
651
652 Some(
653 Button::new("connection-status", label)
654 .label_size(LabelSize::Small)
655 .on_click(|_, window, cx| {
656 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
657 && auto_updater.read(cx).status().is_updated()
658 {
659 workspace::reload(cx);
660 return;
661 }
662 auto_update::check(&Default::default(), window, cx);
663 })
664 .into_any_element(),
665 )
666 }
667 _ => None,
668 }
669 }
670
671 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
672 let client = self.client.clone();
673 Button::new("sign_in", "Sign in")
674 .label_size(LabelSize::Small)
675 .on_click(move |_, window, cx| {
676 let client = client.clone();
677 window
678 .spawn(cx, async move |cx| {
679 client
680 .sign_in_with_optional_connect(true, cx)
681 .await
682 .notify_async_err(cx);
683 })
684 .detach();
685 })
686 }
687
688 pub fn render_app_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
689 let user_store = self.user_store.read(cx);
690 let user = user_store.current_user();
691
692 let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
693 let user_login = user.as_ref().map(|u| u.github_login.clone());
694
695 let is_signed_in = user.is_some();
696
697 let has_subscription_period = user_store.subscription_period().is_some();
698 let plan = user_store.plan().filter(|_| {
699 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
700 has_subscription_period
701 });
702
703 let free_chip_bg = cx
704 .theme()
705 .colors()
706 .editor_background
707 .opacity(0.5)
708 .blend(cx.theme().colors().text_accent.opacity(0.05));
709
710 let pro_chip_bg = cx
711 .theme()
712 .colors()
713 .editor_background
714 .opacity(0.5)
715 .blend(cx.theme().colors().text_accent.opacity(0.2));
716
717 PopoverMenu::new("user-menu")
718 .anchor(Corner::TopRight)
719 .menu(move |window, cx| {
720 ContextMenu::build(window, cx, |menu, _, _cx| {
721 let user_login = user_login.clone();
722
723 let (plan_name, label_color, bg_color) = match plan {
724 None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => {
725 ("Free", Color::Default, free_chip_bg)
726 }
727 Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)) => {
728 ("Pro Trial", Color::Accent, pro_chip_bg)
729 }
730 Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)) => {
731 ("Pro", Color::Accent, pro_chip_bg)
732 }
733 };
734
735 menu.when(is_signed_in, |this| {
736 this.custom_entry(
737 move |_window, _cx| {
738 let user_login = user_login.clone().unwrap_or_default();
739
740 h_flex()
741 .w_full()
742 .justify_between()
743 .child(Label::new(user_login))
744 .child(
745 Chip::new(plan_name.to_string())
746 .bg_color(bg_color)
747 .label_color(label_color),
748 )
749 .into_any_element()
750 },
751 move |_, cx| {
752 cx.open_url(&zed_urls::account_url(cx));
753 },
754 )
755 .separator()
756 })
757 .action("Settings", zed_actions::OpenSettings.boxed_clone())
758 .action("Keymap", Box::new(zed_actions::OpenKeymap))
759 .action(
760 "Themes…",
761 zed_actions::theme_selector::Toggle::default().boxed_clone(),
762 )
763 .action(
764 "Icon Themes…",
765 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
766 )
767 .action(
768 "Extensions",
769 zed_actions::Extensions::default().boxed_clone(),
770 )
771 .when(is_signed_in, |this| {
772 this.separator()
773 .action("Sign Out", client::SignOut.boxed_clone())
774 })
775 })
776 .into()
777 })
778 .map(|this| {
779 if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
780 this.trigger_with_tooltip(
781 ButtonLike::new("user-menu")
782 .children(user_avatar.clone().map(|avatar| Avatar::new(avatar))),
783 Tooltip::text("Toggle User Menu"),
784 )
785 } else {
786 this.trigger_with_tooltip(
787 IconButton::new("user-menu", IconName::ChevronDown)
788 .icon_size(IconSize::Small),
789 Tooltip::text("Toggle User Menu"),
790 )
791 }
792 })
793 .anchor(gpui::Corner::TopRight)
794 }
795}