#include "BoxLayout.hpp" #include "Containers.hpp" #include "Label.hpp" #include "Probes.hpp" #include "StackLayout.hpp" #include "UiDriver.hpp" #include "UnitTest++/UnitTest++.h" #include #include #include using namespace tp; using namespace tp::ui; using namespace tp::ui::testing; namespace { void checkRect(const RectF& expected, const RectF& actual) { CHECK_CLOSE(expected.pos.x, actual.pos.x, 0.01f); CHECK_CLOSE(expected.pos.y, actual.pos.y, 0.01f); CHECK_CLOSE(expected.size.x, actual.size.x, 0.01f); CHECK_CLOSE(expected.size.y, actual.size.y, 0.01f); } } SUITE(BoxLayoutArrangement) { TEST(ColumnStacksChildrenWithGapAndPadding) { Widget root; root.emplaceLayout(5).setPadding(Insets::uniform(10)); Box first({ 40, 20 }); Box second({ 60, 30 }); root.addChild(first); root.addChild(second); UiDriver driver(root, { 200, 200 }); checkRect(makeRect(0, 0, 200, 200), root.rect()); checkRect(makeRect(10, 10, 180, 20), first.rect()); checkRect(makeRect(10, 35, 180, 30), second.rect()); } TEST(RowLaysOutHorizontallyAndStretchesCrossAxis) { Widget root; root.emplaceLayout(4); Box first({ 40, 20 }); Box second({ 60, 30 }); root.addChild(first); root.addChild(second); UiDriver driver(root, { 300, 100 }); checkRect(makeRect(0, 0, 40, 100), first.rect()); checkRect(makeRect(44, 0, 60, 100), second.rect()); } TEST(CrossAlignCenterUsesPreferredCrossSize) { Widget root; root.emplaceLayout().setCrossAlign(CrossAlign::Center); Box child({ 40, 20 }); root.addChild(child); UiDriver driver(root, { 300, 100 }); checkRect(makeRect(0, 40, 40, 20), child.rect()); } TEST(CrossAlignEnd) { Widget root; root.emplaceLayout().setCrossAlign(CrossAlign::End); Box child({ 40, 20 }); root.addChild(child); UiDriver driver(root, { 300, 100 }); checkRect(makeRect(260, 0, 40, 20), child.rect()); } TEST(FillChildrenShareRemainingSpaceByWeight) { Widget root; root.emplaceLayout(); Box fixed({ 100, 10 }); Box single; Box triple; fixed.setSizing(AxisSizing::fixed(100), AxisSizing::fit()); single.setSizing(AxisSizing::fill(1), AxisSizing::fit()); triple.setSizing(AxisSizing::fill(3), AxisSizing::fit()); root.addChild(fixed); root.addChild(single); root.addChild(triple); UiDriver driver(root, { 500, 50 }); CHECK_CLOSE(100, fixed.rect().size.x, 0.01f); CHECK_CLOSE(100, single.rect().size.x, 0.01f); CHECK_CLOSE(300, triple.rect().size.x, 0.01f); CHECK_CLOSE(200, triple.rect().pos.x, 0.01f); } TEST(FillRespectsMaximumAndRedistributes) { Widget root; root.emplaceLayout(); Box capped; Box free; capped.setSizing(AxisSizing::fit(), AxisSizing::fill().atMost(50)); free.setSizing(AxisSizing::fit(), AxisSizing::fill()); root.addChild(capped); root.addChild(free); UiDriver driver(root, { 100, 400 }); CHECK_CLOSE(50, capped.rect().size.y, 0.01f); CHECK_CLOSE(350, free.rect().size.y, 0.01f); } TEST(MainAlignCenterAndEnd) { Widget root; BoxLayout& layout = root.emplaceLayout(); Box child({ 100, 10 }); root.addChild(child); UiDriver driver(root, { 400, 50 }); layout.setMainAlign(MainAlign::Center); driver.layout(); CHECK_CLOSE(150, child.rect().pos.x, 0.01f); layout.setMainAlign(MainAlign::End); driver.layout(); CHECK_CLOSE(300, child.rect().pos.x, 0.01f); } TEST(SpaceBetweenSpreadsChildren) { Widget root; root.emplaceLayout().setMainAlign(MainAlign::SpaceBetween); Box first({ 50, 10 }); Box second({ 50, 10 }); Box third({ 50, 10 }); root.addChild(first); root.addChild(second); root.addChild(third); UiDriver driver(root, { 350, 50 }); CHECK_CLOSE(0, first.rect().pos.x, 0.01f); CHECK_CLOSE(150, second.rect().pos.x, 0.01f); CHECK_CLOSE(300, third.rect().pos.x, 0.01f); } TEST(HiddenChildrenTakeNoSpace) { Widget root; root.emplaceLayout(10); Box first({ 10, 20 }); Box hidden; Box last({ 10, 20 }); hidden.setSizing(AxisSizing::fit(), AxisSizing::fixed(100)); root.addChild(first); root.addChild(hidden); root.addChild(last); hidden.setVisible(false); UiDriver driver(root, { 100, 300 }); CHECK_CLOSE(30, last.rect().pos.y, 0.01f); hidden.setVisible(true); driver.layout(); CHECK_CLOSE(140, last.rect().pos.y, 0.01f); } TEST(OverflowShrinksFlexibleChildrenButNotRigidOnes) { Widget root; root.emplaceLayout(); Box rigid; Box flexible({ 10, 400 }); rigid.setSizing(AxisSizing::fit(), AxisSizing::fixed(100)); root.addChild(rigid); root.addChild(flexible); UiDriver driver(root, { 100, 300 }); CHECK_CLOSE(100, rigid.rect().size.y, 0.01f); CHECK_CLOSE(200, flexible.rect().size.y, 0.01f); } TEST(OverflowTakesSpaceFromScrollableContentNotFromControls) { Widget root; root.emplaceLayout(); Label header("Header"); Widget content; content.setSizing(AxisSizing::fill(), AxisSizing::fill()); content.emplaceLayout(); Box tall({ 10, 2000 }); content.addChild(tall); root.addChild(header); root.addChild(content); UiDriver driver(root, { 200, 300 }); CHECK_CLOSE(driver.ui().theme().metrics.lineHeight, header.size().y, 0.01f); CHECK_CLOSE(300 - header.size().y, content.size().y, 0.01f); } TEST(LayoutChangesAnimateWidgetsAndTheirSiblings) { Widget root; root.emplaceLayout(); Box first({ 100, 50 }); Box second({ 100, 50 }); root.addChild(first); root.addChild(second); UiDriver driver(root, { 600, 100 }); driver.setAutoSettle(false); first.setPreferred({ 300, 50 }); driver.ui().updateLayout(); driver.advance(32); CHECK(first.size().x > 100 && first.size().x < 300); CHECK(second.rect().pos.x > 100 && second.rect().pos.x < 300); CHECK(driver.ui().needsFrame()); driver.settle(); CHECK_CLOSE(300, first.size().x, 0.01f); CHECK_CLOSE(300, second.rect().pos.x, 0.01f); } TEST(ShownWidgetGrowsIntoPlace) { Widget root; root.emplaceLayout(); Box header({ 100, 30 }); Box details({ 100, 80 }); details.setVisible(false); root.addChild(header); root.addChild(details); UiDriver driver(root, { 200, 300 }); driver.setAutoSettle(false); details.setVisible(true); driver.ui().updateLayout(); CHECK(details.size().y < 1); driver.advance(48); CHECK(details.size().y > 1 && details.size().y < 80); driver.settle(); CHECK_CLOSE(80, details.size().y, 0.01f); } TEST(DisablingGeometryAnimationAppliesLayoutImmediately) { Widget root; root.emplaceLayout(); Box first({ 100, 50 }); first.setGeometryAnimated(false); root.addChild(first); UiDriver driver(root, { 600, 100 }); driver.setAutoSettle(false); first.setPreferred({ 300, 50 }); driver.ui().updateLayout(); CHECK_CLOSE(300, first.size().x, 0.01f); } TEST(EmptyContainerIsHarmless) { Widget root; root.emplaceLayout(10); UiDriver driver(root, { 100, 100 }); checkRect(makeRect(0, 0, 100, 100), root.rect()); } } SUITE(FitSizing) { TEST(FitContainerWrapsItsChildren) { Widget root; root.emplaceLayout(); Widget card; card.emplaceLayout(4).setPadding(Insets::uniform(8)); Box a({ 100, 20 }); Box b({ 60, 30 }); card.addChild(a); card.addChild(b); root.addChild(card); UiDriver driver(root, { 800, 600 }); CHECK_CLOSE(116, card.rect().size.x, 0.01f); CHECK_CLOSE(8 + 20 + 4 + 30 + 8, card.rect().size.y, 0.01f); } TEST(FitContainerFollowsChildGrowthInSameFrame) { Widget root; root.emplaceLayout(); Widget card; card.emplaceLayout(); Box child({ 50, 20 }); card.addChild(child); root.addChild(card); UiDriver driver(root); child.setPreferred({ 70, 90 }); driver.layout(); CHECK_CLOSE(70, card.rect().size.x, 0.01f); CHECK_CLOSE(90, card.rect().size.y, 0.01f); } TEST(NestedFillInsideFitInsideFixedResolvesInOnePass) { Widget root; root.emplaceLayout(); Widget fixedFrame; fixedFrame.setSizing(AxisSizing::fixed(300), AxisSizing::fixed(200)); fixedFrame.emplaceLayout(); Widget fitColumn; fitColumn.emplaceLayout(); Box content({ 80, 40 }); Box stretch; stretch.setSizing(AxisSizing::fill(), AxisSizing::fill()); fitColumn.addChild(content); fitColumn.addChild(stretch); Box sibling; sibling.setSizing(AxisSizing::fill(), AxisSizing::fill()); fixedFrame.addChild(fitColumn); fixedFrame.addChild(sibling); root.addChild(fixedFrame); UiDriver driver(root); CHECK_CLOSE(80, fitColumn.rect().size.x, 0.01f); CHECK_CLOSE(200, fitColumn.rect().size.y, 0.01f); CHECK_CLOSE(160, stretch.rect().size.y, 0.01f); CHECK_CLOSE(220, sibling.rect().size.x, 0.01f); } } SUITE(OtherLayouts) { TEST(StackPlacesEveryChildOverWholeArea) { Widget root; root.emplaceLayout().setPadding(Insets::uniform(5)); Box bottom; Box top; root.addChild(bottom); root.addChild(top); UiDriver driver(root, { 100, 80 }); checkRect(makeRect(5, 5, 90, 70), bottom.rect()); checkRect(makeRect(5, 5, 90, 70), top.rect()); } TEST(FreeLayoutUsesRequestedPositionAndPreferredSize) { Widget root; root.emplaceLayout(); Box child({ 30, 40 }); child.requestPosition({ 25, 35 }); root.addChild(child); UiDriver driver(root); checkRect(makeRect(25, 35, 30, 40), child.rect()); child.requestPosition({ 100, 10 }); driver.layout(); checkRect(makeRect(100, 10, 30, 40), child.rect()); } TEST(GridPlacesChildrenInRowsOfEqualColumns) { Widget root; root.emplaceLayout(2, 10); Box cells[3] = { Box({ 10, 20 }), Box({ 10, 40 }), Box({ 10, 30 }) }; for (Box& cell : cells) root.addChild(cell); UiDriver driver(root, { 210, 300 }); checkRect(makeRect(0, 0, 100, 40), cells[0].rect()); checkRect(makeRect(110, 0, 100, 40), cells[1].rect()); checkRect(makeRect(0, 50, 100, 30), cells[2].rect()); } } SUITE(IncrementalLayout) { TEST(SecondPassWithoutChangesArrangesNothing) { Widget root; root.emplaceLayout(); Box children[10]; for (Box& child : children) { child.setPreferred({ 10, 10 }); root.addChild(child); } UiDriver driver(root); driver.ui().resetStats(); driver.layout(); CHECK_EQUAL(0u, driver.ui().stats().arrangedWidgets); CHECK_EQUAL(0u, driver.ui().stats().layoutPasses); } TEST(ChangingOneLeafDoesNotRemeasureSiblingsSubtrees) { Widget root; root.emplaceLayout(); Widget left; left.emplaceLayout(); Box leftLeaves[20]; for (Box& leaf : leftLeaves) { leaf.setPreferred({ 5, 5 }); left.addChild(leaf); } Box changing({ 10, 10 }); root.addChild(left); root.addChild(changing); UiDriver driver(root); driver.ui().resetStats(); changing.setPreferred({ 10, 20 }); driver.layout(); CHECK(driver.ui().stats().measuredWidgets < 6); CHECK_CLOSE(20, changing.rect().size.y, 0.01f); } TEST(ViewportResizeRelayouts) { Widget root; root.emplaceLayout(); Box fill; fill.setSizing(AxisSizing::fill(), AxisSizing::fill()); root.addChild(fill); UiDriver driver(root, { 100, 100 }); driver.resize({ 640, 480 }); checkRect(makeRect(0, 0, 640, 480), fill.rect()); } TEST(LayoutChangeIsVisibleImmediatelyAfterDispatch) { Widget root; root.emplaceLayout(); EventProbe probe; Box other({ 10, 10 }); root.addChild(probe); root.addChild(other); UiDriver driver(root); probe.setPreferred({ 50, 100 }); driver.moveTo({ 5, 105 }); CHECK_CLOSE(100, other.rect().pos.y, 0.01f); CHECK(driver.ui().hoveredWidget() == &other); } } SUITE(TreeOwnership) { TEST(DestroyingChildDetachesItFromParent) { Widget root; root.emplaceLayout(); UiDriver driver(root); { Box temporary({ 10, 10 }); root.addChild(temporary); driver.layout(); CHECK_EQUAL(1u, root.children().size()); } CHECK_EQUAL(0u, root.children().size()); driver.layout(); } TEST(ReparentingMovesChild) { Widget first; Widget second; Box child; first.addChild(child); second.addChild(child); CHECK(child.parent() == &second); CHECK(first.children().empty()); } TEST(CannotCreateCycles) { Widget parent; Widget child; parent.addChild(child); child.addChild(parent); CHECK(parent.parent() == nullptr); CHECK(child.children().empty()); } TEST(AttachedSubtreeKnowsItsUi) { Widget root; Widget middle; Box leaf; middle.addChild(leaf); UiDriver driver(root); CHECK(leaf.ui() == nullptr); root.addChild(middle); CHECK(leaf.ui() == &driver.ui()); root.removeChild(middle); CHECK(leaf.ui() == nullptr); } TEST(WindowPositionAccumulatesAncestors) { Widget root; root.emplaceLayout().setPadding(Insets::uniform(10)); Widget inner; inner.emplaceLayout().setPadding(Insets::uniform(7)); Box leaf({ 5, 5 }); inner.addChild(leaf); root.addChild(inner); UiDriver driver(root); CHECK_CLOSE(17, leaf.windowPosition().x, 0.01f); CHECK_CLOSE(17, leaf.windowPosition().y, 0.01f); } } SUITE(PaintCulling) { TEST(OnlyVisibleRowsOfLongScrolledListArePainted) { Widget root; root.emplaceLayout(); ScrollView scroll; scroll.setSizing(AxisSizing::fill(), AxisSizing::fill()); Widget list; list.emplaceLayout(4); std::vector> rows; for (int index = 0; index < 3000; index++) { rows.push_back(std::make_unique(Vec2F(200.0f, 70.0f), "row " + std::to_string(index))); list.addChild(*rows.back()); } scroll.setContent(&list); root.addChild(scroll); UiDriver driver(root, { 600, 700 }); driver.paint(); const ualni painted = driver.ui().stats().paintedWidgets; CHECK(painted < 40); scroll.scrollTo(scroll.maximumOffset()); driver.settle(); const ualni before = driver.ui().stats().paintedWidgets; driver.paint(); const ualni paintedAtEnd = driver.ui().stats().paintedWidgets - before; CHECK(paintedAtEnd < 40); CHECK(paintedAtEnd > 5); CHECK(overlaps(rows.back()->windowRect(), scroll.windowRect())); } TEST(OverhangingDecorationsNearTheEdgeStillPaint) { Widget root; root.emplaceLayout(); ScrollView scroll; scroll.setSizing(AxisSizing::fill(), AxisSizing::fixed(100)); Widget list; list.emplaceLayout(); Box first(Vec2F(200.0f, 100.0f), "first"); Box second(Vec2F(200.0f, 100.0f), "second"); list.addChild(first); list.addChild(second); scroll.setContent(&list); root.addChild(scroll); UiDriver driver(root, { 400, 300 }); driver.paint(); const ualni before = driver.ui().stats().paintedWidgets; driver.paint(); CHECK(driver.ui().stats().paintedWidgets - before >= 4); } } SUITE(FillSizing) { TEST(FillGrowsPastContentMaximum) { Widget root; root.emplaceLayout(); Widget row; row.setSizing(AxisSizing::fill(), AxisSizing::fit()); RowLayout& rowLayout = row.emplaceLayout(); Box fixedA(Vec2F(100.0f, 20.0f), "a"); fixedA.setSizing(AxisSizing::fixed(100), AxisSizing::fixed(20)); Box fixedB(Vec2F(100.0f, 20.0f), "b"); fixedB.setSizing(AxisSizing::fixed(100), AxisSizing::fixed(20)); row.addChild(fixedA); row.addChild(fixedB); rowLayout.setMainAlign(MainAlign::SpaceBetween); root.addChild(row); UiDriver driver(root, { 800, 200 }); CHECK_CLOSE(800, row.size().x, 0.5f); CHECK_CLOSE(700, fixedB.windowRect().pos.x, 0.5f); } TEST(FillingScrollViewTakesRemainingHeightEvenWhenContentIsShort) { Widget root; root.emplaceLayout(); ScrollView scroll; scroll.setSizing(AxisSizing::fill(), AxisSizing::fill()); Box item(Vec2F(100.0f, 50.0f), "item"); scroll.setContent(&item); Box footer(Vec2F(100.0f, 60.0f), "footer"); footer.setSizing(AxisSizing::fill(), AxisSizing::fixed(60)); root.addChild(scroll); root.addChild(footer); UiDriver driver(root, { 800, 600 }); CHECK_CLOSE(540, scroll.size().y, 0.5f); CHECK_CLOSE(600, footer.windowRect().pos.y + footer.size().y, 0.5f); } TEST(ExplicitMaximumStillCapsFill) { Widget root; root.emplaceLayout(); Box capped(Vec2F(10.0f, 10.0f), "capped"); capped.setSizing(AxisSizing::fill().atMost(300), AxisSizing::fixed(10)); root.addChild(capped); UiDriver driver(root, { 800, 100 }); CHECK_CLOSE(300, capped.size().x, 0.5f); } } SUITE(SurfaceClipping) { TEST(OversizedContentDoesNotPaintOutsideItsPanel) { Widget root; root.emplaceLayout().setCrossAlign(CrossAlign::Start); Panel panel; panel.setSizing(AxisSizing::fixed(200), AxisSizing::fixed(120)); panel.emplaceLayout(); Panel big(Panel::Surface::Raised); big.setSizing(AxisSizing::fixed(200), AxisSizing::fixed(300)); panel.addChild(big); root.addChild(panel); UiDriver driver(root, { 600, 600 }); CHECK(panel.clipsChildren()); const RectF panelRect = panel.windowRect(); const RGBA raised = Theme::standard().palette.raisedSurface; const PaintCommand* oversized = nullptr; for (const PaintCommand& command : driver.paint().commands()) { if (command.kind == PaintCommand::Kind::FillRect && command.color == raised) oversized = &command; } REQUIRE CHECK(oversized != nullptr); CHECK_CLOSE(300, oversized->rect.size.y, 0.5f); CHECK(oversized->clip.pos.y + oversized->clip.size.y <= panelRect.pos.y + panelRect.size.y + 0.5f); } TEST(SurfacelessPanelsLeaveOverhangAlone) { Panel plain(Panel::Surface::None); CHECK(!plain.clipsChildren()); } }