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