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,