cranpose-ui 0.0.59

UI primitives for Cranpose
Documentation
use cranpose_ui::{
    collect_slices_from_modifier, Color, ColorFilter, GraphicsLayer, LayerShape, Modifier,
    RenderEffect, RoundedCornerShape, TransformOrigin,
};
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;

#[test]
fn backdrop_effect_is_visible_in_modifier_slices() {
    let modifier = Modifier::empty().backdrop_effect(RenderEffect::blur(6.0));
    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices
        .graphics_layer()
        .expect("backdrop effect should produce a graphics layer");

    assert!(layer.backdrop_effect.is_some());
}

#[test]
fn graphics_layer_is_evaluated_on_slice_access() {
    let alpha = Rc::new(Cell::new(0.15f32));
    let modifier = Modifier::empty().graphics_layer({
        let alpha = alpha.clone();
        move || GraphicsLayer {
            alpha: alpha.get(),
            ..Default::default()
        }
    });
    let slices = collect_slices_from_modifier(&modifier);

    assert!((slices.graphics_layer().expect("layer").alpha - 0.15).abs() < 1e-6);
    alpha.set(0.72);
    assert!((slices.graphics_layer().expect("layer").alpha - 0.72).abs() < 1e-6);
}

#[test]
fn collecting_slices_does_not_eagerly_evaluate_lazy_graphics_layer() {
    let reads = Rc::new(Cell::new(0usize));
    let modifier = Modifier::empty().graphics_layer({
        let reads = reads.clone();
        move || {
            reads.set(reads.get() + 1);
            GraphicsLayer {
                alpha: 0.42,
                ..Default::default()
            }
        }
    });

    let slices = collect_slices_from_modifier(&modifier);

    assert_eq!(
        reads.get(),
        2,
        "lazy graphics layers should only resolve during node setup before slice access"
    );

    let layer = slices.graphics_layer().expect("layer");
    assert!((layer.alpha - 0.42).abs() < 1e-6);
    assert_eq!(
        reads.get(),
        3,
        "reading the composed graphics layer should evaluate the resolver"
    );
}

#[test]
fn stacked_lazy_translation_and_backdrop_effect_are_both_preserved() {
    let modifier = Modifier::empty()
        .graphics_layer(|| GraphicsLayer {
            translation_x: 64.0,
            translation_y: 48.0,
            ..Default::default()
        })
        .backdrop_effect(RenderEffect::blur(8.0));
    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    assert!((layer.translation_x - 64.0).abs() < 1e-6);
    assert!((layer.translation_y - 48.0).abs() < 1e-6);
    assert!(layer.backdrop_effect.is_some());
}

#[test]
fn stacked_tint_modifiers_compose_in_graphics_layer() {
    let outer = Color::from_rgba_u8(255, 128, 128, 255);
    let inner = Color::from_rgba_u8(128, 255, 64, 128);
    let modifier = Modifier::empty().tint(outer).tint(inner);
    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    let filter = layer.color_filter.expect("expected composed filter");
    let source = [0.6, 0.2, 0.8, 0.5];
    let expected = ColorFilter::tint(inner).apply_rgba(ColorFilter::tint(outer).apply_rgba(source));
    let observed = filter.apply_rgba(source);
    assert!((observed[0] - expected[0]).abs() < 1e-6);
    assert!((observed[1] - expected[1]).abs() < 1e-6);
    assert!((observed[2] - expected[2]).abs() < 1e-6);
    assert!((observed[3] - expected[3]).abs() < 1e-6);
}

#[test]
fn graphics_layer_state_writes_auto_request_render_invalidation() {
    let runtime =
        cranpose_core::runtime::Runtime::new(Arc::new(cranpose_core::runtime::DefaultScheduler));
    let x_state = cranpose_core::MutableState::with_runtime(10.0f32, runtime.handle());

    let modifier = Modifier::empty().graphics_layer(move || GraphicsLayer {
        translation_x: x_state.get(),
        ..Default::default()
    });
    let slices = collect_slices_from_modifier(&modifier);

    let _ = cranpose_ui::take_render_invalidation();
    let layer = slices.graphics_layer().expect("layer expected");
    assert!((layer.translation_x - 10.0).abs() < 1e-6);

    x_state.set(42.0);
    assert!(cranpose_ui::take_render_invalidation());
    let updated = slices.graphics_layer().expect("layer expected");
    assert!((updated.translation_x - 42.0).abs() < 1e-6);
}

#[test]
fn stacked_render_effects_chain_inner_then_outer() {
    let outer = RenderEffect::offset(5.0, -2.0);
    let inner = RenderEffect::blur(6.0);
    let modifier = Modifier::empty()
        .graphics_layer_value(GraphicsLayer {
            render_effect: Some(outer.clone()),
            ..Default::default()
        })
        .graphics_layer_value(GraphicsLayer {
            render_effect: Some(inner.clone()),
            ..Default::default()
        });
    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    match layer.render_effect {
        Some(RenderEffect::Chain { first, second }) => {
            assert_eq!(*first, inner);
            assert_eq!(*second, outer);
        }
        other => panic!("expected chained effect, got {other:?}"),
    }
}

#[test]
fn stacked_render_effects_keep_existing_when_inner_unset() {
    let outer = RenderEffect::blur(9.0);
    let modifier = Modifier::empty()
        .graphics_layer_value(GraphicsLayer {
            render_effect: Some(outer.clone()),
            ..Default::default()
        })
        .graphics_layer_value(GraphicsLayer::default());
    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    assert_eq!(layer.render_effect, Some(outer));
}

#[test]
fn stacked_graphics_layers_merge_new_transform_fields() {
    let modifier = Modifier::empty()
        .graphics_layer_value(GraphicsLayer {
            rotation_z: 12.0,
            camera_distance: 8.0,
            transform_origin: TransformOrigin::CENTER,
            shadow_elevation: 0.0,
            shape: LayerShape::Rectangle,
            clip: false,
            ..Default::default()
        })
        .graphics_layer_value(GraphicsLayer {
            rotation_z: 8.0,
            camera_distance: 14.0,
            transform_origin: TransformOrigin::new(0.2, 0.9),
            shadow_elevation: 6.0,
            shape: LayerShape::Rounded(RoundedCornerShape::uniform(9.0)),
            clip: true,
            ..Default::default()
        });

    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    assert!((layer.rotation_z - 20.0).abs() < 1e-6);
    assert!((layer.camera_distance - 14.0).abs() < 1e-6);
    assert_eq!(layer.transform_origin, TransformOrigin::new(0.2, 0.9));
    assert!((layer.shadow_elevation - 6.0).abs() < 1e-6);
    assert_eq!(
        layer.shape,
        LayerShape::Rounded(RoundedCornerShape::uniform(9.0))
    );
    assert!(layer.clip);
}

#[test]
fn inner_default_graphics_layer_resets_parent_local_fields() {
    let modifier = Modifier::empty()
        .graphics_layer_value(GraphicsLayer {
            camera_distance: 24.0,
            transform_origin: TransformOrigin::new(0.1, 0.9),
            shadow_elevation: 7.0,
            ambient_shadow_color: Color::from_rgba_u8(32, 48, 64, 255),
            spot_shadow_color: Color::from_rgba_u8(96, 112, 128, 255),
            shape: LayerShape::Rounded(RoundedCornerShape::uniform(10.0)),
            compositing_strategy: cranpose_ui::CompositingStrategy::Offscreen,
            blend_mode: cranpose_ui::BlendMode::DstOut,
            ..Default::default()
        })
        .graphics_layer_value(GraphicsLayer::default());

    let slices = collect_slices_from_modifier(&modifier);
    let layer = slices.graphics_layer().expect("layer expected");

    assert!((layer.camera_distance - 8.0).abs() < 1e-6);
    assert_eq!(layer.transform_origin, TransformOrigin::CENTER);
    assert!((layer.shadow_elevation - 0.0).abs() < 1e-6);
    assert_eq!(layer.ambient_shadow_color, Color::BLACK);
    assert_eq!(layer.spot_shadow_color, Color::BLACK);
    assert_eq!(layer.shape, LayerShape::Rectangle);
    assert_eq!(
        layer.compositing_strategy,
        cranpose_ui::CompositingStrategy::Auto
    );
    assert_eq!(layer.blend_mode, cranpose_ui::BlendMode::SrcOver);
}