operad 8.0.1

A cross-platform GUI library for Rust.
Documentation
use operad::native::{NativeWindowOptions, NativeWindowResult};
use operad::{
    root_style, widgets, AccessibilityMeta, AccessibilityRole, AnimatedValues,
    AnimationBlendBinding, AnimationCondition, AnimationMachine, AnimationState,
    AnimationTransition, ColorRgba, InputBehavior, LayoutStyle, ScenePrimitive, StrokeStyle,
    TextStyle, UiDocument, UiNode, UiPoint, UiSize, UiVisual, WidgetAction,
};

const INPUT_OPEN: &str = "open";
const INPUT_MORPH: &str = "morph";

fn main() -> NativeWindowResult {
    operad::native::run_app_with(
        NativeWindowOptions::new("Animation state machine").with_min_size(540.0, 360.0),
        AnimationApp::default(),
        AnimationApp::update,
        AnimationApp::view,
    )
}

#[derive(Default)]
struct AnimationApp {
    open: bool,
}

impl AnimationApp {
    fn update(&mut self, action: WidgetAction) {
        if action
            .binding
            .action_id()
            .is_some_and(|id| id.as_str() == "animation.toggle")
        {
            self.open = !self.open;
        }
    }

    fn view(&self, viewport: UiSize) -> UiDocument {
        let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
        let panel = ui.add_child(
            ui.root(),
            UiNode::container(
                "animation.panel",
                LayoutStyle::column()
                    .with_width_percent(1.0)
                    .with_height_percent(1.0)
                    .with_padding(16.0)
                    .with_gap(12.0),
            )
            .with_visual(UiVisual::panel(ColorRgba::new(13, 17, 23, 255), None, 0.0)),
        );

        widgets::label(
            &mut ui,
            panel,
            "animation.title",
            "Animation state machine",
            heading(),
            LayoutStyle::new().with_width_percent(1.0).with_height(34.0),
        );

        let row = ui.add_child(
            panel,
            UiNode::container(
                "animation.controls",
                LayoutStyle::row()
                    .with_width_percent(1.0)
                    .with_height(40.0)
                    .with_gap(10.0),
            ),
        );
        widgets::button(
            &mut ui,
            row,
            "animation.toggle",
            if self.open { "Close" } else { "Open" },
            widgets::ButtonOptions::default().with_action("animation.toggle"),
        );
        widgets::label(
            &mut ui,
            row,
            "animation.state",
            if self.open {
                "State input: open"
            } else {
                "State input: closed"
            },
            muted(),
            LayoutStyle::new().with_width(220.0).with_height(32.0),
        );

        let stage = ui.add_child(
            panel,
            UiNode::container(
                "animation.stage",
                LayoutStyle::row()
                    .with_width_percent(1.0)
                    .with_height(0.0)
                    .with_flex_grow(1.0),
            )
            .with_visual(UiVisual::panel(
                ColorRgba::new(16, 21, 28, 255),
                Some(StrokeStyle::new(ColorRgba::new(58, 68, 84, 255), 1.0)),
                6.0,
            )),
        );

        ui.add_child(
            stage,
            UiNode::scene(
                "animation.shape",
                shape_primitives(),
                LayoutStyle::new()
                    .with_width_percent(1.0)
                    .with_height_percent(1.0),
            )
            .with_input(InputBehavior::BUTTON)
            .with_animation(shape_machine(self.open))
            .with_accessibility(
                AccessibilityMeta::new(AccessibilityRole::Button)
                    .label("Animated morphing shape")
                    .focusable(),
            ),
        );
        ui
    }
}

fn shape_machine(open: bool) -> AnimationMachine {
    let closed = AnimatedValues::new(0.82, UiPoint::new(0.0, 0.0), 1.0).with_morph(0.0);
    let open_values = AnimatedValues::new(1.0, UiPoint::new(160.0, 0.0), 1.08).with_morph(1.0);
    AnimationMachine::new(
        vec![
            AnimationState::new("closed", closed),
            AnimationState::new("open", open_values),
        ],
        vec![
            AnimationTransition::when(
                "closed",
                "open",
                AnimationCondition::bool(INPUT_OPEN, true),
                0.24,
            ),
            AnimationTransition::when(
                "open",
                "closed",
                AnimationCondition::bool(INPUT_OPEN, false),
                0.18,
            ),
        ],
        "closed",
    )
    .unwrap_or_else(|_| AnimationMachine::single_state("closed", closed))
    .with_bool_input(INPUT_OPEN, open)
    .with_number_input(INPUT_MORPH, if open { 1.0 } else { 0.0 })
    .with_blend_binding(AnimationBlendBinding::new(INPUT_MORPH, "closed", "open"))
}

fn shape_primitives() -> Vec<ScenePrimitive> {
    vec![
        ScenePrimitive::MorphPolygon {
            from_points: vec![
                UiPoint::new(76.0, 54.0),
                UiPoint::new(156.0, 54.0),
                UiPoint::new(156.0, 134.0),
                UiPoint::new(76.0, 134.0),
            ],
            to_points: pentagon_points(UiPoint::new(116.0, 94.0), 48.0),
            amount: 0.0,
            fill: ColorRgba::new(120, 210, 180, 255),
            stroke: Some(StrokeStyle::new(ColorRgba::new(236, 244, 255, 255), 1.5)),
        },
        ScenePrimitive::Circle {
            center: UiPoint::new(102.0, 78.0),
            radius: 8.0,
            fill: ColorRgba::new(244, 248, 255, 255),
            stroke: None,
        },
    ]
}

fn pentagon_points(center: UiPoint, radius: f32) -> Vec<UiPoint> {
    (0..5)
        .map(|index| {
            let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
            UiPoint::new(
                center.x + angle.cos() * radius,
                center.y + angle.sin() * radius,
            )
        })
        .collect()
}

fn heading() -> TextStyle {
    TextStyle {
        font_size: 22.0,
        line_height: 30.0,
        color: ColorRgba::WHITE,
        ..TextStyle::default()
    }
}

fn muted() -> TextStyle {
    TextStyle {
        color: ColorRgba::new(166, 178, 196, 255),
        ..TextStyle::default()
    }
}