From 6ff09865eb1dbf3fd07ce307fc71489eb06d2bc9 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 27 Mar 2023 14:25:11 -0700 Subject: [PATCH] Create copilot auth popup UI --- Cargo.lock | 1 + assets/icons/github-copilot-dummy.svg | 1 + crates/copilot/Cargo.toml | 1 + crates/copilot/src/copilot.rs | 2 +- crates/copilot/src/sign_in.rs | 90 +++++++++++++++++++---- crates/theme/src/theme.rs | 23 ++++-- crates/theme/src/ui.rs | 82 +++++++++++++++++++-- crates/welcome/src/welcome.rs | 101 +++----------------------- styles/src/styleTree/components.ts | 12 +++ styles/src/styleTree/copilot.ts | 60 ++++++++++++--- styles/src/styleTree/welcome.ts | 19 +---- styles/src/styleTree/workspace.ts | 31 +++----- 12 files changed, 253 insertions(+), 170 deletions(-) create mode 100644 assets/icons/github-copilot-dummy.svg diff --git a/Cargo.lock b/Cargo.lock index 5c43455d54..65d68aa3a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,7 @@ dependencies = [ "serde_derive", "settings", "smol", + "theme", "util", "workspace", ] diff --git a/assets/icons/github-copilot-dummy.svg b/assets/icons/github-copilot-dummy.svg new file mode 100644 index 0000000000..4a7ded3976 --- /dev/null +++ b/assets/icons/github-copilot-dummy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index c17e7cac59..a7582a6ffc 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -12,6 +12,7 @@ doctest = false gpui = { path = "../gpui" } language = { path = "../language" } settings = { path = "../settings" } +theme = { path = "../theme" } lsp = { path = "../lsp" } util = { path = "../util" } client = { path = "../client" } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index aa36991fac..2763eea0fd 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -475,7 +475,7 @@ mod tests { .update(cx, |copilot, cx| copilot.sign_in(cx)) .await .unwrap(); - dbg!(copilot.read_with(cx, |copilot, _| copilot.status())); + copilot.read_with(cx, |copilot, _| copilot.status()); let buffer = cx.add_model(|cx| language::Buffer::new(0, "fn foo() -> ", cx)); dbg!(copilot diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 67b93385ac..cdec0b8963 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -1,11 +1,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot}; use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - Axis, Element, Entity, MutableAppContext, View, WindowKind, WindowOptions, + elements::*, geometry::rect::RectF, impl_internal_actions, ClipboardItem, Element, Entity, + MutableAppContext, View, WindowKind, WindowOptions, }; use settings::Settings; +#[derive(PartialEq, Eq, Debug, Clone)] +struct CopyUserCode; + +#[derive(PartialEq, Eq, Debug, Clone)] +struct OpenGithub; + +impl_internal_actions!(copilot_sign_in, [CopyUserCode, OpenGithub]); + pub fn init(cx: &mut MutableAppContext) { let copilot = Copilot::global(cx).unwrap(); @@ -19,16 +26,24 @@ pub fn init(cx: &mut MutableAppContext) { cx.remove_window(window_id); } + let window_size = cx + .global::() + .theme + .copilot + .auth + .popup_dimensions + .to_vec(); + let (window_id, _) = cx.add_window( WindowOptions { bounds: gpui::WindowBounds::Fixed(RectF::new( Default::default(), - vec2f(600., 400.), + window_size, )), titlebar: None, center: true, focus: false, - kind: WindowKind::Normal, + kind: WindowKind::PopUp, is_movable: true, screen: None, }, @@ -62,23 +77,68 @@ impl View for CopilotCodeVerification { fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let style = cx.global::().theme.copilot.clone(); - let auth_text = style.auth_text.clone(); - let prompt = self.prompt.clone(); - Flex::new(Axis::Vertical) - .with_child(Label::new(prompt.user_code.clone(), auth_text.clone()).boxed()) + 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::::new(1, cx, move |_state, _cx| { - Label::new("Click here to open GitHub!", auth_text.clone()).boxed() + MouseEventHandler::::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 |_click, cx| { - cx.platform().open_url(&prompt.verification_uri) + .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(), + ) + .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()) + .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_modal) - .named("Copilot Authentication status modal") + .with_style(style.auth.popup_container) + .constrained() + .with_height(height) + .boxed() } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index ef6a73f5d7..ce4d8a04fb 100644 --- a/crates/theme/src/theme.rs +++ b/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::{CheckboxStyle, IconStyle}; +use ui::{ButtonStyle, CheckboxStyle, Dimensions, IconStyle, SvgStyle}; pub mod ui; @@ -76,8 +76,8 @@ pub struct Workspace { #[derive(Clone, Deserialize, Default)] pub struct BlankPaneStyle { - pub logo: IconStyle, - pub logo_shadow: IconStyle, + pub logo: SvgStyle, + pub logo_shadow: SvgStyle, pub logo_container: ContainerStyle, pub keyboard_hints: ContainerStyle, pub keyboard_hint: Interactive, @@ -118,8 +118,19 @@ pub struct AvatarStyle { #[derive(Deserialize, Default, Clone)] pub struct Copilot { - pub auth_modal: ContainerStyle, - pub auth_text: TextStyle, + 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 copilot_icon: SvgStyle, + pub close_icon: Interactive, } #[derive(Deserialize, Default)] @@ -876,7 +887,7 @@ pub struct FeedbackStyle { #[derive(Clone, Deserialize, Default)] pub struct WelcomeStyle { pub page_width: f32, - pub logo: IconStyle, + pub logo: SvgStyle, pub logo_subheading: ContainedText, pub usage_note: ContainedText, pub checkbox: CheckboxStyle, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 5441e71168..392b1134a6 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,18 +1,22 @@ +use std::borrow::Cow; + use gpui::{ color::Color, elements::{ ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, MouseEventHandler, ParentElement, Svg, }, - Action, Element, ElementBox, EventContext, RenderContext, View, + geometry::vector::{vec2f, Vector2F}, + scene::MouseClick, + Action, Element, ElementBox, EventContext, MouseButton, MouseState, RenderContext, View, }; use serde::Deserialize; -use crate::ContainedText; +use crate::{ContainedText, Interactive}; #[derive(Clone, Deserialize, Default)] pub struct CheckboxStyle { - pub icon: IconStyle, + pub icon: SvgStyle, pub label: ContainedText, pub default: ContainerStyle, pub checked: ContainerStyle, @@ -44,7 +48,7 @@ pub fn checkbox_with_label( ) -> MouseEventHandler { MouseEventHandler::::new(0, cx, |state, _| { let indicator = if checked { - icon(&style.icon) + svg(&style.icon) } else { Empty::new() .constrained() @@ -80,9 +84,9 @@ pub fn checkbox_with_label( } #[derive(Clone, Deserialize, Default)] -pub struct IconStyle { +pub struct SvgStyle { pub color: Color, - pub icon: String, + pub asset: String, pub dimensions: Dimensions, } @@ -92,14 +96,30 @@ pub struct Dimensions { pub height: f32, } -pub fn icon(style: &IconStyle) -> ConstrainedBox { - Svg::new(style.icon.clone()) +impl Dimensions { + pub fn to_vec(&self) -> Vector2F { + vec2f(self.width, self.height) + } +} + +pub fn svg(style: &SvgStyle) -> ConstrainedBox { + Svg::new(style.asset.clone()) .with_color(style.color) .constrained() .with_width(style.dimensions.width) .with_height(style.dimensions.height) } +#[derive(Clone, Deserialize, Default)] +pub struct IconStyle { + icon: SvgStyle, + container: ContainerStyle, +} + +pub fn icon(style: &IconStyle) -> Container { + svg(&style.icon).contained().with_style(style.container) +} + pub fn keystroke_label( label_text: &'static str, label_style: &ContainedText, @@ -147,3 +167,49 @@ pub fn keystroke_label_for( .contained() .with_style(label_style.container) } + +pub type ButtonStyle = Interactive; + +pub fn cta_button( + label: L, + action: A, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, +) -> ElementBox +where + L: Into>, + A: 'static + Action + Clone, + V: View, +{ + cta_button_with_click(label, max_width, style, cx, move |_, cx| { + cx.dispatch_action(action.clone()) + }) +} + +pub fn cta_button_with_click( + label: L, + max_width: f32, + style: &ButtonStyle, + cx: &mut RenderContext, + f: F, +) -> ElementBox +where + L: Into>, + V: View, + F: Fn(MouseClick, &mut EventContext) + 'static, +{ + MouseEventHandler::::new(0, cx, |state, _| { + let style = style.style_for(state, false); + Label::new(label, style.text.to_owned()) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_max_width(max_width) + .boxed() + }) + .on_click(MouseButton::Left, f) + .with_cursor_style(gpui::CursorStyle::PointingHand) + .boxed() +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 3a35920b88..fb55c79a51 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,12 +1,11 @@ mod base_keymap_picker; -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; use db::kvp::KEY_VALUE_STORE; use gpui::{ - elements::{Flex, Label, MouseEventHandler, ParentElement}, - Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, - Subscription, View, ViewContext, + elements::{Flex, Label, ParentElement}, + Element, ElementBox, Entity, MutableAppContext, Subscription, View, ViewContext, }; use settings::{settings_file::SettingsFile, Settings}; @@ -77,7 +76,7 @@ impl View for WelcomePage { .with_children([ Flex::column() .with_children([ - theme::ui::icon(&theme.welcome.logo) + theme::ui::svg(&theme.welcome.logo) .aligned() .contained() .aligned() @@ -98,22 +97,25 @@ impl View for WelcomePage { .boxed(), Flex::column() .with_children([ - self.render_cta_button( + theme::ui::cta_button( "Choose a theme", theme_selector::Toggle, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Choose a keymap", ToggleBaseKeymapSelector, width, + &theme.welcome.button, cx, ), - self.render_cta_button( + theme::ui::cta_button( "Install the CLI", install_cli::Install, width, + &theme.welcome.button, cx, ), ]) @@ -201,89 +203,6 @@ impl WelcomePage { _settings_subscription: settings_subscription, } } - - fn render_cta_button( - &self, - label: L, - action: A, - width: f32, - cx: &mut RenderContext, - ) -> ElementBox - where - L: Into>, - A: 'static + Action + Clone, - { - let theme = cx.global::().theme.clone(); - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.welcome.button.style_for(state, false); - Label::new(label, style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_max_width(width) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(action.clone()) - }) - .with_cursor_style(gpui::CursorStyle::PointingHand) - .boxed() - } - - // fn render_settings_checkbox( - // &self, - // label: &'static str, - // style: &CheckboxStyle, - // checked: bool, - // cx: &mut RenderContext, - // set_value: fn(&mut SettingsFileContent, checked: bool) -> (), - // ) -> ElementBox { - // MouseEventHandler::::new(0, cx, |state, _| { - // let indicator = if checked { - // Svg::new(style.check_icon.clone()) - // .with_color(style.check_icon_color) - // .constrained() - // } else { - // Empty::new().constrained() - // }; - - // Flex::row() - // .with_children([ - // indicator - // .with_width(style.width) - // .with_height(style.height) - // .contained() - // .with_style(if checked { - // if state.hovered() { - // style.hovered_and_checked - // } else { - // style.checked - // } - // } else { - // if state.hovered() { - // style.hovered - // } else { - // style.default - // } - // }) - // .boxed(), - // Label::new(label, style.label.text.clone()) - // .contained() - // .with_style(style.label.container) - // .boxed(), - // ]) - // .align_children_center() - // .boxed() - // }) - // .on_click(gpui::MouseButton::Left, move |_, cx| { - // SettingsFile::update(cx, move |content| set_value(content, !checked)) - // }) - // .with_cursor_style(gpui::CursorStyle::PointingHand) - // .contained() - // .with_style(style.container) - // .boxed() - // } } impl Item for WelcomePage { diff --git a/styles/src/styleTree/components.ts b/styles/src/styleTree/components.ts index 33546c9978..6b21eec405 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/styleTree/components.ts @@ -280,3 +280,15 @@ export function border( ...properties, } } + + +export function svg(color: string, asset: String, width: Number, height: Number) { + return { + color, + asset, + dimensions: { + width, + height, + } + } +} diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts index 66f5c63b4e..4772a2f673 100644 --- a/styles/src/styleTree/copilot.ts +++ b/styles/src/styleTree/copilot.ts @@ -1,21 +1,59 @@ import { ColorScheme } from "../themes/common/colorScheme" -import { background, border, text } from "./components"; +import { background, border, foreground, svg, text } from "./components"; export default function copilot(colorScheme: ColorScheme) { let layer = colorScheme.highest; - return { - authModal: { - background: background(colorScheme.lowest), - border: border(colorScheme.lowest), - shadow: colorScheme.modalShadow, - cornerRadius: 12, - padding: { - bottom: 4, + auth: { + popupContainer: { + background: background(colorScheme.highest), }, - }, - authText: text(layer, "sans") + 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), + 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"), + }, + }, + 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: { + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + } + }, + hover: { + icon: svg(foreground(layer, "on"), "icons/x_mark_16.svg", 16, 16), + } + }, + } } } diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts index 252489ef1b..23e29c4a40 100644 --- a/styles/src/styleTree/welcome.ts +++ b/styles/src/styleTree/welcome.ts @@ -6,6 +6,7 @@ import { foreground, text, TextProperties, + svg, } from "./components" export default function welcome(colorScheme: ColorScheme) { @@ -32,14 +33,7 @@ export default function welcome(colorScheme: ColorScheme) { return { pageWidth: 320, - logo: { - color: foreground(layer, "default"), - icon: "icons/logo_96.svg", - dimensions: { - width: 64, - height: 64, - }, - }, + logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64), logoSubheading: { ...text(layer, "sans", "variant", { size: "md" }), margin: { @@ -109,14 +103,7 @@ export default function welcome(colorScheme: ColorScheme) { ...text(layer, "sans", interactive_text_size), // Also supports margin, container, border, etc. }, - icon: { - color: foreground(layer, "on"), - icon: "icons/check_12.svg", - dimensions: { - width: 12, - height: 12, - }, - }, + icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12), default: { ...checkboxBase, background: background(layer, "default"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 1de2fe9502..11f6561bd3 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -1,6 +1,6 @@ import { ColorScheme } from "../themes/common/colorScheme" import { withOpacity } from "../utils/color" -import { background, border, borderColor, foreground, text } from "./components" +import { background, border, borderColor, foreground, svg, text } from "./components" import statusBar from "./statusBar" import tabBar from "./tabBar" @@ -46,27 +46,14 @@ export default function workspace(colorScheme: ColorScheme) { width: 256, height: 256, }, - logo: { - color: withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, - logoShadow: { - color: withOpacity( - colorScheme.isLight - ? "#FFFFFF" - : colorScheme.lowest.base.default.background, - colorScheme.isLight ? 1 : 0.6 - ), - icon: "icons/logo_96.svg", - dimensions: { - width: 256, - height: 256, - }, - }, + logo: svg(withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), "icons/logo_96.svg", 256, 256), + + logoShadow: svg(withOpacity( + colorScheme.isLight + ? "#FFFFFF" + : colorScheme.lowest.base.default.background, + colorScheme.isLight ? 1 : 0.6 + ), "icons/logo_96.svg", 256, 256), keyboardHints: { margin: { top: 96,