use crate::animations::{AnimatedNode, build_ui_transform};
use bevy::ecs::query::QueryData;
use bevy::prelude::*;
use bevy::ui::{BackgroundGradient, BorderGradient, UiTransform};
use crate::protocol::units::Length;
use crate::style::Style;
use crate::ui_map::{length_to_val, rect_to_border_radius};
mod channels;
mod gradient_channel;
#[cfg(test)]
mod gradient_tests;
pub mod layout;
#[cfg(test)]
mod layout_shared_tests;
#[cfg(test)]
mod layout_tests;
mod scroll;
pub mod shared;
mod spec;
#[cfg(test)]
mod tests;
mod transform3d;
pub use scroll::{ScrollTransitionState, drive_scroll_transition};
#[allow(unused_imports)]
pub use scroll::ScrollTransitionInput;
pub use spec::{ChannelTransition, Transition, TransitionInput};
use channels::with_input_channels;
pub use channels::{Channel, TransitionState};
pub fn apply_transition(ec: &mut EntityCommands, style: Option<&Style>, fresh: bool) {
match (style.and_then(TransitionInput::from_style), fresh) {
(Some(input), true) => {
ec.insert((input, TransitionState::default()));
}
(Some(input), false) => {
ec.insert(input);
ec.insert_if_new(TransitionState::default());
}
(None, true) => {}
(None, false) => {
ec.remove::<TransitionInput>();
ec.remove::<TransitionState>();
}
}
}
#[derive(QueryData)]
#[query_data(mutable)]
pub struct TransitionTargets {
transform: &'static mut UiTransform,
bg: Option<&'static mut BackgroundColor>,
text: Option<&'static mut TextColor>,
image: Option<&'static mut ImageNode>,
node: Option<&'static mut Node>,
anim: Option<&'static AnimatedNode>,
promoted: Option<&'static crate::layer::PromotedLayer>,
layer_alpha: Option<&'static mut crate::layer::LayerGroupAlpha>,
filter_input: Option<&'static crate::filters::FilterInput>,
resolved_filter: Option<&'static mut crate::filters::ResolvedFilterChain>,
backdrop_input: Option<&'static crate::filters::BackdropInput>,
resolved_backdrop: Option<&'static mut crate::filters::ResolvedBackdropChain>,
morph_input: Option<&'static crate::filters::MorphInput>,
resolved_morph: Option<&'static crate::filters::ResolvedMorphChain>,
morph_state: Option<&'static mut crate::filters::MorphState>,
capture_rect: Option<&'static crate::layer::LayerCaptureRect>,
transform3d: Option<&'static mut crate::layer::transform3d::LayerTransform3d>,
gradient_input: Option<&'static crate::ui_map::GradientTargets>,
bg_gradient: Option<&'static mut BackgroundGradient>,
border_gradient: Option<&'static mut BorderGradient>,
shared_seed: Option<&'static shared::SharedSeed>,
}
type AnyTargetChanged = Or<(
Changed<TransitionInput>,
Changed<UiTransform>,
Changed<BackgroundColor>,
Changed<TextColor>,
Changed<ImageNode>,
Changed<Node>,
Changed<AnimatedNode>,
Changed<crate::layer::PromotedLayer>,
Changed<crate::layer::LayerGroupAlpha>,
Changed<crate::filters::FilterInput>,
Changed<crate::filters::ResolvedFilterChain>,
Changed<crate::filters::BackdropInput>,
Changed<crate::filters::ResolvedBackdropChain>,
Changed<crate::filters::MorphInput>,
Or<(
Changed<crate::filters::ResolvedMorphChain>,
Changed<crate::filters::MorphState>,
Changed<crate::layer::LayerCaptureRect>,
Changed<crate::layer::transform3d::LayerTransform3d>,
Changed<crate::ui_map::GradientTargets>,
Changed<BackgroundGradient>,
Changed<BorderGradient>,
Changed<shared::SharedSeed>,
)>,
)>;
#[derive(bevy::ecs::system::SystemParam)]
pub struct RemovedTargets<'w, 's> {
bg: RemovedComponents<'w, 's, BackgroundColor>,
text: RemovedComponents<'w, 's, TextColor>,
image: RemovedComponents<'w, 's, ImageNode>,
node: RemovedComponents<'w, 's, Node>,
anim: RemovedComponents<'w, 's, AnimatedNode>,
promoted: RemovedComponents<'w, 's, crate::layer::PromotedLayer>,
layer_alpha: RemovedComponents<'w, 's, crate::layer::LayerGroupAlpha>,
filter_input: RemovedComponents<'w, 's, crate::filters::FilterInput>,
resolved_filter: RemovedComponents<'w, 's, crate::filters::ResolvedFilterChain>,
backdrop_input: RemovedComponents<'w, 's, crate::filters::BackdropInput>,
resolved_backdrop: RemovedComponents<'w, 's, crate::filters::ResolvedBackdropChain>,
morph_input: RemovedComponents<'w, 's, crate::filters::MorphInput>,
resolved_morph: RemovedComponents<'w, 's, crate::filters::ResolvedMorphChain>,
morph_state: RemovedComponents<'w, 's, crate::filters::MorphState>,
capture_rect: RemovedComponents<'w, 's, crate::layer::LayerCaptureRect>,
transform3d: RemovedComponents<'w, 's, crate::layer::transform3d::LayerTransform3d>,
gradient_input: RemovedComponents<'w, 's, crate::ui_map::GradientTargets>,
bg_gradient: RemovedComponents<'w, 's, BackgroundGradient>,
border_gradient: RemovedComponents<'w, 's, BorderGradient>,
}
impl RemovedTargets<'_, '_> {
crate::animations::removed_any!(
bg,
text,
image,
node,
anim,
promoted,
layer_alpha,
filter_input,
resolved_filter,
backdrop_input,
resolved_backdrop,
morph_input,
resolved_morph,
morph_state,
capture_rect,
transform3d,
gradient_input,
bg_gradient,
border_gradient
);
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn drive_transitions(
time: Res<Time>,
mut commands: Commands,
mut dirt: ResMut<crate::layer::LayerContentDirt>,
filter_registry: Option<Res<crate::filters::FilterRegistry>>,
assets: Option<Res<AssetServer>>,
mut queries: ParamSet<(
Query<Entity, (With<TransitionState>, AnyTargetChanged)>,
Query<(Entity, &TransitionState)>,
Query<(
Entity,
&TransitionInput,
&mut TransitionState,
TransitionTargets,
)>,
)>,
mut removed: RemovedTargets,
mut woken: Local<Vec<Entity>>,
) {
let dt = time.delta_secs();
woken.clear();
let all = removed.any();
for (entity, state) in &queries.p1() {
if all || !state.settled {
woken.push(entity);
}
}
if !all {
woken.extend(&queries.p0());
woken.sort_unstable();
woken.dedup();
}
let mut query = queries.p2();
for entity in woken.drain(..) {
let Ok((entity, input, mut state, mut targets)) = query.get_mut(entity) else {
continue;
};
if !state.initialized {
macro_rules! seed {
($(($ch:ident, $d:tt, $group:ident),)*) => {
$(state.$ch.init(input.$ch.unwrap_or($d));)*
};
}
with_input_channels!(seed);
if let Some(c) = input.background_color {
state.color.init(c);
}
state.filter.wire = targets
.filter_input
.map(|f| f.0.clone())
.unwrap_or_default();
state.filter.channel.init(
targets
.resolved_filter
.as_deref()
.map(|c| c.passes.clone())
.unwrap_or_default(),
);
state.backdrop_filter.wire = targets
.backdrop_input
.map(|f| f.0.clone())
.unwrap_or_default();
state.backdrop_filter.channel.init(
targets
.resolved_backdrop
.as_deref()
.map(|c| c.0.passes.clone())
.unwrap_or_default(),
);
state
.transform3d
.init(&input.transform3d.clone().unwrap_or_default());
state
.background_gradient
.seed(targets.gradient_input.and_then(|g| g.background.as_ref()));
state
.border_gradient
.seed(targets.gradient_input.and_then(|g| g.border.as_ref()));
state.morph.key = targets.morph_input.map(|m| m.key.clone());
state.initialized = true;
if let Some(seed) = targets.shared_seed {
if let Some(spec) = input.spec.shared_element.as_ref() {
state.seed_from(seed, spec.clone());
}
commands.entity(entity).remove::<shared::SharedSeed>();
}
}
let shared_spec = state
.shared
.active
.then(|| state.shared.spec.clone())
.flatten();
let shared_spec = shared_spec.as_ref();
let seed_frame = state.shared.seed_frame;
use crate::animations::props::ChannelId;
let parked = |c: ChannelId| targets.anim.is_some_and(|a| a.0.parked(c));
let skip_transform = parked(ChannelId::Transform);
let skip_opacity = parked(ChannelId::Opacity);
let skip_bg = parked(ChannelId::Background);
let skip_radius = parked(ChannelId::BorderRadius);
let skip_filter = parked(ChannelId::Filter);
let skip_backdrop = parked(ChannelId::Backdrop);
let skip_transform3d = parked(ChannelId::Transform3d);
let skip_bg_gradient = parked(ChannelId::BackgroundGradient);
let skip_border_gradient = parked(ChannelId::BorderGradient);
macro_rules! sync_row {
(transform, $ch:ident, $d:tt, transform) => {
state.$ch.init(input.$ch.unwrap_or($d));
};
(size, $ch:ident, $d:tt, size) => {
state.$ch.init(input.$ch.unwrap_or($d));
};
(radius, $ch:ident, $d:tt, radius) => {
state.$ch.init(input.$ch.unwrap_or($d));
};
($want:ident, $ch:ident, $d:tt, $group:ident) => {};
}
macro_rules! sync_transform_rows {
($(($ch:ident, $d:tt, $group:ident),)*) => { $(sync_row!(transform, $ch, $d, $group);)* };
}
macro_rules! sync_size_rows {
($(($ch:ident, $d:tt, $group:ident),)*) => { $(sync_row!(size, $ch, $d, $group);)* };
}
macro_rules! sync_radius_rows {
($(($ch:ident, $d:tt, $group:ident),)*) => { $(sync_row!(radius, $ch, $d, $group);)* };
}
if input.spec.transform.as_ref().or(shared_spec).is_some() && !skip_transform {
let s = arm_spec(seed_frame, input.spec.transform.as_ref(), shared_spec);
let tx = input
.translate_x
.map(|t| length_to_val(state.translate_x.drive(t, s, dt)));
let ty = input
.translate_y
.map(|t| length_to_val(state.translate_y.drive(t, s, dt)));
let sc = input.scale.map(|t| state.scale.drive(t, s, dt));
let scx = input.scale_x.map(|t| state.scale_x.drive(t, s, dt));
let scy = input.scale_y.map(|t| state.scale_y.drive(t, s, dt));
let rot = input.rotate.map(|t| state.rotate.drive(t, s, dt));
let new = build_ui_transform(tx, ty, sc, scx, scy, rot);
if *targets.transform != new {
crate::animations::push_transform_dirt(
entity,
&targets.transform,
&new,
targets.promoted.is_some(),
&mut dirt,
);
*targets.transform = new;
}
} else if !skip_transform {
with_input_channels!(sync_transform_rows);
}
if input.spec.transform3d.as_ref().or(shared_spec).is_some()
&& !skip_transform3d
&& let Some(target) = &input.transform3d
&& let Some(t3d) = &mut targets.transform3d
{
let new = state.transform3d.drive(
target,
arm_spec(seed_frame, input.spec.transform3d.as_ref(), shared_spec),
dt,
);
if t3d.0 != new {
t3d.0 = new;
}
} else if !skip_transform3d {
state
.transform3d
.init(&input.transform3d.clone().unwrap_or_default());
}
let alpha = if !skip_opacity && let Some(target) = input.opacity {
Some(state.opacity.drive(
target,
arm_spec(seed_frame, input.spec.opacity.as_ref(), shared_spec),
dt,
))
} else {
None
};
let promoted = targets.promoted.is_some();
if !skip_bg && let Some(target) = input.background_color {
if state.shared.active && state.color.runner.is_none() && state.color.current[3] == 0.0
{
state.color.init([target[0], target[1], target[2], 0.0]);
}
let mut rgba = state.color.drive(
target,
arm_spec(
seed_frame,
input.spec.background_color.as_ref(),
shared_spec,
),
dt,
);
if let Some(a) = alpha
&& !promoted
{
rgba[3] = a;
}
let color = rgba_to_color(rgba);
match &mut targets.bg {
Some(c) if c.0 != color => {
c.0 = color;
dirt.nodes.push(entity);
}
Some(_) => {}
None => {
commands.entity(entity).insert(BackgroundColor(color));
dirt.nodes.push(entity);
}
}
}
if let Some(alpha) = alpha {
crate::animations::write_final_alpha(
entity,
alpha,
promoted,
targets.layer_alpha.as_mut(),
targets.bg.as_mut(),
targets.text.as_mut(),
targets.image.as_mut(),
&mut dirt,
);
}
if state.shared.size.is_some()
&& let Some(node) = targets.node.as_mut()
{
state.drive_shared_size(node, input, dt);
}
let flying = state.shared.size.unwrap_or_default();
if input.spec.size.is_some()
&& let Some(node) = targets.node.as_mut()
{
let s = input.spec.size.as_ref();
macro_rules! size_drive {
($ch:ident) => {
if let Some(t) = input.$ch {
let v = length_to_val(state.$ch.drive(t, s, dt));
if node.$ch != v {
node.$ch = v;
}
}
};
}
macro_rules! size_rule {
(width, $d:tt, size) => {
if !flying.width {
size_drive!(width);
}
};
(height, $d:tt, size) => {
if !flying.height {
size_drive!(height);
}
};
($ch:ident, $d:tt, size) => {
size_drive!($ch);
};
($ch:ident, $d:tt, $other:ident) => {};
}
macro_rules! size_walk {
($(($ch:ident, $d:tt, $group:ident),)*) => {
$(size_rule!($ch, $d, $group);)*
};
}
with_input_channels!(size_walk);
} else if state.shared.size.is_none() {
with_input_channels!(sync_size_rows);
}
if input.spec.border_radius.as_ref().or(shared_spec).is_some()
&& !skip_radius
&& let Some(node) = targets.node.as_mut()
{
let target = input.border_radius.unwrap_or_default();
let r = rect_to_border_radius(state.border_radius.drive(
target,
arm_spec(seed_frame, input.spec.border_radius.as_ref(), shared_spec),
dt,
));
if node.border_radius != r {
node.border_radius = r;
dirt.nodes.push(entity);
}
} else if !skip_radius {
with_input_channels!(sync_radius_rows);
}
if !skip_filter
&& state.filter.drive(
targets.filter_input.map(|f| &f.0),
targets.resolved_filter.as_mut().map(Mut::reborrow),
arm_spec(
seed_frame,
input.spec.resolve(ChannelId::Filter),
shared_spec,
),
filter_registry.as_deref(),
assets.as_deref(),
dt,
)
{
dirt.composite_only.push(entity);
}
if !skip_backdrop
&& state.backdrop_filter.drive(
targets.backdrop_input.map(|f| &f.0),
targets
.resolved_backdrop
.as_mut()
.map(|m| m.reborrow().map_unchanged(|b| &mut b.0)),
arm_spec(
seed_frame,
input.spec.resolve(ChannelId::Backdrop),
shared_spec,
),
filter_registry.as_deref(),
assets.as_deref(),
dt,
)
{
dirt.composite_only.push(entity);
}
{
let eased_alpha = alpha.filter(|_| !promoted);
let static_fold = targets.gradient_input.and_then(|g| g.opacity);
if !skip_bg_gradient
&& state.background_gradient.drive_onto(
targets.gradient_input.and_then(|g| g.background.as_ref()),
targets
.bg_gradient
.as_mut()
.map(|m| m.reborrow().map_unchanged(|b| &mut b.0)),
arm_spec(
seed_frame,
input.spec.resolve(ChannelId::BackgroundGradient),
shared_spec,
),
eased_alpha,
static_fold,
dt,
)
{
dirt.nodes.push(entity);
}
if !skip_border_gradient
&& state.border_gradient.drive_onto(
targets.gradient_input.and_then(|g| g.border.as_ref()),
targets
.border_gradient
.as_mut()
.map(|m| m.reborrow().map_unchanged(|b| &mut b.0)),
arm_spec(
seed_frame,
input.spec.resolve(ChannelId::BorderGradient),
shared_spec,
),
eased_alpha,
static_fold,
dt,
)
{
dirt.nodes.push(entity);
}
}
{
use channels::MorphAction;
let morph_spec = input
.spec
.morph_filter
.as_ref()
.unwrap_or_else(|| spec::morph_default());
match state.morph.drive(
targets.morph_input,
targets.resolved_morph.is_some(),
targets.capture_rect,
morph_spec,
dt,
) {
MorphAction::Freeze(new) => {
match &mut targets.morph_state {
Some(s) if **s != new => **s = new,
Some(_) => {}
None => {
commands.entity(entity).insert(new);
}
}
dirt.nodes.push(entity);
}
MorphAction::Progress(p) => {
if let Some(s) = &mut targets.morph_state
&& s.progress != p
{
s.progress = p;
dirt.composite_only.push(entity);
}
}
MorphAction::Deactivate => {
if let Some(s) = &mut targets.morph_state
&& s.active
{
s.active = false;
}
}
MorphAction::None => {}
}
}
if state.shared.seed_frame {
state.shared.armed = state.running_mask();
state.shared.seed_frame = false;
} else if state.shared.active && !state.seeded_still_running() {
state.shared.active = false;
}
let settled = !state.in_flight();
if state.settled != settled {
state.settled = settled;
}
}
}
pub(super) fn color_to_rgba(color: Color) -> [f32; 4] {
let s = color.to_srgba();
[s.red, s.green, s.blue, s.alpha]
}
fn arm_spec<'a>(
seed_frame: bool,
own: Option<&'a ChannelTransition>,
shared: Option<&'a ChannelTransition>,
) -> Option<&'a ChannelTransition> {
if seed_frame { own.or(shared) } else { own }
}
fn rgba_to_color(rgba: [f32; 4]) -> Color {
Color::srgba(rgba[0], rgba[1], rgba[2], rgba[3])
}