Selkie.git | xt/ | 05-gradient-plane.rakutest
use Test;
use lib 'lib';
use Selkie::Gradient;
use Selkie::Sizing;
use Selkie::Test::Snapshot;
use Selkie::Widget;
use Selkie::Widget::GradientFill;
use Notcurses::Native;
use Notcurses::Native::Channel;
use Notcurses::Native::Plane;
=begin pod
The half of L<Selkie::Gradient> that needs a real plane: what notcurses
actually does with the words C<t/92-gradient> builds.
Lives in C<xt/> because it initialises notcurses. C<t/92> stays
plane-free and covers the value type, the collapse table and the channel
words; this file covers the seam between them and the library.
The load-bearing assertion is the first one: an uncollapsed
two-dimensional gradient in a one-row region is B<rejected> by
C<ncplane_gradient>, which paints nothing and returns C<-1>. That is the
failure C<Gradient.for-region> exists to prevent, and asserting it here
means the collapse can never be quietly deleted as "defensive".
=end pod
plan 8;
# Runs an arbitrary block against a real, mounted, correctly-sized
# plane and hands back whatever the block recorded. render-to-string
# destroys the widget's plane afterwards, but the recorded values
# outlive it.
class Probe does Selkie::Widget {
has &.body is required;
has @.log;
method render() {
return without self.plane;
@!log = &!body(self.plane, self);
self.clear-dirty;
}
}
sub on-plane(UInt $rows, UInt $cols, &body --> List) {
my $p = Probe.new(body => &body, sizing => Sizing.flex);
render-to-string($p, :$rows, :$cols);
$p.log.List;
}
# A gradient with four independent corners — degenerate in both a
# single row and a single column.
sub quad(--> Gradient) {
Gradient.corners(
top-left => 0x000000, top-right => 0xFFFFFF,
bottom-left => 0xFF0000, bottom-right => 0x00FF00,
);
}
subtest "THE REASON for-region EXISTS — raw corners are refused in one row" => {
plan 3;
my @r = on-plane 1, 20, -> $plane, $ {
my ($ul, $ur, $ll, $lr) = gradient-channels(quad(), fg => Gradient.uniform(0x808080));
(
# Straight to notcurses, corners untouched.
ncplane_gradient($plane, 0, 0, 1, 20, '·', 0, $ul, $ur, $ll, $lr),
# The same gradient through Selkie, which collapses first.
gradient-fill($plane, quad(), fg => Gradient.uniform(0x808080), egc => '·'),
);
};
is @r[0], -1, "uncollapsed: notcurses refuses the call outright";
is @r[1], 20, "collapsed: all twenty cells painted";
isnt @r[0], @r[1], "which is the whole point — silent no-op vs a ramp";
};
subtest "the same refusal in a single column, and the same rescue" => {
plan 2;
my @r = on-plane 9, 1, -> $plane, $ {
my ($ul, $ur, $ll, $lr) = gradient-channels(quad(), fg => Gradient.uniform(0x808080));
(
ncplane_gradient($plane, 0, 0, 9, 1, '·', 0, $ul, $ur, $ll, $lr),
gradient-fill($plane, quad(), fg => Gradient.uniform(0x808080), egc => '·'),
);
};
is @r[0], -1, "uncollapsed 9 x 1 is refused";
is @r[1], 9, "collapsed 9 x 1 paints every cell";
};
subtest "1 x 1 — the tightest region there is" => {
plan 2;
my @r = on-plane 1, 1, -> $plane, $ {
my ($ul, $ur, $ll, $lr) = gradient-channels(quad());
(
ncplane_gradient($plane, 0, 0, 1, 1, ' ', 0, $ul, $ur, $ll, $lr),
gradient-fill($plane, quad()),
);
};
is @r[0], -1, "uncollapsed 1 x 1 is refused";
is @r[1], 1, "collapsed 1 x 1 paints its one cell";
};
subtest "zero extents mean 'everything remaining'" => {
plan 3;
my @r = on-plane 4, 6, -> $plane, $ {
(
gradient-fill($plane, Gradient.uniform(0x101010)),
gradient-fill($plane, Gradient.uniform(0x101010), rows => 2),
gradient-fill($plane, Gradient.uniform(0x101010), y => 1, x => 2),
);
};
is @r[0], 24, "no extents at all covers the whole 4 x 6 plane";
is @r[1], 12, "rows => 2 still runs to the right edge";
is @r[2], 12, "an offset origin covers what is left of the plane";
};
subtest "an oversized extent is clamped, not refused" => {
plan 3;
# Deliberate divergence from the raw binding: notcurses rejects a
# region running off the plane and paints nothing, which is the
# worse outcome when a pane resizes between layout and render.
my @r = on-plane 4, 6, -> $plane, $ {
my ($ul, $ur, $ll, $lr) = gradient-channels(Gradient.uniform(0x101010));
(
ncplane_gradient($plane, 0, 0, 4, 999, ' ', 0, $ul, $ur, $ll, $lr),
gradient-fill($plane, Gradient.uniform(0x101010), cols => 999),
gradient-fill($plane, Gradient.uniform(0x101010), y => 2, rows => 99),
);
};
is @r[0], -1, "notcurses itself refuses cols => 999 on a 6-col plane";
is @r[1], 24, "Selkie clamps it to the plane instead";
is @r[2], 12, "and clamps against the origin, not just the plane";
};
subtest "an origin outside the plane is still an error" => {
plan 3;
my @r = on-plane 2, 4, -> $plane, $ {
(
gradient-fill($plane, Gradient.uniform(0x101010), y => 2),
gradient-fill($plane, Gradient.uniform(0x101010), x => 4),
(try gradient-fill($plane, Gradient.uniform(0x101010), y => -1)) // 'threw',
);
};
is @r[0], -1, "y at the row count is off the plane";
is @r[1], -1, "x at the column count is off the plane";
is @r[2], 'threw', "a negative origin throws rather than guessing";
};
subtest "stain recolours glyphs and skips cells that have none" => {
plan 4;
my @r = on-plane 2, 10, -> $plane, $w {
ncplane_erase($plane);
ncplane_putstr_yx($plane, 0, 0, 'abcdefghij'); # full width
ncplane_putstr_yx($plane, 1, 0, 'abcde'); # half width
my $painted = gradient-stain($plane, Gradient.horizontal(0x000000, 0xFFFFFF),
fg => Gradient.uniform(0xFFFFFF));
# Read the cells back: the padded row must have moved, the
# unwritten tail of the short row must not.
my uint64 $full-tail = 0;
my uint64 $short-tail = 0;
my uint16 $sm = 0;
ncplane_at_yx($plane, 0, 9, $sm, $full-tail);
ncplane_at_yx($plane, 1, 9, $sm, $short-tail);
($painted, ncchannels_bg_rgb($full-tail), ncchannels_bg_rgb($short-tail),
ncchannels_bg_default_p($short-tail));
};
is @r[0], 20, "every cell in the region is visited";
is @r[1], 0xFFFFFF, "a cell with a glyph took the ramp's right-hand end";
isnt @r[2], 0xFFFFFF, "the cell with no glyph did not";
ok @r[3], "it kept its untouched (default) background";
};
subtest "the half-block path" => {
plan 3;
my @r = on-plane 3, 8, -> $plane, $ {
my $utf8 = ?notcurses_canutf8(ncplane_notcurses($plane));
(
$utf8,
gradient-fill-hires($plane, quad()),
# A single column is degenerate for gradient2x1 too; a
# single row is not, because it interpolates over rows * 2.
gradient-fill-hires($plane, quad(), cols => 1),
);
};
if @r[0] {
is @r[1], 24, "a UTF-8 terminal paints every cell";
is @r[2], 3, "the single-column collapse keeps it legal";
} else {
# Documented contract on a non-UTF-8 terminal: refuse, don't
# throw, so the caller can fall back to gradient-fill.
is @r[1], -1, "without UTF-8 it returns -1 rather than throwing";
is @r[2], -1, "and does so consistently";
}
ok @r[1] == -1 || @r[1] > 0, "never anything in between";
};