kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
//! Shared element (hero) transition between views.

use kael::{prelude::FluentBuilder as _, *};
use std::time::Duration;

use crate::animations::{durations, easings, lerp_pixels};

#[derive(Clone, Copy, Debug)]
struct ElementBounds {
    x: Pixels,
    y: Pixels,
    width: Pixels,
    height: Pixels,
}

impl Default for ElementBounds {
    fn default() -> Self {
        Self {
            x: px(0.0),
            y: px(0.0),
            width: px(0.0),
            height: px(0.0),
        }
    }
}

pub struct SharedElementState {
    source_bounds: Option<ElementBounds>,
    target_bounds: Option<ElementBounds>,
    is_transitioning: bool,
    progress: f32,
    version: usize,
    duration: Duration,
}

impl SharedElementState {
    pub fn new(_cx: &mut Context<Self>) -> Self {
        Self {
            source_bounds: None,
            target_bounds: None,
            is_transitioning: false,
            progress: 0.0,
            version: 0,
            duration: durations::SLOW,
        }
    }

    pub fn set_duration(&mut self, duration: Duration) {
        self.duration = duration;
    }

    pub fn set_source_bounds(&mut self, bounds: Bounds<Pixels>) {
        self.source_bounds = Some(ElementBounds {
            x: bounds.origin.x,
            y: bounds.origin.y,
            width: bounds.size.width,
            height: bounds.size.height,
        });
    }

    pub fn set_target_bounds(&mut self, bounds: Bounds<Pixels>) {
        self.target_bounds = Some(ElementBounds {
            x: bounds.origin.x,
            y: bounds.origin.y,
            width: bounds.size.width,
            height: bounds.size.height,
        });
    }

    pub fn transition_to(&mut self, cx: &mut Context<Self>) {
        if self.source_bounds.is_none() || self.target_bounds.is_none() {
            return;
        }

        self.is_transitioning = true;
        self.progress = 0.0;
        self.version += 1;
        cx.notify();

        let duration = self.duration;
        cx.spawn(async move |this, cx| {
            cx.background_executor().timer(duration).await;
            _ = this.update(cx, |state, cx| {
                state.is_transitioning = false;
                state.progress = 1.0;
                state.source_bounds = state.target_bounds;
                cx.notify();
            });
        })
        .detach();
    }

    pub fn is_transitioning(&self) -> bool {
        self.is_transitioning
    }

    pub fn progress(&self) -> f32 {
        self.progress
    }

    pub fn version(&self) -> usize {
        self.version
    }
}

#[derive(IntoElement)]
pub struct SharedElementTransition {
    id: ElementId,
    state: Entity<SharedElementState>,
    content: Option<AnyElement>,
    style: StyleRefinement,
}

impl SharedElementTransition {
    pub fn new(id: impl Into<ElementId>, state: Entity<SharedElementState>) -> Self {
        Self {
            id: id.into(),
            state,
            content: None,
            style: StyleRefinement::default(),
        }
    }

    pub fn content(mut self, element: impl IntoElement) -> Self {
        self.content = Some(element.into_any_element());
        self
    }
}

impl Styled for SharedElementTransition {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl RenderOnce for SharedElementTransition {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let user_style = self.style;
        let state = self.state.read(cx);
        let is_transitioning = state.is_transitioning;
        let version = state.version;
        let source = state.source_bounds.unwrap_or_default();
        let target = state.target_bounds.unwrap_or_default();
        let duration = state.duration;
        let content = self.content;

        if is_transitioning {
            div()
                .id(self.id.clone())
                .absolute()
                .left(source.x)
                .top(source.y)
                .w(source.width)
                .h(source.height)
                .children(content)
                .map(|this| {
                    let mut el = this;
                    el.style().refine(&user_style);
                    el
                })
                .with_animation(
                    ElementId::Name(format!("set-{}-{}", self.id, version).into()),
                    Animation::new(duration).with_easing(easings::ease_in_out_cubic),
                    move |el, delta| {
                        let curr_x = lerp_pixels(source.x, target.x, delta);
                        let curr_y = lerp_pixels(source.y, target.y, delta);
                        let curr_w = lerp_pixels(source.width, target.width, delta);
                        let curr_h = lerp_pixels(source.height, target.height, delta);

                        el.left(curr_x).top(curr_y).w(curr_w).h(curr_h)
                    },
                )
                .into_any_element()
        } else {
            div()
                .id(self.id)
                .children(content)
                .map(|this| {
                    let mut el = this;
                    el.style().refine(&user_style);
                    el
                })
                .into_any_element()
        }
    }
}