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