use blinc_animation::{AnimatedValue, SchedulerHandle, SpringConfig};
use blinc_core::Rect;
use crate::element::ElementBounds;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AnimationDirection {
Expanding,
Collapsing,
Mixed,
}
pub struct AnimatedOffset {
pub x: Option<AnimatedValue>,
pub y: Option<AnimatedValue>,
}
impl AnimatedOffset {
pub fn none() -> Self {
Self { x: None, y: None }
}
pub fn get_x(&self) -> f32 {
self.x.as_ref().map(|v| v.get()).unwrap_or(0.0)
}
pub fn get_y(&self) -> f32 {
self.y.as_ref().map(|v| v.get()).unwrap_or(0.0)
}
pub fn is_animating(&self) -> bool {
self.x.as_ref().map(|v| v.is_animating()).unwrap_or(false)
|| self.y.as_ref().map(|v| v.is_animating()).unwrap_or(false)
}
}
pub struct AnimatedSizeDelta {
pub width: Option<AnimatedValue>,
pub height: Option<AnimatedValue>,
}
impl AnimatedSizeDelta {
pub fn none() -> Self {
Self {
width: None,
height: None,
}
}
pub fn get_width(&self) -> f32 {
self.width.as_ref().map(|v| v.get()).unwrap_or(0.0)
}
pub fn get_height(&self) -> f32 {
self.height.as_ref().map(|v| v.get()).unwrap_or(0.0)
}
pub fn is_animating(&self) -> bool {
self.width
.as_ref()
.map(|v| v.is_animating())
.unwrap_or(false)
|| self
.height
.as_ref()
.map(|v| v.is_animating())
.unwrap_or(false)
}
}
pub struct VisualAnimation {
pub key: String,
pub from_bounds: ElementBounds,
pub to_bounds: ElementBounds,
pub offset: AnimatedOffset,
pub size_delta: AnimatedSizeDelta,
pub direction: AnimationDirection,
pub spring: SpringConfig,
}
impl VisualAnimation {
pub fn from_bounds_change(
key: String,
from_bounds: ElementBounds,
to_bounds: ElementBounds,
config: &VisualAnimationConfig,
scheduler: SchedulerHandle,
) -> Option<Self> {
let dx = from_bounds.x - to_bounds.x;
let dy = from_bounds.y - to_bounds.y;
let dw = from_bounds.width - to_bounds.width;
let dh = from_bounds.height - to_bounds.height;
let has_position_change =
config.animate.position && (dx.abs() > config.threshold || dy.abs() > config.threshold);
let has_size_change =
config.animate.size && (dw.abs() > config.threshold || dh.abs() > config.threshold);
if !has_position_change && !has_size_change {
return None;
}
let direction = if dh > config.threshold || dw > config.threshold {
AnimationDirection::Collapsing
} else if dh < -config.threshold || dw < -config.threshold {
AnimationDirection::Expanding
} else {
AnimationDirection::Mixed
};
let offset = AnimatedOffset {
x: if config.animate.position && dx.abs() > config.threshold {
let mut anim = AnimatedValue::new(scheduler.clone(), dx, config.spring);
anim.set_target(0.0);
Some(anim)
} else {
None
},
y: if config.animate.position && dy.abs() > config.threshold {
let mut anim = AnimatedValue::new(scheduler.clone(), dy, config.spring);
anim.set_target(0.0);
Some(anim)
} else {
None
},
};
let size_delta = AnimatedSizeDelta {
width: if config.animate.size && dw.abs() > config.threshold {
let mut anim = AnimatedValue::new(scheduler.clone(), dw, config.spring);
anim.set_target(0.0);
Some(anim)
} else {
None
},
height: if config.animate.size && dh.abs() > config.threshold {
let mut anim = AnimatedValue::new(scheduler.clone(), dh, config.spring);
anim.set_target(0.0);
Some(anim)
} else {
None
},
};
Some(Self {
key,
from_bounds,
to_bounds,
offset,
size_delta,
direction,
spring: config.spring,
})
}
pub fn update_target(&mut self, new_to_bounds: ElementBounds, scheduler: SchedulerHandle) {
let current_visual = self.current_visual_bounds();
let dx = current_visual.x - new_to_bounds.x;
let dy = current_visual.y - new_to_bounds.y;
let dw = current_visual.width - new_to_bounds.width;
let dh = current_visual.height - new_to_bounds.height;
if dh > 1.0 || dw > 1.0 {
self.direction = AnimationDirection::Collapsing;
} else if dh < -1.0 || dw < -1.0 {
self.direction = AnimationDirection::Expanding;
}
self.to_bounds = new_to_bounds;
if let Some(ref mut anim) = self.offset.x {
anim.set_immediate(dx);
anim.set_target(0.0);
}
if let Some(ref mut anim) = self.offset.y {
anim.set_immediate(dy);
anim.set_target(0.0);
}
if let Some(ref mut anim) = self.size_delta.width {
anim.set_immediate(dw);
anim.set_target(0.0);
}
if let Some(ref mut anim) = self.size_delta.height {
anim.set_immediate(dh);
anim.set_target(0.0);
}
}
pub fn current_visual_bounds(&self) -> ElementBounds {
ElementBounds {
x: self.to_bounds.x + self.offset.get_x(),
y: self.to_bounds.y + self.offset.get_y(),
width: self.to_bounds.width + self.size_delta.get_width(),
height: self.to_bounds.height + self.size_delta.get_height(),
}
}
pub fn is_animating(&self) -> bool {
self.offset.is_animating() || self.size_delta.is_animating()
}
pub fn is_collapsing(&self) -> bool {
matches!(self.direction, AnimationDirection::Collapsing)
}
pub fn is_expanding(&self) -> bool {
matches!(self.direction, AnimationDirection::Expanding)
}
}
#[derive(Clone, Debug)]
pub struct AnimatedRenderBounds {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub clip_rect: Option<Rect>,
}
impl AnimatedRenderBounds {
pub fn identity() -> Self {
Self {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
clip_rect: None,
}
}
pub fn from_layout(bounds: ElementBounds) -> Self {
Self {
x: bounds.x,
y: bounds.y,
width: bounds.width,
height: bounds.height,
clip_rect: None,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct AnimateProperties {
pub position: bool,
pub size: bool,
}
#[derive(Clone, Debug, Default)]
pub enum ClipBehavior {
#[default]
ClipToAnimated,
ClipToLayout,
NoClip,
}
#[derive(Clone, Debug)]
pub struct VisualAnimationConfig {
pub key: Option<String>,
pub animate: AnimateProperties,
pub spring: SpringConfig,
pub threshold: f32,
pub clip_behavior: ClipBehavior,
}
impl Default for VisualAnimationConfig {
fn default() -> Self {
Self::height()
}
}
impl VisualAnimationConfig {
pub fn height() -> Self {
Self {
key: None,
animate: AnimateProperties {
position: false,
size: true,
},
spring: SpringConfig::snappy(),
threshold: 1.0,
clip_behavior: ClipBehavior::ClipToAnimated,
}
}
pub fn width() -> Self {
Self {
key: None,
animate: AnimateProperties {
position: false,
size: true,
},
spring: SpringConfig::snappy(),
threshold: 1.0,
clip_behavior: ClipBehavior::ClipToAnimated,
}
}
pub fn size() -> Self {
Self {
key: None,
animate: AnimateProperties {
position: false,
size: true,
},
spring: SpringConfig::snappy(),
threshold: 1.0,
clip_behavior: ClipBehavior::ClipToAnimated,
}
}
pub fn position() -> Self {
Self {
key: None,
animate: AnimateProperties {
position: true,
size: false,
},
spring: SpringConfig::snappy(),
threshold: 1.0,
clip_behavior: ClipBehavior::NoClip,
}
}
pub fn all() -> Self {
Self {
key: None,
animate: AnimateProperties {
position: true,
size: true,
},
spring: SpringConfig::snappy(),
threshold: 1.0,
clip_behavior: ClipBehavior::ClipToAnimated,
}
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_spring(mut self, config: SpringConfig) -> Self {
self.spring = config;
self
}
pub fn with_threshold(mut self, threshold: f32) -> Self {
self.threshold = threshold;
self
}
pub fn gentle(self) -> Self {
self.with_spring(SpringConfig::gentle())
}
pub fn wobbly(self) -> Self {
self.with_spring(SpringConfig::wobbly())
}
pub fn stiff(self) -> Self {
self.with_spring(SpringConfig::stiff())
}
pub fn snappy(self) -> Self {
self.with_spring(SpringConfig::snappy())
}
pub fn clip_to_animated(mut self) -> Self {
self.clip_behavior = ClipBehavior::ClipToAnimated;
self
}
pub fn clip_to_layout(mut self) -> Self {
self.clip_behavior = ClipBehavior::ClipToLayout;
self
}
pub fn no_clip(mut self) -> Self {
self.clip_behavior = ClipBehavior::NoClip;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builders() {
let config = VisualAnimationConfig::height();
assert!(!config.animate.position);
assert!(config.animate.size);
let config = VisualAnimationConfig::all();
assert!(config.animate.position);
assert!(config.animate.size);
let config = VisualAnimationConfig::position();
assert!(config.animate.position);
assert!(!config.animate.size);
}
#[test]
fn test_animation_direction() {
assert_eq!(
AnimationDirection::Collapsing,
AnimationDirection::Collapsing
);
assert_ne!(
AnimationDirection::Expanding,
AnimationDirection::Collapsing
);
}
}