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
21pub type WidgetTree = Arc<LayoutTree<Arc<dyn Widget>>>;
28
29pub fn install_widgets(
44 grant: LayoutGrant,
45 tree: &WidgetTree,
46) -> Result<SpaceTransaction, InstallVuiError> {
47 tree.perform_layout(grant).unwrap().installation()
48}
49
50#[derive(Clone, Debug, Eq, PartialEq)]
55#[expect(clippy::exhaustive_structs)]
56pub struct LayoutRequest {
57 pub minimum: GridSize,
60}
61
62impl LayoutRequest {
63 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79#[expect(clippy::exhaustive_structs)] pub struct LayoutGrant {
81 pub bounds: GridAab,
83
84 pub gravity: Gravity,
86}
87
88impl LayoutGrant {
89 pub fn new(bounds: GridAab) -> Self {
91 LayoutGrant {
92 bounds,
93 gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
94 }
95 }
96
97 #[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 let sizes = sizes.min(self.bounds.size());
122
123 let mut origin = GridPoint::new(0, 0, 0);
124 for axis in Axis::ALL {
125 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 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
155#[non_exhaustive]
156pub enum Align {
157 Low,
159 Center,
161 High,
163}
164
165pub type Gravity = euclid::default::Vector3D<Align>;
170
171pub trait Layoutable {
175 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#[derive(Clone, Debug, Eq, PartialEq)]
207#[non_exhaustive]
208pub enum LayoutTree<W> {
209 Leaf(W),
211
212 Spacer(LayoutRequest),
214
215 Margin {
217 margin: FaceMap<u8>,
219 #[allow(missing_docs)]
220 child: Arc<LayoutTree<W>>,
221 },
222
223 Stack {
225 direction: Face6,
227 #[allow(missing_docs)]
228 children: Vec<Arc<LayoutTree<W>>>,
229 },
230
231 Shrink(Arc<LayoutTree<W>>),
233
234 #[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#[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 pub fn empty() -> Arc<Self> {
258 Arc::new(Self::Spacer(LayoutRequest::EMPTY))
259 }
260
261 pub fn leaf(widget_value: W) -> Arc<Self> {
263 Arc::new(Self::Leaf(widget_value))
264 }
265
266 pub fn spacer(requirements: LayoutRequest) -> Arc<Self> {
268 Arc::new(Self::Spacer(requirements))
269 }
270
271 pub fn leaves<'s>(&'s self) -> impl Iterator<Item = &'s W> + Clone {
273 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 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 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 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 break;
363 }
364
365 let child_bounds = bounds.abut(direction.opposite(), -size_on_axis)
367 .unwrap();
368 let remainder_bounds = bounds.abut(direction, -(available_size - size_on_axis))
369 .unwrap();
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();
393 crosshair_pos.z = 0;
394 let crosshair_bounds = crosshair_pos.grid_aab();
395 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 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 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 LayoutTree::Stack {
519 direction: Face6::PY,
520 children: vec![crosshair.clone(), toolbar.clone(), control_bar.clone()],
521 }
522 .requirements()
523 }
524 }
525 }
526}
527
528pub fn leaf_widget(widget: impl IntoWidgetTree) -> WidgetTree {
531 widget.into_widget_tree()
532}
533pub trait IntoWidgetTree {
540 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 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 #[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 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 #[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]), gravity: grant.gravity,
723 }
724 );
725 }
726
727 #[test]
729 fn shrink_to_with_enlarging() {
730 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}