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
17pub type WidgetTree = Arc<LayoutTree<Arc<dyn Widget>>>;
24
25pub fn install_widgets(
40 grant: LayoutGrant,
41 tree: &WidgetTree,
42 read_ticket: ReadTicket<'_>,
43) -> Result<SpaceTransaction, InstallVuiError> {
44 tree.perform_layout(grant).unwrap().installation(read_ticket)
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
52#[expect(clippy::exhaustive_structs)]
53pub struct LayoutRequest {
54 pub minimum: GridSize,
57}
58
59impl LayoutRequest {
60 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76#[expect(clippy::exhaustive_structs)] pub struct LayoutGrant {
78 pub bounds: GridAab,
80
81 pub gravity: Gravity,
83}
84
85impl LayoutGrant {
86 pub fn new(bounds: GridAab) -> Self {
88 LayoutGrant {
89 bounds,
90 gravity: Vector3D::new(Align::Center, Align::Center, Align::Center),
91 }
92 }
93
94 #[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 let sizes = sizes.min(self.bounds.size());
119
120 let mut origin = GridPoint::new(0, 0, 0);
121 for axis in Axis::ALL {
122 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 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#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
147#[non_exhaustive]
148pub enum Align {
149 Low,
151 Center,
153 High,
155}
156
157pub type Gravity = euclid::default::Vector3D<Align>;
162
163pub trait Layoutable {
167 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#[derive(Clone, Debug, Eq, PartialEq)]
199#[non_exhaustive]
200pub enum LayoutTree<W> {
201 Leaf(W),
203
204 Spacer(LayoutRequest),
206
207 Margin {
209 margin: FaceMap<u8>,
211 #[allow(missing_docs)]
212 child: Arc<LayoutTree<W>>,
213 },
214
215 Stack {
217 direction: Face6,
219 #[allow(missing_docs)]
220 children: Vec<Arc<LayoutTree<W>>>,
221 },
222
223 Shrink(Arc<LayoutTree<W>>),
225
226 #[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#[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 pub fn empty() -> Arc<Self> {
250 Arc::new(Self::Spacer(LayoutRequest::EMPTY))
251 }
252
253 pub fn leaf(widget_value: W) -> Arc<Self> {
255 Arc::new(Self::Leaf(widget_value))
256 }
257
258 pub fn spacer(requirements: LayoutRequest) -> Arc<Self> {
260 Arc::new(Self::Spacer(requirements))
261 }
262
263 pub fn leaves<'s>(&'s self) -> impl Iterator<Item = &'s W> + Clone {
265 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 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 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 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 break;
355 }
356
357 let child_bounds = bounds.abut(direction.opposite(), -size_on_axis)
359 .unwrap();
360 let remainder_bounds = bounds.abut(direction, -(available_size - size_on_axis))
361 .unwrap();
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();
385 crosshair_pos.z = 0;
386 let crosshair_bounds = crosshair_pos.grid_aab();
387 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 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 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 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).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 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 #[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]), gravity: grant.gravity,
717 }
718 );
719 }
720
721 #[test]
723 fn shrink_to_with_enlarging() {
724 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}