cranpose-ui-graphics 0.14.0

Geometry, units, paths and render data for Cranpose UI
Documentation
use crate::{
    Brush, Color, CommandRecording, DrawGroup, DrawPrimitive, DrawScope, DrawScopeDefault,
    DrawTransform, EdgeInsets, GroupMarker, Point, RecordLane, Rect, Size,
};

fn red() -> Brush {
    Brush::solid(Color::RED)
}

fn starts(primitives: &[DrawPrimitive]) -> Vec<DrawGroup> {
    primitives
        .iter()
        .filter_map(|primitive| match primitive {
            DrawPrimitive::Group(GroupMarker::Start(group)) => Some(*group),
            _ => None,
        })
        .collect()
}

fn ends(primitives: &[DrawPrimitive]) -> usize {
    primitives
        .iter()
        .filter(|primitive| matches!(primitive, DrawPrimitive::Group(GroupMarker::End)))
        .count()
}

#[test]
fn a_scale_or_turn_keeps_its_pivot_in_place() {
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(100.0, 60.0), &mut recording);
    let pivot = Point::new(30.0, 20.0);
    scope.scale(0.5, 0.25, pivot, &mut |scope| scope.draw_rect(red()));
    scope.rotate(90.0, pivot, &mut |scope| scope.draw_rect(red()));
    let groups = starts(&scope.into_primitives());
    assert_eq!(groups.len(), 2);
    for group in groups {
        let moved = group.transform_to_parent().map_point(pivot);
        assert!(
            (moved.x - pivot.x).abs() < 1e-4 && (moved.y - pivot.y).abs() < 1e-4,
            "{group:?} moves its pivot to {moved:?}"
        );
    }
}

#[test]
fn steps_nest_in_order_and_an_inset_shrinks_the_scope() {
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(100.0, 60.0), &mut recording);
    let mut seen = Vec::new();
    scope.with_transform(
        &[
            DrawTransform::Translate { dx: 5.0, dy: 0.0 },
            DrawTransform::Inset {
                left: 10.0,
                top: 4.0,
                right: 20.0,
                bottom: 6.0,
            },
            DrawTransform::ClipRect(Rect {
                x: 0.0,
                y: 0.0,
                width: 8.0,
                height: 8.0,
            }),
        ],
        &mut |scope| {
            seen.push(scope.size());
            scope.draw_rect(red());
        },
    );
    assert_eq!(seen, [Size::new(70.0, 50.0)]);
    assert_eq!(
        scope.size(),
        Size::new(100.0, 60.0),
        "the scope gets its size back"
    );
    let primitives = scope.into_primitives();
    let groups = starts(&primitives);
    assert_eq!(groups.len(), 3);
    assert_eq!(ends(&primitives), 3);
    assert_eq!(groups[0].translation, Point::new(5.0, 0.0));
    assert_eq!(groups[1].translation, Point::new(10.0, 4.0));
    assert!(groups[2].clip);
    assert!(matches!(
        primitives[3],
        DrawPrimitive::Rect { rect, .. } if rect == Rect::from_size(Size::new(70.0, 50.0))
    ));
}

#[test]
fn a_path_clip_ends_with_its_mask_inside_an_isolated_group() {
    let mut path = crate::Path::new();
    path.move_to(Point::new(10.0, 10.0));
    path.line_to(Point::new(30.0, 10.0));
    path.line_to(Point::new(20.0, 30.0));
    path.close();
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0), &mut recording);
    scope.clip_path(&path, &mut |scope| scope.draw_rect(red()));
    let primitives = scope.into_primitives();
    let [group] = starts(&primitives)[..] else {
        panic!("one group");
    };
    assert!(group.isolated && group.clip);
    let mask = &primitives[primitives.len() - 2];
    assert!(
        matches!(
            mask,
            DrawPrimitive::Blend { primitive, blend_mode: crate::BlendMode::DstOut }
                if matches!(**primitive, DrawPrimitive::Image { rect, .. } if rect == group.bounds)
        ),
        "the mask covers the group's bounds, got {mask:?}"
    );
}

#[test]
fn walking_a_recording_draws_what_its_iterator_yields() {
    let image = crate::ImageBitmap::from_rgba8(2, 2, vec![255; 16]).expect("image");
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0), &mut recording);
    scope.draw_rect(red());
    scope.translate(3.0, 0.0, &mut |scope| {
        scope.scale(2.0, 2.0, Point::ZERO, &mut |scope| {
            scope.draw_rect(red());
            scope.draw_content();
            scope.draw_image(image.clone());
        });
    });
    scope.draw_rect(red());
    scope.finish();
    for segments in [
        recording.all_segments(),
        recording.content_split(true),
        recording.content_split(false),
    ] {
        let iterated: Vec<_> = recording.primitives(segments.clone()).collect();
        let mut walked = Vec::new();
        recording.for_each_primitive(segments, |primitive| walked.push(primitive));
        assert_eq!(walked, iterated);
    }
    let mut walked = Vec::new();
    recording.for_each_primitive(recording.all_segments(), |primitive| walked.push(primitive));
    // Both groups open again after the content marker.
    assert_eq!((starts(&walked).len(), ends(&walked)), (4, 4));
    assert!(
        walked
            .iter()
            .any(|primitive| matches!(primitive, DrawPrimitive::Image { .. }))
    );
}

#[test]
fn content_drawn_inside_a_group_leaves_both_halves_balanced() {
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0), &mut recording);
    scope.translate(3.0, 0.0, &mut |scope| {
        scope.scale(2.0, 2.0, Point::ZERO, &mut |scope| {
            scope.draw_rect(red());
            scope.draw_content();
            scope.draw_rect(red());
        });
    });
    scope.finish();
    let balance = |segments: std::ops::Range<u32>| {
        recording
            .segments_in(&segments)
            .fold((0i32, true), |(depth, never_below), segment| {
                let depth = depth
                    + match segment.lane {
                        RecordLane::GroupStart => 1,
                        RecordLane::GroupEnd => -1,
                        _ => 0,
                    };
                (depth, never_below && depth >= 0)
            })
    };
    assert_eq!(balance(recording.content_split(true)), (0, true));
    assert_eq!(balance(recording.content_split(false)), (0, true));
    let behind: Vec<_> = recording
        .primitives(recording.content_split(true))
        .collect();
    let overlay: Vec<_> = recording
        .primitives(recording.content_split(false))
        .collect();
    assert_eq!(
        starts(&behind),
        starts(&overlay),
        "the overlay reopens the same groups"
    );
}

#[test]
fn a_padded_draw_moves_its_groups_and_not_what_they_hold() {
    let mut recording = CommandRecording::default();
    let mut scope = DrawScopeDefault::new(Size::new(40.0, 40.0), &mut recording);
    scope.draw_inset(EdgeInsets::from_components(4.0, 6.0, 0.0, 0.0), |inner| {
        inner.draw_rect_at(Rect::from_size(Size::new(2.0, 2.0)), red());
        inner.translate(10.0, 0.0, &mut |scope| {
            scope.draw_rect_at(Rect::from_size(Size::new(2.0, 2.0)), red());
        });
    });
    let primitives = scope.into_primitives();
    assert!(matches!(
        primitives[0],
        DrawPrimitive::Rect { rect, .. } if (rect.x, rect.y) == (4.0, 6.0)
    ));
    assert_eq!(starts(&primitives)[0].translation, Point::new(14.0, 6.0));
    assert!(matches!(
        primitives[2],
        DrawPrimitive::Rect { rect, .. } if (rect.x, rect.y) == (0.0, 0.0)
    ));
}