1mod call_controls;
2mod collab;
3mod platforms;
4mod window_controls;
5
6use crate::platforms::{platform_linux, platform_mac, platform_windows};
7use auto_update::AutoUpdateStatus;
8use call::{ActiveCall, ParticipantLocation};
9use client::{Client, UserStore};
10use collab::render_color_ribbon;
11use gpui::{
12 actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
13 IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
14 Subscription, ViewContext, VisualContext, WeakView,
15};
16use project::{Project, RepositoryEntry};
17use recent_projects::RecentProjects;
18use rpc::proto::DevServerStatus;
19use settings::Settings;
20use smallvec::SmallVec;
21use std::sync::Arc;
22use theme::{ActiveTheme, ThemeSettings};
23use ui::{
24 h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton,
25 IconName, Indicator, PopoverMenu, TintColor, Tooltip,
26};
27use util::ResultExt;
28use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
29use workspace::{notifications::NotifyResultExt, Workspace};
30
31const MAX_PROJECT_NAME_LENGTH: usize = 40;
32const MAX_BRANCH_NAME_LENGTH: usize = 40;
33
34actions!(
35 collab,
36 [
37 ShareProject,
38 UnshareProject,
39 ToggleUserMenu,
40 ToggleProjectMenu,
41 SwitchBranch
42 ]
43);
44
45pub fn init(cx: &mut AppContext) {
46 cx.observe_new_views(|workspace: &mut Workspace, cx| {
47 let item = cx.new_view(|cx| TitleBar::new("title-bar", workspace, cx));
48 workspace.set_titlebar_item(item.into(), cx)
49 })
50 .detach();
51}
52
53pub struct TitleBar {
54 platform_style: PlatformStyle,
55 content: Stateful<Div>,
56 children: SmallVec<[AnyElement; 2]>,
57 project: Model<Project>,
58 user_store: Model<UserStore>,
59 client: Arc<Client>,
60 workspace: WeakView<Workspace>,
61 _subscriptions: Vec<Subscription>,
62}
63
64impl Render for TitleBar {
65 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
66 let room = ActiveCall::global(cx).read(cx).room().cloned();
67 let current_user = self.user_store.read(cx).current_user();
68 let client = self.client.clone();
69 let project_id = self.project.read(cx).remote_id();
70 let workspace = self.workspace.upgrade();
71 let close_action = Box::new(workspace::CloseWindow);
72
73 let platform_supported = cfg!(target_os = "macos");
74
75 let height = Self::height(cx);
76
77 let mut title_bar = h_flex()
78 .id("titlebar")
79 .w_full()
80 .pt(Self::top_padding(cx))
81 .h(height + Self::top_padding(cx))
82 .map(|this| {
83 if cx.is_fullscreen() {
84 this.pl_2()
85 } else if self.platform_style == PlatformStyle::Mac {
86 this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
87 } else {
88 this.pl_2()
89 }
90 })
91 .bg(cx.theme().colors().title_bar_background)
92 .content_stretch()
93 .child(
94 div()
95 .id("titlebar-content")
96 .flex()
97 .flex_row()
98 .justify_between()
99 .w_full()
100 // note: on windows titlebar behaviour is handled by the platform implementation
101 .when(cfg!(not(windows)), |this| {
102 this.on_click(|event, cx| {
103 if event.up.click_count == 2 {
104 cx.zoom_window();
105 }
106 })
107 })
108 // left side
109 .child(
110 h_flex()
111 .gap_1()
112 .children(self.render_application_menu(cx))
113 .children(self.render_project_host(cx))
114 .child(self.render_project_name(cx))
115 .children(self.render_project_branch(cx))
116 .on_mouse_move(|_, cx| cx.stop_propagation()),
117 )
118 .child(
119 h_flex()
120 .id("collaborator-list")
121 .w_full()
122 .gap_1()
123 .overflow_x_scroll()
124 .when_some(
125 current_user.clone().zip(client.peer_id()).zip(room.clone()),
126 |this, ((current_user, peer_id), room)| {
127 let player_colors = cx.theme().players();
128 let room = room.read(cx);
129 let mut remote_participants =
130 room.remote_participants().values().collect::<Vec<_>>();
131 remote_participants.sort_by_key(|p| p.participant_index.0);
132
133 let current_user_face_pile = self.render_collaborator(
134 ¤t_user,
135 peer_id,
136 true,
137 room.is_speaking(),
138 room.is_muted(),
139 None,
140 &room,
141 project_id,
142 ¤t_user,
143 cx,
144 );
145
146 this.children(current_user_face_pile.map(|face_pile| {
147 v_flex()
148 .on_mouse_move(|_, cx| cx.stop_propagation())
149 .child(face_pile)
150 .child(render_color_ribbon(player_colors.local().cursor))
151 }))
152 .children(
153 remote_participants.iter().filter_map(|collaborator| {
154 let player_color = player_colors
155 .color_for_participant(collaborator.participant_index.0);
156 let is_following = workspace
157 .as_ref()?
158 .read(cx)
159 .is_being_followed(collaborator.peer_id);
160 let is_present = project_id.map_or(false, |project_id| {
161 collaborator.location
162 == ParticipantLocation::SharedProject { project_id }
163 });
164
165 let facepile = self.render_collaborator(
166 &collaborator.user,
167 collaborator.peer_id,
168 is_present,
169 collaborator.speaking,
170 collaborator.muted,
171 is_following.then_some(player_color.selection),
172 &room,
173 project_id,
174 ¤t_user,
175 cx,
176 )?;
177
178 Some(
179 v_flex()
180 .id(("collaborator", collaborator.user.id))
181 .child(facepile)
182 .child(render_color_ribbon(player_color.cursor))
183 .cursor_pointer()
184 .on_click({
185 let peer_id = collaborator.peer_id;
186 cx.listener(move |this, _, cx| {
187 this.workspace
188 .update(cx, |workspace, cx| {
189 workspace.follow(peer_id, cx);
190 })
191 .ok();
192 })
193 })
194 .tooltip({
195 let login = collaborator.user.github_login.clone();
196 move |cx| {
197 Tooltip::text(format!("Follow {login}"), cx)
198 }
199 }),
200 )
201 }),
202 )
203 },
204 ),
205 )
206 // right side
207 .child(
208 h_flex()
209 .gap_1()
210 .pr_1()
211 .on_mouse_move(|_, cx| cx.stop_propagation())
212 .when_some(room, |this, room| {
213 let room = room.read(cx);
214 let project = self.project.read(cx);
215 let is_local = project.is_local();
216 let is_dev_server_project = project.dev_server_project_id().is_some();
217 let is_shared = (is_local || is_dev_server_project) && project.is_shared();
218 let is_muted = room.is_muted();
219 let is_deafened = room.is_deafened().unwrap_or(false);
220 let is_screen_sharing = room.is_screen_sharing();
221 let can_use_microphone = room.can_use_microphone();
222 let can_share_projects = room.can_share_projects();
223
224 this.when(
225 (is_local || is_dev_server_project) && can_share_projects,
226 |this| {
227 this.child(
228 Button::new(
229 "toggle_sharing",
230 if is_shared { "Unshare" } else { "Share" },
231 )
232 .tooltip(move |cx| {
233 Tooltip::text(
234 if is_shared {
235 "Stop sharing project with call participants"
236 } else {
237 "Share project with call participants"
238 },
239 cx,
240 )
241 })
242 .style(ButtonStyle::Subtle)
243 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
244 .selected(is_shared)
245 .label_size(LabelSize::Small)
246 .on_click(cx.listener(
247 move |this, _, cx| {
248 if is_shared {
249 this.unshare_project(&Default::default(), cx);
250 } else {
251 this.share_project(&Default::default(), cx);
252 }
253 },
254 )),
255 )
256 },
257 )
258 .child(
259 div()
260 .child(
261 IconButton::new("leave-call", ui::IconName::Exit)
262 .style(ButtonStyle::Subtle)
263 .tooltip(|cx| Tooltip::text("Leave call", cx))
264 .icon_size(IconSize::Small)
265 .on_click(move |_, cx| {
266 ActiveCall::global(cx)
267 .update(cx, |call, cx| call.hang_up(cx))
268 .detach_and_log_err(cx);
269 }),
270 )
271 .pr_2(),
272 )
273 .when(can_use_microphone, |this| {
274 this.child(
275 IconButton::new(
276 "mute-microphone",
277 if is_muted {
278 ui::IconName::MicMute
279 } else {
280 ui::IconName::Mic
281 },
282 )
283 .tooltip(move |cx| {
284 Tooltip::text(
285 if !platform_supported {
286 "Cannot share microphone"
287 } else if is_muted {
288 "Unmute microphone"
289 } else {
290 "Mute microphone"
291 },
292 cx,
293 )
294 })
295 .style(ButtonStyle::Subtle)
296 .icon_size(IconSize::Small)
297 .selected(platform_supported && is_muted)
298 .disabled(!platform_supported)
299 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
300 .on_click(move |_, cx| {
301 call_controls::toggle_mute(&Default::default(), cx);
302 }),
303 )
304 })
305 .child(
306 IconButton::new(
307 "mute-sound",
308 if is_deafened {
309 ui::IconName::AudioOff
310 } else {
311 ui::IconName::AudioOn
312 },
313 )
314 .style(ButtonStyle::Subtle)
315 .selected_style(ButtonStyle::Tinted(TintColor::Negative))
316 .icon_size(IconSize::Small)
317 .selected(is_deafened)
318 .disabled(!platform_supported)
319 .tooltip(move |cx| {
320 if !platform_supported {
321 Tooltip::text("Cannot share microphone", cx)
322 } else if can_use_microphone {
323 Tooltip::with_meta(
324 "Deafen Audio",
325 None,
326 "Mic will be muted",
327 cx,
328 )
329 } else {
330 Tooltip::text("Deafen Audio", cx)
331 }
332 })
333 .on_click(move |_, cx| {
334 call_controls::toggle_deafen(&Default::default(), cx)
335 }),
336 )
337 .when(can_share_projects, |this| {
338 this.child(
339 IconButton::new("screen-share", ui::IconName::Screen)
340 .style(ButtonStyle::Subtle)
341 .icon_size(IconSize::Small)
342 .selected(is_screen_sharing)
343 .disabled(!platform_supported)
344 .selected_style(ButtonStyle::Tinted(TintColor::Accent))
345 .tooltip(move |cx| {
346 Tooltip::text(
347 if !platform_supported {
348 "Cannot share screen"
349 } else if is_screen_sharing {
350 "Stop Sharing Screen"
351 } else {
352 "Share Screen"
353 },
354 cx,
355 )
356 })
357 .on_click(move |_, cx| {
358 call_controls::toggle_screen_sharing(&Default::default(), cx)
359 }),
360 )
361 })
362 .child(div().pr_2())
363 })
364 .map(|el| {
365 let status = self.client.status();
366 let status = &*status.borrow();
367 if matches!(status, client::Status::Connected { .. }) {
368 el.child(self.render_user_menu_button(cx))
369 } else {
370 el.children(self.render_connection_status(status, cx))
371 .child(self.render_sign_in_button(cx))
372 .child(self.render_user_menu_button(cx))
373 }
374 }),
375 )
376 );
377
378 // Windows Window Controls
379 title_bar = title_bar.when(
380 self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
381 |title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
382 );
383
384 // Linux Window Controls
385 title_bar = title_bar.when(
386 self.platform_style == PlatformStyle::Linux
387 && !cx.is_fullscreen()
388 && cx.should_render_window_controls(),
389 |title_bar| {
390 title_bar
391 .child(platform_linux::LinuxWindowControls::new(close_action))
392 .on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
393 cx.show_window_menu(ev.position)
394 })
395 .on_mouse_move(move |ev, cx| {
396 if ev.dragging() {
397 cx.start_system_move();
398 }
399 })
400 },
401 );
402
403 title_bar
404 }
405}
406
407impl TitleBar {
408 pub fn new(
409 id: impl Into<ElementId>,
410 workspace: &Workspace,
411 cx: &mut ViewContext<Self>,
412 ) -> Self {
413 let project = workspace.project().clone();
414 let user_store = workspace.app_state().user_store.clone();
415 let client = workspace.app_state().client.clone();
416 let active_call = ActiveCall::global(cx);
417 let mut subscriptions = Vec::new();
418 subscriptions.push(
419 cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| {
420 cx.notify()
421 }),
422 );
423 subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
424 subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
425 subscriptions.push(cx.observe_window_activation(Self::window_activation_changed));
426 subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
427
428 Self {
429 platform_style: PlatformStyle::platform(),
430 content: div().id(id.into()),
431 children: SmallVec::new(),
432 workspace: workspace.weak_handle(),
433 project,
434 user_store,
435 client,
436 _subscriptions: subscriptions,
437 }
438 }
439
440 #[cfg(not(target_os = "windows"))]
441 pub fn height(cx: &mut WindowContext) -> Pixels {
442 (1.75 * cx.rem_size()).max(px(34.))
443 }
444
445 #[cfg(target_os = "windows")]
446 pub fn height(_cx: &mut WindowContext) -> Pixels {
447 // todo(windows) instead of hard coded size report the actual size to the Windows platform API
448 px(32.)
449 }
450
451 #[cfg(not(target_os = "windows"))]
452 fn top_padding(_cx: &WindowContext) -> Pixels {
453 px(0.)
454 }
455
456 #[cfg(target_os = "windows")]
457 fn top_padding(cx: &WindowContext) -> Pixels {
458 use windows::Win32::UI::{
459 HiDpi::GetSystemMetricsForDpi,
460 WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
461 };
462
463 // This top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows:
464 // https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
465 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
466 if cx.is_maximized() {
467 px((padding * 2) as f32)
468 } else {
469 px(0.)
470 }
471 }
472
473 /// Sets the platform style.
474 pub fn platform_style(mut self, style: PlatformStyle) -> Self {
475 self.platform_style = style;
476 self
477 }
478
479 pub fn render_application_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
480 cfg!(not(target_os = "macos")).then(|| {
481 let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
482 let font = cx.text_style().font();
483 let font_id = cx.text_system().resolve_font(&font);
484 let width = cx
485 .text_system()
486 .typographic_bounds(font_id, ui_font_size, 'm')
487 .unwrap()
488 .size
489 .width
490 * 3.0;
491
492 PopoverMenu::new("application-menu")
493 .menu(move |cx| {
494 let width = width;
495 ContextMenu::build(cx, move |menu, _cx| {
496 let width = width;
497 menu.header("Workspace")
498 .action("Open Command Palette", Box::new(command_palette::Toggle))
499 .custom_row(move |cx| {
500 div()
501 .w_full()
502 .flex()
503 .flex_row()
504 .justify_between()
505 .cursor(gpui::CursorStyle::Arrow)
506 .child(Label::new("Buffer Font Size"))
507 .child(
508 div()
509 .flex()
510 .flex_row()
511 .child(div().w(px(16.0)))
512 .child(
513 IconButton::new(
514 "reset-buffer-zoom",
515 IconName::RotateCcw,
516 )
517 .on_click(|_, cx| {
518 cx.dispatch_action(Box::new(
519 zed_actions::ResetBufferFontSize,
520 ))
521 }),
522 )
523 .child(
524 IconButton::new("--buffer-zoom", IconName::Dash)
525 .on_click(|_, cx| {
526 cx.dispatch_action(Box::new(
527 zed_actions::DecreaseBufferFontSize,
528 ))
529 }),
530 )
531 .child(
532 div()
533 .w(width)
534 .flex()
535 .flex_row()
536 .justify_around()
537 .child(Label::new(
538 theme::get_buffer_font_size(cx).to_string(),
539 )),
540 )
541 .child(
542 IconButton::new("+-buffer-zoom", IconName::Plus)
543 .on_click(|_, cx| {
544 cx.dispatch_action(Box::new(
545 zed_actions::IncreaseBufferFontSize,
546 ))
547 }),
548 ),
549 )
550 .into_any_element()
551 })
552 .custom_row(move |cx| {
553 div()
554 .w_full()
555 .flex()
556 .flex_row()
557 .justify_between()
558 .cursor(gpui::CursorStyle::Arrow)
559 .child(Label::new("UI Font Size"))
560 .child(
561 div()
562 .flex()
563 .flex_row()
564 .child(
565 IconButton::new(
566 "reset-ui-zoom",
567 IconName::RotateCcw,
568 )
569 .on_click(|_, cx| {
570 cx.dispatch_action(Box::new(
571 zed_actions::ResetUiFontSize,
572 ))
573 }),
574 )
575 .child(
576 IconButton::new("--ui-zoom", IconName::Dash)
577 .on_click(|_, cx| {
578 cx.dispatch_action(Box::new(
579 zed_actions::DecreaseUiFontSize,
580 ))
581 }),
582 )
583 .child(
584 div()
585 .w(width)
586 .flex()
587 .flex_row()
588 .justify_around()
589 .child(Label::new(
590 theme::get_ui_font_size(cx).to_string(),
591 )),
592 )
593 .child(
594 IconButton::new("+-ui-zoom", IconName::Plus)
595 .on_click(|_, cx| {
596 cx.dispatch_action(Box::new(
597 zed_actions::IncreaseUiFontSize,
598 ))
599 }),
600 ),
601 )
602 .into_any_element()
603 })
604 .header("Project")
605 .action(
606 "Add Folder to Project...",
607 Box::new(workspace::AddFolderToProject),
608 )
609 .action("Open a new Project...", Box::new(workspace::Open))
610 .action(
611 "Open Recent Projects...",
612 Box::new(recent_projects::OpenRecent {
613 create_new_window: false,
614 }),
615 )
616 .header("Help")
617 .action("About Zed", Box::new(zed_actions::About))
618 .action("Welcome", Box::new(workspace::Welcome))
619 .link(
620 "Documentation",
621 Box::new(zed_actions::OpenBrowser {
622 url: "https://zed.dev/docs".into(),
623 }),
624 )
625 .action("Give Feedback", Box::new(feedback::GiveFeedback))
626 .action("Check for Updates", Box::new(auto_update::Check))
627 .action("View Telemetry", Box::new(zed_actions::OpenTelemetryLog))
628 .action(
629 "View Dependency Licenses",
630 Box::new(zed_actions::OpenLicenses),
631 )
632 .separator()
633 .action("Quit", Box::new(zed_actions::Quit))
634 })
635 .into()
636 })
637 .trigger(
638 IconButton::new("application-menu", ui::IconName::Menu)
639 .style(ButtonStyle::Subtle)
640 .tooltip(|cx| Tooltip::text("Open Application Menu", cx))
641 .icon_size(IconSize::Small),
642 )
643 .into_any_element()
644 })
645 }
646
647 pub fn render_project_host(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
648 if let Some(dev_server) =
649 self.project
650 .read(cx)
651 .dev_server_project_id()
652 .and_then(|dev_server_project_id| {
653 dev_server_projects::Store::global(cx)
654 .read(cx)
655 .dev_server_for_project(dev_server_project_id)
656 })
657 {
658 return Some(
659 ButtonLike::new("dev_server_trigger")
660 .child(Indicator::dot().color(
661 if dev_server.status == DevServerStatus::Online {
662 Color::Created
663 } else {
664 Color::Disabled
665 },
666 ))
667 .child(
668 Label::new(dev_server.name.clone())
669 .size(LabelSize::Small)
670 .line_height_style(LineHeightStyle::UiLabel),
671 )
672 .tooltip(move |cx| Tooltip::text("Project is hosted on a dev server", cx))
673 .on_click(cx.listener(|this, _, cx| {
674 if let Some(workspace) = this.workspace.upgrade() {
675 recent_projects::DevServerProjects::open(workspace, cx)
676 }
677 }))
678 .into_any_element(),
679 );
680 }
681
682 if self.project.read(cx).is_disconnected() {
683 return Some(
684 Button::new("disconnected", "Disconnected")
685 .disabled(true)
686 .color(Color::Disabled)
687 .style(ButtonStyle::Subtle)
688 .label_size(LabelSize::Small)
689 .into_any_element(),
690 );
691 }
692
693 let host = self.project.read(cx).host()?;
694 let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?;
695 let participant_index = self
696 .user_store
697 .read(cx)
698 .participant_indices()
699 .get(&host_user.id)?;
700 Some(
701 Button::new("project_owner_trigger", host_user.github_login.clone())
702 .color(Color::Player(participant_index.0))
703 .style(ButtonStyle::Subtle)
704 .label_size(LabelSize::Small)
705 .tooltip(move |cx| {
706 Tooltip::text(
707 format!(
708 "{} is sharing this project. Click to follow.",
709 host_user.github_login.clone()
710 ),
711 cx,
712 )
713 })
714 .on_click({
715 let host_peer_id = host.peer_id;
716 cx.listener(move |this, _, cx| {
717 this.workspace
718 .update(cx, |workspace, cx| {
719 workspace.follow(host_peer_id, cx);
720 })
721 .log_err();
722 })
723 })
724 .into_any_element(),
725 )
726 }
727
728 pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
729 let name = {
730 let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
731 let worktree = worktree.read(cx);
732 worktree.root_name()
733 });
734
735 names.next()
736 };
737 let is_project_selected = name.is_some();
738 let name = if let Some(name) = name {
739 util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH)
740 } else {
741 "Open recent project".to_string()
742 };
743
744 let workspace = self.workspace.clone();
745 Button::new("project_name_trigger", name)
746 .when(!is_project_selected, |b| b.color(Color::Muted))
747 .style(ButtonStyle::Subtle)
748 .label_size(LabelSize::Small)
749 .tooltip(move |cx| {
750 Tooltip::for_action(
751 "Recent Projects",
752 &recent_projects::OpenRecent {
753 create_new_window: false,
754 },
755 cx,
756 )
757 })
758 .on_click(cx.listener(move |_, _, cx| {
759 if let Some(workspace) = workspace.upgrade() {
760 workspace.update(cx, |workspace, cx| {
761 RecentProjects::open(workspace, false, cx);
762 })
763 }
764 }))
765 }
766
767 pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
768 let entry = {
769 let mut names_and_branches =
770 self.project.read(cx).visible_worktrees(cx).map(|worktree| {
771 let worktree = worktree.read(cx);
772 worktree.root_git_entry()
773 });
774
775 names_and_branches.next().flatten()
776 };
777 let workspace = self.workspace.upgrade()?;
778 let branch_name = entry
779 .as_ref()
780 .and_then(RepositoryEntry::branch)
781 .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
782 Some(
783 Button::new("project_branch_trigger", branch_name)
784 .color(Color::Muted)
785 .style(ButtonStyle::Subtle)
786 .label_size(LabelSize::Small)
787 .tooltip(move |cx| {
788 Tooltip::with_meta(
789 "Recent Branches",
790 Some(&ToggleVcsMenu),
791 "Local branches only",
792 cx,
793 )
794 })
795 .on_click(move |_, cx| {
796 let _ = workspace.update(cx, |this, cx| {
797 BranchList::open(this, &Default::default(), cx)
798 });
799 }),
800 )
801 }
802
803 fn window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
804 if cx.is_window_active() {
805 ActiveCall::global(cx)
806 .update(cx, |call, cx| call.set_location(Some(&self.project), cx))
807 .detach_and_log_err(cx);
808 } else if cx.active_window().is_none() {
809 ActiveCall::global(cx)
810 .update(cx, |call, cx| call.set_location(None, cx))
811 .detach_and_log_err(cx);
812 }
813 self.workspace
814 .update(cx, |workspace, cx| {
815 workspace.update_active_view_for_followers(cx);
816 })
817 .ok();
818 }
819
820 fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
821 cx.notify();
822 }
823
824 fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
825 let active_call = ActiveCall::global(cx);
826 let project = self.project.clone();
827 active_call
828 .update(cx, |call, cx| call.share_project(project, cx))
829 .detach_and_log_err(cx);
830 }
831
832 fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
833 let active_call = ActiveCall::global(cx);
834 let project = self.project.clone();
835 active_call
836 .update(cx, |call, cx| call.unshare_project(project, cx))
837 .log_err();
838 }
839
840 fn render_connection_status(
841 &self,
842 status: &client::Status,
843 cx: &mut ViewContext<Self>,
844 ) -> Option<AnyElement> {
845 match status {
846 client::Status::ConnectionError
847 | client::Status::ConnectionLost
848 | client::Status::Reauthenticating { .. }
849 | client::Status::Reconnecting { .. }
850 | client::Status::ReconnectionError { .. } => Some(
851 div()
852 .id("disconnected")
853 .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
854 .tooltip(|cx| Tooltip::text("Disconnected", cx))
855 .into_any_element(),
856 ),
857 client::Status::UpgradeRequired => {
858 let auto_updater = auto_update::AutoUpdater::get(cx);
859 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
860 Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
861 Some(AutoUpdateStatus::Installing)
862 | Some(AutoUpdateStatus::Downloading)
863 | Some(AutoUpdateStatus::Checking) => "Updating...",
864 Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
865 "Please update Zed to Collaborate"
866 }
867 };
868
869 Some(
870 Button::new("connection-status", label)
871 .label_size(LabelSize::Small)
872 .on_click(|_, cx| {
873 if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
874 if auto_updater.read(cx).status().is_updated() {
875 workspace::reload(&Default::default(), cx);
876 return;
877 }
878 }
879 auto_update::check(&Default::default(), cx);
880 })
881 .into_any_element(),
882 )
883 }
884 _ => None,
885 }
886 }
887
888 pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
889 let client = self.client.clone();
890 Button::new("sign_in", "Sign in")
891 .label_size(LabelSize::Small)
892 .on_click(move |_, cx| {
893 let client = client.clone();
894 cx.spawn(move |mut cx| async move {
895 client
896 .authenticate_and_connect(true, &cx)
897 .await
898 .notify_async_err(&mut cx);
899 })
900 .detach();
901 })
902 }
903
904 pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
905 if let Some(user) = self.user_store.read(cx).current_user() {
906 PopoverMenu::new("user-menu")
907 .menu(|cx| {
908 ContextMenu::build(cx, |menu, _| {
909 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
910 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
911 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
912 .action("Extensions", extensions_ui::Extensions.boxed_clone())
913 .separator()
914 .action("Sign Out", client::SignOut.boxed_clone())
915 })
916 .into()
917 })
918 .trigger(
919 ButtonLike::new("user-menu")
920 .child(
921 h_flex()
922 .gap_0p5()
923 .child(Avatar::new(user.avatar_uri.clone()))
924 .child(
925 Icon::new(IconName::ChevronDown)
926 .size(IconSize::Small)
927 .color(Color::Muted),
928 ),
929 )
930 .style(ButtonStyle::Subtle)
931 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
932 )
933 .anchor(gpui::AnchorCorner::TopRight)
934 } else {
935 PopoverMenu::new("user-menu")
936 .menu(|cx| {
937 ContextMenu::build(cx, |menu, _| {
938 menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
939 .action("Key Bindings", Box::new(zed_actions::OpenKeymap))
940 .action("Themes…", theme_selector::Toggle::default().boxed_clone())
941 .action("Extensions", extensions_ui::Extensions.boxed_clone())
942 })
943 .into()
944 })
945 .trigger(
946 ButtonLike::new("user-menu")
947 .child(
948 h_flex().gap_0p5().child(
949 Icon::new(IconName::ChevronDown)
950 .size(IconSize::Small)
951 .color(Color::Muted),
952 ),
953 )
954 .style(ButtonStyle::Subtle)
955 .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
956 )
957 }
958 }
959}
960
961impl InteractiveElement for TitleBar {
962 fn interactivity(&mut self) -> &mut Interactivity {
963 self.content.interactivity()
964 }
965}
966
967impl StatefulInteractiveElement for TitleBar {}
968
969impl ParentElement for TitleBar {
970 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
971 self.children.extend(elements)
972 }
973}