Selkie.git | t/ | 90-text-align.rakutest


use Test;
use lib 'lib';

use Selkie::Align;
use Selkie::Sizing;
use Selkie::Widget::Text;

# Text alignment. The arithmetic lives in the class method
# Text.align-column, so everything here is plane-free — the rendering
# side (that both render and render-region actually use the offset) is
# pinned by xt/snapshots/51-text-center and 52-text-right.

plan 11;

subtest "TextLeft is the default and puts every line at column 0" => {
    plan 4;
    my $t = Selkie::Widget::Text.new(text => 'hello');
    is $t.align, TextLeft, "align defaults to TextLeft";
    is Selkie::Widget::Text.align-column(TextLeft, 5, 40), 0,
        "a short line starts at 0";
    is Selkie::Widget::Text.align-column(TextLeft, 40, 40), 0,
        "an exactly-fitting line starts at 0";
    is Selkie::Widget::Text.align-column(TextLeft, 0, 40), 0,
        "an empty line starts at 0";
};

subtest "TextCenter splits the slack, rounding the left side down" => {
    plan 6;
    is Selkie::Widget::Text.align-column(TextCenter, 5, 11), 3,
        "even slack (6) halves exactly";
    is Selkie::Widget::Text.align-column(TextCenter, 5, 10), 2,
        "odd slack (5) floors: 2 left, 3 right";
    is Selkie::Widget::Text.align-column(TextCenter, 4, 10), 3,
        "…and the extra column always lands on the right";
    is Selkie::Widget::Text.align-column(TextCenter, 10, 10), 0,
        "no slack, no offset";
    is Selkie::Widget::Text.align-column(TextCenter, 9, 10), 0,
        "one column of slack floors to 0, not 0.5";
    is Selkie::Widget::Text.align-column(TextCenter, 0, 9), 4,
        "an empty line still reports the geometric centre";
};

subtest "TextRight ends the line flush with the last column" => {
    plan 4;
    is Selkie::Widget::Text.align-column(TextRight, 5, 11), 6,
        "5 chars in 11 columns starts at 6";
    is Selkie::Widget::Text.align-column(TextRight, 1, 11), 10,
        "a single char sits in the last column";
    is Selkie::Widget::Text.align-column(TextRight, 11, 11), 0,
        "an exactly-fitting line is flush both ways";
    is Selkie::Widget::Text.align-column(TextRight, 0, 11), 11,
        "an empty line reports the far edge (nothing is written there)";
};

subtest "a line wider than the widget clamps to column 0" => {
    plan 6;
    # The overflow case: negative slack must not push the line off the
    # left edge (nor blow up the UInt return). Every alignment agrees.
    for TextLeft, TextCenter, TextRight -> $a {
        is Selkie::Widget::Text.align-column($a, 20, 10), 0,
            "$a: a 20-char line in 10 columns starts at 0";
        is Selkie::Widget::Text.align-column($a, 11, 10), 0,
            "$a: …and so does one that's a single char too long";
    }
};

subtest "zero columns never produces an offset" => {
    plan 4;
    # A widget can legitimately be 0 columns wide mid-relayout.
    for TextLeft, TextCenter, TextRight -> $a {
        is Selkie::Widget::Text.align-column($a, 5, 0), 0,
            "$a: text in a zero-width widget starts at 0";
    }
    is Selkie::Widget::Text.align-column(TextCenter, 0, 0), 0,
        "empty text in a zero-width widget too";
};

subtest "the offset always lands inside the widget for a non-empty line" => {
    plan 1;
    # Property: for any alignment and any line that has at least one
    # character, offset + chars <= cols whenever the line fits, and the
    # offset is 0 when it doesn't. Nothing can be pushed off an edge.
    my $bad = 0;
    for TextLeft, TextCenter, TextRight -> $a {
        for 0 .. 24 -> $cols {
            for 1 .. 24 -> $chars {
                my $x = Selkie::Widget::Text.align-column($a, $chars, $cols);
                $bad++ unless $chars <= $cols
                    ?? $x + $chars <= $cols
                    !! $x == 0;
            }
        }
    }
    is $bad, 0, "no (align, cols, chars) triple escapes the widget";
};

subtest "align can be set at construction" => {
    plan 2;
    my $t = Selkie::Widget::Text.new(text => 'x', align => TextCenter);
    is $t.align, TextCenter, "constructor argument honoured";
    is Selkie::Widget::Text.new(text => 'x', align => TextRight).align, TextRight,
        "…for every value";
};

subtest "set-align updates the alignment and marks dirty" => {
    plan 4;
    my $t = Selkie::Widget::Text.new(text => 'x');
    $t.clear-dirty;
    $t.set-align(TextRight);
    is $t.align, TextRight, "alignment updated";
    ok $t.is-dirty, "marked dirty — the next render repaints";
    $t.clear-dirty;
    $t.set-align(TextRight);
    nok $t.is-dirty, "re-setting the same value is a no-op";
    $t.set-align(TextCenter);
    ok $t.is-dirty, "…but a real change still marks dirty";
};

subtest "alignment does not disturb wrapping" => {
    plan 6;
    # Alignment is applied at putstr time, so the wrap table — and
    # therefore logical-height, which ScrollView depends on — is
    # identical whatever the alignment.
    for TextLeft, TextCenter, TextRight -> $a {
        my $t = Selkie::Widget::Text.new(text => 'a' x 25, align => $a);
        $t.resize(10, 10);
        is $t.logical-height, 3, "$a: 25 chars at width 10 still wraps to 3";

        my $p = Selkie::Widget::Text.new(
            text  => "the quick brown fox jumps over the lazy dog",
            align => $a,
        );
        $p.resize(10, 20);
        is $p.logical-height, 3, "$a: prose wraps identically at width 20";
    }
};

subtest "alignment survives a resize" => {
    plan 3;
    # !on-resize rewraps; it must not reset the alignment, and the new
    # width has to feed straight into the offset.
    my $t = Selkie::Widget::Text.new(text => 'abc', align => TextRight);
    $t.resize(1, 10);
    is Selkie::Widget::Text.align-column($t.align, 3, $t.cols), 7,
        "flush right in 10 columns";
    $t.resize(1, 20);
    is $t.align, TextRight, "alignment unchanged by the resize";
    is Selkie::Widget::Text.align-column($t.align, 3, $t.cols), 17,
        "…and the offset follows the new width";
};

subtest "width is counted in characters, not terminal columns" => {
    plan 2;
    # Documented limitation, pinned so nobody 'fixes' it silently: the
    # widget measures with .chars throughout. A CJK line is counted at
    # half the columns it actually occupies.
    is Selkie::Widget::Text.align-column(TextCenter, '日本語'.chars, 10), 3,
        "3 characters, centred as though 3 columns wide";
    is Selkie::Widget::Text.align-column(TextCenter, 'abc'.chars, 10), 3,
        "…exactly like a 3-character ASCII line";
};