Finish shape of copilot auth UI

Mikayla Maki created

Change summary

crates/copilot/src/copilot.rs   |   8 
crates/copilot/src/sign_in.rs   | 227 ++++++++++++++++++++++++----------
crates/theme/src/theme.rs       |  24 ++-
crates/theme/src/ui.rs          |  62 +++++++++
styles/src/styleTree/copilot.ts | 144 +++++++++++++++++----
5 files changed, 354 insertions(+), 111 deletions(-)

Detailed changes

crates/copilot/src/copilot.rs 🔗

@@ -51,10 +51,10 @@ enum CopilotServer {
 #[derive(Clone, Debug)]
 enum SignInStatus {
     Authorized {
-        user: String,
+        _user: String,
     },
     Unauthorized {
-        user: String,
+        _user: String,
     },
     SigningIn {
         prompt: Option<request::PromptUserDeviceFlow>,
@@ -321,10 +321,10 @@ impl Copilot {
         if let CopilotServer::Started { status, .. } = &mut self.server {
             *status = match lsp_status {
                 request::SignInStatus::Ok { user } | request::SignInStatus::MaybeOk { user } => {
-                    SignInStatus::Authorized { user }
+                    SignInStatus::Authorized { _user: user }
                 }
                 request::SignInStatus::NotAuthorized { user } => {
-                    SignInStatus::Unauthorized { user }
+                    SignInStatus::Unauthorized { _user: user }
                 }
                 _ => SignInStatus::SignedOut,
             };

crates/copilot/src/sign_in.rs 🔗

@@ -26,13 +26,7 @@ pub fn init(cx: &mut MutableAppContext) {
                     cx.remove_window(window_id);
                 }
 
-                let window_size = cx
-                    .global::<Settings>()
-                    .theme
-                    .copilot
-                    .auth
-                    .popup_dimensions
-                    .to_vec();
+                let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
 
                 let (window_id, _) = cx.add_window(
                     WindowOptions {
@@ -43,13 +37,15 @@ pub fn init(cx: &mut MutableAppContext) {
                         titlebar: None,
                         center: true,
                         focus: false,
-                        kind: WindowKind::PopUp,
+                        kind: WindowKind::Normal,
                         is_movable: true,
                         screen: None,
                     },
                     |_| CopilotCodeVerification::new(prompt),
                 );
                 code_verification_window_id = Some(window_id);
+
+                cx.activate_window(window_id);
             }
             _ => {
                 if let Some(window_id) = code_verification_window_id.take() {
@@ -59,6 +55,26 @@ pub fn init(cx: &mut MutableAppContext) {
         }
     })
     .detach();
+
+    // let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
+
+    // let (_window_id, _) = cx.add_window(
+    //     WindowOptions {
+    //         bounds: gpui::WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
+    //         titlebar: None,
+    //         center: true,
+    //         focus: false,
+    //         kind: WindowKind::PopUp,
+    //         is_movable: true,
+    //         screen: None,
+    //     },
+    //     |_| {
+    //         CopilotCodeVerification::new(PromptUserDeviceFlow {
+    //             user_code: "ABCD-1234".to_string(),
+    //             verification_uri: "https://github.com/login/device".to_string(),
+    //         })
+    //     },
+    // );
 }
 
 pub struct CopilotCodeVerification {
@@ -74,71 +90,146 @@ impl View for CopilotCodeVerification {
         "CopilotCodeVerification"
     }
 
+    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
+        cx.notify()
+    }
+
     fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
         let style = cx.global::<Settings>().theme.copilot.clone();
 
-        let instruction_text = style.auth.instruction_text;
-        let user_code_text = style.auth.user_code;
-        let button = style.auth.button;
-        let button_width = style.auth.button_width;
-        let height = style.auth.popup_dimensions.height;
-
-        let user_code = self.prompt.user_code.replace("-", " - ");
-
-        Flex::column()
-            .with_child(
-                MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
-                    let style = style.auth.close_icon.style_for(state, false);
-                    theme::ui::icon(style).boxed()
-                })
-                .on_click(gpui::MouseButton::Left, move |_, cx| {
-                    let window_id = cx.window_id();
-                    cx.remove_window(window_id);
-                })
-                .with_cursor_style(gpui::CursorStyle::PointingHand)
-                .aligned()
-                .right()
-                .boxed(),
-            )
-            .with_child(
-                Flex::column()
-                    .align_children_center()
-                    .with_children([
-                        theme::ui::svg(&style.auth.copilot_icon).boxed(),
-                        Label::new(
-                            "Here is your code to authenticate with github",
-                            instruction_text.clone(),
-                        )
+        let copied = cx
+            .read_from_clipboard()
+            .map(|item| item.text() == &self.prompt.user_code)
+            .unwrap_or(false);
+
+        theme::ui::modal("Authenticate Copilot", &style.modal, cx, |cx| {
+            Flex::column()
+                .align_children_center()
+                .with_children([
+                    Flex::column()
+                        .with_children([
+                            Flex::row()
+                                .with_children([
+                                    theme::ui::svg(&style.auth.copilot_icon).boxed(),
+                                    theme::ui::svg(&style.auth.plus_icon).boxed(),
+                                    theme::ui::svg(&style.auth.zed_icon).boxed(),
+                                ])
+                                .boxed(),
+                            Label::new("Copilot for Zed", style.auth.header_text.clone()).boxed(),
+                        ])
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.header_group)
+                        .aligned()
+                        .boxed(),
+                    Flex::column()
+                        .with_children([
+                            Label::new(
+                                "Here is your code to authenticate with github",
+                                style.auth.instruction_text.clone(),
+                            )
+                            .boxed(),
+                            MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
+                                Flex::row()
+                                    .with_children([
+                                        Label::new(
+                                            self.prompt.user_code.clone(),
+                                            style.auth.device_code.clone(),
+                                        )
+                                        .aligned()
+                                        .contained()
+                                        .with_style(style.auth.device_code_left_container)
+                                        .constrained()
+                                        .with_width(style.auth.device_code_left)
+                                        .boxed(),
+                                        Empty::new()
+                                            .constrained()
+                                            .with_width(1.)
+                                            .with_height(style.auth.device_code_seperator_height)
+                                            .contained()
+                                            .with_background_color(
+                                                style
+                                                    .auth
+                                                    .cta_button
+                                                    .style_for(state, false)
+                                                    .container
+                                                    .border
+                                                    .color,
+                                            )
+                                            .boxed(),
+                                        Label::new(
+                                            if copied { "Copied!" } else { "Copy" },
+                                            style
+                                                .auth
+                                                .cta_button
+                                                .style_for(state, false)
+                                                .text
+                                                .clone(),
+                                        )
+                                        .aligned()
+                                        .contained()
+                                        .with_style(style.auth.device_code_right_container)
+                                        .constrained()
+                                        .with_width(style.auth.device_code_right)
+                                        .boxed(),
+                                    ])
+                                    .contained()
+                                    .with_style(
+                                        style
+                                            .auth
+                                            .device_code_cta
+                                            .style_for(state, false)
+                                            .container,
+                                    )
+                                    .constrained()
+                                    .with_width(style.auth.content_width)
+                                    .boxed()
+                            })
+                            .on_click(gpui::MouseButton::Left, {
+                                let user_code = self.prompt.user_code.clone();
+                                move |_, cx| {
+                                    cx.platform()
+                                        .write_to_clipboard(ClipboardItem::new(user_code.clone()));
+                                    cx.notify();
+                                }
+                            })
+                            .with_cursor_style(gpui::CursorStyle::PointingHand)
+                            .boxed(),
+                        ])
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.device_code_group)
+                        .aligned()
                         .boxed(),
-                        Label::new(user_code, user_code_text.clone()).boxed(),
-                        theme::ui::cta_button_with_click("Copy Code", button_width, &button, cx, {
-                            let user_code = self.prompt.user_code.clone();
-                            move |_, cx| {
-                                cx.platform()
-                                    .write_to_clipboard(ClipboardItem::new(user_code.clone()))
-                            }
-                        }),
-                        Label::new("Copy it and enter it on GitHub", instruction_text.clone())
+                    Flex::column()
+                        .with_children([
+                            Label::new(
+                                "Copy it and enter it on GitHub",
+                                style.auth.instruction_text.clone(),
+                            )
                             .boxed(),
-                        theme::ui::cta_button_with_click(
-                            "Go to Github",
-                            button_width,
-                            &button,
-                            cx,
-                            {
-                                let verification_uri = self.prompt.verification_uri.clone();
-                                move |_, cx| cx.platform().open_url(&verification_uri)
-                            },
-                        ),
-                    ])
-                    .aligned()
-                    .boxed(),
-            )
-            .contained()
-            .with_style(style.auth.popup_container)
-            .constrained()
-            .with_height(height)
-            .boxed()
+                            theme::ui::cta_button_with_click(
+                                "Go to Github",
+                                style.auth.content_width,
+                                &style.auth.cta_button,
+                                cx,
+                                {
+                                    let verification_uri = self.prompt.verification_uri.clone();
+                                    move |_, cx| cx.platform().open_url(&verification_uri)
+                                },
+                            ),
+                        ])
+                        .align_children_center()
+                        .contained()
+                        .with_style(style.auth.github_group)
+                        .aligned()
+                        .boxed(),
+                ])
+                .constrained()
+                .with_width(style.auth.content_width)
+                .aligned()
+                .boxed()
+        })
     }
 }
 

crates/theme/src/theme.rs 🔗

@@ -9,7 +9,7 @@ use gpui::{
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
-use ui::{ButtonStyle, CheckboxStyle, Dimensions, IconStyle, SvgStyle};
+use ui::{ButtonStyle, CheckboxStyle, ModalStyle, SvgStyle};
 
 pub mod ui;
 
@@ -118,19 +118,29 @@ pub struct AvatarStyle {
 
 #[derive(Deserialize, Default, Clone)]
 pub struct Copilot {
+    pub modal: ModalStyle,
     pub auth: CopilotAuth,
 }
 
 #[derive(Deserialize, Default, Clone)]
 pub struct CopilotAuth {
-    pub popup_container: ContainerStyle,
-    pub popup_dimensions: Dimensions,
     pub instruction_text: TextStyle,
-    pub user_code: TextStyle,
-    pub button: ButtonStyle,
-    pub button_width: f32,
+    pub cta_button: ButtonStyle,
+    pub content_width: f32,
     pub copilot_icon: SvgStyle,
-    pub close_icon: Interactive<IconStyle>,
+    pub plus_icon: SvgStyle,
+    pub zed_icon: SvgStyle,
+    pub header_text: TextStyle,
+    pub device_code_group: ContainerStyle,
+    pub github_group: ContainerStyle,
+    pub header_group: ContainerStyle,
+    pub device_code: TextStyle,
+    pub device_code_cta: ButtonStyle,
+    pub device_code_left: f32,
+    pub device_code_left_container: ContainerStyle,
+    pub device_code_right: f32,
+    pub device_code_right_container: ContainerStyle,
+    pub device_code_seperator_height: f32,
 }
 
 #[derive(Deserialize, Default)]

crates/theme/src/ui.rs 🔗

@@ -4,11 +4,12 @@ use gpui::{
     color::Color,
     elements::{
         ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
-        MouseEventHandler, ParentElement, Svg,
+        MouseEventHandler, ParentElement, Stack, Svg,
     },
+    fonts::TextStyle,
     geometry::vector::{vec2f, Vector2F},
     scene::MouseClick,
-    Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View,
+    Action, Element, ElementBox, EventContext, MouseButton, RenderContext, View,
 };
 use serde::Deserialize;
 
@@ -213,3 +214,60 @@ where
     .with_cursor_style(gpui::CursorStyle::PointingHand)
     .boxed()
 }
+
+#[derive(Clone, Deserialize, Default)]
+pub struct ModalStyle {
+    close_icon: Interactive<IconStyle>,
+    container: ContainerStyle,
+    titlebar: ContainerStyle,
+    title_text: TextStyle,
+    dimensions: Dimensions,
+}
+
+impl ModalStyle {
+    pub fn dimensions(&self) -> Vector2F {
+        self.dimensions.to_vec()
+    }
+}
+
+pub fn modal<V, I, F>(
+    title: I,
+    style: &ModalStyle,
+    cx: &mut RenderContext<V>,
+    build_modal: F,
+) -> ElementBox
+where
+    V: View,
+    I: Into<Cow<'static, str>>,
+    F: FnOnce(&mut gpui::RenderContext<V>) -> ElementBox,
+{
+    Flex::column()
+        .with_child(
+            Stack::new()
+                .with_children([
+                    Label::new(title, style.title_text.clone()).boxed(),
+                    // FIXME: Get a better tag type
+                    MouseEventHandler::<V>::new(999999, cx, |state, _cx| {
+                        let style = style.close_icon.style_for(state, false);
+                        icon(style).boxed()
+                    })
+                    .on_click(gpui::MouseButton::Left, move |_, cx| {
+                        let window_id = cx.window_id();
+                        cx.remove_window(window_id);
+                    })
+                    .with_cursor_style(gpui::CursorStyle::PointingHand)
+                    .aligned()
+                    .right()
+                    .boxed(),
+                ])
+                .contained()
+                .with_style(style.titlebar)
+                .boxed(),
+        )
+        .with_child(build_modal(cx))
+        .contained()
+        .with_style(style.container)
+        .constrained()
+        .with_height(style.dimensions().y())
+        .boxed()
+}

styles/src/styleTree/copilot.ts 🔗

@@ -5,41 +5,52 @@ import { background, border, foreground, svg, text } from "./components";
 export default function copilot(colorScheme: ColorScheme) {
     let layer = colorScheme.highest;
 
+    let content_width = 304;
+
+    let ctaButton = { // Copied from welcome screen. FIXME: Move this into a ZDS component
+        background: background(layer),
+        border: border(layer, "active"),
+        cornerRadius: 4,
+        margin: {
+            top: 4,
+            bottom: 4,
+        },
+        padding: {
+            top: 3,
+            bottom: 3,
+            left: 7,
+            right: 7,
+        },
+        ...text(layer, "sans", "default", { size: "sm" }),
+        hover: {
+            ...text(layer, "sans", "default", { size: "sm" }),
+            background: background(layer, "hovered"),
+            border: border(layer, "active"),
+        },
+    };
+
     return {
-        auth: {
-            popupContainer: {
-                background: background(colorScheme.highest),
-            },
-            popupDimensions: {
-                width: 336,
-                height: 256,
-            },
-            instructionText: text(layer, "sans"),
-            userCode:
-                text(layer, "sans", { size: "lg" }),
-            button: { // Copied from welcome screen. FIXME: Move this into a ZDS component
-                background: background(layer),
+        modal: {
+            titleText: text(layer, "sans", { size: "md" }),
+            titlebar: {
                 border: border(layer, "active"),
-                cornerRadius: 4,
-                margin: {
+                padding: {
                     top: 4,
                     bottom: 4,
+                    left: 8,
+                    right: 8,
                 },
-                padding: {
-                    top: 3,
-                    bottom: 3,
-                    left: 7,
-                    right: 7,
-                },
-                ...text(layer, "sans", "default", { size: "sm" }),
-                hover: {
-                    ...text(layer, "sans", "default", { size: "sm" }),
-                    background: background(layer, "hovered"),
-                    border: border(layer, "active"),
-                },
+                margin: {
+                    top: 0,
+                    left: 0,
+                    right: 0,
+                    bottom: 8
+                }
+            },
+            container: {
+                background: background(colorScheme.highest),
+
             },
-            buttonWidth: 320,
-            copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 64, 64),
             closeIcon: {
                 icon: svg(background(layer, "on"), "icons/x_mark_16.svg", 16, 16),
                 container: {
@@ -47,13 +58,86 @@ export default function copilot(colorScheme: ColorScheme) {
                         top: 3,
                         bottom: 3,
                         left: 7,
-                        right: 7,
+                        right: 0,
                     }
                 },
                 hover: {
                     icon: svg(foreground(layer, "on"), "icons/x_mark_16.svg", 16, 16),
                 }
             },
+            dimensions: {
+                width: 400,
+                height: 500,
+            },
+        },
+        auth: {
+            content_width,
+
+            headerGroup: {
+                margin: {
+                    top: 5,
+                    bottom: 5,
+                    left: 0,
+                    right: 0
+                }
+            },
+            headerText: text(layer, "sans", { size: "lg" }),
+            copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 36, 36),
+            plusIcon: svg(foreground(layer, "default"), "icons/plus_16.svg", 36, 36),
+            zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 36, 36),
+
+            instructionText: text(layer, "sans"),
+
+            deviceCodeGroup: {
+                margin: {
+                    top: 5,
+                    bottom: 5,
+                    left: 0,
+                    right: 0
+                }
+            },
+            deviceCode:
+                text(layer, "mono", { size: "md" }),
+            deviceCodeCta: {
+                ...ctaButton,
+                padding: {
+                    top: 0,
+                    bottom: 0,
+                    left: 0,
+                    right: 0,
+                },
+            },
+            deviceCodeLeft: content_width * 2 / 3,
+            deviceCodeLeftContainer: {
+                padding: {
+                    top: 3,
+                    bottom: 3,
+                    left: 0,
+                    right: 0,
+                },
+            },
+            deviceCodeRight: content_width * 1 / 3,
+            deviceCodeRightContainer: {
+                border: border(layer, "active", { bottom: false, right: false, top: false, left: true }),
+                padding: {
+                    top: 3,
+                    bottom: 5,
+                    left: 0,
+                    right: 0,
+                },
+            },
+            deviceCodeSeperatorHeight: 0,
+
+            githubGroup: {
+                margin: {
+                    top: 3,
+                    bottom: 3,
+                    left: 0,
+                    right: 0
+                }
+            },
+
+            ctaButton
         }
     }
 }