sign_in.rs

  1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
  2use gpui::{
  3    elements::*,
  4    geometry::rect::RectF,
  5    platform::{WindowBounds, WindowKind, WindowOptions},
  6    AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
  7    WindowHandle,
  8};
  9use theme::ui::modal;
 10
 11#[derive(PartialEq, Eq, Debug, Clone)]
 12struct CopyUserCode;
 13
 14#[derive(PartialEq, Eq, Debug, Clone)]
 15struct OpenGithub;
 16
 17const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 18
 19pub fn init(cx: &mut AppContext) {
 20    if let Some(copilot) = Copilot::global(cx) {
 21        let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
 22        cx.observe(&copilot, move |copilot, cx| {
 23            let status = copilot.read(cx).status();
 24
 25            match &status {
 26                crate::Status::SigningIn { prompt } => {
 27                    if let Some(window) = verification_window.as_mut() {
 28                        let updated = window
 29                            .root(cx)
 30                            .map(|root| {
 31                                root.update(cx, |verification, cx| {
 32                                    verification.set_status(status.clone(), cx);
 33                                    cx.activate_window();
 34                                })
 35                            })
 36                            .is_some();
 37                        if !updated {
 38                            verification_window = Some(create_copilot_auth_window(cx, &status));
 39                        }
 40                    } else if let Some(_prompt) = prompt {
 41                        verification_window = Some(create_copilot_auth_window(cx, &status));
 42                    }
 43                }
 44                Status::Authorized | Status::Unauthorized => {
 45                    if let Some(window) = verification_window.as_ref() {
 46                        if let Some(verification) = window.root(cx) {
 47                            verification.update(cx, |verification, cx| {
 48                                verification.set_status(status, cx);
 49                                cx.platform().activate(true);
 50                                cx.activate_window();
 51                            });
 52                        }
 53                    }
 54                }
 55                _ => {
 56                    if let Some(code_verification) = verification_window.take() {
 57                        code_verification.update(cx, |cx| cx.remove_window());
 58                    }
 59                }
 60            }
 61        })
 62        .detach();
 63    }
 64}
 65
 66fn create_copilot_auth_window(
 67    cx: &mut AppContext,
 68    status: &Status,
 69) -> WindowHandle<CopilotCodeVerification> {
 70    let window_size = theme::current(cx).copilot.modal.dimensions();
 71    let window_options = WindowOptions {
 72        bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
 73        titlebar: None,
 74        center: true,
 75        focus: true,
 76        show: true,
 77        kind: WindowKind::Normal,
 78        is_movable: true,
 79        screen: None,
 80    };
 81    cx.add_window(window_options, |_cx| {
 82        CopilotCodeVerification::new(status.clone())
 83    })
 84}
 85
 86pub struct CopilotCodeVerification {
 87    status: Status,
 88    connect_clicked: bool,
 89}
 90
 91impl CopilotCodeVerification {
 92    pub fn new(status: Status) -> Self {
 93        Self {
 94            status,
 95            connect_clicked: false,
 96        }
 97    }
 98
 99    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
100        self.status = status;
101        cx.notify();
102    }
103
104    fn render_device_code(
105        data: &PromptUserDeviceFlow,
106        style: &theme::Copilot,
107        cx: &mut ViewContext<Self>,
108    ) -> impl Element<Self> {
109        let copied = cx
110            .read_from_clipboard()
111            .map(|item| item.text() == &data.user_code)
112            .unwrap_or(false);
113
114        let device_code_style = &style.auth.prompting.device_code;
115
116        MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
117            Flex::row()
118                .with_child(
119                    Label::new(data.user_code.clone(), device_code_style.text.clone())
120                        .aligned()
121                        .contained()
122                        .with_style(device_code_style.left_container)
123                        .constrained()
124                        .with_width(device_code_style.left),
125                )
126                .with_child(
127                    Label::new(
128                        if copied { "Copied!" } else { "Copy" },
129                        device_code_style.cta.style_for(state).text.clone(),
130                    )
131                    .aligned()
132                    .contained()
133                    .with_style(*device_code_style.right_container.style_for(state))
134                    .constrained()
135                    .with_width(device_code_style.right),
136                )
137                .contained()
138                .with_style(device_code_style.cta.style_for(state).container)
139        })
140        .on_click(gpui::platform::MouseButton::Left, {
141            let user_code = data.user_code.clone();
142            move |_, _, cx| {
143                cx.platform()
144                    .write_to_clipboard(ClipboardItem::new(user_code.clone()));
145                cx.notify();
146            }
147        })
148        .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
149    }
150
151    fn render_prompting_modal(
152        connect_clicked: bool,
153        data: &PromptUserDeviceFlow,
154        style: &theme::Copilot,
155        cx: &mut ViewContext<Self>,
156    ) -> AnyElement<Self> {
157        enum ConnectButton {}
158
159        Flex::column()
160            .with_child(
161                Flex::column()
162                    .with_children([
163                        Label::new(
164                            "Enable Copilot by connecting",
165                            style.auth.prompting.subheading.text.clone(),
166                        )
167                        .aligned(),
168                        Label::new(
169                            "your existing license.",
170                            style.auth.prompting.subheading.text.clone(),
171                        )
172                        .aligned(),
173                    ])
174                    .align_children_center()
175                    .contained()
176                    .with_style(style.auth.prompting.subheading.container),
177            )
178            .with_child(Self::render_device_code(data, &style, cx))
179            .with_child(
180                Flex::column()
181                    .with_children([
182                        Label::new(
183                            "Paste this code into GitHub after",
184                            style.auth.prompting.hint.text.clone(),
185                        )
186                        .aligned(),
187                        Label::new(
188                            "clicking the button below.",
189                            style.auth.prompting.hint.text.clone(),
190                        )
191                        .aligned(),
192                    ])
193                    .align_children_center()
194                    .contained()
195                    .with_style(style.auth.prompting.hint.container.clone()),
196            )
197            .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
198                if connect_clicked {
199                    "Waiting for connection..."
200                } else {
201                    "Connect to GitHub"
202                },
203                style.auth.content_width,
204                &style.auth.cta_button,
205                cx,
206                {
207                    let verification_uri = data.verification_uri.clone();
208                    move |_, verification, cx| {
209                        cx.platform().open_url(&verification_uri);
210                        verification.connect_clicked = true;
211                    }
212                },
213            ))
214            .align_children_center()
215            .into_any()
216    }
217
218    fn render_enabled_modal(
219        style: &theme::Copilot,
220        cx: &mut ViewContext<Self>,
221    ) -> AnyElement<Self> {
222        enum DoneButton {}
223
224        let enabled_style = &style.auth.authorized;
225        Flex::column()
226            .with_child(
227                Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
228                    .contained()
229                    .with_style(enabled_style.subheading.container)
230                    .aligned(),
231            )
232            .with_child(
233                Flex::column()
234                    .with_children([
235                        Label::new(
236                            "You can update your settings or",
237                            enabled_style.hint.text.clone(),
238                        )
239                        .aligned(),
240                        Label::new(
241                            "sign out from the Copilot menu in",
242                            enabled_style.hint.text.clone(),
243                        )
244                        .aligned(),
245                        Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
246                    ])
247                    .align_children_center()
248                    .contained()
249                    .with_style(enabled_style.hint.container),
250            )
251            .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
252                "Done",
253                style.auth.content_width,
254                &style.auth.cta_button,
255                cx,
256                |_, _, cx| cx.remove_window(),
257            ))
258            .align_children_center()
259            .into_any()
260    }
261
262    fn render_unauthorized_modal(
263        style: &theme::Copilot,
264        cx: &mut ViewContext<Self>,
265    ) -> AnyElement<Self> {
266        let unauthorized_style = &style.auth.not_authorized;
267
268        Flex::column()
269            .with_child(
270                Flex::column()
271                    .with_children([
272                        Label::new(
273                            "Enable Copilot by connecting",
274                            unauthorized_style.subheading.text.clone(),
275                        )
276                        .aligned(),
277                        Label::new(
278                            "your existing license.",
279                            unauthorized_style.subheading.text.clone(),
280                        )
281                        .aligned(),
282                    ])
283                    .align_children_center()
284                    .contained()
285                    .with_style(unauthorized_style.subheading.container),
286            )
287            .with_child(
288                Flex::column()
289                    .with_children([
290                        Label::new(
291                            "You must have an active copilot",
292                            unauthorized_style.warning.text.clone(),
293                        )
294                        .aligned(),
295                        Label::new(
296                            "license to use it in Zed.",
297                            unauthorized_style.warning.text.clone(),
298                        )
299                        .aligned(),
300                    ])
301                    .align_children_center()
302                    .contained()
303                    .with_style(unauthorized_style.warning.container),
304            )
305            .with_child(theme::ui::cta_button::<Self, _, _, _>(
306                "Subscribe on GitHub",
307                style.auth.content_width,
308                &style.auth.cta_button,
309                cx,
310                |_, _, cx| {
311                    cx.remove_window();
312                    cx.platform().open_url(COPILOT_SIGN_UP_URL)
313                },
314            ))
315            .align_children_center()
316            .into_any()
317    }
318}
319
320impl Entity for CopilotCodeVerification {
321    type Event = ();
322}
323
324impl View for CopilotCodeVerification {
325    fn ui_name() -> &'static str {
326        "CopilotCodeVerification"
327    }
328
329    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
330        cx.notify()
331    }
332
333    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
334        cx.notify()
335    }
336
337    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
338        enum ConnectModal {}
339
340        let style = theme::current(cx).clone();
341
342        modal::<ConnectModal, _, _, _, _>(
343            "Connect Copilot to Zed",
344            &style.copilot.modal,
345            cx,
346            |cx| {
347                Flex::column()
348                    .with_children([
349                        theme::ui::icon(&style.copilot.auth.header).into_any(),
350                        match &self.status {
351                            Status::SigningIn {
352                                prompt: Some(prompt),
353                            } => Self::render_prompting_modal(
354                                self.connect_clicked,
355                                &prompt,
356                                &style.copilot,
357                                cx,
358                            ),
359                            Status::Unauthorized => {
360                                self.connect_clicked = false;
361                                Self::render_unauthorized_modal(&style.copilot, cx)
362                            }
363                            Status::Authorized => {
364                                self.connect_clicked = false;
365                                Self::render_enabled_modal(&style.copilot, cx)
366                            }
367                            _ => Empty::new().into_any(),
368                        },
369                    ])
370                    .align_children_center()
371            },
372        )
373        .into_any()
374    }
375}