1mod application_menu;
2mod collab;
3mod platforms;
4mod window_controls;
5
6#[cfg(feature = "stories")]
7mod stories;
8
9use crate::application_menu::ApplicationMenu;
10
11#[cfg(not(target_os = "macos"))]
12use crate::application_menu::{
13 ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
14};
15
16use crate::platforms::{platform_linux, platform_mac, platform_windows};
17use auto_update::AutoUpdateStatus;
18use call::ActiveCall;
19use client::{Client, UserStore};
20use feature_flags::{FeatureFlagAppExt, ZedPro};
21use git_ui::onboarding::GitBanner;
22use gpui::{
23 actions, div, px, Action, AnyElement, App, Context, Decorations, Element, Entity,
24 InteractiveElement, Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
25 StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window,
26};
27use project::Project;
28use rpc::proto;
29use settings::Settings as _;
30use smallvec::SmallVec;
31use std::sync::Arc;
32use theme::ActiveTheme;
33use ui::{
34 h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
35 IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
36};
37use util::ResultExt;
38use workspace::{notifications::NotifyResultExt, Workspace};
39use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
40use zeta::ZedPredictBanner;
41
42#[cfg(feature = "stories")]
43pub use stories::*;
44
45const MAX_PROJECT_NAME_LENGTH: usize = 40;
46const MAX_BRANCH_NAME_LENGTH: usize = 40;
47
48const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
49
50actions!(
51 collab,
52 [
53 ShareProject,
54 UnshareProject,
55 ToggleUserMenu,
56 ToggleProjectMenu,
57 SwitchBranch
58 ]
59);
60
61pub fn init(cx: &mut App) {
62 cx.observe_new(|workspace: &mut Workspace, window, cx| {
63 let Some(window) = window else {
64 return;
65 };
66 let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx));
67 workspace.set_titlebar_item(item.into(), window, cx);
68
69 #[cfg(not(target_os = "macos"))]
70 workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| {
71 if let Some(titlebar) = workspace
72 .titlebar_item()
73 .and_then(|item| item.downcast::<TitleBar>().ok())
74 {
75 titlebar.update(cx, |titlebar, cx| {
76 if let Some(ref menu) = titlebar.application_menu {
77 menu.update(cx, |menu, cx| menu.open_menu(action, window, cx));
78 }
79 });
80 }
81 });
82
83 #[cfg(not(target_os = "macos"))]
84 workspace.register_action(|workspace, _: &ActivateMenuRight, 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 if let Some(ref menu) = titlebar.application_menu {
91 menu.update(cx, |menu, cx| {
92 menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx)
93 });
94 }
95 });
96 }
97 });
98
99 #[cfg(not(target_os = "macos"))]
100 workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| {
101 if let Some(titlebar) = workspace
102 .titlebar_item()
103 .and_then(|item| item.downcast::<TitleBar>().ok())
104 {
105 titlebar.update(cx, |titlebar, cx| {
106 if let Some(ref menu) = titlebar.application_menu {
107 menu.update(cx, |menu, cx| {
108 menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx)
109 });
110 }
111 });
112 }
113 });
114 })
115 .detach();
116}
117
118pub struct TitleBar {
119 platform_style: PlatformStyle,
120 content: Stateful<Div>,
121 children: SmallVec<[AnyElement; 2]>,
122 project: Entity<Project>,
123 user_store: Entity<UserStore>,
124 client: Arc<Client>,
125 workspace: WeakEntity<Workspace>,
126 should_move: bool,
127 application_menu: Option<Entity<ApplicationMenu>>,
128 _subscriptions: Vec<Subscription>,
129 zed_predict_banner: Entity<ZedPredictBanner>,
130 git_banner: Entity<GitBanner>,
131}
132
133impl Render for TitleBar {
134 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
135 let close_action = Box::new(workspace::CloseWindow);
136 let height = Self::height(window);
137 let supported_controls = window.window_controls();
138 let decorations = window.window_decorations();
139 let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
140 if window.is_window_active() && !self.should_move {
141 cx.theme().colors().title_bar_background
142 } else {
143 cx.theme().colors().title_bar_inactive_background
144 }
145 } else {
146 cx.theme().colors().title_bar_background
147 };
148
149 h_flex()
150 .id("titlebar")
151 .w_full()
152 .h(height)
153 .map(|this| {
154 if window.is_fullscreen() {
155 this.pl_2()
156 } else if self.platform_style == PlatformStyle::Mac {
157 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
158 } else {
159 this.pl_2()
160 }
161 })
162 .map(|el| match decorations {
163 Decorations::Server => el,
164 Decorations::Client { tiling, .. } => el
165 .when(!(tiling.top || tiling.right), |el| {
166 el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
167 })
168 .when(!(tiling.top || tiling.left), |el| {
169 el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
170 })
171 // this border is to avoid a transparent gap in the rounded corners
172 .mt(px(-1.))
173 .border(px(1.))
174 .border_color(titlebar_color),
175 })
176 .bg(titlebar_color)
177 .content_stretch()
178 .child(
179 div()
180 .id("titlebar-content")
181 .flex()
182 .flex_row()
183 .items_center()
184 .justify_between()
185 .w_full()
186 // Note: On Windows the title bar behavior is handled by the platform implementation.
187 .when(self.platform_style != PlatformStyle::Windows, |this| {
188 this.on_click(|event, window, _| {
189 if event.up.click_count == 2 {
190 window.zoom_window();
191 }
192 })
193 })
194 .child(
195 h_flex()
196 .gap_1()
197 .map(|title_bar| {
198 let mut render_project_items = true;
199 title_bar
200 .when_some(self.application_menu.clone(), |title_bar, menu| {
201 render_project_items = !menu.read(cx).all_menus_shown();
202 title_bar.child(menu)
203 })
204 .when(render_project_items, |title_bar| {
205 title_bar
206 .children(self.render_project_host(cx))
207 .child(self.render_project_name(cx))
208 .children(self.render_project_branch(cx))
209 })
210 })
211 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
212 )
213 .child(self.render_collaborator_list(window, cx))
214 .child(self.zed_predict_banner.clone())
215 .child(self.git_banner.clone())
216 .child(
217 h_flex()
218 .gap_1()
219 .pr_1()
220 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
221 .children(self.render_call_controls(window, cx))
222 .map(|el| {
223 let status = self.client.status();
224 let status = &*status.borrow();
225 if matches!(status, client::Status::Connected { .. }) {
226 el.child(self.render_user_menu_button(cx))
227 } else {
228 el.children(self.render_connection_status(status, cx))
229 .child(self.render_sign_in_button(cx))
230 .child(self.render_user_menu_button(cx))
231 }
232 }),
233 ),
234 )
235 .when(!window.is_fullscreen(), |title_bar| {
236 match self.platform_style {
237 PlatformStyle::Mac => title_bar,
238 PlatformStyle::Linux => {
239 if matches!(decorations, Decorations::Client { .. }) {
240 title_bar
241 .child(platform_linux::LinuxWindowControls::new(close_action))
242 .when(supported_controls.window_menu, |titlebar| {
243 titlebar.on_mouse_down(
244 gpui::MouseButton::Right,
245 move |ev, window, _| window.show_window_menu(ev.position),
246 )
247 })
248 .on_mouse_move(cx.listener(move |this, _ev, window, _| {
249 if this.should_move {
250 this.should_move = false;
251 window.start_window_move();
252 }
253 }))
254 .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
255 this.should_move = false;
256 }))
257 .on_mouse_up(
258 gpui::MouseButton::Left,
259 cx.listener(move |this, _ev, _window, _cx| {
260 this.should_move = false;
261 }),
262 )
263 .on_mouse_down(
264 gpui::MouseButton::Left,
265 cx.listener(move |this, _ev, _window, _cx| {
266 this.should_move = true;
267 }),
268 )
269 } else {
270 title_bar
271 }
272 }
273 PlatformStyle::Windows => {
274 title_bar.child(platform_windows::WindowsWindowControls::new(height))
275 }
276 }
277 })
278 }
279}
280
281impl TitleBar {
282 pub fn new(
283 id: impl Into<ElementId>,
284 workspace: &Workspace,
285 window: &mut Window,
286 cx: &mut Context<Self>,
287 ) -> Self {
288 let project = workspace.project().clone();
289 let user_store = workspace.app_state().user_store.clone();
290 let client = workspace.app_state().client.clone();
291 let active_call = ActiveCall::global(cx);
292
293 let platform_style = PlatformStyle::platform();
294 let application_menu = match platform_style {
295 PlatformStyle::Mac => {
296 if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() {
297 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
298 } else {
299 None
300 }
301 }
302 PlatformStyle::Linux | PlatformStyle::Windows => {
303 Some(cx.new(|cx| ApplicationMenu::new(window, cx)))
304 }
305 };
306
307 let mut subscriptions = Vec::new();
308 subscriptions.push(
309 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
310 cx.notify()
311 }),
312 );
313 subscriptions.push(cx.subscribe(&project, |_, _, _, cx| cx.notify()));
314 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
315 subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed));
316 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
317
318 let zed_predict_banner = cx.new(ZedPredictBanner::new);
319 let git_banner = cx.new(GitBanner::new);
320
321 Self {
322 platform_style,
323 content: div().id(id.into()),
324 children: SmallVec::new(),
325 application_menu,
326 workspace: workspace.weak_handle(),
327 should_move: false,
328 project,
329 user_store,
330 client,
331 _subscriptions: subscriptions,
332 zed_predict_banner,
333 git_banner,
334 }
335 }
336
337 #[cfg(not(target_os = "windows"))]
338 pub fn height(window: &mut Window) -> Pixels {
339 (1.75 * window.rem_size()).max(px(34.))
340 }
341
342 #[cfg(target_os = "windows")]
343 pub fn height(_window: &mut Window) -> Pixels {
344 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
345 px(32.)
346 }
347
348 /// Sets the platform style.
349 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
350 self.platform_style = style;
351 self
352 }
353
354 fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
355 let options = self.project.read(cx).ssh_connection_options(cx)?;
356 let host: SharedString = options.connection_string().into();
357
358 let nickname = options
359 .nickname
360 .clone()
361 .map(|nick| nick.into())
362 .unwrap_or_else(|| host.clone());
363
364 let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
365 remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
366 remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")),
367 remote::ConnectionState::HeartbeatMissed => (
368 Color::Warning,
369 format!("Connection attempt to {host} missed. Retrying..."),
370 ),
371 remote::ConnectionState::Reconnecting => (
372 Color::Warning,
373 format!("Lost connection to {host}. Reconnecting..."),
374 ),
375 remote::ConnectionState::Disconnected => {
376 (Color::Error, format!("Disconnected from {host}"))
377 }
378 };
379
380 let icon_color = match self.project.read(cx).ssh_connection_state(cx)? {
381 remote::ConnectionState::Connecting => Color::Info,
382 remote::ConnectionState::Connected => Color::Default,
383 remote::ConnectionState::HeartbeatMissed => Color::Warning,
384 remote::ConnectionState::Reconnecting => Color::Warning,
385 remote::ConnectionState::Disconnected => Color::Error,
386 };
387
388 let meta = SharedString::from(meta);
389
390 Some(
391 ButtonLike::new("ssh-server-icon")
392 .child(
393 h_flex()
394 .gap_2()
395 .max_w_32()
396 .child(
397 IconWithIndicator::new(
398 Icon::new(IconName::Server)
399 .size(IconSize::XSmall)
400 .color(icon_color),
401 Some(Indicator::dot().color(indicator_color)),
402 )
403 .indicator_border_color(Some(cx.theme().colors().title_bar_background))
404 .into_any_element(),
405 )
406 .child(
407 Label::new(nickname.clone())
408 .size(LabelSize::Small)
409 .truncate(),
410 ),
411 )
412 .tooltip(move |window, cx| {
413 Tooltip::with_meta(
414 "Remote Project",
415 Some(&OpenRemote),
416 meta.clone(),
417 window,
418 cx,
419 )
420 })
421 .on_click(|_, window, cx| {
422 window.dispatch_action(OpenRemote.boxed_clone(), cx);
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_ssh() {
430 return self.render_ssh_project_host(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.clone()
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 = {
476 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
477 let worktree = worktree.read(cx);
478 worktree.root_name()
479 });
480
481 names.next()
482 };
483 let is_project_selected = name.is_some();
484 let name = if let Some(name) = name {
485 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
486 } else {
487 "Open recent project".to_string()
488 };
489
490 Button::new("project_name_trigger", name)
491 .when(!is_project_selected, |b| b.color(Color::Muted))
492 .style(ButtonStyle::Subtle)
493 .label_size(LabelSize::Small)
494 .tooltip(move |window, cx| {
495 Tooltip::for_action(
496 "Recent Projects",
497 &zed_actions::OpenRecent {
498 create_new_window: false,
499 },
500 window,
501 cx,
502 )
503 })
504 .on_click(cx.listener(move |_, _, window, cx| {
505 window.dispatch_action(
506 OpenRecent {
507 create_new_window: false,
508 }
509 .boxed_clone(),
510 cx,
511 );
512 }))
513 }
514
515 pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
516 let entry = {
517 let mut names_and_branches =
518 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
519 let worktree = worktree.read(cx);
520 worktree.root_git_entry()
521 });
522
523 names_and_branches.next().flatten()
524 };
525 let workspace = self.workspace.upgrade()?;
526 let branch_name = entry
527 .as_ref()
528 .and_then(|entry| entry.branch())
529 .map(|branch| branch.name.clone())
530 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
531 Some(
532 Button::new("project_branch_trigger", branch_name)
533 .color(Color::Muted)
534 .style(ButtonStyle::Subtle)
535 .label_size(LabelSize::Small)
536 .tooltip(move |window, cx| {
537 Tooltip::with_meta(
538 "Recent Branches",
539 Some(&zed_actions::git::Branch),
540 "Local branches only",
541 window,
542 cx,
543 )
544 })
545 .on_click(move |_, window, cx| {
546 let _ = workspace.update(cx, |_this, cx| {
547 window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
548 });
549 }),
550 )
551 }
552
553 fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
554 if window.is_window_active() {
555 ActiveCall::global(cx)
556 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
557 .detach_and_log_err(cx);
558 } else if cx.active_window().is_none() {
559 ActiveCall::global(cx)
560 .update(cx, |call, cx| call.set_location(None, cx))
561 .detach_and_log_err(cx);
562 }
563 self.workspace
564 .update(cx, |workspace, cx| {
565 workspace.update_active_view_for_followers(window, cx);
566 })
567 .ok();
568 }
569
570 fn active_call_changed(&mut self, cx: &mut Context<Self>) {
571 cx.notify();
572 }
573
574 fn share_project(&mut self, _: &ShareProject, cx: &mut Context<Self>) {
575 let active_call = ActiveCall::global(cx);
576 let project = self.project.clone();
577 active_call
578 .update(cx, |call, cx| call.share_project(project, cx))
579 .detach_and_log_err(cx);
580 }
581
582 fn unshare_project(&mut self, _: &UnshareProject, _: &mut Window, cx: &mut Context<Self>) {
583 let active_call = ActiveCall::global(cx);
584 let project = self.project.clone();
585 active_call
586 .update(cx, |call, cx| call.unshare_project(project, cx))
587 .log_err();
588 }
589
590 fn render_connection_status(
591 &self,
592 status: &client::Status,
593 cx: &mut Context<Self>,
594 ) -> Option<AnyElement> {
595 match status {
596 client::Status::ConnectionError
597 | client::Status::ConnectionLost
598 | client::Status::Reauthenticating { .. }
599 | client::Status::Reconnecting { .. }
600 | client::Status::ReconnectionError { .. } => Some(
601 div()
602 .id("disconnected")
603 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
604 .tooltip(Tooltip::text("Disconnected"))
605 .into_any_element(),
606 ),
607 client::Status::UpgradeRequired => {
608 let auto_updater = auto_update::AutoUpdater::get(cx);
609 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
610 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
611 Some(AutoUpdateStatus::Installing)
612 | Some(AutoUpdateStatus::Downloading)
613 | Some(AutoUpdateStatus::Checking) => "Updating...",
614 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
615 "Please update Zed to Collaborate"
616 }
617 };
618
619 Some(
620 Button::new("connection-status", label)
621 .label_size(LabelSize::Small)
622 .on_click(|_, window, cx| {
623 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
624 if auto_updater.read(cx).status().is_updated() {
625 workspace::reload(&Default::default(), cx);
626 return;
627 }
628 }
629 auto_update::check(&Default::default(), window, cx);
630 })
631 .into_any_element(),
632 )
633 }
634 _ => None,
635 }
636 }
637
638 pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
639 let client = self.client.clone();
640 Button::new("sign_in", "Sign in")
641 .label_size(LabelSize::Small)
642 .on_click(move |_, window, cx| {
643 let client = client.clone();
644 window
645 .spawn(cx, async move |cx| {
646 client
647 .authenticate_and_connect(true, &cx)
648 .await
649 .notify_async_err(cx);
650 })
651 .detach();
652 })
653 }
654
655 pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
656 let user_store = self.user_store.read(cx);
657 if let Some(user) = user_store.current_user() {
658 let plan = user_store.current_plan();
659 PopoverMenu::new("user-menu")
660 .menu(move |window, cx| {
661 ContextMenu::build(window, cx, |menu, _, cx| {
662 menu.when(cx.has_flag::<ZedPro>(), |menu| {
663 menu.action(
664 format!(
665 "Current Plan: {}",
666 match plan {
667 None => "",
668 Some(proto::Plan::Free) => "Free",
669 Some(proto::Plan::ZedPro) => "Pro",
670 }
671 ),
672 zed_actions::OpenAccountSettings.boxed_clone(),
673 )
674 .separator()
675 })
676 .action("Settings", zed_actions::OpenSettings.boxed_clone())
677 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
678 .action(
679 "Themes…",
680 zed_actions::theme_selector::Toggle::default().boxed_clone(),
681 )
682 .action(
683 "Icon Themes…",
684 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
685 )
686 .action("Extensions", zed_actions::Extensions.boxed_clone())
687 .separator()
688 .link(
689 "Book Onboarding",
690 OpenBrowser {
691 url: BOOK_ONBOARDING.to_string(),
692 }
693 .boxed_clone(),
694 )
695 .action("Sign Out", client::SignOut.boxed_clone())
696 })
697 .into()
698 })
699 .trigger_with_tooltip(
700 ButtonLike::new("user-menu")
701 .child(
702 h_flex()
703 .gap_0p5()
704 .children(
705 workspace::WorkspaceSettings::get_global(cx)
706 .show_user_picture
707 .then(|| Avatar::new(user.avatar_uri.clone())),
708 )
709 .child(
710 Icon::new(IconName::ChevronDown)
711 .size(IconSize::Small)
712 .color(Color::Muted),
713 ),
714 )
715 .style(ButtonStyle::Subtle),
716 Tooltip::text("Toggle User Menu"),
717 )
718 .anchor(gpui::Corner::TopRight)
719 } else {
720 PopoverMenu::new("user-menu")
721 .menu(|window, cx| {
722 ContextMenu::build(window, cx, |menu, _, _| {
723 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
724 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
725 .action(
726 "Themes…",
727 zed_actions::theme_selector::Toggle::default().boxed_clone(),
728 )
729 .action(
730 "Icon Themes…",
731 zed_actions::icon_theme_selector::Toggle::default().boxed_clone(),
732 )
733 .action("Extensions", zed_actions::Extensions.boxed_clone())
734 .separator()
735 .link(
736 "Book Onboarding",
737 OpenBrowser {
738 url: BOOK_ONBOARDING.to_string(),
739 }
740 .boxed_clone(),
741 )
742 })
743 .into()
744 })
745 .trigger_with_tooltip(
746 IconButton::new("user-menu", IconName::ChevronDown).icon_size(IconSize::Small),
747 Tooltip::text("Toggle User Menu"),
748 )
749 }
750 }
751}
752
753impl InteractiveElement for TitleBar {
754 fn interactivity(&mut self) -> &mut Interactivity {
755 self.content.interactivity()
756 }
757}
758
759impl StatefulInteractiveElement for TitleBar {}
760
761impl ParentElement for TitleBar {
762 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
763 self.children.extend(elements)
764 }
765}