lerni 0.0.6

Lerni content framework
Documentation
use leptos::prelude::*;

use crate::{Align, Frame, VAlign, use_frame};

struct SvgProperties {
    width: i32,
    height: i32,
    align: Align,
    valign: VAlign,
    scale: f32,
    flip_x: bool,
    flip_y: bool,
}

/// SVG widget.
#[component]
pub fn Svg(
    width: i32,
    height: i32,
    #[prop(default = Align::Center)] align: Align,
    #[prop(default = VAlign::Middle)] valign: VAlign,
    #[prop(default = 1.0)] scale: f32,
    #[prop(optional)] flip_x: bool,
    #[prop(optional)] flip_y: bool,
    #[prop(default = true.into(), into)] visible: Signal<bool>,
    #[prop(default = "all .3s".to_string(), into)] transition: String,
    children: Children,
) -> impl IntoView {
    let f = use_frame();
    let props = SvgProperties {
        width,
        height,
        align,
        valign,
        scale,
        flip_x,
        flip_y,
    };
    let transform = calc_transform(&f, &props);

    view! {
        <g
            transform=transform
            style:opacity=move || if visible.get() { "1" } else { "0" }
            style:visibility=move || { if visible.get() { "visible" } else { "hidden" } }
            style:transition=transition
        >
            {children()}
        </g>
    }
}

/// SVG-from-file widget.
#[component]
pub fn SvgFile(
    width: i32,
    height: i32,
    #[prop(default = Align::Center)] align: Align,
    #[prop(default = VAlign::Middle)] valign: VAlign,
    #[prop(default = 1.0)] scale: f32,
    #[prop(optional)] flip_x: bool,
    #[prop(optional)] flip_y: bool,
    #[prop(default = true.into(), into)] visible: Signal<bool>,
    #[prop(default = "all .3s".to_string(), into)] transition: String,
    src: &'static str,
) -> impl IntoView {
    let f = use_frame();
    let props = SvgProperties {
        width,
        height,
        align,
        valign,
        scale,
        flip_x,
        flip_y,
    };
    let transform = calc_transform(&f, &props);

    view! {
        <g
            transform=transform
            inner_html=src
            style:opacity=move || if visible.get() { "1" } else { "0" }
            style:visibility=move || { if visible.get() { "visible" } else { "hidden" } }
            style:transition=transition
        ></g>
    }
}

fn calc_transform(f: &Frame, props: &SvgProperties) -> String {
    let scale = if matches!(props.align, Align::Fill) || matches!(props.valign, VAlign::Fill) {
        let sx = f.width as f32 / props.width as f32;
        let sy = f.height as f32 / props.height as f32;
        sx.min(sy)
    } else {
        props.scale
    };

    let width = (scale * props.width as f32).round() as i32;
    let height = (scale * props.height as f32).round() as i32;

    let mut x = match props.align {
        Align::Left => f.x,
        Align::Center | Align::Fill => f.x + (f.width - width) / 2,
        Align::Right => f.x + f.width - width,
    };
    let mut y = match props.valign {
        VAlign::Top => f.y,
        VAlign::Middle | VAlign::Fill => f.y + (f.height - height) / 2,
        VAlign::Bottom => f.y + f.height - height,
    };

    let mut sx = scale;
    let mut sy = scale;

    if props.flip_x {
        sx = -sx;
        x += width;
    }
    if props.flip_y {
        sy = -sy;
        y += height;
    }
    format!("translate({x} {y}) scale({sx} {sy})")
}