presentar_core/
constraints.rs

1//! Layout constraints for widgets.
2//!
3//! # Examples
4//!
5//! ```
6//! use presentar_core::{Constraints, Size};
7//!
8//! // Create constraints with min/max bounds
9//! let constraints = Constraints::new(0.0, 200.0, 0.0, 100.0);
10//!
11//! // Constrain a size to fit
12//! let size = Size::new(300.0, 50.0);  // Too wide
13//! let bounded = constraints.constrain(size);
14//! assert_eq!(bounded.width, 200.0);   // Clamped to max
15//! assert_eq!(bounded.height, 50.0);   // Within bounds
16//! ```
17
18use crate::geometry::Size;
19use serde::{Deserialize, Serialize};
20
21/// Layout constraints that specify minimum and maximum sizes.
22///
23/// # Examples
24///
25/// ```
26/// use presentar_core::{Constraints, Size};
27///
28/// // Tight constraints allow only one size
29/// let tight = Constraints::tight(Size::new(100.0, 50.0));
30/// assert_eq!(tight.min_width, 100.0);
31/// assert_eq!(tight.max_width, 100.0);
32///
33/// // Loose constraints allow any size up to maximum
34/// let loose = Constraints::loose(Size::new(400.0, 300.0));
35/// assert_eq!(loose.min_width, 0.0);
36/// assert_eq!(loose.max_width, 400.0);
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
39pub struct Constraints {
40    /// Minimum width
41    pub min_width: f32,
42    /// Maximum width
43    pub max_width: f32,
44    /// Minimum height
45    pub min_height: f32,
46    /// Maximum height
47    pub max_height: f32,
48}
49
50impl Constraints {
51    /// Create new constraints.
52    #[must_use]
53    pub const fn new(min_width: f32, max_width: f32, min_height: f32, max_height: f32) -> Self {
54        Self {
55            min_width,
56            max_width,
57            min_height,
58            max_height,
59        }
60    }
61
62    /// Create tight constraints that allow only the exact size.
63    #[must_use]
64    pub const fn tight(size: Size) -> Self {
65        Self::new(size.width, size.width, size.height, size.height)
66    }
67
68    /// Create loose constraints that allow any size up to the given maximum.
69    #[must_use]
70    pub const fn loose(size: Size) -> Self {
71        Self::new(0.0, size.width, 0.0, size.height)
72    }
73
74    /// Create unbounded constraints.
75    #[must_use]
76    pub const fn unbounded() -> Self {
77        Self::new(0.0, f32::INFINITY, 0.0, f32::INFINITY)
78    }
79
80    /// Constrain a size to fit within these constraints.
81    #[must_use]
82    pub fn constrain(&self, size: Size) -> Size {
83        Size::new(
84            size.width.clamp(self.min_width, self.max_width),
85            size.height.clamp(self.min_height, self.max_height),
86        )
87    }
88
89    /// Check if constraints specify an exact size.
90    #[must_use]
91    pub fn is_tight(&self) -> bool {
92        self.min_width == self.max_width && self.min_height == self.max_height
93    }
94
95    /// Check if width is bounded (not infinite).
96    #[must_use]
97    pub fn has_bounded_width(&self) -> bool {
98        self.max_width.is_finite()
99    }
100
101    /// Check if height is bounded (not infinite).
102    #[must_use]
103    pub fn has_bounded_height(&self) -> bool {
104        self.max_height.is_finite()
105    }
106
107    /// Check if both dimensions are bounded.
108    #[must_use]
109    pub fn is_bounded(&self) -> bool {
110        self.has_bounded_width() && self.has_bounded_height()
111    }
112
113    /// Get the biggest size that satisfies these constraints.
114    #[must_use]
115    pub fn biggest(&self) -> Size {
116        Size::new(
117            if self.max_width.is_finite() {
118                self.max_width
119            } else {
120                self.min_width
121            },
122            if self.max_height.is_finite() {
123                self.max_height
124            } else {
125                self.min_height
126            },
127        )
128    }
129
130    /// Get the smallest size that satisfies these constraints.
131    #[must_use]
132    pub const fn smallest(&self) -> Size {
133        Size::new(self.min_width, self.min_height)
134    }
135
136    /// Create constraints with a different minimum width.
137    #[must_use]
138    pub const fn with_min_width(&self, min_width: f32) -> Self {
139        Self::new(min_width, self.max_width, self.min_height, self.max_height)
140    }
141
142    /// Create constraints with a different maximum width.
143    #[must_use]
144    pub const fn with_max_width(&self, max_width: f32) -> Self {
145        Self::new(self.min_width, max_width, self.min_height, self.max_height)
146    }
147
148    /// Create constraints with a different minimum height.
149    #[must_use]
150    pub const fn with_min_height(&self, min_height: f32) -> Self {
151        Self::new(self.min_width, self.max_width, min_height, self.max_height)
152    }
153
154    /// Create constraints with a different maximum height.
155    #[must_use]
156    pub const fn with_max_height(&self, max_height: f32) -> Self {
157        Self::new(self.min_width, self.max_width, self.min_height, max_height)
158    }
159
160    /// Deflate constraints by padding.
161    #[must_use]
162    pub fn deflate(&self, horizontal: f32, vertical: f32) -> Self {
163        Self::new(
164            (self.min_width - horizontal).max(0.0),
165            (self.max_width - horizontal).max(0.0),
166            (self.min_height - vertical).max(0.0),
167            (self.max_height - vertical).max(0.0),
168        )
169    }
170}
171
172impl Default for Constraints {
173    fn default() -> Self {
174        Self::unbounded()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_constraints_default() {
184        let c = Constraints::default();
185        assert_eq!(c.min_width, 0.0);
186        assert_eq!(c.max_width, f32::INFINITY);
187    }
188
189    #[test]
190    fn test_constraints_biggest() {
191        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
192        assert_eq!(c.biggest(), Size::new(100.0, 200.0));
193    }
194
195    #[test]
196    fn test_constraints_smallest() {
197        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
198        assert_eq!(c.smallest(), Size::new(10.0, 20.0));
199    }
200
201    #[test]
202    fn test_constraints_deflate() {
203        let c = Constraints::new(100.0, 200.0, 100.0, 200.0);
204        let deflated = c.deflate(20.0, 20.0);
205        assert_eq!(deflated.min_width, 80.0);
206        assert_eq!(deflated.max_width, 180.0);
207    }
208
209    #[test]
210    fn test_constraints_with_methods() {
211        let c = Constraints::new(0.0, 100.0, 0.0, 100.0);
212        assert_eq!(c.with_min_width(10.0).min_width, 10.0);
213        assert_eq!(c.with_max_width(200.0).max_width, 200.0);
214        assert_eq!(c.with_min_height(10.0).min_height, 10.0);
215        assert_eq!(c.with_max_height(200.0).max_height, 200.0);
216    }
217
218    #[test]
219    fn test_constraints_tight() {
220        let c = Constraints::tight(Size::new(100.0, 50.0));
221        assert_eq!(c.min_width, 100.0);
222        assert_eq!(c.max_width, 100.0);
223        assert_eq!(c.min_height, 50.0);
224        assert_eq!(c.max_height, 50.0);
225        assert!(c.is_tight());
226    }
227
228    #[test]
229    fn test_constraints_loose() {
230        let c = Constraints::loose(Size::new(100.0, 50.0));
231        assert_eq!(c.min_width, 0.0);
232        assert_eq!(c.max_width, 100.0);
233        assert_eq!(c.min_height, 0.0);
234        assert_eq!(c.max_height, 50.0);
235        assert!(!c.is_tight());
236    }
237
238    #[test]
239    fn test_constraints_unbounded() {
240        let c = Constraints::unbounded();
241        assert_eq!(c.min_width, 0.0);
242        assert!(c.max_width.is_infinite());
243        assert!(!c.is_bounded());
244    }
245
246    #[test]
247    fn test_constraints_constrain() {
248        let c = Constraints::new(10.0, 100.0, 20.0, 80.0);
249        assert_eq!(c.constrain(Size::new(50.0, 50.0)), Size::new(50.0, 50.0));
250        assert_eq!(c.constrain(Size::new(5.0, 5.0)), Size::new(10.0, 20.0));
251        assert_eq!(c.constrain(Size::new(200.0, 200.0)), Size::new(100.0, 80.0));
252    }
253
254    #[test]
255    fn test_constraints_is_tight_false() {
256        let c = Constraints::new(0.0, 100.0, 0.0, 100.0);
257        assert!(!c.is_tight());
258    }
259
260    #[test]
261    fn test_constraints_has_bounded_width() {
262        let c = Constraints::new(0.0, 100.0, 0.0, f32::INFINITY);
263        assert!(c.has_bounded_width());
264        assert!(!c.has_bounded_height());
265    }
266
267    #[test]
268    fn test_constraints_is_bounded() {
269        let bounded = Constraints::new(0.0, 100.0, 0.0, 100.0);
270        assert!(bounded.is_bounded());
271
272        let unbounded = Constraints::unbounded();
273        assert!(!unbounded.is_bounded());
274    }
275
276    #[test]
277    fn test_constraints_biggest_unbounded() {
278        let c = Constraints::unbounded();
279        assert_eq!(c.biggest(), Size::new(0.0, 0.0));
280    }
281
282    #[test]
283    fn test_constraints_deflate_to_zero() {
284        let c = Constraints::new(10.0, 20.0, 10.0, 20.0);
285        let deflated = c.deflate(50.0, 50.0);
286        assert_eq!(deflated.min_width, 0.0);
287        assert_eq!(deflated.max_width, 0.0);
288    }
289
290    // =========================================================================
291    // Clone and Copy Trait Tests
292    // =========================================================================
293
294    #[test]
295    fn test_constraints_clone() {
296        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
297        let cloned = c;
298        assert_eq!(c, cloned);
299    }
300
301    #[test]
302    fn test_constraints_copy() {
303        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
304        let copied = c;
305        // Both should be valid and equal
306        assert_eq!(c.min_width, copied.min_width);
307        assert_eq!(c.max_width, copied.max_width);
308    }
309
310    // =========================================================================
311    // Debug Trait Tests
312    // =========================================================================
313
314    #[test]
315    fn test_constraints_debug() {
316        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
317        let debug = format!("{:?}", c);
318        assert!(debug.contains("Constraints"));
319        assert!(debug.contains("min_width"));
320        assert!(debug.contains("max_width"));
321    }
322
323    // =========================================================================
324    // PartialEq Tests
325    // =========================================================================
326
327    #[test]
328    fn test_constraints_equality() {
329        let c1 = Constraints::new(10.0, 100.0, 20.0, 200.0);
330        let c2 = Constraints::new(10.0, 100.0, 20.0, 200.0);
331        assert_eq!(c1, c2);
332    }
333
334    #[test]
335    fn test_constraints_inequality_min_width() {
336        let c1 = Constraints::new(10.0, 100.0, 20.0, 200.0);
337        let c2 = Constraints::new(15.0, 100.0, 20.0, 200.0);
338        assert_ne!(c1, c2);
339    }
340
341    #[test]
342    fn test_constraints_inequality_max_width() {
343        let c1 = Constraints::new(10.0, 100.0, 20.0, 200.0);
344        let c2 = Constraints::new(10.0, 150.0, 20.0, 200.0);
345        assert_ne!(c1, c2);
346    }
347
348    #[test]
349    fn test_constraints_inequality_min_height() {
350        let c1 = Constraints::new(10.0, 100.0, 20.0, 200.0);
351        let c2 = Constraints::new(10.0, 100.0, 25.0, 200.0);
352        assert_ne!(c1, c2);
353    }
354
355    #[test]
356    fn test_constraints_inequality_max_height() {
357        let c1 = Constraints::new(10.0, 100.0, 20.0, 200.0);
358        let c2 = Constraints::new(10.0, 100.0, 20.0, 250.0);
359        assert_ne!(c1, c2);
360    }
361
362    // =========================================================================
363    // Serialization Tests
364    // =========================================================================
365
366    #[test]
367    fn test_constraints_serialize() {
368        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
369        let json = serde_json::to_string(&c).unwrap();
370        assert!(json.contains("min_width"));
371        assert!(json.contains("10"));
372    }
373
374    #[test]
375    fn test_constraints_deserialize() {
376        let json = r#"{"min_width":10.0,"max_width":100.0,"min_height":20.0,"max_height":200.0}"#;
377        let c: Constraints = serde_json::from_str(json).unwrap();
378        assert_eq!(c.min_width, 10.0);
379        assert_eq!(c.max_width, 100.0);
380        assert_eq!(c.min_height, 20.0);
381        assert_eq!(c.max_height, 200.0);
382    }
383
384    #[test]
385    fn test_constraints_roundtrip_serialization() {
386        let original = Constraints::new(15.5, 150.5, 25.5, 250.5);
387        let json = serde_json::to_string(&original).unwrap();
388        let deserialized: Constraints = serde_json::from_str(&json).unwrap();
389        assert_eq!(original, deserialized);
390    }
391
392    // =========================================================================
393    // Constrain Edge Cases
394    // =========================================================================
395
396    #[test]
397    fn test_constrain_at_minimum() {
398        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
399        let size = Size::new(10.0, 20.0);
400        assert_eq!(c.constrain(size), size);
401    }
402
403    #[test]
404    fn test_constrain_at_maximum() {
405        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
406        let size = Size::new(100.0, 200.0);
407        assert_eq!(c.constrain(size), size);
408    }
409
410    #[test]
411    fn test_constrain_zero_size() {
412        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
413        let size = Size::new(0.0, 0.0);
414        assert_eq!(c.constrain(size), Size::new(10.0, 20.0));
415    }
416
417    #[test]
418    fn test_constrain_negative_clamped() {
419        let c = Constraints::new(0.0, 100.0, 0.0, 100.0);
420        let size = Size::new(-10.0, -20.0);
421        assert_eq!(c.constrain(size), Size::new(0.0, 0.0));
422    }
423
424    #[test]
425    fn test_constrain_with_zero_constraints() {
426        let c = Constraints::new(0.0, 0.0, 0.0, 0.0);
427        let size = Size::new(100.0, 100.0);
428        assert_eq!(c.constrain(size), Size::new(0.0, 0.0));
429    }
430
431    // =========================================================================
432    // is_tight Edge Cases
433    // =========================================================================
434
435    #[test]
436    fn test_is_tight_width_only() {
437        let c = Constraints::new(50.0, 50.0, 0.0, 100.0);
438        assert!(!c.is_tight()); // Height is not tight
439    }
440
441    #[test]
442    fn test_is_tight_height_only() {
443        let c = Constraints::new(0.0, 100.0, 50.0, 50.0);
444        assert!(!c.is_tight()); // Width is not tight
445    }
446
447    #[test]
448    fn test_is_tight_zero_size() {
449        let c = Constraints::tight(Size::new(0.0, 0.0));
450        assert!(c.is_tight());
451    }
452
453    // =========================================================================
454    // Bounded Tests
455    // =========================================================================
456
457    #[test]
458    fn test_has_bounded_height_only() {
459        let c = Constraints::new(0.0, f32::INFINITY, 0.0, 100.0);
460        assert!(!c.has_bounded_width());
461        assert!(c.has_bounded_height());
462        assert!(!c.is_bounded());
463    }
464
465    #[test]
466    fn test_has_bounded_width_only() {
467        let c = Constraints::new(0.0, 100.0, 0.0, f32::INFINITY);
468        assert!(c.has_bounded_width());
469        assert!(!c.has_bounded_height());
470        assert!(!c.is_bounded());
471    }
472
473    // =========================================================================
474    // biggest() Edge Cases
475    // =========================================================================
476
477    #[test]
478    fn test_biggest_with_infinity_width_only() {
479        let c = Constraints::new(50.0, f32::INFINITY, 0.0, 100.0);
480        let biggest = c.biggest();
481        assert_eq!(biggest.width, 50.0); // Falls back to min
482        assert_eq!(biggest.height, 100.0);
483    }
484
485    #[test]
486    fn test_biggest_with_infinity_height_only() {
487        let c = Constraints::new(0.0, 100.0, 50.0, f32::INFINITY);
488        let biggest = c.biggest();
489        assert_eq!(biggest.width, 100.0);
490        assert_eq!(biggest.height, 50.0); // Falls back to min
491    }
492
493    #[test]
494    fn test_biggest_tight_constraints() {
495        let c = Constraints::tight(Size::new(42.0, 24.0));
496        assert_eq!(c.biggest(), Size::new(42.0, 24.0));
497    }
498
499    // =========================================================================
500    // smallest() Tests
501    // =========================================================================
502
503    #[test]
504    fn test_smallest_unbounded() {
505        let c = Constraints::unbounded();
506        assert_eq!(c.smallest(), Size::new(0.0, 0.0));
507    }
508
509    #[test]
510    fn test_smallest_tight() {
511        let c = Constraints::tight(Size::new(42.0, 24.0));
512        assert_eq!(c.smallest(), Size::new(42.0, 24.0));
513    }
514
515    #[test]
516    fn test_smallest_loose() {
517        let c = Constraints::loose(Size::new(100.0, 200.0));
518        assert_eq!(c.smallest(), Size::new(0.0, 0.0));
519    }
520
521    // =========================================================================
522    // with_* Methods Chain Tests
523    // =========================================================================
524
525    #[test]
526    fn test_with_methods_chained() {
527        let c = Constraints::unbounded()
528            .with_min_width(10.0)
529            .with_max_width(100.0)
530            .with_min_height(20.0)
531            .with_max_height(200.0);
532
533        assert_eq!(c.min_width, 10.0);
534        assert_eq!(c.max_width, 100.0);
535        assert_eq!(c.min_height, 20.0);
536        assert_eq!(c.max_height, 200.0);
537    }
538
539    #[test]
540    fn test_with_methods_preserve_other_values() {
541        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
542
543        let c2 = c.with_min_width(15.0);
544        assert_eq!(c2.max_width, 100.0);
545        assert_eq!(c2.min_height, 20.0);
546        assert_eq!(c2.max_height, 200.0);
547
548        let c3 = c.with_max_width(150.0);
549        assert_eq!(c3.min_width, 10.0);
550        assert_eq!(c3.min_height, 20.0);
551        assert_eq!(c3.max_height, 200.0);
552    }
553
554    // =========================================================================
555    // deflate() Edge Cases
556    // =========================================================================
557
558    #[test]
559    fn test_deflate_asymmetric() {
560        let c = Constraints::new(20.0, 100.0, 30.0, 150.0);
561        let deflated = c.deflate(10.0, 20.0);
562        assert_eq!(deflated.min_width, 10.0);
563        assert_eq!(deflated.max_width, 90.0);
564        assert_eq!(deflated.min_height, 10.0);
565        assert_eq!(deflated.max_height, 130.0);
566    }
567
568    #[test]
569    fn test_deflate_zero() {
570        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
571        let deflated = c.deflate(0.0, 0.0);
572        assert_eq!(c, deflated);
573    }
574
575    #[test]
576    fn test_deflate_exact_match() {
577        let c = Constraints::new(10.0, 100.0, 20.0, 200.0);
578        let deflated = c.deflate(10.0, 20.0);
579        assert_eq!(deflated.min_width, 0.0);
580        assert_eq!(deflated.max_width, 90.0);
581        assert_eq!(deflated.min_height, 0.0);
582        assert_eq!(deflated.max_height, 180.0);
583    }
584
585    #[test]
586    fn test_deflate_negative_becomes_zero() {
587        let c = Constraints::new(5.0, 10.0, 5.0, 10.0);
588        let deflated = c.deflate(15.0, 15.0);
589        assert_eq!(deflated.min_width, 0.0);
590        assert_eq!(deflated.max_width, 0.0);
591        assert_eq!(deflated.min_height, 0.0);
592        assert_eq!(deflated.max_height, 0.0);
593    }
594
595    // =========================================================================
596    // Constructor Edge Cases
597    // =========================================================================
598
599    #[test]
600    fn test_new_with_zero_values() {
601        let c = Constraints::new(0.0, 0.0, 0.0, 0.0);
602        assert_eq!(c.min_width, 0.0);
603        assert_eq!(c.max_width, 0.0);
604        assert!(c.is_tight());
605    }
606
607    #[test]
608    fn test_tight_with_large_values() {
609        let c = Constraints::tight(Size::new(10000.0, 10000.0));
610        assert!(c.is_tight());
611        assert_eq!(c.biggest(), Size::new(10000.0, 10000.0));
612    }
613
614    #[test]
615    fn test_loose_with_zero() {
616        let c = Constraints::loose(Size::new(0.0, 0.0));
617        assert!(c.is_tight()); // min and max are both 0
618        assert_eq!(c.biggest(), Size::new(0.0, 0.0));
619    }
620
621    // =========================================================================
622    // Default Trait Tests
623    // =========================================================================
624
625    #[test]
626    fn test_default_is_unbounded() {
627        let default = Constraints::default();
628        let unbounded = Constraints::unbounded();
629        assert_eq!(default, unbounded);
630    }
631
632    #[test]
633    fn test_default_not_bounded() {
634        let c = Constraints::default();
635        assert!(!c.is_bounded());
636        assert!(!c.has_bounded_width());
637        assert!(!c.has_bounded_height());
638    }
639}