tailwind_rs_core/utilities/
spacing.rs

1//! Spacing utilities for tailwind-rs
2//!
3//! This module provides utilities for padding, margin, and gap spacing.
4//! It follows Tailwind CSS conventions and provides type-safe spacing values.
5
6use crate::classes::ClassBuilder;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10/// Spacing utility values for padding and margin
11/// 
12/// # Examples
13/// 
14/// ```rust
15/// use tailwind_rs_core::utilities::spacing::SpacingValue;
16/// use tailwind_rs_core::classes::ClassBuilder;
17/// 
18/// let classes = ClassBuilder::new()
19///     .padding(SpacingValue::Integer(4))
20///     .padding_x(SpacingValue::Integer(6))
21///     .build();
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
24pub enum SpacingValue {
25    /// Zero spacing (0)
26    Zero,
27    /// 1px spacing
28    Px,
29    /// Fractional spacing (0.5, 1.5, 2.5, 3.5)
30    Fractional(f32),
31    /// Integer spacing (1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64, 72, 80, 96)
32    Integer(u32),
33    /// Auto spacing
34    Auto,
35    /// Full spacing (100%)
36    Full,
37    /// Screen spacing (100vh)
38    Screen,
39    /// Min content spacing
40    Min,
41    /// Max content spacing
42    Max,
43    /// Fit content spacing
44    Fit,
45}
46
47impl std::hash::Hash for SpacingValue {
48    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
49        match self {
50            SpacingValue::Zero => 0u8.hash(state),
51            SpacingValue::Px => 1u8.hash(state),
52            SpacingValue::Fractional(f) => {
53                2u8.hash(state);
54                // Convert f32 to u32 for hashing (multiply by 1000 to preserve 3 decimal places)
55                ((f * 1000.0) as u32).hash(state);
56            }
57            SpacingValue::Integer(i) => {
58                3u8.hash(state);
59                i.hash(state);
60            }
61            SpacingValue::Auto => 4u8.hash(state),
62            SpacingValue::Full => 5u8.hash(state),
63            SpacingValue::Screen => 6u8.hash(state),
64            SpacingValue::Min => 7u8.hash(state),
65            SpacingValue::Max => 8u8.hash(state),
66            SpacingValue::Fit => 9u8.hash(state),
67        }
68    }
69}
70
71impl std::cmp::Eq for SpacingValue {}
72
73impl SpacingValue {
74    /// Convert to CSS value
75    pub fn to_css_value(&self) -> String {
76        match self {
77            SpacingValue::Zero => "0".to_string(),
78            SpacingValue::Px => "1px".to_string(),
79            SpacingValue::Fractional(f) => format!("{}rem", f * 0.25), // 0.25rem per unit
80            SpacingValue::Integer(i) => format!("{}rem", *i as f32 * 0.25),
81            SpacingValue::Auto => "auto".to_string(),
82            SpacingValue::Full => "100%".to_string(),
83            SpacingValue::Screen => "100vh".to_string(),
84            SpacingValue::Min => "min-content".to_string(),
85            SpacingValue::Max => "max-content".to_string(),
86            SpacingValue::Fit => "fit-content".to_string(),
87        }
88    }
89    
90    /// Convert to class name
91    pub fn to_class_name(&self) -> String {
92        match self {
93            SpacingValue::Zero => "0".to_string(),
94            SpacingValue::Px => "px".to_string(),
95            SpacingValue::Fractional(f) => format!("{}", f),
96            SpacingValue::Integer(i) => i.to_string(),
97            SpacingValue::Auto => "auto".to_string(),
98            SpacingValue::Full => "full".to_string(),
99            SpacingValue::Screen => "screen".to_string(),
100            SpacingValue::Min => "min".to_string(),
101            SpacingValue::Max => "max".to_string(),
102            SpacingValue::Fit => "fit".to_string(),
103        }
104    }
105    
106    /// Get all valid spacing values
107    pub fn all_values() -> Vec<SpacingValue> {
108        vec![
109            SpacingValue::Zero,
110            SpacingValue::Px,
111            SpacingValue::Fractional(0.5),
112            SpacingValue::Fractional(1.5),
113            SpacingValue::Fractional(2.5),
114            SpacingValue::Fractional(3.5),
115            SpacingValue::Integer(1),
116            SpacingValue::Integer(2),
117            SpacingValue::Integer(3),
118            SpacingValue::Integer(4),
119            SpacingValue::Integer(5),
120            SpacingValue::Integer(6),
121            SpacingValue::Integer(8),
122            SpacingValue::Integer(10),
123            SpacingValue::Integer(12),
124            SpacingValue::Integer(16),
125            SpacingValue::Integer(20),
126            SpacingValue::Integer(24),
127            SpacingValue::Integer(32),
128            SpacingValue::Integer(40),
129            SpacingValue::Integer(48),
130            SpacingValue::Integer(56),
131            SpacingValue::Integer(64),
132            SpacingValue::Integer(72),
133            SpacingValue::Integer(80),
134            SpacingValue::Integer(96),
135            SpacingValue::Auto,
136            SpacingValue::Full,
137            SpacingValue::Screen,
138            SpacingValue::Min,
139            SpacingValue::Max,
140            SpacingValue::Fit,
141        ]
142    }
143}
144
145impl fmt::Display for SpacingValue {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        write!(f, "{}", self.to_class_name())
148    }
149}
150
151/// Trait for adding padding utilities to a class builder
152pub trait PaddingUtilities {
153    /// Add padding to all sides
154    fn padding(self, value: SpacingValue) -> Self;
155    
156    /// Add horizontal padding (left and right)
157    fn padding_x(self, value: SpacingValue) -> Self;
158    
159    /// Add vertical padding (top and bottom)
160    fn padding_y(self, value: SpacingValue) -> Self;
161    
162    /// Add top padding
163    fn padding_top(self, value: SpacingValue) -> Self;
164    
165    /// Add right padding
166    fn padding_right(self, value: SpacingValue) -> Self;
167    
168    /// Add bottom padding
169    fn padding_bottom(self, value: SpacingValue) -> Self;
170    
171    /// Add left padding
172    fn padding_left(self, value: SpacingValue) -> Self;
173    
174    /// Add padding to start (left in LTR, right in RTL)
175    fn padding_start(self, value: SpacingValue) -> Self;
176    
177    /// Add padding to end (right in LTR, left in RTL)
178    fn padding_end(self, value: SpacingValue) -> Self;
179}
180
181impl PaddingUtilities for ClassBuilder {
182    fn padding(self, value: SpacingValue) -> Self {
183        self.class(format!("p-{}", value.to_class_name()))
184    }
185    
186    fn padding_x(self, value: SpacingValue) -> Self {
187        self.class(format!("px-{}", value.to_class_name()))
188    }
189    
190    fn padding_y(self, value: SpacingValue) -> Self {
191        self.class(format!("py-{}", value.to_class_name()))
192    }
193    
194    fn padding_top(self, value: SpacingValue) -> Self {
195        self.class(format!("pt-{}", value.to_class_name()))
196    }
197    
198    fn padding_right(self, value: SpacingValue) -> Self {
199        self.class(format!("pr-{}", value.to_class_name()))
200    }
201    
202    fn padding_bottom(self, value: SpacingValue) -> Self {
203        self.class(format!("pb-{}", value.to_class_name()))
204    }
205    
206    fn padding_left(self, value: SpacingValue) -> Self {
207        self.class(format!("pl-{}", value.to_class_name()))
208    }
209    
210    fn padding_start(self, value: SpacingValue) -> Self {
211        self.class(format!("ps-{}", value.to_class_name()))
212    }
213    
214    fn padding_end(self, value: SpacingValue) -> Self {
215        self.class(format!("pe-{}", value.to_class_name()))
216    }
217}
218
219/// Trait for adding margin utilities to a class builder
220pub trait MarginUtilities {
221    /// Add margin to all sides
222    fn margin(self, value: SpacingValue) -> Self;
223    
224    /// Add horizontal margin (left and right)
225    fn margin_x(self, value: SpacingValue) -> Self;
226    
227    /// Add vertical margin (top and bottom)
228    fn margin_y(self, value: SpacingValue) -> Self;
229    
230    /// Add top margin
231    fn margin_top(self, value: SpacingValue) -> Self;
232    
233    /// Add right margin
234    fn margin_right(self, value: SpacingValue) -> Self;
235    
236    /// Add bottom margin
237    fn margin_bottom(self, value: SpacingValue) -> Self;
238    
239    /// Add left margin
240    fn margin_left(self, value: SpacingValue) -> Self;
241    
242    /// Add margin to start (left in LTR, right in RTL)
243    fn margin_start(self, value: SpacingValue) -> Self;
244    
245    /// Add margin to end (right in LTR, left in RTL)
246    fn margin_end(self, value: SpacingValue) -> Self;
247    
248    /// Add negative margin to all sides
249    fn margin_negative(self, value: SpacingValue) -> Self;
250    
251    /// Add negative horizontal margin
252    fn margin_x_negative(self, value: SpacingValue) -> Self;
253    
254    /// Add negative vertical margin
255    fn margin_y_negative(self, value: SpacingValue) -> Self;
256    
257    /// Add negative top margin
258    fn margin_top_negative(self, value: SpacingValue) -> Self;
259    
260    /// Add negative right margin
261    fn margin_right_negative(self, value: SpacingValue) -> Self;
262    
263    /// Add negative bottom margin
264    fn margin_bottom_negative(self, value: SpacingValue) -> Self;
265    
266    /// Add negative left margin
267    fn margin_left_negative(self, value: SpacingValue) -> Self;
268}
269
270impl MarginUtilities for ClassBuilder {
271    fn margin(self, value: SpacingValue) -> Self {
272        self.class(format!("m-{}", value.to_class_name()))
273    }
274    
275    fn margin_x(self, value: SpacingValue) -> Self {
276        self.class(format!("mx-{}", value.to_class_name()))
277    }
278    
279    fn margin_y(self, value: SpacingValue) -> Self {
280        self.class(format!("my-{}", value.to_class_name()))
281    }
282    
283    fn margin_top(self, value: SpacingValue) -> Self {
284        self.class(format!("mt-{}", value.to_class_name()))
285    }
286    
287    fn margin_right(self, value: SpacingValue) -> Self {
288        self.class(format!("mr-{}", value.to_class_name()))
289    }
290    
291    fn margin_bottom(self, value: SpacingValue) -> Self {
292        self.class(format!("mb-{}", value.to_class_name()))
293    }
294    
295    fn margin_left(self, value: SpacingValue) -> Self {
296        self.class(format!("ml-{}", value.to_class_name()))
297    }
298    
299    fn margin_start(self, value: SpacingValue) -> Self {
300        self.class(format!("ms-{}", value.to_class_name()))
301    }
302    
303    fn margin_end(self, value: SpacingValue) -> Self {
304        self.class(format!("me-{}", value.to_class_name()))
305    }
306    
307    fn margin_negative(self, value: SpacingValue) -> Self {
308        self.class(format!("-m-{}", value.to_class_name()))
309    }
310    
311    fn margin_x_negative(self, value: SpacingValue) -> Self {
312        self.class(format!("-mx-{}", value.to_class_name()))
313    }
314    
315    fn margin_y_negative(self, value: SpacingValue) -> Self {
316        self.class(format!("-my-{}", value.to_class_name()))
317    }
318    
319    fn margin_top_negative(self, value: SpacingValue) -> Self {
320        self.class(format!("-mt-{}", value.to_class_name()))
321    }
322    
323    fn margin_right_negative(self, value: SpacingValue) -> Self {
324        self.class(format!("-mr-{}", value.to_class_name()))
325    }
326    
327    fn margin_bottom_negative(self, value: SpacingValue) -> Self {
328        self.class(format!("-mb-{}", value.to_class_name()))
329    }
330    
331    fn margin_left_negative(self, value: SpacingValue) -> Self {
332        self.class(format!("-ml-{}", value.to_class_name()))
333    }
334}
335
336/// Trait for adding gap utilities to a class builder
337pub trait GapUtilities {
338    /// Add gap between grid/flex items
339    fn gap(self, value: SpacingValue) -> Self;
340    
341    /// Add horizontal gap between grid/flex items
342    fn gap_x(self, value: SpacingValue) -> Self;
343    
344    /// Add vertical gap between grid/flex items
345    fn gap_y(self, value: SpacingValue) -> Self;
346}
347
348impl GapUtilities for ClassBuilder {
349    fn gap(self, value: SpacingValue) -> Self {
350        self.class(format!("gap-{}", value.to_class_name()))
351    }
352    
353    fn gap_x(self, value: SpacingValue) -> Self {
354        self.class(format!("gap-x-{}", value.to_class_name()))
355    }
356    
357    fn gap_y(self, value: SpacingValue) -> Self {
358        self.class(format!("gap-y-{}", value.to_class_name()))
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    
366    #[test]
367    fn test_spacing_value_to_css_value() {
368        assert_eq!(SpacingValue::Zero.to_css_value(), "0");
369        assert_eq!(SpacingValue::Px.to_css_value(), "1px");
370        assert_eq!(SpacingValue::Fractional(0.5).to_css_value(), "0.125rem");
371        assert_eq!(SpacingValue::Integer(4).to_css_value(), "1rem");
372        assert_eq!(SpacingValue::Auto.to_css_value(), "auto");
373        assert_eq!(SpacingValue::Full.to_css_value(), "100%");
374        assert_eq!(SpacingValue::Screen.to_css_value(), "100vh");
375    }
376    
377    #[test]
378    fn test_spacing_value_to_class_name() {
379        assert_eq!(SpacingValue::Zero.to_class_name(), "0");
380        assert_eq!(SpacingValue::Px.to_class_name(), "px");
381        assert_eq!(SpacingValue::Fractional(0.5).to_class_name(), "0.5");
382        assert_eq!(SpacingValue::Integer(4).to_class_name(), "4");
383        assert_eq!(SpacingValue::Auto.to_class_name(), "auto");
384        assert_eq!(SpacingValue::Full.to_class_name(), "full");
385        assert_eq!(SpacingValue::Screen.to_class_name(), "screen");
386    }
387    
388    #[test]
389    fn test_padding_utilities() {
390        let classes = ClassBuilder::new()
391            .padding(SpacingValue::Integer(4))
392            .padding_x(SpacingValue::Integer(6))
393            .padding_y(SpacingValue::Integer(2))
394            .build();
395        
396        let css_classes = classes.to_css_classes();
397        assert!(css_classes.contains("p-4"));
398        assert!(css_classes.contains("px-6"));
399        assert!(css_classes.contains("py-2"));
400    }
401    
402    #[test]
403    fn test_margin_utilities() {
404        let classes = ClassBuilder::new()
405            .margin(SpacingValue::Integer(8))
406            .margin_x(SpacingValue::Integer(4))
407            .margin_y(SpacingValue::Integer(2))
408            .build();
409        
410        let css_classes = classes.to_css_classes();
411        assert!(css_classes.contains("m-8"));
412        assert!(css_classes.contains("mx-4"));
413        assert!(css_classes.contains("my-2"));
414    }
415    
416    #[test]
417    fn test_negative_margin_utilities() {
418        let classes = ClassBuilder::new()
419            .margin_negative(SpacingValue::Integer(4))
420            .margin_x_negative(SpacingValue::Integer(2))
421            .margin_y_negative(SpacingValue::Integer(1))
422            .build();
423        
424        let css_classes = classes.to_css_classes();
425        assert!(css_classes.contains("-m-4"));
426        assert!(css_classes.contains("-mx-2"));
427        assert!(css_classes.contains("-my-1"));
428    }
429    
430    #[test]
431    fn test_gap_utilities() {
432        let classes = ClassBuilder::new()
433            .gap(SpacingValue::Integer(4))
434            .gap_x(SpacingValue::Integer(6))
435            .gap_y(SpacingValue::Integer(2))
436            .build();
437        
438        let css_classes = classes.to_css_classes();
439        assert!(css_classes.contains("gap-4"));
440        assert!(css_classes.contains("gap-x-6"));
441        assert!(css_classes.contains("gap-y-2"));
442    }
443    
444    #[test]
445    fn test_fractional_spacing() {
446        let classes = ClassBuilder::new()
447            .padding(SpacingValue::Fractional(0.5))
448            .padding_x(SpacingValue::Fractional(1.5))
449            .padding_y(SpacingValue::Fractional(2.5))
450            .build();
451        
452        let css_classes = classes.to_css_classes();
453        assert!(css_classes.contains("p-0.5"));
454        assert!(css_classes.contains("px-1.5"));
455        assert!(css_classes.contains("py-2.5"));
456    }
457    
458    #[test]
459    fn test_special_spacing_values() {
460        let classes = ClassBuilder::new()
461            .padding(SpacingValue::Auto)
462            .margin(SpacingValue::Full)
463            .gap(SpacingValue::Screen)
464            .build();
465        
466        let css_classes = classes.to_css_classes();
467        assert!(css_classes.contains("p-auto"));
468        assert!(css_classes.contains("m-full"));
469        assert!(css_classes.contains("gap-screen"));
470    }
471}