Selkie.git | t/ | 77-image-rgba-blit.rakutest


use Test;
use lib 'lib';
use NativeCall;
use Notcurses::Native;
use Notcurses::Native::Types;
use Notcurses::Native::Cell;
use Notcurses::Native::Plane;
use Notcurses::Native::Channel;
use Notcurses::Native::Visual;

use Selkie::Test::SnapshotPlatform;

=begin pod
Integration guard for the native wiring behind C<Image.set-rgba>.

C<Image.!load> builds its visual with
C<ncvisual_from_rgba(ptr, height, width × 4, width)> from the retained
RGBA buffer. This file proves those exact parameters round-trip: a
B<non-square> 4×2 image (so a width/height transposition or a wrong
rowstride would corrupt the output) laid out as a flat
C<Buf[uint8]> of R,G,B,A bytes — precisely what C<set-rgba> is handed —
blits back to the original per-pixel colours.

The state-machine side of set-rgba (source-id, source switching, size
validation) is covered headlessly by F<t/76-image-rgba-source.rakutest>;
everything downstream of C<$!visual> (scaling, emit-blit, teardown) is
shared with the file path proven by F<t/69-image-clip-only-blit.rakutest>.
=end pod

plan 1;

unless $*ERR.t || %*ENV<NOTCURSES_FORCE>.so {
    skip 'No terminal available', 1;
    exit 0;
}

my Pointer $devnull = snapshot-fopen(NULL-PATH, 'w');
my $opts = NotcursesOptions.new(
    :loglevel(NCLOGLEVEL_SILENT),
    :flags(NCOPTION_SUPPRESS_BANNERS
        +| NCOPTION_NO_ALTERNATE_SCREEN
        +| NCOPTION_INHIBIT_SETLOCALE),
);
my $nc = notcurses_init($opts, $devnull);
unless $nc.defined {
    skip 'notcurses_init failed (no usable terminal)', 1;
    exit 0;
}
LEAVE {
    notcurses_stop($nc) if $nc.defined;
    snapshot-fclose($devnull) if $devnull.defined;
}

# A 4-wide × 2-tall RGBA image as a flat Buf[uint8] — the layout
# Image.set-rgba retains (R,G,B,A per pixel, row-major, tightly packed).
constant W = 4;
constant H = 2;
my @pixels =
    (255,0,0),   (0,255,0),   (0,0,255),     (255,255,0),    # row 0
    (255,0,255), (0,255,255), (128,128,128), (255,255,255);  # row 1
my $rgba = Buf[uint8].new;
$rgba.append($_[0], $_[1], $_[2], 255) for @pixels;

sub cell-bg-rgb($plane, Int $y, Int $x) {
    my $cell = Nccell.new;
    ncplane_at_yx_cell($plane, $y, $x, $cell);
    my uint32 $r; my uint32 $g; my uint32 $b;
    ncchannels_bg_rgb8($cell.channels, $r, $g, $b);
    nccell_release($plane, $cell);
    ($r.Int, $g.Int, $b.Int);
}

subtest 'set-rgba parameters blit back to the source pixels' => {
    plan W * H + 1;

    # Mirror Image.!load exactly: set-rgba retains the caller's Buf and
    # !load hands its storage straight to ncvisual_from_rgba via
    # nativecast(Pointer, $buf) — zero copy. Pass the Buf the same way.
    my $visual = ncvisual_from_rgba(nativecast(Pointer, $rgba), H, W * 4, W);
    ok $visual.defined, 'ncvisual_from_rgba built a visual from the RGBA Buf';
    LEAVE { ncvisual_destroy($visual) if $visual.defined }

    # Blit 1:1 — NCBLIT_1x1 so one cell == one source pixel.
    my $std  = notcurses_stdplane($nc);
    my $opts = NcplaneOptions.new(y => 0, x => 0, rows => H, cols => W);
    my $plane = ncplane_create($std, $opts);
    LEAVE { ncplane_destroy($plane) if $plane.defined }

    my $vopts = NcvisualOptions.new(scaling => NCSCALE_NONE, blitter => NCBLIT_1x1);
    $vopts.set-plane($plane);
    ncvisual_blit($nc, $visual, $vopts);

    for ^H -> $y {
        for ^W -> $x {
            my $want = @pixels[$y * W + $x].List;
            is-deeply cell-bg-rgb($plane, $y, $x), $want,
                "cell ($y,$x) == source pixel ({$want.join(',')})";
        }
    }
};

done-testing;