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