argui-paint 0.2.1

Renderer-independent painting primitives for Argui
Documentation
use argui_core::{Color, Point, Rect, Size};
use argui_paint::{
    BlendMode, CornerRadii, DisplayList, DisplayListError, EffectArgument, EffectId,
    EffectInstance, EffectValue, Filter, LayerMask, LayerStyle, Shadow,
};

fn bounds() -> Rect {
    Rect::new(Point::new(20.0, 30.0), Size::new(100.0, 50.0))
}

#[test]
fn effect_layers_are_explicit_and_compute_conservative_bounds() {
    let layer = LayerStyle::new(bounds())
        .opacity(0.8)
        .blend(BlendMode::Screen)
        .mask(LayerMask::Bounds)
        .filter(Filter::Blur(4.0))
        .shadow(Shadow::drop(
            [20.0, -2.0],
            8.0,
            Color::srgba(0.0, 0.0, 0.0, 0.5),
        ));

    assert!(layer.requires_offscreen());
    assert_eq!(layer.expanded_bounds().origin, Point::new(-24.0, -14.0));
    assert_eq!(layer.expanded_bounds().size, Size::new(188.0, 138.0));
    assert!(!LayerStyle::new(bounds()).requires_offscreen());
    assert_eq!(Filter::Contrast(2.0).expansion(), 0.0);

    let scaled = layer.scaled(1.5);
    assert_eq!(scaled.bounds.origin, Point::new(30.0, 45.0));
    assert_eq!(scaled.bounds.size, Size::new(150.0, 75.0));
    assert!(matches!(scaled.filters[0], Filter::Blur(6.0)));
    assert_eq!(scaled.shadows[0].offset, [30.0, -3.0]);
    assert_eq!(
        LayerStyle::new(bounds())
            .mask(LayerMask::Rounded(CornerRadii::all(3.0)))
            .scaled(2.0)
            .mask,
        LayerMask::Rounded(CornerRadii::all(6.0))
    );

    let glow = Shadow::glow(20.0, Color::WHITE)
        .blur(24.0)
        .spread(3.0)
        .inset(true);
    assert_eq!(glow.offset, [0.0, 0.0]);
    assert_eq!(glow.blur, 24.0);
    assert_eq!(glow.spread, 3.0);
    assert!(glow.inset);
}

#[test]
fn every_layer_feature_independently_requests_offscreen_rendering() {
    let plain = LayerStyle::new(bounds());
    assert!(!plain.requires_offscreen());
    assert!(plain.clone().opacity(0.5).requires_offscreen());
    assert!(
        plain
            .clone()
            .blend(BlendMode::Multiply)
            .requires_offscreen()
    );
    assert!(
        plain
            .clone()
            .filter(Filter::Brightness(1.2))
            .requires_offscreen()
    );
    assert!(
        plain
            .clone()
            .backdrop(Filter::Saturation(1.2))
            .requires_offscreen()
    );
    assert!(
        plain
            .clone()
            .shadow(Shadow::glow(4.0, Color::WHITE))
            .requires_offscreen()
    );
    assert!(plain.mask(LayerMask::Bounds).requires_offscreen());

    let outer = LayerStyle::new(bounds()).filter(Filter::Effect(
        EffectInstance::new(EffectId::new("test.outer"), Vec::<EffectArgument>::new())
            .expansion(12.0),
    ));
    assert_eq!(outer.foreground_expansion(), 12.0);
    assert_eq!(outer.foreground_bounds().origin, Point::new(8.0, 18.0));
    assert_eq!(outer.scaled(2.0).foreground_expansion(), 24.0);
}

#[test]
fn effect_pixel_parameters_follow_dpi_without_scaling_unitless_values() {
    let effect = Filter::Effect(
        EffectInstance::new(
            EffectId::new("test.scaled"),
            [
                ("unitless", EffectValue::F32(0.5)),
                ("radius", EffectValue::LogicalPixels(12.0)),
            ],
        )
        .expansion(8.0),
    );
    let Filter::Effect(scaled) = effect.scaled(2.0) else {
        panic!("expected an effect instance");
    };
    assert_eq!(scaled.parameters[0].value, EffectValue::F32(0.5));
    assert_eq!(scaled.parameters[1].value, EffectValue::LogicalPixels(24.0));
    assert_eq!(scaled.expansion, 16.0);
}

#[test]
fn balanced_nested_layers_validate_without_gpu_state() {
    let layer = LayerStyle::new(bounds());
    let mut list = DisplayList::new();
    list.begin_layer(layer.clone());
    list.begin_layer(layer);
    list.end_layer();
    list.end_layer();
    assert_eq!(list.validate(), Ok(()));

    list.end_layer();
    assert_eq!(
        list.validate(),
        Err(DisplayListError::UnexpectedLayerEnd { command: 4 })
    );

    let mut open = DisplayList::new();
    open.begin_layer(LayerStyle::new(bounds()));
    assert_eq!(
        open.validate(),
        Err(DisplayListError::UnclosedLayers { count: 1 })
    );
    assert!(
        open.validate()
            .unwrap_err()
            .to_string()
            .contains("unclosed")
    );
}