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