tailwind_rs_core/responsive/
responsive_values.rs

1//! # Responsive Value Handling
2//!
3//! This module provides utilities for handling responsive values across different breakpoints.
4
5use super::breakpoints::Breakpoint;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// A responsive value that can have different values for different breakpoints
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct ResponsiveValue<T> {
12    /// Values for each breakpoint
13    pub values: HashMap<Breakpoint, T>,
14}
15
16impl<T> ResponsiveValue<T> {
17    /// Create a new responsive value
18    pub fn new() -> Self {
19        Self {
20            values: HashMap::new(),
21        }
22    }
23    
24    /// Create a responsive value with a base value
25    pub fn with_base(base: T) -> Self {
26        let mut values = HashMap::new();
27        values.insert(Breakpoint::Base, base);
28        Self { values }
29    }
30    
31    /// Set a value for a specific breakpoint
32    pub fn set_breakpoint(&mut self, breakpoint: Breakpoint, value: T) {
33        self.values.insert(breakpoint, value);
34    }
35    
36    /// Get a value for a specific breakpoint
37    pub fn get_breakpoint(&self, breakpoint: Breakpoint) -> Option<&T> {
38        self.values.get(&breakpoint)
39    }
40    
41    /// Get a value for a specific breakpoint, falling back to the base value
42    pub fn get_breakpoint_or_base(&self, breakpoint: Breakpoint) -> Option<&T> {
43        self.values.get(&breakpoint).or_else(|| self.values.get(&Breakpoint::Base))
44    }
45    
46    /// Get the base value
47    pub fn get_base(&self) -> Option<&T> {
48        self.values.get(&Breakpoint::Base)
49    }
50    
51    /// Check if a breakpoint has a value
52    pub fn has_breakpoint(&self, breakpoint: Breakpoint) -> bool {
53        self.values.contains_key(&breakpoint)
54    }
55    
56    /// Get all breakpoints that have values
57    pub fn get_breakpoints(&self) -> Vec<Breakpoint> {
58        self.values.keys().cloned().collect()
59    }
60    
61    /// Check if the responsive value is empty
62    pub fn is_empty(&self) -> bool {
63        self.values.is_empty()
64    }
65    
66    /// Get the number of breakpoints with values
67    pub fn len(&self) -> usize {
68        self.values.len()
69    }
70    
71    /// Clear all values
72    pub fn clear(&mut self) {
73        self.values.clear();
74    }
75    
76    /// Remove a value for a specific breakpoint
77    pub fn remove_breakpoint(&mut self, breakpoint: Breakpoint) -> Option<T> {
78        self.values.remove(&breakpoint)
79    }
80    
81    /// Get the value for the most appropriate breakpoint based on screen width
82    pub fn get_for_width(&self, screen_width: u32) -> Option<&T> {
83        // Find the highest breakpoint that is active for this screen width
84        let active_breakpoints: Vec<Breakpoint> = self.values
85            .keys()
86            .filter(|&&bp| screen_width >= bp.min_width())
87            .cloned()
88            .collect();
89        
90        if active_breakpoints.is_empty() {
91            return self.get_base();
92        }
93        
94        // Find the breakpoint with the highest min_width among active ones
95        let best_breakpoint = active_breakpoints
96            .into_iter()
97            .max_by_key(|bp| bp.min_width())?;
98        
99        self.get_breakpoint(best_breakpoint)
100    }
101    
102    /// Generate CSS classes for all breakpoints
103    pub fn to_css_classes<F>(&self, class_generator: F) -> String
104    where
105        F: Fn(&T) -> String,
106    {
107        let mut classes = Vec::new();
108        
109        for breakpoint in Breakpoint::all() {
110            if let Some(value) = self.get_breakpoint(breakpoint) {
111                let class = class_generator(value);
112                if !class.is_empty() {
113                    if breakpoint == Breakpoint::Base {
114                        classes.push(class);
115                    } else {
116                        classes.push(format!("{}{}", breakpoint.prefix(), class));
117                    }
118                }
119            }
120        }
121        
122        classes.join(" ")
123    }
124}
125
126impl<T> Default for ResponsiveValue<T> {
127    fn default() -> Self {
128        Self::new()
129    }
130}
131
132impl<T> From<T> for ResponsiveValue<T> {
133    fn from(value: T) -> Self {
134        Self::with_base(value)
135    }
136}
137
138impl<T> From<HashMap<Breakpoint, T>> for ResponsiveValue<T> {
139    fn from(values: HashMap<Breakpoint, T>) -> Self {
140        Self { values }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_responsive_value_new() {
150        let rv = ResponsiveValue::<String>::new();
151        assert!(rv.is_empty());
152        assert_eq!(rv.len(), 0);
153    }
154
155    #[test]
156    fn test_responsive_value_with_base() {
157        let rv = ResponsiveValue::with_base("base".to_string());
158        assert!(!rv.is_empty());
159        assert_eq!(rv.len(), 1);
160        assert_eq!(rv.get_base(), Some(&"base".to_string()));
161    }
162
163    #[test]
164    fn test_responsive_value_set_get_breakpoint() {
165        let mut rv = ResponsiveValue::new();
166        rv.set_breakpoint(Breakpoint::Base, "base".to_string());
167        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
168        
169        assert_eq!(rv.get_breakpoint(Breakpoint::Base), Some(&"base".to_string()));
170        assert_eq!(rv.get_breakpoint(Breakpoint::Sm), Some(&"sm".to_string()));
171        assert_eq!(rv.get_breakpoint(Breakpoint::Md), None);
172    }
173
174    #[test]
175    fn test_responsive_value_get_breakpoint_or_base() {
176        let mut rv = ResponsiveValue::new();
177        rv.set_breakpoint(Breakpoint::Base, "base".to_string());
178        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
179        
180        assert_eq!(rv.get_breakpoint_or_base(Breakpoint::Base), Some(&"base".to_string()));
181        assert_eq!(rv.get_breakpoint_or_base(Breakpoint::Sm), Some(&"sm".to_string()));
182        assert_eq!(rv.get_breakpoint_or_base(Breakpoint::Md), Some(&"base".to_string()));
183    }
184
185    #[test]
186    fn test_responsive_value_has_breakpoint() {
187        let mut rv = ResponsiveValue::new();
188        rv.set_breakpoint(Breakpoint::Base, "base".to_string());
189        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
190        
191        assert!(rv.has_breakpoint(Breakpoint::Base));
192        assert!(rv.has_breakpoint(Breakpoint::Sm));
193        assert!(!rv.has_breakpoint(Breakpoint::Md));
194    }
195
196    #[test]
197    fn test_responsive_value_get_breakpoints() {
198        let mut rv = ResponsiveValue::new();
199        rv.set_breakpoint(Breakpoint::Base, "base".to_string());
200        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
201        rv.set_breakpoint(Breakpoint::Lg, "lg".to_string());
202        
203        let breakpoints = rv.get_breakpoints();
204        assert_eq!(breakpoints.len(), 3);
205        assert!(breakpoints.contains(&Breakpoint::Base));
206        assert!(breakpoints.contains(&Breakpoint::Sm));
207        assert!(breakpoints.contains(&Breakpoint::Lg));
208    }
209
210    #[test]
211    fn test_responsive_value_clear() {
212        let mut rv = ResponsiveValue::with_base("base".to_string());
213        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
214        
215        assert!(!rv.is_empty());
216        rv.clear();
217        assert!(rv.is_empty());
218    }
219
220    #[test]
221    fn test_responsive_value_remove_breakpoint() {
222        let mut rv = ResponsiveValue::with_base("base".to_string());
223        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
224        
225        assert_eq!(rv.len(), 2);
226        let removed = rv.remove_breakpoint(Breakpoint::Sm);
227        assert_eq!(removed, Some("sm".to_string()));
228        assert_eq!(rv.len(), 1);
229        assert!(!rv.has_breakpoint(Breakpoint::Sm));
230    }
231
232    #[test]
233    fn test_responsive_value_get_for_width() {
234        let mut rv = ResponsiveValue::new();
235        rv.set_breakpoint(Breakpoint::Base, "base".to_string());
236        rv.set_breakpoint(Breakpoint::Sm, "sm".to_string());
237        rv.set_breakpoint(Breakpoint::Md, "md".to_string());
238        
239        // Test width 0 (base only)
240        assert_eq!(rv.get_for_width(0), Some(&"base".to_string()));
241        
242        // Test width 640 (sm active)
243        assert_eq!(rv.get_for_width(640), Some(&"sm".to_string()));
244        
245        // Test width 768 (md active)
246        assert_eq!(rv.get_for_width(768), Some(&"md".to_string()));
247        
248        // Test width 1000 (md still active)
249        assert_eq!(rv.get_for_width(1000), Some(&"md".to_string()));
250    }
251
252    #[test]
253    fn test_responsive_value_to_css_classes() {
254        let mut rv = ResponsiveValue::new();
255        rv.set_breakpoint(Breakpoint::Base, "text-sm".to_string());
256        rv.set_breakpoint(Breakpoint::Sm, "text-base".to_string());
257        rv.set_breakpoint(Breakpoint::Md, "text-lg".to_string());
258        
259        let classes = rv.to_css_classes(|v| v.clone());
260        assert!(classes.contains("text-sm"));
261        assert!(classes.contains("sm:text-base"));
262        assert!(classes.contains("md:text-lg"));
263    }
264
265    #[test]
266    fn test_responsive_value_from() {
267        let rv = ResponsiveValue::from("base".to_string());
268        assert_eq!(rv.get_base(), Some(&"base".to_string()));
269        
270        let mut map = HashMap::new();
271        map.insert(Breakpoint::Base, "base".to_string());
272        map.insert(Breakpoint::Sm, "sm".to_string());
273        
274        let rv = ResponsiveValue::from(map);
275        assert_eq!(rv.len(), 2);
276        assert_eq!(rv.get_breakpoint(Breakpoint::Base), Some(&"base".to_string()));
277        assert_eq!(rv.get_breakpoint(Breakpoint::Sm), Some(&"sm".to_string()));
278    }
279}