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 DimensionValue::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 = DimensionValue::Fixed(Px(0));
213
214 /// Fill with no constraints.
215 pub const FILLED: Self = DimensionValue::Fill {
216 min: None,
217 max: None,
218 };
219
220 /// Wrap with no constraints.
221 pub const WRAP: Self = DimensionValue::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 fn get_max(&self) -> Option<Px> {
249 match self {
250 DimensionValue::Fixed(value) => Some(*value),
251 DimensionValue::Wrap { max, .. } => *max,
252 DimensionValue::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 /// # 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 DimensionValue::Fixed(value) => Some(*value),
282 DimensionValue::Wrap { min, .. } => *min,
283 DimensionValue::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 DimensionValue::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 DimensionValue::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 DimensionValue::Fixed(px) => DimensionValue::Fixed(px - rhs),
308 DimensionValue::Wrap { min, max } => DimensionValue::Wrap {
309 min,
310 max: max.map(|m| m - rhs),
311 },
312 DimensionValue::Fill { min, max } => DimensionValue::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 DimensionValue::Fixed(px) => DimensionValue::Fixed(px + rhs),
326 DimensionValue::Wrap { min, max } => DimensionValue::Wrap {
327 min,
328 max: max.map(|m| m + rhs),
329 },
330 DimensionValue::Fill { min, max } => DimensionValue::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 DimensionValue::Fixed(px) => *px = *px + rhs,
342 DimensionValue::Wrap { max, .. } => {
343 if let Some(m) = max {
344 *m = *m + rhs;
345 }
346 }
347 DimensionValue::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 DimensionValue::Fixed(px) => *px = *px - rhs,
360 DimensionValue::Wrap { max, .. } => {
361 if let Some(m) = max {
362 *m = *m - rhs;
363 }
364 }
365 DimensionValue::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 /// # use tessera_ui::{Constraint, DimensionValue};
422 /// let flexible = Constraint::NONE;
423 /// assert_eq!(flexible.width, DimensionValue::Wrap { min: None, max: None });
424 /// assert_eq!(flexible.height, DimensionValue::Wrap { min: None, max: None });
425 /// ```
426 pub const NONE: Self = Self {
427 width: DimensionValue::Wrap {
428 min: None,
429 max: None,
430 },
431 height: DimensionValue::Wrap {
432 min: None,
433 max: None,
434 },
435 };
436
437 /// Creates a new constraint with the specified width and height dimensions.
438 ///
439 /// This is the primary constructor for creating constraint instances.
440 ///
441 /// # Parameters
442 /// - `width`: The dimension value for the width constraint
443 /// - `height`: The dimension value for the height constraint
444 ///
445 /// # Example
446 /// ```
447 /// # use tessera_ui::Px;
448 /// # use tessera_ui::{Constraint, DimensionValue};
449 /// let constraint = Constraint::new(
450 /// DimensionValue::Fixed(Px(100)),
451 /// DimensionValue::Fill { min: Some(Px(50)), max: None }
452 /// );
453 /// ```
454 pub fn new(width: DimensionValue, height: DimensionValue) -> Self {
455 Self { width, height }
456 }
457
458 /// Merges this constraint with a parent constraint to resolve layout conflicts.
459 ///
460 /// This method implements the core constraint resolution algorithm used throughout
461 /// Tessera's layout system. When components are nested, their constraints must be
462 /// merged to ensure consistent and predictable layout behavior.
463 ///
464 /// # Merge Rules
465 ///
466 /// The merging follows a priority system designed to respect component intentions
467 /// while ensuring layout consistency:
468 ///
469 /// ## Fixed Constraints (Highest Priority)
470 /// - **Fixed always wins**: A fixed constraint cannot be overridden by its parent
471 /// - Fixed dimensions maintain their exact size regardless of available space
472 ///
473 /// ## Wrap Constraints (Content-Based)
474 /// - **Preserves content sizing**: Wrap constraints maintain their intrinsic sizing behavior
475 /// - When parent is Fixed: Child wraps within parent's fixed bounds
476 /// - When parent is Wrap: Child combines min/max constraints with parent
477 /// - When parent is Fill: Child wraps within parent's fill bounds
478 ///
479 /// ## Fill Constraints (Space-Filling)
480 /// - **Adapts to available space**: Fill constraints expand within parent bounds
481 /// - When parent is Fixed: Child fills parent's fixed space (respecting own min/max)
482 /// - When parent is Wrap: Child fills available space within parent's wrap bounds
483 /// - When parent is Fill: Child combines fill constraints with parent
484 ///
485 /// # Parameters
486 /// - `parent_constraint`: The constraint from the parent component
487 ///
488 /// # Returns
489 /// A new constraint that represents the resolved layout requirements
490 ///
491 /// # Examples
492 ///
493 /// ```
494 /// # use tessera_ui::Px;
495 /// # use tessera_ui::{Constraint, DimensionValue};
496 /// // Fixed child in fixed parent - child wins
497 /// let parent = Constraint::new(
498 /// DimensionValue::Fixed(Px(200)),
499 /// DimensionValue::Fixed(Px(200))
500 /// );
501 /// let child = Constraint::new(
502 /// DimensionValue::Fixed(Px(100)),
503 /// DimensionValue::Fixed(Px(100))
504 /// );
505 /// let merged = child.merge(&parent);
506 /// assert_eq!(merged.width, DimensionValue::Fixed(Px(100)));
507 ///
508 /// // Fill child in fixed parent - child fills parent's space
509 /// let child_fill = Constraint::new(
510 /// DimensionValue::Fill { min: Some(Px(50)), max: None },
511 /// DimensionValue::Fill { min: Some(Px(50)), max: None }
512 /// );
513 /// let merged_fill = child_fill.merge(&parent);
514 /// assert_eq!(merged_fill.width, DimensionValue::Fill {
515 /// min: Some(Px(50)),
516 /// max: Some(Px(200))
517 /// });
518 /// ```
519 pub fn merge(&self, parent_constraint: &Constraint) -> Self {
520 let new_width = Self::merge_dimension(self.width, parent_constraint.width);
521 let new_height = Self::merge_dimension(self.height, parent_constraint.height);
522 Constraint::new(new_width, new_height)
523 }
524
525 /// Internal helper method that merges two dimension values according to the constraint rules.
526 ///
527 /// This method implements the detailed logic for merging individual dimension constraints.
528 /// It's called by the public `merge` method to handle width and height dimensions separately.
529 ///
530 /// # Parameters
531 /// - `child_dim`: The dimension constraint from the child component
532 /// - `parent_dim`: The dimension constraint from the parent component
533 ///
534 /// # Returns
535 /// The merged dimension value that respects both constraints appropriately
536 fn merge_dimension(child_dim: DimensionValue, parent_dim: DimensionValue) -> DimensionValue {
537 match child_dim {
538 DimensionValue::Fixed(cv) => DimensionValue::Fixed(cv), // Child's Fixed overrides
539 DimensionValue::Wrap {
540 min: c_min,
541 max: c_max,
542 } => match parent_dim {
543 DimensionValue::Fixed(pv) => DimensionValue::Wrap {
544 // Wrap stays as Wrap, but constrained by parent's fixed size
545 min: c_min, // Keep child's own min
546 max: match c_max {
547 Some(c) => Some(c.min(pv)), // Child's max capped by parent's fixed size
548 None => Some(pv), // Parent's fixed size becomes the max
549 },
550 },
551 DimensionValue::Wrap {
552 min: _p_min,
553 max: p_max,
554 } => DimensionValue::Wrap {
555 // Combine min/max from parent and child for Wrap
556 min: c_min, // Wrap always keeps its own min, never inherits from parent
557 max: match (c_max, p_max) {
558 (Some(c), Some(p)) => Some(c.min(p)), // Take the more restrictive max
559 (Some(c), None) => Some(c),
560 (None, Some(p)) => Some(p),
561 (None, None) => None,
562 },
563 },
564 DimensionValue::Fill {
565 min: _p_fill_min,
566 max: p_fill_max,
567 } => DimensionValue::Wrap {
568 // Child wants to wrap, so it stays as Wrap
569 min: c_min, // Keep child's own min, don't inherit from parent's Fill
570 max: match (c_max, p_fill_max) {
571 (Some(c), Some(p)) => Some(c.min(p)), // Child's max should cap parent's fill max
572 (Some(c), None) => Some(c),
573 (None, Some(p)) => Some(p),
574 (None, None) => None,
575 },
576 },
577 },
578 DimensionValue::Fill {
579 min: c_fill_min,
580 max: c_fill_max,
581 } => match parent_dim {
582 DimensionValue::Fixed(pv) => {
583 // Child wants to fill, parent is fixed. Result is Fill with parent's fixed size as max.
584 DimensionValue::Fill {
585 min: c_fill_min, // Keep child's own min
586 max: match c_fill_max {
587 Some(c) => Some(c.min(pv)), // Child's max capped by parent's fixed size
588 None => Some(pv), // Parent's fixed size becomes the max
589 },
590 }
591 }
592 DimensionValue::Wrap {
593 min: p_wrap_min,
594 max: p_wrap_max,
595 } => DimensionValue::Fill {
596 // Fill remains Fill, parent Wrap offers no concrete size unless it has max
597 min: c_fill_min.or(p_wrap_min), // Child's fill min, or parent's wrap min
598 max: match (c_fill_max, p_wrap_max) {
599 // Child's fill max, potentially capped by parent's wrap max
600 (Some(cf), Some(pw)) => Some(cf.min(pw)),
601 (Some(cf), None) => Some(cf),
602 (None, Some(pw)) => Some(pw),
603 (None, None) => None,
604 },
605 },
606 DimensionValue::Fill {
607 min: p_fill_min,
608 max: p_fill_max,
609 } => {
610 // Both are Fill. Combine min and max.
611 // New min is the greater of the two mins (or the existing one).
612 // New max is the smaller of the two maxes (or the existing one).
613 let new_min = match (c_fill_min, p_fill_min) {
614 (Some(cm), Some(pm)) => Some(cm.max(pm)),
615 (Some(cm), None) => Some(cm),
616 (None, Some(pm)) => Some(pm),
617 (None, None) => None,
618 };
619 let new_max = match (c_fill_max, p_fill_max) {
620 (Some(cm), Some(pm)) => Some(cm.min(pm)),
621 (Some(cm), None) => Some(cm),
622 (None, Some(pm)) => Some(pm),
623 (None, None) => None,
624 };
625 // Ensure min <= max if both are Some
626 let (final_min, final_max) = match (new_min, new_max) {
627 (Some(n_min), Some(n_max)) if n_min > n_max => (Some(n_max), Some(n_max)), // Or handle error/warning
628 _ => (new_min, new_max),
629 };
630 DimensionValue::Fill {
631 min: final_min,
632 max: final_max,
633 }
634 }
635 },
636 }
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643
644 #[test]
645 fn test_fixed_parent_wrap_child_wrap_grandchild() {
646 // Test three-level hierarchy: Fixed(100) -> Wrap{20-80} -> Wrap{10-50}
647 // This tests constraint propagation through multiple levels
648
649 // Parent component with fixed 100x100 size
650 let parent = Constraint::new(
651 DimensionValue::Fixed(Px(100)),
652 DimensionValue::Fixed(Px(100)),
653 );
654
655 // Child component that wraps content with bounds 20-80
656 let child = Constraint::new(
657 DimensionValue::Wrap {
658 min: Some(Px(20)),
659 max: Some(Px(80)),
660 },
661 DimensionValue::Wrap {
662 min: Some(Px(20)),
663 max: Some(Px(80)),
664 },
665 );
666
667 // Grandchild component that wraps content with bounds 10-50
668 let grandchild = Constraint::new(
669 DimensionValue::Wrap {
670 min: Some(Px(10)),
671 max: Some(Px(50)),
672 },
673 DimensionValue::Wrap {
674 min: Some(Px(10)),
675 max: Some(Px(50)),
676 },
677 );
678
679 // First level merge: child merges with fixed parent
680 let merged_child = child.merge(&parent);
681
682 // Child is Wrap, parent is Fixed - result should be Wrap with child's constraints
683 // Since child's max (80) is less than parent's fixed size (100), child keeps its bounds
684 assert_eq!(
685 merged_child.width,
686 DimensionValue::Wrap {
687 min: Some(Px(20)),
688 max: Some(Px(80))
689 }
690 );
691 assert_eq!(
692 merged_child.height,
693 DimensionValue::Wrap {
694 min: Some(Px(20)),
695 max: Some(Px(80))
696 }
697 );
698
699 // Second level merge: grandchild merges with merged child
700 let final_result = grandchild.merge(&merged_child);
701
702 // Both are Wrap - result should be Wrap with the more restrictive constraints
703 // Grandchild's max (50) is smaller than merged child's max (80), so grandchild wins
704 assert_eq!(
705 final_result.width,
706 DimensionValue::Wrap {
707 min: Some(Px(10)),
708 max: Some(Px(50))
709 }
710 );
711 assert_eq!(
712 final_result.height,
713 DimensionValue::Wrap {
714 min: Some(Px(10)),
715 max: Some(Px(50))
716 }
717 );
718 }
719
720 #[test]
721 fn test_fill_parent_wrap_child() {
722 // Test Fill parent with Wrap child: Fill{50-200} -> Wrap{30-150}
723 // Child should remain Wrap and keep its own constraints
724
725 let parent = Constraint::new(
726 DimensionValue::Fill {
727 min: Some(Px(50)),
728 max: Some(Px(200)),
729 },
730 DimensionValue::Fill {
731 min: Some(Px(50)),
732 max: Some(Px(200)),
733 },
734 );
735
736 let child = Constraint::new(
737 DimensionValue::Wrap {
738 min: Some(Px(30)),
739 max: Some(Px(150)),
740 },
741 DimensionValue::Wrap {
742 min: Some(Px(30)),
743 max: Some(Px(150)),
744 },
745 );
746
747 let result = child.merge(&parent);
748
749 // Child is Wrap, parent is Fill - result should be Wrap
750 // Child keeps its own min (30px) and max (150px) since both are within parent's bounds
751 assert_eq!(
752 result.width,
753 DimensionValue::Wrap {
754 min: Some(Px(30)),
755 max: Some(Px(150))
756 }
757 );
758 assert_eq!(
759 result.height,
760 DimensionValue::Wrap {
761 min: Some(Px(30)),
762 max: Some(Px(150))
763 }
764 );
765 }
766
767 #[test]
768 fn test_fill_parent_wrap_child_no_child_min() {
769 // Test Fill parent with Wrap child that has no minimum: Fill{50-200} -> Wrap{None-150}
770 // Child should keep its own constraints and not inherit parent's minimum
771
772 let parent = Constraint::new(
773 DimensionValue::Fill {
774 min: Some(Px(50)),
775 max: Some(Px(200)),
776 },
777 DimensionValue::Fill {
778 min: Some(Px(50)),
779 max: Some(Px(200)),
780 },
781 );
782
783 let child = Constraint::new(
784 DimensionValue::Wrap {
785 min: None,
786 max: Some(Px(150)),
787 },
788 DimensionValue::Wrap {
789 min: None,
790 max: Some(Px(150)),
791 },
792 );
793
794 let result = child.merge(&parent);
795
796 // Child is Wrap and should keep its own min (None), not inherit from parent's Fill min
797 // This preserves the wrap behavior of sizing to content without artificial minimums
798 assert_eq!(
799 result.width,
800 DimensionValue::Wrap {
801 min: None,
802 max: Some(Px(150))
803 }
804 );
805 assert_eq!(
806 result.height,
807 DimensionValue::Wrap {
808 min: None,
809 max: Some(Px(150))
810 }
811 );
812 }
813
814 #[test]
815 fn test_fill_parent_wrap_child_no_parent_max() {
816 // Test Fill parent with no maximum and Wrap child: Fill{50-None} -> Wrap{30-150}
817 // Child should keep its own constraints since parent has no upper bound
818
819 let parent = Constraint::new(
820 DimensionValue::Fill {
821 min: Some(Px(50)),
822 max: None,
823 },
824 DimensionValue::Fill {
825 min: Some(Px(50)),
826 max: None,
827 },
828 );
829
830 let child = Constraint::new(
831 DimensionValue::Wrap {
832 min: Some(Px(30)),
833 max: Some(Px(150)),
834 },
835 DimensionValue::Wrap {
836 min: Some(Px(30)),
837 max: Some(Px(150)),
838 },
839 );
840
841 let result = child.merge(&parent);
842
843 // Child should keep its own constraints since parent Fill has no max to constrain it
844 assert_eq!(
845 result.width,
846 DimensionValue::Wrap {
847 min: Some(Px(30)),
848 max: Some(Px(150))
849 }
850 );
851 assert_eq!(
852 result.height,
853 DimensionValue::Wrap {
854 min: Some(Px(30)),
855 max: Some(Px(150))
856 }
857 );
858 }
859
860 #[test]
861 fn test_fixed_parent_wrap_child() {
862 // Test Fixed parent with Wrap child: Fixed(100) -> Wrap{30-120}
863 // Child's max should be capped by parent's fixed size
864
865 let parent = Constraint::new(
866 DimensionValue::Fixed(Px(100)),
867 DimensionValue::Fixed(Px(100)),
868 );
869
870 let child = Constraint::new(
871 DimensionValue::Wrap {
872 min: Some(Px(30)),
873 max: Some(Px(120)),
874 },
875 DimensionValue::Wrap {
876 min: Some(Px(30)),
877 max: Some(Px(120)),
878 },
879 );
880
881 let result = child.merge(&parent);
882
883 // Child remains Wrap but max is limited by parent's fixed size
884 // min keeps child's own value (30px)
885 // max becomes the smaller of child's max (120px) and parent's fixed size (100px)
886 assert_eq!(
887 result.width,
888 DimensionValue::Wrap {
889 min: Some(Px(30)),
890 max: Some(Px(100))
891 }
892 );
893 assert_eq!(
894 result.height,
895 DimensionValue::Wrap {
896 min: Some(Px(30)),
897 max: Some(Px(100))
898 }
899 );
900 }
901
902 #[test]
903 fn test_fixed_parent_wrap_child_no_child_max() {
904 // Test Fixed parent with Wrap child that has no maximum: Fixed(100) -> Wrap{30-None}
905 // Parent's fixed size should become the child's maximum
906
907 let parent = Constraint::new(
908 DimensionValue::Fixed(Px(100)),
909 DimensionValue::Fixed(Px(100)),
910 );
911
912 let child = Constraint::new(
913 DimensionValue::Wrap {
914 min: Some(Px(30)),
915 max: None,
916 },
917 DimensionValue::Wrap {
918 min: Some(Px(30)),
919 max: None,
920 },
921 );
922
923 let result = child.merge(&parent);
924
925 // Child remains Wrap, parent's fixed size becomes the maximum constraint
926 // This prevents the child from growing beyond the parent's available space
927 assert_eq!(
928 result.width,
929 DimensionValue::Wrap {
930 min: Some(Px(30)),
931 max: Some(Px(100))
932 }
933 );
934 assert_eq!(
935 result.height,
936 DimensionValue::Wrap {
937 min: Some(Px(30)),
938 max: Some(Px(100))
939 }
940 );
941 }
942
943 #[test]
944 fn test_fixed_parent_fill_child() {
945 // Test Fixed parent with Fill child: Fixed(100) -> Fill{30-120}
946 // Child should fill parent's space but be capped by parent's fixed size
947
948 let parent = Constraint::new(
949 DimensionValue::Fixed(Px(100)),
950 DimensionValue::Fixed(Px(100)),
951 );
952
953 let child = Constraint::new(
954 DimensionValue::Fill {
955 min: Some(Px(30)),
956 max: Some(Px(120)),
957 },
958 DimensionValue::Fill {
959 min: Some(Px(30)),
960 max: Some(Px(120)),
961 },
962 );
963
964 let result = child.merge(&parent);
965
966 // Child remains Fill but max is limited by parent's fixed size
967 // min keeps child's own value (30px)
968 // max becomes the smaller of child's max (120px) and parent's fixed size (100px)
969 assert_eq!(
970 result.width,
971 DimensionValue::Fill {
972 min: Some(Px(30)),
973 max: Some(Px(100))
974 }
975 );
976 assert_eq!(
977 result.height,
978 DimensionValue::Fill {
979 min: Some(Px(30)),
980 max: Some(Px(100))
981 }
982 );
983 }
984
985 #[test]
986 fn test_fixed_parent_fill_child_no_child_max() {
987 // Test Fixed parent with Fill child that has no maximum: Fixed(100) -> Fill{30-None}
988 // Parent's fixed size should become the child's maximum
989
990 let parent = Constraint::new(
991 DimensionValue::Fixed(Px(100)),
992 DimensionValue::Fixed(Px(100)),
993 );
994
995 let child = Constraint::new(
996 DimensionValue::Fill {
997 min: Some(Px(30)),
998 max: None,
999 },
1000 DimensionValue::Fill {
1001 min: Some(Px(30)),
1002 max: None,
1003 },
1004 );
1005
1006 let result = child.merge(&parent);
1007
1008 // Child remains Fill, parent's fixed size becomes the maximum constraint
1009 // This ensures the child fills exactly the parent's available space
1010 assert_eq!(
1011 result.width,
1012 DimensionValue::Fill {
1013 min: Some(Px(30)),
1014 max: Some(Px(100))
1015 }
1016 );
1017 assert_eq!(
1018 result.height,
1019 DimensionValue::Fill {
1020 min: Some(Px(30)),
1021 max: Some(Px(100))
1022 }
1023 );
1024 }
1025
1026 #[test]
1027 fn test_fixed_parent_fill_child_no_child_min() {
1028 // Test Fixed parent with Fill child that has no minimum: Fixed(100) -> Fill{None-120}
1029 // Child should fill parent's space with no minimum constraint
1030
1031 let parent = Constraint::new(
1032 DimensionValue::Fixed(Px(100)),
1033 DimensionValue::Fixed(Px(100)),
1034 );
1035
1036 let child = Constraint::new(
1037 DimensionValue::Fill {
1038 min: None,
1039 max: Some(Px(120)),
1040 },
1041 DimensionValue::Fill {
1042 min: None,
1043 max: Some(Px(120)),
1044 },
1045 );
1046
1047 let result = child.merge(&parent);
1048
1049 // Child remains Fill, keeps its own min (None), max is limited by parent's fixed size
1050 // This allows the child to fill the parent's space without any minimum size requirement
1051 assert_eq!(
1052 result.width,
1053 DimensionValue::Fill {
1054 min: None,
1055 max: Some(Px(100))
1056 }
1057 );
1058 assert_eq!(
1059 result.height,
1060 DimensionValue::Fill {
1061 min: None,
1062 max: Some(Px(100))
1063 }
1064 );
1065 }
1066}