onboarding.rs

  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}