tailwind_rs_core/responsive/
responsive_config.rs1use super::breakpoints::Breakpoint;
6use crate::error::{Result, TailwindError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct ResponsiveConfig {
13 pub breakpoints: HashMap<Breakpoint, BreakpointConfig>,
15 pub defaults: ResponsiveDefaults,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct BreakpointConfig {
22 pub min_width: u32,
24 pub max_width: Option<u32>,
26 pub enabled: bool,
28 pub media_query: Option<String>,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct ResponsiveDefaults {
35 pub default_breakpoint: Breakpoint,
37 pub include_base: bool,
39 pub mobile_first: bool,
41}
42
43impl ResponsiveConfig {
44 pub fn new() -> Self {
46 Self::default()
47 }
48
49 pub fn with_breakpoints(breakpoints: HashMap<Breakpoint, BreakpointConfig>) -> Self {
51 Self {
52 breakpoints,
53 defaults: ResponsiveDefaults::default(),
54 }
55 }
56
57 pub fn get_breakpoint_config(&self, breakpoint: Breakpoint) -> Option<&BreakpointConfig> {
59 self.breakpoints.get(&breakpoint)
60 }
61
62 pub fn set_breakpoint_config(&mut self, breakpoint: Breakpoint, config: BreakpointConfig) {
64 self.breakpoints.insert(breakpoint, config);
65 }
66
67 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 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 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 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 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 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 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 pub fn validate(&self) -> Result<()> {
135 if !self.breakpoints.contains_key(&Breakpoint::Base) {
137 return Err(TailwindError::config(
138 "Base breakpoint is required".to_string(),
139 ));
140 }
141
142 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct Responsive {
250 pub config: ResponsiveConfig,
252 pub current_breakpoint: Breakpoint,
254}
255
256impl Responsive {
257 pub fn new() -> Self {
259 Self::default()
260 }
261
262 pub fn with_config(config: ResponsiveConfig) -> Self {
264 Self {
265 current_breakpoint: config.defaults.default_breakpoint,
266 config,
267 }
268 }
269
270 pub fn get_current_breakpoint(&self) -> Breakpoint {
272 self.current_breakpoint
273 }
274
275 pub fn set_current_breakpoint(&mut self, breakpoint: Breakpoint) {
277 self.current_breakpoint = breakpoint;
278 }
279
280 pub fn get_config(&self) -> &ResponsiveConfig {
282 &self.config
283 }
284
285 pub fn update_config(&mut self, config: ResponsiveConfig) {
287 self.config = config;
288 }
289
290 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 pub fn get_breakpoint_for_width(&self, screen_width: u32) -> Breakpoint {
307 let enabled_breakpoints = self.config.get_enabled_breakpoints();
308
309 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 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}