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