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