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