beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
use super::transition::color_nearly_equal;
use super::*;
use crate::style::resolve_color_value;
use crate::utility::{UtilityTransitionProperty, UtilityVal, UtilityVisualStylePatch};
use bevy::time::TimeUpdateStrategy;
use bevy::ui::Val::Px;
use std::time::Duration;

fn sample_styles() -> UiStateVisualStyles {
    UiStateVisualStyles {
        base: UtilityVisualStylePatch {
            background_color: Some("var(--color-button-bg)".to_string()),
            text_color: Some("var(--color-button-text)".to_string()),
            border_color: Some("var(--color-panel-subtle-border)".to_string()),
            ..default()
        },
        hover: UtilityVisualStylePatch {
            background_color: Some("var(--color-button-bg-hover)".to_string()),
            ..default()
        },
        active: UtilityVisualStylePatch {
            background_color: Some("var(--color-button-bg-active)".to_string()),
            ..default()
        },
        focus: UtilityVisualStylePatch {
            outline_width: Some(UtilityVal::Px(2.0)),
            outline_color: Some("var(--color-primary)".to_string()),
            ..default()
        },
        disabled: UtilityVisualStylePatch::default(),
    }
}

fn sample_transition_styles() -> UiStateVisualStyles {
    let mut styles = sample_styles();
    styles.base.transition_property = Some(UtilityTransitionProperty::Colors);
    styles.base.transition_duration_ms = Some(150.0);
    styles
}

#[test]
fn active_overrides_hover_and_focus_keeps_outline() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins);
    app.add_plugins(UiStateStylePlugin);

    let entity = app
        .world_mut()
        .spawn((
            sample_styles(),
            BackgroundColor(Color::NONE),
            BorderColor::all(Color::NONE),
            TextColor(Color::NONE),
            Outline::new(Px(0.0), Px(2.0), Color::NONE),
            crate::focus::UiHovered,
            crate::focus::UiFocused,
            crate::focus::UiPressed,
        ))
        .id();

    app.update();
    app.update();

    let entity_ref = app.world().entity(entity);
    let background = entity_ref.get::<BackgroundColor>().expect("background");
    let outline = entity_ref.get::<Outline>().expect("outline");
    assert_eq!(
        background.0,
        resolve_color_value("var(--color-button-bg-active)").expect("active color")
    );
    assert_eq!(outline.width, Px(2.0));
    assert_eq!(
        outline.color,
        resolve_color_value("var(--color-primary)").expect("primary color")
    );
}

#[test]
fn state_source_uses_parent_state() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins);
    app.add_plugins(UiStateStylePlugin);

    let parent = app.world_mut().spawn(crate::focus::UiHovered).id();
    let child = app
        .world_mut()
        .spawn((
            sample_styles(),
            UiStateStyleSource(parent),
            TextColor(Color::NONE),
        ))
        .id();

    app.update();
    app.update();

    let text = app
        .world()
        .entity(child)
        .get::<TextColor>()
        .expect("text color");
    assert_eq!(
        text.0,
        resolve_color_value("var(--color-button-text)").expect("text color")
    );
}

#[test]
fn color_transition_reaches_target() {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins);
    app.add_plugins(UiStateStylePlugin);

    let entity = app
        .world_mut()
        .spawn((
            sample_transition_styles(),
            BackgroundColor(resolve_color_value("var(--color-button-bg)").expect("base color")),
        ))
        .id();

    app.update();
    app.world_mut()
        .entity_mut(entity)
        .insert(crate::focus::UiHovered);
    app.update();
    app.update();

    app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(
        75,
    )));
    app.update();
    let mid = app
        .world()
        .entity(entity)
        .get::<BackgroundColor>()
        .expect("background")
        .0;
    let target = resolve_color_value("var(--color-button-bg-hover)").expect("hover color");
    assert!(!color_nearly_equal(mid, target));

    app.insert_resource(TimeUpdateStrategy::ManualDuration(Duration::from_millis(
        100,
    )));
    app.update();
    let final_color = app
        .world()
        .entity(entity)
        .get::<BackgroundColor>()
        .expect("background")
        .0;
    assert!(color_nearly_equal(final_color, target));
}