dynamic_spacing.rs

  1use proc_macro::TokenStream;
  2use quote::{format_ident, quote};
  3use syn::{
  4    LitInt, Token, parse::Parse, parse::ParseStream, parse_macro_input, punctuated::Punctuated,
  5};
  6
  7struct DynamicSpacingInput {
  8    values: Punctuated<DynamicSpacingValue, Token![,]>,
  9}
 10
 11// The input for the derive macro is a list of values.
 12//
 13// When a single value is provided, the standard spacing formula is
 14// used to derive the of spacing values.
 15//
 16// When a tuple of three values is provided, the values are used as
 17// the spacing values directly.
 18enum DynamicSpacingValue {
 19    Single(LitInt),
 20    Tuple(LitInt, LitInt, LitInt),
 21}
 22
 23impl Parse for DynamicSpacingInput {
 24    fn parse(input: ParseStream) -> syn::Result<Self> {
 25        Ok(DynamicSpacingInput {
 26            values: input.parse_terminated(DynamicSpacingValue::parse, Token![,])?,
 27        })
 28    }
 29}
 30
 31impl Parse for DynamicSpacingValue {
 32    fn parse(input: ParseStream) -> syn::Result<Self> {
 33        if input.peek(syn::token::Paren) {
 34            let content;
 35            syn::parenthesized!(content in input);
 36            let a: LitInt = content.parse()?;
 37            content.parse::<Token![,]>()?;
 38            let b: LitInt = content.parse()?;
 39            content.parse::<Token![,]>()?;
 40            let c: LitInt = content.parse()?;
 41            Ok(DynamicSpacingValue::Tuple(a, b, c))
 42        } else {
 43            Ok(DynamicSpacingValue::Single(input.parse()?))
 44        }
 45    }
 46}
 47
 48/// Derives the spacing method for the `DynamicSpacing` enum.
 49pub fn derive_spacing(input: TokenStream) -> TokenStream {
 50    let input = parse_macro_input!(input as DynamicSpacingInput);
 51
 52    let spacing_ratios: Vec<_> = input
 53        .values
 54        .iter()
 55        .map(|v| {
 56            let variant = match v {
 57                DynamicSpacingValue::Single(n) => {
 58                    format_ident!("Base{:02}", n.base10_parse::<u32>().unwrap())
 59                }
 60                DynamicSpacingValue::Tuple(_, b, _) => {
 61                    format_ident!("Base{:02}", b.base10_parse::<u32>().unwrap())
 62                }
 63            };
 64            match v {
 65                DynamicSpacingValue::Single(n) => {
 66                    let n = n.base10_parse::<f32>().unwrap();
 67                    quote! {
 68                        DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density {
 69                            ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX,
 70                            ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX,
 71                            ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX,
 72                        }
 73                    }
 74                }
 75                DynamicSpacingValue::Tuple(a, b, c) => {
 76                    let a = a.base10_parse::<f32>().unwrap();
 77                    let b = b.base10_parse::<f32>().unwrap();
 78                    let c = c.base10_parse::<f32>().unwrap();
 79                    quote! {
 80                        DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density {
 81                            ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX,
 82                            ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX,
 83                            ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX,
 84                        }
 85                    }
 86                }
 87            }
 88        })
 89        .collect();
 90
 91    let (variant_names, doc_strings): (Vec<_>, Vec<_>) = input
 92        .values
 93        .iter()
 94        .map(|v| {
 95            let variant = match v {
 96                DynamicSpacingValue::Single(n) => {
 97                    format_ident!("Base{:02}", n.base10_parse::<u32>().unwrap())
 98                }
 99                DynamicSpacingValue::Tuple(_, b, _) => {
100                    format_ident!("Base{:02}", b.base10_parse::<u32>().unwrap())
101                }
102            };
103            let doc_string = match v {
104                DynamicSpacingValue::Single(n) => {
105                    let n = n.base10_parse::<f32>().unwrap();
106                    let compact = (n - 4.0).max(0.0);
107                    let comfortable = n + 4.0;
108                    format!(
109                        "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.",
110                        compact, n, comfortable
111                    )
112                }
113                DynamicSpacingValue::Tuple(a, b, c) => {
114                    let a = a.base10_parse::<f32>().unwrap();
115                    let b = b.base10_parse::<f32>().unwrap();
116                    let c = c.base10_parse::<f32>().unwrap();
117                    format!(
118                        "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.",
119                        a, b, c
120                    )
121                }
122            };
123            (quote!(#variant), quote!(#doc_string))
124        })
125        .unzip();
126
127    let expanded = quote! {
128        /// A dynamic spacing system that adjusts spacing based on
129        /// [UiDensity].
130        ///
131        /// The number following "Base" refers to the base pixel size
132        /// at the default rem size and spacing settings.
133        ///
134        /// When possible, [DynamicSpacing] should be used over manual
135        /// or built-in spacing values in places dynamic spacing is needed.
136        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
137        pub enum DynamicSpacing {
138            #(
139                #[doc = #doc_strings]
140                #variant_names,
141            )*
142        }
143
144        impl DynamicSpacing {
145            /// Returns the spacing ratio, should only be used internally.
146            fn spacing_ratio(&self, cx: &App) -> f32 {
147                const BASE_REM_SIZE_IN_PX: f32 = 16.0;
148                match self {
149                    #(#spacing_ratios,)*
150                }
151            }
152
153            /// Returns the spacing value in rems.
154            pub fn rems(&self, cx: &App) -> Rems {
155                rems(self.spacing_ratio(cx))
156            }
157
158            /// Returns the spacing value in pixels.
159            pub fn px(&self, cx: &App) -> Pixels {
160                let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size(cx).into();
161                px(ui_font_size_f32 * self.spacing_ratio(cx))
162            }
163        }
164    };
165
166    TokenStream::from(expanded)
167}