1use gpui::{
2 svg, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global,
3 MouseDownEvent, Render,
4};
5use ui::{prelude::*, ButtonLike, TintColor, Tooltip};
6use util::ResultExt;
7use workspace::{ModalView, Workspace};
8
9use crate::git_panel::GitPanel;
10
11macro_rules! git_onboarding_event {
12 ($name:expr) => {
13 telemetry::event!($name, source = "Git Onboarding");
14 };
15 ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
16 telemetry::event!($name, source = "Git Onboarding", $($key $(= $value)?),+);
17 };
18}
19
20/// Introduces user to the Git Panel and overall improved Git support
21pub struct GitOnboardingModal {
22 focus_handle: FocusHandle,
23 workspace: Entity<Workspace>,
24}
25
26impl GitOnboardingModal {
27 pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
28 let workspace_entity = cx.entity();
29 workspace.toggle_modal(window, cx, |_window, cx| Self {
30 workspace: workspace_entity,
31 focus_handle: cx.focus_handle(),
32 });
33 }
34
35 fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
36 self.workspace.update(cx, |workspace, cx| {
37 workspace.focus_panel::<GitPanel>(window, cx);
38 });
39
40 cx.emit(DismissEvent);
41
42 git_onboarding_event!("Open Panel Clicked");
43 }
44
45 fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
46 cx.open_url("https://zed.dev/blog/git");
47 cx.notify();
48
49 git_onboarding_event!("Blog Link Clicked");
50 }
51
52 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
53 cx.emit(DismissEvent);
54 }
55}
56
57impl EventEmitter<DismissEvent> for GitOnboardingModal {}
58
59impl Focusable for GitOnboardingModal {
60 fn focus_handle(&self, _cx: &App) -> FocusHandle {
61 self.focus_handle.clone()
62 }
63}
64
65impl ModalView for GitOnboardingModal {}
66
67impl Render for GitOnboardingModal {
68 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
69 let window_height = window.viewport_size().height;
70 let max_height = window_height - px(200.);
71
72 let base = v_flex()
73 .id("git-onboarding")
74 .key_context("GitOnboardingModal")
75 .relative()
76 .w(px(450.))
77 .h_full()
78 .max_h(max_height)
79 .p_4()
80 .gap_2()
81 .elevation_3(cx)
82 .track_focus(&self.focus_handle(cx))
83 .overflow_hidden()
84 .on_action(cx.listener(Self::cancel))
85 .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
86 git_onboarding_event!("Cancelled", trigger = "Action");
87 cx.emit(DismissEvent);
88 }))
89 .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
90 this.focus_handle.focus(window);
91 }))
92 .child(
93 div().p_1p5().absolute().inset_0().h(px(160.)).child(
94 svg()
95 .path("icons/git_onboarding_bg.svg")
96 .text_color(cx.theme().colors().icon_disabled)
97 .w(px(420.))
98 .h(px(128.))
99 .overflow_hidden(),
100 ),
101 )
102 .child(
103 v_flex()
104 .w_full()
105 .gap_1()
106 .child(
107 Label::new("Introducing")
108 .size(LabelSize::Small)
109 .color(Color::Muted),
110 )
111 .child(Headline::new("Native Git Support").size(HeadlineSize::Large)),
112 )
113 .child(h_flex().absolute().top_2().right_2().child(
114 IconButton::new("cancel", IconName::X).on_click(cx.listener(
115 |_, _: &ClickEvent, _window, cx| {
116 git_onboarding_event!("Cancelled", trigger = "X click");
117 cx.emit(DismissEvent);
118 },
119 )),
120 ));
121
122 let open_panel_button = Button::new("open-panel", "Get Started with the Git Panel")
123 .icon_size(IconSize::Indicator)
124 .style(ButtonStyle::Tinted(TintColor::Accent))
125 .full_width()
126 .on_click(cx.listener(Self::open_panel));
127
128 let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
129 .icon(IconName::ArrowUpRight)
130 .icon_size(IconSize::Indicator)
131 .icon_color(Color::Muted)
132 .full_width()
133 .on_click(cx.listener(Self::view_blog));
134
135 let copy = "First-class support for staging, committing, pulling, pushing, viewing diffs, and more. All without leaving Zed.";
136
137 base.child(Label::new(copy).color(Color::Muted)).child(
138 v_flex()
139 .w_full()
140 .mt_2()
141 .gap_2()
142 .child(open_panel_button)
143 .child(blog_post_button),
144 )
145 }
146}
147
148/// Prompts the user to try Zed's git features
149pub struct GitBanner {
150 dismissed: bool,
151}
152
153#[derive(Clone)]
154struct GitBannerGlobal(Entity<GitBanner>);
155impl Global for GitBannerGlobal {}
156
157impl GitBanner {
158 pub fn new(cx: &mut Context<Self>) -> Self {
159 cx.set_global(GitBannerGlobal(cx.entity()));
160 Self {
161 dismissed: get_dismissed(),
162 }
163 }
164
165 fn should_show(&self, _cx: &mut App) -> bool {
166 !self.dismissed
167 }
168
169 fn dismiss(&mut self, cx: &mut Context<Self>) {
170 git_onboarding_event!("Banner Dismissed");
171 persist_dismissed(cx);
172 self.dismissed = true;
173 cx.notify();
174 }
175}
176
177const DISMISSED_AT_KEY: &str = "zed_git_banner_dismissed_at";
178
179fn get_dismissed() -> bool {
180 db::kvp::KEY_VALUE_STORE
181 .read_kvp(DISMISSED_AT_KEY)
182 .log_err()
183 .map_or(false, |dismissed| dismissed.is_some())
184}
185
186fn persist_dismissed(cx: &mut App) {
187 cx.spawn(async |_| {
188 let time = chrono::Utc::now().to_rfc3339();
189 db::kvp::KEY_VALUE_STORE
190 .write_kvp(DISMISSED_AT_KEY.into(), time)
191 .await
192 })
193 .detach_and_log_err(cx);
194}
195
196pub(crate) fn clear_dismissed(cx: &mut App) {
197 cx.defer(|cx| {
198 cx.global::<GitBannerGlobal>()
199 .clone()
200 .0
201 .update(cx, |this, cx| {
202 this.dismissed = false;
203 cx.notify();
204 });
205 });
206
207 cx.spawn(async |_| {
208 db::kvp::KEY_VALUE_STORE
209 .delete_kvp(DISMISSED_AT_KEY.into())
210 .await
211 })
212 .detach_and_log_err(cx);
213}
214
215impl Render for GitBanner {
216 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
217 if !self.should_show(cx) {
218 return div();
219 }
220
221 let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
222 let banner = h_flex()
223 .rounded_sm()
224 .border_1()
225 .border_color(border_color)
226 .child(
227 ButtonLike::new("try-git")
228 .child(
229 h_flex()
230 .h_full()
231 .items_center()
232 .gap_1()
233 .child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small))
234 .child(
235 h_flex()
236 .gap_0p5()
237 .child(
238 Label::new("Introducing:")
239 .size(LabelSize::Small)
240 .color(Color::Muted),
241 )
242 .child(Label::new("Git Support").size(LabelSize::Small)),
243 ),
244 )
245 .on_click(cx.listener(|this, _, window, cx| {
246 git_onboarding_event!("Banner Clicked");
247 this.dismiss(cx);
248 window.dispatch_action(
249 Box::new(zed_actions::OpenGitIntegrationOnboarding),
250 cx,
251 )
252 })),
253 )
254 .child(
255 div().border_l_1().border_color(border_color).child(
256 IconButton::new("close", IconName::Close)
257 .icon_size(IconSize::Indicator)
258 .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
259 .tooltip(|window, cx| {
260 Tooltip::with_meta(
261 "Close Announcement Banner",
262 None,
263 "It won't show again for this feature",
264 window,
265 cx,
266 )
267 }),
268 ),
269 );
270
271 div().pr_2().child(banner)
272 }
273}