Selkie.git | t/ | 86-border-padding.rakutest
use Test;
use lib 'lib';
use Selkie::Widget;
use Selkie::Widget::Border;
# Border padding. inner-rect is deliberately plane-free (same reasoning
# as title-column in t/84), so the whole content-box contract can be
# pinned headlessly. The one thing that genuinely needs a live plane —
# that a collapsed content box parks the content widget rather than
# leaving a stale, oversized plane bleeding past the frame — is proved
# end-to-end by xt/snapshots/47-border-padding-collapse.
class TestWidget does Selkie::Widget {
method render() { self.clear-dirty }
}
plan 12;
subtest "no padding reproduces the historical content box" => {
plan 4;
# Back-compat proof: before padding existed, content sat at (1, 1)
# with rows-2 x cols-2. Every golden in the distro and downstream
# depends on that staying true for a default-constructed Border.
my $b = Selkie::Widget::Border.new;
is $b.padding-top, 0, "top defaults to flush";
is-deeply $b.inner-rect(10, 20), (1, 1, 8, 18),
"10x20 frame yields an 8x18 content box at (1, 1)";
is-deeply $b.inner-rect(3, 3), (1, 1, 1, 1),
"the 3x3 minimum still leaves a single content cell";
is-deeply $b.inner-rect(24, 80), (1, 1, 22, 78),
"a full-screen frame is unchanged";
};
subtest "uniform padding insets all four edges" => {
plan 3;
my $b = Selkie::Widget::Border.new(padding => 1);
is-deeply $b.inner-rect(10, 20), (2, 2, 6, 16),
"one cell of inset costs two rows and two columns";
my $b2 = Selkie::Widget::Border.new(padding => 2);
is-deeply $b2.inner-rect(10, 20), (3, 3, 4, 14),
"two cells of inset cost four rows and four columns";
my $b0 = Selkie::Widget::Border.new(padding => 0);
is-deeply $b0.inner-rect(10, 20), (1, 1, 8, 18),
"an explicit padding of 0 is the flush layout";
};
subtest "asymmetric padding moves only the edges it names" => {
plan 3;
my $b = Selkie::Widget::Border.new(
padding-top => 2,
padding-left => 3,
padding-right => 1,
padding-bottom => 0,
);
is-deeply $b.inner-rect(10, 20), (3, 4, 6, 14),
"origin shifts by top/left, extents shrink by both pairs";
my $left-only = Selkie::Widget::Border.new(padding-left => 4);
is-deeply $left-only.inner-rect(6, 12), (1, 5, 4, 6),
"left padding alone doesn't touch the vertical axis";
my $bottom-only = Selkie::Widget::Border.new(padding-bottom => 2);
is-deeply $bottom-only.inner-rect(6, 12), (1, 1, 2, 10),
"bottom padding shortens the box without moving its origin";
};
subtest "padding composes with hidden edges" => {
plan 4;
my $top = Selkie::Widget::Border.new(padding => 1);
$top.hide-top-border = True;
is-deeply $top.inner-rect(10, 20), (1, 2, 7, 16),
"a hidden top edge frees its row, padding still applies";
my $bot = Selkie::Widget::Border.new(padding => 1);
$bot.hide-bottom-border = True;
is-deeply $bot.inner-rect(10, 20), (2, 2, 7, 16),
"a hidden bottom edge frees a row at the far end";
my $both = Selkie::Widget::Border.new(padding => 1);
$both.hide-top-border = True;
$both.hide-bottom-border = True;
is-deeply $both.inner-rect(10, 20), (1, 2, 8, 16),
"both hidden: only the padding costs rows";
my $flush = Selkie::Widget::Border.new;
$flush.hide-top-border = True;
$flush.hide-bottom-border = True;
is-deeply $flush.inner-rect(10, 20), (0, 1, 10, 18),
"…and with no padding that's the full height, as before";
};
subtest "horizontal padding can collapse the content box" => {
plan 3;
# cols - 2 - left - right hits exactly zero here.
my $b = Selkie::Widget::Border.new(padding => 4);
is-deeply $b.inner-rect(20, 10), (5, 5, 10, 0),
"10 columns with 4 either side leaves no content columns";
is $b.inner-rect(20, 10)[3], 0, "the column extent is the collapsed one";
isnt $b.inner-rect(20, 10)[2], 0, "rows are still fine — one axis is enough";
};
subtest "vertical padding can collapse the content box" => {
plan 2;
my $b = Selkie::Widget::Border.new(padding-top => 3, padding-bottom => 3);
is-deeply $b.inner-rect(8, 20), (4, 1, 0, 18),
"8 rows with 3 above and 3 below leaves no content rows";
is-deeply Selkie::Widget::Border.new(padding => 5).inner-rect(6, 30),
(6, 6, 0, 18),
"padding that overshoots the frame clamps rows at 0, never negative";
};
subtest "degenerate geometry never yields a negative extent" => {
plan 1;
# Sweep the small end of the space, including the sub-3x3 frames
# render short-circuits on and the zero-dimension planes a
# collapsing layout can hand us. Nothing here may go negative:
# a negative extent would be passed to ncplane_resize_simple.
my $bad = 0;
for 0 .. 8 -> $rows {
for 0 .. 8 -> $cols {
for 0 .. 4 -> $pad {
my $b = Selkie::Widget::Border.new(padding => $pad);
my ($y, $x, $ir, $ic) = $b.inner-rect($rows, $cols);
$bad++ if $y < 0 || $x < 0 || $ir < 0 || $ic < 0;
}
}
}
is $bad, 0, "405 (rows, cols, padding) combinations, no negative extents";
};
subtest "the constructor shorthand fans out to all four edges" => {
plan 5;
my $b = Selkie::Widget::Border.new(padding => 3);
is $b.padding-top, 3, "top";
is $b.padding-right, 3, "right";
is $b.padding-bottom, 3, "bottom";
is $b.padding-left, 3, "left";
nok Selkie::Widget::Border.new.can('padding'),
"no scalar `padding` accessor — the four edges are the state";
};
subtest "explicit edges win over the shorthand" => {
plan 4;
# `padding => 1, padding-top => 0` is the titled-panel idiom: the
# title already separates the content from the top edge.
my $b = Selkie::Widget::Border.new(padding => 1, padding-top => 0);
is $b.padding-top, 0, "the explicit edge survives the fan-out";
is $b.padding-right, 1, "unnamed edges take the shorthand";
is $b.padding-bottom, 1, "…bottom too";
is-deeply $b.inner-rect(10, 20), (1, 2, 7, 16),
"and the content box reflects the mix";
};
subtest "set-padding sets every edge and marks dirty" => {
plan 6;
my $b = Selkie::Widget::Border.new;
$b.clear-dirty;
$b.set-padding(2);
is $b.padding-top, 2, "top";
is $b.padding-right, 2, "right";
is $b.padding-bottom, 2, "bottom";
is $b.padding-left, 2, "left";
ok $b.is-dirty, "dirty after set-padding — the next render re-lays out";
$b.clear-dirty;
$b.set-padding(0);
is-deeply ($b.padding-top, $b.padding-right, $b.padding-bottom, $b.padding-left),
(0, 0, 0, 0), "back to flush";
};
subtest "set-padding-edges touches only the edges it's given" => {
plan 6;
my $b = Selkie::Widget::Border.new(padding => 1);
$b.clear-dirty;
$b.set-padding-edges(left => 4, right => 4);
is $b.padding-left, 4, "left updated";
is $b.padding-right, 4, "right updated";
is $b.padding-top, 1, "top left alone";
is $b.padding-bottom, 1, "bottom left alone";
ok $b.is-dirty, "dirty after set-padding-edges";
# Zero is a value, not "unset" — a caller must be able to clear one
# edge without clearing the rest.
$b.set-padding-edges(left => 0);
is-deeply ($b.padding-left, $b.padding-right), (0, 4),
"an explicit 0 clears just that edge";
};
subtest "a collapsed content box is the condition render parks on" => {
plan 5;
# inner-rect is the predicate Border.render branches on: when
# either extent is 0 it parks the content instead of attempting a
# resize notcurses would reject (leaving a stale, oversized plane
# painting through the frame). Pinning the predicate here; the
# rendered proof is xt/snapshots/47-border-padding-collapse.
my $collapsed = Selkie::Widget::Border.new(padding => 3);
$collapsed.set-content(TestWidget.new);
my ($, $, $ir, $ic) = $collapsed.inner-rect(6, 8);
ok $ir == 0 || $ic == 0, "8-wide frame with 3 either side collapses";
my $roomy = Selkie::Widget::Border.new(padding => 1);
my ($, $, $rr, $rc) = $roomy.inner-rect(6, 8);
ok $rr > 0 && $rc > 0, "one cell of padding still leaves a content box";
# A Border narrow enough to collapse can widen back out again —
# parking is not a terminal state.
is-deeply $collapsed.inner-rect(6, 20), (4, 4, 0, 12),
"widening alone doesn't help when the vertical axis is the problem";
is-deeply $collapsed.inner-rect(12, 20), (4, 4, 4, 12),
"given both, the content box comes back";
ok $collapsed.content.defined,
"the content widget is retained throughout — parked, never dropped";
};