tailwind_rs_core/responsive/
responsive_builder.rs

1//! # Responsive Builder Pattern
2//!
3//! This module provides a builder pattern for creating responsive classes.
4
5use super::breakpoints::Breakpoint;
6use super::responsive_config::ResponsiveConfig;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Builder for creating responsive classes
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ResponsiveBuilder {
13    /// Classes for each breakpoint
14    classes: HashMap<Breakpoint, Vec<String>>,
15    /// Configuration
16    config: ResponsiveConfig,
17}
18
19impl ResponsiveBuilder {
20    /// Create a new responsive builder
21    pub fn new() -> Self {
22        Self::default()
23    }
24    
25    /// Create a responsive builder with custom configuration
26    pub fn with_config(config: ResponsiveConfig) -> Self {
27        Self {
28            classes: HashMap::new(),
29            config,
30        }
31    }
32    
33    /// Add a class for a specific breakpoint
34    pub fn add_class(&mut self, breakpoint: Breakpoint, class: impl Into<String>) -> &mut Self {
35        self.classes
36            .entry(breakpoint)
37            .or_insert_with(Vec::new)
38            .push(class.into());
39        self
40    }
41    
42    /// Add classes for a specific breakpoint
43    pub fn add_classes(&mut self, breakpoint: Breakpoint, classes: Vec<String>) -> &mut Self {
44        self.classes
45            .entry(breakpoint)
46            .or_insert_with(Vec::new)
47            .extend(classes);
48        self
49    }
50    
51    /// Add a class for the base breakpoint
52    pub fn base(&mut self, class: impl Into<String>) -> &mut Self {
53        self.add_class(Breakpoint::Base, class)
54    }
55    
56    /// Add a class for the small breakpoint
57    pub fn sm(&mut self, class: impl Into<String>) -> &mut Self {
58        self.add_class(Breakpoint::Sm, class)
59    }
60    
61    /// Add a class for the medium breakpoint
62    pub fn md(&mut self, class: impl Into<String>) -> &mut Self {
63        self.add_class(Breakpoint::Md, class)
64    }
65    
66    /// Add a class for the large breakpoint
67    pub fn lg(&mut self, class: impl Into<String>) -> &mut Self {
68        self.add_class(Breakpoint::Lg, class)
69    }
70    
71    /// Add a class for the extra large breakpoint
72    pub fn xl(&mut self, class: impl Into<String>) -> &mut Self {
73        self.add_class(Breakpoint::Xl, class)
74    }
75    
76    /// Add a class for the 2X large breakpoint
77    pub fn xl2(&mut self, class: impl Into<String>) -> &mut Self {
78        self.add_class(Breakpoint::Xl2, class)
79    }
80    
81    /// Add responsive classes for all breakpoints
82    pub fn responsive(&mut self, base: impl Into<String>, sm: Option<String>, md: Option<String>, lg: Option<String>, xl: Option<String>, xl2: Option<String>) -> &mut Self {
83        self.base(base);
84        
85        if let Some(sm_class) = sm {
86            self.sm(sm_class);
87        }
88        if let Some(md_class) = md {
89            self.md(md_class);
90        }
91        if let Some(lg_class) = lg {
92            self.lg(lg_class);
93        }
94        if let Some(xl_class) = xl {
95            self.xl(xl_class);
96        }
97        if let Some(xl2_class) = xl2 {
98            self.xl2(xl2_class);
99        }
100        
101        self
102    }
103    
104    /// Get classes for a specific breakpoint
105    pub fn get_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
106        self.classes.get(&breakpoint).cloned().unwrap_or_default()
107    }
108    
109    /// Get all classes for all breakpoints
110    pub fn get_all_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
111        self.classes.clone()
112    }
113    
114    /// Check if the builder is empty
115    pub fn is_empty(&self) -> bool {
116        self.classes.is_empty() || self.classes.values().all(|classes| classes.is_empty())
117    }
118    
119    /// Get the number of breakpoints with classes
120    pub fn len(&self) -> usize {
121        self.classes.len()
122    }
123    
124    /// Clear all classes
125    pub fn clear(&mut self) {
126        self.classes.clear();
127    }
128    
129    /// Remove classes for a specific breakpoint
130    pub fn remove_breakpoint(&mut self, breakpoint: Breakpoint) -> Vec<String> {
131        self.classes.remove(&breakpoint).unwrap_or_default()
132    }
133    
134    /// Build the final CSS classes string
135    pub fn build(&self) -> String {
136        let mut classes = Vec::new();
137        
138        // Add classes in breakpoint order
139        for breakpoint in Breakpoint::all() {
140            if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
141                if !breakpoint_classes.is_empty() {
142                    let breakpoint_classes_str = breakpoint_classes.join(" ");
143                    if breakpoint == Breakpoint::Base {
144                        classes.push(breakpoint_classes_str);
145                    } else {
146                        classes.push(format!("{}{}", breakpoint.prefix(), breakpoint_classes_str));
147                    }
148                }
149            }
150        }
151        
152        classes.join(" ")
153    }
154    
155    /// Build classes for a specific screen width
156    pub fn build_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.config.get_breakpoint_for_width(screen_width);
161        
162        // Add classes from base up to the target breakpoint
163        for breakpoint in Breakpoint::all() {
164            if breakpoint.min_width() <= target_breakpoint.min_width() {
165                if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
166                    if !breakpoint_classes.is_empty() {
167                        let breakpoint_classes_str = breakpoint_classes.join(" ");
168                        if breakpoint == Breakpoint::Base {
169                            classes.push(breakpoint_classes_str);
170                        } else {
171                            classes.push(format!("{}{}", breakpoint.prefix(), breakpoint_classes_str));
172                        }
173                    }
174                }
175            }
176        }
177        
178        classes.join(" ")
179    }
180    
181    /// Get the configuration
182    pub fn get_config(&self) -> &ResponsiveConfig {
183        &self.config
184    }
185    
186    /// Update the configuration
187    pub fn update_config(&mut self, config: ResponsiveConfig) {
188        self.config = config;
189    }
190}
191
192impl Default for ResponsiveBuilder {
193    fn default() -> Self {
194        Self {
195            classes: HashMap::new(),
196            config: ResponsiveConfig::default(),
197        }
198    }
199}
200
201impl std::fmt::Display for ResponsiveBuilder {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        write!(f, "{}", self.build())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_responsive_builder_new() {
213        let builder = ResponsiveBuilder::new();
214        assert!(builder.is_empty());
215        assert_eq!(builder.len(), 0);
216    }
217
218    #[test]
219    fn test_responsive_builder_add_class() {
220        let mut builder = ResponsiveBuilder::new();
221        builder.add_class(Breakpoint::Base, "text-sm");
222        builder.add_class(Breakpoint::Sm, "text-base");
223        
224        assert!(!builder.is_empty());
225        assert_eq!(builder.len(), 2);
226        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
227        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
228    }
229
230    #[test]
231    fn test_responsive_builder_add_classes() {
232        let mut builder = ResponsiveBuilder::new();
233        builder.add_classes(Breakpoint::Base, vec!["text-sm".to_string(), "font-medium".to_string()]);
234        
235        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm", "font-medium"]);
236    }
237
238    #[test]
239    fn test_responsive_builder_breakpoint_methods() {
240        let mut builder = ResponsiveBuilder::new();
241        builder.base("text-sm");
242        builder.sm("text-base");
243        builder.md("text-lg");
244        builder.lg("text-xl");
245        builder.xl("text-2xl");
246        builder.xl2("text-3xl");
247        
248        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
249        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
250        assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
251        assert_eq!(builder.get_classes(Breakpoint::Lg), vec!["text-xl"]);
252        assert_eq!(builder.get_classes(Breakpoint::Xl), vec!["text-2xl"]);
253        assert_eq!(builder.get_classes(Breakpoint::Xl2), vec!["text-3xl"]);
254    }
255
256    #[test]
257    fn test_responsive_builder_responsive() {
258        let mut builder = ResponsiveBuilder::new();
259        builder.responsive(
260            "text-sm",
261            Some("text-base".to_string()),
262            Some("text-lg".to_string()),
263            None,
264            None,
265            None,
266        );
267        
268        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
269        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
270        assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
271        assert_eq!(builder.get_classes(Breakpoint::Lg), Vec::<String>::new());
272    }
273
274    #[test]
275    fn test_responsive_builder_build() {
276        let mut builder = ResponsiveBuilder::new();
277        builder.base("text-sm");
278        builder.sm("text-base");
279        builder.md("text-lg");
280        
281        let result = builder.build();
282        assert!(result.contains("text-sm"));
283        assert!(result.contains("sm:text-base"));
284        assert!(result.contains("md:text-lg"));
285    }
286
287    #[test]
288    fn test_responsive_builder_build_for_width() {
289        let mut builder = ResponsiveBuilder::new();
290        builder.base("text-sm");
291        builder.sm("text-base");
292        builder.md("text-lg");
293        
294        // Test width 0 (base only)
295        let result_0 = builder.build_for_width(0);
296        assert!(result_0.contains("text-sm"));
297        assert!(!result_0.contains("sm:"));
298        assert!(!result_0.contains("md:"));
299        
300        // Test width 640 (sm active)
301        let result_640 = builder.build_for_width(640);
302        assert!(result_640.contains("text-sm"));
303        assert!(result_640.contains("sm:text-base"));
304        assert!(!result_640.contains("md:"));
305        
306        // Test width 768 (md active)
307        let result_768 = builder.build_for_width(768);
308        assert!(result_768.contains("text-sm"));
309        assert!(result_768.contains("sm:text-base"));
310        assert!(result_768.contains("md:text-lg"));
311    }
312
313    #[test]
314    fn test_responsive_builder_clear() {
315        let mut builder = ResponsiveBuilder::new();
316        builder.base("text-sm");
317        builder.sm("text-base");
318        
319        assert!(!builder.is_empty());
320        builder.clear();
321        assert!(builder.is_empty());
322    }
323
324    #[test]
325    fn test_responsive_builder_remove_breakpoint() {
326        let mut builder = ResponsiveBuilder::new();
327        builder.base("text-sm");
328        builder.sm("text-base");
329        
330        assert_eq!(builder.len(), 2);
331        let removed = builder.remove_breakpoint(Breakpoint::Sm);
332        assert_eq!(removed, vec!["text-base"]);
333        assert_eq!(builder.len(), 1);
334        assert_eq!(builder.get_classes(Breakpoint::Sm), Vec::<String>::new());
335    }
336
337    #[test]
338    fn test_responsive_builder_display() {
339        let mut builder = ResponsiveBuilder::new();
340        builder.base("text-sm");
341        builder.sm("text-base");
342        
343        let result = format!("{}", builder);
344        assert!(result.contains("text-sm"));
345        assert!(result.contains("sm:text-base"));
346    }
347}