#include "PaneTree.h" #include using namespace udpscope; namespace { const Rect kScreen{0.0, 0.0, 1000.0, 600.0}; std::vector leavesOf(const PaneTree& tree, const Rect& area) { std::vector leaves; std::vector splitters; tree.layout(area, leaves, splitters); return leaves; } } /* namespace */ TEST(PaneTree, StartsAsOneEmptyLeafFillingTheArea) { PaneTree tree; EXPECT_EQ(tree.leafCount(), 1u); const auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 1u); EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0); EXPECT_DOUBLE_EQ(leaves[0].rect.h, 600.0); EXPECT_TRUE(leaves[0].leaf->signals.empty()); } TEST(PaneTree, SplittingIntoColumnsHalvesTheWidth) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); const auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); EXPECT_DOUBLE_EQ(leaves[0].rect.w, 500.0); EXPECT_DOUBLE_EQ(leaves[1].rect.w, 500.0); EXPECT_DOUBLE_EQ(leaves[0].rect.h, 600.0); EXPECT_DOUBLE_EQ(leaves[1].rect.x, 500.0); } TEST(PaneTree, SplittingIntoRowsHalvesTheHeight) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Rows); const auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); EXPECT_DOUBLE_EQ(leaves[0].rect.h, 300.0); EXPECT_DOUBLE_EQ(leaves[1].rect.y, 300.0); EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0); } // The pane being split keeps its content; the new pane is the empty one. TEST(PaneTree, SplitKeepsTheOriginalContentInTheFirstChild) { PaneTree tree; tree.root()->signals.push_back(Assignment{"Voltage", Color{}, 1.5f, VScale{}}); tree.splitLeaf(tree.root(), Orient::Columns); const auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); ASSERT_EQ(leaves[0].leaf->signals.size(), 1u); EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Voltage"); EXPECT_TRUE(leaves[1].leaf->signals.empty()); } TEST(PaneTree, ClosingALeafGivesItsSpaceToTheSibling) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); leaves[1].leaf->signals.push_back(Assignment{"Keep", Color{}, 1.5f, VScale{}}); tree.closeLeaf(leaves[0].leaf); EXPECT_EQ(tree.leafCount(), 1u); leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 1u); EXPECT_DOUBLE_EQ(leaves[0].rect.w, 1000.0); ASSERT_EQ(leaves[0].leaf->signals.size(), 1u); EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Keep"); } TEST(PaneTree, RefusesToCloseTheLastLeaf) { PaneTree tree; tree.closeLeaf(tree.root()); EXPECT_EQ(tree.leafCount(), 1u); } // A pane in the middle of a 3x3 touches no window edge. It must still be // splittable, which is why handles are inset inside the pane rather than // keyed on the window border. TEST(PaneTree, AnInteriorPaneIsStillSplittable) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Rows); // top / bottom auto leaves = leavesOf(tree, kScreen); tree.splitLeaf(leaves[1].leaf, Orient::Rows); // 3 rows leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 3u); PaneNode* middle = leaves[1].leaf; tree.splitLeaf(middle, Orient::Columns); leaves = leavesOf(tree, kScreen); tree.splitLeaf(leaves[2].leaf, Orient::Columns); EXPECT_EQ(tree.leafCount(), 5u); } TEST(PaneTree, LayoutReportsOneSplitterPerSplitNode) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); auto leaves = leavesOf(tree, kScreen); tree.splitLeaf(leaves[0].leaf, Orient::Rows); std::vector out; std::vector splitters; tree.layout(kScreen, out, splitters); EXPECT_EQ(out.size(), 3u); EXPECT_EQ(splitters.size(), 2u); } TEST(PaneTree, RatioSurvivesALayoutRoundTrip) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); tree.setRatio(tree.root(), 0.25); const auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); EXPECT_DOUBLE_EQ(leaves[0].rect.w, 250.0); EXPECT_DOUBLE_EQ(leaves[1].rect.w, 750.0); } TEST(PaneTree, RatioIsClampedSoNeitherPaneGoesBelowTheMinimum) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); tree.setRatio(tree.root(), 0.001); const auto leaves = leavesOf(tree, kScreen); EXPECT_GE(leaves[0].rect.w, kMinPaneSize); EXPECT_GE(leaves[1].rect.w, kMinPaneSize); } TEST(PaneTree, HitTestFindsTheSplitterBetweenTwoPanes) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); std::vector leaves; std::vector splitters; tree.layout(kScreen, leaves, splitters); ASSERT_EQ(splitters.size(), 1u); const PaneTree::Splitter* hit = tree.hitTestSplitter(splitters, 500.0, 300.0); ASSERT_NE(hit, nullptr); EXPECT_EQ(hit->orient, Orient::Columns); EXPECT_EQ(tree.hitTestSplitter(splitters, 100.0, 300.0), nullptr); } TEST(PaneTree, HitTestFindsInsetSplitHandlesAndTheCloseButton) { const Rect pane{0.0, 0.0, 400.0, 300.0}; EXPECT_EQ(PaneTree::hitTestHandle(pane, 8.0, 150.0), Handle::Left); EXPECT_EQ(PaneTree::hitTestHandle(pane, 392.0, 150.0), Handle::Right); EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 8.0), Handle::Top); EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 292.0), Handle::Bottom); EXPECT_EQ(PaneTree::hitTestHandle(pane, 392.0, 8.0), Handle::Close); EXPECT_EQ(PaneTree::hitTestHandle(pane, 200.0, 150.0), Handle::None); } // Gap 1: closeLeaf only tested with first child closed; test closing the second child. TEST(PaneTree, ClosingTheSecondLeafPreservesTheFirstLeafContent) { PaneTree tree; tree.splitLeaf(tree.root(), Orient::Columns); auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); // Assign distinct signals to each leaf leaves[0].leaf->signals.push_back(Assignment{"Signal_A", Color{}, 1.5f, VScale{}}); leaves[1].leaf->signals.push_back(Assignment{"Signal_B", Color{}, 1.5f, VScale{}}); // Close the second leaf; the first should survive with its content tree.closeLeaf(leaves[1].leaf); EXPECT_EQ(tree.leafCount(), 1u); leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 1u); ASSERT_EQ(leaves[0].leaf->signals.size(), 1u); EXPECT_EQ(leaves[0].leaf->signals[0].signalName, "Signal_A"); } // Gap 2: closeLeaf only tested at depth 1; test at depth 2 (deeper recursion in findParent). TEST(PaneTree, ClosingALeafAtDepth2PreservesOthersAndUpdatesCount) { PaneTree tree; // Build tree: split root (a, b), split b to get depth-2 leaf in the RIGHT subtree tree.splitLeaf(tree.root(), Orient::Columns); // depth 1: root splits into a, b auto leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); tree.splitLeaf(leaves[1].leaf, Orient::Rows); // depth 2: b splits into b.a, b.b leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 3u); // Assign distinct signals to each of the three leaves leaves[0].leaf->signals.push_back(Assignment{"Depth1_Left", Color{}, 1.5f, VScale{}}); leaves[1].leaf->signals.push_back(Assignment{"Depth2_TopRight", Color{}, 1.5f, VScale{}}); leaves[2].leaf->signals.push_back(Assignment{"Depth2_BottomRight", Color{}, 1.5f, VScale{}}); // Close the first depth-2 leaf (leaves[1], which is in the right subtree) tree.closeLeaf(leaves[1].leaf); EXPECT_EQ(tree.leafCount(), 2u); leaves = leavesOf(tree, kScreen); ASSERT_EQ(leaves.size(), 2u); // Verify the surviving depth-2 leaf has its signal intact bool found_left = false; bool found_bottom_right = false; for (const auto& leaf : leaves) { ASSERT_EQ(leaf.leaf->signals.size(), 1u); if (leaf.leaf->signals[0].signalName == "Depth1_Left") { found_left = true; } if (leaf.leaf->signals[0].signalName == "Depth2_BottomRight") { found_bottom_right = true; } } EXPECT_TRUE(found_left); EXPECT_TRUE(found_bottom_right); }