use cranpose_animation::AnimationType;
use cranpose_ui_graphics::Size;
use crate::{
composable,
modifier::GraphicsLayer,
widgets::animated_content::{ContentTransform, SeekableAnimatedContent},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlideDirection {
Left,
Right,
Up,
Down,
Start,
End,
}
impl SlideDirection {
fn is_vertical(self) -> bool {
matches!(self, Self::Up | Self::Down)
}
fn is_backward(self, rtl: bool) -> bool {
match self {
Self::Left | Self::Up => true,
Self::Right | Self::Down => false,
Self::Start => !rtl,
Self::End => rtl,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum Slide {
Fraction(f32),
Container {
towards: SlideDirection,
entering: bool,
},
}
impl Slide {
fn offset(self, own: f32, container: f32, rtl: bool) -> f32 {
match self {
Slide::Fraction(fraction) => fraction * own,
Slide::Container { towards, entering } => {
if entering == towards.is_backward(rtl) {
container
} else {
-own
}
}
}
}
fn uses_direction(self) -> bool {
matches!(
self,
Slide::Container {
towards: SlideDirection::Start | SlideDirection::End,
..
}
)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct ContainerFrame {
pub(crate) size: Size,
pub(crate) rtl: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) struct TransitionEffects {
fade: bool,
slide_x: Option<Slide>,
slide_y: Option<Slide>,
scale: Option<f32>,
}
impl TransitionEffects {
fn merged(self, other: Self) -> Self {
Self {
fade: self.fade || other.fade,
slide_x: self.slide_x.or(other.slide_x),
slide_y: self.slide_y.or(other.slide_y),
scale: self.scale.or(other.scale),
}
}
pub(crate) fn uses_direction(self) -> bool {
self.slide_x.is_some_and(Slide::uses_direction)
}
pub(crate) fn layer(
self,
progress: f32,
size: Size,
container: ContainerFrame,
) -> GraphicsLayer {
let away = 1.0 - progress;
let offset = |slide: Option<Slide>, own: f32, extent: f32| {
slide.map_or(0.0, |slide| slide.offset(own, extent, container.rtl) * away)
};
GraphicsLayer {
alpha: if self.fade {
progress.clamp(0.0, 1.0)
} else {
1.0
},
translation_x: offset(self.slide_x, size.width, container.size.width),
translation_y: offset(self.slide_y, size.height, container.size.height),
scale: self
.scale
.map_or(1.0, |scale| scale + (1.0 - scale) * progress),
..GraphicsLayer::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct EnterTransition {
effects: TransitionEffects,
animation: Option<AnimationType>,
}
impl EnterTransition {
pub fn none() -> Self {
Self {
effects: TransitionEffects::default(),
animation: None,
}
}
pub fn with_animation(mut self, animation: AnimationType) -> Self {
self.animation = Some(animation);
self
}
pub fn together_with(self, exit: ExitTransition) -> ContentTransform {
ContentTransform {
enter: self,
exit,
size_transform: None,
}
}
pub(crate) fn effects(&self) -> TransitionEffects {
self.effects
}
pub(crate) fn animation(&self) -> AnimationType {
self.animation.unwrap_or_default()
}
}
impl std::ops::Add for EnterTransition {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
effects: self.effects.merged(other.effects),
animation: self.animation.or(other.animation),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExitTransition {
effects: TransitionEffects,
animation: Option<AnimationType>,
}
impl ExitTransition {
pub fn none() -> Self {
Self {
effects: TransitionEffects::default(),
animation: None,
}
}
pub fn with_animation(mut self, animation: AnimationType) -> Self {
self.animation = Some(animation);
self
}
pub(crate) fn effects(&self) -> TransitionEffects {
self.effects
}
pub(crate) fn animation(&self) -> AnimationType {
self.animation.unwrap_or_default()
}
}
impl std::ops::Add for ExitTransition {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
effects: self.effects.merged(other.effects),
animation: self.animation.or(other.animation),
}
}
}
pub fn fade_in() -> EnterTransition {
enter(TransitionEffects {
fade: true,
..TransitionEffects::default()
})
}
pub fn fade_out() -> ExitTransition {
exit(TransitionEffects {
fade: true,
..TransitionEffects::default()
})
}
pub fn slide_in_vertically(initial_offset_fraction: f32) -> EnterTransition {
enter(TransitionEffects {
slide_y: Some(Slide::Fraction(initial_offset_fraction)),
..TransitionEffects::default()
})
}
pub fn slide_out_vertically(target_offset_fraction: f32) -> ExitTransition {
exit(TransitionEffects {
slide_y: Some(Slide::Fraction(target_offset_fraction)),
..TransitionEffects::default()
})
}
pub fn slide_in_horizontally(initial_offset_fraction: f32) -> EnterTransition {
enter(TransitionEffects {
slide_x: Some(Slide::Fraction(initial_offset_fraction)),
..TransitionEffects::default()
})
}
pub fn slide_out_horizontally(target_offset_fraction: f32) -> ExitTransition {
exit(TransitionEffects {
slide_x: Some(Slide::Fraction(target_offset_fraction)),
..TransitionEffects::default()
})
}
pub fn slide_into_container(towards: SlideDirection) -> EnterTransition {
enter(container_slide(towards, true))
}
pub fn slide_out_of_container(towards: SlideDirection) -> ExitTransition {
exit(container_slide(towards, false))
}
fn container_slide(towards: SlideDirection, entering: bool) -> TransitionEffects {
let slide = Some(Slide::Container { towards, entering });
if towards.is_vertical() {
TransitionEffects {
slide_y: slide,
..TransitionEffects::default()
}
} else {
TransitionEffects {
slide_x: slide,
..TransitionEffects::default()
}
}
}
pub fn scale_in(initial_scale: f32) -> EnterTransition {
enter(TransitionEffects {
scale: Some(initial_scale),
..TransitionEffects::default()
})
}
pub fn scale_out(target_scale: f32) -> ExitTransition {
exit(TransitionEffects {
scale: Some(target_scale),
..TransitionEffects::default()
})
}
fn enter(effects: TransitionEffects) -> EnterTransition {
EnterTransition {
effects,
animation: None,
}
}
fn exit(effects: TransitionEffects) -> ExitTransition {
ExitTransition {
effects,
animation: None,
}
}
#[composable]
pub fn AnimatedVisibility<F>(
visible: bool,
enter: EnterTransition,
exit: ExitTransition,
mut content: F,
) where
F: FnMut() + 'static,
{
SeekableAnimatedContent(
visible,
None,
move |_, &shown| {
if shown {
enter.together_with(ExitTransition::none().with_animation(enter.animation()))
} else {
EnterTransition::none()
.with_animation(exit.animation())
.together_with(exit)
}
},
move |shown| {
if shown {
content();
}
},
);
}