use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform, UiTransform};
use super::spec::ChannelTransition;
use super::{TransitionInput, TransitionState, color_to_rgba};
use crate::protocol::{NodeId, units::Length};
use crate::ui_map::length_to_val;
#[derive(Default)]
pub(super) struct SharedFlight {
pub active: bool,
pub spec: Option<ChannelTransition>,
pub seed_frame: bool,
pub armed: u32,
pub rect: Option<SharedRect>,
pub origin: Option<SharedRect>,
pub destination: Option<SharedRect>,
pub reflow: Option<SharedReflow>,
pub size: Option<SharedSizeFlight>,
}
#[derive(Debug, Clone, PartialEq)]
pub(super) struct SharedReflow {
pub anchor: Vec2,
pub natural: Vec2,
fit: Option<ReflowFit>,
}
#[derive(Debug, Clone, PartialEq)]
struct ReflowFit {
displacement: Vec2,
own_step: Vec2,
ancestor_steps: Vec<(Entity, Vec2)>,
norm_sq: f32,
}
const FIT_MIN_STEP: f32 = super::layout::LAYOUT_SNAP_EPSILON;
impl SharedReflow {
pub fn new(anchor: Vec2, natural: Vec2) -> Self {
Self {
anchor,
natural,
fit: None,
}
}
pub fn observe(&mut self, center: Vec2, size: Vec2, ancestors: &[(Entity, Vec2)]) -> Vec2 {
let own = size - self.natural;
let displacement = center - self.anchor;
let fit = match &self.fit {
Some(f) => f,
None => {
let norm_sq = own.length_squared()
+ ancestors
.iter()
.map(|(_, s)| s.length_squared())
.sum::<f32>();
if norm_sq < FIT_MIN_STEP * FIT_MIN_STEP {
return displacement;
}
self.fit.insert(ReflowFit {
displacement,
own_step: own,
ancestor_steps: ancestors.to_vec(),
norm_sq,
})
}
};
let mut along = own.dot(fit.own_step);
for (e, step) in &fit.ancestor_steps {
let live = ancestors
.iter()
.find(|(a, _)| a == e)
.map(|(_, s)| *s)
.unwrap_or(Vec2::ZERO);
along += live.dot(*step);
}
let external = displacement - fit.displacement * (along / fit.norm_sq);
let quiet = |v: f32| if v.abs() < FIT_MIN_STEP { 0.0 } else { v };
Vec2::new(quiet(external.x), quiet(external.y))
}
}
impl SharedRect {
pub fn shifted(self, delta: Vec2) -> Self {
Self {
center: self.center + delta,
size: self.size,
}
}
}
#[derive(Default, Clone, Copy)]
pub(super) struct SharedSizeFlight {
pub width: bool,
pub height: bool,
pub flex: Option<(f32, f32)>,
}
const SIZE_SNAP_EPSILON: f32 = super::layout::LAYOUT_SNAP_EPSILON;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SharedRect {
pub center: Vec2,
pub size: Vec2,
}
#[derive(Component)]
pub struct SharedSeed {
pub(super) state: Box<TransitionState>,
pub rect: SharedRect,
}
#[cfg(test)]
impl SharedSeed {
pub(crate) fn opacity_current(&self) -> f32 {
self.state.opacity.current
}
pub(crate) fn scale_current(&self) -> f32 {
self.state.scale.current
}
}
#[derive(Resource, Default)]
pub struct PendingSharedSeeds(pub(crate) std::collections::HashMap<NodeId, SharedSeed>);
pub(crate) fn snapshot(world: &World, outgoing: Entity) -> Option<SharedSeed> {
let entity = world.get_entity(outgoing).ok()?;
let computed = entity.get::<ComputedNode>()?;
let global = entity.get::<UiGlobalTransform>()?;
let m = global.matrix2;
let own_scale = entity
.get::<UiTransform>()
.map(|t| t.scale)
.filter(|s| s.x.abs() > f32::EPSILON && s.y.abs() > f32::EPSILON)
.unwrap_or(Vec2::ONE);
let rect = SharedRect {
center: global.translation,
size: computed.size * Vec2::new(m.x_axis.length(), m.y_axis.length()) / own_scale,
};
let mut state = match entity.get::<TransitionState>() {
Some(s) => s.seeded_copy(),
None => {
let mut s = TransitionState::at_identity();
if let Some(t) = entity.get::<UiTransform>() {
let len = |v: Val| match v {
Val::Px(x) => Some(Length::Px(x)),
Val::Percent(x) => Some(Length::Percent(x)),
_ => None,
};
if let Some(x) = len(t.translation.x) {
s.translate_x.init(x);
}
if let Some(y) = len(t.translation.y) {
s.translate_y.init(y);
}
s.scale_x.init(t.scale.x);
s.scale_y.init(t.scale.y);
s.rotate.init(t.rotation.as_radians());
}
s
}
};
if let Some(bg) = entity.get::<BackgroundColor>() {
state.color.init(color_to_rgba(bg.0));
}
Some(SharedSeed {
state: Box::new(state),
rect,
})
}
pub(crate) fn snapshot_into_pending(world: &mut World, outgoing: Entity, incoming: NodeId) {
if let Some(seed) = snapshot(world, outgoing) {
world
.get_resource_or_init::<PendingSharedSeeds>()
.0
.insert(incoming, seed);
}
}
pub(crate) fn discard_pending(world: &mut World, incoming: NodeId) {
if let Some(mut p) = world.get_resource_mut::<PendingSharedSeeds>() {
p.0.remove(&incoming);
}
}
pub(crate) fn clear_pending(world: &mut World) {
if let Some(mut p) = world.get_resource_mut::<PendingSharedSeeds>() {
p.0.clear();
}
}
pub(crate) fn stamp_pending(world: &mut World, incoming: NodeId, entity: Entity) {
let Some(seed) = world
.get_resource_mut::<PendingSharedSeeds>()
.and_then(|mut p| p.0.remove(&incoming))
else {
return;
};
let Ok(mut em) = world.get_entity_mut(entity) else {
return;
};
if em
.get::<TransitionInput>()
.is_some_and(|i| i.spec.shared_element.is_some())
{
em.insert(seed);
}
}
fn copy_value_channels(to: &mut TransitionState, from: &TransitionState) {
macro_rules! row {
($ch:ident, size) => {};
($ch:ident, $group:ident) => {
to.$ch.init(from.$ch.current);
};
}
macro_rules! rows {
($(($ch:ident, $d:tt, $group:ident),)*) => {
$(row!($ch, $group);)*
};
}
super::channels::with_input_channels!(rows);
to.color.init(from.color.current);
to.filter.seed_from(&from.filter);
to.backdrop_filter.seed_from(&from.backdrop_filter);
to.background_gradient.seed_from(&from.background_gradient);
to.border_gradient.seed_from(&from.border_gradient);
to.transform3d.seed_from(&from.transform3d);
}
impl TransitionState {
pub(super) fn at_identity() -> TransitionState {
let mut s = TransitionState::default();
macro_rules! rows {
($(($ch:ident, $d:tt, $group:ident),)*) => {
$(s.$ch.init($d);)*
};
}
super::channels::with_input_channels!(rows);
s
}
pub(super) fn seeded_copy(&self) -> TransitionState {
let mut s = TransitionState::default();
copy_value_channels(&mut s, self);
s
}
pub(super) fn seed_from(&mut self, seed: &SharedSeed, spec: ChannelTransition) {
copy_value_channels(self, &seed.state);
self.shared = SharedFlight {
active: true,
spec: Some(spec),
seed_frame: true,
armed: 0,
rect: Some(seed.rect),
origin: None,
destination: None,
reflow: None,
size: None,
};
}
pub(super) fn running_mask(&self) -> u32 {
let mut mask = 0u32;
let mut bit = 0u32;
macro_rules! push {
($cond:expr) => {
if $cond {
mask |= 1 << bit;
}
bit += 1;
};
}
macro_rules! row {
($ch:ident, size) => {};
($ch:ident, $group:ident) => {
push!(self.$ch.runner.is_some());
};
}
macro_rules! rows {
($(($ch:ident, $d:tt, $group:ident),)*) => {
$(row!($ch, $group);)*
};
}
super::channels::with_input_channels!(rows);
push!(self.color.runner.is_some());
push!(self.filter.channel.runner.is_some());
push!(self.backdrop_filter.channel.runner.is_some());
push!(self.background_gradient.in_flight());
push!(self.border_gradient.in_flight());
push!(self.transform3d.in_flight());
let _ = bit;
mask
}
pub(super) fn seeded_still_running(&self) -> bool {
(self.shared.armed & self.running_mask()) != 0
|| self.shared.size.is_some()
|| self.shared.rect.is_some()
|| self.layout.shared_active()
}
pub(super) fn arm_shared_size(
&mut self,
seed_px: [f32; 2],
natural_px: [f32; 2],
inverse_scale_factor: f32,
spec: &ChannelTransition,
) {
let flying = |i: usize| {
natural_px[i] > 0.0 && (seed_px[i] - natural_px[i]).abs() >= SIZE_SNAP_EPSILON
};
let flight = SharedSizeFlight {
width: flying(0),
height: flying(1),
flex: None,
};
macro_rules! arm {
($ch:ident, $i:expr) => {
self.$ch
.init(Length::Px(seed_px[$i] * inverse_scale_factor));
self.$ch
.arm(Length::Px(natural_px[$i] * inverse_scale_factor), spec);
};
}
if flight.width {
arm!(width, 0);
}
if flight.height {
arm!(height, 1);
}
if flight.width || flight.height {
self.shared.size = Some(flight);
}
}
pub(super) fn drive_shared_size(&mut self, node: &mut Node, input: &TransitionInput, dt: f32) {
let Some(mut flight) = self.shared.size else {
return;
};
let (mut grow, mut shrink) = flight.flex.unwrap_or((node.flex_grow, node.flex_shrink));
if node.flex_grow != 0.0 {
grow = node.flex_grow;
node.flex_grow = 0.0;
}
if node.flex_shrink != 0.0 {
shrink = node.flex_shrink;
node.flex_shrink = 0.0;
}
flight.flex = Some((grow, shrink));
macro_rules! axis {
($flag:ident, $ch:ident) => {
if flight.$flag {
let v = match self.$ch.tick(dt) {
Some(false) => length_to_val(self.$ch.current),
_ => {
let authored = input.$ch.unwrap_or(Length::Auto);
self.$ch.init(authored);
flight.$flag = false;
length_to_val(authored)
}
};
if node.$ch != v {
node.$ch = v;
}
}
};
}
axis!(width, width);
axis!(height, height);
if flight.width || flight.height {
self.shared.size = Some(flight);
} else {
if node.flex_grow != grow {
node.flex_grow = grow;
}
if node.flex_shrink != shrink {
node.flex_shrink = shrink;
}
self.shared.size = None;
}
}
}