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