Selkie.git | t/ | 84-border-title.rakutest


use Test;
use lib 'lib';

use Selkie::BorderStyle;
use Selkie::Widget::Border;

# Title placement is pure arithmetic exposed as a class method, so all
# of it is testable without a notcurses plane. The invariant that
# matters: a title never lands on a corner glyph.

plan 11;

my constant B = Selkie::Widget::Border;

subtest "title defaults" => {
    plan 5;
    my $b = Selkie::Widget::Border.new;
    is $b.title-align, TitleLeft, "top title is left-aligned by default";
    is $b.bottom-title, '', "no bottom title by default";
    is $b.bottom-title-align, TitleLeft,
        "bottom title is left-aligned by default too (symmetry)";
    is $b.title-prefix, ' ', "single-space prefix";
    is $b.title-suffix, ' ', "single-space suffix";
};

subtest "left alignment is byte-identical to the historical column" => {
    plan 5;
    # Border has always drawn its title at column 2. Every existing
    # golden in this distro and downstream depends on it.
    for 13, 24, 40, 120 -> $cols {
        is B.title-column(TitleLeft, $cols, ' Settings '), 2,
            "cols=$cols → column 2";
    }
    # The one case where left-alignment moved: a title truncated to the
    # full interior width used to be painted at column 2, which ran the
    # last character over the top-right corner glyph. It now starts at
    # column 1 and the corner survives. No golden in this distro or
    # downstream exercised that (it needs title.chars >= cols-4).
    is B.title-column(TitleLeft, 12, ' ' ~ ('x' x 8) ~ ' '), 1,
        "a title filling the interior is pulled back off the corner";
};

subtest "centre alignment" => {
    plan 3;
    # cols=24, decorated=10 → (24-10)/2 = 7
    is B.title-column(TitleCenter, 24, ' Settings '), 7, "even leftover";
    # cols=20, decorated=5 → 15/2 = 7.5 → floor 7
    is B.title-column(TitleCenter, 20, ' abc '), 7, "odd leftover floors";
    is B.title-column(TitleCenter, 21, ' abc '), 8, "odd width";
};

subtest "right alignment" => {
    plan 3;
    # Ends two columns short of the right corner: last cell is cols-3.
    is B.title-column(TitleRight, 24, ' Settings '), 12, "cols=24";
    is B.title-column(TitleRight, 24, ' Settings ') + ' Settings '.chars,
        22, "last painted column is cols-3";
    is B.title-column(TitleRight, 40, ' a '), 35, "narrow title";
};

subtest "corners are never overwritten" => {
    plan 3;
    # Property sweep: for every width and every title that fits, the
    # placement stays strictly inside the corners.
    my @bad-lo;
    my @bad-hi;
    for 5 .. 60 -> $cols {
        for 1 .. ($cols - 4) -> $chars {
            my $decorated = ' ' ~ ('x' x $chars) ~ ' ';
            for TitleLeft, TitleCenter, TitleRight -> $align {
                my $x = B.title-column($align, $cols, $decorated);
                @bad-lo.push("$align/$cols/$chars") if $x < 1;
                @bad-hi.push("$align/$cols/$chars")
                    if $x + $decorated.chars > $cols - 1;
            }
        }
    }
    is @bad-lo.elems, 0, "never starts on the left corner";
    is @bad-hi.elems, 0, "never runs into the right corner";
    ok @bad-lo.elems + @bad-hi.elems == 0,
        "sweep covered widths 5..60 with clean results";
};

subtest "clamping when the title fills the whole interior" => {
    plan 3;
    # decorated.chars == cols-2 exactly fills columns 1..cols-2. The
    # only legal start is 1, whatever the alignment asked for.
    my $decorated = ' ' ~ ('x' x 18) ~ ' ';    # 20 chars
    is B.title-column(TitleLeft,   22, $decorated), 1, "left clamped down";
    is B.title-column(TitleCenter, 22, $decorated), 1, "centre already 1";
    is B.title-column(TitleRight,  22, $decorated), 1, "right clamped up";
};

subtest "cols == 5 degenerate width" => {
    plan 4;
    # The narrowest frame that still shows a title: one character of
    # text between prefix and suffix, filling columns 1..3.
    my $decorated = ' X ';
    is B.title-column(TitleLeft,   5, $decorated), 1, "left";
    is B.title-column(TitleCenter, 5, $decorated), 1, "centre";
    is B.title-column(TitleRight,  5, $decorated), 1, "right";
    ok B.title-column(TitleLeft, 5, $decorated) + 3 == 4,
        "ends on the last interior column, leaving the corner alone";
};

subtest "over-long decorated strings bottom out at column 1" => {
    plan 2;
    # Border truncates before it gets here, so this is defence in depth
    # for direct callers: the clamp can't produce a negative column.
    is B.title-column(TitleRight, 10, ' ' x 40), 1, "right";
    is B.title-column(TitleCenter, 10, ' ' x 40), 1, "centre";
};

subtest "title setters mark dirty" => {
    plan 4;
    my $b = Selkie::Widget::Border.new;

    $b.clear-dirty;
    $b.set-title-align(TitleCenter);
    ok $b.is-dirty, "set-title-align marks dirty";
    is $b.title-align, TitleCenter, "and takes effect";

    $b.clear-dirty;
    $b.set-bottom-title('q quit');
    ok $b.is-dirty, "set-bottom-title marks dirty";

    $b.clear-dirty;
    $b.set-bottom-title-align(TitleRight);
    ok $b.is-dirty, "set-bottom-title-align marks dirty";
};

subtest "affix setter" => {
    plan 3;
    my $b = Selkie::Widget::Border.new;
    $b.clear-dirty;
    $b.set-title-affixes('┤ ', ' ├');
    ok $b.is-dirty, "set-title-affixes marks dirty";
    is $b.title-prefix, '┤ ', "prefix stored";
    is $b.title-suffix, ' ├', "suffix stored";
};

subtest "constructor accepts every title knob" => {
    plan 6;
    my $b = Selkie::Widget::Border.new(
        title              => 'Inbox',
        title-align        => TitleCenter,
        bottom-title       => 'q quit',
        bottom-title-align => TitleRight,
        title-prefix       => '[',
        title-suffix       => ']',
    );
    is $b.title, 'Inbox', "title";
    is $b.title-align, TitleCenter, "title-align";
    is $b.bottom-title, 'q quit', "bottom-title";
    is $b.bottom-title-align, TitleRight, "bottom-title-align";
    is $b.title-prefix, '[', "title-prefix";
    is $b.title-suffix, ']', "title-suffix";
};