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, PaddingUtilities};
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::Integer(1),
113            SpacingValue::Fractional(1.5),
114            SpacingValue::Integer(2),
115            SpacingValue::Fractional(2.5),
116            SpacingValue::Integer(3),
117            SpacingValue::Fractional(3.5),
118            SpacingValue::Integer(4),
119            SpacingValue::Integer(5),
120            SpacingValue::Integer(6),
121            SpacingValue::Integer(7),
122            SpacingValue::Integer(8),
123            SpacingValue::Integer(9),
124            SpacingValue::Integer(10),
125            SpacingValue::Integer(11),
126            SpacingValue::Integer(12),
127            SpacingValue::Integer(14),
128            SpacingValue::Integer(16),
129            SpacingValue::Integer(20),
130            SpacingValue::Integer(24),
131            SpacingValue::Integer(28),
132            SpacingValue::Integer(32),
133            SpacingValue::Integer(36),
134            SpacingValue::Integer(40),
135            SpacingValue::Integer(44),
136            SpacingValue::Integer(48),
137            SpacingValue::Integer(52),
138            SpacingValue::Integer(56),
139            SpacingValue::Integer(60),
140            SpacingValue::Integer(64),
141            SpacingValue::Integer(72),
142            SpacingValue::Integer(80),
143            SpacingValue::Integer(96),
144            SpacingValue::Auto,
145            SpacingValue::Full,
146            SpacingValue::Screen,
147            SpacingValue::Min,
148            SpacingValue::Max,
149            SpacingValue::Fit,
150        ]
151    }
152}
153
154impl fmt::Display for SpacingValue {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        write!(f, "{}", self.to_class_name())
157    }
158}
159
160/// Trait for adding padding utilities to a class builder
161pub trait PaddingUtilities {
162    /// Add padding to all sides
163    fn padding(self, value: SpacingValue) -> Self;
164    
165    /// Add horizontal padding (left and right)
166    fn padding_x(self, value: SpacingValue) -> Self;
167    
168    /// Add vertical padding (top and bottom)
169    fn padding_y(self, value: SpacingValue) -> Self;
170    
171    /// Add top padding
172    fn padding_top(self, value: SpacingValue) -> Self;
173    
174    /// Add right padding
175    fn padding_right(self, value: SpacingValue) -> Self;
176    
177    /// Add bottom padding
178    fn padding_bottom(self, value: SpacingValue) -> Self;
179    
180    /// Add left padding
181    fn padding_left(self, value: SpacingValue) -> Self;
182    
183    /// Add padding to start (left in LTR, right in RTL)
184    fn padding_start(self, value: SpacingValue) -> Self;
185    
186    /// Add padding to end (right in LTR, left in RTL)
187    fn padding_end(self, value: SpacingValue) -> Self;
188}
189
190impl PaddingUtilities for ClassBuilder {
191    fn padding(self, value: SpacingValue) -> Self {
192        self.class(format!("p-{}", value.to_class_name()))
193    }
194    
195    fn padding_x(self, value: SpacingValue) -> Self {
196        self.class(format!("px-{}", value.to_class_name()))
197    }
198    
199    fn padding_y(self, value: SpacingValue) -> Self {
200        self.class(format!("py-{}", value.to_class_name()))
201    }
202    
203    fn padding_top(self, value: SpacingValue) -> Self {
204        self.class(format!("pt-{}", value.to_class_name()))
205    }
206    
207    fn padding_right(self, value: SpacingValue) -> Self {
208        self.class(format!("pr-{}", value.to_class_name()))
209    }
210    
211    fn padding_bottom(self, value: SpacingValue) -> Self {
212        self.class(format!("pb-{}", value.to_class_name()))
213    }
214    
215    fn padding_left(self, value: SpacingValue) -> Self {
216        self.class(format!("pl-{}", value.to_class_name()))
217    }
218    
219    fn padding_start(self, value: SpacingValue) -> Self {
220        self.class(format!("ps-{}", value.to_class_name()))
221    }
222    
223    fn padding_end(self, value: SpacingValue) -> Self {
224        self.class(format!("pe-{}", value.to_class_name()))
225    }
226}
227
228/// Trait for adding margin utilities to a class builder
229pub trait MarginUtilities {
230    /// Add margin to all sides
231    fn margin(self, value: SpacingValue) -> Self;
232    
233    /// Add horizontal margin (left and right)
234    fn margin_x(self, value: SpacingValue) -> Self;
235    
236    /// Add vertical margin (top and bottom)
237    fn margin_y(self, value: SpacingValue) -> Self;
238    
239    /// Add top margin
240    fn margin_top(self, value: SpacingValue) -> Self;
241    
242    /// Add right margin
243    fn margin_right(self, value: SpacingValue) -> Self;
244    
245    /// Add bottom margin
246    fn margin_bottom(self, value: SpacingValue) -> Self;
247    
248    /// Add left margin
249    fn margin_left(self, value: SpacingValue) -> Self;
250    
251    /// Add margin to start (left in LTR, right in RTL)
252    fn margin_start(self, value: SpacingValue) -> Self;
253    
254    /// Add margin to end (right in LTR, left in RTL)
255    fn margin_end(self, value: SpacingValue) -> Self;
256    
257    /// Add negative margin to all sides
258    fn margin_negative(self, value: SpacingValue) -> Self;
259    
260    /// Add negative horizontal margin
261    fn margin_x_negative(self, value: SpacingValue) -> Self;
262    
263    /// Add negative vertical margin
264    fn margin_y_negative(self, value: SpacingValue) -> Self;
265    
266    /// Add negative top margin
267    fn margin_top_negative(self, value: SpacingValue) -> Self;
268    
269    /// Add negative right margin
270    fn margin_right_negative(self, value: SpacingValue) -> Self;
271    
272    /// Add negative bottom margin
273    fn margin_bottom_negative(self, value: SpacingValue) -> Self;
274    
275    /// Add negative left margin
276    fn margin_left_negative(self, value: SpacingValue) -> Self;
277}
278
279impl MarginUtilities for ClassBuilder {
280    fn margin(self, value: SpacingValue) -> Self {
281        self.class(format!("m-{}", value.to_class_name()))
282    }
283    
284    fn margin_x(self, value: SpacingValue) -> Self {
285        self.class(format!("mx-{}", value.to_class_name()))
286    }
287    
288    fn margin_y(self, value: SpacingValue) -> Self {
289        self.class(format!("my-{}", value.to_class_name()))
290    }
291    
292    fn margin_top(self, value: SpacingValue) -> Self {
293        self.class(format!("mt-{}", value.to_class_name()))
294    }
295    
296    fn margin_right(self, value: SpacingValue) -> Self {
297        self.class(format!("mr-{}", value.to_class_name()))
298    }
299    
300    fn margin_bottom(self, value: SpacingValue) -> Self {
301        self.class(format!("mb-{}", value.to_class_name()))
302    }
303    
304    fn margin_left(self, value: SpacingValue) -> Self {
305        self.class(format!("ml-{}", value.to_class_name()))
306    }
307    
308    fn margin_start(self, value: SpacingValue) -> Self {
309        self.class(format!("ms-{}", value.to_class_name()))
310    }
311    
312    fn margin_end(self, value: SpacingValue) -> Self {
313        self.class(format!("me-{}", value.to_class_name()))
314    }
315    
316    fn margin_negative(self, value: SpacingValue) -> Self {
317        self.class(format!("-m-{}", value.to_class_name()))
318    }
319    
320    fn margin_x_negative(self, value: SpacingValue) -> Self {
321        self.class(format!("-mx-{}", value.to_class_name()))
322    }
323    
324    fn margin_y_negative(self, value: SpacingValue) -> Self {
325        self.class(format!("-my-{}", value.to_class_name()))
326    }
327    
328    fn margin_top_negative(self, value: SpacingValue) -> Self {
329        self.class(format!("-mt-{}", value.to_class_name()))
330    }
331    
332    fn margin_right_negative(self, value: SpacingValue) -> Self {
333        self.class(format!("-mr-{}", value.to_class_name()))
334    }
335    
336    fn margin_bottom_negative(self, value: SpacingValue) -> Self {
337        self.class(format!("-mb-{}", value.to_class_name()))
338    }
339    
340    fn margin_left_negative(self, value: SpacingValue) -> Self {
341        self.class(format!("-ml-{}", value.to_class_name()))
342    }
343}
344
345/// Trait for adding gap utilities to a class builder
346pub trait GapUtilities {
347    /// Add gap between grid/flex items
348    fn gap(self, value: SpacingValue) -> Self;
349    
350    /// Add horizontal gap between grid/flex items
351    fn gap_x(self, value: SpacingValue) -> Self;
352    
353    /// Add vertical gap between grid/flex items
354    fn gap_y(self, value: SpacingValue) -> Self;
355}
356
357impl GapUtilities for ClassBuilder {
358    fn gap(self, value: SpacingValue) -> Self {
359        self.class(format!("gap-{}", value.to_class_name()))
360    }
361    
362    fn gap_x(self, value: SpacingValue) -> Self {
363        self.class(format!("gap-x-{}", value.to_class_name()))
364    }
365    
366    fn gap_y(self, value: SpacingValue) -> Self {
367        self.class(format!("gap-y-{}", value.to_class_name()))
368    }
369}
370
371/// Trait for adding space-between utilities to a class builder
372pub trait SpaceBetweenUtilities {
373    /// Add horizontal space between child elements
374    fn space_x(self, value: SpacingValue) -> Self;
375    
376    /// Add vertical space between child elements
377    fn space_y(self, value: SpacingValue) -> Self;
378    
379    /// Reverse horizontal space between child elements
380    fn space_x_reverse(self) -> Self;
381    
382    /// Reverse vertical space between child elements
383    fn space_y_reverse(self) -> Self;
384}
385
386impl SpaceBetweenUtilities for ClassBuilder {
387    fn space_x(self, value: SpacingValue) -> Self {
388        self.class(format!("space-x-{}", value.to_class_name()))
389    }
390    
391    fn space_y(self, value: SpacingValue) -> Self {
392        self.class(format!("space-y-{}", value.to_class_name()))
393    }
394    
395    fn space_x_reverse(self) -> Self {
396        self.class("space-x-reverse".to_string())
397    }
398    
399    fn space_y_reverse(self) -> Self {
400        self.class("space-y-reverse".to_string())
401    }
402}
403
404/// Convenience methods for space-between utilities
405impl ClassBuilder {
406    /// Add horizontal space between child elements with value 2
407    pub fn space_x_2(self) -> Self {
408        self.space_x(SpacingValue::Integer(2))
409    }
410    
411    /// Add horizontal space between child elements with value 4
412    pub fn space_x_4(self) -> Self {
413        self.space_x(SpacingValue::Integer(4))
414    }
415    
416    /// Add vertical space between child elements with value 2
417    pub fn space_y_2(self) -> Self {
418        self.space_y(SpacingValue::Integer(2))
419    }
420    
421    /// Add vertical space between child elements with value 4
422    pub fn space_y_4(self) -> Self {
423        self.space_y(SpacingValue::Integer(4))
424    }
425}
426
427/// Trait for adding divide utilities to a class builder
428pub trait SpacingDivideUtilities {
429    /// Add horizontal divider between child elements
430    fn divide_x(self, value: SpacingValue) -> Self;
431    
432    /// Add vertical divider between child elements
433    fn divide_y(self, value: SpacingValue) -> Self;
434    
435    /// Reverse horizontal divider between child elements
436    fn divide_x_reverse(self) -> Self;
437    
438    /// Reverse vertical divider between child elements
439    fn divide_y_reverse(self) -> Self;
440}
441
442impl SpacingDivideUtilities for ClassBuilder {
443    fn divide_x(self, value: SpacingValue) -> Self {
444        self.class(format!("divide-x-{}", value.to_class_name()))
445    }
446    
447    fn divide_y(self, value: SpacingValue) -> Self {
448        self.class(format!("divide-y-{}", value.to_class_name()))
449    }
450    
451    fn divide_x_reverse(self) -> Self {
452        self.class("divide-x-reverse".to_string())
453    }
454    
455    fn divide_y_reverse(self) -> Self {
456        self.class("divide-y-reverse".to_string())
457    }
458}
459
460/// Convenience methods for divide utilities
461impl ClassBuilder {
462    /// Add horizontal divider between child elements with value 2
463    pub fn divide_x_2(self) -> Self {
464        self.divide_x(SpacingValue::Integer(2))
465    }
466    
467    /// Add horizontal divider between child elements with value 4
468    pub fn divide_x_4(self) -> Self {
469        self.divide_x(SpacingValue::Integer(4))
470    }
471    
472    /// Add vertical divider between child elements with value 2
473    pub fn divide_y_2(self) -> Self {
474        self.divide_y(SpacingValue::Integer(2))
475    }
476    
477    /// Add vertical divider between child elements with value 4
478    pub fn divide_y_4(self) -> Self {
479        self.divide_y(SpacingValue::Integer(4))
480    }
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    
487    #[test]
488    fn test_spacing_value_to_css_value() {
489        assert_eq!(SpacingValue::Zero.to_css_value(), "0");
490        assert_eq!(SpacingValue::Px.to_css_value(), "1px");
491        assert_eq!(SpacingValue::Fractional(0.5).to_css_value(), "0.125rem");
492        assert_eq!(SpacingValue::Integer(4).to_css_value(), "1rem");
493        assert_eq!(SpacingValue::Auto.to_css_value(), "auto");
494        assert_eq!(SpacingValue::Full.to_css_value(), "100%");
495        assert_eq!(SpacingValue::Screen.to_css_value(), "100vh");
496    }
497    
498    #[test]
499    fn test_spacing_value_to_class_name() {
500        assert_eq!(SpacingValue::Zero.to_class_name(), "0");
501        assert_eq!(SpacingValue::Px.to_class_name(), "px");
502        assert_eq!(SpacingValue::Fractional(0.5).to_class_name(), "0.5");
503        assert_eq!(SpacingValue::Integer(4).to_class_name(), "4");
504        assert_eq!(SpacingValue::Auto.to_class_name(), "auto");
505        assert_eq!(SpacingValue::Full.to_class_name(), "full");
506        assert_eq!(SpacingValue::Screen.to_class_name(), "screen");
507    }
508    
509    #[test]
510    fn test_padding_utilities() {
511        let classes = ClassBuilder::new()
512            .padding(SpacingValue::Integer(4))
513            .padding_x(SpacingValue::Integer(6))
514            .padding_y(SpacingValue::Integer(2))
515            .build();
516        
517        let css_classes = classes.to_css_classes();
518        assert!(css_classes.contains("p-4"));
519        assert!(css_classes.contains("px-6"));
520        assert!(css_classes.contains("py-2"));
521    }
522    
523    #[test]
524    fn test_margin_utilities() {
525        let classes = ClassBuilder::new()
526            .margin(SpacingValue::Integer(8))
527            .margin_x(SpacingValue::Integer(4))
528            .margin_y(SpacingValue::Integer(2))
529            .build();
530        
531        let css_classes = classes.to_css_classes();
532        assert!(css_classes.contains("m-8"));
533        assert!(css_classes.contains("mx-4"));
534        assert!(css_classes.contains("my-2"));
535    }
536    
537    #[test]
538    fn test_negative_margin_utilities() {
539        let classes = ClassBuilder::new()
540            .margin_negative(SpacingValue::Integer(4))
541            .margin_x_negative(SpacingValue::Integer(2))
542            .margin_y_negative(SpacingValue::Integer(1))
543            .build();
544        
545        let css_classes = classes.to_css_classes();
546        assert!(css_classes.contains("-m-4"));
547        assert!(css_classes.contains("-mx-2"));
548        assert!(css_classes.contains("-my-1"));
549    }
550    
551    #[test]
552    fn test_gap_utilities() {
553        let classes = ClassBuilder::new()
554            .gap(SpacingValue::Integer(4))
555            .gap_x(SpacingValue::Integer(6))
556            .gap_y(SpacingValue::Integer(2))
557            .build();
558        
559        let css_classes = classes.to_css_classes();
560        assert!(css_classes.contains("gap-4"));
561        assert!(css_classes.contains("gap-x-6"));
562        assert!(css_classes.contains("gap-y-2"));
563    }
564    
565    #[test]
566    fn test_fractional_spacing() {
567        let classes = ClassBuilder::new()
568            .padding(SpacingValue::Fractional(0.5))
569            .padding_x(SpacingValue::Fractional(1.5))
570            .padding_y(SpacingValue::Fractional(2.5))
571            .build();
572        
573        let css_classes = classes.to_css_classes();
574        assert!(css_classes.contains("p-0.5"));
575        assert!(css_classes.contains("px-1.5"));
576        assert!(css_classes.contains("py-2.5"));
577    }
578    
579    #[test]
580    fn test_special_spacing_values() {
581        let classes = ClassBuilder::new()
582            .padding(SpacingValue::Auto)
583            .margin(SpacingValue::Full)
584            .gap(SpacingValue::Screen)
585            .build();
586        
587        let css_classes = classes.to_css_classes();
588        assert!(css_classes.contains("p-auto"));
589        assert!(css_classes.contains("m-full"));
590        assert!(css_classes.contains("gap-screen"));
591    }
592
593    /// Test that all Tailwind CSS spacing values are supported
594    #[test]
595    fn test_all_tailwind_spacing_values() {
596        // Tailwind CSS spacing scale: 0, px, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96
597        let expected_values = vec![
598            "0", "px", "0.5", "1", "1.5", "2", "2.5", "3", "3.5", "4", "5", "6", "7", "8", "9", "10", 
599            "11", "12", "14", "16", "20", "24", "28", "32", "36", "40", "44", "48", "52", "56", "60", 
600            "64", "72", "80", "96"
601        ];
602        
603        let actual_values: Vec<String> = SpacingValue::all_values()
604            .iter()
605            .map(|v| v.to_class_name())
606            .collect();
607        
608        for expected in expected_values {
609            assert!(
610                actual_values.contains(&expected.to_string()),
611                "Missing spacing value: {}",
612                expected
613            );
614        }
615    }
616
617    /// Test that space-between utilities are implemented
618    #[test]
619    fn test_space_between_utilities() {
620        let classes = ClassBuilder::new()
621            .space_x_4()  // space-x-4
622            .space_y_2()  // space-y-2
623            .space_x_reverse()  // space-x-reverse
624            .space_y_reverse()  // space-y-reverse
625            .build();
626        
627        let css_classes = classes.to_css_classes();
628        assert!(css_classes.contains("space-x-4"));
629        assert!(css_classes.contains("space-y-2"));
630        assert!(css_classes.contains("space-x-reverse"));
631        assert!(css_classes.contains("space-y-reverse"));
632    }
633
634    /// Test that divide utilities are implemented
635    #[test]
636    fn test_divide_utilities() {
637        let classes = ClassBuilder::new()
638            .divide_x_2()  // divide-x-2
639            .divide_y_4()  // divide-y-4
640            .divide_x_reverse()  // divide-x-reverse
641            .divide_y_reverse()  // divide-y-reverse
642            .build();
643        
644        let css_classes = classes.to_css_classes();
645        assert!(css_classes.contains("divide-x-2"));
646        assert!(css_classes.contains("divide-y-4"));
647        assert!(css_classes.contains("divide-x-reverse"));
648        assert!(css_classes.contains("divide-y-reverse"));
649    }
650}