use std::collections::HashMap;
use bevy::ecs::query::QueryData;
use bevy::prelude::*;
use bevy::ui::UiTransform;
use super::protocol::{AnimatableProperty, AnimatedBindings};
mod filter_params;
mod node_colors;
mod shape;
#[cfg(test)]
mod tests;
mod warn;
use filter_params::apply_filter_params;
use node_colors::stage_node_and_colors;
use super::{AnimatedNode, SharedValues, build_ui_transform, eval_scalar, props};
#[derive(QueryData)]
#[query_data(mutable)]
pub(super) struct AnimTargets {
transform: &'static mut UiTransform,
bg: Option<&'static mut BackgroundColor>,
border: Option<&'static mut BorderColor>,
text: Option<&'static mut TextColor>,
image: Option<&'static mut ImageNode>,
node: Option<&'static mut Node>,
promoted: Option<&'static crate::layer::PromotedLayer>,
layer_alpha: Option<&'static mut crate::layer::LayerGroupAlpha>,
resolved_filter: Option<&'static mut crate::filters::ResolvedFilterChain>,
resolved_backdrop: Option<&'static mut crate::filters::ResolvedBackdropChain>,
rnode: Option<&'static crate::bridge::RNode>,
transform3d: Option<&'static mut crate::layer::transform3d::LayerTransform3d>,
shape: Option<&'static mut crate::svg::SvgShape>,
}
pub(super) struct ValidationMemory<S>(HashMap<Entity, S>);
impl<S> Default for ValidationMemory<S> {
fn default() -> Self {
Self(HashMap::new())
}
}
impl<S: PartialEq> ValidationMemory<S> {
pub(super) fn should_validate(&self, entity: Entity, current: &S) -> bool {
self.0.get(&entity) != Some(current)
}
pub(super) fn stamp(&mut self, entity: Entity, validated_now: bool, pre: &S, post: S) {
if validated_now || &post != pre {
self.0.insert(entity, post);
}
}
pub(super) fn prune(&mut self, live: &[Entity]) {
if self.0.len() > live.len() {
self.0.retain(|e, _| live.contains(e));
}
}
}
#[allow(clippy::type_complexity)]
pub(super) fn apply_animated_nodes(
mut commands: Commands,
values: Res<SharedValues>,
mut dirt: ResMut<crate::layer::LayerContentDirt>,
mut validated: Local<ValidationMemory<(Option<u32>, Option<u32>)>>,
mut shape_validated: Local<ValidationMemory<()>>,
mut query: Query<(Entity, Ref<AnimatedNode>, AnimTargets)>,
) {
let mut filter_bound: Vec<Entity> = Vec::new();
let mut shape_bound: Vec<Entity> = Vec::new();
for (entity, anim, mut t) in &mut query {
let b = &anim.0;
let promoted = t.promoted.is_some();
stage_transform(entity, b, &values, promoted, &mut dirt, &mut t);
stage_transform3d(b, &values, &mut t);
let opacity_alpha = b
.get(AnimatableProperty::Opacity)
.and_then(|x| eval_scalar(x, &values));
stage_node_and_colors(
entity,
&mut commands,
b,
&values,
opacity_alpha,
promoted,
&mut dirt,
&mut t,
);
stage_opacity(entity, opacity_alpha, promoted, &mut dirt, &mut t);
stage_filter_params(
entity,
anim.is_changed(),
b,
&values,
&mut validated,
&mut filter_bound,
&mut dirt,
&mut t,
);
stage_shape_attrs(
entity,
anim.is_changed(),
b,
&values,
&mut shape_validated,
&mut shape_bound,
&mut t,
);
}
validated.prune(&filter_bound);
shape_validated.prune(&shape_bound);
}
pub(crate) fn push_transform_dirt(
entity: Entity,
old: &UiTransform,
new: &UiTransform,
promoted: bool,
dirt: &mut crate::layer::LayerContentDirt,
) {
let translate_only = old.scale == new.scale && old.rotation == new.rotation;
if promoted && translate_only {
dirt.composite_only.push(entity);
} else {
dirt.nodes.push(entity);
}
}
fn stage_transform(
entity: Entity,
b: &AnimatedBindings,
values: &SharedValues,
promoted: bool,
dirt: &mut crate::layer::LayerContentDirt,
t: &mut AnimTargetsItem,
) {
use AnimatableProperty as P;
if !b.has_transform() {
return;
}
let new = build_ui_transform(
b.get(P::TranslateX)
.and_then(|x| eval_scalar(x, values))
.map(Val::Px),
b.get(P::TranslateY)
.and_then(|x| eval_scalar(x, values))
.map(Val::Px),
b.get(P::Scale).and_then(|x| eval_scalar(x, values)),
b.get(P::ScaleX).and_then(|x| eval_scalar(x, values)),
b.get(P::ScaleY).and_then(|x| eval_scalar(x, values)),
b.get(P::Rotate)
.and_then(|x| eval_scalar(x, values))
.map(f32::to_radians),
);
if *t.transform != new {
push_transform_dirt(entity, &t.transform, &new, promoted, dirt);
*t.transform = new;
}
}
fn stage_transform3d(b: &AnimatedBindings, values: &SharedValues, t: &mut AnimTargetsItem) {
use AnimatableProperty as P;
if !b.has_transform3d() {
return;
}
let Some(t3d) = &mut t.transform3d else {
return;
};
use crate::animations::protocol::Transform3dField as F;
use crate::protocol::animatable::Animatable::Static;
use crate::protocol::{transform::Transform3dOrigin, units::Angle, units::Length};
let mut new = t3d.0.clone();
for (property, binding) in b.iter() {
if !matches!(property, P::Transform3d(_)) {
continue;
}
let Some(v) = eval_scalar(binding, values) else {
continue;
};
let deg = || Some(Static(Angle::from_radians(v.to_radians())));
let origin =
|o: &crate::protocol::transform::Transform3d| o.origin.clone().unwrap_or_default();
macro_rules! rule {
($prop:tt, (t3d $f:ident num $d:tt)) => {
if property == &$prop {
new.$f = Some(Static(v));
}
};
($prop:tt, (t3d $f:ident angle)) => {
if property == &$prop {
new.$f = deg();
}
};
($prop:tt, (t3d_origin x)) => {
if property == &$prop {
new.origin = Some(Transform3dOrigin {
x: Static(Length::Px(v)),
y: origin(&new).y,
});
}
};
($prop:tt, (t3d_origin y)) => {
if property == &$prop {
new.origin = Some(Transform3dOrigin {
x: origin(&new).x,
y: Static(Length::Px(v)),
});
}
};
($prop:tt, $other:tt) => {};
}
macro_rules! walk {
($(($prop:tt, $kind:ident, $acc:tt, $write:tt, $stage:ident, $park:ident),)*) => {
$(rule!($prop, $acc);)*
};
}
props::with_animatable_props!(walk);
}
if t3d.0 != new {
t3d.0 = new;
}
}
fn stage_opacity(
entity: Entity,
opacity_alpha: Option<f32>,
promoted: bool,
dirt: &mut crate::layer::LayerContentDirt,
t: &mut AnimTargetsItem,
) {
if let Some(alpha) = opacity_alpha
&& promoted
{
if let Some(la) = &mut t.layer_alpha
&& la.0 != alpha
{
la.0 = alpha;
dirt.composite_only.push(entity);
}
} else if let Some(alpha) = opacity_alpha {
let with_alpha = |color: Color| -> Option<Color> {
let mut s = color.to_srgba();
(s.alpha != alpha).then(|| {
s.alpha = alpha;
Color::Srgba(s)
})
};
let mut wrote = false;
if let Some(c) = &mut t.bg
&& let Some(new) = with_alpha(c.0)
{
c.0 = new;
wrote = true;
}
if let Some(tc) = &mut t.text
&& let Some(new) = with_alpha(tc.0)
{
tc.0 = new;
wrote = true;
}
if let Some(img) = &mut t.image
&& let Some(new) = with_alpha(img.color)
{
img.color = new;
wrote = true;
}
if wrote {
dirt.nodes.push(entity);
}
}
}
#[allow(clippy::too_many_arguments)]
fn stage_filter_params(
entity: Entity,
anim_changed: bool,
b: &AnimatedBindings,
values: &SharedValues,
validated: &mut ValidationMemory<(Option<u32>, Option<u32>)>,
filter_bound: &mut Vec<Entity>,
dirt: &mut crate::layer::LayerContentDirt,
t: &mut AnimTargetsItem,
) {
let has_filter = b.has_filter_params();
let has_backdrop = b.has_backdrop_params();
if !(has_filter || has_backdrop) {
return;
}
filter_bound.push(entity);
let pre = (
t.resolved_filter.as_ref().map(|c| c.version),
t.resolved_backdrop.as_ref().map(|c| c.0.version),
);
let validate = anim_changed || validated.should_validate(entity, &pre);
if has_filter {
apply_filter_params(
entity,
b,
values,
t.resolved_filter.as_mut(),
t.rnode,
validate,
dirt,
false,
);
}
if has_backdrop {
let mut backdrop = t
.resolved_backdrop
.as_mut()
.map(|m| m.reborrow().map_unchanged(|b| &mut b.0));
apply_filter_params(
entity,
b,
values,
backdrop.as_mut(),
t.rnode,
validate,
dirt,
true,
);
}
let post = (
t.resolved_filter.as_ref().map(|c| c.version),
t.resolved_backdrop.as_ref().map(|c| c.0.version),
);
validated.stamp(entity, validate, &pre, post);
}
fn stage_shape_attrs(
entity: Entity,
anim_changed: bool,
b: &AnimatedBindings,
values: &SharedValues,
shape_validated: &mut ValidationMemory<()>,
shape_bound: &mut Vec<Entity>,
t: &mut AnimTargetsItem,
) {
if !b.has_shape_attrs() {
return;
}
shape_bound.push(entity);
let validate = anim_changed || shape_validated.should_validate(entity, &());
shape::apply_shape_attrs(b, values, t.shape.as_mut(), t.rnode, validate);
shape_validated.stamp(entity, validate, &(), ());
}