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