tessera_ui/component_tree/
constraint.rs

1//! # Layout Constraint System
2//!
3//! This module provides the core constraint system for Tessera's layout engine.
4//! It defines how components specify their sizing requirements and how these
5//! constraints are resolved in a component hierarchy.
6//!
7//! ## Overview
8//!
9//! The constraint system is built around two main concepts:
10//!
11//! - **[`DimensionValue`]**: Specifies how a single dimension (width or height) should be calculated
12//! - **[`Constraint`]**: Combines width and height dimension values for complete layout specification
13//!
14//! ## Dimension Types
15//!
16//! There are three fundamental ways a component can specify its size:
17//!
18//! ### Fixed
19//! The component has a specific, unchanging size:
20//! ```
21//! # use tessera_ui::Px;
22//! # use tessera_ui::DimensionValue;
23//! let fixed_width = DimensionValue::Fixed(Px(100));
24//! ```
25//!
26//! ### Wrap
27//! The component sizes itself to fit its content, with optional bounds:
28//! ```
29//! # use tessera_ui::Px;
30//! # use tessera_ui::DimensionValue;
31//! // Wrap content with no limits
32//! let wrap_content = DimensionValue::Wrap { min: None, max: None };
33//!
34//! // Wrap content but ensure at least 50px wide
35//! let wrap_with_min = DimensionValue::Wrap { min: Some(Px(50)), max: None };
36//!
37//! // Wrap content but never exceed 200px
38//! let wrap_with_max = DimensionValue::Wrap { min: None, max: Some(Px(200)) };
39//!
40//! // Wrap content within bounds
41//! let wrap_bounded = DimensionValue::Wrap {
42//!     min: Some(Px(50)),
43//!     max: Some(Px(200))
44//! };
45//! ```
46//!
47//! ### Fill
48//! The component expands to fill available space, with optional bounds:
49//! ```
50//! # use tessera_ui::Px;
51//! # use tessera_ui::DimensionValue;
52//! // Fill all available space
53//! let fill_all = DimensionValue::Fill { min: None, max: None };
54//!
55//! // Fill space but ensure at least 100px
56//! let fill_with_min = DimensionValue::Fill { min: Some(Px(100)), max: None };
57//!
58//! // Fill space but never exceed 300px
59//! let fill_with_max = DimensionValue::Fill { min: None, max: Some(Px(300)) };
60//! ```
61//!
62//! ## Constraint Merging
63//!
64//! When components are nested, their constraints must be merged to resolve conflicts
65//! and ensure consistent layout. The [`Constraint::merge`] method implements this
66//! logic with the following rules:
67//!
68//! - **Fixed always wins**: A fixed constraint cannot be overridden by its parent
69//! - **Wrap preserves content sizing**: Wrap constraints maintain their intrinsic sizing behavior
70//! - **Fill adapts to available space**: Fill constraints expand within parent bounds
71//!
72//! ### Merge Examples
73//!
74//! ```
75//! # use tessera_ui::Px;
76//! # use tessera_ui::{Constraint, DimensionValue};
77//! // Parent provides 200px of space
78//! let parent = Constraint::new(
79//!     DimensionValue::Fixed(Px(200)),
80//!     DimensionValue::Fixed(Px(200))
81//! );
82//!
83//! // Child wants to fill with minimum 50px
84//! let child = Constraint::new(
85//!     DimensionValue::Fill { min: Some(Px(50)), max: None },
86//!     DimensionValue::Fill { min: Some(Px(50)), max: None }
87//! );
88//!
89//! // Result: Child fills parent's 200px space, respecting its 50px minimum
90//! let merged = child.merge(&parent);
91//! assert_eq!(merged.width, DimensionValue::Fill {
92//!     min: Some(Px(50)),
93//!     max: Some(Px(200))
94//! });
95//! ```
96//!
97//! ## Usage in Components
98//!
99//! Components typically specify their constraints during the measurement phase:
100//!
101//! ```rust,ignore
102//! #[tessera]
103//! fn my_component() {
104//!     measure(|constraints| {
105//!         // This component wants to be exactly 100x50 pixels
106//!         let my_constraint = Constraint::new(
107//!             DimensionValue::Fixed(Px(100)),
108//!             DimensionValue::Fixed(Px(50))
109//!         );
110//!         
111//!         // Measure children with merged constraints
112//!         let child_constraint = my_constraint.merge(&constraints);
113//!         // ... measure children ...
114//!         
115//!         ComputedData::new(Size::new(Px(100), Px(50)))
116//!     });
117//! }
118//! ```
119
120use std::ops::Sub;
121
122use crate::{Dp, Px};
123
124/// Defines how a dimension (width or height) should be calculated.
125///
126/// This enum represents the three fundamental sizing strategies available
127/// in Tessera's layout system. Each variant provides different behavior
128/// for how a component determines its size in a given dimension.
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
130pub enum DimensionValue {
131    /// The dimension is a fixed value in logical pixels.
132    ///
133    /// This variant represents a component that has a specific, unchanging size.
134    /// Fixed dimensions cannot be overridden by parent constraints and will
135    /// always maintain their specified size regardless of available space.
136    ///
137    /// # Example
138    /// ```
139    /// # use tessera_ui::Px;
140    /// # use tessera_ui::DimensionValue;
141    /// let button_width = DimensionValue::Fixed(Px(120));
142    /// ```
143    Fixed(Px),
144
145    /// The dimension should wrap its content, optionally bounded by min and/or max logical pixels.
146    ///
147    /// This variant represents a component that sizes itself based on its content.
148    /// The component will be as small as possible while still containing all its content,
149    /// but can be constrained by optional minimum and maximum bounds.
150    ///
151    /// # Parameters
152    /// - `min`: Optional minimum size - the component will never be smaller than this
153    /// - `max`: Optional maximum size - the component will never be larger than this
154    ///
155    /// # Examples
156    /// ```
157    /// # use tessera_ui::Px;
158    /// # use tessera_ui::DimensionValue;
159    /// // Text that wraps to its content size
160    /// let text_width = DimensionValue::Wrap { min: None, max: None };
161    ///
162    /// // Text with minimum width to prevent being too narrow
163    /// let min_text_width = DimensionValue::Wrap { min: Some(Px(100)), max: None };
164    ///
165    /// // Text that wraps but never exceeds container width
166    /// let bounded_text = DimensionValue::Wrap { min: Some(Px(50)), max: Some(Px(300)) };
167    /// ```
168    Wrap { min: Option<Px>, max: Option<Px> },
169
170    /// The dimension should fill the available space, optionally bounded by min and/or max logical pixels.
171    ///
172    /// This variant represents a component that expands to use all available space
173    /// provided by its parent. The expansion can be constrained by optional minimum
174    /// and maximum bounds.
175    ///
176    /// # Parameters
177    /// - `min`: Optional minimum size - the component will never be smaller than this
178    /// - `max`: Optional maximum size - the component will never be larger than this
179    ///
180    /// # Examples
181    /// ```
182    /// # use tessera_ui::Px;
183    /// # use tessera_ui::DimensionValue;
184    /// // Fill all available space
185    /// let flexible_width = DimensionValue::Fill { min: None, max: None };
186    ///
187    /// // Fill space but ensure minimum usability
188    /// let min_fill_width = DimensionValue::Fill { min: Some(Px(200)), max: None };
189    ///
190    /// // Fill space but cap maximum size for readability
191    /// let capped_fill = DimensionValue::Fill { min: Some(Px(100)), max: Some(Px(800)) };
192    /// ```
193    Fill { min: Option<Px>, max: Option<Px> },
194}
195
196impl DimensionValue {
197    /// Zero-sized dimension, equivalent to `Fixed(Px(0))`.
198    pub const ZERO: Self = DimensionValue::Fixed(Px(0));
199
200    /// Fill with no constraints.
201    pub const FILLED: Self = DimensionValue::Fill {
202        min: None,
203        max: None,
204    };
205
206    /// Wrap with no constraints.
207    pub const WRAP: Self = DimensionValue::Wrap {
208        min: None,
209        max: None,
210    };
211}
212
213impl Default for DimensionValue {
214    /// Returns the default dimension value: `Wrap { min: None, max: None }`.
215    ///
216    /// This default represents a component that sizes itself to its content
217    /// without any constraints, which is the most flexible and commonly used
218    /// sizing behavior.
219    fn default() -> Self {
220        DimensionValue::Wrap {
221            min: None,
222            max: None,
223        }
224    }
225}
226
227impl DimensionValue {
228    /// Converts this dimension value to a maximum pixel value.
229    ///
230    /// This method is useful during layout calculation when you need to determine
231    /// the maximum space a component might occupy.
232    ///
233    /// # Parameters
234    /// - `default`: The value to use when no maximum is specified
235    ///
236    /// # Returns
237    /// - For `Fixed`: Returns the fixed value
238    /// - For `Wrap` and `Fill`: Returns the `max` value if specified, otherwise the `default`
239    ///
240    /// # Example
241    /// ```
242    /// # use tessera_ui::Px;
243    /// # use tessera_ui::DimensionValue;
244    /// let fixed = DimensionValue::Fixed(Px(100));
245    /// assert_eq!(fixed.to_max_px(Px(200)), Px(100));
246    ///
247    /// let wrap_unbounded = DimensionValue::Wrap { min: None, max: None };
248    /// assert_eq!(wrap_unbounded.to_max_px(Px(200)), Px(200));
249    ///
250    /// let wrap_bounded = DimensionValue::Wrap { min: None, max: Some(Px(150)) };
251    /// assert_eq!(wrap_bounded.to_max_px(Px(200)), Px(150));
252    /// ```
253    pub fn to_max_px(&self, default: Px) -> Px {
254        match self {
255            DimensionValue::Fixed(value) => *value,
256            DimensionValue::Wrap { max, .. } => max.unwrap_or(default),
257            DimensionValue::Fill { max, .. } => max.unwrap_or(default),
258        }
259    }
260
261    /// Returns the maximum value of this dimension, if defined.
262    ///
263    /// This method extracts the maximum constraint from a dimension value,
264    /// which is useful for layout calculations and constraint validation.
265    ///
266    /// # Returns
267    /// - For `Fixed`: Returns `Some(fixed_value)` since fixed dimensions have an implicit maximum
268    /// - For `Wrap` and `Fill`: Returns the `max` value if specified, otherwise `None`
269    ///
270    /// # Example
271    /// ```
272    /// # use tessera_ui::Px;
273    /// # use tessera_ui::DimensionValue;
274    /// let fixed = DimensionValue::Fixed(Px(100));
275    /// assert_eq!(fixed.get_max(), Some(Px(100)));
276    ///
277    /// let wrap_bounded = DimensionValue::Wrap { min: Some(Px(50)), max: Some(Px(200)) };
278    /// assert_eq!(wrap_bounded.get_max(), Some(Px(200)));
279    ///
280    /// let wrap_unbounded = DimensionValue::Wrap { min: None, max: None };
281    /// assert_eq!(wrap_unbounded.get_max(), None);
282    /// ```
283    pub fn get_max(&self) -> Option<Px> {
284        match self {
285            DimensionValue::Fixed(value) => Some(*value),
286            DimensionValue::Wrap { max, .. } => *max,
287            DimensionValue::Fill { max, .. } => *max,
288        }
289    }
290
291    /// Returns the minimum value of this dimension, if defined.
292    ///
293    /// This method extracts the minimum constraint from a dimension value,
294    /// which is useful for layout calculations and ensuring components
295    /// maintain their minimum required size.
296    ///
297    /// # Returns
298    /// - For `Fixed`: Returns `Some(fixed_value)` since fixed dimensions have an implicit minimum
299    /// - For `Wrap` and `Fill`: Returns the `min` value if specified, otherwise `None`
300    ///
301    /// # Example
302    /// ```
303    /// # use tessera_ui::Px;
304    /// # use tessera_ui::DimensionValue;
305    /// let fixed = DimensionValue::Fixed(Px(100));
306    /// assert_eq!(fixed.get_min(), Some(Px(100)));
307    ///
308    /// let fill_bounded = DimensionValue::Fill { min: Some(Px(50)), max: Some(Px(200)) };
309    /// assert_eq!(fill_bounded.get_min(), Some(Px(50)));
310    ///
311    /// let fill_unbounded = DimensionValue::Fill { min: None, max: None };
312    /// assert_eq!(fill_unbounded.get_min(), None);
313    /// ```
314    pub fn get_min(&self) -> Option<Px> {
315        match self {
316            DimensionValue::Fixed(value) => Some(*value),
317            DimensionValue::Wrap { min, .. } => *min,
318            DimensionValue::Fill { min, .. } => *min,
319        }
320    }
321}
322
323impl From<Px> for DimensionValue {
324    /// Converts a `Px` value to a `DimensionValue::Fixed`.
325    fn from(value: Px) -> Self {
326        DimensionValue::Fixed(value)
327    }
328}
329
330impl From<Dp> for DimensionValue {
331    /// Converts a `Dp` value to a `DimensionValue::Fixed`.
332    fn from(value: Dp) -> Self {
333        DimensionValue::Fixed(value.into())
334    }
335}
336
337impl Sub<Px> for DimensionValue {
338    type Output = DimensionValue;
339
340    fn sub(self, rhs: Px) -> Self::Output {
341        match self {
342            DimensionValue::Fixed(px) => DimensionValue::Fixed(px - rhs),
343            DimensionValue::Wrap { min, max } => DimensionValue::Wrap {
344                min,
345                max: max.map(|m| m - rhs),
346            },
347            DimensionValue::Fill { min, max } => DimensionValue::Fill {
348                min,
349                max: max.map(|m| m - rhs),
350            },
351        }
352    }
353}
354
355impl std::ops::Add<Px> for DimensionValue {
356    type Output = DimensionValue;
357
358    fn add(self, rhs: Px) -> Self::Output {
359        match self {
360            DimensionValue::Fixed(px) => DimensionValue::Fixed(px + rhs),
361            DimensionValue::Wrap { min, max } => DimensionValue::Wrap {
362                min,
363                max: max.map(|m| m + rhs),
364            },
365            DimensionValue::Fill { min, max } => DimensionValue::Fill {
366                min,
367                max: max.map(|m| m + rhs),
368            },
369        }
370    }
371}
372
373impl std::ops::AddAssign<Px> for DimensionValue {
374    fn add_assign(&mut self, rhs: Px) {
375        match self {
376            DimensionValue::Fixed(px) => *px = *px + rhs,
377            DimensionValue::Wrap { max, .. } => {
378                if let Some(m) = max {
379                    *m = *m + rhs;
380                }
381            }
382            DimensionValue::Fill { max, .. } => {
383                if let Some(m) = max {
384                    *m = *m + rhs;
385                }
386            }
387        }
388    }
389}
390
391impl std::ops::SubAssign<Px> for DimensionValue {
392    fn sub_assign(&mut self, rhs: Px) {
393        match self {
394            DimensionValue::Fixed(px) => *px = *px - rhs,
395            DimensionValue::Wrap { max, .. } => {
396                if let Some(m) = max {
397                    *m = *m - rhs;
398                }
399            }
400            DimensionValue::Fill { max, .. } => {
401                if let Some(m) = max {
402                    *m = *m - rhs;
403                }
404            }
405        }
406    }
407}
408
409/// Represents layout constraints for a component node.
410///
411/// A `Constraint` combines width and height dimension values to provide
412/// complete layout specification for a component. It defines how a component
413/// should size itself in both dimensions and provides methods for merging
414/// constraints in a component hierarchy.
415///
416/// # Examples
417///
418/// ```
419/// # use tessera_ui::Px;
420/// # use tessera_ui::{Constraint, DimensionValue};
421/// // A button with fixed size
422/// let button_constraint = Constraint::new(
423///     DimensionValue::Fixed(Px(120)),
424///     DimensionValue::Fixed(Px(40))
425/// );
426///
427/// // A flexible container that fills width but wraps height
428/// let container_constraint = Constraint::new(
429///     DimensionValue::Fill { min: Some(Px(200)), max: None },
430///     DimensionValue::Wrap { min: None, max: None }
431/// );
432///
433/// // A text component with bounded wrapping
434/// let text_constraint = Constraint::new(
435///     DimensionValue::Wrap { min: Some(Px(100)), max: Some(Px(400)) },
436///     DimensionValue::Wrap { min: None, max: None }
437/// );
438/// ```
439#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
440pub struct Constraint {
441    /// The width dimension constraint
442    pub width: DimensionValue,
443    /// The height dimension constraint
444    pub height: DimensionValue,
445}
446
447impl Constraint {
448    /// A constraint that specifies no preference (Wrap { None, None } for both width and height).
449    ///
450    /// This constant represents the most flexible constraint possible, where a component
451    /// will size itself to its content without any bounds. It's equivalent to the default
452    /// constraint and is useful as a starting point for constraint calculations.
453    ///
454    /// # Example
455    /// ```
456    /// # use tessera_ui::{Constraint, DimensionValue};
457    /// let flexible = Constraint::NONE;
458    /// assert_eq!(flexible.width, DimensionValue::Wrap { min: None, max: None });
459    /// assert_eq!(flexible.height, DimensionValue::Wrap { min: None, max: None });
460    /// ```
461    pub const NONE: Self = Self {
462        width: DimensionValue::Wrap {
463            min: None,
464            max: None,
465        },
466        height: DimensionValue::Wrap {
467            min: None,
468            max: None,
469        },
470    };
471
472    /// Creates a new constraint with the specified width and height dimensions.
473    ///
474    /// This is the primary constructor for creating constraint instances.
475    ///
476    /// # Parameters
477    /// - `width`: The dimension value for the width constraint
478    /// - `height`: The dimension value for the height constraint
479    ///
480    /// # Example
481    /// ```
482    /// # use tessera_ui::Px;
483    /// # use tessera_ui::{Constraint, DimensionValue};
484    /// let constraint = Constraint::new(
485    ///     DimensionValue::Fixed(Px(100)),
486    ///     DimensionValue::Fill { min: Some(Px(50)), max: None }
487    /// );
488    /// ```
489    pub fn new(width: DimensionValue, height: DimensionValue) -> Self {
490        Self { width, height }
491    }
492
493    /// Merges this constraint with a parent constraint to resolve layout conflicts.
494    ///
495    /// This method implements the core constraint resolution algorithm used throughout
496    /// Tessera's layout system. When components are nested, their constraints must be
497    /// merged to ensure consistent and predictable layout behavior.
498    ///
499    /// # Merge Rules
500    ///
501    /// The merging follows a priority system designed to respect component intentions
502    /// while ensuring layout consistency:
503    ///
504    /// ## Fixed Constraints (Highest Priority)
505    /// - **Fixed always wins**: A fixed constraint cannot be overridden by its parent
506    /// - Fixed dimensions maintain their exact size regardless of available space
507    ///
508    /// ## Wrap Constraints (Content-Based)
509    /// - **Preserves content sizing**: Wrap constraints maintain their intrinsic sizing behavior
510    /// - When parent is Fixed: Child wraps within parent's fixed bounds
511    /// - When parent is Wrap: Child combines min/max constraints with parent
512    /// - When parent is Fill: Child wraps within parent's fill bounds
513    ///
514    /// ## Fill Constraints (Space-Filling)
515    /// - **Adapts to available space**: Fill constraints expand within parent bounds
516    /// - When parent is Fixed: Child fills parent's fixed space (respecting own min/max)
517    /// - When parent is Wrap: Child fills available space within parent's wrap bounds
518    /// - When parent is Fill: Child combines fill constraints with parent
519    ///
520    /// # Parameters
521    /// - `parent_constraint`: The constraint from the parent component
522    ///
523    /// # Returns
524    /// A new constraint that represents the resolved layout requirements
525    ///
526    /// # Examples
527    ///
528    /// ```
529    /// # use tessera_ui::Px;
530    /// # use tessera_ui::{Constraint, DimensionValue};
531    /// // Fixed child in fixed parent - child wins
532    /// let parent = Constraint::new(
533    ///     DimensionValue::Fixed(Px(200)),
534    ///     DimensionValue::Fixed(Px(200))
535    /// );
536    /// let child = Constraint::new(
537    ///     DimensionValue::Fixed(Px(100)),
538    ///     DimensionValue::Fixed(Px(100))
539    /// );
540    /// let merged = child.merge(&parent);
541    /// assert_eq!(merged.width, DimensionValue::Fixed(Px(100)));
542    ///
543    /// // Fill child in fixed parent - child fills parent's space
544    /// let child_fill = Constraint::new(
545    ///     DimensionValue::Fill { min: Some(Px(50)), max: None },
546    ///     DimensionValue::Fill { min: Some(Px(50)), max: None }
547    /// );
548    /// let merged_fill = child_fill.merge(&parent);
549    /// assert_eq!(merged_fill.width, DimensionValue::Fill {
550    ///     min: Some(Px(50)),
551    ///     max: Some(Px(200))
552    /// });
553    /// ```
554    pub fn merge(&self, parent_constraint: &Constraint) -> Self {
555        let new_width = Self::merge_dimension(self.width, parent_constraint.width);
556        let new_height = Self::merge_dimension(self.height, parent_constraint.height);
557        Constraint::new(new_width, new_height)
558    }
559
560    /// Internal helper method that merges two dimension values according to the constraint rules.
561    ///
562    /// This method implements the detailed logic for merging individual dimension constraints.
563    /// It's called by the public `merge` method to handle width and height dimensions separately.
564    ///
565    /// # Parameters
566    /// - `child_dim`: The dimension constraint from the child component
567    /// - `parent_dim`: The dimension constraint from the parent component
568    ///
569    /// # Returns
570    /// The merged dimension value that respects both constraints appropriately
571    fn merge_dimension(child_dim: DimensionValue, parent_dim: DimensionValue) -> DimensionValue {
572        match child_dim {
573            DimensionValue::Fixed(cv) => DimensionValue::Fixed(cv), // Child's Fixed overrides
574            DimensionValue::Wrap {
575                min: c_min,
576                max: c_max,
577            } => match parent_dim {
578                DimensionValue::Fixed(pv) => DimensionValue::Wrap {
579                    // Wrap stays as Wrap, but constrained by parent's fixed size
580                    min: c_min, // Keep child's own min
581                    max: match c_max {
582                        Some(c) => Some(c.min(pv)), // Child's max capped by parent's fixed size
583                        None => Some(pv),           // Parent's fixed size becomes the max
584                    },
585                },
586                DimensionValue::Wrap {
587                    min: _p_min,
588                    max: p_max,
589                } => DimensionValue::Wrap {
590                    // Combine min/max from parent and child for Wrap
591                    min: c_min, // Wrap always keeps its own min, never inherits from parent
592                    max: match (c_max, p_max) {
593                        (Some(c), Some(p)) => Some(c.min(p)), // Take the more restrictive max
594                        (Some(c), None) => Some(c),
595                        (None, Some(p)) => Some(p),
596                        (None, None) => None,
597                    },
598                },
599                DimensionValue::Fill {
600                    min: _p_fill_min,
601                    max: p_fill_max,
602                } => DimensionValue::Wrap {
603                    // Child wants to wrap, so it stays as Wrap
604                    min: c_min, // Keep child's own min, don't inherit from parent's Fill
605                    max: match (c_max, p_fill_max) {
606                        (Some(c), Some(p)) => Some(c.min(p)), // Child's max should cap parent's fill max
607                        (Some(c), None) => Some(c),
608                        (None, Some(p)) => Some(p),
609                        (None, None) => None,
610                    },
611                },
612            },
613            DimensionValue::Fill {
614                min: c_fill_min,
615                max: c_fill_max,
616            } => match parent_dim {
617                DimensionValue::Fixed(pv) => {
618                    // Child wants to fill, parent is fixed. Result is Fill with parent's fixed size as max.
619                    DimensionValue::Fill {
620                        min: c_fill_min, // Keep child's own min
621                        max: match c_fill_max {
622                            Some(c) => Some(c.min(pv)), // Child's max capped by parent's fixed size
623                            None => Some(pv),           // Parent's fixed size becomes the max
624                        },
625                    }
626                }
627                DimensionValue::Wrap {
628                    min: p_wrap_min,
629                    max: p_wrap_max,
630                } => DimensionValue::Fill {
631                    // Fill remains Fill, parent Wrap offers no concrete size unless it has max
632                    min: c_fill_min.or(p_wrap_min), // Child's fill min, or parent's wrap min
633                    max: match (c_fill_max, p_wrap_max) {
634                        // Child's fill max, potentially capped by parent's wrap max
635                        (Some(cf), Some(pw)) => Some(cf.min(pw)),
636                        (Some(cf), None) => Some(cf),
637                        (None, Some(pw)) => Some(pw),
638                        (None, None) => None,
639                    },
640                },
641                DimensionValue::Fill {
642                    min: p_fill_min,
643                    max: p_fill_max,
644                } => {
645                    // Both are Fill. Combine min and max.
646                    // New min is the greater of the two mins (or the existing one).
647                    // New max is the smaller of the two maxes (or the existing one).
648                    let new_min = match (c_fill_min, p_fill_min) {
649                        (Some(cm), Some(pm)) => Some(cm.max(pm)),
650                        (Some(cm), None) => Some(cm),
651                        (None, Some(pm)) => Some(pm),
652                        (None, None) => None,
653                    };
654                    let new_max = match (c_fill_max, p_fill_max) {
655                        (Some(cm), Some(pm)) => Some(cm.min(pm)),
656                        (Some(cm), None) => Some(cm),
657                        (None, Some(pm)) => Some(pm),
658                        (None, None) => None,
659                    };
660                    // Ensure min <= max if both are Some
661                    let (final_min, final_max) = match (new_min, new_max) {
662                        (Some(n_min), Some(n_max)) if n_min > n_max => (Some(n_max), Some(n_max)), // Or handle error/warning
663                        _ => (new_min, new_max),
664                    };
665                    DimensionValue::Fill {
666                        min: final_min,
667                        max: final_max,
668                    }
669                }
670            },
671        }
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_fixed_parent_wrap_child_wrap_grandchild() {
681        // Test three-level hierarchy: Fixed(100) -> Wrap{20-80} -> Wrap{10-50}
682        // This tests constraint propagation through multiple levels
683
684        // Parent component with fixed 100x100 size
685        let parent = Constraint::new(
686            DimensionValue::Fixed(Px(100)),
687            DimensionValue::Fixed(Px(100)),
688        );
689
690        // Child component that wraps content with bounds 20-80
691        let child = Constraint::new(
692            DimensionValue::Wrap {
693                min: Some(Px(20)),
694                max: Some(Px(80)),
695            },
696            DimensionValue::Wrap {
697                min: Some(Px(20)),
698                max: Some(Px(80)),
699            },
700        );
701
702        // Grandchild component that wraps content with bounds 10-50
703        let grandchild = Constraint::new(
704            DimensionValue::Wrap {
705                min: Some(Px(10)),
706                max: Some(Px(50)),
707            },
708            DimensionValue::Wrap {
709                min: Some(Px(10)),
710                max: Some(Px(50)),
711            },
712        );
713
714        // First level merge: child merges with fixed parent
715        let merged_child = child.merge(&parent);
716
717        // Child is Wrap, parent is Fixed - result should be Wrap with child's constraints
718        // Since child's max (80) is less than parent's fixed size (100), child keeps its bounds
719        assert_eq!(
720            merged_child.width,
721            DimensionValue::Wrap {
722                min: Some(Px(20)),
723                max: Some(Px(80))
724            }
725        );
726        assert_eq!(
727            merged_child.height,
728            DimensionValue::Wrap {
729                min: Some(Px(20)),
730                max: Some(Px(80))
731            }
732        );
733
734        // Second level merge: grandchild merges with merged child
735        let final_result = grandchild.merge(&merged_child);
736
737        // Both are Wrap - result should be Wrap with the more restrictive constraints
738        // Grandchild's max (50) is smaller than merged child's max (80), so grandchild wins
739        assert_eq!(
740            final_result.width,
741            DimensionValue::Wrap {
742                min: Some(Px(10)),
743                max: Some(Px(50))
744            }
745        );
746        assert_eq!(
747            final_result.height,
748            DimensionValue::Wrap {
749                min: Some(Px(10)),
750                max: Some(Px(50))
751            }
752        );
753    }
754
755    #[test]
756    fn test_fill_parent_wrap_child() {
757        // Test Fill parent with Wrap child: Fill{50-200} -> Wrap{30-150}
758        // Child should remain Wrap and keep its own constraints
759
760        let parent = Constraint::new(
761            DimensionValue::Fill {
762                min: Some(Px(50)),
763                max: Some(Px(200)),
764            },
765            DimensionValue::Fill {
766                min: Some(Px(50)),
767                max: Some(Px(200)),
768            },
769        );
770
771        let child = Constraint::new(
772            DimensionValue::Wrap {
773                min: Some(Px(30)),
774                max: Some(Px(150)),
775            },
776            DimensionValue::Wrap {
777                min: Some(Px(30)),
778                max: Some(Px(150)),
779            },
780        );
781
782        let result = child.merge(&parent);
783
784        // Child is Wrap, parent is Fill - result should be Wrap
785        // Child keeps its own min (30px) and max (150px) since both are within parent's bounds
786        assert_eq!(
787            result.width,
788            DimensionValue::Wrap {
789                min: Some(Px(30)),
790                max: Some(Px(150))
791            }
792        );
793        assert_eq!(
794            result.height,
795            DimensionValue::Wrap {
796                min: Some(Px(30)),
797                max: Some(Px(150))
798            }
799        );
800    }
801
802    #[test]
803    fn test_fill_parent_wrap_child_no_child_min() {
804        // Test Fill parent with Wrap child that has no minimum: Fill{50-200} -> Wrap{None-150}
805        // Child should keep its own constraints and not inherit parent's minimum
806
807        let parent = Constraint::new(
808            DimensionValue::Fill {
809                min: Some(Px(50)),
810                max: Some(Px(200)),
811            },
812            DimensionValue::Fill {
813                min: Some(Px(50)),
814                max: Some(Px(200)),
815            },
816        );
817
818        let child = Constraint::new(
819            DimensionValue::Wrap {
820                min: None,
821                max: Some(Px(150)),
822            },
823            DimensionValue::Wrap {
824                min: None,
825                max: Some(Px(150)),
826            },
827        );
828
829        let result = child.merge(&parent);
830
831        // Child is Wrap and should keep its own min (None), not inherit from parent's Fill min
832        // This preserves the wrap behavior of sizing to content without artificial minimums
833        assert_eq!(
834            result.width,
835            DimensionValue::Wrap {
836                min: None,
837                max: Some(Px(150))
838            }
839        );
840        assert_eq!(
841            result.height,
842            DimensionValue::Wrap {
843                min: None,
844                max: Some(Px(150))
845            }
846        );
847    }
848
849    #[test]
850    fn test_fill_parent_wrap_child_no_parent_max() {
851        // Test Fill parent with no maximum and Wrap child: Fill{50-None} -> Wrap{30-150}
852        // Child should keep its own constraints since parent has no upper bound
853
854        let parent = Constraint::new(
855            DimensionValue::Fill {
856                min: Some(Px(50)),
857                max: None,
858            },
859            DimensionValue::Fill {
860                min: Some(Px(50)),
861                max: None,
862            },
863        );
864
865        let child = Constraint::new(
866            DimensionValue::Wrap {
867                min: Some(Px(30)),
868                max: Some(Px(150)),
869            },
870            DimensionValue::Wrap {
871                min: Some(Px(30)),
872                max: Some(Px(150)),
873            },
874        );
875
876        let result = child.merge(&parent);
877
878        // Child should keep its own constraints since parent Fill has no max to constrain it
879        assert_eq!(
880            result.width,
881            DimensionValue::Wrap {
882                min: Some(Px(30)),
883                max: Some(Px(150))
884            }
885        );
886        assert_eq!(
887            result.height,
888            DimensionValue::Wrap {
889                min: Some(Px(30)),
890                max: Some(Px(150))
891            }
892        );
893    }
894
895    #[test]
896    fn test_fixed_parent_wrap_child() {
897        // Test Fixed parent with Wrap child: Fixed(100) -> Wrap{30-120}
898        // Child's max should be capped by parent's fixed size
899
900        let parent = Constraint::new(
901            DimensionValue::Fixed(Px(100)),
902            DimensionValue::Fixed(Px(100)),
903        );
904
905        let child = Constraint::new(
906            DimensionValue::Wrap {
907                min: Some(Px(30)),
908                max: Some(Px(120)),
909            },
910            DimensionValue::Wrap {
911                min: Some(Px(30)),
912                max: Some(Px(120)),
913            },
914        );
915
916        let result = child.merge(&parent);
917
918        // Child remains Wrap but max is limited by parent's fixed size
919        // min keeps child's own value (30px)
920        // max becomes the smaller of child's max (120px) and parent's fixed size (100px)
921        assert_eq!(
922            result.width,
923            DimensionValue::Wrap {
924                min: Some(Px(30)),
925                max: Some(Px(100))
926            }
927        );
928        assert_eq!(
929            result.height,
930            DimensionValue::Wrap {
931                min: Some(Px(30)),
932                max: Some(Px(100))
933            }
934        );
935    }
936
937    #[test]
938    fn test_fixed_parent_wrap_child_no_child_max() {
939        // Test Fixed parent with Wrap child that has no maximum: Fixed(100) -> Wrap{30-None}
940        // Parent's fixed size should become the child's maximum
941
942        let parent = Constraint::new(
943            DimensionValue::Fixed(Px(100)),
944            DimensionValue::Fixed(Px(100)),
945        );
946
947        let child = Constraint::new(
948            DimensionValue::Wrap {
949                min: Some(Px(30)),
950                max: None,
951            },
952            DimensionValue::Wrap {
953                min: Some(Px(30)),
954                max: None,
955            },
956        );
957
958        let result = child.merge(&parent);
959
960        // Child remains Wrap, parent's fixed size becomes the maximum constraint
961        // This prevents the child from growing beyond the parent's available space
962        assert_eq!(
963            result.width,
964            DimensionValue::Wrap {
965                min: Some(Px(30)),
966                max: Some(Px(100))
967            }
968        );
969        assert_eq!(
970            result.height,
971            DimensionValue::Wrap {
972                min: Some(Px(30)),
973                max: Some(Px(100))
974            }
975        );
976    }
977
978    #[test]
979    fn test_fixed_parent_fill_child() {
980        // Test Fixed parent with Fill child: Fixed(100) -> Fill{30-120}
981        // Child should fill parent's space but be capped by parent's fixed size
982
983        let parent = Constraint::new(
984            DimensionValue::Fixed(Px(100)),
985            DimensionValue::Fixed(Px(100)),
986        );
987
988        let child = Constraint::new(
989            DimensionValue::Fill {
990                min: Some(Px(30)),
991                max: Some(Px(120)),
992            },
993            DimensionValue::Fill {
994                min: Some(Px(30)),
995                max: Some(Px(120)),
996            },
997        );
998
999        let result = child.merge(&parent);
1000
1001        // Child remains Fill but max is limited by parent's fixed size
1002        // min keeps child's own value (30px)
1003        // max becomes the smaller of child's max (120px) and parent's fixed size (100px)
1004        assert_eq!(
1005            result.width,
1006            DimensionValue::Fill {
1007                min: Some(Px(30)),
1008                max: Some(Px(100))
1009            }
1010        );
1011        assert_eq!(
1012            result.height,
1013            DimensionValue::Fill {
1014                min: Some(Px(30)),
1015                max: Some(Px(100))
1016            }
1017        );
1018    }
1019
1020    #[test]
1021    fn test_fixed_parent_fill_child_no_child_max() {
1022        // Test Fixed parent with Fill child that has no maximum: Fixed(100) -> Fill{30-None}
1023        // Parent's fixed size should become the child's maximum
1024
1025        let parent = Constraint::new(
1026            DimensionValue::Fixed(Px(100)),
1027            DimensionValue::Fixed(Px(100)),
1028        );
1029
1030        let child = Constraint::new(
1031            DimensionValue::Fill {
1032                min: Some(Px(30)),
1033                max: None,
1034            },
1035            DimensionValue::Fill {
1036                min: Some(Px(30)),
1037                max: None,
1038            },
1039        );
1040
1041        let result = child.merge(&parent);
1042
1043        // Child remains Fill, parent's fixed size becomes the maximum constraint
1044        // This ensures the child fills exactly the parent's available space
1045        assert_eq!(
1046            result.width,
1047            DimensionValue::Fill {
1048                min: Some(Px(30)),
1049                max: Some(Px(100))
1050            }
1051        );
1052        assert_eq!(
1053            result.height,
1054            DimensionValue::Fill {
1055                min: Some(Px(30)),
1056                max: Some(Px(100))
1057            }
1058        );
1059    }
1060
1061    #[test]
1062    fn test_fixed_parent_fill_child_no_child_min() {
1063        // Test Fixed parent with Fill child that has no minimum: Fixed(100) -> Fill{None-120}
1064        // Child should fill parent's space with no minimum constraint
1065
1066        let parent = Constraint::new(
1067            DimensionValue::Fixed(Px(100)),
1068            DimensionValue::Fixed(Px(100)),
1069        );
1070
1071        let child = Constraint::new(
1072            DimensionValue::Fill {
1073                min: None,
1074                max: Some(Px(120)),
1075            },
1076            DimensionValue::Fill {
1077                min: None,
1078                max: Some(Px(120)),
1079            },
1080        );
1081
1082        let result = child.merge(&parent);
1083
1084        // Child remains Fill, keeps its own min (None), max is limited by parent's fixed size
1085        // This allows the child to fill the parent's space without any minimum size requirement
1086        assert_eq!(
1087            result.width,
1088            DimensionValue::Fill {
1089                min: None,
1090                max: Some(Px(100))
1091            }
1092        );
1093        assert_eq!(
1094            result.height,
1095            DimensionValue::Fill {
1096                min: None,
1097                max: Some(Px(100))
1098            }
1099        );
1100    }
1101}