1use crate::classes::ClassBuilder;
8use serde::{Deserialize, Serialize};
9use std::fmt;
10
11#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
25pub enum SizingValue {
26 Zero,
28 Px,
30 Fractional(f32),
32 Integer(u32),
34 Auto,
36 Full,
38 Screen,
40 Min,
42 Max,
44 Fit,
46 Fraction(Fraction),
48 GridFraction(GridFraction),
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
54pub enum Fraction {
55 Half,
57 Third,
59 TwoThirds,
61 Quarter,
63 TwoQuarters,
65 ThreeQuarters,
67 Fifth,
69 TwoFifths,
71 ThreeFifths,
73 FourFifths,
75 Sixth,
77 TwoSixths,
79 ThreeSixths,
81 FourSixths,
83 FiveSixths,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
89pub enum GridFraction {
90 OneTwelfth,
92 TwoTwelfths,
94 ThreeTwelfths,
96 FourTwelfths,
98 FiveTwelfths,
100 SixTwelfths,
102 SevenTwelfths,
104 EightTwelfths,
106 NineTwelfths,
108 TenTwelfths,
110 ElevenTwelfths,
112}
113
114impl std::hash::Hash for SizingValue {
115 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
116 match self {
117 SizingValue::Zero => 0u8.hash(state),
118 SizingValue::Px => 1u8.hash(state),
119 SizingValue::Fractional(f) => {
120 2u8.hash(state);
121 ((f * 1000.0) as u32).hash(state);
122 }
123 SizingValue::Integer(i) => {
124 3u8.hash(state);
125 i.hash(state);
126 }
127 SizingValue::Auto => 4u8.hash(state),
128 SizingValue::Full => 5u8.hash(state),
129 SizingValue::Screen => 6u8.hash(state),
130 SizingValue::Min => 7u8.hash(state),
131 SizingValue::Max => 8u8.hash(state),
132 SizingValue::Fit => 9u8.hash(state),
133 SizingValue::Fraction(f) => {
134 10u8.hash(state);
135 std::mem::discriminant(f).hash(state);
136 }
137 SizingValue::GridFraction(gf) => {
138 11u8.hash(state);
139 std::mem::discriminant(gf).hash(state);
140 }
141 }
142 }
143}
144
145impl std::hash::Hash for Fraction {
146 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
147 std::mem::discriminant(self).hash(state);
148 }
149}
150
151impl std::hash::Hash for GridFraction {
152 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
153 std::mem::discriminant(self).hash(state);
154 }
155}
156
157impl std::cmp::Eq for SizingValue {}
158impl std::cmp::Eq for Fraction {}
159impl std::cmp::Eq for GridFraction {}
160
161impl SizingValue {
162 pub fn to_css_value(&self) -> String {
164 match self {
165 SizingValue::Zero => "0".to_string(),
166 SizingValue::Px => "1px".to_string(),
167 SizingValue::Fractional(f) => format!("{}rem", f * 0.25),
168 SizingValue::Integer(i) => format!("{}rem", *i as f32 * 0.25),
169 SizingValue::Auto => "auto".to_string(),
170 SizingValue::Full => "100%".to_string(),
171 SizingValue::Screen => "100vh".to_string(), SizingValue::Min => "min-content".to_string(),
173 SizingValue::Max => "max-content".to_string(),
174 SizingValue::Fit => "fit-content".to_string(),
175 SizingValue::Fraction(f) => f.to_css_value(),
176 SizingValue::GridFraction(gf) => gf.to_css_value(),
177 }
178 }
179
180 pub fn to_css_value_width(&self) -> String {
182 match self {
183 SizingValue::Screen => "100vw".to_string(),
184 _ => self.to_css_value(),
185 }
186 }
187
188 pub fn to_css_value_height(&self) -> String {
190 match self {
191 SizingValue::Screen => "100vh".to_string(),
192 _ => self.to_css_value(),
193 }
194 }
195
196 pub fn to_class_name(&self) -> String {
198 match self {
199 SizingValue::Zero => "0".to_string(),
200 SizingValue::Px => "px".to_string(),
201 SizingValue::Fractional(f) => format!("{}", f),
202 SizingValue::Integer(i) => i.to_string(),
203 SizingValue::Auto => "auto".to_string(),
204 SizingValue::Full => "full".to_string(),
205 SizingValue::Screen => "screen".to_string(),
206 SizingValue::Min => "min".to_string(),
207 SizingValue::Max => "max".to_string(),
208 SizingValue::Fit => "fit".to_string(),
209 SizingValue::Fraction(f) => f.to_class_name(),
210 SizingValue::GridFraction(gf) => gf.to_class_name(),
211 }
212 }
213
214 pub fn all_values() -> Vec<SizingValue> {
216 let mut values = vec![
217 SizingValue::Zero,
218 SizingValue::Px,
219 SizingValue::Fractional(0.5),
220 SizingValue::Fractional(1.5),
221 SizingValue::Fractional(2.5),
222 SizingValue::Fractional(3.5),
223 SizingValue::Integer(1),
224 SizingValue::Integer(2),
225 SizingValue::Integer(3),
226 SizingValue::Integer(4),
227 SizingValue::Integer(5),
228 SizingValue::Integer(6),
229 SizingValue::Integer(8),
230 SizingValue::Integer(10),
231 SizingValue::Integer(12),
232 SizingValue::Integer(16),
233 SizingValue::Integer(20),
234 SizingValue::Integer(24),
235 SizingValue::Integer(32),
236 SizingValue::Integer(40),
237 SizingValue::Integer(48),
238 SizingValue::Integer(56),
239 SizingValue::Integer(64),
240 SizingValue::Integer(72),
241 SizingValue::Integer(80),
242 SizingValue::Integer(96),
243 SizingValue::Auto,
244 SizingValue::Full,
245 SizingValue::Screen,
246 SizingValue::Min,
247 SizingValue::Max,
248 SizingValue::Fit,
249 ];
250
251 values.extend([
253 SizingValue::Fraction(Fraction::Half),
254 SizingValue::Fraction(Fraction::Third),
255 SizingValue::Fraction(Fraction::TwoThirds),
256 SizingValue::Fraction(Fraction::Quarter),
257 SizingValue::Fraction(Fraction::TwoQuarters),
258 SizingValue::Fraction(Fraction::ThreeQuarters),
259 SizingValue::Fraction(Fraction::Fifth),
260 SizingValue::Fraction(Fraction::TwoFifths),
261 SizingValue::Fraction(Fraction::ThreeFifths),
262 SizingValue::Fraction(Fraction::FourFifths),
263 SizingValue::Fraction(Fraction::Sixth),
264 SizingValue::Fraction(Fraction::TwoSixths),
265 SizingValue::Fraction(Fraction::ThreeSixths),
266 SizingValue::Fraction(Fraction::FourSixths),
267 SizingValue::Fraction(Fraction::FiveSixths),
268 ]);
269
270 values.extend([
272 SizingValue::GridFraction(GridFraction::OneTwelfth),
273 SizingValue::GridFraction(GridFraction::TwoTwelfths),
274 SizingValue::GridFraction(GridFraction::ThreeTwelfths),
275 SizingValue::GridFraction(GridFraction::FourTwelfths),
276 SizingValue::GridFraction(GridFraction::FiveTwelfths),
277 SizingValue::GridFraction(GridFraction::SixTwelfths),
278 SizingValue::GridFraction(GridFraction::SevenTwelfths),
279 SizingValue::GridFraction(GridFraction::EightTwelfths),
280 SizingValue::GridFraction(GridFraction::NineTwelfths),
281 SizingValue::GridFraction(GridFraction::TenTwelfths),
282 SizingValue::GridFraction(GridFraction::ElevenTwelfths),
283 ]);
284
285 values
286 }
287}
288
289impl Fraction {
290 pub fn to_css_value(&self) -> String {
291 match self {
292 Fraction::Half => "50%".to_string(),
293 Fraction::Third => "33.333333%".to_string(),
294 Fraction::TwoThirds => "66.666667%".to_string(),
295 Fraction::Quarter => "25%".to_string(),
296 Fraction::TwoQuarters => "50%".to_string(),
297 Fraction::ThreeQuarters => "75%".to_string(),
298 Fraction::Fifth => "20%".to_string(),
299 Fraction::TwoFifths => "40%".to_string(),
300 Fraction::ThreeFifths => "60%".to_string(),
301 Fraction::FourFifths => "80%".to_string(),
302 Fraction::Sixth => "16.666667%".to_string(),
303 Fraction::TwoSixths => "33.333333%".to_string(),
304 Fraction::ThreeSixths => "50%".to_string(),
305 Fraction::FourSixths => "66.666667%".to_string(),
306 Fraction::FiveSixths => "83.333333%".to_string(),
307 }
308 }
309
310 pub fn to_class_name(&self) -> String {
311 match self {
312 Fraction::Half => "1/2".to_string(),
313 Fraction::Third => "1/3".to_string(),
314 Fraction::TwoThirds => "2/3".to_string(),
315 Fraction::Quarter => "1/4".to_string(),
316 Fraction::TwoQuarters => "2/4".to_string(),
317 Fraction::ThreeQuarters => "3/4".to_string(),
318 Fraction::Fifth => "1/5".to_string(),
319 Fraction::TwoFifths => "2/5".to_string(),
320 Fraction::ThreeFifths => "3/5".to_string(),
321 Fraction::FourFifths => "4/5".to_string(),
322 Fraction::Sixth => "1/6".to_string(),
323 Fraction::TwoSixths => "2/6".to_string(),
324 Fraction::ThreeSixths => "3/6".to_string(),
325 Fraction::FourSixths => "4/6".to_string(),
326 Fraction::FiveSixths => "5/6".to_string(),
327 }
328 }
329}
330
331impl GridFraction {
332 pub fn to_css_value(&self) -> String {
333 match self {
334 GridFraction::OneTwelfth => "8.333333%".to_string(),
335 GridFraction::TwoTwelfths => "16.666667%".to_string(),
336 GridFraction::ThreeTwelfths => "25%".to_string(),
337 GridFraction::FourTwelfths => "33.333333%".to_string(),
338 GridFraction::FiveTwelfths => "41.666667%".to_string(),
339 GridFraction::SixTwelfths => "50%".to_string(),
340 GridFraction::SevenTwelfths => "58.333333%".to_string(),
341 GridFraction::EightTwelfths => "66.666667%".to_string(),
342 GridFraction::NineTwelfths => "75%".to_string(),
343 GridFraction::TenTwelfths => "83.333333%".to_string(),
344 GridFraction::ElevenTwelfths => "91.666667%".to_string(),
345 }
346 }
347
348 pub fn to_class_name(&self) -> String {
349 match self {
350 GridFraction::OneTwelfth => "1/12".to_string(),
351 GridFraction::TwoTwelfths => "2/12".to_string(),
352 GridFraction::ThreeTwelfths => "3/12".to_string(),
353 GridFraction::FourTwelfths => "4/12".to_string(),
354 GridFraction::FiveTwelfths => "5/12".to_string(),
355 GridFraction::SixTwelfths => "6/12".to_string(),
356 GridFraction::SevenTwelfths => "7/12".to_string(),
357 GridFraction::EightTwelfths => "8/12".to_string(),
358 GridFraction::NineTwelfths => "9/12".to_string(),
359 GridFraction::TenTwelfths => "10/12".to_string(),
360 GridFraction::ElevenTwelfths => "11/12".to_string(),
361 }
362 }
363}
364
365impl fmt::Display for SizingValue {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "{}", self.to_class_name())
368 }
369}
370
371pub trait WidthUtilities {
373 fn width(self, value: SizingValue) -> Self;
375
376 fn min_width(self, value: SizingValue) -> Self;
378
379 fn max_width(self, value: SizingValue) -> Self;
381}
382
383impl WidthUtilities for ClassBuilder {
384 fn width(self, value: SizingValue) -> Self {
385 self.class(format!("w-{}", value.to_class_name()))
386 }
387
388 fn min_width(self, value: SizingValue) -> Self {
389 self.class(format!("min-w-{}", value.to_class_name()))
390 }
391
392 fn max_width(self, value: SizingValue) -> Self {
393 self.class(format!("max-w-{}", value.to_class_name()))
394 }
395}
396
397pub trait HeightUtilities {
399 fn height(self, value: SizingValue) -> Self;
401
402 fn min_height(self, value: SizingValue) -> Self;
404
405 fn max_height(self, value: SizingValue) -> Self;
407}
408
409impl HeightUtilities for ClassBuilder {
410 fn height(self, value: SizingValue) -> Self {
411 self.class(format!("h-{}", value.to_class_name()))
412 }
413
414 fn min_height(self, value: SizingValue) -> Self {
415 self.class(format!("min-h-{}", value.to_class_name()))
416 }
417
418 fn max_height(self, value: SizingValue) -> Self {
419 self.class(format!("max-h-{}", value.to_class_name()))
420 }
421}
422
423pub trait AspectRatioUtilities {
425 fn aspect_ratio(self, ratio: &str) -> Self;
427}
428
429impl AspectRatioUtilities for ClassBuilder {
430 fn aspect_ratio(self, ratio: &str) -> Self {
431 match ratio {
432 "1/1" => self.class("aspect-square"),
433 "16/9" => self.class("aspect-video"),
434 "4/3" => self.class("aspect-[4/3]"),
435 "3/2" => self.class("aspect-[3/2]"),
436 "2/3" => self.class("aspect-[2/3]"),
437 "9/16" => self.class("aspect-[9/16]"),
438 _ => self.class(format!("aspect-[{}]", ratio)),
439 }
440 }
441}
442
443impl ClassBuilder {
445 pub fn aspect_square(self) -> Self {
447 self.aspect_ratio("1/1")
448 }
449
450 pub fn aspect_video(self) -> Self {
452 self.aspect_ratio("16/9")
453 }
454
455 pub fn aspect_4_3(self) -> Self {
457 self.aspect_ratio("4/3")
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_sizing_value_to_css_value() {
467 assert_eq!(SizingValue::Zero.to_css_value(), "0");
468 assert_eq!(SizingValue::Px.to_css_value(), "1px");
469 assert_eq!(SizingValue::Fractional(0.5).to_css_value(), "0.125rem");
470 assert_eq!(SizingValue::Integer(4).to_css_value(), "1rem");
471 assert_eq!(SizingValue::Auto.to_css_value(), "auto");
472 assert_eq!(SizingValue::Full.to_css_value(), "100%");
473 assert_eq!(SizingValue::Screen.to_css_value(), "100vh");
474 }
475
476 #[test]
477 fn test_sizing_value_to_css_value_width() {
478 assert_eq!(SizingValue::Screen.to_css_value_width(), "100vw");
479 assert_eq!(SizingValue::Full.to_css_value_width(), "100%");
480 }
481
482 #[test]
483 fn test_sizing_value_to_css_value_height() {
484 assert_eq!(SizingValue::Screen.to_css_value_height(), "100vh");
485 assert_eq!(SizingValue::Full.to_css_value_height(), "100%");
486 }
487
488 #[test]
489 fn test_fraction_to_css_value() {
490 assert_eq!(Fraction::Half.to_css_value(), "50%");
491 assert_eq!(Fraction::Third.to_css_value(), "33.333333%");
492 assert_eq!(Fraction::TwoThirds.to_css_value(), "66.666667%");
493 }
494
495 #[test]
496 fn test_fraction_to_class_name() {
497 assert_eq!(Fraction::Half.to_class_name(), "1/2");
498 assert_eq!(Fraction::Third.to_class_name(), "1/3");
499 assert_eq!(Fraction::TwoThirds.to_class_name(), "2/3");
500 }
501
502 #[test]
503 fn test_grid_fraction_to_css_value() {
504 assert_eq!(GridFraction::OneTwelfth.to_css_value(), "8.333333%");
505 assert_eq!(GridFraction::SixTwelfths.to_css_value(), "50%");
506 assert_eq!(GridFraction::ElevenTwelfths.to_css_value(), "91.666667%");
507 }
508
509 #[test]
510 fn test_grid_fraction_to_class_name() {
511 assert_eq!(GridFraction::OneTwelfth.to_class_name(), "1/12");
512 assert_eq!(GridFraction::SixTwelfths.to_class_name(), "6/12");
513 assert_eq!(GridFraction::ElevenTwelfths.to_class_name(), "11/12");
514 }
515
516 #[test]
517 fn test_width_utilities() {
518 let classes = ClassBuilder::new()
519 .width(SizingValue::Full)
520 .min_width(SizingValue::Integer(4))
521 .max_width(SizingValue::Integer(8))
522 .build();
523
524 let css_classes = classes.to_css_classes();
525 assert!(css_classes.contains("w-full"));
526 assert!(css_classes.contains("min-w-4"));
527 assert!(css_classes.contains("max-w-8"));
528 }
529
530 #[test]
531 fn test_height_utilities() {
532 let classes = ClassBuilder::new()
533 .height(SizingValue::Screen)
534 .min_height(SizingValue::Integer(4))
535 .max_height(SizingValue::Integer(8))
536 .build();
537
538 let css_classes = classes.to_css_classes();
539 assert!(css_classes.contains("h-screen"));
540 assert!(css_classes.contains("min-h-4"));
541 assert!(css_classes.contains("max-h-8"));
542 }
543
544 #[test]
545 fn test_fractional_sizing() {
546 let classes = ClassBuilder::new()
547 .width(SizingValue::Fraction(Fraction::Half))
548 .height(SizingValue::Fraction(Fraction::Third))
549 .build();
550
551 let css_classes = classes.to_css_classes();
552 assert!(css_classes.contains("w-1/2"));
553 assert!(css_classes.contains("h-1/3"));
554 }
555
556 #[test]
557 fn test_grid_fractional_sizing() {
558 let classes = ClassBuilder::new()
559 .width(SizingValue::GridFraction(GridFraction::SixTwelfths))
560 .height(SizingValue::GridFraction(GridFraction::FourTwelfths))
561 .build();
562
563 let css_classes = classes.to_css_classes();
564 assert!(css_classes.contains("w-6/12"));
565 assert!(css_classes.contains("h-4/12"));
566 }
567
568 #[test]
569 fn test_special_sizing_values() {
570 let classes = ClassBuilder::new()
571 .width(SizingValue::Auto)
572 .height(SizingValue::Fit)
573 .min_width(SizingValue::Min)
574 .max_width(SizingValue::Max)
575 .build();
576
577 let css_classes = classes.to_css_classes();
578 assert!(css_classes.contains("w-auto"));
579 assert!(css_classes.contains("h-fit"));
580 assert!(css_classes.contains("min-w-min"));
581 assert!(css_classes.contains("max-w-max"));
582 }
583
584 #[test]
586 fn test_all_tailwind_sizing_values() {
587 let test_values = vec![
589 (SizingValue::Zero, "w-0"),
591 (SizingValue::Integer(1), "w-1"),
592 (SizingValue::Integer(2), "w-2"),
593 (SizingValue::Integer(3), "w-3"),
594 (SizingValue::Integer(4), "w-4"),
595 (SizingValue::Integer(5), "w-5"),
596 (SizingValue::Integer(6), "w-6"),
597 (SizingValue::Integer(8), "w-8"),
598 (SizingValue::Integer(10), "w-10"),
599 (SizingValue::Integer(12), "w-12"),
600 (SizingValue::Integer(16), "w-16"),
601 (SizingValue::Integer(20), "w-20"),
602 (SizingValue::Integer(24), "w-24"),
603 (SizingValue::Integer(32), "w-32"),
604 (SizingValue::Integer(40), "w-40"),
605 (SizingValue::Integer(48), "w-48"),
606 (SizingValue::Integer(56), "w-56"),
607 (SizingValue::Integer(64), "w-64"),
608 (SizingValue::Integer(72), "w-72"),
609 (SizingValue::Integer(80), "w-80"),
610 (SizingValue::Integer(96), "w-96"),
611 (SizingValue::Fractional(0.5), "w-0.5"),
613 (SizingValue::Fractional(1.5), "w-1.5"),
614 (SizingValue::Fractional(2.5), "w-2.5"),
615 (SizingValue::Fractional(3.5), "w-3.5"),
616 (SizingValue::Px, "w-px"),
618 (SizingValue::Auto, "w-auto"),
619 (SizingValue::Full, "w-full"),
620 (SizingValue::Screen, "w-screen"),
621 (SizingValue::Min, "w-min"),
622 (SizingValue::Max, "w-max"),
623 (SizingValue::Fit, "w-fit"),
624 ];
625
626 for (value, expected_class) in test_values {
627 let classes = ClassBuilder::new().width(value).build();
628 let css_classes = classes.to_css_classes();
629 assert!(css_classes.contains(expected_class),
630 "Missing sizing value: {} (expected class: {})",
631 format!("{:?}", value), expected_class);
632 }
633 }
634
635 #[test]
637 fn test_aspect_ratio_utilities() {
638 let classes = ClassBuilder::new()
640 .aspect_ratio("1/1") .aspect_ratio("16/9") .aspect_ratio("4/3") .build();
644
645 let css_classes = classes.to_css_classes();
646 assert!(css_classes.contains("aspect-square"));
647 assert!(css_classes.contains("aspect-video"));
648 assert!(css_classes.contains("aspect-[4/3]"));
649 }
650}