Selkie.git | t/ | 68-image-clip-only-math.rakutest


use Test;
use lib 'lib';

use Selkie::Widget::Image;  # exports compute-clip-only-blit

plan 8;

# A square image whose natural rendering is a SUBSET of the widget cell
# rect (rcellx < self-cols). This is the atma case: 256×256 source,
# 24×9 widget, cell-px (13, 29) → rcellx=21, rcelly=9.
my %atma-base =
    self-rows  => 9,  self-cols  => 24,
    cell-px-y  => 29, cell-px-x  => 13,
    rcelly     => 9,  rcellx     => 21,
    dest-y     => 6,  dest-x     => 1,
    source-col => 0,  cols       => 24,
;

# A wide image whose natural rendering covers the widget's full width
# but with VERTICAL margin (rcelly < self-rows). The spaceship case:
# 835×384 source, 24×8 widget, cell-px (13, 29) → rcellx=24, rcelly=5.
my %spaceship-base =
    self-rows  => 8,  self-cols  => 24,
    cell-px-y  => 29, cell-px-x  => 13,
    rcelly     => 5,  rcellx     => 24,
    dest-y     => 0,  dest-x     => 1,
    source-col => 0,  cols       => 24,
;

subtest "fully visible — full source crop, plane = image cell rect" => {
    plan 8;
    # atma fully visible: image at widget rows 0..8, cols 1..21.
    my %r = compute-clip-only-blit(
        |%atma-base, source-row => 0, rows => 9,
    );
    ok %r, "renders a blit";
    is %r<begy>,  0,            "begy = 0 (top of source)";
    is %r<leny>,  9 * 29,       "leny = full rcelly × cell-px-y";
    is %r<begx>,  0,            "begx = 0";
    is %r<lenx>,  21 * 13,      "lenx = full rcellx × cell-px-x";
    is %r<blit-rows>, 9,        "blit-rows = rcelly";
    is %r<blit-cols>, 21,       "blit-cols = rcellx";
    is %r<blit-dest-x>, 1 + 1,
        "blit-dest-x offset by img-left-cell (= (24-21)/2 = 1)";
};

subtest "top clipped (scrolling down) — source crop is BOTTOM of image" => {
    plan 6;
    # atma top 3 cells clipped: source-row=3, rows=6 visible.
    # Image top is at img-top-cell=0, so 3 image rows are clipped from top.
    my %r = compute-clip-only-blit(
        |%atma-base, source-row => 3, rows => 6,
    );
    ok %r, "renders a blit";
    is %r<begy>,  3 * 29,
        "begy = 3 rows × cell-px-y (skipping the clipped top)";
    is %r<leny>,  6 * 29,
        "leny = visible row count × cell-px-y";
    is %r<blit-rows>, 6,        "blit-rows = visible cell count";
    is %r<blit-cols>, 21,
        "blit-cols UNCHANGED (no horizontal scaling per scroll)";
    is %r<lenx>,  21 * 13,
        "lenx UNCHANGED (no horizontal source-crop scaling)";
};

subtest "bottom clipped (scrolling up) — source crop is TOP of image" => {
    plan 6;
    # atma bottom 3 cells clipped: source-row=0, only top 6 widget rows
    # are visible. Image's top is at widget row 0, so 6 image rows visible.
    my %r = compute-clip-only-blit(
        |%atma-base, source-row => 0, rows => 6,
    );
    ok %r, "renders a blit";
    is %r<begy>,  0,                 "begy = 0 (start at top of source)";
    is %r<leny>,  6 * 29,            "leny = visible row count × cell-px-y";
    is %r<blit-rows>, 6,             "blit-rows = visible cell count";
    is %r<blit-cols>, 21,            "blit-cols UNCHANGED";
    is %r<lenx>,  21 * 13,           "lenx UNCHANGED";
};

subtest "image entirely outside viewport — empty hash" => {
    plan 3;
    # source-row past the image's bottom edge.
    my %r1 = compute-clip-only-blit(
        |%atma-base, source-row => 100, rows => 5,
    );
    nok %r1, "scroll way past bottom → no blit";

    # rows=0 — viewport has zero height.
    my %r2 = compute-clip-only-blit(
        |%atma-base, source-row => 0, rows => 0,
    );
    nok %r2, "zero visible rows → no blit";

    # vertical scroll past the image's top edge by enough that
    # vis-cell-y-end <= vis-cell-y-start.
    my %r3 = compute-clip-only-blit(
        |%atma-base, source-row => 9, rows => 5,
    );
    nok %r3, "scroll exactly past bottom edge → no blit";
};

subtest "image with vertical margin (spaceship) clipped" => {
    plan 8;
    # spaceship natural: rcelly=5, rcellx=24, in 8-row widget.
    # img-top-cell = (8-5)/2 = 1.

    # Fully visible card: source-row=0, rows=8. Visible image cells =
    # [max(1,0) .. min(6, 8)) = [1, 6) = 5 cells.
    my %full = compute-clip-only-blit(
        |%spaceship-base, source-row => 0, rows => 8,
    );
    ok %full, "fully-visible: renders";
    is %full<blit-rows>, 5,           "blit-rows = full rcelly";
    is %full<blit-dest-y>, 0 + 1,     "dest-y offset by img-top-cell";
    is %full<begy>, 0,                "begy = 0";

    # Clip top by 3 of the widget rows (source-row=3, rows=5).
    # Visible image cells = [max(1,3) .. min(6, 8)) = [3, 6) = 3 cells.
    # Image-local row range = [3-1, 6-1) = [2, 5).
    my %tc = compute-clip-only-blit(
        |%spaceship-base, source-row => 3, rows => 5,
    );
    ok %tc, "top-clipped: renders";
    is %tc<begy>, 2 * 29,
        "begy = (image-local first visible row) × cell-px-y";
    is %tc<leny>, 3 * 29,             "leny = 3 cells × cell-px-y";
    is %tc<blit-rows>, 3,             "blit-rows = visible cells";
};

subtest "width invariance across every vertical scroll position" => {
    # The plan's central guarantee: for atma in a 9-row widget, blit-cols
    # and lenx stay constant for every source-row that produces a blit.
    my $expected-cols = 21;
    my $expected-lenx = 21 * 13;
    my @seen-cols;
    my @seen-lenx;
    for 0 .. 8 -> $sr {
        for 1 .. (9 - $sr) -> $rows {
            my %r = compute-clip-only-blit(
                |%atma-base, source-row => $sr, rows => $rows,
            );
            next unless %r;
            @seen-cols.push(%r<blit-cols>);
            @seen-lenx.push(%r<lenx>);
        }
    }
    plan 2;
    is @seen-cols.unique, ($expected-cols,),
        "blit-cols is always $expected-cols across all vertical scroll positions";
    is @seen-lenx.unique, ($expected-lenx,),
        "lenx is always $expected-lenx across all vertical scroll positions";
};

subtest "blit-dest-y tracks visible-image-top relative to viewport" => {
    plan 4;
    # For an image at img-top-cell = 1 (spaceship), scrolling such that
    # source-row=0 (image's top still visible): blit-dest-y = dest-y + 1
    # (image starts 1 cell below the viewport top).
    my %r1 = compute-clip-only-blit(
        |%spaceship-base, source-row => 0, rows => 8, dest-y => 7,
    );
    is %r1<blit-dest-y>, 7 + 1,
        "image at top of viewport: dest-y + img-top-cell";

    # source-row=3: top 3 widget rows clipped, image's top (cell 1) is
    # clipped too. Image's first visible cell is widget row 3 → image-local
    # row 2. blit-dest-y = dest-y + (3 - 3) = dest-y.
    my %r2 = compute-clip-only-blit(
        |%spaceship-base, source-row => 3, rows => 5, dest-y => 0,
    );
    is %r2<blit-dest-y>, 0,
        "top-clipped past img-top-cell: dest-y + 0";

    # source-row=1: image's top at widget cell 1, source-row at 1 means
    # widget row 0 is clipped (BUT not part of image — img-top-cell=1).
    # vis-cell-y-start = max(1, 1) = 1. blit-dest-y = dest-y + 0.
    my %r3 = compute-clip-only-blit(
        |%spaceship-base, source-row => 1, rows => 7, dest-y => 0,
    );
    is %r3<blit-dest-y>, 0,
        "source-row exactly at img-top-cell: dest-y + 0";

    # source-row=2 with image at top-cell=1: vis-cell-y-start = 2,
    # image-local row 1. blit-dest-y = dest-y + (2-2) = dest-y.
    my %r4 = compute-clip-only-blit(
        |%spaceship-base, source-row => 2, rows => 6, dest-y => 0,
    );
    is %r4<blit-dest-y>, 0,
        "source-row past img-top-cell by 1: dest-y + 0";
};

subtest "horizontal source-col clipping is symmetric" => {
    plan 5;
    # Build a wide-source case where img-left-cell > 0. atma is the
    # natural fit: rcellx=21 in self-cols=24 → img-left-cell = 1.

    # Clip 2 widget cells from the left (source-col=2). The image's
    # left edge is at widget col 1, so source-col=2 cuts into the image.
    # Visible image cols = [max(1,2) .. min(22, 2+22)) = [2, 22) = 20.
    # Image-local col range = [2-1, 22-1) = [1, 21).
    my %r = compute-clip-only-blit(
        |%atma-base, source-row => 0, rows => 9,
        source-col => 2, cols => 22,
    );
    ok %r, "horizontal clip renders";
    is %r<begx>, 1 * 13,         "begx = image-local first col × cell-px-x";
    is %r<lenx>, 20 * 13,        "lenx = visible-img-col-count × cell-px-x";
    is %r<blit-cols>, 20,        "blit-cols = visible-img-col-count";

    # Fully clip from left: source-col such that no image cols visible.
    my %r2 = compute-clip-only-blit(
        |%atma-base, source-row => 0, rows => 9,
        source-col => 25, cols => 5,
    );
    nok %r2, "scroll past image's right edge horizontally: no blit";
};