gpui: Make style macros more composable (#14007)

Marshall Bowers created

This PR begins the process of breaking up the `style_helpers!` macro
into smaller macros that can be used to generate methods for a related
subset of styles.

The style method macros also now accept an optional `visibility`
parameter to control the visibility of the generated methods. This
allows for adding these methods to a struct instead of a just a trait.

For example, to expose just the padding styles on a `Facepile` we can do
this:

```rs
impl Facepile {
    fn style(&mut self) -> &mut StyleRefinement {
        self.base.style()
    }

    gpui::padding_style_methods!({
        visibility: pub
    });
}
```

Release Notes:

- N/A

Change summary

crates/gpui/src/styled.rs               |  20 -
crates/gpui_macros/src/gpui_macros.rs   |  23 +
crates/gpui_macros/src/style_helpers.rs | 312 ++++++++++++++++++--------
crates/ui/src/components/facepile.rs    |  31 +-
4 files changed, 259 insertions(+), 127 deletions(-)

Detailed changes

crates/gpui/src/styled.rs ๐Ÿ”—

@@ -1,9 +1,10 @@
 use crate::{
     self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
     DefiniteLength, Fill, FlexDirection, FlexWrap, Font, FontStyle, FontWeight, Hsla,
-    JustifyContent, Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
+    JustifyContent, Length, SharedString, StyleRefinement, Visibility, WhiteSpace,
 };
 use crate::{BoxShadow, TextStyleRefinement};
+pub use gpui_macros::{margin_style_methods, padding_style_methods, position_style_methods};
 use smallvec::{smallvec, SmallVec};
 use taffy::style::{AlignContent, Display, Overflow};
 
@@ -14,20 +15,9 @@ pub trait Styled: Sized {
     fn style(&mut self) -> &mut StyleRefinement;
 
     gpui_macros::style_helpers!();
-
-    /// Sets the position of the element to `relative`.
-    /// [Docs](https://tailwindcss.com/docs/position)
-    fn relative(mut self) -> Self {
-        self.style().position = Some(Position::Relative);
-        self
-    }
-
-    /// Sets the position of the element to `absolute`.
-    /// [Docs](https://tailwindcss.com/docs/position)
-    fn absolute(mut self) -> Self {
-        self.style().position = Some(Position::Absolute);
-        self
-    }
+    gpui_macros::margin_style_methods!();
+    gpui_macros::padding_style_methods!();
+    gpui_macros::position_style_methods!();
 
     /// Sets the display type of the element to `block`.
     /// [Docs](https://tailwindcss.com/docs/display)

crates/gpui_macros/src/gpui_macros.rs ๐Ÿ”—

@@ -27,13 +27,34 @@ pub fn derive_render(input: TokenStream) -> TokenStream {
     derive_render::derive_render(input)
 }
 
-/// Used by gpui to generate the style helpers.
+/// Used by GPUI to generate the style helpers.
 #[proc_macro]
 #[doc(hidden)]
 pub fn style_helpers(input: TokenStream) -> TokenStream {
     style_helpers::style_helpers(input)
 }
 
+/// Generates methods for margin styles.
+#[proc_macro]
+#[doc(hidden)]
+pub fn margin_style_methods(input: TokenStream) -> TokenStream {
+    style_helpers::margin_style_methods(input)
+}
+
+/// Generates methods for padding styles.
+#[proc_macro]
+#[doc(hidden)]
+pub fn padding_style_methods(input: TokenStream) -> TokenStream {
+    style_helpers::padding_style_methods(input)
+}
+
+/// Generates methods for position styles.
+#[proc_macro]
+#[doc(hidden)]
+pub fn position_style_methods(input: TokenStream) -> TokenStream {
+    style_helpers::position_style_methods(input)
+}
+
 /// #[gpui::test] can be used to annotate test functions that run with GPUI support.
 /// it supports both synchronous and asynchronous tests, and can provide you with
 /// as many `TestAppContext` instances as you need.

crates/gpui_macros/src/style_helpers.rs ๐Ÿ”—

@@ -2,15 +2,38 @@ use proc_macro::TokenStream;
 use proc_macro2::TokenStream as TokenStream2;
 use quote::{format_ident, quote};
 use syn::{
+    braced,
     parse::{Parse, ParseStream, Result},
-    parse_macro_input,
+    parse_macro_input, Token, Visibility,
 };
 
-struct StyleableMacroInput;
+#[derive(Debug)]
+struct StyleableMacroInput {
+    method_visibility: Visibility,
+}
 
 impl Parse for StyleableMacroInput {
-    fn parse(_input: ParseStream) -> Result<Self> {
-        Ok(StyleableMacroInput)
+    fn parse(input: ParseStream) -> Result<Self> {
+        if !input.peek(syn::token::Brace) {
+            return Ok(Self {
+                method_visibility: Visibility::Inherited,
+            });
+        }
+
+        let content;
+        braced!(content in input);
+
+        let mut method_visibility = None;
+
+        let ident: syn::Ident = content.parse()?;
+        if ident == "visibility" {
+            let _colon: Token![:] = content.parse()?;
+            method_visibility = Some(content.parse()?);
+        }
+
+        Ok(Self {
+            method_visibility: method_visibility.unwrap_or(Visibility::Inherited),
+        })
     }
 }
 
@@ -24,11 +47,110 @@ pub fn style_helpers(input: TokenStream) -> TokenStream {
     output.into()
 }
 
-fn generate_methods() -> Vec<TokenStream2> {
+pub fn margin_style_methods(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as StyleableMacroInput);
+    let methods = generate_box_style_methods(
+        margin_box_style_prefixes(),
+        box_style_suffixes(),
+        input.method_visibility,
+    );
+    let output = quote! {
+        #(#methods)*
+    };
+
+    output.into()
+}
+
+pub fn padding_style_methods(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as StyleableMacroInput);
+    let methods = generate_box_style_methods(
+        padding_box_style_prefixes(),
+        box_style_suffixes(),
+        input.method_visibility,
+    );
+    let output = quote! {
+        #(#methods)*
+    };
+
+    output.into()
+}
+
+pub fn position_style_methods(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as StyleableMacroInput);
+    let visibility = input.method_visibility;
+    let methods = generate_box_style_methods(
+        position_box_style_prefixes(),
+        box_style_suffixes(),
+        visibility.clone(),
+    );
+    let output = quote! {
+        /// Sets the position of the element to `relative`.
+        /// [Docs](https://tailwindcss.com/docs/position)
+        #visibility fn relative(mut self) -> Self {
+            self.style().position = Some(gpui::Position::Relative);
+            self
+        }
+
+        /// Sets the position of the element to `absolute`.
+        /// [Docs](https://tailwindcss.com/docs/position)
+        #visibility fn absolute(mut self) -> Self {
+            self.style().position = Some(gpui::Position::Absolute);
+            self
+        }
+
+        #(#methods)*
+    };
+
+    output.into()
+}
+
+struct BoxStylePrefix {
+    prefix: &'static str,
+    auto_allowed: bool,
+    fields: Vec<TokenStream2>,
+    doc_string_prefix: &'static str,
+}
+
+struct BoxStyleSuffix {
+    suffix: &'static str,
+    length_tokens: TokenStream2,
+    doc_string_suffix: &'static str,
+}
+
+struct CornerStylePrefix {
+    prefix: &'static str,
+    fields: Vec<TokenStream2>,
+    doc_string_prefix: &'static str,
+}
+
+struct CornerStyleSuffix {
+    suffix: &'static str,
+    radius_tokens: TokenStream2,
+    doc_string_suffix: &'static str,
+}
+
+struct BorderStylePrefix {
+    prefix: &'static str,
+    fields: Vec<TokenStream2>,
+    doc_string_prefix: &'static str,
+}
+
+struct BorderStyleSuffix {
+    suffix: &'static str,
+    width_tokens: TokenStream2,
+    doc_string_suffix: &'static str,
+}
+
+fn generate_box_style_methods(
+    prefixes: Vec<BoxStylePrefix>,
+    suffixes: Vec<BoxStyleSuffix>,
+    visibility: Visibility,
+) -> Vec<TokenStream2> {
     let mut methods = Vec::new();
 
-    for box_style_prefix in box_prefixes() {
+    for box_style_prefix in prefixes {
         methods.push(generate_custom_value_setter(
+            visibility.clone(),
             box_style_prefix.prefix,
             if box_style_prefix.auto_allowed {
                 quote! { Length }
@@ -39,9 +161,10 @@ fn generate_methods() -> Vec<TokenStream2> {
             &box_style_prefix.doc_string_prefix,
         ));
 
-        for box_style_suffix in box_suffixes() {
+        for box_style_suffix in &suffixes {
             if box_style_suffix.suffix != "auto" || box_style_prefix.auto_allowed {
                 methods.push(generate_predefined_setter(
+                    visibility.clone(),
                     box_style_prefix.prefix,
                     box_style_suffix.suffix,
                     &box_style_prefix.fields,
@@ -57,6 +180,7 @@ fn generate_methods() -> Vec<TokenStream2> {
 
             if box_style_suffix.suffix != "auto" {
                 methods.push(generate_predefined_setter(
+                    visibility.clone(),
                     box_style_prefix.prefix,
                     box_style_suffix.suffix,
                     &box_style_prefix.fields,
@@ -72,8 +196,17 @@ fn generate_methods() -> Vec<TokenStream2> {
         }
     }
 
+    methods
+}
+
+fn generate_methods() -> Vec<TokenStream2> {
+    let visibility = Visibility::Inherited;
+    let mut methods =
+        generate_box_style_methods(box_prefixes(), box_style_suffixes(), visibility.clone());
+
     for corner_style_prefix in corner_prefixes() {
         methods.push(generate_custom_value_setter(
+            visibility.clone(),
             corner_style_prefix.prefix,
             quote! { AbsoluteLength },
             &corner_style_prefix.fields,
@@ -82,6 +215,7 @@ fn generate_methods() -> Vec<TokenStream2> {
 
         for corner_style_suffix in corner_suffixes() {
             methods.push(generate_predefined_setter(
+                visibility.clone(),
                 corner_style_prefix.prefix,
                 corner_style_suffix.suffix,
                 &corner_style_prefix.fields,
@@ -98,6 +232,7 @@ fn generate_methods() -> Vec<TokenStream2> {
 
     for border_style_prefix in border_prefixes() {
         methods.push(generate_custom_value_setter(
+            visibility.clone(),
             border_style_prefix.prefix,
             quote! { AbsoluteLength },
             &border_style_prefix.fields,
@@ -106,6 +241,7 @@ fn generate_methods() -> Vec<TokenStream2> {
 
         for border_style_suffix in border_suffixes() {
             methods.push(generate_predefined_setter(
+                visibility.clone(),
                 border_style_prefix.prefix,
                 border_style_suffix.suffix,
                 &border_style_prefix.fields,
@@ -123,6 +259,7 @@ fn generate_methods() -> Vec<TokenStream2> {
 }
 
 fn generate_predefined_setter(
+    visibility: Visibility,
     name: &'static str,
     length: &'static str,
     fields: &[TokenStream2],
@@ -153,7 +290,7 @@ fn generate_predefined_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self) -> Self {
+        #visibility fn #method_name(mut self) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self
@@ -164,6 +301,7 @@ fn generate_predefined_setter(
 }
 
 fn generate_custom_value_setter(
+    visibility: Visibility,
     prefix: &str,
     length_type: TokenStream2,
     fields: &[TokenStream2],
@@ -186,7 +324,7 @@ fn generate_custom_value_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
+        #visibility fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self
@@ -196,65 +334,8 @@ fn generate_custom_value_setter(
     method
 }
 
-struct BoxStylePrefix {
-    prefix: &'static str,
-    auto_allowed: bool,
-    fields: Vec<TokenStream2>,
-    doc_string_prefix: &'static str,
-}
-
-fn box_prefixes() -> Vec<BoxStylePrefix> {
+fn margin_box_style_prefixes() -> Vec<BoxStylePrefix> {
     vec![
-        BoxStylePrefix {
-            prefix: "w",
-            auto_allowed: true,
-            fields: vec![quote! { size.width }],
-            doc_string_prefix: "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)",
-        },
-        BoxStylePrefix {
-            prefix: "h",
-            auto_allowed: true,
-            fields: vec![quote! { size.height }],
-            doc_string_prefix: "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)",
-        },
-        BoxStylePrefix {
-            prefix: "size",
-            auto_allowed: true,
-            fields: vec![quote! {size.width}, quote! {size.height}],
-            doc_string_prefix: "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
-        BoxStylePrefix {
-            prefix: "min_w",
-            auto_allowed: true,
-            fields: vec![quote! { min_size.width }],
-            doc_string_prefix: "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
-        BoxStylePrefix {
-            prefix: "min_h",
-            auto_allowed: true,
-            fields: vec![quote! { min_size.height }],
-            doc_string_prefix: "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
-        BoxStylePrefix {
-            prefix: "max_w",
-            auto_allowed: true,
-            fields: vec![quote! { max_size.width }],
-            doc_string_prefix: "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
-        BoxStylePrefix {
-            prefix: "max_h",
-            auto_allowed: true,
-            fields: vec![quote! { max_size.height }],
-            doc_string_prefix: "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)",
-        },
         BoxStylePrefix {
             prefix: "m",
             auto_allowed: true,
@@ -302,6 +383,11 @@ fn box_prefixes() -> Vec<BoxStylePrefix> {
             fields: vec![quote! { margin.right }],
             doc_string_prefix: "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)",
         },
+    ]
+}
+
+fn padding_box_style_prefixes() -> Vec<BoxStylePrefix> {
+    vec![
         BoxStylePrefix {
             prefix: "p",
             auto_allowed: false,
@@ -349,6 +435,11 @@ fn box_prefixes() -> Vec<BoxStylePrefix> {
             fields: vec![quote! { padding.right }],
             doc_string_prefix: "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)",
         },
+    ]
+}
+
+fn position_box_style_prefixes() -> Vec<BoxStylePrefix> {
+    vec![
         BoxStylePrefix {
             prefix: "inset",
             auto_allowed: true,
@@ -384,6 +475,61 @@ fn box_prefixes() -> Vec<BoxStylePrefix> {
             fields: vec![quote! { inset.right }],
             doc_string_prefix: "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)",
         },
+    ]
+}
+
+fn box_prefixes() -> Vec<BoxStylePrefix> {
+    vec![
+        BoxStylePrefix {
+            prefix: "w",
+            auto_allowed: true,
+            fields: vec![quote! { size.width }],
+            doc_string_prefix: "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)",
+        },
+        BoxStylePrefix {
+            prefix: "h",
+            auto_allowed: true,
+            fields: vec![quote! { size.height }],
+            doc_string_prefix: "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)",
+        },
+        BoxStylePrefix {
+            prefix: "size",
+            auto_allowed: true,
+            fields: vec![quote! {size.width}, quote! {size.height}],
+            doc_string_prefix: "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
+        BoxStylePrefix {
+            prefix: "min_w",
+            auto_allowed: true,
+            fields: vec![quote! { min_size.width }],
+            doc_string_prefix: "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
+        BoxStylePrefix {
+            prefix: "min_h",
+            auto_allowed: true,
+            fields: vec![quote! { min_size.height }],
+            doc_string_prefix: "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
+        BoxStylePrefix {
+            prefix: "max_w",
+            auto_allowed: true,
+            fields: vec![quote! { max_size.width }],
+            doc_string_prefix: "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
+        BoxStylePrefix {
+            prefix: "max_h",
+            auto_allowed: true,
+            fields: vec![quote! { max_size.height }],
+            doc_string_prefix: "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)",
+        },
         BoxStylePrefix {
             prefix: "gap",
             auto_allowed: false,
@@ -405,13 +551,7 @@ fn box_prefixes() -> Vec<BoxStylePrefix> {
     ]
 }
 
-struct BoxStyleSuffix {
-    suffix: &'static str,
-    length_tokens: TokenStream2,
-    doc_string_suffix: &'static str,
-}
-
-fn box_suffixes() -> Vec<BoxStyleSuffix> {
+fn box_style_suffixes() -> Vec<BoxStyleSuffix> {
     vec![
         BoxStyleSuffix {
             suffix: "0",
@@ -646,12 +786,6 @@ fn box_suffixes() -> Vec<BoxStyleSuffix> {
     ]
 }
 
-struct CornerStylePrefix {
-    prefix: &'static str,
-    fields: Vec<TokenStream2>,
-    doc_string_prefix: &'static str,
-}
-
 fn corner_prefixes() -> Vec<CornerStylePrefix> {
     vec![
         CornerStylePrefix {
@@ -719,12 +853,6 @@ fn corner_prefixes() -> Vec<CornerStylePrefix> {
     ]
 }
 
-struct CornerStyleSuffix {
-    suffix: &'static str,
-    radius_tokens: TokenStream2,
-    doc_string_suffix: &'static str,
-}
-
 fn corner_suffixes() -> Vec<CornerStyleSuffix> {
     vec![
         CornerStyleSuffix {
@@ -770,12 +898,6 @@ fn corner_suffixes() -> Vec<CornerStyleSuffix> {
     ]
 }
 
-struct BorderStylePrefix {
-    prefix: &'static str,
-    fields: Vec<TokenStream2>,
-    doc_string_prefix: &'static str,
-}
-
 fn border_prefixes() -> Vec<BorderStylePrefix> {
     vec![
         BorderStylePrefix {
@@ -827,12 +949,6 @@ fn border_prefixes() -> Vec<BorderStylePrefix> {
     ]
 }
 
-struct BorderStyleSuffix {
-    suffix: &'static str,
-    width_tokens: TokenStream2,
-    doc_string_suffix: &'static str,
-}
-
 fn border_suffixes() -> Vec<BorderStyleSuffix> {
     vec![
         BorderStyleSuffix {

crates/ui/src/components/facepile.rs ๐Ÿ”—

@@ -1,5 +1,5 @@
 use crate::prelude::*;
-use gpui::AnyElement;
+use gpui::{AnyElement, StyleRefinement};
 use smallvec::SmallVec;
 
 /// A facepile is a collection of faces stacked horizontallyโ€“
@@ -23,6 +23,23 @@ impl Facepile {
     }
 }
 
+impl ParentElement for Facepile {
+    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+        self.faces.extend(elements);
+    }
+}
+
+// Style methods.
+impl Facepile {
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.base.style()
+    }
+
+    gpui::padding_style_methods!({
+        visibility: pub
+    });
+}
+
 impl RenderOnce for Facepile {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
         // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
@@ -40,15 +57,3 @@ impl RenderOnce for Facepile {
             )
     }
 }
-
-impl ParentElement for Facepile {
-    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
-        self.faces.extend(elements);
-    }
-}
-
-impl Styled for Facepile {
-    fn style(&mut self) -> &mut gpui::StyleRefinement {
-        self.base.style()
-    }
-}