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";
};