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