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,157 @@
|
||||
#include "PaneTree.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace udpscope {
|
||||
|
||||
PaneTree::PaneTree() : root_(new PaneNode()) {}
|
||||
|
||||
void PaneTree::setRoot(std::unique_ptr<PaneNode> node) {
|
||||
if (node) { root_ = std::move(node); }
|
||||
}
|
||||
|
||||
double PaneTree::clampRatio(double ratio, double extent) {
|
||||
if (extent <= 2.0 * kMinPaneSize) {
|
||||
return 0.5; /* Too small to honour the minimum on both sides. */
|
||||
}
|
||||
const double lo = kMinPaneSize / extent;
|
||||
return std::min(std::max(ratio, lo), 1.0 - lo);
|
||||
}
|
||||
|
||||
void PaneTree::layoutNode(PaneNode* node, const Rect& r,
|
||||
std::vector<Placed>& leaves,
|
||||
std::vector<Splitter>& splitters) {
|
||||
if (node == nullptr) { return; }
|
||||
if (node->leaf) {
|
||||
leaves.push_back(Placed{node, r});
|
||||
return;
|
||||
}
|
||||
|
||||
if (node->orient == Orient::Columns) {
|
||||
const double ratio = clampRatio(node->ratio, r.w);
|
||||
const double wA = r.w * ratio;
|
||||
layoutNode(node->a.get(), Rect{r.x, r.y, wA, r.h}, leaves, splitters);
|
||||
layoutNode(node->b.get(), Rect{r.x + wA, r.y, r.w - wA, r.h}, leaves, splitters);
|
||||
splitters.push_back(Splitter{
|
||||
node,
|
||||
Rect{r.x + wA - kSplitterGrab * 0.5, r.y, kSplitterGrab, r.h},
|
||||
Orient::Columns});
|
||||
} else {
|
||||
const double ratio = clampRatio(node->ratio, r.h);
|
||||
const double hA = r.h * ratio;
|
||||
layoutNode(node->a.get(), Rect{r.x, r.y, r.w, hA}, leaves, splitters);
|
||||
layoutNode(node->b.get(), Rect{r.x, r.y + hA, r.w, r.h - hA}, leaves, splitters);
|
||||
splitters.push_back(Splitter{
|
||||
node,
|
||||
Rect{r.x, r.y + hA - kSplitterGrab * 0.5, r.w, kSplitterGrab},
|
||||
Orient::Rows});
|
||||
}
|
||||
}
|
||||
|
||||
void PaneTree::layout(const Rect& area,
|
||||
std::vector<Placed>& leaves,
|
||||
std::vector<Splitter>& splitters) const {
|
||||
leaves.clear();
|
||||
splitters.clear();
|
||||
layoutNode(root_.get(), area, leaves, splitters);
|
||||
}
|
||||
|
||||
void PaneTree::splitLeaf(PaneNode* leaf, Orient orient) {
|
||||
if (leaf == nullptr || !leaf->leaf) { return; }
|
||||
|
||||
/* Move the existing content into a new first child; the second is empty. */
|
||||
std::unique_ptr<PaneNode> first(new PaneNode());
|
||||
first->signals = std::move(leaf->signals);
|
||||
first->profilePane = leaf->profilePane;
|
||||
|
||||
std::unique_ptr<PaneNode> second(new PaneNode());
|
||||
|
||||
leaf->leaf = false;
|
||||
leaf->orient = orient;
|
||||
leaf->ratio = 0.5;
|
||||
leaf->signals.clear();
|
||||
leaf->a = std::move(first);
|
||||
leaf->b = std::move(second);
|
||||
}
|
||||
|
||||
PaneNode* PaneTree::findParent(PaneNode* node, const PaneNode* child) {
|
||||
if (node == nullptr || node->leaf) { return nullptr; }
|
||||
if (node->a.get() == child || node->b.get() == child) { return node; }
|
||||
if (PaneNode* p = findParent(node->a.get(), child)) { return p; }
|
||||
return findParent(node->b.get(), child);
|
||||
}
|
||||
|
||||
void PaneTree::closeLeaf(PaneNode* leaf) {
|
||||
if (leaf == nullptr || !leaf->leaf) { return; }
|
||||
|
||||
PaneNode* parent = findParent(root_.get(), leaf);
|
||||
if (parent == nullptr) {
|
||||
return; /* The root is the only leaf; a scope with no pane is useless. */
|
||||
}
|
||||
|
||||
std::unique_ptr<PaneNode> survivor =
|
||||
(parent->a.get() == leaf) ? std::move(parent->b) : std::move(parent->a);
|
||||
|
||||
/* Collapse the parent into the survivor in place, so the parent pointer
|
||||
* held by any caller stays valid. */
|
||||
parent->leaf = survivor->leaf;
|
||||
parent->signals = std::move(survivor->signals);
|
||||
parent->profilePane = survivor->profilePane;
|
||||
parent->orient = survivor->orient;
|
||||
parent->ratio = survivor->ratio;
|
||||
parent->a = std::move(survivor->a);
|
||||
parent->b = std::move(survivor->b);
|
||||
}
|
||||
|
||||
void PaneTree::setRatio(PaneNode* split, double ratio) {
|
||||
if (split != nullptr && !split->leaf) {
|
||||
split->ratio = std::min(std::max(ratio, 0.0), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
size_t PaneTree::countLeaves(const PaneNode* node) {
|
||||
if (node == nullptr) { return 0; }
|
||||
if (node->leaf) { return 1; }
|
||||
return countLeaves(node->a.get()) + countLeaves(node->b.get());
|
||||
}
|
||||
|
||||
size_t PaneTree::leafCount() const { return countLeaves(root_.get()); }
|
||||
|
||||
const PaneTree::Splitter* PaneTree::hitTestSplitter(
|
||||
const std::vector<Splitter>& splitters, double px, double py) const {
|
||||
for (const Splitter& s : splitters) {
|
||||
if (s.rect.contains(px, py)) { return &s; }
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Handle PaneTree::hitTestHandle(const Rect& pane, double px, double py) {
|
||||
if (!pane.contains(px, py)) { return Handle::None; }
|
||||
|
||||
const double relX = px - pane.x;
|
||||
const double relY = py - pane.y;
|
||||
const double midY = pane.h * 0.5;
|
||||
const double midX = pane.w * 0.5;
|
||||
const double half = kHandleSize * 0.5;
|
||||
|
||||
/* Close sits in the top-right corner and wins over the edge handles. */
|
||||
if (relX >= pane.w - kHandleSize && relY <= kHandleSize) {
|
||||
return Handle::Close;
|
||||
}
|
||||
if (relX <= kHandleSize && std::abs(relY - midY) <= half * 3.0) {
|
||||
return Handle::Left;
|
||||
}
|
||||
if (relX >= pane.w - kHandleSize && std::abs(relY - midY) <= half * 3.0) {
|
||||
return Handle::Right;
|
||||
}
|
||||
if (relY <= kHandleSize && std::abs(relX - midX) <= half * 3.0) {
|
||||
return Handle::Top;
|
||||
}
|
||||
if (relY >= pane.h - kHandleSize && std::abs(relX - midX) <= half * 3.0) {
|
||||
return Handle::Bottom;
|
||||
}
|
||||
return Handle::None;
|
||||
}
|
||||
|
||||
} /* namespace udpscope */
|
||||
Reference in New Issue
Block a user