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(
83        &mut self,
84        base: impl Into<String>,
85        sm: Option<String>,
86        md: Option<String>,
87        lg: Option<String>,
88        xl: Option<String>,
89        xl2: Option<String>,
90    ) -> &mut Self {
91        self.base(base);
92
93        if let Some(sm_class) = sm {
94            self.sm(sm_class);
95        }
96        if let Some(md_class) = md {
97            self.md(md_class);
98        }
99        if let Some(lg_class) = lg {
100            self.lg(lg_class);
101        }
102        if let Some(xl_class) = xl {
103            self.xl(xl_class);
104        }
105        if let Some(xl2_class) = xl2 {
106            self.xl2(xl2_class);
107        }
108
109        self
110    }
111
112    /// Get classes for a specific breakpoint
113    pub fn get_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
114        self.classes.get(&breakpoint).cloned().unwrap_or_default()
115    }
116
117    /// Get all classes for all breakpoints
118    pub fn get_all_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
119        self.classes.clone()
120    }
121
122    /// Check if the builder is empty
123    pub fn is_empty(&self) -> bool {
124        self.classes.is_empty() || self.classes.values().all(|classes| classes.is_empty())
125    }
126
127    /// Get the number of breakpoints with classes
128    pub fn len(&self) -> usize {
129        self.classes.len()
130    }
131
132    /// Clear all classes
133    pub fn clear(&mut self) {
134        self.classes.clear();
135    }
136
137    /// Remove classes for a specific breakpoint
138    pub fn remove_breakpoint(&mut self, breakpoint: Breakpoint) -> Vec<String> {
139        self.classes.remove(&breakpoint).unwrap_or_default()
140    }
141
142    /// Build the final CSS classes string
143    pub fn build(&self) -> String {
144        let mut classes = Vec::new();
145
146        // Add classes in breakpoint order
147        for breakpoint in Breakpoint::all() {
148            if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
149                if !breakpoint_classes.is_empty() {
150                    let breakpoint_classes_str = breakpoint_classes.join(" ");
151                    if breakpoint == Breakpoint::Base {
152                        classes.push(breakpoint_classes_str);
153                    } else {
154                        classes.push(format!("{}{}", breakpoint.prefix(), breakpoint_classes_str));
155                    }
156                }
157            }
158        }
159
160        classes.join(" ")
161    }
162
163    /// Build classes for a specific screen width
164    pub fn build_for_width(&self, screen_width: u32) -> String {
165        let mut classes = Vec::new();
166
167        // Find the appropriate breakpoint for this screen width
168        let target_breakpoint = self.config.get_breakpoint_for_width(screen_width);
169
170        // Add classes from base up to the target breakpoint
171        for breakpoint in Breakpoint::all() {
172            if breakpoint.min_width() <= target_breakpoint.min_width() {
173                if let Some(breakpoint_classes) = self.classes.get(&breakpoint) {
174                    if !breakpoint_classes.is_empty() {
175                        let breakpoint_classes_str = breakpoint_classes.join(" ");
176                        if breakpoint == Breakpoint::Base {
177                            classes.push(breakpoint_classes_str);
178                        } else {
179                            classes.push(format!(
180                                "{}{}",
181                                breakpoint.prefix(),
182                                breakpoint_classes_str
183                            ));
184                        }
185                    }
186                }
187            }
188        }
189
190        classes.join(" ")
191    }
192
193    /// Get the configuration
194    pub fn get_config(&self) -> &ResponsiveConfig {
195        &self.config
196    }
197
198    /// Update the configuration
199    pub fn update_config(&mut self, config: ResponsiveConfig) {
200        self.config = config;
201    }
202}
203
204impl Default for ResponsiveBuilder {
205    fn default() -> Self {
206        Self {
207            classes: HashMap::new(),
208            config: ResponsiveConfig::default(),
209        }
210    }
211}
212
213impl std::fmt::Display for ResponsiveBuilder {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        write!(f, "{}", self.build())
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_responsive_builder_new() {
225        let builder = ResponsiveBuilder::new();
226        assert!(builder.is_empty());
227        assert_eq!(builder.len(), 0);
228    }
229
230    #[test]
231    fn test_responsive_builder_add_class() {
232        let mut builder = ResponsiveBuilder::new();
233        builder.add_class(Breakpoint::Base, "text-sm");
234        builder.add_class(Breakpoint::Sm, "text-base");
235
236        assert!(!builder.is_empty());
237        assert_eq!(builder.len(), 2);
238        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
239        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
240    }
241
242    #[test]
243    fn test_responsive_builder_add_classes() {
244        let mut builder = ResponsiveBuilder::new();
245        builder.add_classes(
246            Breakpoint::Base,
247            vec!["text-sm".to_string(), "font-medium".to_string()],
248        );
249
250        assert_eq!(
251            builder.get_classes(Breakpoint::Base),
252            vec!["text-sm", "font-medium"]
253        );
254    }
255
256    #[test]
257    fn test_responsive_builder_breakpoint_methods() {
258        let mut builder = ResponsiveBuilder::new();
259        builder.base("text-sm");
260        builder.sm("text-base");
261        builder.md("text-lg");
262        builder.lg("text-xl");
263        builder.xl("text-2xl");
264        builder.xl2("text-3xl");
265
266        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
267        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
268        assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
269        assert_eq!(builder.get_classes(Breakpoint::Lg), vec!["text-xl"]);
270        assert_eq!(builder.get_classes(Breakpoint::Xl), vec!["text-2xl"]);
271        assert_eq!(builder.get_classes(Breakpoint::Xl2), vec!["text-3xl"]);
272    }
273
274    #[test]
275    fn test_responsive_builder_responsive() {
276        let mut builder = ResponsiveBuilder::new();
277        builder.responsive(
278            "text-sm",
279            Some("text-base".to_string()),
280            Some("text-lg".to_string()),
281            None,
282            None,
283            None,
284        );
285
286        assert_eq!(builder.get_classes(Breakpoint::Base), vec!["text-sm"]);
287        assert_eq!(builder.get_classes(Breakpoint::Sm), vec!["text-base"]);
288        assert_eq!(builder.get_classes(Breakpoint::Md), vec!["text-lg"]);
289        assert_eq!(builder.get_classes(Breakpoint::Lg), Vec::<String>::new());
290    }
291
292    #[test]
293    fn test_responsive_builder_build() {
294        let mut builder = ResponsiveBuilder::new();
295        builder.base("text-sm");
296        builder.sm("text-base");
297        builder.md("text-lg");
298
299        let result = builder.build();
300        assert!(result.contains("text-sm"));
301        assert!(result.contains("sm:text-base"));
302        assert!(result.contains("md:text-lg"));
303    }
304
305    #[test]
306    fn test_responsive_builder_build_for_width() {
307        let mut builder = ResponsiveBuilder::new();
308        builder.base("text-sm");
309        builder.sm("text-base");
310        builder.md("text-lg");
311
312        // Test width 0 (base only)
313        let result_0 = builder.build_for_width(0);
314        assert!(result_0.contains("text-sm"));
315        assert!(!result_0.contains("sm:"));
316        assert!(!result_0.contains("md:"));
317
318        // Test width 640 (sm active)
319        let result_640 = builder.build_for_width(640);
320        assert!(result_640.contains("text-sm"));
321        assert!(result_640.contains("sm:text-base"));
322        assert!(!result_640.contains("md:"));
323
324        // Test width 768 (md active)
325        let result_768 = builder.build_for_width(768);
326        assert!(result_768.contains("text-sm"));
327        assert!(result_768.contains("sm:text-base"));
328        assert!(result_768.contains("md:text-lg"));
329    }
330
331    #[test]
332    fn test_responsive_builder_clear() {
333        let mut builder = ResponsiveBuilder::new();
334        builder.base("text-sm");
335        builder.sm("text-base");
336
337        assert!(!builder.is_empty());
338        builder.clear();
339        assert!(builder.is_empty());
340    }
341
342    #[test]
343    fn test_responsive_builder_remove_breakpoint() {
344        let mut builder = ResponsiveBuilder::new();
345        builder.base("text-sm");
346        builder.sm("text-base");
347
348        assert_eq!(builder.len(), 2);
349        let removed = builder.remove_breakpoint(Breakpoint::Sm);
350        assert_eq!(removed, vec!["text-base"]);
351        assert_eq!(builder.len(), 1);
352        assert_eq!(builder.get_classes(Breakpoint::Sm), Vec::<String>::new());
353    }
354
355    #[test]
356    fn test_responsive_builder_display() {
357        let mut builder = ResponsiveBuilder::new();
358        builder.base("text-sm");
359        builder.sm("text-base");
360
361        let result = format!("{}", builder);
362        assert!(result.contains("text-sm"));
363        assert!(result.contains("sm:text-base"));
364    }
365}