#![doc = include_str!("../content_sized.md")]
use std::{any::type_name, convert::Infallible, marker::PhantomData};
use bevy::app::{App, Update};
use bevy::ecs::prelude::*;
use bevy::ecs::query::{ROQueryItem, ReadOnlyWorldQuery};
use bevy::ecs::schedule::SystemSetConfigs;
use bevy::ecs::system::{assert_is_system, StaticSystemParam, SystemParam};
use bevy::log::{debug, error, trace};
use bevy::prelude::{Name, Parent};
use bevy_mod_sysfail::{sysfail, FailureMode};
use thiserror::Error;
use crate::direction::Axis;
use crate::error::Handle;
use crate::{
ComputeLayout, ComputeLayoutSet, Container, LeafNode, LeafRule, Node, Root, Rule, Size,
};
pub use crate::labels::{ContentSizedComputeSystem, ContentSizedComputeSystemSet};
type Result<T> = std::result::Result<T, BadRule>;
#[derive(Debug, Clone, Error)]
enum Why<T> {
#[error("{}.compute_content returned a Nan when computing {1}'s {0}. Size must be a number.", type_name::<T>())]
Nan(Axis, Handle),
#[error("When computing content of {}: {0} depends on its parent, but it has no parents :(", type_name::<T>())]
Orphan(Handle),
#[error("Not shown, crate::error::Why::CyclicRule should do this job")]
CyclicRule,
#[error("This error never occurs")]
_Ignore(PhantomData<fn(T)>, Infallible),
}
impl<T> FailureMode for Why<T> {
type ID = ();
fn identify(&self) {}
}
pub trait AppContentSizeExt {
fn add_content_sized<S: ComputeContentParam>(&mut self) -> &mut Self
where
for<'w, 's> S::Item<'w, 's>: ComputeContentSize<Components = S::Components>;
}
impl AppContentSizeExt for App {
fn add_content_sized<S: ComputeContentParam>(&mut self) -> &mut Self
where
for<'w, 's> S::Item<'w, 's>: ComputeContentSize<Components = S::Components>,
{
let set = ContentSizedComputeSystem::<S>::default();
self.add_systems(
Update,
compute_content_size::<S>
.in_set(ComputeLayoutSet)
.in_set(ContentSizedComputeSystemSet)
.in_set(set),
);
self.configure_sets(Update, S::condition(set));
self.configure_sets(Update, ComputeLayout.after(set));
self
}
}
pub trait ComputeContentParam: SystemParam + 'static
where
for<'w, 's> Self::Item<'w, 's>: ComputeContentSize<Components = Self::Components>,
{
type Components: ReadOnlyWorldQuery + 'static;
fn condition(label: ContentSizedComputeSystem<Self>) -> SystemSetConfigs;
}
pub trait ComputeContentSize: SystemParam {
type Components: ReadOnlyWorldQuery + 'static;
fn compute_content(
&self,
components: ROQueryItem<Self::Components>,
set_size: Size<Option<f32>>,
) -> Size<f32>;
}
type BasicQuery<'w, 's, C, F> =
Query<'w, 's, (Entity, Option<&'static Name>, Option<&'static Parent>, C), F>;
type NodeQuery<'w, 's> =
BasicQuery<'w, 's, AnyOf<(&'static Node, &'static Root)>, Without<LeafNode>>;
#[sysfail(log(level = "error"))]
fn compute_content_size<S: ComputeContentParam>(
compute_param: StaticSystemParam<S>,
mut content_sized: BasicQuery<(&mut Node, S::Components), With<LeafNode>>,
nodes: NodeQuery,
) -> std::result::Result<(), Why<S>>
where
for<'w, 's> S::Item<'w, 's>: ComputeContentSize<Components = S::Components>,
{
let mut errs: Option<(Why<S>, usize)> = None;
assert_is_system(compute_content_size::<S>);
debug!(
"Computing content-sized nodes for {}",
bevy::utils::get_short_name(std::any::type_name::<S>())
);
for (e, name, parent, (node, components)) in &mut content_sized {
if !node.content_sized() {
continue;
}
trace!("Computing size of a node with constraints: {node:?}");
let size = match node_content_size(parent, &node, &nodes) {
Ok(size) => size,
Err(err) => {
let errs = errs.get_or_insert((err.into_why(e, name), 0));
errs.1 += 1;
continue;
}
};
let computed = compute_param.compute_content(components, size);
let computed = Size {
width: size.width.is_none().then_some(computed.width),
height: size.height.is_none().then_some(computed.height),
};
trace!("It is: {computed:?}");
if let Err(err) = set_node_content_size(node, computed) {
let errs = errs.get_or_insert((err.into_why(e, name), 0));
errs.1 += 1;
};
}
if let Some((err, _)) = errs.take() {
Err(err)
} else {
Ok(())
}
}
enum BadRule {
OrphanUnnamed,
Orphan(Handle),
Nan(Axis),
Cyclic,
}
impl BadRule {
fn into_why<T>(self, e: Entity, name: Option<&Name>) -> Why<T> {
use Handle::{Named, Unnamed};
let handle = || name.map_or(Unnamed(e), |n| Named(n.clone()));
match self {
Self::OrphanUnnamed => Why::Orphan(handle()),
Self::Orphan(handle) => Why::Orphan(handle),
Self::Nan(axis) => Why::Nan(axis, handle()),
Self::Cyclic => Why::CyclicRule,
}
}
fn name(self, e: Entity, name: Option<&Name>) -> Self {
use Handle::{Named, Unnamed};
let handle = || name.map_or(Unnamed(e), |n| Named(n.clone()));
match self {
Self::OrphanUnnamed => Self::Orphan(handle()),
Self::Orphan(_) | Self::Nan(_) | Self::Cyclic => self,
}
}
}
const fn get_rules<'a>(node: (Option<&'a Node>, Option<&'a Root>)) -> Result<&'a Size<Rule>> {
match node {
(Some(Node::Container(Container { rules, .. })), _)
| (None, Some(Root { node: Container { rules, .. }, .. })) => Ok(rules),
_ => Err(BadRule::OrphanUnnamed),
}
}
fn node_content_size(
parent: Option<&Parent>,
node: &Node,
nodes: &NodeQuery,
) -> Result<Size<Option<f32>>> {
let leaf_size = |axis, rule| match rule {
LeafRule::Parent(ratio) => Ok(Some(parent_size(ratio, axis, parent, nodes)?)),
LeafRule::Fixed(value) => Ok(Some(value)),
LeafRule::Content(_) => Ok(None),
};
if let Node::Box(size) = node {
Ok(Size {
width: leaf_size(Axis::Horizontal, size.width)?,
height: leaf_size(Axis::Vertical, size.height)?,
})
} else {
unreachable!(
"node_content_size is only called on node.is_content_sized() \
meaning this branch should never be reached"
);
}
}
fn parent_size(ratio: f32, axis: Axis, this: Option<&Parent>, nodes: &NodeQuery) -> Result<f32> {
use BadRule::OrphanUnnamed as Orphan;
let this = this.ok_or(Orphan)?.get();
let (e, n, parent, node) = nodes.get(this).map_err(|_| Orphan)?;
let rules = get_rules(node)?;
match axis.relative(rules.as_ref()).main {
Rule::Children(_) => Err(BadRule::Cyclic),
&Rule::Fixed(value) => Ok(ratio * value),
Rule::Parent(this_ratio) => {
parent_size(ratio * this_ratio, axis, parent, nodes).map_err(|err| err.name(e, n))
}
}
}
fn set_node_content_size(mut node: Mut<Node>, new: Size<Option<f32>>) -> Result<()> {
let Node::Box(size) = &mut *node else {
unreachable!(
"set_node_content_size is only called on node.is_content_sized() \
meaning this branch should never be reached"
);
};
if let (LeafRule::Content(to_update), Some(new)) = (&mut size.width, new.width) {
if new.is_nan() {
return Err(BadRule::Nan(Axis::Horizontal));
}
*to_update = new;
}
if let (LeafRule::Content(to_update), Some(new)) = (&mut size.height, new.height) {
if new.is_nan() {
return Err(BadRule::Nan(Axis::Vertical));
}
*to_update = new;
}
Ok(())
}