tailwind_rs_core/responsive/
responsive_config.rs

1//! # Responsive Configuration Management
2//!
3//! This module provides configuration management for responsive design.
4
5use super::breakpoints::Breakpoint;
6use crate::error::{Result, TailwindError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Configuration for responsive design
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ResponsiveConfig {
13    /// Breakpoint configurations
14    pub breakpoints: HashMap<Breakpoint, BreakpointConfig>,
15    /// Default settings
16    pub defaults: ResponsiveDefaults,
17}
18
19/// Configuration for a specific breakpoint
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct BreakpointConfig {
22    /// Minimum width for this breakpoint
23    pub min_width: u32,
24    /// Maximum width for this breakpoint (optional)
25    pub max_width: Option<u32>,
26    /// Whether this breakpoint is enabled
27    pub enabled: bool,
28    /// Custom media query (optional)
29    pub media_query: Option<String>,
30}
31
32/// Default settings for responsive design
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct ResponsiveDefaults {
35    /// Default breakpoint to use when none is specified
36    pub default_breakpoint: Breakpoint,
37    /// Whether to include base breakpoint in generated classes
38    pub include_base: bool,
39    /// Whether to use mobile-first approach
40    pub mobile_first: bool,
41}
42
43impl ResponsiveConfig {
44    /// Create a new responsive configuration
45    pub fn new() -> Self {
46        Self::default()
47    }
48    
49    /// Create a responsive configuration with custom breakpoints
50    pub fn with_breakpoints(breakpoints: HashMap<Breakpoint, BreakpointConfig>) -> Self {
51        Self {
52            breakpoints,
53            defaults: ResponsiveDefaults::default(),
54        }
55    }
56    
57    /// Get configuration for a specific breakpoint
58    pub fn get_breakpoint_config(&self, breakpoint: Breakpoint) -> Option<&BreakpointConfig> {
59        self.breakpoints.get(&breakpoint)
60    }
61    
62    /// Set configuration for a specific breakpoint
63    pub fn set_breakpoint_config(&mut self, breakpoint: Breakpoint, config: BreakpointConfig) {
64        self.breakpoints.insert(breakpoint, config);
65    }
66    
67    /// Check if a breakpoint is enabled
68    pub fn is_breakpoint_enabled(&self, breakpoint: Breakpoint) -> bool {
69        self.breakpoints
70            .get(&breakpoint)
71            .map(|config| config.enabled)
72            .unwrap_or(true)
73    }
74    
75    /// Get the minimum width for a breakpoint
76    pub fn get_breakpoint_min_width(&self, breakpoint: Breakpoint) -> u32 {
77        self.breakpoints
78            .get(&breakpoint)
79            .map(|config| config.min_width)
80            .unwrap_or_else(|| breakpoint.min_width())
81    }
82    
83    /// Get the maximum width for a breakpoint
84    pub fn get_breakpoint_max_width(&self, breakpoint: Breakpoint) -> Option<u32> {
85        self.breakpoints
86            .get(&breakpoint)
87            .and_then(|config| config.max_width)
88    }
89    
90    /// Get the media query for a breakpoint
91    pub fn get_breakpoint_media_query(&self, breakpoint: Breakpoint) -> String {
92        if let Some(config) = self.breakpoints.get(&breakpoint) {
93            if let Some(ref media_query) = config.media_query {
94                return media_query.clone();
95            }
96        }
97        
98        // Generate default media query
99        let min_width = self.get_breakpoint_min_width(breakpoint);
100        if min_width == 0 {
101            String::new()
102        } else {
103            format!("(min-width: {}px)", min_width)
104        }
105    }
106    
107    /// Get all enabled breakpoints
108    pub fn get_enabled_breakpoints(&self) -> Vec<Breakpoint> {
109        self.breakpoints
110            .iter()
111            .filter(|(_, config)| config.enabled)
112            .map(|(breakpoint, _)| *breakpoint)
113            .collect()
114    }
115    
116    /// Get the appropriate breakpoint for a given screen width
117    pub fn get_breakpoint_for_width(&self, screen_width: u32) -> Breakpoint {
118        if screen_width >= Breakpoint::Xl2.min_width() {
119            Breakpoint::Xl2
120        } else if screen_width >= Breakpoint::Xl.min_width() {
121            Breakpoint::Xl
122        } else if screen_width >= Breakpoint::Lg.min_width() {
123            Breakpoint::Lg
124        } else if screen_width >= Breakpoint::Md.min_width() {
125            Breakpoint::Md
126        } else if screen_width >= Breakpoint::Sm.min_width() {
127            Breakpoint::Sm
128        } else {
129            Breakpoint::Base
130        }
131    }
132    
133    /// Validate the configuration
134    pub fn validate(&self) -> Result<()> {
135        // Check that base breakpoint exists
136        if !self.breakpoints.contains_key(&Breakpoint::Base) {
137            return Err(TailwindError::config(
138                "Base breakpoint is required".to_string(),
139            ));
140        }
141        
142        // Check that breakpoints are in order
143        let mut breakpoints: Vec<Breakpoint> = self.breakpoints.keys().cloned().collect();
144        breakpoints.sort_by_key(|bp| bp.min_width());
145        
146        for i in 1..breakpoints.len() {
147            let prev_min = self.get_breakpoint_min_width(breakpoints[i - 1]);
148            let curr_min = self.get_breakpoint_min_width(breakpoints[i]);
149            
150            if prev_min >= curr_min {
151                return Err(TailwindError::config(format!(
152                    "Breakpoint {} ({}px) must be greater than {} ({}px)",
153                    breakpoints[i],
154                    curr_min,
155                    breakpoints[i - 1],
156                    prev_min
157                )));
158            }
159        }
160        
161        Ok(())
162    }
163}
164
165impl Default for ResponsiveConfig {
166    fn default() -> Self {
167        let mut breakpoints = HashMap::new();
168        
169        // Add default breakpoint configurations
170        breakpoints.insert(
171            Breakpoint::Base,
172            BreakpointConfig {
173                min_width: 0,
174                max_width: None,
175                enabled: true,
176                media_query: None,
177            },
178        );
179        
180        breakpoints.insert(
181            Breakpoint::Sm,
182            BreakpointConfig {
183                min_width: 640,
184                max_width: None,
185                enabled: true,
186                media_query: None,
187            },
188        );
189        
190        breakpoints.insert(
191            Breakpoint::Md,
192            BreakpointConfig {
193                min_width: 768,
194                max_width: None,
195                enabled: true,
196                media_query: None,
197            },
198        );
199        
200        breakpoints.insert(
201            Breakpoint::Lg,
202            BreakpointConfig {
203                min_width: 1024,
204                max_width: None,
205                enabled: true,
206                media_query: None,
207            },
208        );
209        
210        breakpoints.insert(
211            Breakpoint::Xl,
212            BreakpointConfig {
213                min_width: 1280,
214                max_width: None,
215                enabled: true,
216                media_query: None,
217            },
218        );
219        
220        breakpoints.insert(
221            Breakpoint::Xl2,
222            BreakpointConfig {
223                min_width: 1536,
224                max_width: None,
225                enabled: true,
226                media_query: None,
227            },
228        );
229        
230        Self {
231            breakpoints,
232            defaults: ResponsiveDefaults::default(),
233        }
234    }
235}
236
237impl Default for ResponsiveDefaults {
238    fn default() -> Self {
239        Self {
240            default_breakpoint: Breakpoint::Base,
241            include_base: true,
242            mobile_first: true,
243        }
244    }
245}
246
247/// Main responsive struct that uses the configuration
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct Responsive {
250    /// The configuration
251    pub config: ResponsiveConfig,
252    /// Current breakpoint
253    pub current_breakpoint: Breakpoint,
254}
255
256impl Responsive {
257    /// Create a new responsive instance
258    pub fn new() -> Self {
259        Self::default()
260    }
261    
262    /// Create a responsive instance with custom configuration
263    pub fn with_config(config: ResponsiveConfig) -> Self {
264        Self {
265            current_breakpoint: config.defaults.default_breakpoint,
266            config,
267        }
268    }
269    
270    /// Get the current breakpoint
271    pub fn get_current_breakpoint(&self) -> Breakpoint {
272        self.current_breakpoint
273    }
274    
275    /// Set the current breakpoint
276    pub fn set_current_breakpoint(&mut self, breakpoint: Breakpoint) {
277        self.current_breakpoint = breakpoint;
278    }
279    
280    /// Get the configuration
281    pub fn get_config(&self) -> &ResponsiveConfig {
282        &self.config
283    }
284    
285    /// Update the configuration
286    pub fn update_config(&mut self, config: ResponsiveConfig) {
287        self.config = config;
288    }
289    
290    /// Check if a breakpoint is active for the current screen width
291    pub fn is_breakpoint_active(&self, breakpoint: Breakpoint, screen_width: u32) -> bool {
292        if !self.config.is_breakpoint_enabled(breakpoint) {
293            return false;
294        }
295        
296        let min_width = self.config.get_breakpoint_min_width(breakpoint);
297        let max_width = self.config.get_breakpoint_max_width(breakpoint);
298        
299        let min_active = screen_width >= min_width;
300        let max_active = max_width.map_or(true, |max| screen_width < max);
301        
302        min_active && max_active
303    }
304    
305    /// Get the appropriate breakpoint for a given screen width
306    pub fn get_breakpoint_for_width(&self, screen_width: u32) -> Breakpoint {
307        let enabled_breakpoints = self.config.get_enabled_breakpoints();
308        
309        // Find the highest breakpoint that is active for this screen width
310        let active_breakpoints: Vec<Breakpoint> = enabled_breakpoints
311            .into_iter()
312            .filter(|&bp| self.is_breakpoint_active(bp, screen_width))
313            .collect();
314        
315        if active_breakpoints.is_empty() {
316            return self.config.defaults.default_breakpoint;
317        }
318        
319        // Find the breakpoint with the highest min_width among active ones
320        active_breakpoints
321            .into_iter()
322            .max_by_key(|bp| self.config.get_breakpoint_min_width(*bp))
323            .unwrap_or(self.config.defaults.default_breakpoint)
324    }
325}
326
327impl Default for Responsive {
328    fn default() -> Self {
329        Self {
330            config: ResponsiveConfig::default(),
331            current_breakpoint: Breakpoint::Base,
332        }
333    }
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339
340    #[test]
341    fn test_responsive_config_new() {
342        let config = ResponsiveConfig::new();
343        assert_eq!(config.breakpoints.len(), 6);
344        assert!(config.breakpoints.contains_key(&Breakpoint::Base));
345        assert!(config.breakpoints.contains_key(&Breakpoint::Sm));
346        assert!(config.breakpoints.contains_key(&Breakpoint::Md));
347        assert!(config.breakpoints.contains_key(&Breakpoint::Lg));
348        assert!(config.breakpoints.contains_key(&Breakpoint::Xl));
349        assert!(config.breakpoints.contains_key(&Breakpoint::Xl2));
350    }
351
352    #[test]
353    fn test_responsive_config_get_breakpoint_config() {
354        let config = ResponsiveConfig::new();
355        let base_config = config.get_breakpoint_config(Breakpoint::Base);
356        assert!(base_config.is_some());
357        assert_eq!(base_config.unwrap().min_width, 0);
358        
359        let sm_config = config.get_breakpoint_config(Breakpoint::Sm);
360        assert!(sm_config.is_some());
361        assert_eq!(sm_config.unwrap().min_width, 640);
362    }
363
364    #[test]
365    fn test_responsive_config_set_breakpoint_config() {
366        let mut config = ResponsiveConfig::new();
367        let custom_config = BreakpointConfig {
368            min_width: 800,
369            max_width: Some(1200),
370            enabled: true,
371            media_query: Some("(min-width: 800px) and (max-width: 1200px)".to_string()),
372        };
373        
374        config.set_breakpoint_config(Breakpoint::Md, custom_config.clone());
375        let retrieved_config = config.get_breakpoint_config(Breakpoint::Md);
376        assert_eq!(retrieved_config, Some(&custom_config));
377    }
378
379    #[test]
380    fn test_responsive_config_is_breakpoint_enabled() {
381        let config = ResponsiveConfig::new();
382        assert!(config.is_breakpoint_enabled(Breakpoint::Base));
383        assert!(config.is_breakpoint_enabled(Breakpoint::Sm));
384        assert!(config.is_breakpoint_enabled(Breakpoint::Md));
385    }
386
387    #[test]
388    fn test_responsive_config_get_breakpoint_min_width() {
389        let config = ResponsiveConfig::new();
390        assert_eq!(config.get_breakpoint_min_width(Breakpoint::Base), 0);
391        assert_eq!(config.get_breakpoint_min_width(Breakpoint::Sm), 640);
392        assert_eq!(config.get_breakpoint_min_width(Breakpoint::Md), 768);
393    }
394
395    #[test]
396    fn test_responsive_config_get_breakpoint_media_query() {
397        let config = ResponsiveConfig::new();
398        assert_eq!(config.get_breakpoint_media_query(Breakpoint::Base), "");
399        assert_eq!(config.get_breakpoint_media_query(Breakpoint::Sm), "(min-width: 640px)");
400        assert_eq!(config.get_breakpoint_media_query(Breakpoint::Md), "(min-width: 768px)");
401    }
402
403    #[test]
404    fn test_responsive_config_get_enabled_breakpoints() {
405        let config = ResponsiveConfig::new();
406        let enabled = config.get_enabled_breakpoints();
407        assert_eq!(enabled.len(), 6);
408        assert!(enabled.contains(&Breakpoint::Base));
409        assert!(enabled.contains(&Breakpoint::Sm));
410        assert!(enabled.contains(&Breakpoint::Md));
411    }
412
413    #[test]
414    fn test_responsive_config_validate() {
415        let config = ResponsiveConfig::new();
416        assert!(config.validate().is_ok());
417    }
418
419    #[test]
420    fn test_responsive_new() {
421        let responsive = Responsive::new();
422        assert_eq!(responsive.get_current_breakpoint(), Breakpoint::Base);
423        assert_eq!(responsive.get_config().breakpoints.len(), 6);
424    }
425
426    #[test]
427    fn test_responsive_set_current_breakpoint() {
428        let mut responsive = Responsive::new();
429        responsive.set_current_breakpoint(Breakpoint::Md);
430        assert_eq!(responsive.get_current_breakpoint(), Breakpoint::Md);
431    }
432
433    #[test]
434    fn test_responsive_is_breakpoint_active() {
435        let responsive = Responsive::new();
436        assert!(responsive.is_breakpoint_active(Breakpoint::Base, 0));
437        assert!(responsive.is_breakpoint_active(Breakpoint::Sm, 640));
438        assert!(responsive.is_breakpoint_active(Breakpoint::Md, 768));
439        assert!(!responsive.is_breakpoint_active(Breakpoint::Sm, 639));
440    }
441
442    #[test]
443    fn test_responsive_get_breakpoint_for_width() {
444        let responsive = Responsive::new();
445        assert_eq!(responsive.get_breakpoint_for_width(0), Breakpoint::Base);
446        assert_eq!(responsive.get_breakpoint_for_width(640), Breakpoint::Sm);
447        assert_eq!(responsive.get_breakpoint_for_width(768), Breakpoint::Md);
448        assert_eq!(responsive.get_breakpoint_for_width(1024), Breakpoint::Lg);
449        assert_eq!(responsive.get_breakpoint_for_width(1280), Breakpoint::Xl);
450        assert_eq!(responsive.get_breakpoint_for_width(1536), Breakpoint::Xl2);
451    }
452}