lgui-core 0.2.2

Platform-neutral runtime and UI primitives for LGUI
Documentation
use super::*;
use crate::core::ShadowStyle;

fn tree(x: f32, shadow: Option<ShadowStyle>) -> HostTree {
    let mut tree = HostTree::new();
    let mut root = UiNode::new(
        UiId::new("shadow"),
        UiNodeKind::Group,
        UiRect::new(x, 40.0, x + 40.0, 80.0),
    );
    root.shadow = shadow;
    tree.push(root);
    tree.push(
        UiNode::new(
            UiId::new("circle"),
            UiNodeKind::Ellipse,
            UiRect::new(x + 5.0, 45.0, x + 35.0, 75.0),
        )
        .parent(UiId::new("shadow"))
        .style(VisualStyle::filled(Color::WHITE)),
    );
    tree
}

#[test]
fn shadow_is_one_alpha_layer_and_does_not_change_layout_or_hit_bounds() {
    let tree = tree(40.0, Some(ShadowStyle::default()));
    let node = tree.node(&UiId::new("shadow")).unwrap();
    assert_eq!(node.layout_rect, UiRect::new(40.0, 40.0, 80.0, 80.0));
    assert_eq!(node.hit_rect, node.layout_rect);
    let scene = compile_scene(&tree);
    assert_eq!(scene.commands().len(), 1);
    let ScenePrimitive::CompositingLayer {
        rect,
        spec,
        commands,
        ..
    } = &scene.commands()[0]
    else {
        panic!("shadow layer");
    };
    assert!(rect.left < 40.0 && rect.right > 80.0 && rect.bottom > 80.0);
    assert_eq!(spec.shadow, Some(ShadowStyle::default()));
    assert_eq!(commands.len(), 1);
    assert!(matches!(commands[0], ScenePrimitive::Ellipse { .. }));
    assert_eq!(
        commands[0].rect().translate(rect.left, rect.top),
        tree.node(&UiId::new("circle")).unwrap().layout_rect
    );
    assert_eq!(scene_root_ids(&tree), vec![UiId::new("shadow")]);
    assert_eq!(
        compile_scene_root(&tree, &UiId::new("shadow")).commands(),
        scene.commands()
    );
}

#[test]
fn no_shadow_keeps_existing_scene_and_movement_reuses_shadow_content() {
    assert!(matches!(
        compile_scene(&tree(40.0, None)).commands()[0],
        ScenePrimitive::Ellipse { .. }
    ));
    let a = compile_scene(&tree(40.0, Some(ShadowStyle::default())));
    let b = compile_scene(&tree(100.0, Some(ShadowStyle::default())));
    let signature = |scene: &Scene| match scene.commands()[0] {
        ScenePrimitive::CompositingLayer {
            content_signature, ..
        } => content_signature,
        _ => panic!("layer"),
    };
    assert_eq!(signature(&a), signature(&b));
}

#[test]
fn shadow_scales_filter_distances_with_dpi() {
    let style = ShadowStyle::default()
        .offset(-2.0, 4.0)
        .blur(3.0)
        .spread(2.0);
    let logical = compile_scene(&tree(40.0, Some(style)));
    let physical = logical.project_to_physical(UiScale::new(1.5));
    let ScenePrimitive::CompositingLayer { spec, rect, .. } = physical.commands()[0] else {
        panic!("layer");
    };
    let projected = spec.shadow.unwrap();
    assert_eq!(
        (
            projected.offset_x(),
            projected.offset_y(),
            projected.blur_sigma(),
            projected.spread_radius()
        ),
        (-3.0, 6.0, 4.5, 3.0)
    );
    let expected = UiScale::new(1.5).physical_ui_rect(logical.commands()[0].rect());
    assert_eq!(
        rect,
        UiRect::new(
            expected.left.floor(),
            expected.top.floor(),
            expected.right.ceil(),
            expected.bottom.ceil()
        )
    );
}

#[test]
fn shadow_respects_internal_clip_and_excludes_detached_popups() {
    let mut tree = tree(40.0, Some(ShadowStyle::default()));
    tree.push(
        UiNode::new(
            UiId::new("popup"),
            UiNodeKind::Panel,
            UiRect::new(200.0, 200.0, 250.0, 250.0),
        )
        .parent(UiId::new("shadow"))
        .style(VisualStyle::filled(Color::WHITE))
        .render_phase(RenderPhase::Popup),
    );
    let scene = compile_scene(&tree);
    assert_eq!(scene.commands().len(), 2);
    assert_eq!(scene.commands()[1].id(), &UiId::new("popup"));
    assert!(scene.commands()[0].paint_bounds().right < 200.0);

    let mut clipped = HostTree::new();
    let bounds = UiRect::new(40.0, 40.0, 80.0, 80.0);
    clipped.push(
        UiNode::new(UiId::new("clip"), UiNodeKind::Clip, bounds).shadow(ShadowStyle::default()),
    );
    clipped.push(
        UiNode::new(
            UiId::new("wide"),
            UiNodeKind::Ellipse,
            bounds.inflate(40.0, 40.0),
        )
        .parent(UiId::new("clip"))
        .style(VisualStyle::filled(Color::WHITE)),
    );
    let scene = compile_scene(&clipped);
    let ScenePrimitive::CompositingLayer { commands, .. } = &scene.commands()[0] else {
        panic!("layer");
    };
    assert!(matches!(&commands[0], ScenePrimitive::Clip { commands, .. } if commands.len() == 1));
    assert!(scene.commands()[0].paint_bounds().right < 110.0);
}

#[test]
fn shadow_captures_overflow_and_nested_layers_without_cache_identity_collision() {
    let mut tree = HostTree::new();
    let bounds = UiRect::new(40.0, 40.0, 80.0, 80.0);
    tree.push(
        UiNode::new(UiId::new("composite"), UiNodeKind::CompositingLayer, bounds)
            .compositing_layer(CompositingLayerSpec::new().translation(70.0, 0.0))
            .shadow(ShadowStyle::default()),
    );
    tree.push(
        UiNode::new(UiId::new("shape"), UiNodeKind::Ellipse, bounds)
            .parent(UiId::new("composite"))
            .style(VisualStyle::filled(Color::WHITE)),
    );
    let scene = compile_scene(&tree);
    let ScenePrimitive::CompositingLayer {
        id, commands, rect, ..
    } = &scene.commands()[0]
    else {
        panic!("layer");
    };
    assert_ne!(id, commands[0].id());
    assert!(rect.right > 150.0);
}

#[test]
fn nested_shadow_damage_includes_blur_and_offset() {
    let first = compile_scene(&tree(40.0, Some(ShadowStyle::default())));
    let mut changed = tree(40.0, Some(ShadowStyle::default()));
    changed.push(
        UiNode::new(
            UiId::new("extra"),
            UiNodeKind::Panel,
            UiRect::new(45.0, 45.0, 50.0, 50.0),
        )
        .parent(UiId::new("shadow"))
        .style(VisualStyle::filled(Color::BLACK)),
    );
    let second = compile_scene(&changed);
    let damage = compositing_layer_damage(
        first.commands(),
        second.commands(),
        UiRect::new(0.0, 0.0, 500.0, 500.0),
    );
    let bounds = damage.into_iter().reduce(UiRect::union).unwrap();
    assert!(bounds.left <= first.commands()[0].rect().left);
    assert!(bounds.bottom >= second.commands()[0].rect().bottom);
}