cranpose-ui 0.0.58

UI primitives for Cranpose
Documentation
use cranpose_ui::{
    collect_slices_from_modifier, current_density, set_density, BlendMode, Brush, Dp, DpOffset,
    DrawCommand, LayerShape, Modifier, Point, RoundedCornerShape, Shadow, Size,
};
use cranpose_ui_graphics::{DrawPrimitive, ShadowPrimitive};

fn contains_blend_mode(primitives: &[DrawPrimitive], mode: BlendMode) -> bool {
    primitives.iter().any(|primitive| match primitive {
        DrawPrimitive::Blend {
            primitive,
            blend_mode,
        } => *blend_mode == mode || contains_blend_mode(std::slice::from_ref(primitive), mode),
        DrawPrimitive::Shadow(ShadowPrimitive::Drop {
            shape, blend_mode, ..
        }) => *blend_mode == mode || contains_blend_mode(std::slice::from_ref(shape), mode),
        DrawPrimitive::Shadow(ShadowPrimitive::Inner {
            fill,
            cutout,
            blend_mode,
            ..
        }) => {
            *blend_mode == mode
                || mode == BlendMode::DstOut
                || contains_blend_mode(std::slice::from_ref(fill), mode)
                || contains_blend_mode(std::slice::from_ref(cutout), mode)
        }
        _ => false,
    })
}

fn contains_gradient_brush(primitives: &[DrawPrimitive]) -> bool {
    primitives.iter().any(|primitive| match primitive {
        DrawPrimitive::Rect { brush, .. } | DrawPrimitive::RoundRect { brush, .. } => {
            matches!(
                brush,
                Brush::LinearGradient { .. }
                    | Brush::RadialGradient { .. }
                    | Brush::SweepGradient { .. }
            )
        }
        DrawPrimitive::Blend { primitive, .. } => {
            contains_gradient_brush(std::slice::from_ref(primitive))
        }
        DrawPrimitive::Shadow(ShadowPrimitive::Drop { shape, .. }) => {
            contains_gradient_brush(std::slice::from_ref(shape))
        }
        DrawPrimitive::Shadow(ShadowPrimitive::Inner { fill, cutout, .. }) => {
            contains_gradient_brush(std::slice::from_ref(fill))
                || contains_gradient_brush(std::slice::from_ref(cutout))
        }
        _ => false,
    })
}

fn behind_command_tags(modifier: &Modifier) -> Vec<&'static str> {
    let slices = collect_slices_from_modifier(modifier);
    slices
        .draw_commands()
        .iter()
        .filter_map(|command| match command {
            DrawCommand::Behind(draw) => {
                let primitives = draw(Size {
                    width: 64.0,
                    height: 36.0,
                });
                let has_shadow = primitives
                    .iter()
                    .any(|primitive| matches!(primitive, DrawPrimitive::Shadow(_)));
                Some(if has_shadow { "shadow" } else { "paint" })
            }
            _ => None,
        })
        .collect()
}

#[test]
fn drop_and_inner_shadow_emit_expected_draw_layers() {
    let shape = LayerShape::Rounded(RoundedCornerShape::uniform(10.0));
    let modifier = Modifier::empty()
        .drop_shadow(shape, |scope| {
            scope.radius = 12.0;
            scope.spread = 3.0;
            scope.offset = Point::new(6.0, 8.0);
        })
        .background(cranpose_ui::Color::from_rgba_u8(200, 80, 70, 255))
        .rounded_corners(10.0)
        .inner_shadow(shape, |scope| {
            scope.radius = 10.0;
            scope.offset = Point::new(4.0, 3.0);
        });

    let slices = collect_slices_from_modifier(&modifier);
    assert_eq!(slices.draw_commands().len(), 3);

    let draw_size = Size {
        width: 72.0,
        height: 44.0,
    };
    let has_behind = slices
        .draw_commands()
        .iter()
        .any(|command| matches!(command, DrawCommand::Behind(_)));
    let has_overlay = slices
        .draw_commands()
        .iter()
        .any(|command| matches!(command, DrawCommand::Overlay(_)));
    assert!(has_behind, "drop shadow should render in behind phase");
    assert!(has_overlay, "inner shadow should render in overlay phase");

    let overlay_primitives = slices
        .draw_commands()
        .iter()
        .find_map(|command| match command {
            DrawCommand::Overlay(draw) => Some(draw(draw_size)),
            _ => None,
        })
        .expect("overlay command expected");
    assert!(
        contains_blend_mode(&overlay_primitives, BlendMode::DstOut),
        "inner shadow overlay should carve interior using DstOut"
    );
}

#[test]
fn drop_shadow_before_background_renders_behind_paint() {
    let modifier = Modifier::empty()
        .drop_shadow(
            LayerShape::Rounded(RoundedCornerShape::uniform(8.0)),
            |scope| {
                scope.radius = 10.0;
                scope.offset = Point::new(4.0, 6.0);
            },
        )
        .background(cranpose_ui::Color::from_rgba_u8(220, 100, 80, 255))
        .rounded_corners(8.0);

    assert_eq!(
        behind_command_tags(&modifier),
        vec!["shadow", "paint"],
        "drop_shadow().background() must keep shadow behind content paint"
    );
}

#[test]
fn background_before_drop_shadow_renders_shadow_on_top() {
    let modifier = Modifier::empty()
        .background(cranpose_ui::Color::from_rgba_u8(220, 100, 80, 255))
        .rounded_corners(8.0)
        .drop_shadow(
            LayerShape::Rounded(RoundedCornerShape::uniform(8.0)),
            |scope| {
                scope.radius = 10.0;
                scope.offset = Point::new(4.0, 6.0);
            },
        );

    assert_eq!(
        behind_command_tags(&modifier),
        vec!["paint", "shadow"],
        "background().drop_shadow() should draw shadow above background due modifier order"
    );
}

#[test]
fn static_shadow_uses_density_when_converted_to_px() {
    struct DensityGuard(f32);
    impl Drop for DensityGuard {
        fn drop(&mut self) {
            set_density(self.0);
        }
    }

    let guard = DensityGuard(current_density());
    set_density(2.0);

    let modifier = Modifier::empty().inner_shadow_value(
        LayerShape::Rectangle,
        Shadow {
            offset: DpOffset::new(Dp(2.5), Dp(0.0)),
            ..Default::default()
        },
    );
    let slices = collect_slices_from_modifier(&modifier);
    let overlay_primitives = slices
        .draw_commands()
        .iter()
        .find_map(|command| match command {
            DrawCommand::Overlay(draw) => Some(draw(Size {
                width: 40.0,
                height: 20.0,
            })),
            _ => None,
        })
        .expect("overlay command expected");

    let cutout_x = overlay_primitives
        .iter()
        .find_map(|primitive| match primitive {
            DrawPrimitive::Shadow(ShadowPrimitive::Inner { cutout, .. }) => match cutout.as_ref() {
                DrawPrimitive::Rect { rect, .. } | DrawPrimitive::RoundRect { rect, .. } => {
                    Some(rect.x)
                }
                _ => None,
            },
            _ => None,
        });
    assert_eq!(cutout_x, Some(5.0));

    drop(guard);
}

#[test]
fn shadow_brush_and_blend_mode_are_applied() {
    let modifier = Modifier::empty().drop_shadow(LayerShape::Rectangle, |scope| {
        scope.radius = 10.0;
        scope.spread = 3.0;
        scope.alpha = 0.8;
        scope.blend_mode = BlendMode::Overlay;
        scope.brush = Some(Brush::vertical_gradient(
            vec![
                cranpose_ui::Color::from_rgba_u8(220, 40, 180, 255),
                cranpose_ui::Color::from_rgba_u8(20, 160, 240, 220),
            ],
            0.0,
            40.0,
        ));
    });

    let slices = collect_slices_from_modifier(&modifier);
    let behind_primitives = slices
        .draw_commands()
        .iter()
        .find_map(|command| match command {
            DrawCommand::Behind(draw) => Some(draw(Size {
                width: 64.0,
                height: 36.0,
            })),
            _ => None,
        })
        .expect("behind command expected");

    assert!(
        contains_blend_mode(&behind_primitives, BlendMode::Overlay),
        "drop shadow should preserve configured blend mode"
    );
    assert!(
        contains_gradient_brush(&behind_primitives),
        "drop shadow should preserve configured gradient brush"
    );
}