simple-render 0.1.0

a simple immediate mode wayland ui renderer inspired by clay
Documentation
use crate::{
    ui::{Color, Inset, PaintTransform, Spacing},
    wayland::FrameAction,
};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Easing {
    #[default]
    Linear,
    EaseIn,
    EaseOut,
    EaseInOut,
}

impl Easing {
    pub fn apply(self, progress: f32) -> f32 {
        let progress = progress.clamp(0.0, 1.0);
        match self {
            Self::Linear => progress,
            Self::EaseIn => progress * progress,
            Self::EaseOut => 1.0 - (1.0 - progress) * (1.0 - progress),
            Self::EaseInOut if progress < 0.5 => 2.0 * progress * progress,
            Self::EaseInOut => 1.0 - (-2.0 * progress + 2.0).powi(2) / 2.0,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Animation {
    pub duration_ms: u32,
    pub easing: Easing,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AnimationFrame {
    pub elapsed_ms: u32,
    pub progress: f32,
    pub complete: bool,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VisualTransition {
    pub animation: Animation,
    pub from_opacity: f32,
    pub to_opacity: f32,
    pub from_scale: f32,
    pub to_scale: f32,
    pub from_translate_x: i32,
    pub to_translate_x: i32,
    pub from_translate_y: i32,
    pub to_translate_y: i32,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct VisualTransitionFrame {
    pub animation: AnimationFrame,
    pub opacity: f32,
    pub transform: PaintTransform,
}

impl AnimationFrame {
    pub fn should_animate(self) -> bool {
        !self.complete
    }

    pub fn frame_action(self) -> FrameAction {
        if self.should_animate() {
            FrameAction::Animate
        } else {
            FrameAction::Wait
        }
    }
}

impl Animation {
    pub const fn new(duration_ms: u32, easing: Easing) -> Self {
        Self {
            duration_ms,
            easing,
        }
    }

    pub fn progress(self, elapsed_ms: u32) -> f32 {
        if self.duration_ms == 0 {
            return 1.0;
        }

        self.easing
            .apply(elapsed_ms as f32 / self.duration_ms as f32)
    }

    pub fn is_complete(self, elapsed_ms: u32) -> bool {
        elapsed_ms >= self.duration_ms
    }

    pub fn frame(self, elapsed_ms: u32) -> AnimationFrame {
        AnimationFrame {
            elapsed_ms,
            progress: self.progress(elapsed_ms),
            complete: self.is_complete(elapsed_ms),
        }
    }
}

impl VisualTransition {
    pub const fn new(animation: Animation) -> Self {
        Self {
            animation,
            from_opacity: 1.0,
            to_opacity: 1.0,
            from_scale: 1.0,
            to_scale: 1.0,
            from_translate_x: 0,
            to_translate_x: 0,
            from_translate_y: 0,
            to_translate_y: 0,
        }
    }

    pub const fn fade(animation: Animation, from_opacity: f32, to_opacity: f32) -> Self {
        Self {
            from_opacity,
            to_opacity,
            ..Self::new(animation)
        }
    }

    pub const fn scale(animation: Animation, from_scale: f32, to_scale: f32) -> Self {
        Self {
            from_scale,
            to_scale,
            ..Self::new(animation)
        }
    }

    pub const fn translate(
        animation: Animation,
        from_translate_x: i32,
        to_translate_x: i32,
        from_translate_y: i32,
        to_translate_y: i32,
    ) -> Self {
        Self {
            from_translate_x,
            to_translate_x,
            from_translate_y,
            to_translate_y,
            ..Self::new(animation)
        }
    }

    pub const fn opacity(mut self, from: f32, to: f32) -> Self {
        self.from_opacity = from;
        self.to_opacity = to;
        self
    }

    pub const fn visual_scale(mut self, from: f32, to: f32) -> Self {
        self.from_scale = from;
        self.to_scale = to;
        self
    }

    pub const fn translation(mut self, from_x: i32, to_x: i32, from_y: i32, to_y: i32) -> Self {
        self.from_translate_x = from_x;
        self.to_translate_x = to_x;
        self.from_translate_y = from_y;
        self.to_translate_y = to_y;
        self
    }

    pub fn frame(self, elapsed_ms: u32) -> VisualTransitionFrame {
        let animation = self.animation.frame(elapsed_ms);
        VisualTransitionFrame {
            animation,
            opacity: lerp_f32(self.from_opacity, self.to_opacity, animation.progress)
                .clamp(0.0, 1.0),
            transform: PaintTransform::new(
                lerp_f32(self.from_scale, self.to_scale, animation.progress),
                lerp_i32(
                    self.from_translate_x,
                    self.to_translate_x,
                    animation.progress,
                ),
                lerp_i32(
                    self.from_translate_y,
                    self.to_translate_y,
                    animation.progress,
                ),
            ),
        }
    }
}

impl VisualTransitionFrame {
    pub fn frame_action(self) -> FrameAction {
        self.animation.frame_action()
    }
}

pub fn lerp_f32(from: f32, to: f32, progress: f32) -> f32 {
    from + (to - from) * progress.clamp(0.0, 1.0)
}

pub fn lerp_u32(from: u32, to: u32, progress: f32) -> u32 {
    lerp_f32(from as f32, to as f32, progress).round() as u32
}

pub fn lerp_i32(from: i32, to: i32, progress: f32) -> i32 {
    lerp_f32(from as f32, to as f32, progress).round() as i32
}

pub fn lerp_color(from: Color, to: Color, progress: f32) -> Color {
    Color::rgba(
        lerp_u8(from.red, to.red, progress),
        lerp_u8(from.green, to.green, progress),
        lerp_u8(from.blue, to.blue, progress),
        lerp_u8(from.alpha, to.alpha, progress),
    )
}

pub fn lerp_spacing(from: Spacing, to: Spacing, progress: f32) -> Spacing {
    Spacing {
        top: lerp_u32(from.top, to.top, progress),
        right: lerp_u32(from.right, to.right, progress),
        bottom: lerp_u32(from.bottom, to.bottom, progress),
        left: lerp_u32(from.left, to.left, progress),
    }
}

pub fn lerp_inset(from: Inset, to: Inset, progress: f32) -> Inset {
    Inset {
        top: lerp_optional_u32(from.top, to.top, progress),
        right: lerp_optional_u32(from.right, to.right, progress),
        bottom: lerp_optional_u32(from.bottom, to.bottom, progress),
        left: lerp_optional_u32(from.left, to.left, progress),
    }
}

fn lerp_u8(from: u8, to: u8, progress: f32) -> u8 {
    lerp_f32(from as f32, to as f32, progress)
        .round()
        .clamp(0.0, 255.0) as u8
}

fn lerp_optional_u32(from: Option<u32>, to: Option<u32>, progress: f32) -> Option<u32> {
    match (from, to) {
        (Some(from), Some(to)) => Some(lerp_u32(from, to, progress)),
        (Some(from), None) => (progress < 1.0).then_some(from),
        (None, Some(to)) => (progress > 0.0).then_some(to),
        (None, None) => None,
    }
}