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}