all_is_cubes_ui/vui/
layout.rs

1#![allow(
2    clippy::module_name_repetitions,
3    reason = "false positive; TODO: remove after Rust 1.84 is released"
4)]
5
6use alloc::boxed::Box;
7use alloc::rc::Rc;
8use alloc::sync::Arc;
9use alloc::vec;
10use alloc::vec::Vec;
11use core::fmt;
12
13use all_is_cubes::euclid::{self, size3, Size3D, Vector3D};
14use all_is_cubes::math::{Axis, Cube, Face6, FaceMap, GridAab, GridPoint, GridSize};
15use all_is_cubes::space::{self, Space, SpaceTransaction};
16use all_is_cubes::transaction::{self, Merge as _, Transaction as _};
17use all_is_cubes::util::{ConciseDebug, Fmt};
18
19use crate::vui::{InstallVuiError, Widget, WidgetBehavior};
20
21/// A tree of [`Widget`]s that can be put in a [`Space`] to create UI.
22///
23/// Create this via [`LayoutTree`]. Use it via [`install_widgets()`].
24// ---
25// TODO: can we come up with a way to not even need this type alias?
26// The Arcs are clunky to use.
27pub type WidgetTree = Arc<LayoutTree<Arc<dyn Widget>>>;
28
29/// Lay out a widget tree and produce the transaction to install it.
30///
31/// This is a combination of:
32///
33/// * [`LayoutTree::perform_layout()`] to choose locations
34/// * [`LayoutTree::installation()`] to convert the tree to a transaction
35///
36/// with error propagation from all operations and constraint of the input type.
37///
38/// See also [`LayoutTree::to_space()`] if you do not need to install widgets in an
39/// already-existing [`Space`].
40///
41/// TODO: This function needs a better name and location. Also, it would be nice if it could
42/// help with handling the potential error resulting from executing the transaction.
43pub fn install_widgets(
44    grant: LayoutGrant,
45    tree: &WidgetTree,
46) -> Result<SpaceTransaction, InstallVuiError> {
47    tree.perform_layout(grant).unwrap(/* currently infallible */).installation()
48}
49
50/// Requested size and relative positioning of a widget or other thing occupying space,
51/// to be interpreted by a layout algorithm to choose the real position.
52///
53/// TODO: give this type and [`Layoutable`] better names
54#[derive(Clone, Debug, Eq, PartialEq)]
55#[expect(clippy::exhaustive_structs)]
56pub struct LayoutRequest {
57    /// The minimum dimensions required, without which correct functionality
58    /// is not possible.
59    pub minimum: GridSize,
60}
61
62impl LayoutRequest {
63    /// A request for no space at all.
64    pub const EMPTY: Self = Self {
65        minimum: size3(0, 0, 0),
66    };
67}
68
69impl Fmt<ConciseDebug> for LayoutRequest {
70    fn fmt(&self, fmt: &mut fmt::Formatter<'_>, _: &ConciseDebug) -> fmt::Result {
71        let &Self { minimum } = self;
72        write!(fmt, "{:?}", minimum.to_array())
73    }
74}
75
76/// Region a widget has been given by the layout algorithm, based on its
77/// [`LayoutRequest`].
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79#[expect(clippy::exhaustive_structs)] // TODO: constructor or something
80pub struct LayoutGrant {
81    /// The widget may have exclusive access to this volume.
82    pub bounds: GridAab,
83
84    /// Preferred alignment for non-stretchy widgets.
85    pub gravity: Gravity,
86}
87
88impl LayoutGrant {
89    /// Construct a `LayoutGrant` from scratch, such as to begin layout.
90    pub fn new(bounds: GridAab) -> Self {
91        LayoutGrant {
92            bounds,
93            gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
94        }
95    }
96
97    /// Shrink the bounds to the requested size, obeying the gravity
98    /// parameter to choose where to position the result.
99    ///
100    /// If the given size is larger than this grant on any axis then that axis will be
101    /// unchanged.
102    ///
103    /// `enlarge_for_symmetry` controls the behavior when `sizes` has different parity on
104    /// any axis than `self.bounds` (one size is odd and the other is even), and
105    /// `self.gravity` requests centering; `true` requests that the returned size should
106    /// be one greater on that axis, and `false` that the position should be rounded down
107    /// (asymmetric placement).
108    #[must_use]
109    pub fn shrink_to(self, mut sizes: GridSize, enlarge_for_symmetry: bool) -> Self {
110        if enlarge_for_symmetry {
111            for axis in Axis::ALL {
112                if self.gravity[axis] == Align::Center
113                    && self.bounds.size()[axis].rem_euclid(2) != sizes[axis].rem_euclid(2)
114                {
115                    sizes[axis] += 1;
116                }
117            }
118        }
119
120        // Ensure we don't enlarge the size of self by clamping the proposed size
121        let sizes = sizes.min(self.bounds.size());
122
123        let mut origin = GridPoint::new(0, 0, 0);
124        for axis in Axis::ALL {
125            // TODO: numeric overflow considerations
126            let l = self.bounds.lower_bounds()[axis];
127            let h = self.bounds.upper_bounds()[axis]
128                .checked_sub_unsigned(sizes[axis])
129                .unwrap();
130            origin[axis] = match self.gravity[axis] {
131                Align::Low => l,
132                Align::Center => l + (h - l) / 2,
133                Align::High => h,
134            };
135        }
136        LayoutGrant {
137            bounds: GridAab::from_lower_size(origin, sizes),
138            gravity: self.gravity,
139        }
140    }
141
142    /// As `shrink_to()` but returning a single cube, as long as the grant is nonempty.
143    ///
144    /// This is a common pattern but I'm not sure it should be, so this isn't public.
145    pub(crate) fn shrink_to_cube(&self) -> Option<Cube> {
146        self.shrink_to(GridSize::new(1, 1, 1), false)
147            .bounds
148            .interior_iter()
149            .next()
150    }
151}
152
153/// Where to position things, on a given axis, when available space exceeds required space.
154#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
155#[non_exhaustive]
156pub enum Align {
157    /// All the way in the direction of the lower corner (left, down, back).
158    Low,
159    /// Centered, or as close as possible.
160    Center,
161    /// All the way in the direction of the upper corner (right, up, front).
162    High,
163}
164
165/// Specifies which corner of available space a widget should prefer to position
166/// itself towards if it is not intending to fill that space.
167///
168/// TODO: Use a better enum
169pub type Gravity = euclid::default::Vector3D<Align>;
170
171/// Something which can occupy space in a [`LayoutTree`], or is one.
172///
173/// TODO: give this trait and [`LayoutRequest`] better names
174pub trait Layoutable {
175    /// Requested minimum size and positioning of this object.
176    fn requirements(&self) -> LayoutRequest;
177}
178
179impl<T: ?Sized + Layoutable> Layoutable for &'_ T {
180    fn requirements(&self) -> LayoutRequest {
181        (**self).requirements()
182    }
183}
184impl<T: ?Sized + Layoutable> Layoutable for Box<T> {
185    fn requirements(&self) -> LayoutRequest {
186        (**self).requirements()
187    }
188}
189impl<T: ?Sized + Layoutable> Layoutable for Rc<T> {
190    fn requirements(&self) -> LayoutRequest {
191        (**self).requirements()
192    }
193}
194impl<T: ?Sized + Layoutable> Layoutable for Arc<T> {
195    fn requirements(&self) -> LayoutRequest {
196        (**self).requirements()
197    }
198}
199
200/// A user interface laid out in 3-dimensional space.
201///
202/// Leaf nodes contain values of type `W` which describe individual 'widgets' (values
203/// that implement [`Layoutable`]); the tree structure itself describes how they are
204/// arranged relative to each other. In this system, widgets do not contain other widgets
205/// (at least, not for the purposes of the layout algorithm).
206#[derive(Clone, Debug, Eq, PartialEq)]
207#[non_exhaustive]
208pub enum LayoutTree<W> {
209    /// A single widget.
210    Leaf(W),
211
212    /// An space laid out like a widget but left empty.
213    Spacer(LayoutRequest),
214
215    /// Add the specified amount of space around the child.
216    Margin {
217        /// Minimum amount of space to leave on each face.
218        margin: FaceMap<u8>,
219        #[allow(missing_docs)]
220        child: Arc<LayoutTree<W>>,
221    },
222
223    /// Fill the available space with the children arranged along an axis.
224    Stack {
225        /// Which axis of space to arrange on.
226        direction: Face6,
227        #[allow(missing_docs)]
228        children: Vec<Arc<LayoutTree<W>>>,
229    },
230
231    /// Don't lay out the contents bigger than minimum.
232    Shrink(Arc<LayoutTree<W>>),
233
234    /// A custom layout dedicated to the HUD.
235    /// TODO: Find a better abstraction than a variant of `LayoutTree` for this.
236    #[allow(missing_docs)]
237    Hud {
238        crosshair: Arc<LayoutTree<W>>,
239        toolbar: Arc<LayoutTree<W>>,
240        control_bar: Arc<LayoutTree<W>>,
241    },
242}
243
244/// Result of [`LayoutTree::perform_layout`]: specifies where items were positioned, in
245/// absolute coordinates (independent of the tree).
246#[derive(Clone, Copy, Debug, Eq, PartialEq)]
247#[expect(clippy::exhaustive_structs)]
248pub struct Positioned<W> {
249    #[allow(missing_docs)]
250    pub value: W,
251    #[allow(missing_docs)]
252    pub position: LayoutGrant,
253}
254
255impl<W> LayoutTree<W> {
256    /// Constructs a tree node that takes up no space.
257    pub fn empty() -> Arc<Self> {
258        Arc::new(Self::Spacer(LayoutRequest::EMPTY))
259    }
260
261    /// Constructs a [`LayoutTree::Leaf`], already wrapped in [`Arc`].
262    pub fn leaf(widget_value: W) -> Arc<Self> {
263        Arc::new(Self::Leaf(widget_value))
264    }
265
266    /// Constructs a [`LayoutTree::Spacer`], already wrapped in [`Arc`].
267    pub fn spacer(requirements: LayoutRequest) -> Arc<Self> {
268        Arc::new(Self::Spacer(requirements))
269    }
270
271    /// Iterates over every leaf (value of type `W`) in this tree.
272    pub fn leaves<'s>(&'s self) -> impl Iterator<Item = &'s W> + Clone {
273        // TODO: Reimplement this as a direct iterator instead of collecting
274        let mut leaves: Vec<&'s W> = Vec::new();
275        self.for_each_leaf(&mut |leaf| leaves.push(leaf));
276        leaves.into_iter()
277    }
278
279    fn for_each_leaf<'s, F>(&'s self, function: &mut F)
280    where
281        F: FnMut(&'s W),
282    {
283        match self {
284            LayoutTree::Leaf(value) => function(value),
285            LayoutTree::Spacer(_) => {}
286            LayoutTree::Margin { margin: _, child } => child.for_each_leaf(function),
287            LayoutTree::Stack {
288                direction: _,
289                children,
290            } => {
291                for child in children {
292                    child.for_each_leaf(function)
293                }
294            }
295            LayoutTree::Shrink(child) => {
296                child.for_each_leaf(function);
297            }
298            LayoutTree::Hud {
299                crosshair,
300                toolbar,
301                control_bar,
302            } => {
303                crosshair.for_each_leaf(function);
304                toolbar.for_each_leaf(function);
305                control_bar.for_each_leaf(function);
306            }
307        }
308    }
309}
310
311impl<W: Layoutable + Clone> LayoutTree<W> {
312    /// Given the specified outermost bounds, perform layout and return a tree
313    /// whose leaves are all [`Positioned`].
314    ///
315    /// TODO: haven't decided whether layout can fail yet, hence the placeholder non-error
316    pub fn perform_layout(
317        &self,
318        grant: LayoutGrant,
319    ) -> Result<Arc<LayoutTree<Positioned<W>>>, core::convert::Infallible> {
320        Ok(Arc::new(match *self {
321            LayoutTree::Leaf(ref w) => LayoutTree::Leaf(Positioned {
322                // TODO: Implicitly Arc the leaf values? Or just drop this idea of the tree being
323                // shared at all?
324                value: W::clone(w),
325                position: grant,
326            }),
327            LayoutTree::Spacer(ref r) => LayoutTree::Spacer(r.clone()),
328            LayoutTree::Margin { margin, ref child } => LayoutTree::Margin {
329                margin,
330                child: child.perform_layout(LayoutGrant {
331                    // TODO: more gradual too-small behavior than this unwrap_or() provides
332                    bounds: grant
333                        .bounds
334                        .shrink(margin.map(|_, m| m.into()))
335                        .unwrap_or(grant.bounds),
336                    gravity: grant.gravity,
337                })?,
338            },
339            LayoutTree::Stack {
340                direction,
341                ref children,
342            } => {
343                let axis = direction.axis();
344                let gravity = {
345                    let mut g = grant.gravity;
346                    g[direction.axis()] = if direction.is_positive() {
347                        Align::Low
348                    } else {
349                        Align::High
350                    };
351                    g
352                };
353
354                let mut positioned_children = Vec::with_capacity(children.len());
355                let mut bounds = grant.bounds;
356                for child in children {
357                    let requirements = child.requirements();
358                    let size_on_axis = requirements.minimum.to_i32()[axis];
359                    let available_size = bounds.size().to_i32()[axis];
360                    if size_on_axis > available_size {
361                        // TODO: emit detectable warning
362                        break;
363                    }
364
365                    // TODO: remainder computation is inelegant - we want .expand() but single axis
366                    let child_bounds = bounds.abut(direction.opposite(), -size_on_axis)
367                        .unwrap(/* always smaller, can't overflow */);
368                    let remainder_bounds = bounds.abut(direction, -(available_size - size_on_axis))
369                        .unwrap(/* always smaller, can't overflow */);
370
371                    positioned_children.push(child.perform_layout(LayoutGrant {
372                        bounds: child_bounds,
373                        gravity,
374                    })?);
375                    bounds = remainder_bounds;
376                }
377                LayoutTree::Stack {
378                    direction,
379                    children: positioned_children,
380                }
381            }
382            LayoutTree::Shrink(ref child) => {
383                let grant = grant.shrink_to(child.requirements().minimum, true);
384                LayoutTree::Shrink(child.perform_layout(grant)?)
385            }
386            LayoutTree::Hud {
387                ref crosshair,
388                ref toolbar,
389                ref control_bar,
390            } => {
391                let mut crosshair_pos =
392                    Cube::containing(grant.bounds.center()).unwrap(/* TODO: not unwrap */);
393                crosshair_pos.z = 0;
394                let crosshair_bounds = crosshair_pos.grid_aab();
395                // TODO: bounds of toolbar and control_bar should be just small enough to miss the crosshair. Also figure out exactly what their Z range should be
396                LayoutTree::Hud {
397                    crosshair: crosshair.perform_layout(LayoutGrant {
398                        bounds: crosshair_bounds,
399                        gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
400                    })?,
401                    toolbar: toolbar.perform_layout(LayoutGrant {
402                        bounds: GridAab::from_lower_upper(
403                            [
404                                grant.bounds.lower_bounds().x,
405                                grant.bounds.lower_bounds().y,
406                                0,
407                            ],
408                            [
409                                grant.bounds.upper_bounds().x,
410                                crosshair_bounds.lower_bounds().y,
411                                grant.bounds.upper_bounds().z,
412                            ],
413                        ),
414                        gravity: Vector3D::new(Align::Center, Align::Low, Align::Center),
415                    })?,
416                    control_bar: control_bar.perform_layout(LayoutGrant {
417                        bounds: GridAab::from_lower_upper(
418                            [
419                                grant.bounds.lower_bounds().x,
420                                crosshair_bounds.upper_bounds().y,
421                                -1,
422                            ],
423                            grant.bounds.upper_bounds(),
424                        ),
425                        gravity: Vector3D::new(Align::High, Align::High, Align::Low),
426                    })?,
427                }
428            }
429        }))
430    }
431}
432
433impl LayoutTree<Arc<dyn Widget>> {
434    /// Create a [`Space`] with these widgets installed in it, just large enough to fit.
435    ///
436    /// Note that the widgets will not actually appear as blocks until the first time the
437    /// space is stepped.
438    pub fn to_space<B: space::builder::Bounds>(
439        self: &Arc<Self>,
440        builder: space::Builder<B>,
441        gravity: Gravity,
442    ) -> Result<Space, InstallVuiError> {
443        let mut space = builder
444            .bounds_if_not_set(|| GridAab::from_lower_size([0, 0, 0], self.requirements().minimum))
445            .build();
446
447        install_widgets(
448            LayoutGrant {
449                bounds: space.bounds(),
450                gravity,
451            },
452            self,
453        )?
454        .execute(&mut space, &mut transaction::no_outputs)
455        .map_err(|error| InstallVuiError::ExecuteInstallation { error })?;
456
457        Ok(space)
458    }
459}
460
461impl LayoutTree<Positioned<Arc<dyn Widget>>> {
462    /// Creates a transaction which will install all of the widgets in this tree.
463    ///
464    /// Returns an error if the widgets conflict with each other.
465    pub fn installation(&self) -> Result<SpaceTransaction, InstallVuiError> {
466        let mut txn = SpaceTransaction::default();
467        for positioned_widget @ Positioned { value, position } in self.leaves() {
468            let widget = value.clone();
469            let controller_installation = WidgetBehavior::installation(
470                positioned_widget.clone(),
471                widget.controller(position),
472            )?;
473            validate_widget_transaction(value, &controller_installation, position)?;
474            txn.merge_from(controller_installation)
475                .map_err(|error| InstallVuiError::Conflict { error })?;
476        }
477        Ok(txn)
478    }
479}
480
481impl<W: Layoutable> Layoutable for LayoutTree<W> {
482    fn requirements(&self) -> LayoutRequest {
483        match *self {
484            LayoutTree::Leaf(ref w) => w.requirements(),
485            LayoutTree::Spacer(ref requirements) => requirements.clone(),
486            LayoutTree::Margin { margin, ref child } => {
487                let mut req = child.requirements();
488                req.minimum += Size3D::from(margin.negatives() + margin.positives()).to_u32();
489                req
490            }
491            LayoutTree::Stack {
492                direction,
493                ref children,
494            } => {
495                let mut accumulator = GridSize::zero();
496                let stack_axis = direction.axis();
497                for child in children {
498                    let child_req = child.requirements();
499                    for axis in Axis::ALL {
500                        if axis == stack_axis {
501                            accumulator[axis] += child_req.minimum[axis];
502                        } else {
503                            accumulator[axis] = accumulator[axis].max(child_req.minimum[axis]);
504                        }
505                    }
506                }
507                LayoutRequest {
508                    minimum: accumulator,
509                }
510            }
511            LayoutTree::Shrink(ref child) => child.requirements(),
512            LayoutTree::Hud {
513                ref crosshair,
514                ref toolbar,
515                ref control_bar,
516            } => {
517                // Minimum space is the same as a stack, for now
518                LayoutTree::Stack {
519                    direction: Face6::PY,
520                    children: vec![crosshair.clone(), toolbar.clone(), control_bar.clone()],
521                }
522                .requirements()
523            }
524        }
525    }
526}
527
528/// Narrower version of [`LayoutTree::leaf`] which is maximally ergonomic for wrapping individual
529/// [`Widget`] implementors without any type issues.
530pub fn leaf_widget(widget: impl IntoWidgetTree) -> WidgetTree {
531    widget.into_widget_tree()
532}
533/// Conversion into [`WidgetTree`]s.
534///
535/// You may use this trait via the function [`leaf_widget()`] instead of importing it.
536/// It exists so that, given a concrete widget type, we can reach `Arc<dyn Widget>` without
537/// encountering any type ambiguities in the conversion steps or requiring the caller to do
538/// any explicit `Arc` wrapping or type annotation.
539pub trait IntoWidgetTree {
540    /// Wrap `self` as necessary to make it into a [`WidgetTree`] [leaf](LayoutTree::Leaf).
541    fn into_widget_tree(self) -> WidgetTree;
542}
543impl<W: Widget + Sized + 'static> IntoWidgetTree for W {
544    #[inline]
545    fn into_widget_tree(self) -> WidgetTree {
546        let dyn_widget: Arc<dyn Widget> = Arc::new(self);
547        LayoutTree::leaf(dyn_widget)
548    }
549}
550impl<W: Widget + Sized + 'static> IntoWidgetTree for Arc<W> {
551    #[inline]
552    fn into_widget_tree(self) -> WidgetTree {
553        let dyn_widget: Arc<dyn Widget> = self;
554        LayoutTree::leaf(dyn_widget)
555    }
556}
557impl IntoWidgetTree for Arc<dyn Widget> {
558    #[inline]
559    fn into_widget_tree(self) -> WidgetTree {
560        LayoutTree::leaf(self)
561    }
562}
563
564pub(super) fn validate_widget_transaction(
565    widget: &Arc<dyn Widget>,
566    transaction: &SpaceTransaction,
567    grant: &LayoutGrant,
568) -> Result<(), InstallVuiError> {
569    match transaction.bounds() {
570        None => Ok(()),
571        Some(txn_bounds) => {
572            if grant.bounds.contains_box(txn_bounds) {
573                Ok(())
574            } else {
575                // TODO: This being InstallVuiError isn't great if we might want to validate
576                // transactions happening after installation.
577                Err(InstallVuiError::OutOfBounds {
578                    widget: widget.clone(),
579                    grant: *grant,
580                    erroneous: txn_bounds,
581                })
582            }
583        }
584    }
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use all_is_cubes::euclid::vec3;
591    use pretty_assertions::assert_eq;
592
593    /// Trivial implementation of [`Layoutable`].
594    #[derive(Clone, Debug, PartialEq)]
595    struct LT {
596        label: &'static str,
597        requirements: LayoutRequest,
598    }
599
600    impl LT {
601        fn new(label: &'static str, minimum_size: impl Into<GridSize>) -> Self {
602            Self {
603                label,
604                requirements: LayoutRequest {
605                    minimum: minimum_size.into(),
606                },
607            }
608        }
609    }
610
611    impl Layoutable for LT {
612        fn requirements(&self) -> LayoutRequest {
613            self.requirements.clone()
614        }
615    }
616
617    #[test]
618    fn simple_stack_with_extra_room() {
619        let tree = LayoutTree::Stack {
620            direction: Face6::PX,
621            children: vec![
622                LayoutTree::leaf(LT::new("a", [1, 1, 1])),
623                LayoutTree::leaf(LT::new("b", [1, 1, 1])),
624                LayoutTree::leaf(LT::new("c", [1, 1, 1])),
625            ],
626        };
627        let grant = LayoutGrant::new(GridAab::from_lower_size([10, 10, 10], [10, 10, 10]));
628        let stack_gravity = vec3(Align::Low, Align::Center, Align::Center);
629        assert_eq!(
630            tree.perform_layout(grant)
631                .unwrap()
632                .leaves()
633                .collect::<Vec<_>>(),
634            vec![
635                &Positioned {
636                    value: LT::new("a", [1, 1, 1]),
637                    position: LayoutGrant {
638                        bounds: GridAab::from_lower_size([10, 10, 10], [1, 10, 10]),
639                        gravity: stack_gravity,
640                    },
641                },
642                &Positioned {
643                    value: LT::new("b", [1, 1, 1]),
644                    position: LayoutGrant {
645                        bounds: GridAab::from_lower_size([11, 10, 10], [1, 10, 10]),
646                        gravity: stack_gravity,
647                    },
648                },
649                &Positioned {
650                    value: LT::new("c", [1, 1, 1]),
651                    position: LayoutGrant {
652                        bounds: GridAab::from_lower_size([12, 10, 10], [1, 10, 10]),
653                        gravity: stack_gravity,
654                    },
655                }
656            ]
657        );
658    }
659
660    #[test]
661    fn spacer() {
662        let tree = LayoutTree::Stack {
663            direction: Face6::PX,
664            children: vec![
665                LayoutTree::leaf(LT::new("a", [1, 1, 1])),
666                LayoutTree::spacer(LayoutRequest {
667                    minimum: size3(3, 1, 1),
668                }),
669                LayoutTree::leaf(LT::new("b", [1, 1, 1])),
670            ],
671        };
672        let grant = LayoutGrant::new(GridAab::from_lower_size([10, 10, 10], [10, 10, 10]));
673        let stack_gravity = vec3(Align::Low, Align::Center, Align::Center);
674        assert_eq!(
675            tree.perform_layout(grant)
676                .unwrap()
677                .leaves()
678                .collect::<Vec<_>>(),
679            vec![
680                &Positioned {
681                    value: LT::new("a", [1, 1, 1]),
682                    position: LayoutGrant {
683                        bounds: GridAab::from_lower_size([10, 10, 10], [1, 10, 10]),
684                        gravity: stack_gravity,
685                    },
686                },
687                &Positioned {
688                    value: LT::new("b", [1, 1, 1]),
689                    position: LayoutGrant {
690                        bounds: GridAab::from_lower_size([14, 10, 10], [1, 10, 10]),
691                        gravity: stack_gravity,
692                    },
693                }
694            ]
695        );
696    }
697
698    #[test]
699    fn shrink_to_bigger_than_grant_does_not_enlarge_grant() {
700        // X axis is too small already
701        // Y axis is exactly right
702        // Z axis is too large and thus subject to centering
703        // (because LayoutGrant::new sets gravity to center)
704        assert_eq!(
705            LayoutGrant::new(GridAab::from_lower_size([0, 0, 0], [5, 10, 20]))
706                .shrink_to(size3(10, 10, 10), false),
707            LayoutGrant::new(GridAab::from_lower_size([0, 0, 5], [5, 10, 10]))
708        );
709    }
710
711    /// `shrink_to()` called with centering and `enlarge_for_symmetry` false.
712    #[test]
713    fn shrink_to_rounding_without_enlarging() {
714        let grant = LayoutGrant {
715            bounds: GridAab::from_lower_size([10, 10, 10], [10, 10, 11]),
716            gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
717        };
718        assert_eq!(
719            grant.shrink_to(size3(1, 2, 2), false),
720            LayoutGrant {
721                bounds: GridAab::from_lower_size([14, 14, 14], [1, 2, 2]), // TODO: oughta be rounding down
722                gravity: grant.gravity,
723            }
724        );
725    }
726
727    /// `shrink_to()` called with centering and `enlarge_for_symmetry` true.
728    #[test]
729    fn shrink_to_with_enlarging() {
730        // In each case where the parity does not match between the minimum and the
731        // grant, the size of the post-layout grant should be enlarged to match the
732        // original grant's parity.
733        // X axis is an odd size in even grant
734        // Y axis is an even size in an even grant (should stay the same size)
735        // Z axis is an even size in an odd grant
736        let grant = LayoutGrant {
737            bounds: GridAab::from_lower_size([10, 10, 10], [10, 10, 9]),
738            gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
739        };
740        assert_eq!(
741            grant.shrink_to(size3(1, 2, 2), true),
742            LayoutGrant {
743                bounds: GridAab::from_lower_size([14, 14, 13], [2, 2, 3]),
744                gravity: grant.gravity,
745            }
746        );
747    }
748}