use bevy::ecs::entity::{EntityHashMap, EntityHashSet};
use bevy::math::Affine2;
use bevy::prelude::*;
use bevy::ui::ui_surface::UiSurface;
use bevy::ui::{ComputedNode, LayoutConfig, UiGlobalTransform};
use super::channels::{ProgressChannel, SIZE_ROWS};
use super::shared::{SharedRect, SharedReflow};
use super::spec::ChannelTransition;
use super::{TransitionInput, TransitionState};
use crate::animations::AnimatedNode;
pub type LayoutRect = [f32; 4];
pub const LAYOUT_SNAP_EPSILON: f32 = 0.5;
pub const SIZE_STEP_SLACK: f32 = 4.0;
const MIN_SHOWN_PX: f32 = 0.01;
#[derive(Default)]
pub struct LayoutChannel {
channel: ProgressChannel<LayoutRect>,
seeded: bool,
adopt_tail: RectWriter,
own_size: [Option<f32>; SIZE_ROWS],
own_step: Option<f32>,
shared: SharedMode,
hold: bool,
}
#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
pub enum RectWriter {
#[default]
None,
SizeChannel,
Binding,
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum SharedMode {
#[default]
Off,
SeedFrame,
TranslateOnly,
Landed,
}
impl LayoutChannel {
pub fn seed_shared(
&mut self,
seed: LayoutRect,
measured: LayoutRect,
spec: &ChannelTransition,
) {
self.seeded = true;
self.adopt_tail = RectWriter::None;
let within_epsilon = (0..4).all(|i| (seed[i] - measured[i]).abs() < LAYOUT_SNAP_EPSILON);
let to_zero = measured[2] <= 0.0 || measured[3] <= 0.0;
if within_epsilon || to_zero {
self.channel.init(measured);
self.shared = SharedMode::Off;
return;
}
self.channel.init(seed);
self.channel.arm(measured, spec);
self.shared = SharedMode::SeedFrame;
}
pub fn shared_active(&self) -> bool {
self.shared != SharedMode::Off
}
pub fn on_seed_frame(&self) -> bool {
self.shared == SharedMode::SeedFrame
}
pub fn rebase_shared(&mut self, seed: LayoutRect) {
if self.shared != SharedMode::Off {
self.channel.rebase(seed);
}
}
pub fn hold_shared(&mut self, hold: bool) {
self.hold = hold;
}
fn drive_shared(
&mut self,
destination: LayoutRect,
measured: LayoutRect,
dt: f32,
) -> Option<LayoutRect> {
if destination != self.channel.target {
self.channel.target = destination;
}
let seed_frame = self.shared == SharedMode::SeedFrame;
let landed = self.shared == SharedMode::Landed;
if landed {
self.channel.init(destination);
}
self.shared = SharedMode::TranslateOnly;
let settled = landed || !matches!(self.channel.tick(dt), Some(false));
if settled {
if self.hold {
self.shared = SharedMode::Landed;
self.channel.init(destination);
return Some([destination[0], destination[1], measured[2], measured[3]]);
}
self.shared = SharedMode::Off;
self.adopt_tail = RectWriter::SizeChannel;
let c = self.channel.current;
return (c != measured).then_some(c);
}
let c = self.channel.current;
Some(if seed_frame {
c
} else {
[c[0], c[1], measured[2], measured[3]]
})
}
pub fn drive(
&mut self,
measured: LayoutRect,
destination: LayoutRect,
spec: &ChannelTransition,
writer: RectWriter,
dt: f32,
) -> Option<LayoutRect> {
if self.shared != SharedMode::Off {
return self.drive_shared(destination, measured, dt);
}
let last = std::mem::replace(&mut self.adopt_tail, writer);
if !self.seeded {
self.channel.init(measured);
self.seeded = true;
return None;
}
let owner = if writer == RectWriter::None {
last
} else {
writer
};
match owner {
RectWriter::Binding => {
self.channel.init(measured);
return None;
}
RectWriter::SizeChannel => return self.follow_size_flight(measured, spec, dt),
RectWriter::None => {}
}
if measured != self.channel.target {
let old = self.channel.target;
let to_zero = measured[2] <= 0.0 || measured[3] <= 0.0;
let from_zero = old[2] <= 0.0 || old[3] <= 0.0;
let within_epsilon = (0..4).all(|i| (measured[i] - old[i]).abs() < LAYOUT_SNAP_EPSILON);
if to_zero {
self.channel.init(measured);
} else if within_epsilon {
if self.in_flight() {
self.channel.target = measured;
} else {
self.channel.init(measured);
}
} else {
if from_zero {
self.channel.init([measured[0], measured[1], 0.0, 0.0]);
}
self.channel.arm(measured, spec);
}
}
self.channel.tick(dt);
(self.channel.current != self.channel.target).then_some(self.channel.current)
}
pub fn note_own_size(&mut self, now: [Option<f32>; SIZE_ROWS]) {
self.own_step = now
.iter()
.zip(&self.own_size)
.filter_map(|(a, b)| Some(((*a)? - (*b)?).abs()))
.reduce(f32::max);
self.own_size = now;
}
fn follow_size_flight(
&mut self,
measured: LayoutRect,
spec: &ChannelTransition,
dt: f32,
) -> Option<LayoutRect> {
if self.in_flight() {
self.channel.target = measured;
} else {
let old = self.channel.current;
let size_step = self.own_step.unwrap_or_else(|| {
(measured[2] - old[2])
.abs()
.max((measured[3] - old[3]).abs())
});
let explained = SIZE_STEP_SLACK * size_step + LAYOUT_SNAP_EPSILON;
let jump = (measured[0] - old[0]).abs() > explained
|| (measured[1] - old[1]).abs() > explained;
let any_zero = [old, measured].iter().any(|r| r[2] <= 0.0 || r[3] <= 0.0);
if !jump || any_zero {
self.channel.init(measured);
return None;
}
self.channel.arm(measured, spec);
}
self.channel.tick(dt);
let c = self.channel.current;
(c != measured).then_some([c[0], c[1], measured[2], measured[3]])
}
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn in_flight(&self) -> bool {
self.channel.runner.is_some()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutDelta {
pub translation: Vec2,
pub scale: Vec2,
pub root_anchored: bool,
pub seed_frame: bool,
}
impl LayoutDelta {
pub fn between(shown: LayoutRect, laid_out: LayoutRect) -> Self {
Self {
translation: Vec2::new(shown[0] - laid_out[0], shown[1] - laid_out[1]),
scale: Vec2::new(
shown[2].max(MIN_SHOWN_PX) / laid_out[2],
shown[3].max(MIN_SHOWN_PX) / laid_out[3],
),
root_anchored: false,
seed_frame: false,
}
}
}
fn seed_in_parent_space(
rect: SharedRect,
own: Affine2,
parent: Affine2,
measured: LayoutRect,
) -> Option<LayoutRect> {
if parent.matrix2.determinant().abs() <= f32::EPSILON {
return None;
}
let offset = parent.inverse().matrix2 * (rect.center - own.translation);
let parent_scale = Vec2::new(
parent.matrix2.x_axis.length(),
parent.matrix2.y_axis.length(),
);
Some([
measured[0] + offset.x,
measured[1] + offset.y,
rect.size.x / parent_scale.x,
rect.size.y / parent_scale.y,
])
}
fn compensate_seed_frame_radius(computed: &mut Mut<ComputedNode>, scale: Vec2) {
let s = 0.5 * (scale.x + scale.y);
if s <= f32::EPSILON || (s - 1.0).abs() < 1e-6 {
return;
}
let max = 0.5 * computed.size.min_element();
let fix = |r: f32| (r / s).clamp(0.0, max);
let radius = &mut computed.bypass_change_detection().border_radius;
radius.top_left = fix(radius.top_left);
radius.top_right = fix(radius.top_right);
radius.bottom_right = fix(radius.bottom_right);
radius.bottom_left = fix(radius.bottom_left);
}
type LayoutNodeQuery = (
Entity,
&'static TransitionInput,
&'static mut TransitionState,
Option<&'static AnimatedNode>,
Option<&'static mut ComputedNode>,
);
#[allow(clippy::too_many_arguments)]
pub fn drive_layout_transitions(
time: Res<Time>,
mut ui_surface: ResMut<UiSurface>,
mut states: Query<LayoutNodeQuery>,
parents: Query<&ChildOf>,
children: Query<&Children>,
mut globals: Query<&mut UiGlobalTransform>,
layout_configs: Query<&LayoutConfig>,
mut last_animating: Local<EntityHashSet>,
) {
let dt = time.delta_secs();
let mut deltas: EntityHashMap<LayoutDelta> = EntityHashMap::default();
let flying: EntityHashMap<Vec2> = states
.iter()
.filter_map(|(e, _, state, _, computed)| {
let r = state.shared.reflow.as_ref()?;
Some((e, computed?.size - r.natural))
})
.collect();
let inverse = |a: Affine2| (a.matrix2.determinant().abs() > f32::EPSILON).then(|| a.inverse());
for (entity, input, mut state, anim, mut computed) in &mut states {
let shared_spec = state.shared.spec.clone();
let shared_spec = shared_spec.as_ref();
let spec = input.spec.layout.as_ref().or_else(|| {
(state.shared.rect.is_some() || state.layout.shared_active())
.then_some(shared_spec)
.flatten()
});
let Some(spec) = spec else {
if state.layout.seeded {
state.layout.reset();
}
state.shared.origin = None;
state.shared.destination = None;
state.shared.reflow = None;
continue;
};
let use_rounding = layout_configs.is_empty()
|| std::iter::once(entity)
.chain(parents.iter_ancestors(entity))
.find_map(|e| layout_configs.get(e).ok())
.is_none_or(|c| c.use_rounding);
let Ok((layout, _unrounded)) = ui_surface.get_layout(entity, use_rounding) else {
continue;
};
let size = Vec2::new(layout.size.width, layout.size.height);
let measured = [
layout.location.x + size.x * 0.5,
layout.location.y + size.y * 0.5,
size.x,
size.y,
];
let own = globals
.get(entity)
.map(|g| **g)
.unwrap_or(Affine2::IDENTITY);
let parent = parents
.get(entity)
.ok()
.and_then(|c| globals.get(c.parent()).ok())
.map(|g| **g)
.unwrap_or(Affine2::IDENTITY);
let in_parent_space = |rect: SharedRect| seed_in_parent_space(rect, own, parent, measured);
let in_flight = state.layout.shared_active();
let external = match state.shared.reflow.as_mut() {
Some(r) if in_flight => {
let ancestors: Vec<(Entity, Vec2)> = parents
.iter_ancestors(entity)
.filter_map(|a| flying.get(&a).map(|step| (a, *step)))
.collect();
r.observe(own.translation, size, &ancestors)
}
_ => Vec2::ZERO,
};
if let Some(rect) = state.shared.rect.take()
&& let Some(shared) = shared_spec
&& let Some(seed) = in_parent_space(rect)
{
state.layout.seed_shared(seed, measured, shared);
let flying = state.layout.shared_active();
state.shared.origin = flying.then_some(rect);
state.shared.destination = flying.then(|| SharedRect {
center: own.translation,
size: Vec2::new(measured[2], measured[3])
* Vec2::new(
parent.matrix2.x_axis.length(),
parent.matrix2.y_axis.length(),
),
});
state.shared.reflow = flying.then(|| SharedReflow::new(own.translation, size));
if !anim.is_some_and(|a| a.0.has_node_props()) {
let inverse_scale_factor = computed
.as_deref()
.map(|c| c.inverse_scale_factor)
.unwrap_or(1.0);
state.arm_shared_size(
[seed[2], seed[3]],
[measured[2], measured[3]],
inverse_scale_factor,
shared,
);
}
} else if let Some(rect) = state.shared.origin
&& let Some(seed) = in_parent_space(rect.shifted(external))
{
state.layout.rebase_shared(seed);
}
let destination = state
.shared
.destination
.map(|rect| rect.shifted(external))
.and_then(in_parent_space)
.unwrap_or(measured);
let writer = if anim.is_some_and(|a| a.0.has_node_props()) {
RectWriter::Binding
} else if state.size_in_flight() && !state.layout.shared_active() {
RectWriter::SizeChannel
} else {
RectWriter::None
};
let seed_frame = state.layout.on_seed_frame();
let shared_flight = state.layout.shared_active();
if shared_flight {
let hold = parents
.iter_ancestors(entity)
.any(|a| last_animating.contains(&a));
state.layout.hold_shared(hold);
}
let own_size = state.size_currents_px();
state.layout.note_own_size(own_size);
if let Some(shown) = state.layout.drive(measured, destination, spec, writer, dt) {
let mut delta = LayoutDelta::between(shown, measured);
delta.root_anchored = shared_flight;
delta.seed_frame = seed_frame;
if seed_frame && let Some(computed) = computed.as_mut() {
compensate_seed_frame_radius(computed, delta.scale);
}
deltas.insert(entity, delta);
}
if !state.layout.shared_active() {
state.shared.origin = None;
state.shared.destination = None;
state.shared.reflow = None;
}
}
last_animating.clear();
if deltas.is_empty() {
return;
}
last_animating.extend(deltas.keys().copied());
let has_animating_ancestor =
|e: Entity| parents.iter_ancestors(e).any(|a| deltas.contains_key(&a));
let roots: Vec<Entity> = deltas
.keys()
.copied()
.filter(|&e| !has_animating_ancestor(e))
.collect();
let mut stack: Vec<(Entity, Affine2, Affine2, Affine2, Vec2)> = Vec::new();
for root in roots {
let parent = parents
.get(root)
.ok()
.and_then(|c| globals.get(c.parent()).ok())
.map(|g| **g)
.unwrap_or(Affine2::IDENTITY);
if let Some(inv) = inverse(parent) {
stack.push((root, inv, parent, parent, Vec2::ONE));
}
}
while let Some((entity, parent_inverse, parent_pristine, parent_composed, offset_scale)) =
stack.pop()
{
let Ok(mut global) = globals.get_mut(entity) else {
continue;
};
let pristine = **global;
let local = parent_inverse * pristine;
let mut placed = local;
placed.translation *= offset_scale;
let delta = deltas.get(&entity);
let (composed, for_children) = match delta {
Some(d) => {
let (base, local) = if d.root_anchored {
(parent_pristine, local)
} else {
(parent_composed, placed)
};
let unscaled = base * Affine2::from_translation(d.translation) * local;
(unscaled * Affine2::from_scale(d.scale), unscaled)
}
None => {
let c = parent_composed * placed;
(c, c)
}
};
if composed != pristine {
*global = composed.into();
}
let child_offsets = delta
.filter(|d| d.seed_frame)
.map_or(Vec2::ONE, |d| d.scale);
if let Ok(kids) = children.get(entity)
&& let Some(inv) = inverse(pristine)
{
for &child in kids {
stack.push((child, inv, pristine, for_children, child_offsets));
}
}
}
}