tailwind_rs_core/responsive/
responsive_builder.rs1use super::breakpoints::Breakpoint;
6use super::responsive_config::ResponsiveConfig;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12#[derive(Default)]
13pub struct ResponsiveBuilder {
14 classes: HashMap<Breakpoint, Vec<String>>,
16 config: ResponsiveConfig,
18}
19
20impl ResponsiveBuilder {
21 pub fn new() -> Self {
23 Self::default()
24 }
25
26 pub fn with_config(config: ResponsiveConfig) -> Self {
28 Self {
29 classes: HashMap::new(),
30 config,
31 }
32 }
33
34 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 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 pub fn base(&mut self, class: impl Into<String>) -> &mut Self {
54 self.add_class(Breakpoint::Base, class)
55 }
56
57 pub fn sm(&mut self, class: impl Into<String>) -> &mut Self {
59 self.add_class(Breakpoint::Sm, class)
60 }
61
62 pub fn md(&mut self, class: impl Into<String>) -> &mut Self {
64 self.add_class(Breakpoint::Md, class)
65 }
66
67 pub fn lg(&mut self, class: impl Into<String>) -> &mut Self {
69 self.add_class(Breakpoint::Lg, class)
70 }
71
72 pub fn xl(&mut self, class: impl Into<String>) -> &mut Self {
74 self.add_class(Breakpoint::Xl, class)
75 }
76
77 pub fn xl2(&mut self, class: impl Into<String>) -> &mut Self {
79 self.add_class(Breakpoint::Xl2, class)
80 }
81
82 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 pub fn get_classes(&self, breakpoint: Breakpoint) -> Vec<String> {
115 self.classes.get(&breakpoint).cloned().unwrap_or_default()
116 }
117
118 pub fn get_all_classes(&self) -> HashMap<Breakpoint, Vec<String>> {
120 self.classes.clone()
121 }
122
123 pub fn is_empty(&self) -> bool {
125 self.classes.is_empty() || self.classes.values().all(|classes| classes.is_empty())
126 }
127
128 pub fn len(&self) -> usize {
130 self.classes.len()
131 }
132
133 pub fn clear(&mut self) {
135 self.classes.clear();
136 }
137
138 pub fn remove_breakpoint(&mut self, breakpoint: Breakpoint) -> Vec<String> {
140 self.classes.remove(&breakpoint).unwrap_or_default()
141 }
142
143 pub fn build(&self) -> String {
145 let mut classes = Vec::new();
146
147 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 pub fn build_for_width(&self, screen_width: u32) -> String {
166 let mut classes = Vec::new();
167
168 let target_breakpoint = self.config.get_breakpoint_for_width(screen_width);
170
171 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 pub fn get_config(&self) -> &ResponsiveConfig {
196 &self.config
197 }
198
199 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 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 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 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}