all_is_cubes_ui/vui/
layout.rs

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