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