Notcurses-Native.git | xt/ | 11-cell-exhaustive.rakutest edit


use Test;
use lib 'lib';
use lib 't/lib';
use TestHelper;
use NativeCall;
use Notcurses::Native;
use Notcurses::Native::Types;
use Notcurses::Native::Plane;
use Notcurses::Native::Cell;

plan 17;

# === Initialize notcurses ===


my $opts = NotcursesOptions.new(
	:loglevel(NCLOGLEVEL_SILENT),
	:flags(NCOPTION_SUPPRESS_BANNERS +| NCOPTION_NO_ALTERNATE_SCREEN +| NCOPTION_INHIBIT_SETLOCALE),
);
my ($nc, $) = test-init-nc($opts);
unless $nc.defined {
    skip 'No terminal available', 16;
    exit 0;
}
LEAVE { notcurses_stop($nc) if $nc.defined }

my $std = notcurses_stdplane($nc);
my $child = ncplane_create($std, NcplaneOptions.new(:rows(10), :cols(40)));
ok $child.defined, 'Child plane created for cell operations';
LEAVE { ncplane_destroy($child) if $child.defined }

# === Subtest 1: nccell_init ===

subtest 'nccell_init zeroes a cell' => {
    plan 4;
    my $cell = Nccell.new(:gcluster(0xDEAD), :stylemask(0xFF), :channels(0xCAFEBABE));
    nccell_init($cell);
    is $cell.gcluster, 0, 'gcluster zeroed';
    is $cell.gcluster_backstop, 0, 'backstop zeroed';
    is $cell.stylemask, 0, 'stylemask zeroed';
    is $cell.channels, 0, 'channels zeroed';
}

# === Subtest 2: nccell_load and nccell_strdup ===

subtest 'nccell_load and nccell_strdup roundtrip' => {
    plan 8;

    # ASCII character
    my $c1 = Nccell.new;
    nccell_init($c1);
    my $bytes = nccell_load($child, $c1, 'A');
    ok $bytes > 0, "nccell_load returns positive bytes for ASCII ($bytes)";
    my $str = nccell_strdup($child, $c1);
    is $str, 'A', 'strdup returns loaded ASCII character';
    nccell_release($child, $c1);

    # Multi-byte: requires UTF-8 locale
    if notcurses_canutf8($nc) {
        my $c2 = Nccell.new;
        nccell_init($c2);
        $bytes = nccell_load($child, $c2, "\x[00E9]");
        ok $bytes > 0, "nccell_load succeeds for U+00E9 ($bytes bytes)";
        is nccell_cols($c2), 1, 'U+00E9 is 1 column wide';
        nccell_release($child, $c2);

        my $c3 = Nccell.new;
        nccell_init($c3);
        $bytes = nccell_load($child, $c3, "\x[4E16]");
        ok $bytes > 0, "nccell_load succeeds for U+4E16 ($bytes bytes)";
        ok nccell_cols($c3) >= 1, "CJK cell reports >= 1 column ({nccell_cols($c3)})";
        nccell_release($child, $c3);
    } else {
        skip 'No UTF-8 support — skipping multi-byte cell tests', 4;
    }

    # Loading from a longer string takes only the first grapheme cluster
    my $c4 = Nccell.new;
    nccell_init($c4);
    $bytes = nccell_load($child, $c4, 'Hello');
    ok $bytes > 0, 'nccell_load from multi-char string succeeds';
    $str = nccell_strdup($child, $c4);
    is $str, 'H', 'strdup returns only first grapheme cluster';
    nccell_release($child, $c4);
}

# === Subtest 3: nccell_load_char ===

subtest 'nccell_load_char loads a single byte character' => {
    plan 3;
    my $cell = Nccell.new;
    nccell_init($cell);
    my $ret = nccell_load_char($child, $cell, 0x5A);  # 'Z'
    ok $ret > 0, "nccell_load_char returns positive ($ret)";
    my $str = nccell_strdup($child, $cell);
    is $str, 'Z', 'Loaded char is Z';
    is nccell_cols($cell), 1, 'Single-width character has 1 column';
    nccell_release($child, $cell);
}

# === Subtest 4: nccell_prime ===

subtest 'nccell_prime loads with style and channels' => {
    plan 4;
    my $cell = Nccell.new;
    nccell_init($cell);
    my $style = NCSTYLE_BOLD +| NCSTYLE_ITALIC;
    my uint64 $channels = 0x00FF000000FF0000;
    my $ret = nccell_prime($child, $cell, 'X', $style, $channels);
    ok $ret > 0, "nccell_prime returns positive ($ret)";
    my $str = nccell_strdup($child, $cell);
    is $str, 'X', 'Prime loaded correct character';
    is nccell_styles($cell), $style, 'Prime set correct styles';
    is nccell_channels($cell), $channels, 'Prime set correct channels';
    nccell_release($child, $cell);
}

# === Subtest 5: nccell_duplicate ===

subtest 'nccell_duplicate creates independent copy' => {
    plan 5;
    my $src = Nccell.new;
    nccell_init($src);
    nccell_load($child, $src, 'D');
    nccell_set_fg_rgb($src, 0x112233);
    nccell_set_bg_rgb($src, 0x445566);
    nccell_set_styles($src, NCSTYLE_UNDERLINE);

    my $dst = Nccell.new;
    nccell_init($dst);
    my $ret = nccell_duplicate($child, $dst, $src);
    is $ret, 0, 'nccell_duplicate returns 0 on success';

    my $dst-str = nccell_strdup($child, $dst);
    is $dst-str, 'D', 'Duplicate has same character';
    is nccell_fg_rgb($dst), 0x112233, 'Duplicate has same fg color';
    is nccell_bg_rgb($dst), 0x445566, 'Duplicate has same bg color';

    # Modify source, verify duplicate is independent
    nccell_set_fg_rgb($src, 0xAABBCC);
    is nccell_fg_rgb($dst), 0x112233, 'Duplicate fg unchanged after modifying source';

    nccell_release($child, $src);
    nccell_release($child, $dst);
}

# === Subtest 6: Foreground/background RGB set/get roundtrips ===

subtest 'Foreground and background RGB set/get roundtrips' => {
    plan 10;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Set/get fg as packed RGB
    nccell_set_fg_rgb($cell, 0xABCDEF);
    is nccell_fg_rgb($cell), 0xABCDEF, 'fg_rgb roundtrips packed value';

    # Set/get bg as packed RGB
    nccell_set_bg_rgb($cell, 0x123456);
    is nccell_bg_rgb($cell), 0x123456, 'bg_rgb roundtrips packed value';

    # Verify fg unchanged after setting bg
    is nccell_fg_rgb($cell), 0xABCDEF, 'fg_rgb unchanged after setting bg';

    # Set/get fg as RGB8 components
    nccell_set_fg_rgb8($cell, 0x10, 0x20, 0x30);
    my uint32 $r = 0; my uint32 $g = 0; my uint32 $b = 0;
    nccell_fg_rgb8($cell, $r, $g, $b);
    is $r, 0x10, 'fg r component roundtrips';
    is $g, 0x20, 'fg g component roundtrips';
    is $b, 0x30, 'fg b component roundtrips';

    # Set/get bg as RGB8 components
    nccell_set_bg_rgb8($cell, 0xA0, 0xB0, 0xC0);
    $r = 0; $g = 0; $b = 0;
    nccell_bg_rgb8($cell, $r, $g, $b);
    is $r, 0xA0, 'bg r component roundtrips';
    is $g, 0xB0, 'bg g component roundtrips';
    is $b, 0xC0, 'bg b component roundtrips';

    # Verify packed value matches components
    is nccell_bg_rgb($cell), 0xA0B0C0, 'bg_rgb matches assembled components';
}

# === Subtest 7: Alpha set/get ===

subtest 'Alpha set/get for foreground and background' => {
    plan 8;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Default alpha is OPAQUE
    is nccell_fg_alpha($cell), NCALPHA_OPAQUE, 'fg alpha defaults to OPAQUE';
    is nccell_bg_alpha($cell), NCALPHA_OPAQUE, 'bg alpha defaults to OPAQUE';

    # Set fg to BLEND
    my $ret = nccell_set_fg_alpha($cell, NCALPHA_BLEND);
    is $ret, 0, 'set_fg_alpha BLEND returns 0';
    is nccell_fg_alpha($cell), NCALPHA_BLEND, 'fg alpha is BLEND';

    # Set bg to TRANSPARENT
    $ret = nccell_set_bg_alpha($cell, NCALPHA_TRANSPARENT);
    is $ret, 0, 'set_bg_alpha TRANSPARENT returns 0';
    is nccell_bg_alpha($cell), NCALPHA_TRANSPARENT, 'bg alpha is TRANSPARENT';

    # Verify fg alpha unchanged after setting bg alpha
    is nccell_fg_alpha($cell), NCALPHA_BLEND, 'fg alpha unchanged after setting bg alpha';

    # Set fg to HIGHCONTRAST
    nccell_set_fg_alpha($cell, NCALPHA_HIGHCONTRAST);
    is nccell_fg_alpha($cell), NCALPHA_HIGHCONTRAST, 'fg alpha is HIGHCONTRAST';
}

# === Subtest 8: Default flag set/get ===

subtest 'Default foreground/background flag' => {
    plan 6;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Fresh cell uses default colors
    ok nccell_fg_default_p($cell), 'Fresh cell has fg default set';
    ok nccell_bg_default_p($cell), 'Fresh cell has bg default set';

    # Setting an explicit RGB clears the default flag
    nccell_set_fg_rgb($cell, 0xFF0000);
    nok nccell_fg_default_p($cell), 'fg default cleared after setting RGB';
    ok nccell_bg_default_p($cell), 'bg default still set';

    # Restore default
    nccell_set_fg_default($cell);
    ok nccell_fg_default_p($cell), 'fg default restored';

    # Same for bg
    nccell_set_bg_rgb($cell, 0x00FF00);
    nccell_set_bg_default($cell);
    ok nccell_bg_default_p($cell), 'bg default restored after set+clear';
}

# === Subtest 9: Palette index set/get ===

subtest 'Palette index foreground/background' => {
    plan 8;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Initially not palette-indexed
    nok nccell_fg_palindex_p($cell), 'Fresh cell fg is not palette-indexed';
    nok nccell_bg_palindex_p($cell), 'Fresh cell bg is not palette-indexed';

    # Set fg palette index
    my $ret = nccell_set_fg_palindex($cell, 42);
    is $ret, 0, 'set_fg_palindex returns 0';
    ok nccell_fg_palindex_p($cell), 'fg is now palette-indexed';
    is nccell_fg_palindex($cell), 42, 'fg palindex is 42';

    # Set bg palette index
    $ret = nccell_set_bg_palindex($cell, 200);
    is $ret, 0, 'set_bg_palindex returns 0';
    ok nccell_bg_palindex_p($cell), 'bg is now palette-indexed';
    is nccell_bg_palindex($cell), 200, 'bg palindex is 200';
}

# === Subtest 10: Style manipulation ===

subtest 'Style set, on, and off operations' => {
    plan 8;
    my $cell = Nccell.new;
    nccell_init($cell);

    is nccell_styles($cell), NCSTYLE_NONE, 'Fresh cell has no styles';

    # Set bold
    nccell_set_styles($cell, NCSTYLE_BOLD);
    is nccell_styles($cell), NCSTYLE_BOLD, 'set_styles to BOLD';

    # Add italic via on_styles
    nccell_on_styles($cell, NCSTYLE_ITALIC);
    is nccell_styles($cell), NCSTYLE_BOLD +| NCSTYLE_ITALIC, 'on_styles added ITALIC';

    # Add underline
    nccell_on_styles($cell, NCSTYLE_UNDERLINE);
    is nccell_styles($cell), NCSTYLE_BOLD +| NCSTYLE_ITALIC +| NCSTYLE_UNDERLINE,
        'on_styles added UNDERLINE';

    # Remove bold via off_styles
    nccell_off_styles($cell, NCSTYLE_BOLD);
    is nccell_styles($cell), NCSTYLE_ITALIC +| NCSTYLE_UNDERLINE,
        'off_styles removed BOLD';

    # Off a style that is not set (no-op)
    nccell_off_styles($cell, NCSTYLE_STRUCK);
    is nccell_styles($cell), NCSTYLE_ITALIC +| NCSTYLE_UNDERLINE,
        'off_styles of unset style is a no-op';

    # Set overwrites all styles
    nccell_set_styles($cell, NCSTYLE_STRUCK);
    is nccell_styles($cell), NCSTYLE_STRUCK, 'set_styles overwrites to STRUCK only';

    # Clear all styles
    nccell_set_styles($cell, NCSTYLE_NONE);
    is nccell_styles($cell), NCSTYLE_NONE, 'set_styles to NONE clears all';
}

# === Subtest 11: Whole channels and individual channel set/get ===

subtest 'Whole channels and individual fchannel/bchannel' => {
    plan 6;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Set whole channels
    my uint64 $ch = 0x00AABBCC00112233;
    nccell_set_channels($cell, $ch);
    is nccell_channels($cell), $ch, 'set/get channels roundtrip';

    # Read individual channels
    is nccell_fchannel($cell), 0x00AABBCC, 'fchannel extracts upper 32 bits';
    is nccell_bchannel($cell), 0x00112233, 'bchannel extracts lower 32 bits';

    # Set individual channels
    nccell_set_fchannel($cell, 0x00DEADBE);
    is nccell_fchannel($cell), 0x00DEADBE, 'set_fchannel updates fg channel';
    is nccell_bchannel($cell), 0x00112233, 'bchannel unchanged after set_fchannel';

    nccell_set_bchannel($cell, 0x00CAFE01);
    is nccell_bchannel($cell), 0x00CAFE01, 'set_bchannel updates bg channel';
}

# === Subtest 12: nccell_extract ===

subtest 'nccell_extract returns character, style, and channels' => {
    plan 4;
    my $cell = Nccell.new;
    nccell_init($cell);
    nccell_load($child, $cell, 'E');
    nccell_set_styles($cell, NCSTYLE_BOLD +| NCSTYLE_UNDERLINE);
    nccell_set_fg_rgb($cell, 0xAA0000);
    nccell_set_bg_rgb($cell, 0x0000BB);

    my uint16 $stylemask = 0;
    my uint64 $channels = 0;
    my $str = nccell_extract($child, $cell, $stylemask, $channels);

    is $str, 'E', 'extract returns correct character';
    is $stylemask, NCSTYLE_BOLD +| NCSTYLE_UNDERLINE, 'extract returns correct stylemask';
    ok $channels != 0, 'extract returns non-zero channels';
    # Verify fg portion of channels contains our color
    is nccell_fg_rgb($cell), 0xAA0000, 'fg color consistent with what was set';
    nccell_release($child, $cell);
}

# === Subtest 13: nccellcmp ===

subtest 'nccellcmp compares cells' => {
    plan 3;

    # Two identical cells
    my $c1 = Nccell.new;
    nccell_init($c1);
    nccell_load($child, $c1, 'Q');
    nccell_set_fg_rgb($c1, 0x111111);

    my $c2 = Nccell.new;
    nccell_init($c2);
    nccell_duplicate($child, $c2, $c1);

    nok nccellcmp($child, $c1, $child, $c2), 'Identical cells compare as equal (false = no difference)';

    # Different character
    my $c3 = Nccell.new;
    nccell_init($c3);
    nccell_load($child, $c3, 'R');
    nccell_set_fg_rgb($c3, 0x111111);

    ok nccellcmp($child, $c1, $child, $c3), 'Different characters compare as unequal';

    # Same char, different color
    my $c4 = Nccell.new;
    nccell_init($c4);
    nccell_load($child, $c4, 'Q');
    nccell_set_fg_rgb($c4, 0x999999);

    ok nccellcmp($child, $c1, $child, $c4), 'Same char but different fg color compares as unequal';

    nccell_release($child, $c1);
    nccell_release($child, $c2);
    nccell_release($child, $c3);
    nccell_release($child, $c4);
}

# === Subtest 14: Box cell helpers ===

subtest 'Box cell loading (rounded, double, ascii, light, heavy)' => {
    plan 15;

    sub test-box-cells(Str $label, &loader) {
        my $ul = Nccell.new; nccell_init($ul);
        my $ur = Nccell.new; nccell_init($ur);
        my $ll = Nccell.new; nccell_init($ll);
        my $lr = Nccell.new; nccell_init($lr);
        my $hl = Nccell.new; nccell_init($hl);
        my $vl = Nccell.new; nccell_init($vl);

        my $ret = loader($child, 0, 0, $ul, $ur, $ll, $lr, $hl, $vl);
        is $ret, 0, "$label box returns 0";

        my $ul-str = nccell_strdup($child, $ul);
        ok $ul-str.defined && $ul-str.chars > 0, "$label box ul has content: {$ul-str // '(undef)'}";

        my $hl-str = nccell_strdup($child, $hl);
        ok $hl-str.defined && $hl-str.chars > 0, "$label box hl has content: {$hl-str // '(undef)'}";

        nccell_release($child, $ul);
        nccell_release($child, $ur);
        nccell_release($child, $ll);
        nccell_release($child, $lr);
        nccell_release($child, $hl);
        nccell_release($child, $vl);
    }

    test-box-cells('rounded', &nccells_rounded_box);
    test-box-cells('double', &nccells_double_box);
    test-box-cells('ascii', &nccells_ascii_box);
    test-box-cells('light', &nccells_light_box);
    test-box-cells('heavy', &nccells_heavy_box);
}

# === Subtest 15: nccell_cols for single-width and wide characters ===

subtest 'nccell_cols reports column width' => {
    plan 3;

    # Single-width ASCII
    my $c1 = Nccell.new;
    nccell_init($c1);
    nccell_load($child, $c1, 'W');
    is nccell_cols($c1), 1, 'ASCII character W is 1 column wide';
    nccell_release($child, $c1);

    # Another ASCII character for consistency
    my $c2 = Nccell.new;
    nccell_init($c2);
    nccell_load($child, $c2, '!');
    is nccell_cols($c2), 1, 'Punctuation ! is 1 column wide';
    nccell_release($child, $c2);

    # Fresh cell (no load) should report 1 column (default width field)
    my $c3 = Nccell.new;
    nccell_init($c3);
    is nccell_cols($c3), 1, 'Empty initialized cell reports 1 column';
}

# === Subtest 16: nccell_set_fg_rgb8_clipped / nccell_set_bg_rgb8_clipped ===

subtest 'Clipped RGB8 set clamps out-of-range values' => {
    plan 4;
    my $cell = Nccell.new;
    nccell_init($cell);

    # Set fg with values that exceed byte range (should be clamped)
    nccell_set_fg_rgb8_clipped($cell, 300, -10, 128);
    my uint32 $r = 0; my uint32 $g = 0; my uint32 $b = 0;
    nccell_fg_rgb8($cell, $r, $g, $b);
    is $r, 255, 'fg r clamped from 300 to 255';
    is $g, 0, 'fg g clamped from -10 to 0';

    # Set bg with clipped values
    nccell_set_bg_rgb8_clipped($cell, 0, 500, 100);
    $r = 0; $g = 0; $b = 0;
    nccell_bg_rgb8($cell, $r, $g, $b);
    is $g, 255, 'bg g clamped from 500 to 255';
    is $b, 100, 'bg b passes through unchanged at 100';
}