sign_in.rs

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