early tui layout

This commit is contained in:
Boof2015
2026-08-14 17:15:47 -04:00
parent 7047df3e38
commit e712421bad
10 changed files with 624 additions and 76 deletions
+241
View File
@@ -0,0 +1,241 @@
#include "dashboard_layout.h"
#include <algorithm>
#include <numeric>
#include <utility>
namespace Prism::Tui {
namespace {
struct MinimumSize {
int width = 1;
int height = 1;
};
MinimumSize panelMinimumSize(PanelId panel) {
switch (panel) {
case PanelId::Spectrum:
return {30, 5};
case PanelId::Levels:
return {30, 5};
}
return {1, 1};
}
MinimumSize nodeMinimumSize(const LayoutNode& node) {
if (node.isLeaf()) {
return panelMinimumSize(*node.panel);
}
MinimumSize result;
result.width = node.axis == SplitAxis::Columns ? 0 : 1;
result.height = node.axis == SplitAxis::Rows ? 0 : 1;
for (const auto& child : node.children) {
const auto childMinimum = nodeMinimumSize(child);
if (node.axis == SplitAxis::Columns) {
result.width += childMinimum.width;
result.height = std::max(result.height, childMinimum.height);
} else {
result.width = std::max(result.width, childMinimum.width);
result.height += childMinimum.height;
}
}
return result;
}
std::vector<int> partitionExtent(int total,
const std::vector<int>& weights,
const std::vector<int>& minimums) {
if (weights.empty()) {
return {};
}
std::vector<int> result(weights.size(), 0);
const int minimumTotal = std::accumulate(minimums.begin(), minimums.end(), 0);
if (minimumTotal <= total) {
int remaining = total;
int remainingWeight = std::accumulate(weights.begin(), weights.end(), 0);
for (size_t index = 0; index < result.size(); ++index) {
const int weight = std::max(1, weights[index]);
result[index] = index + 1 == result.size()
? remaining
: remaining * weight / std::max(1, remainingWeight);
remaining -= result[index];
remainingWeight -= weight;
}
// Preserve the requested ratio whenever possible, then borrow from
// larger panes to honor each panel's usable minimum size.
for (size_t index = 0; index < result.size(); ++index) {
int needed = std::max(0, minimums[index] - result[index]);
for (size_t donor = 0; donor < result.size() && needed > 0; ++donor) {
if (donor == index) continue;
const int available = std::max(0, result[donor] - minimums[donor]);
const int transfer = std::min(needed, available);
result[donor] -= transfer;
result[index] += transfer;
needed -= transfer;
}
}
return result;
}
int remaining = std::max(0, total);
int remainingWeight = std::accumulate(weights.begin(), weights.end(), 0);
for (size_t index = 0; index < result.size(); ++index) {
const int weight = std::max(1, weights[index]);
result[index] = index + 1 == result.size()
? remaining
: remaining * weight / std::max(1, remainingWeight);
remaining -= result[index];
remainingWeight -= weight;
}
return result;
}
void resolveNode(const LayoutNode& node,
int x,
int y,
int width,
int height,
std::vector<PanelRect>& output) {
if (node.isLeaf()) {
output.push_back({*node.panel, x, y, width, height});
return;
}
if (node.children.empty()) {
return;
}
std::vector<int> weights;
std::vector<int> minimums;
weights.reserve(node.children.size());
minimums.reserve(node.children.size());
for (const auto& child : node.children) {
weights.push_back(std::max(1, child.weight));
const auto minimum = nodeMinimumSize(child);
minimums.push_back(node.axis == SplitAxis::Columns
? minimum.width
: minimum.height);
}
const int extent = node.axis == SplitAxis::Columns ? width : height;
const auto spans = partitionExtent(extent, weights, minimums);
int offset = 0;
for (size_t index = 0; index < node.children.size(); ++index) {
if (node.axis == SplitAxis::Columns) {
resolveNode(node.children[index], x + offset, y, spans[index], height, output);
} else {
resolveNode(node.children[index], x, y + offset, width, spans[index], output);
}
offset += spans[index];
}
}
LayoutPreset resolvePreset(LayoutPreset requested, int width, int height) {
constexpr int minimumColumnsWidth = 72;
if (requested == LayoutPreset::Columns && width < minimumColumnsWidth) {
return LayoutPreset::Stacked;
}
if (requested != LayoutPreset::Automatic) {
return requested;
}
return width >= 96 && height >= 28
? LayoutPreset::Columns
: LayoutPreset::Stacked;
}
LayoutNode makeRoot(LayoutPreset preset) {
if (preset == LayoutPreset::Columns) {
return LayoutNode::split(SplitAxis::Columns, {
LayoutNode::leaf(PanelId::Spectrum, 4),
LayoutNode::leaf(PanelId::Levels, 1),
});
}
return LayoutNode::split(SplitAxis::Rows, {
LayoutNode::leaf(PanelId::Spectrum, 4),
LayoutNode::leaf(PanelId::Levels, 1),
});
}
} // namespace
LayoutNode LayoutNode::leaf(PanelId panel, int weight) {
LayoutNode node;
node.panel = panel;
node.weight = weight;
return node;
}
LayoutNode LayoutNode::split(SplitAxis axis,
std::vector<LayoutNode> children,
int weight) {
LayoutNode node;
node.axis = axis;
node.weight = weight;
node.children = std::move(children);
return node;
}
DashboardLayout buildDashboardLayout(int width,
int height,
LayoutPreset requestedPreset,
std::optional<PanelId> expandedPanel) {
DashboardLayout layout;
layout.requestedPreset = requestedPreset;
layout.terminalTooSmall = width < kMinimumTerminalWidth || height < kMinimumTerminalHeight;
if (layout.terminalTooSmall) {
return layout;
}
layout.resolvedPreset = resolvePreset(requestedPreset, width, height);
layout.root = expandedPanel
? LayoutNode::leaf(*expandedPanel)
: makeRoot(layout.resolvedPreset);
// The header and footer each consume one terminal row.
resolveNode(layout.root, 0, 0, width, height - 2, layout.panels);
return layout;
}
LayoutPreset nextLayoutPreset(LayoutPreset preset) {
switch (preset) {
case LayoutPreset::Automatic:
return LayoutPreset::Stacked;
case LayoutPreset::Stacked:
return LayoutPreset::Columns;
case LayoutPreset::Columns:
return LayoutPreset::Automatic;
}
return LayoutPreset::Automatic;
}
std::string layoutPresetName(LayoutPreset preset) {
switch (preset) {
case LayoutPreset::Automatic:
return "auto";
case LayoutPreset::Stacked:
return "stacked";
case LayoutPreset::Columns:
return "columns";
}
return "auto";
}
std::vector<PanelId> panelOrder() {
return {PanelId::Spectrum, PanelId::Levels};
}
PanelId nextPanel(PanelId panel, bool reverse) {
const auto panels = panelOrder();
const auto found = std::find(panels.begin(), panels.end(), panel);
const size_t index = found == panels.end()
? 0
: static_cast<size_t>(std::distance(panels.begin(), found));
if (reverse) {
return panels[(index + panels.size() - 1) % panels.size()];
}
return panels[(index + 1) % panels.size()];
}
} // namespace Prism::Tui