341 lines
13 KiB
C++
341 lines
13 KiB
C++
// MplexLayoutNode and MplexLayout: transcription of tai.py:11261-11546.
|
|
#include "mplex_layout.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "mplex_pane.h"
|
|
|
|
namespace detz::mplex {
|
|
|
|
namespace {
|
|
|
|
using Node = MplexLayoutNode::Ptr;
|
|
|
|
long index_of(const std::vector<Node>& kids, const Node& node) {
|
|
for (size_t i = 0; i < kids.size(); ++i)
|
|
if (kids[i] == node) return static_cast<long>(i);
|
|
return -1;
|
|
}
|
|
|
|
// Python floor division and modulo (divmod) for ints.
|
|
void divmod(int a, int b, int& q, int& r) {
|
|
q = a / b;
|
|
r = a % b;
|
|
if (r != 0 && ((r < 0) != (b < 0))) {
|
|
--q;
|
|
r += b;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MplexLayoutNode::MplexLayoutNode(const std::string& kind_in, std::shared_ptr<MplexPane> pane_in, int w_in, int h_in)
|
|
: kind(kind_in), pane(std::move(pane_in)), w(w_in), h(h_in) {}
|
|
|
|
std::vector<MplexLayoutNode::Ptr> MplexLayoutNode::leaves() {
|
|
if (kind == "pane") return {shared_from_this()};
|
|
std::vector<Ptr> found;
|
|
for (auto& child : children) {
|
|
auto sub = child->leaves();
|
|
found.insert(found.end(), sub.begin(), sub.end());
|
|
}
|
|
return found;
|
|
}
|
|
|
|
MplexLayoutNode::Ptr MplexLayoutNode::adopt(const Ptr& child) {
|
|
child->parent = shared_from_this();
|
|
children.push_back(child);
|
|
return child;
|
|
}
|
|
|
|
int MplexLayout::size_of(const Node& node, const std::string& kind) { return kind == "lr" ? node->w : node->h; }
|
|
|
|
void MplexLayout::set_size(const Node& node, const std::string& kind, int value) {
|
|
if (kind == "lr") node->w = value;
|
|
else node->h = value;
|
|
}
|
|
|
|
void MplexLayout::assign(const Node& node, int x, int y, int w, int h) {
|
|
node->x = x;
|
|
node->y = y;
|
|
node->w = w;
|
|
node->h = h;
|
|
if (node->kind == "pane") return;
|
|
std::vector<Node>& kids = node->children;
|
|
int count = static_cast<int>(kids.size());
|
|
const std::string kind = node->kind;
|
|
int total = (kind == "lr" ? w : h) - (count - 1);
|
|
std::vector<int> sizes;
|
|
if (total < count) {
|
|
sizes.assign(static_cast<size_t>(count), 1);
|
|
} else {
|
|
for (auto& kid : kids) sizes.push_back(std::max(1, size_of(kid, kind)));
|
|
int weight = 0;
|
|
for (int s : sizes) weight += s;
|
|
if (weight == 0) weight = count;
|
|
if (weight != total) {
|
|
for (auto& s : sizes) {
|
|
// int(round(size * total / weight)): true division, round half to even
|
|
double scaled = static_cast<double>(static_cast<long long>(s) * total) / static_cast<double>(weight);
|
|
s = std::max(1, static_cast<int>(std::nearbyint(scaled)));
|
|
}
|
|
}
|
|
int sum = 0;
|
|
for (int s : sizes) sum += s;
|
|
int diff = total - sum;
|
|
while (diff != 0) {
|
|
bool moved = false;
|
|
for (int step = 0; step < count; ++step) {
|
|
int index = diff > 0 ? step : count - 1 - step;
|
|
if (diff > 0) {
|
|
sizes[static_cast<size_t>(index)] += 1;
|
|
diff -= 1;
|
|
moved = true;
|
|
} else if (sizes[static_cast<size_t>(index)] > 1) {
|
|
sizes[static_cast<size_t>(index)] -= 1;
|
|
diff += 1;
|
|
moved = true;
|
|
}
|
|
if (diff == 0) break;
|
|
}
|
|
if (!moved) break;
|
|
}
|
|
}
|
|
std::vector<std::pair<Node, int>> order;
|
|
for (int i = 0; i < count; ++i) order.emplace_back(kids[static_cast<size_t>(i)], sizes[static_cast<size_t>(i)]);
|
|
if (node->reverse) std::reverse(order.begin(), order.end());
|
|
int pos = kind == "lr" ? x : y;
|
|
for (auto& [kid, size] : order) {
|
|
if (kind == "lr") assign(kid, pos, y, size, h);
|
|
else assign(kid, x, pos, w, size);
|
|
pos += size + 1;
|
|
}
|
|
}
|
|
|
|
std::pair<MplexLayout::Node, MplexLayout::Node> MplexLayout::split(Node root, const Node& leaf, const std::string& kind,
|
|
bool before,
|
|
const std::shared_ptr<MplexPane>& new_pane,
|
|
int size) {
|
|
int total = size_of(leaf, kind);
|
|
if (total < 3) throw MplexError("no space for new pane");
|
|
int avail = total - 1;
|
|
int new_size;
|
|
if (size == -1) {
|
|
new_size = before ? avail - avail / 2 : avail / 2;
|
|
} else {
|
|
new_size = std::max(1, std::min(avail - 1, size));
|
|
}
|
|
int old_size = avail - new_size;
|
|
Node fresh = std::make_shared<MplexLayoutNode>("pane", new_pane, leaf->w, leaf->h);
|
|
Node parent = leaf->parent_node();
|
|
if (parent && parent->kind == kind) {
|
|
long index = index_of(parent->children, leaf);
|
|
parent->children.insert(parent->children.begin() + (before ? index : index + 1), fresh);
|
|
fresh->parent = parent;
|
|
} else {
|
|
Node box = std::make_shared<MplexLayoutNode>(kind, nullptr, leaf->w, leaf->h);
|
|
box->x = leaf->x;
|
|
box->y = leaf->y;
|
|
box->parent = parent;
|
|
if (!parent) {
|
|
root = box;
|
|
} else {
|
|
parent->children[static_cast<size_t>(index_of(parent->children, leaf))] = box;
|
|
}
|
|
if (before) {
|
|
box->adopt(fresh);
|
|
box->adopt(leaf);
|
|
} else {
|
|
box->adopt(leaf);
|
|
box->adopt(fresh);
|
|
}
|
|
}
|
|
set_size(leaf, kind, old_size);
|
|
set_size(fresh, kind, new_size);
|
|
return {root, fresh};
|
|
}
|
|
|
|
MplexLayout::Node MplexLayout::remove(Node root, const Node& leaf) {
|
|
Node parent = leaf->parent_node();
|
|
if (!parent) return nullptr;
|
|
long index = index_of(parent->children, leaf);
|
|
parent->children.erase(parent->children.begin() + index);
|
|
Node neighbour = index > 0 ? parent->children[static_cast<size_t>(index - 1)] : parent->children[0];
|
|
const std::string kind = parent->kind;
|
|
set_size(neighbour, kind, size_of(neighbour, kind) + size_of(leaf, kind) + 1);
|
|
if (parent->children.size() == 1) {
|
|
Node only = parent->children[0];
|
|
Node grand = parent->parent_node();
|
|
only->w = parent->w;
|
|
only->h = parent->h;
|
|
only->parent = grand;
|
|
if (!grand) return only;
|
|
long slot = index_of(grand->children, parent);
|
|
if (only->kind == grand->kind) {
|
|
std::vector<Node> moved = only->children;
|
|
grand->children.erase(grand->children.begin() + slot);
|
|
grand->children.insert(grand->children.begin() + slot, moved.begin(), moved.end());
|
|
for (auto& child : moved) child->parent = grand;
|
|
} else {
|
|
grand->children[static_cast<size_t>(slot)] = only;
|
|
}
|
|
}
|
|
return root;
|
|
}
|
|
|
|
void MplexLayout::resize_by(const Node& leaf, const std::string& kind, int delta, bool prefer_after) {
|
|
Node node = leaf;
|
|
while (node->parent_node() && node->parent_node()->kind != kind) node = node->parent_node();
|
|
Node parent = node->parent_node();
|
|
if (!parent) throw MplexError("no border to resize");
|
|
std::vector<Node>& kids = parent->children;
|
|
long index = index_of(kids, node);
|
|
bool use_after = prefer_after ? (index < static_cast<long>(kids.size()) - 1) : (index == 0);
|
|
long first = use_after ? index : index - 1;
|
|
Node a = kids[static_cast<size_t>(first)], b = kids[static_cast<size_t>(first + 1)];
|
|
int size_a = size_of(a, kind), size_b = size_of(b, kind);
|
|
int new_a = std::max(1, std::min(size_a + size_b - 1, size_a + delta));
|
|
set_size(a, kind, new_a);
|
|
set_size(b, kind, size_a + size_b - new_a);
|
|
}
|
|
|
|
std::vector<int> MplexLayout::shares(int total, int count, bool spare_first) {
|
|
int room = total - (count - 1);
|
|
int base, extra;
|
|
divmod(room, count, base, extra);
|
|
std::vector<int> sizes(static_cast<size_t>(count), base);
|
|
if (spare_first) {
|
|
for (int i = 0; i < extra; ++i) sizes[static_cast<size_t>(i)] += 1;
|
|
} else {
|
|
for (int i = count - extra; i < count; ++i) sizes[static_cast<size_t>(i)] += 1;
|
|
}
|
|
return sizes;
|
|
}
|
|
|
|
void MplexLayout::resize_to(const Node& leaf, const std::string& kind, int size) {
|
|
int change = size - size_of(leaf, kind);
|
|
Node node = leaf;
|
|
while (node->parent_node() && node->parent_node()->kind != kind) node = node->parent_node();
|
|
Node parent = node->parent_node();
|
|
if (!parent) throw MplexError("no border to resize");
|
|
bool has_next = index_of(parent->children, node) < static_cast<long>(parent->children.size()) - 1;
|
|
resize_by(leaf, kind, has_next ? change : -change, true);
|
|
}
|
|
|
|
MplexLayout::Node MplexLayout::preset(const std::string& name, const std::vector<std::shared_ptr<MplexPane>>& panes,
|
|
int width, int height, int main_width, int main_height) {
|
|
int count = static_cast<int>(panes.size());
|
|
std::vector<Node> leaves;
|
|
for (const auto& pane : panes) leaves.push_back(std::make_shared<MplexLayoutNode>("pane", pane, 1, 1));
|
|
const std::string suffix = "-mirrored";
|
|
bool mirrored = name.size() >= suffix.size() && name.compare(name.size() - suffix.size(), suffix.size(), suffix) == 0;
|
|
std::string base = mirrored ? name.substr(0, name.size() - suffix.size()) : name;
|
|
if (count == 1) {
|
|
leaves[0]->w = width;
|
|
leaves[0]->h = height;
|
|
return leaves[0];
|
|
}
|
|
|
|
auto box = [&](const std::string& kind, const std::vector<Node>& items, int w, int h, bool spare_first = true) {
|
|
Node node = std::make_shared<MplexLayoutNode>(kind, nullptr, w, h);
|
|
for (const auto& item : items) node->adopt(item);
|
|
std::vector<int> sizes = shares(kind == "lr" ? w : h, static_cast<int>(items.size()), spare_first);
|
|
for (size_t i = 0; i < items.size(); ++i) {
|
|
if (kind == "lr") {
|
|
items[i]->w = sizes[i];
|
|
items[i]->h = h;
|
|
} else {
|
|
items[i]->w = w;
|
|
items[i]->h = sizes[i];
|
|
}
|
|
}
|
|
return node;
|
|
};
|
|
|
|
if (base == "even-horizontal") return box("lr", leaves, width, height);
|
|
if (base == "even-vertical") return box("tb", leaves, width, height);
|
|
if (base == "main-horizontal" || base == "main-vertical") {
|
|
bool horizontal = base == "main-horizontal";
|
|
int span = horizontal ? height : width;
|
|
int main_size = std::max(1, std::min(span - 2, horizontal ? main_height : main_width));
|
|
Node main = leaves[0];
|
|
std::vector<Node> rest(leaves.begin() + 1, leaves.end());
|
|
int other_size = span - main_size - 1;
|
|
Node others, root;
|
|
if (horizontal) {
|
|
others = rest.size() == 1 ? rest[0] : box("lr", rest, width, other_size);
|
|
main->w = width;
|
|
main->h = main_size;
|
|
others->w = width;
|
|
others->h = other_size;
|
|
root = box("tb", {main, others}, width, height);
|
|
} else {
|
|
others = rest.size() == 1 ? rest[0] : box("tb", rest, other_size, height);
|
|
main->w = main_size;
|
|
main->h = height;
|
|
others->w = other_size;
|
|
others->h = height;
|
|
root = box("lr", {main, others}, width, height);
|
|
}
|
|
if (horizontal) {
|
|
main->h = main_size;
|
|
others->h = other_size;
|
|
} else {
|
|
main->w = main_size;
|
|
others->w = other_size;
|
|
}
|
|
root->reverse = mirrored;
|
|
return root;
|
|
}
|
|
int rows = 1, columns = 1;
|
|
while (rows * columns < count) {
|
|
rows += 1;
|
|
if (rows * columns < count) columns += 1;
|
|
}
|
|
std::vector<Node> lines;
|
|
for (int start = 0; start < count; start += columns) {
|
|
std::vector<Node> chunk(leaves.begin() + start, leaves.begin() + std::min(count, start + columns));
|
|
lines.push_back(chunk.size() == 1 ? chunk[0] : box("lr", chunk, width, 1, false));
|
|
}
|
|
if (lines.size() == 1) return lines[0];
|
|
Node root = box("tb", lines, width, height, false);
|
|
for (auto& line : lines) line->w = width;
|
|
return root;
|
|
}
|
|
|
|
std::shared_ptr<MplexPane> MplexLayout::neighbour(const std::vector<std::shared_ptr<MplexPane>>& panes,
|
|
const std::shared_ptr<MplexPane>& current, char direction, int width,
|
|
int height) {
|
|
int left = current->x, top = current->y;
|
|
int right = current->x + current->w, bottom = current->y + current->h;
|
|
auto overlaps = [&](const std::shared_ptr<MplexPane>& other) {
|
|
if (direction == 'L' || direction == 'R') return other->y < bottom && other->y + other->h > top;
|
|
return other->x < right && other->x + other->w > left;
|
|
};
|
|
std::vector<std::shared_ptr<MplexPane>> wanted;
|
|
for (const auto& other : panes) {
|
|
if (other == current || !overlaps(other)) continue;
|
|
if (direction == 'L' && other->x + other->w + 1 == left) wanted.push_back(other);
|
|
else if (direction == 'R' && other->x == right + 1) wanted.push_back(other);
|
|
else if (direction == 'U' && other->y + other->h + 1 == top) wanted.push_back(other);
|
|
else if (direction == 'D' && other->y == bottom + 1) wanted.push_back(other);
|
|
}
|
|
if (wanted.empty()) {
|
|
for (const auto& other : panes) {
|
|
if (other == current || !overlaps(other)) continue;
|
|
if (direction == 'L' && other->x + other->w >= width) wanted.push_back(other);
|
|
else if (direction == 'R' && other->x == 0) wanted.push_back(other);
|
|
else if (direction == 'U' && other->y + other->h >= height) wanted.push_back(other);
|
|
else if (direction == 'D' && other->y == 0) wanted.push_back(other);
|
|
}
|
|
}
|
|
if (wanted.empty()) return nullptr;
|
|
auto best = wanted.begin();
|
|
for (auto it = wanted.begin(); it != wanted.end(); ++it)
|
|
if ((*it)->last_active > (*best)->last_active) best = it;
|
|
return *best;
|
|
}
|
|
|
|
} // namespace detz::mplex
|