feat(udpscope): BSP pane tree with split, close and hit-testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
c1029a25df
commit
0e5d103e73
@@ -0,0 +1,169 @@
|
||||
#include "PaneTree.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace udpscope;
|
||||
|
||||
namespace {
|
||||
|
||||
const Rect kScreen{0.0, 0.0, 1000.0, 600.0};
|
||||
|
||||
std::vector<PaneTree::Placed> leavesOf(const PaneTree& tree, const Rect& area) {
|
||||
std::vector<PaneTree::Placed> leaves;
|
||||
std::vector<PaneTree::Splitter> 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<PaneTree::Placed> out;
|
||||
std::vector<PaneTree::Splitter> 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<PaneTree::Placed> leaves;
|
||||
std::vector<PaneTree::Splitter> 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);
|
||||
}
|
||||
Reference in New Issue
Block a user