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