// MplexLayoutNode and MplexLayout: transcription of tai.py:11261-11546. #include "mplex_layout.h" #include #include #include "mplex_pane.h" namespace detz::mplex { namespace { using Node = MplexLayoutNode::Ptr; long index_of(const std::vector& kids, const Node& node) { for (size_t i = 0; i < kids.size(); ++i) if (kids[i] == node) return static_cast(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 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::leaves() { if (kind == "pane") return {shared_from_this()}; std::vector 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& kids = node->children; int count = static_cast(kids.size()); const std::string kind = node->kind; int total = (kind == "lr" ? w : h) - (count - 1); std::vector sizes; if (total < count) { sizes.assign(static_cast(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(static_cast(s) * total) / static_cast(weight); s = std::max(1, static_cast(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(index)] += 1; diff -= 1; moved = true; } else if (sizes[static_cast(index)] > 1) { sizes[static_cast(index)] -= 1; diff += 1; moved = true; } if (diff == 0) break; } if (!moved) break; } } std::vector> order; for (int i = 0; i < count; ++i) order.emplace_back(kids[static_cast(i)], sizes[static_cast(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::split(Node root, const Node& leaf, const std::string& kind, bool before, const std::shared_ptr& 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("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(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(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(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 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(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& kids = parent->children; long index = index_of(kids, node); bool use_after = prefer_after ? (index < static_cast(kids.size()) - 1) : (index == 0); long first = use_after ? index : index - 1; Node a = kids[static_cast(first)], b = kids[static_cast(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 MplexLayout::shares(int total, int count, bool spare_first) { int room = total - (count - 1); int base, extra; divmod(room, count, base, extra); std::vector sizes(static_cast(count), base); if (spare_first) { for (int i = 0; i < extra; ++i) sizes[static_cast(i)] += 1; } else { for (int i = count - extra; i < count; ++i) sizes[static_cast(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(parent->children.size()) - 1; resize_by(leaf, kind, has_next ? change : -change, true); } MplexLayout::Node MplexLayout::preset(const std::string& name, const std::vector>& panes, int width, int height, int main_width, int main_height) { int count = static_cast(panes.size()); std::vector leaves; for (const auto& pane : panes) leaves.push_back(std::make_shared("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& items, int w, int h, bool spare_first = true) { Node node = std::make_shared(kind, nullptr, w, h); for (const auto& item : items) node->adopt(item); std::vector sizes = shares(kind == "lr" ? w : h, static_cast(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 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 lines; for (int start = 0; start < count; start += columns) { std::vector 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 MplexLayout::neighbour(const std::vector>& panes, const std::shared_ptr& 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& 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> 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