Skip to main content

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