452 lines
17 KiB
C++
452 lines
17 KiB
C++
#include "ui/Layout.h"
|
|
#include "ui/Theme.h"
|
|
|
|
#include <imgui.h>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace eve::ui {
|
|
namespace {
|
|
|
|
float clampV(float v, float lo, float hi) {
|
|
if (lo > 0.f) v = std::max(v, lo);
|
|
if (hi > 0.f) v = std::min(v, hi);
|
|
return v;
|
|
}
|
|
|
|
float effectiveGrow(const FlexItemSpec &s) {
|
|
float g = s.flexGrow;
|
|
if (s.isSpacer && g <= 0.f) g = 1.f;
|
|
return std::max(0.f, g);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FlexResult flexArrange(bool row, float gap, float availMain, float availCross,
|
|
FlexAlign containerAlign, FlexJustify justify,
|
|
const std::vector<FlexItemSpec> &items) {
|
|
FlexResult res;
|
|
res.items.resize(items.size());
|
|
if (availMain < 0.f) availMain = 0.f;
|
|
if (availCross < 0.f) availCross = 0.f;
|
|
|
|
const int n = int(items.size());
|
|
std::vector<float> mainSize(size_t(n), 0.f);
|
|
std::vector<float> crossSize(size_t(n), 0.f);
|
|
std::vector<float> mainPos(size_t(n), 0.f);
|
|
std::vector<float> crossPos(size_t(n), 0.f);
|
|
|
|
float flowTotal = 0.f;
|
|
int flowCount = 0;
|
|
float growSum = 0.f;
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
const FlexItemSpec &s = items[size_t(i)];
|
|
float m = s.explicitMain > 0.f
|
|
? s.explicitMain
|
|
: (s.percentMain > 0.f ? s.percentMain * availMain : s.basisMain);
|
|
m = clampV(m, s.minMain, s.maxMain);
|
|
float c = s.explicitCross > 0.f
|
|
? s.explicitCross
|
|
: (s.percentCross > 0.f ? s.percentCross * availCross : s.basisCross);
|
|
c = clampV(c, s.minCross, s.maxCross);
|
|
mainSize[size_t(i)] = m;
|
|
crossSize[size_t(i)] = c;
|
|
|
|
if (s.absolute) continue;
|
|
growSum += effectiveGrow(s);
|
|
flowTotal += m + s.marginBefore + s.marginAfter;
|
|
++flowCount;
|
|
}
|
|
|
|
const int gapCount = std::max(0, flowCount - 1);
|
|
float total = flowTotal + float(gapCount) * gap;
|
|
float freeSpace = availMain - total;
|
|
if (freeSpace < 0.f) freeSpace = 0.f;
|
|
|
|
float leading = 0.f;
|
|
float between = gap;
|
|
if (growSum > 0.f) {
|
|
for (int i = 0; i < n; ++i) {
|
|
const FlexItemSpec &s = items[size_t(i)];
|
|
if (s.absolute) continue;
|
|
const float g = effectiveGrow(s);
|
|
if (g > 0.f) mainSize[size_t(i)] += freeSpace * (g / growSum);
|
|
}
|
|
} else if (freeSpace > 0.f && flowCount > 0) {
|
|
switch (justify) {
|
|
case FlexJustify::Center:
|
|
leading = freeSpace * 0.5f;
|
|
break;
|
|
case FlexJustify::End:
|
|
leading = freeSpace;
|
|
break;
|
|
case FlexJustify::SpaceBetween:
|
|
if (gapCount > 0) between = gap + freeSpace / float(gapCount);
|
|
break;
|
|
case FlexJustify::SpaceAround:
|
|
if (flowCount > 0) {
|
|
const float edge = freeSpace / float(flowCount * 2);
|
|
leading = edge;
|
|
between = gap + edge * 2.f;
|
|
}
|
|
break;
|
|
case FlexJustify::Start:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
float cur = leading;
|
|
for (int i = 0; i < n; ++i) {
|
|
const FlexItemSpec &s = items[size_t(i)];
|
|
if (s.absolute) {
|
|
mainPos[size_t(i)] =
|
|
s.anchorMain * availMain + s.posMain - s.anchorMain * mainSize[size_t(i)];
|
|
continue;
|
|
}
|
|
mainPos[size_t(i)] = cur + s.marginBefore;
|
|
cur += s.marginBefore + mainSize[size_t(i)] + s.marginAfter + between;
|
|
}
|
|
const float contentMain = flowCount > 0 ? cur - between : 0.f;
|
|
|
|
float contentCross = 0.f;
|
|
for (int i = 0; i < n; ++i) {
|
|
const FlexItemSpec &s = items[size_t(i)];
|
|
if (s.absolute) {
|
|
crossPos[size_t(i)] =
|
|
s.anchorCross * availCross + s.posCross - s.anchorCross * crossSize[size_t(i)];
|
|
contentCross = std::max(contentCross,
|
|
crossPos[size_t(i)] + crossSize[size_t(i)] +
|
|
s.marginCrossAfter);
|
|
continue;
|
|
}
|
|
const float cb = s.marginCrossBefore;
|
|
const float ca = s.marginCrossAfter;
|
|
const bool stretch = s.alignSelf >= 0
|
|
? s.alignSelf == int(FlexAlign::Stretch)
|
|
: containerAlign == FlexAlign::Stretch;
|
|
if (stretch && s.explicitCross <= 0.f && s.percentCross <= 0.f) {
|
|
float c = availCross - cb - ca;
|
|
if (c < 0.f) c = 0.f;
|
|
crossSize[size_t(i)] = c;
|
|
}
|
|
switch (containerAlign) {
|
|
case FlexAlign::Center:
|
|
crossPos[size_t(i)] = (availCross - crossSize[size_t(i)] - cb - ca) * 0.5f + cb;
|
|
break;
|
|
case FlexAlign::End:
|
|
crossPos[size_t(i)] = availCross - crossSize[size_t(i)] - ca;
|
|
break;
|
|
case FlexAlign::Start:
|
|
case FlexAlign::Stretch:
|
|
default:
|
|
crossPos[size_t(i)] = cb;
|
|
break;
|
|
}
|
|
contentCross = std::max(contentCross,
|
|
crossPos[size_t(i)] + crossSize[size_t(i)] + ca);
|
|
}
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
FlexRect &r = res.items[size_t(i)];
|
|
if (row) {
|
|
r.x = mainPos[size_t(i)];
|
|
r.y = crossPos[size_t(i)];
|
|
r.w = mainSize[size_t(i)];
|
|
r.h = crossSize[size_t(i)];
|
|
} else {
|
|
r.x = crossPos[size_t(i)];
|
|
r.y = mainPos[size_t(i)];
|
|
r.w = crossSize[size_t(i)];
|
|
r.h = mainSize[size_t(i)];
|
|
}
|
|
}
|
|
|
|
res.contentW = row ? contentMain : contentCross;
|
|
res.contentH = row ? contentCross : contentMain;
|
|
return res;
|
|
}
|
|
|
|
void measureFlowChildren(UIHost::Tree &tree, int firstChild, float *outW, float *outH) {
|
|
float rowW = 0.f;
|
|
float rowH = 0.f;
|
|
float maxW = 0.f;
|
|
float totalH = 0.f;
|
|
bool sameRow = false;
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
int index = firstChild;
|
|
while (index >= 0 && index < int(tree.nodes.size())) {
|
|
UINode &n = tree.nodes[size_t(index)];
|
|
const int next = n.nextSibling;
|
|
if (n.visible) {
|
|
if (n.type == NodeType::SameLine) {
|
|
sameRow = true;
|
|
} else {
|
|
measureNode(tree, index);
|
|
const float w = n.measuredW + n.marginL + n.marginR;
|
|
const float h = n.measuredH + n.marginT + n.marginB;
|
|
if (sameRow) {
|
|
rowW += style.ItemSpacing.x + w;
|
|
} else {
|
|
if (rowH > 0.f) totalH += style.ItemSpacing.y;
|
|
totalH += rowH;
|
|
rowW = w;
|
|
rowH = 0.f;
|
|
}
|
|
rowH = std::max(rowH, h);
|
|
maxW = std::max(maxW, rowW);
|
|
sameRow = false;
|
|
}
|
|
}
|
|
index = next;
|
|
}
|
|
if (rowH > 0.f) totalH += rowH;
|
|
if (outW) *outW = maxW;
|
|
if (outH) *outH = totalH;
|
|
}
|
|
|
|
void measureNode(UIHost::Tree &tree, int index) {
|
|
if (index < 0 || index >= int(tree.nodes.size())) return;
|
|
UINode &n = tree.nodes[size_t(index)];
|
|
n.measuredW = 0.f;
|
|
n.measuredH = 0.f;
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
|
|
switch (n.type) {
|
|
case NodeType::Text: {
|
|
const ImVec2 t = n.wrapWidth > 0.f
|
|
? ImGui::CalcTextSize(n.text.c_str(), nullptr, false, n.wrapWidth)
|
|
: ImGui::CalcTextSize(n.text.c_str());
|
|
n.measuredW = t.x;
|
|
n.measuredH = t.y;
|
|
break;
|
|
}
|
|
case NodeType::Combo:
|
|
case NodeType::SearchField: {
|
|
n.measuredW = globalTheme().layout.searchMinWidth * themeUiScale();
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Button: {
|
|
const char *label = n.text.empty() ? "Button" : n.text.c_str();
|
|
const ImVec2 t = ImGui::CalcTextSize(label);
|
|
n.measuredW = t.x + style.FramePadding.x * 2.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Checkbox: {
|
|
const char *label = n.text.empty() ? "Check" : n.text.c_str();
|
|
const ImVec2 t = ImGui::CalcTextSize(label);
|
|
n.measuredW = ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + t.x;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Switch: {
|
|
const ImVec2 t = ImGui::CalcTextSize(n.text.c_str());
|
|
const float trackW = ImGui::GetFrameHeight() * 1.7f;
|
|
n.measuredW = trackW + (n.text.empty() ? 0.f : style.ItemInnerSpacing.x + t.x);
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Badge: {
|
|
const ImVec2 t = ImGui::CalcTextSize(n.text.c_str());
|
|
n.measuredW = t.x + style.FramePadding.x * 2.f;
|
|
n.measuredH = t.y + 6.f;
|
|
break;
|
|
}
|
|
case NodeType::Slider:
|
|
case NodeType::InputText:
|
|
n.measuredW = 120.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
case NodeType::Progress:
|
|
n.measuredW = 100.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
case NodeType::Image:
|
|
case NodeType::ImageButton:
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : 32.f;
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : 32.f;
|
|
break;
|
|
case NodeType::Separator:
|
|
n.measuredW = style.ItemSpacing.x;
|
|
n.measuredH = 1.f;
|
|
break;
|
|
case NodeType::Spacer:
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : 0.f;
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : 0.f;
|
|
break;
|
|
case NodeType::Child:
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : 80.f;
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : 120.f;
|
|
break;
|
|
case NodeType::ScrollList:
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : 0.f;
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : 120.f;
|
|
break;
|
|
case NodeType::Viewport:
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : 0.f;
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : 240.f;
|
|
break;
|
|
case NodeType::CollapsingHeader: {
|
|
const char *label = n.text.empty() ? "Section" : n.text.c_str();
|
|
const ImVec2 t = ImGui::CalcTextSize(label);
|
|
n.measuredW = t.x + style.FramePadding.x * 2.f + 18.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
if (n.open) {
|
|
float w = 0.f, h = 0.f;
|
|
measureFlowChildren(tree, n.firstChild, &w, &h);
|
|
n.measuredW = std::max(n.measuredW, w);
|
|
n.measuredH += h;
|
|
}
|
|
break;
|
|
}
|
|
case NodeType::SectionHeader: {
|
|
const ImVec2 t = ImGui::CalcTextSize(n.text.c_str());
|
|
n.measuredW = t.x + 10.f;
|
|
n.measuredH = std::max(ImGui::GetFrameHeight(), t.y + style.ItemSpacing.y) +
|
|
globalTheme().layout.sectionSpacingY * themeUiScale();
|
|
break;
|
|
}
|
|
case NodeType::MenuItem: {
|
|
const ImVec2 label = ImGui::CalcTextSize(n.text.c_str());
|
|
const ImVec2 shortcut = ImGui::CalcTextSize(n.valueText.c_str());
|
|
n.measuredW = label.x + shortcut.x + style.ItemSpacing.x * 4.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Menu: {
|
|
const ImVec2 t = ImGui::CalcTextSize(n.text.c_str());
|
|
n.measuredW = t.x + style.FramePadding.x * 2.f;
|
|
n.measuredH = ImGui::GetFrameHeight();
|
|
break;
|
|
}
|
|
case NodeType::Toolbar:
|
|
case NodeType::StatusBar: {
|
|
float w = 0.f, h = 0.f;
|
|
for (int c = n.firstChild; c >= 0; c = tree.nodes[size_t(c)].nextSibling) {
|
|
UINode &child = tree.nodes[size_t(c)];
|
|
if (!child.visible) continue;
|
|
measureNode(tree, c);
|
|
w += child.measuredW;
|
|
h = std::max(h, child.measuredH);
|
|
}
|
|
n.measuredW = w;
|
|
const float defaultHeight =
|
|
(n.type == NodeType::StatusBar ? globalTheme().layout.statusBarHeight
|
|
: globalTheme().layout.toolbarHeight) *
|
|
themeUiScale();
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : defaultHeight;
|
|
break;
|
|
}
|
|
case NodeType::Toolbox: {
|
|
int count = 0;
|
|
for (int c = n.firstChild; c >= 0; c = tree.nodes[size_t(c)].nextSibling) {
|
|
measureNode(tree, c);
|
|
if (tree.nodes[size_t(c)].visible) ++count;
|
|
}
|
|
const float cell = n.itemHeight > 0.f
|
|
? n.itemHeight
|
|
: globalTheme().layout.toolboxCellSize * themeUiScale();
|
|
const int cols = int(n.value) > 0 ? int(n.value) : std::max(1, std::min(4, count));
|
|
const int rows = cols > 0 ? (count + cols - 1) / cols : 0;
|
|
n.measuredW = float(cols) * cell + float(std::max(0, cols - 1)) * style.ItemSpacing.x;
|
|
n.measuredH = float(rows) * cell + float(std::max(0, rows - 1)) * style.ItemSpacing.y;
|
|
break;
|
|
}
|
|
case NodeType::Sidebar: {
|
|
float w = 0.f, h = 0.f;
|
|
measureFlowChildren(tree, n.firstChild, &w, &h);
|
|
const float defaultWidth = globalTheme().layout.sidebarWidth * themeUiScale();
|
|
n.measuredW = n.sizeX > 0.f ? n.sizeX : std::max(defaultWidth, w);
|
|
n.measuredH = n.sizeY > 0.f ? n.sizeY : h;
|
|
break;
|
|
}
|
|
case NodeType::SplitPane: {
|
|
int first = n.firstChild;
|
|
int second = first >= 0 ? tree.nodes[size_t(first)].nextSibling : -1;
|
|
if (first >= 0) measureNode(tree, first);
|
|
if (second >= 0) measureNode(tree, second);
|
|
if (n.flexDirection == FlexDirection::Row) {
|
|
n.measuredW = (first >= 0 ? tree.nodes[size_t(first)].measuredW : 0.f) +
|
|
globalTheme().layout.splitterSize * themeUiScale() +
|
|
(second >= 0 ? tree.nodes[size_t(second)].measuredW : 0.f);
|
|
n.measuredH = std::max(first >= 0 ? tree.nodes[size_t(first)].measuredH : 0.f,
|
|
second >= 0 ? tree.nodes[size_t(second)].measuredH : 0.f);
|
|
} else {
|
|
n.measuredW = std::max(first >= 0 ? tree.nodes[size_t(first)].measuredW : 0.f,
|
|
second >= 0 ? tree.nodes[size_t(second)].measuredW : 0.f);
|
|
n.measuredH = (first >= 0 ? tree.nodes[size_t(first)].measuredH : 0.f) +
|
|
globalTheme().layout.splitterSize * themeUiScale() +
|
|
(second >= 0 ? tree.nodes[size_t(second)].measuredH : 0.f);
|
|
}
|
|
break;
|
|
}
|
|
case NodeType::Group:
|
|
case NodeType::Card:
|
|
case NodeType::NinePatchPanel:
|
|
case NodeType::MenuBar: {
|
|
float w = 0.f, h = 0.f;
|
|
measureFlowChildren(tree, n.firstChild, &w, &h);
|
|
n.measuredW = w + n.paddingL + n.paddingR;
|
|
n.measuredH = h + n.paddingT + n.paddingB;
|
|
break;
|
|
}
|
|
case NodeType::Flex: {
|
|
const bool row = n.flexDirection == FlexDirection::Row;
|
|
float mainSum = 0.f;
|
|
float crossMax = 0.f;
|
|
int count = 0;
|
|
for (int c = n.firstChild; c >= 0; c = tree.nodes[size_t(c)].nextSibling) {
|
|
UINode &child = tree.nodes[size_t(c)];
|
|
if (!child.visible) continue;
|
|
measureNode(tree, c);
|
|
if (child.absolute) continue; // absolutely placed items don't size the flex
|
|
const float m = row ? child.measuredW : child.measuredH;
|
|
const float cm = row ? child.measuredH : child.measuredW;
|
|
const float mb = row ? child.marginL : child.marginT;
|
|
const float ma = row ? child.marginR : child.marginB;
|
|
const float cb = row ? child.marginT : child.marginL;
|
|
const float ca = row ? child.marginB : child.marginR;
|
|
mainSum += m + mb + ma;
|
|
crossMax = std::max(crossMax, cm + cb + ca);
|
|
++count;
|
|
}
|
|
const float gap = n.gap >= 0.f ? n.gap : (row ? style.ItemSpacing.x : style.ItemSpacing.y);
|
|
const float padMain = row ? n.paddingL + n.paddingR : n.paddingT + n.paddingB;
|
|
const float padCross = row ? n.paddingT + n.paddingB : n.paddingL + n.paddingR;
|
|
const float mainSize = mainSum + float(std::max(0, count - 1)) * gap + padMain;
|
|
const float crossSize = crossMax + padCross;
|
|
n.measuredW = row ? mainSize : crossSize;
|
|
n.measuredH = row ? crossSize : mainSize;
|
|
break;
|
|
}
|
|
case NodeType::Window: {
|
|
float w = 0.f, h = 0.f;
|
|
measureFlowChildren(tree, n.firstChild, &w, &h);
|
|
n.measuredW = w + n.paddingL + n.paddingR;
|
|
n.measuredH = h + n.paddingT + n.paddingB;
|
|
break;
|
|
}
|
|
case NodeType::SameLine:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (n.type != NodeType::Child && n.type != NodeType::Window) {
|
|
if (n.sizeX > 0.f) n.measuredW = n.sizeX;
|
|
if (n.sizeY > 0.f) n.measuredH = n.sizeY;
|
|
}
|
|
if (n.minSizeX > 0.f) n.measuredW = std::max(n.measuredW, n.minSizeX);
|
|
if (n.minSizeY > 0.f) n.measuredH = std::max(n.measuredH, n.minSizeY);
|
|
if (n.maxSizeX > 0.f) n.measuredW = std::min(n.measuredW, n.maxSizeX);
|
|
if (n.maxSizeY > 0.f) n.measuredH = std::min(n.measuredH, n.maxSizeY);
|
|
}
|
|
|
|
void measureTree(UIHost::Tree &tree) {
|
|
if (tree.root >= 0) measureNode(tree, tree.root);
|
|
}
|
|
|
|
} // namespace eve::ui
|