Start scaffolding out the Copilot Modal UI

Nate Butler and Mikayla Maki created

Co-Authored-By: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/gpui2_macros/src/style_helpers.rs | 10 ++
crates/storybook2/src/story_selector.rs  |  5 +
crates/ui2/src/components.rs             |  4 +
crates/ui2/src/components/copilot.rs     | 63 ++++++++++++++++++
crates/ui2/src/components/list.rs        | 11 +--
crates/ui2/src/components/modal.rs       | 87 ++++++++++++++++++++++++++
crates/ui2/src/elements/button.rs        |  6 +
crates/ui2/src/prelude.rs                |  3 
8 files changed, 180 insertions(+), 9 deletions(-)

Detailed changes

crates/gpui2_macros/src/style_helpers.rs 🔗

@@ -173,6 +173,7 @@ fn generate_custom_value_setter(
     method
 }
 
+/// Returns a vec of (Property name, has 'auto' suffix, tokens for accessing the property, documentation)
 fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)> {
     vec![
         (
@@ -188,24 +189,32 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)>
             vec![quote! {size.width}, quote! {size.height}],
             "Sets the width and height of the element."
         ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
         (
             "min_w",
             true,
             vec![quote! { min_size.width }],
             "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)",
         ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
         (
             "min_h",
             true,
             vec![quote! { min_size.height }],
             "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)",
         ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
         (
             "max_w",
             true,
             vec![quote! { max_size.width }],
             "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)",
         ),
+        // TODO: These don't use the same size ramp as the others
+        // see https://tailwindcss.com/docs/max-width
         (
             "max_h",
             true,
@@ -336,6 +345,7 @@ fn box_prefixes() -> Vec<(&'static str, bool, Vec<TokenStream2>, &'static str)>
     ]
 }
 
+/// Returns a vec of (Suffix size, tokens that correspond to this size, documentation)
 fn box_suffixes() -> Vec<(&'static str, TokenStream2, &'static str)> {
     vec![
         ("0", quote! { px(0.) }, "0px"),

crates/storybook2/src/story_selector.rs 🔗

@@ -65,6 +65,7 @@ pub enum ComponentStory {
     ChatPanel,
     CollabPanel,
     CommandPalette,
+    Copilot,
     ContextMenu,
     Facepile,
     Keybinding,
@@ -170,6 +171,10 @@ impl ComponentStory {
                 ui::TrafficLightsStory::new().into_any()
             })
             .into_any(),
+            Self::Copilot => view(cx.entity(|cx| ()), |_, _| {
+                ui::CopilotModalStory::new().into_any()
+            })
+            .into_any(),
             Self::Workspace => ui::WorkspaceStory::view(cx).into_any(),
         }
     }

crates/ui2/src/components.rs 🔗

@@ -6,12 +6,14 @@ mod chat_panel;
 mod collab_panel;
 mod command_palette;
 mod context_menu;
+mod copilot;
 mod editor_pane;
 mod facepile;
 mod icon_button;
 mod keybinding;
 mod language_selector;
 mod list;
+mod modal;
 mod multi_buffer;
 mod notification_toast;
 mod notifications_panel;
@@ -40,12 +42,14 @@ pub use chat_panel::*;
 pub use collab_panel::*;
 pub use command_palette::*;
 pub use context_menu::*;
+pub use copilot::*;
 pub use editor_pane::*;
 pub use facepile::*;
 pub use icon_button::*;
 pub use keybinding::*;
 pub use language_selector::*;
 pub use list::*;
+pub use modal::*;
 pub use multi_buffer::*;
 pub use notification_toast::*;
 pub use notifications_panel::*;

crates/ui2/src/components/copilot.rs 🔗

@@ -0,0 +1,63 @@
+use std::marker::PhantomData;
+
+use crate::{prelude::*, Button, Label, LabelColor, Modal};
+
+#[derive(Element)]
+pub struct CopilotModal<S: 'static + Send + Sync + Clone> {
+    id: ElementId,
+    state_type: PhantomData<S>,
+}
+
+impl<S: 'static + Send + Sync + Clone> CopilotModal<S> {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            state_type: PhantomData,
+        }
+    }
+
+    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+        let color = ThemeColor::new(cx);
+
+        div().id(self.id.clone()).child(
+            Modal::new("some-id")
+                .title("Connect Copilot to Zed")
+                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted))
+                .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
+        )
+    }
+}
+
+#[cfg(feature = "stories")]
+pub use stories::*;
+
+#[cfg(feature = "stories")]
+mod stories {
+    use crate::Story;
+
+    use super::*;
+
+    #[derive(Element)]
+    pub struct CopilotModalStory<S: 'static + Send + Sync + Clone> {
+        state_type: PhantomData<S>,
+    }
+
+    impl<S: 'static + Send + Sync + Clone> CopilotModalStory<S> {
+        pub fn new() -> Self {
+            Self {
+                state_type: PhantomData,
+            }
+        }
+
+        fn render(
+            &mut self,
+            _view: &mut S,
+            cx: &mut ViewContext<S>,
+        ) -> impl Element<ViewState = S> {
+            Story::container(cx)
+                .child(Story::title_for::<_, CopilotModal<S>>(cx))
+                .child(Story::label(cx, "Default"))
+                .child(CopilotModal::new("copilot-modal"))
+        }
+    }
+}

crates/ui2/src/components/list.rs 🔗

@@ -518,13 +518,10 @@ impl<S: 'static + Send + Sync> ListDetailsEntry<S> {
                 this.child(Label::new(self.meta.clone().unwrap()).color(LabelColor::Muted))
             })
             .child(
-                h_stack().gap_1().justify_end().children(
-                    self.actions
-                        .take()
-                        .unwrap_or_default()
-                        .into_iter()
-                        .map(|action| action),
-                ),
+                h_stack()
+                    .gap_1()
+                    .justify_end()
+                    .children(self.actions.take().unwrap_or_default().into_iter()),
             )
     }
 }

crates/ui2/src/components/modal.rs 🔗

@@ -0,0 +1,87 @@
+use std::marker::PhantomData;
+
+use gpui2::AnyElement;
+use smallvec::SmallVec;
+
+use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
+
+#[derive(Element)]
+pub struct Modal<S: 'static + Send + Sync> {
+    id: ElementId,
+    state_type: PhantomData<S>,
+    title: Option<SharedString>,
+    primary_action: Option<Button<S>>,
+    secondary_action: Option<Button<S>>,
+    children: SmallVec<[AnyElement<S>; 2]>,
+}
+
+impl<S: 'static + Send + Sync> Modal<S> {
+    pub fn new(id: impl Into<ElementId>) -> Self {
+        Self {
+            id: id.into(),
+            state_type: PhantomData,
+            title: None,
+            primary_action: None,
+            secondary_action: None,
+            children: SmallVec::new(),
+        }
+    }
+
+    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
+        self.title = Some(title.into());
+        self
+    }
+
+    pub fn primary_action(mut self, action: Button<S>) -> Self {
+        self.primary_action = Some(action);
+        self
+    }
+
+    pub fn secondary_action(mut self, action: Button<S>) -> Self {
+        self.secondary_action = Some(action);
+        self
+    }
+
+    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+        let color = ThemeColor::new(cx);
+
+        v_stack()
+            .id(self.id.clone())
+            .w_96()
+            // .rounded_xl()
+            .bg(color.background)
+            .border()
+            .border_color(color.border)
+            .shadow_2xl()
+            .child(
+                h_stack()
+                    .justify_between()
+                    .p_1()
+                    .border_b()
+                    .border_color(color.border)
+                    .child(div().children(self.title.clone().map(|t| Label::new(t))))
+                    .child(IconButton::new(Icon::Close)),
+            )
+            .child(v_stack().p_1().children(self.children.drain(..)))
+            .when(
+                self.primary_action.is_some() || self.secondary_action.is_some(),
+                |this| {
+                    this.child(
+                        h_stack()
+                            .border_t()
+                            .border_color(color.border)
+                            .p_1()
+                            .justify_end()
+                            .children(self.secondary_action.take())
+                            .children(self.primary_action.take()),
+                    )
+                },
+            )
+    }
+}
+
+impl<S: 'static + Send + Sync> ParentElement for Modal<S> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<Self::ViewState>; 2]> {
+        &mut self.children
+    }
+}

crates/ui2/src/elements/button.rs 🔗

@@ -146,7 +146,11 @@ impl<S: 'static + Send + Sync> Button<S> {
         self.icon.map(|i| IconElement::new(i).color(icon_color))
     }
 
-    fn render(&mut self, _view: &mut S, cx: &mut ViewContext<S>) -> impl Element<ViewState = S> {
+    pub fn render(
+        &mut self,
+        _view: &mut S,
+        cx: &mut ViewContext<S>,
+    ) -> impl Element<ViewState = S> {
         let icon_color = self.icon_color();
         let border_color = self.border_color(cx);
         let settings = user_settings(cx);

crates/ui2/src/prelude.rs 🔗

@@ -110,7 +110,7 @@ pub struct ThemeColor {
     /// The background color of an elevated surface, like a modal, tooltip or toast.
     pub elevated_surface: Hsla,
     pub surface: Hsla,
-    /// Window background color
+    /// Window background color of the base app
     pub background: Hsla,
     /// Default background for elements like filled buttons,
     /// text fields, checkboxes, radio buttons, etc.
@@ -149,6 +149,7 @@ pub struct ThemeColor {
     pub title_bar: Hsla,
     pub toolbar: Hsla,
     pub tab_bar: Hsla,
+    /// The background of the editor
     pub editor: Hsla,
     pub editor_subheader: Hsla,
     pub editor_active_line: Hsla,