sign_in.rs

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