modal.rs

  1use std::{sync::Arc, time::Duration};
  2
  3use client::{Client, UserStore};
  4use feature_flags::FeatureFlagAppExt as _;
  5use fs::Fs;
  6use gpui::{
  7    ease_in_out, svg, Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter,
  8    FocusHandle, Focusable, MouseDownEvent, Render,
  9};
 10use language::language_settings::{AllLanguageSettings, InlineCompletionProvider};
 11use settings::{update_settings_file, Settings};
 12use ui::{prelude::*, CheckboxWithLabel, TintColor};
 13use workspace::{notifications::NotifyTaskExt, ModalView, Workspace};
 14
 15/// Introduces user to AI inline prediction feature and terms of service
 16pub struct ZedPredictModal {
 17    user_store: Entity<UserStore>,
 18    client: Arc<Client>,
 19    fs: Arc<dyn Fs>,
 20    focus_handle: FocusHandle,
 21    sign_in_status: SignInStatus,
 22    terms_of_service: bool,
 23}
 24
 25#[derive(PartialEq, Eq)]
 26enum SignInStatus {
 27    /// Signed out or signed in but not from this modal
 28    Idle,
 29    /// Authentication triggered from this modal
 30    Waiting,
 31    /// Signed in after authentication from this modal
 32    SignedIn,
 33}
 34
 35impl ZedPredictModal {
 36    fn new(
 37        user_store: Entity<UserStore>,
 38        client: Arc<Client>,
 39        fs: Arc<dyn Fs>,
 40        cx: &mut Context<Self>,
 41    ) -> Self {
 42        ZedPredictModal {
 43            user_store,
 44            client,
 45            fs,
 46            focus_handle: cx.focus_handle(),
 47            sign_in_status: SignInStatus::Idle,
 48            terms_of_service: false,
 49        }
 50    }
 51
 52    pub fn toggle(
 53        workspace: Entity<Workspace>,
 54        user_store: Entity<UserStore>,
 55        client: Arc<Client>,
 56        fs: Arc<dyn Fs>,
 57        window: &mut Window,
 58        cx: &mut App,
 59    ) {
 60        workspace.update(cx, |this, cx| {
 61            this.toggle_modal(window, cx, |_window, cx| {
 62                ZedPredictModal::new(user_store, client, fs, cx)
 63            });
 64        });
 65    }
 66
 67    fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
 68        cx.open_url("https://zed.dev/terms-of-service");
 69        cx.notify();
 70    }
 71
 72    fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
 73        cx.open_url("https://zed.dev/blog/"); // TODO Add the link when live
 74        cx.notify();
 75    }
 76
 77    fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 78        let task = self
 79            .user_store
 80            .update(cx, |this, cx| this.accept_terms_of_service(cx));
 81
 82        cx.spawn(|this, mut cx| async move {
 83            task.await?;
 84
 85            this.update(&mut cx, |this, cx| {
 86                update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| {
 87                    file.features
 88                        .get_or_insert(Default::default())
 89                        .inline_completion_provider = Some(InlineCompletionProvider::Zed);
 90                });
 91
 92                cx.emit(DismissEvent);
 93            })
 94        })
 95        .detach_and_notify_err(window, cx);
 96    }
 97
 98    fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
 99        let client = self.client.clone();
100        self.sign_in_status = SignInStatus::Waiting;
101
102        cx.spawn(move |this, mut cx| async move {
103            let result = client.authenticate_and_connect(true, &cx).await;
104
105            let status = match result {
106                Ok(_) => SignInStatus::SignedIn,
107                Err(_) => SignInStatus::Idle,
108            };
109
110            this.update(&mut cx, |this, cx| {
111                this.sign_in_status = status;
112                cx.notify()
113            })?;
114
115            result
116        })
117        .detach_and_notify_err(window, cx);
118    }
119
120    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
121        cx.emit(DismissEvent);
122    }
123}
124
125impl EventEmitter<DismissEvent> for ZedPredictModal {}
126
127impl Focusable for ZedPredictModal {
128    fn focus_handle(&self, _cx: &App) -> FocusHandle {
129        self.focus_handle.clone()
130    }
131}
132
133impl ModalView for ZedPredictModal {}
134
135impl Render for ZedPredictModal {
136    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
137        let base = v_flex()
138            .w(px(420.))
139            .p_4()
140            .relative()
141            .gap_2()
142            .overflow_hidden()
143            .elevation_3(cx)
144            .id("zed predict tos")
145            .track_focus(&self.focus_handle(cx))
146            .on_action(cx.listener(Self::cancel))
147            .key_context("ZedPredictModal")
148            .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
149                cx.emit(DismissEvent);
150            }))
151            .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
152                this.focus_handle.focus(window);
153            }))
154            .child(
155                div()
156                    .p_1p5()
157                    .absolute()
158                    .top_0()
159                    .left_0()
160                    .right_0()
161                    .h(px(200.))
162                    .child(
163                        svg()
164                            .path("icons/zed_predict_bg.svg")
165                            .text_color(cx.theme().colors().icon_disabled)
166                            .w(px(416.))
167                            .h(px(128.))
168                            .overflow_hidden(),
169                    ),
170            )
171            .child(
172                h_flex()
173                    .w_full()
174                    .mb_2()
175                    .justify_between()
176                    .child(
177                        v_flex()
178                            .gap_1()
179                            .child(
180                                Label::new("Introducing Zed AI's")
181                                    .size(LabelSize::Small)
182                                    .color(Color::Muted),
183                            )
184                            .child(Headline::new("Edit Prediction").size(HeadlineSize::Large)),
185                    )
186                    .child({
187                        let tab = |n: usize| {
188                            let text_color = cx.theme().colors().text;
189                            let border_color = cx.theme().colors().text_accent.opacity(0.4);
190
191                            h_flex().child(
192                                h_flex()
193                                    .px_4()
194                                    .py_0p5()
195                                    .bg(cx.theme().colors().editor_background)
196                                    .border_1()
197                                    .border_color(border_color)
198                                    .rounded_md()
199                                    .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
200                                    .text_size(TextSize::XSmall.rems(cx))
201                                    .text_color(text_color)
202                                    .child("tab")
203                                    .with_animation(
204                                        ElementId::Integer(n),
205                                        Animation::new(Duration::from_secs(2)).repeat(),
206                                        move |tab, delta| {
207                                            let delta = (delta - 0.15 * n as f32) / 0.7;
208                                            let delta = 1.0 - (0.5 - delta).abs() * 2.;
209                                            let delta = ease_in_out(delta.clamp(0., 1.));
210                                            let delta = 0.1 + 0.9 * delta;
211
212                                            tab.border_color(border_color.opacity(delta))
213                                                .text_color(text_color.opacity(delta))
214                                        },
215                                    ),
216                            )
217                        };
218
219                        v_flex()
220                            .gap_2()
221                            .items_center()
222                            .pr_4()
223                            .child(tab(0).ml_neg_20())
224                            .child(tab(1))
225                            .child(tab(2).ml_20())
226                    }),
227            )
228            .child(h_flex().absolute().top_2().right_2().child(
229                IconButton::new("cancel", IconName::X).on_click(cx.listener(
230                    |_, _: &ClickEvent, _window, cx| {
231                        cx.emit(DismissEvent);
232                    },
233                )),
234            ));
235
236        let blog_post_button = if cx.is_staff() {
237            Some(
238                Button::new("view-blog", "Read the Blog Post")
239                    .full_width()
240                    .icon(IconName::ArrowUpRight)
241                    .icon_size(IconSize::Indicator)
242                    .icon_color(Color::Muted)
243                    .on_click(cx.listener(Self::view_blog)),
244            )
245        } else {
246            // TODO: put back when blog post is published
247            None
248        };
249
250        if self.user_store.read(cx).current_user().is_some() {
251            let copy = match self.sign_in_status {
252                SignInStatus::Idle => "Get accurate and helpful edit predictions at every keystroke. To set Zed as your inline completions provider, ensure you:",
253                SignInStatus::SignedIn => "Almost there! Ensure you:",
254                SignInStatus::Waiting => unreachable!(),
255            };
256
257            base.child(Label::new(copy).color(Color::Muted))
258                .child(
259                    h_flex()
260                        .gap_0p5()
261                        .child(CheckboxWithLabel::new(
262                            "tos-checkbox",
263                            Label::new("Have read and accepted the").color(Color::Muted),
264                            self.terms_of_service.into(),
265                            cx.listener(move |this, state, _window, cx| {
266                                this.terms_of_service = *state == ToggleState::Selected;
267                                cx.notify()
268                            }),
269                        ))
270                        .child(
271                            Button::new("view-tos", "Terms of Service")
272                                .icon(IconName::ArrowUpRight)
273                                .icon_size(IconSize::Indicator)
274                                .icon_color(Color::Muted)
275                                .on_click(cx.listener(Self::view_terms)),
276                        ),
277                )
278                .child(
279                    v_flex()
280                        .mt_2()
281                        .gap_2()
282                        .w_full()
283                        .child(
284                            Button::new("accept-tos", "Enable Edit Predictions")
285                                .disabled(!self.terms_of_service)
286                                .style(ButtonStyle::Tinted(TintColor::Accent))
287                                .full_width()
288                                .on_click(cx.listener(Self::accept_and_enable)),
289                        )
290                        .children(blog_post_button),
291                )
292        } else {
293            base.child(
294                Label::new("To set Zed as your inline completions provider, please sign in.")
295                    .color(Color::Muted),
296            )
297            .child(
298                v_flex()
299                    .mt_2()
300                    .gap_2()
301                    .w_full()
302                    .child(
303                        Button::new("accept-tos", "Sign in with GitHub")
304                            .disabled(self.sign_in_status == SignInStatus::Waiting)
305                            .style(ButtonStyle::Tinted(TintColor::Accent))
306                            .full_width()
307                            .on_click(cx.listener(Self::sign_in)),
308                    )
309                    .children(blog_post_button),
310            )
311        }
312    }
313}