use std::rc::Rc;
use std::sync::Arc;
use all_is_cubes::cgmath::{Vector3, Zero as _};
use all_is_cubes::math::{
point_to_enclosing_cube, Face6, GridAab, GridCoordinate, GridPoint, GridVector,
};
use all_is_cubes::space::{Space, SpaceBuilder, SpaceTransaction};
use all_is_cubes::transaction::{Merge as _, Transaction};
use crate::vui::{InstallVuiError, Widget, WidgetBehavior};
pub type WidgetTree = Arc<LayoutTree<Arc<dyn Widget>>>;
pub fn install_widgets(
grant: LayoutGrant,
tree: &WidgetTree,
) -> Result<SpaceTransaction, InstallVuiError> {
tree.perform_layout(grant).unwrap().installation()
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[allow(clippy::exhaustive_structs)]
pub struct LayoutRequest {
pub minimum: GridVector,
}
impl LayoutRequest {
pub const EMPTY: Self = Self {
minimum: GridVector::new(0, 0, 0),
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(clippy::exhaustive_structs)] pub struct LayoutGrant {
pub bounds: GridAab,
pub gravity: Gravity,
}
impl LayoutGrant {
pub fn new(bounds: GridAab) -> Self {
LayoutGrant {
bounds,
gravity: Vector3::new(Align::Center, Align::Center, Align::Center),
}
}
#[must_use]
pub fn shrink_to(self, sizes: GridVector) -> Self {
assert!(
sizes.x >= 0 && sizes.y >= 0 && sizes.z >= 0,
"sizes to shrink to must be positive, not {sizes:?}"
);
let sizes = sizes.zip(self.bounds.size(), GridCoordinate::min);
let mut origin = GridPoint::new(0, 0, 0);
for axis in 0..3 {
let l = self.bounds.lower_bounds()[axis];
let h = self.bounds.upper_bounds()[axis] - sizes[axis];
origin[axis] = match self.gravity[axis] {
Align::Low => l,
Align::Center => l + (h - l) / 2, Align::High => h,
};
}
LayoutGrant {
bounds: GridAab::from_lower_size(origin, sizes),
gravity: self.gravity,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum Align {
Low,
Center,
High,
}
pub type Gravity = Vector3<Align>;
pub trait Layoutable {
fn requirements(&self) -> LayoutRequest;
}
impl<T: ?Sized + Layoutable> Layoutable for &'_ T {
fn requirements(&self) -> LayoutRequest {
(**self).requirements()
}
}
impl<T: ?Sized + Layoutable> Layoutable for Box<T> {
fn requirements(&self) -> LayoutRequest {
(**self).requirements()
}
}
impl<T: ?Sized + Layoutable> Layoutable for Rc<T> {
fn requirements(&self) -> LayoutRequest {
(**self).requirements()
}
}
impl<T: ?Sized + Layoutable> Layoutable for Arc<T> {
fn requirements(&self) -> LayoutRequest {
(**self).requirements()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum LayoutTree<W> {
Leaf(W),
Spacer(LayoutRequest),
Stack {
direction: Face6,
children: Vec<Arc<LayoutTree<W>>>,
},
Hud {
crosshair: Arc<LayoutTree<W>>,
toolbar: Arc<LayoutTree<W>>,
control_bar: Arc<LayoutTree<W>>,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(clippy::exhaustive_structs)]
pub struct Positioned<W> {
pub value: W,
pub position: LayoutGrant,
}
impl<W> LayoutTree<W> {
pub fn empty() -> Arc<Self> {
Arc::new(Self::Spacer(LayoutRequest::EMPTY))
}
pub fn leaf(widget_value: W) -> Arc<Self> {
Arc::new(Self::Leaf(widget_value))
}
pub fn spacer(requirements: LayoutRequest) -> Arc<Self> {
Arc::new(Self::Spacer(requirements))
}
pub fn leaves<'s>(&'s self) -> impl Iterator<Item = &'s W> + Clone {
let mut leaves: Vec<&'s W> = Vec::new();
self.for_each_leaf(&mut |leaf| leaves.push(leaf));
leaves.into_iter()
}
fn for_each_leaf<'s, F>(&'s self, function: &mut F)
where
F: FnMut(&'s W),
{
match self {
LayoutTree::Leaf(value) => function(value),
LayoutTree::Spacer(_) => {}
LayoutTree::Stack {
direction: _,
children,
} => {
for child in children {
child.for_each_leaf(function)
}
}
LayoutTree::Hud {
crosshair,
toolbar,
control_bar,
} => {
crosshair.for_each_leaf(function);
toolbar.for_each_leaf(function);
control_bar.for_each_leaf(function);
}
}
}
}
impl<W: Layoutable + Clone> LayoutTree<W> {
pub fn perform_layout(
&self,
grant: LayoutGrant,
) -> Result<Arc<LayoutTree<Positioned<W>>>, std::convert::Infallible> {
Ok(Arc::new(match *self {
LayoutTree::Leaf(ref w) => LayoutTree::Leaf(Positioned {
value: W::clone(w),
position: grant,
}),
LayoutTree::Spacer(ref r) => LayoutTree::Spacer(r.clone()),
LayoutTree::Stack {
direction,
ref children,
} => {
let mut positioned_children = Vec::with_capacity(children.len());
let mut bounds = grant.bounds;
for child in children {
let requirements = child.requirements();
let axis = direction.axis_number();
let size_on_axis = requirements.minimum[axis];
let available_size = bounds.size()[axis];
if size_on_axis > available_size {
break;
}
let child_bounds = bounds.abut(direction.opposite(), -size_on_axis)
.unwrap();
let remainder_bounds = bounds.abut(direction, -(available_size - size_on_axis))
.unwrap();
positioned_children.push(child.perform_layout(LayoutGrant {
bounds: child_bounds,
gravity: grant.gravity,
})?);
bounds = remainder_bounds;
}
LayoutTree::Stack {
direction,
children: positioned_children,
}
}
LayoutTree::Hud {
ref crosshair,
ref toolbar,
ref control_bar,
} => {
let mut crosshair_pos =
point_to_enclosing_cube(grant.bounds.center()).unwrap();
crosshair_pos.z = 0;
let crosshair_bounds = GridAab::single_cube(crosshair_pos);
LayoutTree::Hud {
crosshair: crosshair.perform_layout(LayoutGrant {
bounds: crosshair_bounds,
gravity: Vector3::new(Align::Center, Align::Center, Align::Center),
})?,
toolbar: toolbar.perform_layout(LayoutGrant {
bounds: GridAab::from_lower_upper(
[
grant.bounds.lower_bounds().x,
grant.bounds.lower_bounds().y,
0,
],
[
grant.bounds.upper_bounds().x,
crosshair_bounds.lower_bounds().y,
grant.bounds.upper_bounds().z,
],
),
gravity: Vector3::new(Align::Center, Align::Low, Align::Center),
})?,
control_bar: control_bar.perform_layout(LayoutGrant {
bounds: GridAab::from_lower_upper(
[
grant.bounds.lower_bounds().x,
crosshair_bounds.upper_bounds().y,
-1,
],
grant.bounds.upper_bounds(),
),
gravity: Vector3::new(Align::High, Align::High, Align::Low),
})?,
}
}
}))
}
}
impl LayoutTree<Arc<dyn Widget>> {
pub fn to_space<B: all_is_cubes::space::SpaceBuilderBounds>(
self: Arc<Self>,
builder: SpaceBuilder<B>,
gravity: Gravity,
) -> Result<Space, InstallVuiError> {
let mut space = builder
.bounds_if_not_set(|| GridAab::from_lower_size([0, 0, 0], self.requirements().minimum))
.build();
install_widgets(
LayoutGrant {
bounds: space.bounds(),
gravity,
},
&self,
)?
.execute(&mut space)
.map_err(|error| InstallVuiError::ExecuteInstallation { error })?;
Ok(space)
}
}
impl LayoutTree<Positioned<Arc<dyn Widget>>> {
pub fn installation(&self) -> Result<SpaceTransaction, InstallVuiError> {
let mut txn = SpaceTransaction::default();
for positioned_widget @ Positioned { value, position } in self.leaves() {
let widget = value.clone();
let controller_installation = WidgetBehavior::installation(
positioned_widget.clone(),
widget.controller(position),
)?;
validate_widget_transaction(value, &controller_installation, position)?;
txn = txn
.merge(controller_installation)
.map_err(|error| InstallVuiError::Conflict { error })?;
}
Ok(txn)
}
}
impl<W: Layoutable> Layoutable for LayoutTree<W> {
fn requirements(&self) -> LayoutRequest {
match *self {
LayoutTree::Leaf(ref w) => w.requirements(),
LayoutTree::Spacer(ref requirements) => requirements.clone(),
LayoutTree::Stack {
direction,
ref children,
} => {
let mut accumulator = GridVector::zero();
let stack_axis = direction.axis_number();
for child in children {
let child_req = child.requirements();
for axis in 0..3 {
if axis == stack_axis {
accumulator[axis] += child_req.minimum[axis];
} else {
accumulator[axis] = accumulator[axis].max(child_req.minimum[axis]);
}
}
}
LayoutRequest {
minimum: accumulator,
}
}
LayoutTree::Hud {
ref crosshair,
ref toolbar,
ref control_bar,
} => {
LayoutTree::Stack {
direction: Face6::PY,
children: vec![crosshair.clone(), toolbar.clone(), control_bar.clone()],
}
.requirements()
}
}
}
}
pub(super) fn validate_widget_transaction(
widget: &Arc<dyn Widget>,
transaction: &SpaceTransaction,
grant: &LayoutGrant,
) -> Result<(), InstallVuiError> {
match transaction.bounds() {
None => Ok(()),
Some(txn_bounds) => {
if grant.bounds.contains_box(txn_bounds) {
Ok(())
} else {
Err(InstallVuiError::OutOfBounds {
widget: widget.clone(),
grant: *grant,
erroneous: txn_bounds,
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use all_is_cubes::math::Face6;
use pretty_assertions::assert_eq;
#[derive(Clone, Debug, PartialEq)]
struct LT {
label: &'static str,
requirements: LayoutRequest,
}
impl LT {
fn new(label: &'static str, minimum_size: impl Into<GridVector>) -> Self {
Self {
label,
requirements: LayoutRequest {
minimum: minimum_size.into(),
},
}
}
}
impl Layoutable for LT {
fn requirements(&self) -> LayoutRequest {
self.requirements.clone()
}
}
#[test]
fn simple_stack_with_extra_room() {
let tree = LayoutTree::Stack {
direction: Face6::PX,
children: vec![
LayoutTree::leaf(LT::new("a", [1, 1, 1])),
LayoutTree::leaf(LT::new("b", [1, 1, 1])),
LayoutTree::leaf(LT::new("c", [1, 1, 1])),
],
};
let grant = LayoutGrant::new(GridAab::from_lower_size([10, 10, 10], [10, 10, 10]));
assert_eq!(
tree.perform_layout(grant)
.unwrap()
.leaves()
.collect::<Vec<_>>(),
vec![
&Positioned {
value: LT::new("a", [1, 1, 1]),
position: LayoutGrant {
bounds: GridAab::from_lower_size([10, 10, 10], [1, 10, 10]),
gravity: grant.gravity,
},
},
&Positioned {
value: LT::new("b", [1, 1, 1]),
position: LayoutGrant {
bounds: GridAab::from_lower_size([11, 10, 10], [1, 10, 10]),
gravity: grant.gravity,
},
},
&Positioned {
value: LT::new("c", [1, 1, 1]),
position: LayoutGrant {
bounds: GridAab::from_lower_size([12, 10, 10], [1, 10, 10]),
gravity: grant.gravity,
},
}
]
);
}
#[test]
fn spacer() {
let tree = LayoutTree::Stack {
direction: Face6::PX,
children: vec![
LayoutTree::leaf(LT::new("a", [1, 1, 1])),
LayoutTree::spacer(LayoutRequest {
minimum: GridVector::new(3, 1, 1),
}),
LayoutTree::leaf(LT::new("b", [1, 1, 1])),
],
};
let grant = LayoutGrant::new(GridAab::from_lower_size([10, 10, 10], [10, 10, 10]));
assert_eq!(
tree.perform_layout(grant)
.unwrap()
.leaves()
.collect::<Vec<_>>(),
vec![
&Positioned {
value: LT::new("a", [1, 1, 1]),
position: LayoutGrant {
bounds: GridAab::from_lower_size([10, 10, 10], [1, 10, 10]),
gravity: grant.gravity,
},
},
&Positioned {
value: LT::new("b", [1, 1, 1]),
position: LayoutGrant {
bounds: GridAab::from_lower_size([14, 10, 10], [1, 10, 10]),
gravity: grant.gravity,
},
}
]
);
}
#[test]
fn shrink_to_bigger_than_grant_does_not_enlarge_grant() {
assert_eq!(
LayoutGrant::new(GridAab::from_lower_size([0, 0, 0], [5, 10, 20]))
.shrink_to(Vector3::new(10, 10, 10)),
LayoutGrant::new(GridAab::from_lower_size([0, 0, 5], [5, 10, 10]))
);
}
}