cranpose-ui-graphics 0.9.1

Pure math/data for drawing & units in Cranpose
Documentation
use super::*;

#[test]
fn alpha_pixels_keep_their_allocation_and_expand_exactly() {
    let alpha: Vec<u8> = (0..=255).collect();
    let address = alpha.as_ptr();
    let image = ImageBitmap::from_alpha8(16, 16, [19, 127, 239], alpha).expect("mask");
    assert_eq!(
        image.format(),
        ImagePixelFormat::Alpha8 {
            color: [19, 127, 239]
        }
    );
    assert_eq!(image.pixels().as_ptr(), address);
    assert_eq!(image.pixels().len(), 256);
    assert!(!image.is_opaque());
    assert_eq!(image.intrinsic_size(), Size::new(16.0, 16.0));
    let clone = image.clone();
    assert_eq!(clone.pixels().as_ptr(), address);
    let expanded = image.rgba8_pixels();
    assert!(matches!(expanded, Cow::Owned(_)));
    for (index, pixel) in expanded.as_chunks::<4>().0.iter().enumerate() {
        assert_eq!(*pixel, [19, 127, 239, index as u8]);
        assert_eq!(image.rgba8_pixel(index), *pixel);
    }
    let rgba = ImageBitmap::from_rgba8(16, 16, expanded.into_owned()).expect("rgba");
    assert_eq!(rgba.format(), ImagePixelFormat::Rgba8);
    assert!(matches!(rgba.rgba8_pixels(), Cow::Borrowed(_)));
    assert_eq!(rgba.rgba8_pixels().as_ptr(), rgba.pixels().as_ptr());
    for index in 0..256 {
        assert_eq!(rgba.rgba8_pixel(index), image.rgba8_pixel(index));
    }
    assert_ne!(
        rgba.id(),
        image.id(),
        "texture formats must have distinct cache identities"
    );
}

#[test]
fn alpha_bitmap_identity_includes_color_dimensions_and_coverage() {
    let image = ImageBitmap::from_alpha8(2, 1, [12, 34, 56], vec![255, 255]).expect("mask");
    let same = ImageBitmap::from_alpha8(2, 1, [12, 34, 56], vec![255, 255]).expect("same");
    assert_eq!(image, same);
    assert!(image.is_opaque());
    for other in [
        ImageBitmap::from_alpha8(1, 2, [12, 34, 56], vec![255, 255]),
        ImageBitmap::from_alpha8(2, 1, [13, 34, 56], vec![255, 255]),
        ImageBitmap::from_alpha8(2, 1, [12, 34, 56], vec![255, 254]),
    ] {
        assert_ne!(image, other.expect("distinct mask"));
    }
}

#[test]
fn alpha_constructor_rejects_invalid_dimensions_and_lengths() {
    for (width, height, alpha, expected) in [
        (0, 1, vec![], ImageBitmapError::InvalidDimensions),
        (1, 0, vec![], ImageBitmapError::InvalidDimensions),
        (
            u32::MAX,
            u32::MAX,
            vec![],
            ImageBitmapError::DimensionsTooLarge,
        ),
        (
            2,
            1,
            vec![255],
            ImageBitmapError::PixelDataLengthMismatch {
                expected: 2,
                actual: 1,
            },
        ),
        (
            2,
            1,
            vec![255; 3],
            ImageBitmapError::PixelDataLengthMismatch {
                expected: 2,
                actual: 3,
            },
        ),
    ] {
        assert_eq!(
            ImageBitmap::from_alpha8(width, height, [1, 2, 3], alpha),
            Err(expected)
        );
    }
}

#[test]
fn pixel_reads_reject_out_of_bounds_indices_in_both_formats() {
    for image in [
        ImageBitmap::from_alpha8(1, 1, [1, 2, 3], vec![255]).expect("mask"),
        ImageBitmap::from_rgba8(1, 1, vec![1, 2, 3, 255]).expect("rgba"),
    ] {
        assert!(std::panic::catch_unwind(|| image.rgba8_pixel(1)).is_err());
        assert!(std::panic::catch_unwind(|| image.rgba8_pixel(usize::MAX)).is_err());
    }
}

#[test]
fn owned_pixels_keep_their_allocation_through_clones() {
    let pixels = vec![17; 64 * 64 * 4];
    assert_eq!(pixels.len(), pixels.capacity());
    let address = pixels.as_ptr();
    let bitmap = ImageBitmap::from_rgba8(64, 64, pixels).expect("owned bitmap");
    assert_eq!(bitmap.pixels().as_ptr(), address);
    let clone = bitmap.clone();
    drop(bitmap);
    assert_eq!(clone.pixels().as_ptr(), address);
    assert!(clone.pixels().iter().all(|byte| *byte == 17));
}

#[test]
fn bitmap_handles_share_their_metadata() {
    assert_eq!(size_of::<ImageBitmap>(), size_of::<Arc<()>>());
    assert_eq!(size_of::<Option<ImageBitmap>>(), size_of::<ImageBitmap>());
}

#[test]
fn borrowed_pixels_are_owned_before_the_source_changes() {
    let mut pixels = vec![17, 23, 41, 255, 3, 5, 7, 128];
    let borrowed = ImageBitmap::from_rgba8_slice(2, 1, &pixels).expect("borrowed bitmap");
    let owned = ImageBitmap::from_rgba8(2, 1, pixels.clone()).expect("owned bitmap");
    assert_eq!(borrowed, owned);
    assert_eq!(borrowed.is_opaque(), owned.is_opaque());
    pixels.fill(0);
    drop(pixels);
    assert_eq!(borrowed.pixels(), owned.pixels());
    assert_eq!(borrowed.intrinsic_size(), owned.intrinsic_size());
}

#[test]
fn both_constructors_reject_invalid_dimensions_and_lengths() {
    for (width, height, pixels, expected) in [
        (0, 1, vec![0; 4], ImageBitmapError::InvalidDimensions),
        (1, 0, vec![], ImageBitmapError::InvalidDimensions),
        (
            u32::MAX,
            u32::MAX,
            vec![],
            ImageBitmapError::DimensionsTooLarge,
        ),
        (
            1,
            1,
            vec![0; 3],
            ImageBitmapError::PixelDataLengthMismatch {
                expected: 4,
                actual: 3,
            },
        ),
        (
            1,
            1,
            vec![0; 5],
            ImageBitmapError::PixelDataLengthMismatch {
                expected: 4,
                actual: 5,
            },
        ),
    ] {
        assert_eq!(
            ImageBitmap::from_rgba8_slice(width, height, &pixels),
            Err(expected.clone())
        );
        assert_eq!(
            ImageBitmap::from_rgba8(width, height, pixels),
            Err(expected)
        );
    }
}

#[test]
fn the_same_bytes_under_transposed_dimensions_are_different_bitmaps() {
    let pixels = vec![1, 2, 3, 255, 4, 5, 6, 255];
    let wide = ImageBitmap::from_rgba8(2, 1, pixels.clone()).expect("wide bitmap");
    let tall = ImageBitmap::from_rgba8(1, 2, pixels).expect("tall bitmap");

    assert_ne!(wide.id(), tall.id());
}

#[test]
fn one_flipped_bit_deep_in_a_large_bitmap_changes_its_id() {
    let (width, height) = (640u32, 360u32);
    let mut pixels: Vec<u8> = (0..width * height * 4)
        .map(|index| (index.wrapping_mul(2_654_435_761) >> 11) as u8)
        .collect();
    let original = ImageBitmap::from_rgba8_slice(width, height, &pixels).expect("original");
    let last = pixels.len() - 1;
    pixels[last - 4096] ^= 0b0000_0100;
    let flipped = ImageBitmap::from_rgba8_slice(width, height, &pixels).expect("flipped");

    assert_ne!(original.id(), flipped.id());
}

#[test]
fn every_colour_filter_states_itself_as_a_matrix() {
    let identity = ColorFilter::Matrix([
        1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
        1.0, 0.0,
    ]);
    assert_eq!(
        ColorFilter::matrix(identity.as_matrix()).as_matrix(),
        identity.as_matrix(),
        "a matrix filter is its own matrix"
    );

    let tint = Color(0.25, 0.5, 0.75, 1.0);
    let matrix = ColorFilter::tint(tint).as_matrix();
    for row in 0..4 {
        for column in 0..3 {
            assert_eq!(matrix[row * 5 + column], 0.0, "row {row} column {column}");
        }
        assert_eq!(matrix[row * 5 + 4], 0.0, "row {row} offset");
    }
    assert_eq!(matrix[3], tint.r());
    assert_eq!(matrix[8], tint.g());
    assert_eq!(matrix[13], tint.b());
    assert_eq!(matrix[18], tint.a());

    let modulate = ColorFilter::modulate(Color(0.5, 0.25, 0.125, 1.0)).as_matrix();
    assert_eq!(modulate[0], 0.5);
    assert_eq!(modulate[6], 0.25);
    assert_eq!(modulate[12], 0.125);
    assert_eq!(modulate[18], 1.0);
}

#[test]
fn image_bitmap_ids_do_not_use_process_global_or_allocation_identity() {
    let source = include_str!("../image.rs");
    let image_counter = ["static ", "NEXT_IMAGE_BITMAP_ID"].concat();
    let pixel_pointer = ["Arc::", "as_ptr(&self.pixels)"].concat();

    assert!(
        !source.contains(&image_counter) && !source.contains(&pixel_pointer),
        "image bitmap ids must be derived from bitmap content, not global counters or allocation addresses"
    );
}

#[test]
fn from_rgba8_accepts_valid_data() {
    let bitmap =
        ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 255]).expect("valid bitmap");

    assert_eq!(bitmap.width(), 2);
    assert_eq!(bitmap.height(), 1);
    assert_eq!(bitmap.pixels().len(), 8);
    assert!(bitmap.is_opaque());
}

#[test]
fn from_rgba8_tracks_transparency() {
    let bitmap =
        ImageBitmap::from_rgba8(2, 1, vec![255, 0, 0, 255, 0, 255, 0, 128]).expect("valid bitmap");

    assert!(!bitmap.is_opaque());
}

#[test]
fn from_rgba8_rejects_zero_dimensions() {
    let err = ImageBitmap::from_rgba8(0, 2, vec![]).expect_err("must fail");
    assert_eq!(err, ImageBitmapError::InvalidDimensions);
}

#[test]
fn from_rgba8_rejects_wrong_pixel_length() {
    let err = ImageBitmap::from_rgba8(2, 2, vec![0; 15]).expect_err("must fail");
    assert_eq!(
        err,
        ImageBitmapError::PixelDataLengthMismatch {
            expected: 16,
            actual: 15,
        }
    );
}

#[test]
fn from_rgba8_slice_accepts_valid_data() {
    let pixels = [255u8, 0, 0, 255];
    let bitmap = ImageBitmap::from_rgba8_slice(1, 1, &pixels).expect("valid bitmap");
    assert_eq!(bitmap.pixels(), &pixels);
}

#[test]
fn ids_are_content_derived() {
    let a = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap a");
    let a_clone = a.clone();
    let b = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 0, 255]).expect("bitmap b");
    let c = ImageBitmap::from_rgba8(1, 1, vec![0, 0, 1, 255]).expect("bitmap c");
    let d = ImageBitmap::from_rgba8(2, 1, vec![0, 0, 0, 255, 0, 0, 0, 255]).expect("bitmap d");

    assert_eq!(a.id(), a_clone.id());
    assert_eq!(a.id(), b.id());
    assert_ne!(a.id(), c.id());
    assert_ne!(a.id(), d.id());
}

#[test]
fn intrinsic_size_matches_dimensions() {
    let bitmap = ImageBitmap::from_rgba8(3, 4, vec![255; 3 * 4 * 4]).expect("bitmap");
    assert_eq!(bitmap.intrinsic_size(), Size::new(3.0, 4.0));
}

#[test]
fn tint_filter_multiplies_channels() {
    let filter = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 64, 128));
    let tinted = filter.apply_rgba([1.0, 0.5, 1.0, 1.0]);
    assert!((tinted[0] - (128.0 / 255.0)).abs() < 1e-5);
    assert!((tinted[1] - 0.5).abs() < 1e-5);
    assert!((tinted[2] - (64.0 / 255.0)).abs() < 1e-5);
    assert!((tinted[3] - (128.0 / 255.0)).abs() < 1e-5);
}

#[test]
fn tint_constructor_matches_variant() {
    let color = Color::from_rgba_u8(10, 20, 30, 40);
    assert_eq!(ColorFilter::tint(color), ColorFilter::Tint(color));
}

#[test]
fn tint_filter_uses_src_in_behavior() {
    let filter = ColorFilter::tint(Color::from_rgba_u8(255, 128, 0, 128));
    let tinted = filter.apply_rgba([0.2, 0.4, 0.8, 0.25]);
    assert!((tinted[0] - 0.25).abs() < 1e-5);
    assert!((tinted[1] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
    assert!(tinted[2].abs() < 1e-5);
    assert!((tinted[3] - (0.25 * 128.0 / 255.0)).abs() < 1e-5);
}

#[test]
fn matrix_filter_transforms_channels() {
    let matrix = [
        1.0, 0.0, 0.0, 0.0, 0.1, 0.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
        1.0, 0.0,
    ];
    let filter = ColorFilter::matrix(matrix);
    let transformed = filter.apply_rgba([0.2, 0.6, 0.9, 0.4]);
    assert!((transformed[0] - 0.3).abs() < 1e-5);
    assert!((transformed[1] - 0.3).abs() < 1e-5);
    assert!((transformed[2] - 0.4).abs() < 1e-5);
    assert!((transformed[3] - 0.4).abs() < 1e-5);
}

#[test]
fn filter_compose_applies_in_order() {
    let first = ColorFilter::modulate(Color::from_rgba_u8(128, 255, 255, 255));
    let second = ColorFilter::tint(Color::from_rgba_u8(255, 0, 0, 255));
    let chained = first.compose(second);
    let direct_second = second.apply_rgba(first.apply_rgba([0.8, 0.4, 0.2, 0.5]));
    let composed = chained.apply_rgba([0.8, 0.4, 0.2, 0.5]);
    assert!((direct_second[0] - composed[0]).abs() < 1e-5);
    assert!((direct_second[1] - composed[1]).abs() < 1e-5);
    assert!((direct_second[2] - composed[2]).abs() < 1e-5);
    assert!((direct_second[3] - composed[3]).abs() < 1e-5);
}