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 |this| this.child(self.render_sign_in_button(cx)),
206 )
207 .when(TitleBarSettings::get_global(cx).show_user_menu, |this| {
208 this.child(self.render_user_menu_button(cx))
209 })
210 .into_any_element(),
211 );
212
213 if show_menus {
214 self.platform_titlebar.update(cx, |this, _| {
215 this.set_children(
216 self.application_menu
217 .clone()
218 .map(|menu| menu.into_any_element()),
219 );
220 });
221
222 let height = PlatformTitleBar::height(window);
223 let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| {
224 platform_titlebar.title_bar_color(window, cx)
225 });
226
227 v_flex()
228 .w_full()
229 .child(self.platform_titlebar.clone().into_any_element())
230 .child(
231 h_flex()
232 .bg(title_bar_color)
233 .h(height)
234 .pl_2()
235 .justify_between()
236 .w_full()
237 .children(children),
238 )
239 .into_any_element()
240 } else {
241 self.platform_titlebar.update(cx, |this, _| {
242 this.set_children(children);
243 });
244 self.platform_titlebar.clone().into_any_element()
245 }
246 }
247}
248
249impl TitleBar {
250 pub fn new(
251 id: impl Into<ElementId>,
252 workspace: &Workspace,
253 window: &mut Window,
254 cx: &mut Context<Self>,
255 ) -> Self {
256 let project = workspace.project().clone();
257 let git_store = project.read(cx).git_store().clone();
258 let user_store = workspace.app_state().user_store.clone();
259 let client = workspace.app_state().client.clone();
260 let active_call = ActiveCall::global(cx);
261
262 let platform_style = PlatformStyle::platform();
263 let application_menu = match platform_style {
264 PlatformStyle::Mac => {
265 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
266 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
267 } else {
268 None
269 }
270 }
271 PlatformStyle::Linux | PlatformStyle::Windows => {
272 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
273 }
274 };
275
276 let mut subscriptions = Vec::new();
277 subscriptions.push(
278 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
279 cx.notify()
280 }),
281 );
282 subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify()));
283 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
284 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
285 subscriptions.push(
286 cx.subscribe(&git_store, move |_, _, event, cx| match event {
287 GitStoreEvent::ActiveRepositoryChanged(_)
288 | GitStoreEvent::RepositoryUpdated(_, _, true) => {
289 cx.notify();
290 }
291 _ => {}
292 }),
293 );
294 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
295
296 let banner = cx.new(|cx| {
297 OnboardingBanner::new(
298 "ACP Claude Code Onboarding",
299 IconName::AiClaude,
300 "Claude Code",
301 Some("Introducing:".into()),
302 zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(),
303 cx,
304 )
305 // When updating this to a non-AI feature release, remove this line.
306 .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai)
307 });
308
309 let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx));
310
311 Self {
312 platform_titlebar,
313 application_menu,
314 workspace: workspace.weak_handle(),
315 project,
316 user_store,
317 client,
318 _subscriptions: subscriptions,
319 banner,
320 screen_share_popover_handle: Default::default(),
321 }
322 }
323
324 fn project_name(&self, cx: &Context<Self>) -> Option<SharedString> {
325 self.project
326 .read(cx)
327 .visible_worktrees(cx)
328 .map(|worktree| {
329 let worktree = worktree.read(cx);
330 let settings_location = SettingsLocation {
331 worktree_id: worktree.id(),
332 path: RelPath::empty(),
333 };
334
335 let settings = WorktreeSettings::get(Some(settings_location), cx);
336 let name = match &settings.project_name {
337 Some(name) => name.as_str(),
338 None => worktree.root_name_str(),
339 };
340 SharedString::new(name)
341 })
342 .next()
343 }
344
345 fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
346 let options = self.project.read(cx).remote_connection_options(cx)?;
347 let host: SharedString = options.display_name().into();
348
349 let (nickname, tooltip_title, icon) = match options {
350 RemoteConnectionOptions::Ssh(options) => (
351 options.nickname.map(|nick| nick.into()),
352 "Remote Project",
353 IconName::Server,
354 ),
355 RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux),
356 RemoteConnectionOptions::Docker(_dev_container_connection) => {
357 (None, "Dev Container", IconName::Box)
358 }
359 };
360
361 let nickname = nickname.unwrap_or_else(|| host.clone());
362
363 let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? {
364 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
365 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
366 remote::ConnectionState::HeartbeatMissed => (
367 Color::Warning,
368 format!("Connection attempt to {host} missed. Retrying..."),
369 ),
370 remote::ConnectionState::Reconnecting => (
371 Color::Warning,
372 format!("Lost connection to {host}. Reconnecting..."),
373 ),
374 remote::ConnectionState::Disconnected => {
375 (Color::Error, format!("Disconnected from {host}"))
376 }
377 };
378
379 let icon_color = match self.project.read(cx).remote_connection_state(cx)? {
380 remote::ConnectionState::Connecting => Color::Info,
381 remote::ConnectionState::Connected => Color::Default,
382 remote::ConnectionState::HeartbeatMissed => Color::Warning,
383 remote::ConnectionState::Reconnecting => Color::Warning,
384 remote::ConnectionState::Disconnected => Color::Error,
385 };
386
387 let meta = SharedString::from(meta);
388
389 Some(
390 ButtonLike::new("ssh-server-icon")
391 .child(
392 h_flex()
393 .gap_2()
394 .max_w_32()
395 .child(
396 IconWithIndicator::new(
397 Icon::new(icon).size(IconSize::Small).color(icon_color),
398 Some(Indicator::dot().color(indicator_color)),
399 )
400 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
401 .into_any_element(),
402 )
403 .child(Label::new(nickname).size(LabelSize::Small).truncate()),
404 )
405 .tooltip(move |_window, cx| {
406 Tooltip::with_meta(
407 tooltip_title,
408 Some(&OpenRemote {
409 from_existing_connection: false,
410 create_new_window: false,
411 }),
412 meta.clone(),
413 cx,
414 )
415 })
416 .on_click(|_, window, cx| {
417 window.dispatch_action(
418 OpenRemote {
419 from_existing_connection: false,
420 create_new_window: false,
421 }
422 .boxed_clone(),
423 cx,
424 );
425 })
426 .into_any_element(),
427 )
428 }
429
430 pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
431 if self.project.read(cx).is_via_remote_server() {
432 return self.render_remote_project_connection(cx);
433 }
434
435 if self.project.read(cx).is_disconnected(cx) {
436 return Some(
437 Button::new("disconnected", "Disconnected")
438 .disabled(true)
439 .color(Color::Disabled)
440 .style(ButtonStyle::Subtle)
441 .label_size(LabelSize::Small)
442 .into_any_element(),
443 );
444 }
445
446 let host = self.project.read(cx).host()?;
447 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
448 let participant_index = self
449 .user_store
450 .read(cx)
451 .participant_indices()
452 .get(&host_user.id)?;
453 Some(
454 Button::new("project_owner_trigger", host_user.github_login.clone())
455 .color(Color::Player(participant_index.0))
456 .style(ButtonStyle::Subtle)
457 .label_size(LabelSize::Small)
458 .tooltip(Tooltip::text(format!(
459 "{} is sharing this project. Click to follow.",
460 host_user.github_login
461 )))
462 .on_click({
463 let host_peer_id = host.peer_id;
464 cx.listener(move |this, _, window, cx| {
465 this.workspace
466 .update(cx, |workspace, cx| {
467 workspace.follow(host_peer_id, window, cx);
468 })
469 .log_err();
470 })
471 })
472 .into_any_element(),
473 )
474 }
475
476 pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
477 let name = self.project_name(cx);
478 let is_project_selected = name.is_some();
479 let name = if let Some(name) = name {
480 util::truncate_and_trailoff(&name, MAX_PROJECT_NAME_LENGTH)
481 } else {
482 "Open recent project".to_string()
483 };
484
485 Button::new("project_name_trigger", name)
486 .when(!is_project_selected, |b| b.color(Color::Muted))
487 .style(ButtonStyle::Subtle)
488 .label_size(LabelSize::Small)
489 .tooltip(move |_window, cx| {
490 Tooltip::for_action(
491 "Recent Projects",
492 &zed_actions::OpenRecent {
493 create_new_window: false,
494 },
495 cx,
496 )
497 })
498 .on_click(cx.listener(move |_, _, window, cx| {
499 window.dispatch_action(
500 OpenRecent {
501 create_new_window: false,
502 }
503 .boxed_clone(),
504 cx,
505 );
506 }))
507 }
508
509 pub fn render_project_repo(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
510 let settings = TitleBarSettings::get_global(cx);
511 let repository = self.project.read(cx).active_repository(cx)?;
512 let repository_count = self.project.read(cx).repositories(cx).len();
513 let workspace = self.workspace.upgrade()?;
514 let repo = repository.read(cx);
515 let branch_name = repo
516 .branch
517 .as_ref()
518 .map(|branch| branch.name())
519 .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
520 .or_else(|| {
521 repo.head_commit.as_ref().map(|commit| {
522 commit
523 .sha
524 .chars()
525 .take(MAX_SHORT_SHA_LENGTH)
526 .collect::<String>()
527 })
528 })?;
529 let project_name = self.project_name(cx);
530 let repo_name = repo
531 .work_directory_abs_path
532 .file_name()
533 .and_then(|name| name.to_str())
534 .map(SharedString::new);
535 let show_repo_name =
536 repository_count > 1 && repo.branch.is_some() && repo_name != project_name;
537 let branch_name = if let Some(repo_name) = repo_name.filter(|_| show_repo_name) {
538 format!("{repo_name}/{branch_name}")
539 } else {
540 branch_name
541 };
542
543 Some(
544 Button::new("project_branch_trigger", branch_name)
545 .color(Color::Muted)
546 .style(ButtonStyle::Subtle)
547 .label_size(LabelSize::Small)
548 .tooltip(move |_window, cx| {
549 Tooltip::with_meta(
550 "Recent Branches",
551 Some(&zed_actions::git::Branch),
552 "Local branches only",
553 cx,
554 )
555 })
556 .on_click(move |_, window, cx| {
557 let _ = workspace.update(cx, |this, cx| {
558 window.focus(&this.active_pane().focus_handle(cx));
559 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
560 });
561 })
562 .when(settings.show_branch_icon, |branch_button| {
563 let (icon, icon_color) = {
564 let status = repo.status_summary();
565 let tracked = status.index + status.worktree;
566 if status.conflict > 0 {
567 (IconName::Warning, Color::VersionControlConflict)
568 } else if tracked.modified > 0 {
569 (IconName::SquareDot, Color::VersionControlModified)
570 } else if tracked.added > 0 || status.untracked > 0 {
571 (IconName::SquarePlus, Color::VersionControlAdded)
572 } else if tracked.deleted > 0 {
573 (IconName::SquareMinus, Color::VersionControlDeleted)
574 } else {
575 (IconName::GitBranch, Color::Muted)
576 }
577 };
578
579 branch_button
580 .icon(icon)
581 .icon_position(IconPosition::Start)
582 .icon_color(icon_color)
583 .icon_size(IconSize::Indicator)
584 }),
585 )
586 }
587
588 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
589 if window.is_window_active() {
590 ActiveCall::global(cx)
591 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
592 .detach_and_log_err(cx);
593 } else if cx.active_window().is_none() {
594 ActiveCall::global(cx)
595 .update(cx, |call, cx| call.set_location(None, cx))
596 .detach_and_log_err(cx);
597 }
598 self.workspace
599 .update(cx, |workspace, cx| {
600 workspace.update_active_view_for_followers(window, cx);
601 })
602 .ok();
603 }
604
605 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
606 cx.notify();
607 }
608
609 fn share_project(&mut self, cx: &mut Context<Self>) {
610 let active_call = ActiveCall::global(cx);
611 let project = self.project.clone();
612 active_call
613 .update(cx, |call, cx| call.share_project(project, cx))
614 .detach_and_log_err(cx);
615 }
616
617 fn unshare_project(&mut self, _: &mut Window, cx: &mut Context<Self>) {
618 let active_call = ActiveCall::global(cx);
619 let project = self.project.clone();
620 active_call
621 .update(cx, |call, cx| call.unshare_project(project, cx))
622 .log_err();
623 }
624
625 fn render_connection_status(
626 &self,
627 status: &client::Status,
628 cx: &mut Context<Self>,
629 ) -> Option<AnyElement> {
630 match status {
631 client::Status::ConnectionError
632 | client::Status::ConnectionLost
633 | client::Status::Reauthenticating
634 | client::Status::Reconnecting
635 | client::Status::ReconnectionError { .. } => Some(
636 div()
637 .id("disconnected")
638 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
639 .tooltip(Tooltip::text("Disconnected"))
640 .into_any_element(),
641 ),
642 client::Status::UpgradeRequired => {
643 let auto_updater = auto_update::AutoUpdater::get(cx);
644 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
645 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
646 Some(AutoUpdateStatus::Installing { .. })
647 | Some(AutoUpdateStatus::Downloading { .. })
648 | Some(AutoUpdateStatus::Checking) => "Updating...",
649 Some(AutoUpdateStatus::Idle)
650 | Some(AutoUpdateStatus::Errored { .. })
651 | None => "Please update Zed to Collaborate",
652 };
653
654 Some(
655 Button::new("connection-status", label)
656 .label_size(LabelSize::Small)
657 .on_click(|_, window, cx| {
658 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx)
659 && auto_updater.read(cx).status().is_updated()
660 {
661 workspace::reload(cx);
662 return;
663 }
664 auto_update::check(&Default::default(), window, cx);
665 })
666 .into_any_element(),
667 )
668 }
669 _ => None,
670 }
671 }
672
673 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
674 let client = self.client.clone();
675 Button::new("sign_in", "Sign in")
676 .label_size(LabelSize::Small)
677 .on_click(move |_, window, cx| {
678 let client = client.clone();
679 window
680 .spawn(cx, async move |cx| {
681 client
682 .sign_in_with_optional_connect(true, cx)
683 .await
684 .notify_async_err(cx);
685 })
686 .detach();
687 })
688 }
689
690 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
691 let user_store = self.user_store.read(cx);
692 let user = user_store.current_user();
693
694 let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone());
695 let user_login = user.as_ref().map(|u| u.github_login.clone());
696
697 let is_signed_in = user.is_some();
698
699 let has_subscription_period = user_store.subscription_period().is_some();
700 let plan = user_store.plan().filter(|_| {
701 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
702 has_subscription_period
703 });
704
705 let free_chip_bg = cx
706 .theme()
707 .colors()
708 .editor_background
709 .opacity(0.5)
710 .blend(cx.theme().colors().text_accent.opacity(0.05));
711
712 let pro_chip_bg = cx
713 .theme()
714 .colors()
715 .editor_background
716 .opacity(0.5)
717 .blend(cx.theme().colors().text_accent.opacity(0.2));
718
719 PopoverMenu::new("user-menu")
720 .anchor(Corner::TopRight)
721 .menu(move |window, cx| {
722 ContextMenu::build(window, cx, |menu, _, _cx| {
723 let user_login = user_login.clone();
724
725 let (plan_name, label_color, bg_color) = match plan {
726 None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => {
727 ("Free", Color::Default, free_chip_bg)
728 }
729 Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)) => {
730 ("Pro Trial", Color::Accent, pro_chip_bg)
731 }
732 Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)) => {
733 ("Pro", Color::Accent, pro_chip_bg)
734 }
735 };
736
737 menu.when(is_signed_in, |this| {
738 this.custom_entry(
739 move |_window, _cx| {
740 let user_login = user_login.clone().unwrap_or_default();
741
742 h_flex()
743 .w_full()
744 .justify_between()
745 .child(Label::new(user_login))
746 .child(
747 Chip::new(plan_name.to_string())
748 .bg_color(bg_color)
749 .label_color(label_color),
750 )
751 .into_any_element()
752 },
753 move |_, cx| {
754 cx.open_url(&zed_urls::account_url(cx));
755 },
756 )
757 .separator()
758 })
759 .action("Settings", zed_actions::OpenSettings.boxed_clone())
760 .action("Keymap", Box::new(zed_actions::OpenKeymap))
761 .action(
762 "Themes…",
763 zed_actions::theme_selector::Toggle::default().boxed_clone(),
764 )
765 .action(
766 "Icon Themes…",
767 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
768 )
769 .action(
770 "Extensions",
771 zed_actions::Extensions::default().boxed_clone(),
772 )
773 .when(is_signed_in, |this| {
774 this.separator()
775 .action("Sign Out", client::SignOut.boxed_clone())
776 })
777 })
778 .into()
779 })
780 .map(|this| {
781 if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture {
782 this.trigger_with_tooltip(
783 ButtonLike::new("user-menu")
784 .children(user_avatar.clone().map(|avatar| Avatar::new(avatar))),
785 Tooltip::text("Toggle User Menu"),
786 )
787 } else {
788 this.trigger_with_tooltip(
789 IconButton::new("user-menu", IconName::ChevronDown)
790 .icon_size(IconSize::Small),
791 Tooltip::text("Toggle User Menu"),
792 )
793 }
794 })
795 .anchor(gpui::Corner::TopRight)
796 }
797}