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