Modules/Widgets/tests/AlgorithmTests.cpp
Шурупов Илья Викторович 9e5dc76a86 Keep floating panels from shrinking below their content
2026-10-14 20:32:05 +03:00

256 lines
7.9 KiB
C++

#include "Animation.hpp"
#include "Popup.hpp"
#include "SpaceDistribution.hpp"
#include "UnitTest++/UnitTest++.h"
using namespace tp;
using namespace tp::ui;
namespace {
constexpr float kTolerance = 0.05f;
Extent grower(halnf preferred, halnf weight = 1, halnf minimum = 0, halnf maximum = kUnbounded) {
return { minimum, preferred, maximum, weight };
}
}
SUITE(AxisSizingResolution) {
TEST(FixedIgnoresContent) {
const Extent resolved = AxisSizing::fixed(120).resolve(Extent::shrinkable(40));
CHECK_CLOSE(120, resolved.minimum, kTolerance);
CHECK_CLOSE(120, resolved.preferred, kTolerance);
CHECK_CLOSE(120, resolved.maximum, kTolerance);
CHECK_CLOSE(0, resolved.grow, kTolerance);
}
TEST(FitKeepsContentPreferenceWithoutGrowing) {
const Extent resolved = AxisSizing::fit().resolve(Extent::shrinkable(40));
CHECK_CLOSE(40, resolved.preferred, kTolerance);
CHECK_CLOSE(0, resolved.grow, kTolerance);
}
TEST(PreferredNeverDropsBelowContentMinimum) {
const Extent roomy = AxisSizing::preferred(200).resolve(Extent::shrinkable(40, 30));
CHECK_CLOSE(30, roomy.minimum, kTolerance);
CHECK_CLOSE(200, roomy.preferred, kTolerance);
CHECK_CLOSE(200, roomy.maximum, kTolerance);
const Extent tight = AxisSizing::preferred(10).resolve(Extent::shrinkable(40, 30));
CHECK_CLOSE(30, tight.preferred, kTolerance);
CHECK_CLOSE(30, tight.maximum, kTolerance);
}
TEST(FillGrowsWithWeight) {
const Extent resolved = AxisSizing::fill(3).resolve(Extent::shrinkable(40));
CHECK_CLOSE(40, resolved.preferred, kTolerance);
CHECK_CLOSE(3, resolved.grow, kTolerance);
}
TEST(BoundsClampPreferenceAndRange) {
const Extent resolved = AxisSizing::fill().atLeast(50).atMost(80).resolve(Extent::shrinkable(10));
CHECK_CLOSE(50, resolved.minimum, kTolerance);
CHECK_CLOSE(50, resolved.preferred, kTolerance);
CHECK_CLOSE(80, resolved.maximum, kTolerance);
}
TEST(MinimumAboveMaximumWins) {
const Extent resolved = AxisSizing::fit().atLeast(90).atMost(40).resolve(Extent::shrinkable(10));
CHECK_CLOSE(40, resolved.minimum, kTolerance);
CHECK(resolved.maximum >= resolved.minimum);
}
}
SUITE(SpaceDistribution) {
TEST(RigidItemsKeepPreferredSizes) {
const auto lengths = distributeSpace({ Extent::rigid(10), Extent::rigid(20) }, 100);
CHECK_CLOSE(10, lengths[0], kTolerance);
CHECK_CLOSE(20, lengths[1], kTolerance);
}
TEST(EqualWeightsShareFreeSpaceEqually) {
const auto lengths = distributeSpace({ grower(0), grower(0) }, 100);
CHECK_CLOSE(50, lengths[0], kTolerance);
CHECK_CLOSE(50, lengths[1], kTolerance);
}
TEST(WeightsSplitProportionally) {
const auto lengths = distributeSpace({ grower(0, 1), grower(0, 3) }, 100);
CHECK_CLOSE(25, lengths[0], kTolerance);
CHECK_CLOSE(75, lengths[1], kTolerance);
}
TEST(GrowingStartsFromPreferredSizes) {
const auto lengths = distributeSpace({ grower(20), grower(40) }, 100);
CHECK_CLOSE(40, lengths[0], kTolerance);
CHECK_CLOSE(60, lengths[1], kTolerance);
}
TEST(FrozenItemGivesSpaceToOthers) {
const auto lengths = distributeSpace({ grower(0, 1, 0, 10), grower(0) }, 100);
CHECK_CLOSE(10, lengths[0], kTolerance);
CHECK_CLOSE(90, lengths[1], kTolerance);
}
TEST(RigidPlusGrowerFillsRemainder) {
const auto lengths = distributeSpace({ Extent::rigid(30), grower(0) }, 100);
CHECK_CLOSE(30, lengths[0], kTolerance);
CHECK_CLOSE(70, lengths[1], kTolerance);
}
TEST(ShrinkingIsProportionalToSlackAboveMinimum) {
const auto lengths = distributeSpace({ grower(60, 0, 50), grower(60, 0, 0) }, 80);
const halnf deficit = 40;
const halnf totalSlack = 10 + 60;
CHECK_CLOSE(60 - deficit * 10 / totalSlack, lengths[0], kTolerance);
CHECK_CLOSE(60 - deficit * 60 / totalSlack, lengths[1], kTolerance);
CHECK(lengths[0] >= 50);
}
TEST(OverflowStopsAtMinimums) {
const auto lengths = distributeSpace({ grower(40, 0, 40), grower(40, 0, 40) }, 50);
CHECK_CLOSE(40, lengths[0], kTolerance);
CHECK_CLOSE(40, lengths[1], kTolerance);
}
TEST(FractionalSpaceTerminates) {
std::vector<Extent> items(7, grower(0, 1, 0, 13.37f));
const auto lengths = distributeSpace(items, 333.333f);
for (halnf length : lengths)
CHECK_CLOSE(13.37f, length, kTolerance);
}
TEST(EmptyInputReturnsEmpty) { CHECK(distributeSpace({}, 100).empty()); }
}
SUITE(PixelSnapping) {
TEST(EdgesAreRoundedSoSegmentsTile) {
const auto segments = placeSnapped({ 33.3f, 33.3f, 33.4f }, 0, 0);
CHECK_EQUAL(0, segments[0].start);
CHECK_EQUAL(segments[0].start + segments[0].length, segments[1].start);
CHECK_EQUAL(segments[1].start + segments[1].length, segments[2].start);
CHECK_EQUAL(100, segments[2].start + segments[2].length);
}
TEST(GapsSeparateSegments) {
const auto segments = placeSnapped({ 10, 10 }, 5, 4);
CHECK_EQUAL(5, segments[0].start);
CHECK_EQUAL(19, segments[1].start);
}
}
SUITE(PopupPlacement) {
const RectF kScreen = makeRect(0, 0, 400, 300);
const RectF kAnchor = makeRect(100, 100, 80, 20);
TEST(BelowAlignedToStart) {
PopupOptions options;
options.gap = 2;
const RectF placed = placePopup(kAnchor, { 120, 50 }, kScreen, options);
CHECK_CLOSE(100, placed.pos.x, kTolerance);
CHECK_CLOSE(122, placed.pos.y, kTolerance);
}
TEST(RightOfAnchor) {
PopupOptions options;
options.placement = Placement::Right;
options.gap = 0;
const RectF placed = placePopup(kAnchor, { 50, 50 }, kScreen, options);
CHECK_CLOSE(180, placed.pos.x, kTolerance);
CHECK_CLOSE(100, placed.pos.y, kTolerance);
}
TEST(CenterAlignment) {
PopupOptions options;
options.align = PlacementAlign::Center;
options.gap = 0;
const RectF placed = placePopup(kAnchor, { 40, 10 }, kScreen, options);
CHECK_CLOSE(120, placed.pos.x, kTolerance);
}
TEST(FlipsAboveWhenNoRoomBelow) {
PopupOptions options;
options.gap = 0;
const RectF lowAnchor = makeRect(100, 270, 80, 20);
const RectF placed = placePopup(lowAnchor, { 100, 100 }, kScreen, options);
CHECK_CLOSE(170, placed.pos.y, kTolerance);
}
TEST(FlipsLeftWhenNoRoomRight) {
PopupOptions options;
options.placement = Placement::Right;
options.gap = 0;
const RectF edgeAnchor = makeRect(350, 50, 40, 20);
const RectF placed = placePopup(edgeAnchor, { 100, 50 }, kScreen, options);
CHECK_CLOSE(250, placed.pos.x, kTolerance);
}
TEST(SlidesInsideScreenOnCrossAxis) {
PopupOptions options;
options.gap = 0;
const RectF edgeAnchor = makeRect(380, 10, 20, 20);
const RectF placed = placePopup(edgeAnchor, { 100, 50 }, kScreen, options);
CHECK_CLOSE(300, placed.pos.x, kTolerance);
}
TEST(OversizedPopupPinnedToOrigin) {
PopupOptions options;
const RectF placed = placePopup(kAnchor, { 1000, 1000 }, kScreen, options);
CHECK_CLOSE(0, placed.pos.x, kTolerance);
CHECK_CLOSE(0, placed.pos.y, kTolerance);
}
}
SUITE(SpringAnimation) {
TEST(ConvergesToTarget) {
AnimatedFloat value(0, Motion::quick());
value.animateTo(100);
int frames = 0;
while (value.advance(16) && frames < 1000)
frames++;
CHECK_CLOSE(100, value.value(), 0.01f);
CHECK(frames < 120);
CHECK(!value.isAnimating());
}
TEST(MovesTowardTargetMonotonicallyAtFirst) {
AnimatedFloat value(0, Motion::smooth());
value.animateTo(10);
value.advance(16);
const halnf first = value.value();
value.advance(16);
CHECK(first > 0);
CHECK(value.value() > first);
}
TEST(JumpAndFinishAreImmediate) {
AnimatedRect rect(makeRect(0, 0, 10, 10));
rect.animateTo(makeRect(50, 50, 20, 20));
CHECK(rect.isAnimating());
rect.finish();
CHECK(!rect.isAnimating());
CHECK(rect.value() == makeRect(50, 50, 20, 20));
rect.jumpTo(makeRect(1, 2, 3, 4));
CHECK(rect.value() == makeRect(1, 2, 3, 4));
}
TEST(ColorChannelsAnimateTogether) {
AnimatedColor color(RGBA(0, 0, 0, 1));
color.animateTo(RGBA(1, 0.5f, 0, 1));
while (color.advance(16)) {
}
CHECK_CLOSE(1, color.value().r, 0.01f);
CHECK_CLOSE(0.5f, color.value().g, 0.01f);
}
TEST(LargeTimeStepIsStable) {
AnimatedFloat value(0, Motion::quick());
value.animateTo(1);
value.advance(5000);
CHECK_CLOSE(1, value.value(), 0.01f);
}
}