use crate::animations::{AnimatedNode, build_ui_transform};
use bevy::ecs::query::QueryData;
use bevy::prelude::*;
use bevy::ui::UiTransform;
use crate::protocol::{style::Style, units::Length};
use crate::ui_map::length_to_val;
mod channels;
mod scroll;
mod shape_channel;
mod spec;
#[cfg(test)]
mod tests;
mod transform3d;
pub use scroll::{ScrollTransitionState, apply_scroll_transition, drive_scroll_transition};
#[allow(unused_imports)]
pub use scroll::ScrollTransitionInput;
pub use spec::{ChannelTransition, Transition, TransitionInput};
pub use channels::TransitionState;
use channels::with_input_channels;
pub fn apply_transition(ec: &mut EntityCommands, style: &Option<Style>) {
match style.as_ref().and_then(TransitionInput::from_style) {
Some(input) => {
ec.insert(input);
ec.insert_if_new(TransitionState::default());
}
None => {
ec.remove::<TransitionInput>();
ec.remove::<TransitionState>();
}
}
}
pub fn apply_shape_transition(ec: &mut EntityCommands, attrs: Option<&crate::svg::ShapeAttrs>) {
match attrs.is_some_and(|a| a.transition.is_some()) {
true => {
ec.insert_if_new(TransitionInput::default());
ec.insert_if_new(TransitionState::default());
}
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>,
transform3d: Option<&'static mut crate::layer::transform3d::LayerTransform3d>,
shape: Option<&'static mut crate::svg::SvgShape>,
}
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 query: Query<(
Entity,
&TransitionInput,
&mut TransitionState,
TransitionTargets,
)>,
) {
let dt = time.delta_secs();
for (entity, input, mut state, mut targets) in &mut query {
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.initialized = true;
}
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_filter = parked(ChannelId::Filter);
let skip_backdrop = parked(ChannelId::Backdrop);
let skip_transform3d = parked(ChannelId::Transform3d);
if input.spec.for_transform().is_some() && !skip_transform {
let s = input.spec.for_transform();
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;
}
}
if input.spec.for_transform3d().is_some()
&& !skip_transform3d
&& let Some(target) = &input.transform3d
&& let Some(t3d) = &mut targets.transform3d
{
let new = state
.transform3d
.drive(target, input.spec.for_transform3d(), dt);
if t3d.0 != new {
t3d.0 = new;
}
}
let alpha = if !skip_opacity && let Some(target) = input.opacity {
Some(state.opacity.drive(target, input.spec.for_opacity(), dt))
} else {
None
};
let promoted = targets.promoted.is_some();
if !skip_bg && let Some(target) = input.background_color {
let mut rgba = state.color.drive(target, input.spec.for_background(), 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
&& promoted
{
if let Some(la) = &mut targets.layer_alpha
&& la.0 != alpha
{
la.0 = alpha;
dirt.composite_only.push(entity);
}
} else if let Some(alpha) = alpha {
let mut wrote = false;
if let Some(c) = &mut targets.bg
&& c.0.alpha() != alpha
{
c.0 = c.0.with_alpha(alpha);
wrote = true;
}
if let Some(tc) = &mut targets.text
&& tc.0.alpha() != alpha
{
tc.0 = tc.0.with_alpha(alpha);
wrote = true;
}
if let Some(img) = &mut targets.image
&& img.color.alpha() != alpha
{
img.color = img.color.with_alpha(alpha);
wrote = true;
}
if wrote {
dirt.nodes.push(entity);
}
}
if input.spec.for_size().is_some()
&& let Some(node) = targets.node.as_mut()
{
let s = input.spec.for_size();
macro_rules! size_rule {
($ch:ident, $d:tt, size) => {
if let Some(t) = input.$ch {
let v = length_to_val(state.$ch.drive(t, s, dt));
if node.$ch != v {
node.$ch = v;
}
}
};
($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);
}
if !skip_filter
&& state.filter.drive(
targets.filter_input.map(|f| &f.0),
targets.resolved_filter.as_mut().map(Mut::reborrow),
input.spec.resolve(ChannelId::Filter),
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)),
input.spec.resolve(ChannelId::Backdrop),
filter_registry.as_deref(),
assets.as_deref(),
dt,
)
{
dirt.composite_only.push(entity);
}
if let Some(shape) = &mut targets.shape {
if targets.anim.is_some_and(|a| a.0.has_shape_attrs()) {
state.shape.reset();
} else {
state.shape.drive(shape, dt);
}
}
}
}
fn color_to_rgba(color: Color) -> [f32; 4] {
let s = color.to_srgba();
[s.red, s.green, s.blue, s.alpha]
}
fn rgba_to_color(rgba: [f32; 4]) -> Color {
Color::srgba(rgba[0], rgba[1], rgba[2], rgba[3])
}