Implement component test page

Mikayla created

Change summary

Cargo.lock                                  |   3 
crates/component_test/Cargo.toml            |  18 +++
crates/component_test/src/component_test.rs | 122 +++++++++++++++++++++++
crates/gpui/src/elements/component.rs       |   8 
crates/gpui/src/elements/flex.rs            |  33 +++++-
crates/theme/src/components.rs              |  18 +-
crates/theme/src/theme.rs                   |  10 +
crates/zed/Cargo.toml                       |   1 
crates/zed/src/main.rs                      |   1 
styles/src/style_tree/app.ts                |   5 
styles/src/style_tree/component_test.ts     |  19 +++
11 files changed, 217 insertions(+), 21 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1566,7 +1566,9 @@ dependencies = [
 name = "component_test"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "gpui",
+ "project",
  "settings",
  "theme",
  "util",
@@ -9668,6 +9670,7 @@ dependencies = [
  "collab_ui",
  "collections",
  "command_palette",
+ "component_test",
  "context_menu",
  "copilot",
  "copilot_button",

crates/component_test/Cargo.toml 🔗

@@ -0,0 +1,18 @@
+[package]
+name = "component_test"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/component_test.rs"
+doctest = false
+
+[dependencies]
+anyhow.workspace = true
+gpui = { path = "../gpui" }
+settings = { path = "../settings" }
+util = { path = "../util" }
+theme = { path = "../theme" }
+workspace = { path = "../workspace" }
+project = { path = "../project" }

crates/component_test/src/component_test.rs 🔗

@@ -0,0 +1,122 @@
+use gpui::{
+    actions,
+    color::Color,
+    elements::{Component, Flex, ParentElement, SafeStylable},
+    AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
+};
+use project::Project;
+use theme::components::{action_button::Button, label::Label, ComponentExt};
+use workspace::{
+    item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId,
+};
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(ComponentTest::toggle_disclosure);
+    cx.add_action(ComponentTest::toggle_toggle);
+    cx.add_action(ComponentTest::deploy);
+    register_deserializable_item::<ComponentTest>(cx);
+}
+
+actions!(
+    test,
+    [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest]
+);
+
+struct ComponentTest {
+    disclosed: bool,
+    toggled: bool,
+}
+
+impl ComponentTest {
+    fn new() -> Self {
+        Self {
+            disclosed: false,
+            toggled: false,
+        }
+    }
+
+    fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext<Workspace>) {
+        workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx);
+    }
+
+    fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext<Self>) {
+        self.disclosed = !self.disclosed;
+        cx.notify();
+    }
+
+    fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext<Self>) {
+        self.toggled = !self.toggled;
+        cx.notify();
+    }
+}
+
+impl Entity for ComponentTest {
+    type Event = ();
+}
+
+impl View for ComponentTest {
+    fn ui_name() -> &'static str {
+        "Component Test"
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
+        let theme = theme::current(cx);
+
+        PaneBackdrop::new(
+            cx.view_id(),
+            Flex::column()
+                .with_spacing(10.)
+                .with_child(
+                    Button::action(NoAction)
+                        .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+                        .with_contents(Label::new("Click me!"))
+                        .with_style(theme.component_test.button.clone())
+                        .element(),
+                )
+                .with_child(
+                    Button::action(ToggleToggle)
+                        .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone())
+                        .with_contents(Label::new("Toggle me!"))
+                        .toggleable(self.toggled)
+                        .with_style(theme.component_test.toggle.clone())
+                        .element(),
+                )
+                .with_child(
+                    Label::new("A disclosure")
+                        .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure))
+                        .with_style(theme.component_test.disclosure.clone())
+                        .element(),
+                )
+                .constrained()
+                .with_width(200.)
+                .aligned()
+                .into_any(),
+        )
+        .into_any()
+    }
+}
+
+impl Item for ComponentTest {
+    fn tab_content<V: View>(
+        &self,
+        _: Option<usize>,
+        style: &theme::Tab,
+        _: &AppContext,
+    ) -> gpui::AnyElement<V> {
+        gpui::elements::Label::new("Component test", style.label.clone()).into_any()
+    }
+
+    fn serialized_item_kind() -> Option<&'static str> {
+        Some("ComponentTest")
+    }
+
+    fn deserialize(
+        _project: ModelHandle<Project>,
+        _workspace: WeakViewHandle<Workspace>,
+        _workspace_id: WorkspaceId,
+        _item_id: ItemId,
+        cx: &mut ViewContext<Pane>,
+    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+        Task::ready(Ok(cx.add_view(|_| Self::new())))
+    }
+}

crates/gpui/src/elements/component.rs 🔗

@@ -128,7 +128,7 @@ impl<V: View, C: Component> StatefulComponent<V> for C {
 pub trait StatefulStylable<V: View>: StatefulComponent<V> {
     type Style: Clone;
 
-    fn stateful_with_style(self, style: Self::Style) -> Self;
+    fn with_style(self, style: Self::Style) -> Self;
 }
 
 /// Same as SafeStylable, but generic over a view type
@@ -136,7 +136,7 @@ pub trait StatefulSafeStylable<V: View> {
     type Style: Clone;
     type Output: StatefulComponent<V>;
 
-    fn stateful_with_style(self, style: Self::Style) -> Self::Output;
+    fn with_style(self, style: Self::Style) -> Self::Output;
 }
 
 /// Converting from stateless to stateful
@@ -145,7 +145,7 @@ impl<V: View, C: SafeStylable> StatefulSafeStylable<V> for C {
 
     type Output = C::Output;
 
-    fn stateful_with_style(self, style: Self::Style) -> Self::Output {
+    fn with_style(self, style: Self::Style) -> Self::Output {
         self.with_style(style)
     }
 }
@@ -192,7 +192,7 @@ impl<C: StatefulComponent<V>, V: View> StatefulSafeStylable<V> for StatefulStyla
 
     type Output = C;
 
-    fn stateful_with_style(self, _: Self::Style) -> Self::Output {
+    fn with_style(self, _: Self::Style) -> Self::Output {
         self.component
     }
 }

crates/gpui/src/elements/flex.rs 🔗

@@ -22,6 +22,7 @@ pub struct Flex<V: View> {
     children: Vec<AnyElement<V>>,
     scroll_state: Option<(ElementStateHandle<Rc<ScrollState>>, usize)>,
     child_alignment: f32,
+    spacing: f32,
 }
 
 impl<V: View> Flex<V> {
@@ -31,6 +32,7 @@ impl<V: View> Flex<V> {
             children: Default::default(),
             scroll_state: None,
             child_alignment: -1.,
+            spacing: 0.,
         }
     }
 
@@ -51,6 +53,11 @@ impl<V: View> Flex<V> {
         self
     }
 
+    pub fn with_spacing(mut self, spacing: f32) -> Self {
+        self.spacing = spacing;
+        self
+    }
+
     pub fn scrollable<Tag>(
         mut self,
         element_id: usize,
@@ -81,7 +88,8 @@ impl<V: View> Flex<V> {
         cx: &mut LayoutContext<V>,
     ) {
         let cross_axis = self.axis.invert();
-        for child in &mut self.children {
+        let last = self.children.len() - 1;
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             if let Some(metadata) = child.metadata::<FlexParentData>() {
                 if let Some((flex, expanded)) = metadata.flex {
                     if expanded != layout_expanded {
@@ -93,6 +101,10 @@ impl<V: View> Flex<V> {
                     } else {
                         let space_per_flex = *remaining_space / *remaining_flex;
                         space_per_flex * flex
+                    } - if ix == 0 || ix == last {
+                        self.spacing / 2.
+                    } else {
+                        self.spacing
                     };
                     let child_min = if expanded { child_max } else { 0. };
                     let child_constraint = match self.axis {
@@ -137,7 +149,8 @@ impl<V: View> Element<V> for Flex<V> {
 
         let cross_axis = self.axis.invert();
         let mut cross_axis_max: f32 = 0.0;
-        for child in &mut self.children {
+        let last = self.children.len().saturating_sub(1);
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             let metadata = child.metadata::<FlexParentData>();
             contains_float |= metadata.map_or(false, |metadata| metadata.float);
 
@@ -155,7 +168,12 @@ impl<V: View> Element<V> for Flex<V> {
                     ),
                 };
                 let size = child.layout(child_constraint, view, cx);
-                fixed_space += size.along(self.axis);
+                fixed_space += size.along(self.axis)
+                    + if ix == 0 || ix == last {
+                        self.spacing / 2.
+                    } else {
+                        self.spacing
+                    };
                 cross_axis_max = cross_axis_max.max(size.along(cross_axis));
             }
         }
@@ -315,7 +333,8 @@ impl<V: View> Element<V> for Flex<V> {
             }
         }
 
-        for child in &mut self.children {
+        let last = self.children.len().saturating_sub(1);
+        for (ix, child) in &mut self.children.iter_mut().enumerate() {
             if remaining_space > 0. {
                 if let Some(metadata) = child.metadata::<FlexParentData>() {
                     if metadata.float {
@@ -353,9 +372,11 @@ impl<V: View> Element<V> for Flex<V> {
 
             child.paint(scene, aligned_child_origin, visible_bounds, view, cx);
 
+            let spacing = if ix == last { 0. } else { self.spacing };
+
             match self.axis {
-                Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
-                Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
+                Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0),
+                Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing),
             }
         }
 

crates/theme/src/components.rs 🔗

@@ -74,8 +74,9 @@ pub mod disclosure {
     }
 
     impl<C> Disclosable<C, ()> {
-        pub fn with_id(self, id: usize) -> Disclosable<C, ()> {
-            Disclosable { id, ..self }
+        pub fn with_id(mut self, id: usize) -> Disclosable<C, ()> {
+            self.id = id;
+            self
         }
     }
 
@@ -181,7 +182,7 @@ pub mod action_button {
     use gpui::{
         elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle},
         platform::{CursorStyle, MouseButton},
-        Action, Element, TypeTag, View,
+        Action, Element, EventContext, TypeTag, View,
     };
     use schemars::JsonSchema;
     use serde_derive::Deserialize;
@@ -211,14 +212,14 @@ pub mod action_button {
     }
 
     impl Button<(), ()> {
-        pub fn dynamic_action(action: Box<dyn Action>) -> Self {
+        pub fn dynamic_action(action: Box<dyn Action>) -> Button<(), ()> {
             Self {
                 contents: (),
                 tag: action.type_tag(),
+                action,
                 style: Interactive::new_blank(),
                 tooltip: None,
                 id: 0,
-                action,
             }
         }
 
@@ -292,7 +293,7 @@ pub mod action_button {
             })
             .on_click(MouseButton::Left, {
                 let action = self.action.boxed_clone();
-                move |_, _, cx| {
+                move |_, _, cx: &mut EventContext<V>| {
                     let window = cx.window();
                     let view = cx.view_id();
                     let action = action.boxed_clone();
@@ -437,6 +438,7 @@ pub mod label {
 
     use gpui::{
         elements::{Component, LabelStyle, SafeStylable},
+        fonts::TextStyle,
         Element,
     };
 
@@ -455,14 +457,14 @@ pub mod label {
     }
 
     impl SafeStylable for Label<()> {
-        type Style = LabelStyle;
+        type Style = TextStyle;
 
         type Output = Label<LabelStyle>;
 
         fn with_style(self, style: Self::Style) -> Self::Output {
             Label {
                 text: self.text,
-                style,
+                style: style.into(),
             }
         }
     }

crates/theme/src/theme.rs 🔗

@@ -3,7 +3,7 @@ mod theme_registry;
 mod theme_settings;
 pub mod ui;
 
-use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle};
+use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle};
 use gpui::{
     color::Color,
     elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle},
@@ -66,6 +66,7 @@ pub struct Theme {
     pub feedback: FeedbackStyle,
     pub welcome: WelcomeStyle,
     pub titlebar: Titlebar,
+    pub component_test: ComponentTest,
 }
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]
@@ -260,6 +261,13 @@ pub struct CollabPanel {
     pub face_overlap: f32,
 }
 
+#[derive(Deserialize, Default, JsonSchema)]
+pub struct ComponentTest {
+    pub button: Interactive<ButtonStyle<TextStyle>>,
+    pub toggle: Toggleable<Interactive<ButtonStyle<TextStyle>>>,
+    pub disclosure: DisclosureStyle<TextStyle>,
+}
+
 #[derive(Deserialize, Default, JsonSchema)]
 pub struct TabbedModal {
     pub tab_button: Toggleable<Interactive<ContainedText>>,

crates/zed/Cargo.toml 🔗

@@ -25,6 +25,7 @@ cli = { path = "../cli" }
 collab_ui = { path = "../collab_ui" }
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }
+component_test = { path = "../component_test" }
 context_menu = { path = "../context_menu" }
 client = { path = "../client" }
 clock = { path = "../clock" }

crates/zed/src/main.rs 🔗

@@ -166,6 +166,7 @@ fn main() {
         terminal_view::init(cx);
         copilot::init(http.clone(), node_runtime, cx);
         ai::init(cx);
+        component_test::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
         cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))

styles/src/style_tree/app.ts 🔗

@@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification"
 import project_shared_notification from "./project_shared_notification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
-import contact_finder from "./contact_finder"
 import collab_panel from "./collab_panel"
 import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
 import incoming_call_notification from "./incoming_call_notification"
@@ -22,6 +21,7 @@ import assistant from "./assistant"
 import { titlebar } from "./titlebar"
 import editor from "./editor"
 import feedback from "./feedback"
+import component_test from "./component_test"
 import { useTheme } from "../common"
 
 export default function app(): any {
@@ -54,6 +54,7 @@ export default function app(): any {
         tooltip: tooltip(),
         terminal: terminal(),
         assistant: assistant(),
-        feedback: feedback()
+        feedback: feedback(),
+        component_test: component_test(),
     }
 }

styles/src/style_tree/component_test.ts 🔗

@@ -0,0 +1,19 @@
+import { toggle_label_button_style } from "../component/label_button"
+import { useTheme } from "../common"
+import { text_button } from "../component/text_button"
+import { toggleable_icon_button } from "../component/icon_button"
+import { text } from "./components"
+
+export default function contacts_panel(): any {
+    const theme = useTheme()
+
+    return {
+        button: text_button({}),
+        toggle: toggle_label_button_style({ active_color: "accent" }),
+        disclosure: {
+            ...text(theme.lowest, "sans", "base"),
+            button: toggleable_icon_button(theme, {}),
+            spacing: 4,
+        }
+    }
+}