sign_in.rs

  1use crate::{request::PromptUserDeviceFlow, Copilot, Status};
  2use gpui::{
  3    elements::*, geometry::rect::RectF, ClipboardItem, Element, Entity, MutableAppContext, View,
  4    ViewContext, ViewHandle, WindowKind, WindowOptions,
  5};
  6use settings::Settings;
  7
  8#[derive(PartialEq, Eq, Debug, Clone)]
  9struct CopyUserCode;
 10
 11#[derive(PartialEq, Eq, Debug, Clone)]
 12struct OpenGithub;
 13
 14const _COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
 15
 16pub fn init(cx: &mut MutableAppContext) {
 17    let copilot = Copilot::global(cx).unwrap();
 18
 19    let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
 20    cx.observe(&copilot, move |copilot, cx| {
 21        let status = copilot.read(cx).status();
 22
 23        match &status {
 24            crate::Status::SigningIn { prompt } => {
 25                if let Some(code_verification) = code_verification.as_ref() {
 26                    code_verification.update(cx, |code_verification, cx| {
 27                        code_verification.set_status(status, cx)
 28                    });
 29                    cx.activate_window(code_verification.window_id());
 30                } else if let Some(_prompt) = prompt {
 31                    let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
 32                    let window_options = WindowOptions {
 33                        bounds: gpui::WindowBounds::Fixed(RectF::new(
 34                            Default::default(),
 35                            window_size,
 36                        )),
 37                        titlebar: None,
 38                        center: true,
 39                        focus: true,
 40                        kind: WindowKind::Normal,
 41                        is_movable: true,
 42                        screen: None,
 43                    };
 44                    let (_, view) =
 45                        cx.add_window(window_options, |_cx| CopilotCodeVerification::new(status));
 46                    code_verification = Some(view);
 47                }
 48            }
 49            Status::Authorized | Status::Unauthorized => {
 50                if let Some(code_verification) = code_verification.as_ref() {
 51                    code_verification.update(cx, |code_verification, cx| {
 52                        code_verification.set_status(status, cx)
 53                    });
 54
 55                    cx.platform().activate(true);
 56                    cx.activate_window(code_verification.window_id());
 57                }
 58            }
 59            _ => {
 60                if let Some(code_verification) = code_verification.take() {
 61                    cx.remove_window(code_verification.window_id());
 62                }
 63            }
 64        }
 65    })
 66    .detach();
 67
 68    // Modal theming test:
 69    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
 70    // let window_options = WindowOptions {
 71    //     bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
 72    //     titlebar: None,
 73    //     center: false,
 74    //     focus: false,
 75    //     kind: WindowKind::PopUp,
 76    //     is_movable: true,
 77    //     screen: None,
 78    // };
 79    // let (_, _view) = cx.add_window(window_options, |_cx| {
 80    //     CopilotCodeVerification::new(Status::SigningIn {
 81    //         prompt: Some(PromptUserDeviceFlow {
 82    //             user_code: "ABCD-1234".to_string(),
 83    //             verification_uri: "https://github.com/login/device".to_string(),
 84    //         }),
 85    //     })
 86    // });
 87
 88    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
 89    // let window_options = WindowOptions {
 90    //     bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(window_size.x(), 0.), window_size)),
 91    //     titlebar: None,
 92    //     center: false,
 93    //     focus: false,
 94    //     kind: WindowKind::PopUp,
 95    //     is_movable: true,
 96    //     screen: None,
 97    // };
 98    // let (_, _view) = cx.add_window(window_options, |_cx| {
 99    //     CopilotCodeVerification::new(Status::Authorized)
100    // });
101
102    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
103    // let window_options = WindowOptions {
104    //     bounds: gpui::WindowBounds::Fixed(RectF::new(vec2f(0., window_size.y()), window_size)),
105    //     titlebar: None,
106    //     center: false,
107    //     focus: false,
108    //     kind: WindowKind::PopUp,
109    //     is_movable: true,
110    //     screen: None,
111    // };
112    // let (_, _view) = cx.add_window(window_options, |_cx| {
113    //     CopilotCodeVerification::new(Status::Unauthorized)
114    // });
115}
116
117pub struct CopilotCodeVerification {
118    status: Status,
119}
120
121impl CopilotCodeVerification {
122    pub fn new(status: Status) -> Self {
123        Self { status }
124    }
125
126    pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
127        self.status = status;
128        cx.notify();
129    }
130
131    fn render_device_code(
132        data: &PromptUserDeviceFlow,
133        style: &theme::Copilot,
134        cx: &mut gpui::RenderContext<Self>,
135    ) -> ElementBox {
136        let copied = cx
137            .read_from_clipboard()
138            .map(|item| item.text() == &data.user_code)
139            .unwrap_or(false);
140
141        Flex::column()
142            .with_children([
143                MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
144                    Flex::row()
145                        .with_children([
146                            Label::new(data.user_code.clone(), style.auth.device_code.clone())
147                                .aligned()
148                                .contained()
149                                .with_style(style.auth.device_code_left_container)
150                                .constrained()
151                                .with_width(style.auth.device_code_left)
152                                .boxed(),
153                            Empty::new()
154                                .constrained()
155                                .with_width(1.)
156                                .with_height(style.auth.device_code_seperator_height)
157                                .contained()
158                                .with_background_color(
159                                    style
160                                        .auth
161                                        .cta_button
162                                        .style_for(state, false)
163                                        .container
164                                        .border
165                                        .color,
166                                )
167                                .boxed(),
168                            Label::new(
169                                if copied { "Copied!" } else { "Copy" },
170                                style.auth.cta_button.style_for(state, false).text.clone(),
171                            )
172                            .aligned()
173                            .contained()
174                            .with_style(style.auth.device_code_right_container)
175                            .constrained()
176                            .with_width(style.auth.device_code_right)
177                            .boxed(),
178                        ])
179                        .contained()
180                        .with_style(style.auth.device_code_cta.style_for(state, false).container)
181                        .constrained()
182                        .with_width(style.auth.content_width)
183                        .boxed()
184                })
185                .on_click(gpui::MouseButton::Left, {
186                    let user_code = data.user_code.clone();
187                    move |_, cx| {
188                        cx.platform()
189                            .write_to_clipboard(ClipboardItem::new(user_code.clone()));
190                        cx.notify();
191                    }
192                })
193                .with_cursor_style(gpui::CursorStyle::PointingHand)
194                .boxed(),
195                Flex::column()
196                    .with_children([
197                        Label::new(
198                            "Paste this code into GitHub after",
199                            style.auth.hint.text.clone(),
200                        )
201                        .boxed(),
202                        Label::new("clicking the button below.", style.auth.hint.text.clone())
203                            .boxed(),
204                    ])
205                    .align_children_center()
206                    .contained()
207                    .with_style(style.auth.hint.container.clone())
208                    .boxed(),
209            ])
210            .align_children_center()
211            .contained()
212            .with_style(style.auth.device_code_group)
213            .aligned()
214            .boxed()
215    }
216
217    fn render_not_authorized_warning(style: &theme::Copilot) -> ElementBox {
218        Flex::column()
219            .with_children([
220                Flex::column()
221                    .with_children([
222                        Label::new(
223                            "You must have an active copilot",
224                            style.auth.warning.text.to_owned(),
225                        )
226                        .aligned()
227                        .boxed(),
228                        Label::new(
229                            "license to use it in Zed.",
230                            style.auth.warning.text.to_owned(),
231                        )
232                        .aligned()
233                        .boxed(),
234                    ])
235                    .align_children_center()
236                    .contained()
237                    .with_style(style.auth.warning.container)
238                    .boxed(),
239                Flex::column()
240                    .with_children([
241                        Label::new(
242                            "Try connecting again once you",
243                            style.auth.hint.text.to_owned(),
244                        )
245                        .aligned()
246                        .boxed(),
247                        Label::new(
248                            "have activated a Copilot license.",
249                            style.auth.hint.text.to_owned(),
250                        )
251                        .aligned()
252                        .boxed(),
253                    ])
254                    .align_children_center()
255                    .contained()
256                    .with_style(style.auth.not_authorized_hint)
257                    .boxed(),
258            ])
259            .align_children_center()
260            .boxed()
261    }
262
263    fn render_copilot_enabled(style: &theme::Copilot) -> ElementBox {
264        Flex::column()
265            .with_children([
266                Label::new(
267                    "You can update your settings or",
268                    style.auth.hint.text.clone(),
269                )
270                .aligned()
271                .boxed(),
272                Label::new(
273                    "sign out from the Copilot menu in",
274                    style.auth.hint.text.clone(),
275                )
276                .aligned()
277                .boxed(),
278                Label::new("the status bar.", style.auth.hint.text.clone())
279                    .aligned()
280                    .boxed(),
281            ])
282            .align_children_center()
283            .contained()
284            .with_style(style.auth.enabled_hint)
285            .boxed()
286    }
287
288    fn render_prompting_modal(
289        data: &PromptUserDeviceFlow,
290        style: &theme::Copilot,
291        cx: &mut gpui::RenderContext<Self>,
292    ) -> ElementBox {
293        theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
294            Flex::column()
295                .with_children([
296                    Flex::column()
297                        .with_children([
298                            Flex::row()
299                                .with_children([
300                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
301                                    theme::ui::icon(&style.auth.plus_icon).boxed(),
302                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
303                                ])
304                                .boxed(),
305                            Flex::column()
306                                .with_children([
307                                    Label::new(
308                                        "Enable Copilot by connecting",
309                                        style.auth.enable_text.clone(),
310                                    )
311                                    .boxed(),
312                                    Label::new(
313                                        "your existing license.",
314                                        style.auth.enable_text.clone(),
315                                    )
316                                    .boxed(),
317                                ])
318                                .align_children_center()
319                                .contained()
320                                .with_style(style.auth.enable_group.clone())
321                                .boxed(),
322                        ])
323                        .align_children_center()
324                        .contained()
325                        .with_style(style.auth.header_group)
326                        .aligned()
327                        .boxed(),
328                    Self::render_device_code(data, &style, cx),
329                    // match &self.prompt {
330                    //     SignInContents::PromptingUser(data) => {
331
332                    //     }
333                    //     SignInContents::Unauthorized => Self::render_not_authorized_warning(&style),
334                    //     SignInContents::Enabled => Self::render_copilot_enabled(&style),
335                    // },
336                    Flex::column()
337                        .with_child(
338                            theme::ui::cta_button_with_click(
339                                "Connect to GitHub",
340                                style.auth.content_width,
341                                &style.auth.cta_button,
342                                cx,
343                                {
344                                    let verification_uri = data.verification_uri.clone();
345                                    move |_, cx| cx.platform().open_url(&verification_uri)
346                                },
347                            ),
348                            // {
349                            // match &self.prompt {
350                            //     SignInContents::PromptingUser(data) => {
351
352                            //     }
353                            //     // SignInContents::Unauthorized => theme::ui::cta_button_with_click(
354                            //     //     "Close",
355                            //     //     style.auth.content_width,
356                            //     //     &style.auth.cta_button,
357                            //     //     cx,
358                            //     //     |_, cx| {
359                            //     //         let window_id = cx.window_id();
360                            //     //         cx.remove_window(window_id)
361                            //     //     },
362                            //     // ),
363                            //     // SignInContents::Enabled => theme::ui::cta_button_with_click(
364                            //     //     "Done",
365                            //     //     style.auth.content_width,
366                            //     //     &style.auth.cta_button,
367                            //     //     cx,
368                            //     //     |_, cx| {
369                            //     //         let window_id = cx.window_id();
370                            //     //         cx.remove_window(window_id)
371                            //     //     },
372                            //     // ),
373                            // }
374                        )
375                        .align_children_center()
376                        .contained()
377                        .with_style(style.auth.github_group)
378                        .aligned()
379                        .boxed(),
380                ])
381                .align_children_center()
382                .constrained()
383                .with_width(style.auth.content_width)
384                .aligned()
385                .boxed()
386        })
387    }
388    fn render_enabled_modal(
389        style: &theme::Copilot,
390        cx: &mut gpui::RenderContext<Self>,
391    ) -> ElementBox {
392        theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
393            Flex::column()
394                .with_children([
395                    Flex::column()
396                        .with_children([
397                            Flex::row()
398                                .with_children([
399                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
400                                    theme::ui::icon(&style.auth.plus_icon).boxed(),
401                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
402                                ])
403                                .boxed(),
404                            Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
405                        ])
406                        .align_children_center()
407                        .contained()
408                        .with_style(style.auth.header_group)
409                        .aligned()
410                        .boxed(),
411                    Self::render_copilot_enabled(&style),
412                    Flex::column()
413                        .with_child(theme::ui::cta_button_with_click(
414                            "Close",
415                            style.auth.content_width,
416                            &style.auth.cta_button,
417                            cx,
418                            |_, cx| {
419                                let window_id = cx.window_id();
420                                cx.remove_window(window_id)
421                            },
422                        ))
423                        .align_children_center()
424                        .contained()
425                        .with_style(style.auth.github_group)
426                        .aligned()
427                        .boxed(),
428                ])
429                .align_children_center()
430                .constrained()
431                .with_width(style.auth.content_width)
432                .aligned()
433                .boxed()
434        })
435    }
436    fn render_unauthorized_modal(
437        style: &theme::Copilot,
438        cx: &mut gpui::RenderContext<Self>,
439    ) -> ElementBox {
440        theme::ui::modal("Connect Copilot to Zed", &style.modal, cx, |cx| {
441            Flex::column()
442                .with_children([
443                    Flex::column()
444                        .with_children([
445                            Flex::row()
446                                .with_children([
447                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
448                                    theme::ui::icon(&style.auth.plus_icon).boxed(),
449                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
450                                ])
451                                .boxed(),
452                            Flex::column()
453                                .with_children([
454                                    Label::new(
455                                        "Enable Copilot by connecting",
456                                        style.auth.enable_text.clone(),
457                                    )
458                                    .boxed(),
459                                    Label::new(
460                                        "your existing license.",
461                                        style.auth.enable_text.clone(),
462                                    )
463                                    .boxed(),
464                                ])
465                                .align_children_center()
466                                .contained()
467                                .with_style(style.auth.enable_group.clone())
468                                .boxed(),
469                        ])
470                        .align_children_center()
471                        .contained()
472                        .with_style(style.auth.header_group)
473                        .aligned()
474                        .boxed(),
475                    Self::render_not_authorized_warning(&style),
476                    Flex::column()
477                        .with_child(theme::ui::cta_button_with_click(
478                            "Close",
479                            style.auth.content_width,
480                            &style.auth.cta_button,
481                            cx,
482                            |_, cx| {
483                                let window_id = cx.window_id();
484                                cx.remove_window(window_id)
485                            },
486                        ))
487                        .align_children_center()
488                        .contained()
489                        .with_style(style.auth.github_group)
490                        .aligned()
491                        .boxed(),
492                ])
493                .align_children_center()
494                .constrained()
495                .with_width(style.auth.content_width)
496                .aligned()
497                .boxed()
498        })
499    }
500}
501
502impl Entity for CopilotCodeVerification {
503    type Event = ();
504}
505
506impl View for CopilotCodeVerification {
507    fn ui_name() -> &'static str {
508        "CopilotCodeVerification"
509    }
510
511    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
512        cx.notify()
513    }
514
515    fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
516        cx.notify()
517    }
518
519    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
520        let style = cx.global::<Settings>().theme.copilot.clone();
521        match &self.status {
522            Status::SigningIn {
523                prompt: Some(prompt),
524            } => Self::render_prompting_modal(&prompt, &style, cx),
525            Status::Unauthorized => Self::render_unauthorized_modal(&style, cx),
526            Status::Authorized => Self::render_enabled_modal(&style, cx),
527            _ => Empty::new().boxed(),
528        }
529    }
530}