Selkie.git | t/ | 64-layout-allocate.rakutest
use Test;
use lib 'lib';
use Selkie::Widget;
use Selkie::Sizing;
use Selkie::Layout::Allocate;
# Concrete stub that exposes only what allocate-along-axis cares about.
# We can't reuse TestWidget alone because allocate-along-axis reads
# `.sizing.mode` and `.sizing.value`, both of which Sizing already
# provides — TestWidget's constructor accepts `:sizing` via Widget's
# attribute, so we get the pass-through for free.
class StubKid does Selkie::Widget {
method render() { self.clear-dirty }
}
plan 8;
subtest "all-fixed children fit exactly" => {
plan 4;
my @kids = (
StubKid.new(sizing => Sizing.fixed(3)),
StubKid.new(sizing => Sizing.fixed(4)),
StubKid.new(sizing => Sizing.fixed(3)),
);
my @a = allocate-along-axis(@kids, 10);
is @a.elems, 3, "one allocation per child";
is @a[0], 3, "first child gets 3";
is @a[1], 4, "second child gets 4";
is @a[2], 3, "third child gets 3";
};
subtest "all-fixed children overflow → trailing children clipped to 0" => {
plan 4;
# Fixed children consume in order; once $available hits 0 the rest
# silently get 0, mirroring CSS overflow behaviour. The parent does
# NOT try to redistribute.
my @kids = (
StubKid.new(sizing => Sizing.fixed(6)),
StubKid.new(sizing => Sizing.fixed(8)),
StubKid.new(sizing => Sizing.fixed(4)),
);
my @a = allocate-along-axis(@kids, 10);
is @a[0], 6, "first child gets full request";
is @a[1], 4, "second child clipped to remaining 4";
is @a[2], 0, "third child clipped to 0";
is sum(@a),10, "total allocation equals available space";
};
subtest "fixed + percent + flex compose" => {
plan 5;
my @kids = (
StubKid.new(sizing => Sizing.fixed(2)), # 2 cells
StubKid.new(sizing => Sizing.percent(20)), # 20% of 50 = 10 cells
StubKid.new(sizing => Sizing.flex(1)), # rest: (50-12)/2 = 19
StubKid.new(sizing => Sizing.flex(1)), # rest: 19
);
my @a = allocate-along-axis(@kids, 50);
is @a[0], 2, "fixed child unchanged";
is @a[1], 10, "percent child takes 20% of 50 = 10";
is @a[2], 19, "first flex child takes half of remaining 38";
is @a[3], 19, "second flex child takes the other half";
is sum(@a),50, "total equals available";
};
subtest "rounding remainder goes to last (highest-index) flex child" => {
plan 4;
# 10 cells / 3 flex children = 3.33 each. Floor → 3, 3, 3 with
# 1 remaining; the last flex child should pick it up.
my @kids = (
StubKid.new(sizing => Sizing.flex(1)),
StubKid.new(sizing => Sizing.flex(1)),
StubKid.new(sizing => Sizing.flex(1)),
);
my @a = allocate-along-axis(@kids, 10);
is @a[0], 3, "first flex floored to 3";
is @a[1], 3, "middle flex floored to 3";
is @a[2], 4, "last flex absorbs rounding remainder";
is sum(@a),10, "total equals available";
};
subtest "equal flex weights → equal split when divisible" => {
plan 3;
my @kids = (
StubKid.new(sizing => Sizing.flex(1)),
StubKid.new(sizing => Sizing.flex(1)),
);
my @a = allocate-along-axis(@kids, 12);
is @a[0], 6, "first flex = half";
is @a[1], 6, "second flex = half";
is sum(@a),12, "total equals available";
};
subtest "weighted flex children get proportional shares" => {
plan 3;
# flex(2) gets twice flex(1)'s share. 12 cells split 1:2 → 4 + 8.
my @kids = (
StubKid.new(sizing => Sizing.flex(1)),
StubKid.new(sizing => Sizing.flex(2)),
);
my @a = allocate-along-axis(@kids, 12);
is @a[0], 4, "flex(1) gets 1/3 of 12";
is @a[1], 8, "flex(2) gets 2/3 of 12";
is sum(@a),12, "total equals available";
};
subtest "zero available → all allocations zero" => {
plan 4;
my @kids = (
StubKid.new(sizing => Sizing.fixed(3)),
StubKid.new(sizing => Sizing.percent(50)),
StubKid.new(sizing => Sizing.flex(1)),
);
my @a = allocate-along-axis(@kids, 0);
is @a[0], 0, "fixed gets nothing when nothing's available";
is @a[1], 0, "percent gets nothing";
is @a[2], 0, "flex gets nothing";
is sum(@a),0, "total is zero";
};
subtest "gap 0 is a no-op across every scenario above" => {
# Property check over the same inputs the subtests above pin
# exact numbers for: adding the gap parameter must not have
# perturbed a single allocation on the gap-free path. Every caller
# in the framework (and downstream) omits :gap, so a discrepancy
# here would move every golden in the distro.
my @scenarios =
'all-fixed exact fit' => ((Sizing.fixed(3), Sizing.fixed(4), Sizing.fixed(3)), 10),
'all-fixed overflow' => ((Sizing.fixed(6), Sizing.fixed(8), Sizing.fixed(4)), 10),
'fixed+percent+flex' => ((Sizing.fixed(2), Sizing.percent(20),
Sizing.flex(1), Sizing.flex(1)), 50),
'rounding remainder' => ((Sizing.flex(1), Sizing.flex(1), Sizing.flex(1)), 10),
'even split' => ((Sizing.flex(1), Sizing.flex(1)), 12),
'weighted flex' => ((Sizing.flex(1), Sizing.flex(2)), 12),
'zero available' => ((Sizing.fixed(3), Sizing.percent(50), Sizing.flex(1)), 0),
;
plan @scenarios.elems;
for @scenarios -> (:key($name), :value((@sizings, $total))) {
my @kids = @sizings.map({ StubKid.new(sizing => $_) });
is-deeply allocate-along-axis(@kids, $total, :gap(0)),
allocate-along-axis(@kids, $total),
"$name: :gap(0) allocates identically";
}
};