use crate::tree::LayoutRect;
use astrelis_core::math::Vec2;
const EXP_LERP_SPEED: f32 = 12.0;
const GHOST_TAB_OPACITY: f32 = 0.7;
const PANEL_TRANSITION_DURATION: f32 = 0.2;
const TAB_REORDER_DURATION: f32 = 0.15;
const DROP_PREVIEW_FADE_SPEED: f32 = 8.0;
#[derive(Debug, Default)]
pub struct DockAnimationState {
pub ghost_tab: Option<GhostTabAnimation>,
pub ghost_group: Option<GhostGroupAnimation>,
pub panel_transition: Option<PanelTransition>,
pub separator_ease: Option<SeparatorEase>,
pub tab_reorder: Option<TabReorderAnimation>,
pub drop_preview: Option<DropPreviewAnimation>,
}
impl DockAnimationState {
pub fn new() -> Self {
Self::default()
}
pub fn update(&mut self, dt: f32) -> bool {
let mut any_active = false;
if let Some(ref mut anim) = self.ghost_tab {
anim.update(dt);
if anim.is_done() {
self.ghost_tab = None;
} else {
any_active = true;
}
}
if let Some(ref mut anim) = self.ghost_group {
anim.update(dt);
if anim.is_done() {
self.ghost_group = None;
} else {
any_active = true;
}
}
if let Some(ref mut anim) = self.panel_transition {
anim.update(dt);
if anim.is_done() {
self.panel_transition = None;
} else {
any_active = true;
}
}
if let Some(ref mut anim) = self.separator_ease {
anim.update(dt);
if anim.is_done() {
self.separator_ease = None;
} else {
any_active = true;
}
}
if let Some(ref mut anim) = self.tab_reorder {
anim.update(dt);
if anim.is_done() {
self.tab_reorder = None;
} else {
any_active = true;
}
}
if let Some(ref mut anim) = self.drop_preview {
anim.update(dt);
if anim.is_done() {
self.drop_preview = None;
} else {
any_active = true;
}
}
any_active
}
pub fn has_active_animations(&self) -> bool {
self.ghost_tab.is_some()
|| self.ghost_group.is_some()
|| self.panel_transition.is_some()
|| self.separator_ease.is_some()
|| self.tab_reorder.is_some()
|| self.drop_preview.is_some()
}
pub fn clear(&mut self) {
*self = Self::default();
}
}
#[derive(Debug, Clone)]
pub struct GhostTabAnimation {
pub position: Vec2,
pub target: Vec2,
pub size: Vec2,
pub label: String,
pub opacity: f32,
pub fading_out: bool,
}
impl GhostTabAnimation {
pub fn new(position: Vec2, size: Vec2, label: String) -> Self {
Self {
position,
target: position,
size,
label,
opacity: 0.0,
fading_out: false,
}
}
pub fn update(&mut self, dt: f32) {
if self.fading_out {
self.opacity = (self.opacity - DROP_PREVIEW_FADE_SPEED * dt).max(0.0);
} else {
self.opacity = (self.opacity + DROP_PREVIEW_FADE_SPEED * dt).min(GHOST_TAB_OPACITY);
let factor = 1.0 - (-EXP_LERP_SPEED * dt).exp();
self.position = self.position.lerp(self.target, factor);
}
}
pub fn set_target(&mut self, target: Vec2) {
self.target = target;
}
pub fn fade_out(&mut self) {
self.fading_out = true;
}
pub fn is_done(&self) -> bool {
self.fading_out && self.opacity <= 0.0
}
}
#[derive(Debug, Clone)]
pub struct GhostGroupAnimation {
pub position: Vec2,
pub target: Vec2,
pub size: Vec2,
pub labels: Vec<String>,
pub opacity: f32,
pub fading_out: bool,
}
impl GhostGroupAnimation {
pub fn new(position: Vec2, size: Vec2, labels: Vec<String>) -> Self {
Self {
position,
target: position,
size,
labels,
opacity: 0.0,
fading_out: false,
}
}
pub fn update(&mut self, dt: f32) {
if self.fading_out {
self.opacity = (self.opacity - DROP_PREVIEW_FADE_SPEED * dt).max(0.0);
} else {
self.opacity = (self.opacity + DROP_PREVIEW_FADE_SPEED * dt).min(GHOST_TAB_OPACITY);
let factor = 1.0 - (-EXP_LERP_SPEED * dt).exp();
self.position = self.position.lerp(self.target, factor);
}
}
pub fn set_target(&mut self, target: Vec2) {
self.target = target;
}
pub fn fade_out(&mut self) {
self.fading_out = true;
}
pub fn is_done(&self) -> bool {
self.fading_out && self.opacity <= 0.0
}
}
#[derive(Debug, Clone)]
pub struct PanelTransition {
pub from_ratio: f32,
pub to_ratio: f32,
pub current_ratio: f32,
elapsed: f32,
duration: f32,
}
impl PanelTransition {
pub fn new(from_ratio: f32, to_ratio: f32) -> Self {
Self {
from_ratio,
to_ratio,
current_ratio: from_ratio,
elapsed: 0.0,
duration: PANEL_TRANSITION_DURATION,
}
}
pub fn with_duration(mut self, duration: f32) -> Self {
self.duration = duration;
self
}
pub fn update(&mut self, dt: f32) {
self.elapsed += dt;
let t = (self.elapsed / self.duration).min(1.0);
let eased = 1.0 - (1.0 - t).powi(3);
self.current_ratio = self.from_ratio + (self.to_ratio - self.from_ratio) * eased;
}
pub fn is_done(&self) -> bool {
self.elapsed >= self.duration
}
pub fn progress(&self) -> f32 {
(self.elapsed / self.duration).min(1.0)
}
}
#[derive(Debug, Clone)]
pub struct SeparatorEase {
pub current_ratio: f32,
pub target_ratio: f32,
pub active: bool,
}
impl SeparatorEase {
pub fn new(ratio: f32) -> Self {
Self {
current_ratio: ratio,
target_ratio: ratio,
active: true,
}
}
pub fn set_target(&mut self, target: f32) {
self.target_ratio = target;
}
pub fn stop(&mut self) {
self.active = false;
}
pub fn update(&mut self, dt: f32) {
let factor = 1.0 - (-EXP_LERP_SPEED * dt).exp();
self.current_ratio += (self.target_ratio - self.current_ratio) * factor;
}
pub fn is_done(&self) -> bool {
!self.active && (self.current_ratio - self.target_ratio).abs() < 0.001
}
}
#[derive(Debug, Clone)]
pub struct TabReorderAnimation {
pub tab_index: usize,
pub from_offset_x: f32,
pub current_offset_x: f32,
elapsed: f32,
duration: f32,
}
impl TabReorderAnimation {
pub fn new(tab_index: usize, from_offset_x: f32) -> Self {
Self {
tab_index,
from_offset_x,
current_offset_x: from_offset_x,
elapsed: 0.0,
duration: TAB_REORDER_DURATION,
}
}
pub fn update(&mut self, dt: f32) {
self.elapsed += dt;
let t = (self.elapsed / self.duration).min(1.0);
let eased = 1.0 - (1.0 - t) * (1.0 - t);
self.current_offset_x = self.from_offset_x * (1.0 - eased);
}
pub fn is_done(&self) -> bool {
self.elapsed >= self.duration
}
}
#[derive(Debug, Clone)]
pub struct DropPreviewAnimation {
pub opacity: f32,
pub target_opacity: f32,
pub current_bounds: LayoutRect,
pub target_bounds: LayoutRect,
}
impl DropPreviewAnimation {
pub fn new(bounds: LayoutRect) -> Self {
Self {
opacity: 0.0,
target_opacity: 1.0,
current_bounds: bounds,
target_bounds: bounds,
}
}
pub fn set_target(&mut self, bounds: LayoutRect) {
self.target_bounds = bounds;
self.target_opacity = 1.0;
}
pub fn fade_out(&mut self) {
self.target_opacity = 0.0;
}
pub fn update(&mut self, dt: f32) {
let opacity_diff = self.target_opacity - self.opacity;
self.opacity += opacity_diff * (1.0 - (-DROP_PREVIEW_FADE_SPEED * dt).exp());
let factor = 1.0 - (-EXP_LERP_SPEED * dt).exp();
self.current_bounds.x += (self.target_bounds.x - self.current_bounds.x) * factor;
self.current_bounds.y += (self.target_bounds.y - self.current_bounds.y) * factor;
self.current_bounds.width +=
(self.target_bounds.width - self.current_bounds.width) * factor;
self.current_bounds.height +=
(self.target_bounds.height - self.current_bounds.height) * factor;
}
pub fn is_done(&self) -> bool {
self.target_opacity <= 0.0 && self.opacity < 0.01
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ghost_tab_fades_in() {
let mut anim = GhostTabAnimation::new(Vec2::ZERO, Vec2::new(100.0, 28.0), "Tab".into());
assert_eq!(anim.opacity, 0.0);
for _ in 0..10 {
anim.update(1.0 / 60.0);
}
assert!(anim.opacity > 0.0);
assert!(!anim.is_done());
}
#[test]
fn ghost_tab_fades_out() {
let mut anim = GhostTabAnimation::new(Vec2::ZERO, Vec2::new(100.0, 28.0), "Tab".into());
anim.opacity = GHOST_TAB_OPACITY;
anim.fade_out();
for _ in 0..60 {
anim.update(1.0 / 60.0);
}
assert!(anim.is_done());
}
#[test]
fn ghost_tab_follows_target() {
let mut anim = GhostTabAnimation::new(Vec2::ZERO, Vec2::new(100.0, 28.0), "Tab".into());
anim.set_target(Vec2::new(200.0, 100.0));
for _ in 0..60 {
anim.update(1.0 / 60.0);
}
assert!((anim.position.x - 200.0).abs() < 1.0);
assert!((anim.position.y - 100.0).abs() < 1.0);
}
#[test]
fn panel_transition_completes() {
let mut anim = PanelTransition::new(0.0, 0.5);
assert_eq!(anim.current_ratio, 0.0);
let steps = (PANEL_TRANSITION_DURATION * 60.0) as usize + 1;
for _ in 0..steps {
anim.update(1.0 / 60.0);
}
assert!(anim.is_done());
assert!((anim.current_ratio - 0.5).abs() < 0.01);
}
#[test]
fn separator_ease_converges() {
let mut anim = SeparatorEase::new(0.3);
anim.set_target(0.7);
anim.stop();
for _ in 0..120 {
anim.update(1.0 / 60.0);
}
assert!(anim.is_done());
assert!((anim.current_ratio - 0.7).abs() < 0.001);
}
#[test]
fn tab_reorder_slides_to_zero() {
let mut anim = TabReorderAnimation::new(0, -50.0);
assert_eq!(anim.current_offset_x, -50.0);
let steps = (TAB_REORDER_DURATION * 60.0) as usize + 1;
for _ in 0..steps {
anim.update(1.0 / 60.0);
}
assert!(anim.is_done());
assert!(anim.current_offset_x.abs() < 0.1);
}
#[test]
fn drop_preview_fades_in_and_out() {
let bounds = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut anim = DropPreviewAnimation::new(bounds);
assert_eq!(anim.opacity, 0.0);
for _ in 0..30 {
anim.update(1.0 / 60.0);
}
assert!(anim.opacity > 0.5);
anim.fade_out();
for _ in 0..60 {
anim.update(1.0 / 60.0);
}
assert!(anim.is_done());
}
#[test]
fn drop_preview_transitions_bounds() {
let bounds = LayoutRect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let mut anim = DropPreviewAnimation::new(bounds);
let new_bounds = LayoutRect {
x: 50.0,
y: 50.0,
width: 200.0,
height: 200.0,
};
anim.set_target(new_bounds);
for _ in 0..120 {
anim.update(1.0 / 60.0);
}
assert!((anim.current_bounds.x - 50.0).abs() < 1.0);
assert!((anim.current_bounds.width - 200.0).abs() < 1.0);
}
#[test]
fn dock_animation_state_updates_all() {
let mut state = DockAnimationState::new();
state.ghost_tab = Some(GhostTabAnimation::new(
Vec2::ZERO,
Vec2::new(100.0, 28.0),
"Tab".into(),
));
state.panel_transition = Some(PanelTransition::new(0.0, 0.5));
assert!(state.has_active_animations());
for _ in 0..120 {
state.update(1.0 / 60.0);
}
assert!(state.panel_transition.is_none());
assert!(state.ghost_tab.is_some());
}
}