aura-anim-iced 0.2.2

Iced integration for the aura typed animation runtime.
Documentation
//! Animated side menu using Presence and Spring.

use std::time::Instant;

use aura_anim_core::{
    Animatable, MotionRuntime, Presence, Spring, SpringConfig,
    timing::{Easing, Timing},
};
use iced::{
    Background, Border, Color, Element, Fill, Shadow, Subscription, Theme, Vector,
    widget::{button, column, container, row, text},
};

#[derive(Clone, Debug, Animatable)]
struct MenuMotion {
    width: f32,
    opacity: f32,
    glow: f32,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Section {
    Inbox,
    Projects,
    Archive,
}

impl Section {
    const ALL: [Self; 3] = [Self::Inbox, Self::Projects, Self::Archive];

    const fn label(self) -> &'static str {
        match self {
            Self::Inbox => "Inbox",
            Self::Projects => "Projects",
            Self::Archive => "Archive",
        }
    }
}

struct MenuExample {
    runtime: MotionRuntime,
    menu: Presence<MenuMotion>,
    selected: Section,
}

#[derive(Clone, Copy, Debug)]
enum Message {
    Frame(Instant),
    Toggle,
    Select(Section),
}

fn main() -> iced::Result {
    iced::application(MenuExample::new, MenuExample::update, MenuExample::view)
        .title("Aura Anim - Animated Menu")
        .theme(theme)
        .subscription(MenuExample::subscription)
        .window_size((860.0, 520.0))
        .run()
}

impl MenuExample {
    fn new() -> Self {
        let mut runtime = MotionRuntime::new();
        let menu = Presence::new(
            &mut runtime,
            hidden(),
            visible(),
            Timing::new(190.0).with_easing(Easing::EaseOut),
        );

        Self {
            runtime,
            menu,
            selected: Section::Inbox,
        }
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::Frame(now) => {
                aura_anim_iced::frame(&mut self.runtime, now);
                self.menu.sync(&self.runtime);
            }
            Message::Toggle => {
                let current = self.menu.value(&self.runtime).clone();
                let target = if self.menu.is_visible() {
                    hidden()
                } else {
                    visible()
                };
                let spring = Spring::new(
                    current,
                    target,
                    SpringConfig {
                        stiffness: 300.0,
                        damping: 29.0,
                        ..SpringConfig::default()
                    },
                );

                if self.menu.is_visible() {
                    self.menu.hide_with(spring, &mut self.runtime);
                } else {
                    self.menu.show_with(spring, &mut self.runtime);
                }
            }
            Message::Select(section) => self.selected = section,
        }
    }

    fn subscription(&self) -> Subscription<Message> {
        aura_anim_iced::subscription(&self.runtime).map(Message::Frame)
    }

    fn view(&self) -> Element<'_, Message> {
        let main = container(
            column![
                row![
                    button(if self.menu.is_visible() {
                        "Close menu"
                    } else {
                        "Open menu"
                    })
                    .on_press(Message::Toggle),
                    text("Workspace").size(14).color(Color::from_rgb8(160, 169, 200))
                ]
                .spacing(14),
                text(self.selected.label()).size(32).color(Color::WHITE),
                text("The main content stays interactive while the menu mounts, animates, and unmounts.")
                    .size(14)
                    .color(Color::from_rgb8(169, 178, 207)),
                container(
                    column![
                        text("Selected view").size(13).color(Color::from_rgb8(111, 226, 198)),
                        text(format!("{} content", self.selected.label()))
                            .size(22)
                            .color(Color::WHITE)
                    ]
                    .spacing(10)
                )
                .width(Fill)
                .height(220)
                .padding(24)
                .style(|_| panel_style(Color::from_rgb8(55, 65, 96), 0.12))
            ]
            .spacing(20),
        )
        .width(Fill)
        .height(Fill);

        let menu: Element<'_, Message> = if self.menu.is_mounted() {
            let motion = self.menu.value(&self.runtime);
            let entries = column(Section::ALL.map(|section| {
                let selected = section == self.selected;
                button(text(section.label()).size(15))
                    .width(Fill)
                    .on_press(Message::Select(section))
                    .style(move |theme, status| {
                        if selected {
                            button::primary(theme, status)
                        } else {
                            button::text(theme, status)
                        }
                    })
                    .into()
            }))
            .spacing(8);

            container(
                column![
                    text("Navigation").size(20).color(Color::from_rgba(
                        1.0,
                        1.0,
                        1.0,
                        motion.opacity
                    )),
                    entries,
                    text("Presence keeps this mounted until exit completes.")
                        .size(12)
                        .color(Color::from_rgba(0.66, 0.7, 0.84, motion.opacity))
                ]
                .spacing(18),
            )
            .width(motion.width.max(1.0))
            .height(Fill)
            .padding(20)
            .style(move |_| panel_style(Color::from_rgb8(95, 82, 210), motion.glow))
            .into()
        } else {
            container("").width(0).into()
        };

        container(row![menu, main].spacing(20))
            .width(Fill)
            .height(Fill)
            .padding(28)
            .style(|_| page_style())
            .into()
    }
}

fn hidden() -> MenuMotion {
    MenuMotion {
        width: 0.0,
        opacity: 0.0,
        glow: 0.0,
    }
}

fn visible() -> MenuMotion {
    MenuMotion {
        width: 240.0,
        opacity: 1.0,
        glow: 0.42,
    }
}

fn theme(_: &MenuExample) -> Theme {
    Theme::TokyoNight
}

fn page_style() -> container::Style {
    container::Style::default()
        .background(Color::from_rgb8(18, 21, 34))
        .color(Color::WHITE)
}

fn panel_style(accent: Color, glow: f32) -> container::Style {
    container::Style {
        background: Some(Background::Color(Color::from_rgba(
            accent.r * 0.22,
            accent.g * 0.22,
            accent.b * 0.22,
            0.96,
        ))),
        border: Border::default()
            .rounded(8)
            .width(1)
            .color(Color::from_rgba(
                accent.r,
                accent.g,
                accent.b,
                0.28 + glow * 0.4,
            )),
        shadow: Shadow {
            color: Color::from_rgba(accent.r, accent.g, accent.b, glow * 0.3),
            offset: Vector::new(0.0, 8.0),
            blur_radius: 18.0 + glow * 24.0,
        },
        ..container::Style::default()
    }
}