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}