tailwind_rs_core/responsive/
grid.rs

1//! # Grid Responsive Utilities
2//!
3//! This module provides grid-specific responsive utilities.
4
5use super::breakpoints::Breakpoint;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Responsive grid container
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ResponsiveGrid {
12    /// Number of columns for each breakpoint
13    pub columns: HashMap<Breakpoint, u32>,
14    /// Gap between grid items for each breakpoint
15    pub gap: HashMap<Breakpoint, u32>,
16    /// Row gap for each breakpoint
17    pub row_gap: HashMap<Breakpoint, u32>,
18    /// Column gap for each breakpoint
19    pub column_gap: HashMap<Breakpoint, u32>,
20}
21
22impl ResponsiveGrid {
23    /// Create a new responsive grid container
24    pub fn new() -> Self {
25        Self::default()
26    }
27    
28    /// Create a responsive grid container with base values
29    pub fn with_base(columns: u32, gap: u32) -> Self {
30        let mut grid = Self::new();
31        grid.set_columns(Breakpoint::Base, columns);
32        grid.set_gap(Breakpoint::Base, gap);
33        grid
34    }
35    
36    /// Set number of columns for a specific breakpoint
37    pub fn set_columns(&mut self, breakpoint: Breakpoint, columns: u32) {
38        self.columns.insert(breakpoint, columns);
39    }
40    
41    /// Set gap for a specific breakpoint
42    pub fn set_gap(&mut self, breakpoint: Breakpoint, gap: u32) {
43        self.gap.insert(breakpoint, gap);
44    }
45    
46    /// Set row gap for a specific breakpoint
47    pub fn set_row_gap(&mut self, breakpoint: Breakpoint, gap: u32) {
48        self.row_gap.insert(breakpoint, gap);
49    }
50    
51    /// Set column gap for a specific breakpoint
52    pub fn set_column_gap(&mut self, breakpoint: Breakpoint, gap: u32) {
53        self.column_gap.insert(breakpoint, gap);
54    }
55    
56    /// Get number of columns for a specific breakpoint
57    pub fn get_columns(&self, breakpoint: Breakpoint) -> Option<u32> {
58        self.columns.get(&breakpoint).copied()
59    }
60    
61    /// Get gap for a specific breakpoint
62    pub fn get_gap(&self, breakpoint: Breakpoint) -> Option<u32> {
63        self.gap.get(&breakpoint).copied()
64    }
65    
66    /// Get row gap for a specific breakpoint
67    pub fn get_row_gap(&self, breakpoint: Breakpoint) -> Option<u32> {
68        self.row_gap.get(&breakpoint).copied()
69    }
70    
71    /// Get column gap for a specific breakpoint
72    pub fn get_column_gap(&self, breakpoint: Breakpoint) -> Option<u32> {
73        self.column_gap.get(&breakpoint).copied()
74    }
75    
76    /// Get number of columns for a specific breakpoint, falling back to base
77    pub fn get_columns_or_base(&self, breakpoint: Breakpoint) -> Option<u32> {
78        self.columns.get(&breakpoint).copied()
79            .or_else(|| self.columns.get(&Breakpoint::Base).copied())
80    }
81    
82    /// Get gap for a specific breakpoint, falling back to base
83    pub fn get_gap_or_base(&self, breakpoint: Breakpoint) -> Option<u32> {
84        self.gap.get(&breakpoint).copied()
85            .or_else(|| self.gap.get(&Breakpoint::Base).copied())
86    }
87    
88    /// Generate CSS classes for all breakpoints
89    pub fn to_css_classes(&self) -> String {
90        let mut classes = Vec::new();
91        
92        // Add grid columns classes
93        for (breakpoint, &columns) in &self.columns {
94            let class = if columns == 1 {
95                "grid-cols-1".to_string()
96            } else {
97                format!("grid-cols-{}", columns)
98            };
99            
100            if *breakpoint == Breakpoint::Base {
101                classes.push(class);
102            } else {
103                classes.push(format!("{}{}", breakpoint.prefix(), class));
104            }
105        }
106        
107        // Add gap classes
108        for (breakpoint, &gap) in &self.gap {
109            let class = if gap == 0 {
110                "gap-0".to_string()
111            } else {
112                format!("gap-{}", gap)
113            };
114            
115            if *breakpoint == Breakpoint::Base {
116                classes.push(class);
117            } else {
118                classes.push(format!("{}{}", breakpoint.prefix(), class));
119            }
120        }
121        
122        // Add row gap classes
123        for (breakpoint, &gap) in &self.row_gap {
124            let class = if gap == 0 {
125                "gap-y-0".to_string()
126            } else {
127                format!("gap-y-{}", gap)
128            };
129            
130            if *breakpoint == Breakpoint::Base {
131                classes.push(class);
132            } else {
133                classes.push(format!("{}{}", breakpoint.prefix(), class));
134            }
135        }
136        
137        // Add column gap classes
138        for (breakpoint, &gap) in &self.column_gap {
139            let class = if gap == 0 {
140                "gap-x-0".to_string()
141            } else {
142                format!("gap-x-{}", gap)
143            };
144            
145            if *breakpoint == Breakpoint::Base {
146                classes.push(class);
147            } else {
148                classes.push(format!("{}{}", breakpoint.prefix(), class));
149            }
150        }
151        
152        classes.join(" ")
153    }
154    
155    /// Generate CSS classes for a specific screen width
156    pub fn to_css_classes_for_width(&self, screen_width: u32) -> String {
157        let mut classes = Vec::new();
158        
159        // Find the appropriate breakpoint for this screen width
160        let target_breakpoint = self.get_breakpoint_for_width(screen_width);
161        
162        // Add grid columns class
163        if let Some(columns) = self.get_columns_or_base(target_breakpoint) {
164            let class = if columns == 1 {
165                "grid-cols-1".to_string()
166            } else {
167                format!("grid-cols-{}", columns)
168            };
169            classes.push(class);
170        }
171        
172        // Add gap class
173        if let Some(gap) = self.get_gap_or_base(target_breakpoint) {
174            let class = if gap == 0 {
175                "gap-0".to_string()
176            } else {
177                format!("gap-{}", gap)
178            };
179            classes.push(class);
180        }
181        
182        // Add row gap class
183        if let Some(gap) = self.get_row_gap(target_breakpoint) {
184            let class = if gap == 0 {
185                "gap-y-0".to_string()
186            } else {
187                format!("gap-y-{}", gap)
188            };
189            classes.push(class);
190        }
191        
192        // Add column gap class
193        if let Some(gap) = self.get_column_gap(target_breakpoint) {
194            let class = if gap == 0 {
195                "gap-x-0".to_string()
196            } else {
197                format!("gap-x-{}", gap)
198            };
199            classes.push(class);
200        }
201        
202        classes.join(" ")
203    }
204    
205    /// Get the appropriate breakpoint for a given screen width
206    fn get_breakpoint_for_width(&self, screen_width: u32) -> Breakpoint {
207        if screen_width >= Breakpoint::Xl2.min_width() {
208            Breakpoint::Xl2
209        } else if screen_width >= Breakpoint::Xl.min_width() {
210            Breakpoint::Xl
211        } else if screen_width >= Breakpoint::Lg.min_width() {
212            Breakpoint::Lg
213        } else if screen_width >= Breakpoint::Md.min_width() {
214            Breakpoint::Md
215        } else if screen_width >= Breakpoint::Sm.min_width() {
216            Breakpoint::Sm
217        } else {
218            Breakpoint::Base
219        }
220    }
221    
222    /// Check if the grid is empty
223    pub fn is_empty(&self) -> bool {
224        self.columns.is_empty() && self.gap.is_empty() && self.row_gap.is_empty() && self.column_gap.is_empty()
225    }
226    
227    /// Get the number of breakpoints with configurations
228    pub fn len(&self) -> usize {
229        let mut count = 0;
230        count += self.columns.len();
231        count += self.gap.len();
232        count += self.row_gap.len();
233        count += self.column_gap.len();
234        count
235    }
236    
237    /// Clear all configurations
238    pub fn clear(&mut self) {
239        self.columns.clear();
240        self.gap.clear();
241        self.row_gap.clear();
242        self.column_gap.clear();
243    }
244}
245
246impl Default for ResponsiveGrid {
247    fn default() -> Self {
248        let mut grid = Self {
249            columns: HashMap::new(),
250            gap: HashMap::new(),
251            row_gap: HashMap::new(),
252            column_gap: HashMap::new(),
253        };
254        
255        // Set default values
256        grid.set_columns(Breakpoint::Base, 1);
257        grid.set_gap(Breakpoint::Base, 0);
258        
259        grid
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_responsive_grid_new() {
269        let grid = ResponsiveGrid::new();
270        assert_eq!(grid.get_columns(Breakpoint::Base), Some(1));
271        assert_eq!(grid.get_gap(Breakpoint::Base), Some(0));
272    }
273
274    #[test]
275    fn test_responsive_grid_with_base() {
276        let grid = ResponsiveGrid::with_base(3, 4);
277        assert_eq!(grid.get_columns(Breakpoint::Base), Some(3));
278        assert_eq!(grid.get_gap(Breakpoint::Base), Some(4));
279    }
280
281    #[test]
282    fn test_responsive_grid_set_get() {
283        let mut grid = ResponsiveGrid::new();
284        
285        grid.set_columns(Breakpoint::Sm, 2);
286        grid.set_columns(Breakpoint::Md, 3);
287        grid.set_columns(Breakpoint::Lg, 4);
288        grid.set_gap(Breakpoint::Sm, 2);
289        grid.set_gap(Breakpoint::Md, 4);
290        grid.set_row_gap(Breakpoint::Lg, 6);
291        grid.set_column_gap(Breakpoint::Xl, 8);
292        
293        assert_eq!(grid.get_columns(Breakpoint::Sm), Some(2));
294        assert_eq!(grid.get_columns(Breakpoint::Md), Some(3));
295        assert_eq!(grid.get_columns(Breakpoint::Lg), Some(4));
296        assert_eq!(grid.get_gap(Breakpoint::Sm), Some(2));
297        assert_eq!(grid.get_gap(Breakpoint::Md), Some(4));
298        assert_eq!(grid.get_row_gap(Breakpoint::Lg), Some(6));
299        assert_eq!(grid.get_column_gap(Breakpoint::Xl), Some(8));
300    }
301
302    #[test]
303    fn test_responsive_grid_get_or_base() {
304        let mut grid = ResponsiveGrid::new();
305        grid.set_columns(Breakpoint::Base, 1);
306        grid.set_columns(Breakpoint::Sm, 2);
307        grid.set_gap(Breakpoint::Base, 0);
308        grid.set_gap(Breakpoint::Md, 4);
309        
310        assert_eq!(grid.get_columns_or_base(Breakpoint::Base), Some(1));
311        assert_eq!(grid.get_columns_or_base(Breakpoint::Sm), Some(2));
312        assert_eq!(grid.get_columns_or_base(Breakpoint::Md), Some(1)); // Falls back to base
313        assert_eq!(grid.get_gap_or_base(Breakpoint::Base), Some(0));
314        assert_eq!(grid.get_gap_or_base(Breakpoint::Sm), Some(0)); // Falls back to base
315        assert_eq!(grid.get_gap_or_base(Breakpoint::Md), Some(4));
316    }
317
318    #[test]
319    fn test_responsive_grid_to_css_classes() {
320        let mut grid = ResponsiveGrid::new();
321        grid.set_columns(Breakpoint::Base, 1);
322        grid.set_columns(Breakpoint::Sm, 2);
323        grid.set_columns(Breakpoint::Md, 3);
324        grid.set_gap(Breakpoint::Base, 0);
325        grid.set_gap(Breakpoint::Sm, 2);
326        grid.set_gap(Breakpoint::Md, 4);
327        
328        let classes = grid.to_css_classes();
329        assert!(classes.contains("grid-cols-1"));
330        assert!(classes.contains("sm:grid-cols-2"));
331        assert!(classes.contains("md:grid-cols-3"));
332        assert!(classes.contains("gap-0"));
333        assert!(classes.contains("sm:gap-2"));
334        assert!(classes.contains("md:gap-4"));
335    }
336
337    #[test]
338    fn test_responsive_grid_to_css_classes_for_width() {
339        let mut grid = ResponsiveGrid::new();
340        grid.set_columns(Breakpoint::Base, 1);
341        grid.set_columns(Breakpoint::Sm, 2);
342        grid.set_columns(Breakpoint::Md, 3);
343        grid.set_gap(Breakpoint::Base, 0);
344        grid.set_gap(Breakpoint::Sm, 2);
345        grid.set_gap(Breakpoint::Md, 4);
346        
347        // Test width 0 (base only)
348        let classes_0 = grid.to_css_classes_for_width(0);
349        assert!(classes_0.contains("grid-cols-1"));
350        assert!(classes_0.contains("gap-0"));
351        assert!(!classes_0.contains("grid-cols-2"));
352        assert!(!classes_0.contains("gap-2"));
353        
354        // Test width 640 (sm active)
355        let classes_640 = grid.to_css_classes_for_width(640);
356        assert!(classes_640.contains("grid-cols-2"));
357        assert!(classes_640.contains("gap-2"));
358        assert!(!classes_640.contains("grid-cols-3"));
359        assert!(!classes_640.contains("gap-4"));
360        
361        // Test width 768 (md active)
362        let classes_768 = grid.to_css_classes_for_width(768);
363        assert!(classes_768.contains("grid-cols-3"));
364        assert!(classes_768.contains("gap-4"));
365    }
366
367    #[test]
368    fn test_responsive_grid_is_empty() {
369        let grid = ResponsiveGrid::new();
370        assert!(!grid.is_empty()); // Has default values
371        
372        let mut empty_grid = ResponsiveGrid {
373            columns: HashMap::new(),
374            gap: HashMap::new(),
375            row_gap: HashMap::new(),
376            column_gap: HashMap::new(),
377        };
378        assert!(empty_grid.is_empty());
379    }
380
381    #[test]
382    fn test_responsive_grid_clear() {
383        let mut grid = ResponsiveGrid::new();
384        grid.set_columns(Breakpoint::Sm, 2);
385        grid.set_gap(Breakpoint::Md, 4);
386        
387        assert!(!grid.is_empty());
388        grid.clear();
389        assert!(grid.is_empty());
390    }
391}