aboutsummaryrefslogtreecommitdiff
path: root/app/source/Cplt/UI/UI_Templates.cpp
diff options
context:
space:
mode:
authorrtk0c <[email protected]>2022-06-30 21:38:53 -0700
committerrtk0c <[email protected]>2022-06-30 21:38:53 -0700
commit7fe47a9d5b1727a61dc724523b530762f6d6ba19 (patch)
treee95be6e66db504ed06d00b72c579565bab873277 /app/source/Cplt/UI/UI_Templates.cpp
parent2cf952088d375ac8b2f45b144462af0953436cff (diff)
Restructure project
Diffstat (limited to 'app/source/Cplt/UI/UI_Templates.cpp')
-rw-r--r--app/source/Cplt/UI/UI_Templates.cpp977
1 files changed, 977 insertions, 0 deletions
diff --git a/app/source/Cplt/UI/UI_Templates.cpp b/app/source/Cplt/UI/UI_Templates.cpp
new file mode 100644
index 0000000..e01a97d
--- /dev/null
+++ b/app/source/Cplt/UI/UI_Templates.cpp
@@ -0,0 +1,977 @@
+#include "UI.hpp"
+
+#include <Cplt/Model/GlobalStates.hpp>
+#include <Cplt/Model/Project.hpp>
+#include <Cplt/Model/Template/TableTemplate.hpp>
+#include <Cplt/Model/Template/TableTemplateIterator.hpp>
+#include <Cplt/Model/Template/Template.hpp>
+#include <Cplt/Utils/I18n.hpp>
+
+#include <IconsFontAwesome.h>
+#include <imgui.h>
+#include <imgui_extra_math.h>
+#include <imgui_internal.h>
+#include <imgui_stdlib.h>
+#include <charconv>
+#include <fstream>
+#include <iostream>
+#include <utility>
+#include <variant>
+
+namespace CPLT_UNITY_ID {
+class TemplateUI
+{
+public:
+ static std::unique_ptr<TemplateUI> CreateByKind(std::unique_ptr<Template> tmpl);
+ static std::unique_ptr<TemplateUI> CreateByKind(Template::Kind kind);
+
+ virtual ~TemplateUI() = default;
+ virtual void Display() = 0;
+ virtual void Close() = 0;
+};
+
+// Table template styles
+constexpr ImU32 kSingleParamOutline = IM_COL32(255, 255, 0, 255);
+constexpr ImU32 kArrayGroupOutline = IM_COL32(255, 0, 0, 255);
+
+class TableTemplateUI : public TemplateUI
+{
+private:
+ std::unique_ptr<TableTemplate> mTable;
+
+ struct UICell
+ {
+ bool Hovered = false;
+ bool Held = false;
+ bool Selected = false;
+ };
+ std::vector<UICell> mUICells;
+
+ struct UIArrayGroup
+ {
+ ImVec2 Pos;
+ ImVec2 Size;
+ };
+ std::vector<UIArrayGroup> mUIArrayGroups;
+
+ struct Sizer
+ {
+ bool Hovered = false;
+ bool Held = false;
+ };
+ std::vector<Sizer> mRowSizers;
+ std::vector<Sizer> mColSizers;
+
+ /* Selection range */
+ Vec2i mSelectionTL;
+ Vec2i mSelectionBR;
+
+ /* Selection states */
+
+ /// "CStates" stands for "Constant cell selection States"
+ struct CStates
+ {
+ };
+
+ /// "SStates" stands for "Singular parameter selection States".
+ struct SStates
+ {
+ std::string EditBuffer;
+ bool ErrorDuplicateVarName;
+ bool HasLeftAG;
+ bool HasRightAG;
+ };
+
+ /// "AStates" stands for "Array group parameter selection States".
+ struct AStates
+ {
+ std::string EditBuffer;
+ bool ErrorDuplicateVarName;
+ };
+
+ // "RStates" stands for "Range selection States".
+ struct RStates
+ {
+ };
+
+ union
+ {
+ // Initialize to this element
+ std::monostate mIdleState{};
+ CStates mCS;
+ SStates mSS;
+ AStates mAS;
+ RStates mRS;
+ };
+
+ /* Table resizer dialog states */
+ int mNewTableWidth;
+ int mNewTableHeight;
+
+ /* Table states */
+ enum EditMode
+ {
+ ModeEditing,
+ ModeColumnResizing,
+ ModeRowResizing,
+ };
+ EditMode mMode = ModeEditing;
+
+ float mStartDragDim;
+ /// Depending on row/column sizer being dragged, this will be the y/x coordinate
+ float mStartDragMouseCoordinate;
+
+ bool mDirty = false;
+ bool mFirstDraw = true;
+
+public:
+ TableTemplateUI(std::unique_ptr<TableTemplate> table)
+ : mTable{ std::move(table) }
+ , mSelectionTL{ -1, -1 }
+ , mSelectionBR{ -1, -1 }
+ {
+ // TODO debug code
+ Resize(6, 5);
+ }
+
+ ~TableTemplateUI() override
+ {
+ // We can't move this to be a destructor of the union
+ // because that way it would run after the destruction of mTable
+ if (!IsSelected()) {
+ // Case: mIdleState
+ // Noop
+ } else if (mSelectionTL == mSelectionBR) {
+ switch (mTable->GetCell(mSelectionTL).Type) {
+ case TableCell::ConstantCell:
+ // Case: mCS
+ // Noop
+ break;
+
+ case TableCell::SingularParametricCell:
+ // Case: mSS
+ mSS.EditBuffer.std::string::~string();
+ break;
+
+ case TableCell::ArrayParametricCell:
+ // Case: mAS
+ mAS.EditBuffer.std::string::~string();
+ break;
+ }
+ } else {
+ // Case: mRS
+ // Noop
+ }
+ }
+
+ void Display() override
+ {
+ ImGui::Columns(2);
+ if (mFirstDraw) {
+ mFirstDraw = false;
+ ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.15f);
+ }
+
+ DisplayInspector();
+ ImGui::NextColumn();
+
+ auto initialPos = ImGui::GetCursorPos();
+ DisplayTable();
+ DisplayTableResizers(initialPos);
+ ImGui::NextColumn();
+
+ ImGui::Columns(1);
+ }
+
+ void Close() override
+ {
+ // TODO
+ }
+
+ void Resize(int width, int height)
+ {
+ mTable->Resize(width, height);
+ mUICells.resize(width * height);
+ mUIArrayGroups.resize(mTable->GetArrayGroupCount());
+ mRowSizers.resize(width);
+ mColSizers.resize(height);
+
+ for (size_t i = 0; i < mUIArrayGroups.size(); ++i) {
+ auto& ag = mTable->GetArrayGroup(i);
+ auto& uag = mUIArrayGroups[i];
+
+ auto itemSpacing = ImGui::GetStyle().ItemSpacing;
+ uag.Pos.x = CalcTablePixelWidth() + itemSpacing.x;
+ uag.Pos.y = CalcTablePixelHeight() + itemSpacing.y;
+
+ uag.Size.x = mTable->GetRowHeight(ag.Row);
+ uag.Size.y = 0;
+ for (int x = ag.LeftCell; x <= ag.RightCell; ++x) {
+ uag.Size.y += mTable->GetColumnWidth(x);
+ }
+ }
+
+ mSelectionTL = { 0, 0 };
+ mSelectionBR = { 0, 0 };
+ }
+
+private:
+ void DisplayInspector()
+ {
+ bool openedDummy = true;
+
+ // This is an id, no need to localize
+ if (ImGui::BeginTabBar("Inspector")) {
+ if (ImGui::BeginTabItem(I18N_TEXT("Cell", L10N_TABLE_CELL))) {
+ if (!IsSelected()) {
+ ImGui::Text(I18N_TEXT("Select a cell to edit", L10N_TABLE_CELL_SELECT_MSG));
+ } else if (mSelectionTL == mSelectionBR) {
+ DisplayCellProperties(mSelectionTL);
+ } else {
+ DisplayRangeProperties(mSelectionTL, mSelectionBR);
+ }
+ ImGui::EndTabItem();
+ }
+
+ auto OpenPopup = [](const char* name) {
+ // Act as if ImGui::OpenPopup is executed in the previous id stack frame (tab bar level)
+ // Note: we can't simply use ImGui::GetItemID() here, because that would return the id of the ImGui::Button
+ auto tabBar = ImGui::GetCurrentContext()->CurrentTabBar;
+ auto id = tabBar->Tabs[tabBar->LastTabItemIdx].ID;
+ ImGui::PopID();
+ ImGui::OpenPopup(name);
+ ImGui::PushOverrideID(id);
+ };
+ if (ImGui::BeginTabItem(I18N_TEXT("Table", L10N_TABLE))) {
+ if (ImGui::Button(I18N_TEXT("Configure table properties...", L10N_TABLE_CONFIGURE_PROPERTIES))) {
+ mNewTableWidth = mTable->GetTableWidth();
+ mNewTableHeight = mTable->GetTableHeight();
+ OpenPopup(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES));
+ }
+
+ int mode = mMode;
+ ImGui::RadioButton(I18N_TEXT("Edit table", L10N_TABLE_EDIT_TABLE), &mode, ModeEditing);
+ ImGui::RadioButton(I18N_TEXT("Resize column widths", L10N_TABLE_EDIT_RESIZE_COLS), &mode, ModeColumnResizing);
+ ImGui::RadioButton(I18N_TEXT("Resize rows heights", L10N_TABLE_EDIT_RESIZE_ROWS), &mode, ModeRowResizing);
+ mMode = static_cast<EditMode>(mode);
+
+ // Table contents
+ DisplayTableContents();
+
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Table properties", L10N_TABLE_PROPERTIES), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ DisplayTableProperties();
+ ImGui::EndPopup();
+ }
+
+ ImGui::EndTabBar();
+ }
+ }
+
+ static char NthUppercaseLetter(int n)
+ {
+ return (char)((int)'A' + n);
+ }
+
+ static void ExcelRow(int row, char* bufferBegin, char* bufferEnd)
+ {
+ auto res = std::to_chars(bufferBegin, bufferEnd, row);
+ if (res.ec != std::errc()) {
+ return;
+ }
+ }
+
+ static char* ExcelColumn(int column, char* bufferBegin, char* bufferEnd)
+ {
+ // https://stackoverflow.com/a/182924/11323702
+
+ int dividend = column;
+ int modulo;
+
+ char* writeHead = bufferEnd - 1;
+ *writeHead = '\0';
+ --writeHead;
+
+ while (dividend > 0) {
+ if (writeHead < bufferBegin) {
+ return nullptr;
+ }
+
+ modulo = (dividend - 1) % 26;
+
+ *writeHead = NthUppercaseLetter(modulo);
+ --writeHead;
+
+ dividend = (dividend - modulo) / 26;
+ }
+
+ // `writeHead` at this point would be a one-past-the-bufferEnd reverse iterator (i.e. one-past-the-(text)beginning in the bufferBegin)
+ // add 1 to get to the actual beginning of the text
+ return writeHead + 1;
+ }
+
+ void DisplayCellProperties(Vec2i pos)
+ {
+ auto& cell = mTable->GetCell(pos);
+ auto& uiCell = mUICells[pos.y * mTable->GetTableWidth() + pos.x];
+
+ char colStr[8]; // 2147483647 -> FXSHRXW, len == 7, along with \0
+ char* colBegin = ExcelColumn(pos.x + 1, std::begin(colStr), std::end(colStr));
+ char rowStr[11]; // len(2147483647) == 10, along with \0
+ ExcelRow(pos.y + 1, std::begin(rowStr), std::end(rowStr));
+ ImGui::Text(I18N_TEXT("Location: %s%s", L10N_TABLE_CELL_POS), colBegin, rowStr);
+
+ switch (cell.Type) {
+ case TableCell::ConstantCell:
+ ImGui::Text(I18N_TEXT("Type: Constant", L10N_TABLE_CELL_TYPE_CONST));
+ break;
+ case TableCell::SingularParametricCell:
+ ImGui::Text(I18N_TEXT("Type: Single parameter", L10N_TABLE_CELL_TYPE_PARAM));
+ break;
+ case TableCell::ArrayParametricCell:
+ ImGui::Text(I18N_TEXT("Type: Array group", L10N_TABLE_CELL_TYPE_CREATE_AG));
+ break;
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button(ICON_FA_EDIT)) {
+ ImGui::OpenPopup("ConvertCtxMenu");
+ }
+ if (ImGui::BeginPopup("ConvertCtxMenu")) {
+ bool constantEnabled = cell.Type != TableCell::ConstantCell;
+ if (ImGui::MenuItem(I18N_TEXT("Convert to regular cell", L10N_TABLE_CELL_CONV_CONST), nullptr, false, constantEnabled)) {
+ mTable->SetCellType(pos, TableCell::ConstantCell);
+ ResetCS();
+ }
+
+ bool singleEnabled = cell.Type != TableCell::SingularParametricCell;
+ if (ImGui::MenuItem(I18N_TEXT("Convert to parameter cell", L10N_TABLE_CELL_CONV_PARAM), nullptr, false, singleEnabled)) {
+ mTable->SetCellType(pos, TableCell::SingularParametricCell);
+ ResetSS(pos);
+ }
+
+ bool arrayEnabled = cell.Type != TableCell::ArrayParametricCell;
+ if (ImGui::MenuItem(I18N_TEXT("Add to a new array group", L10N_TABLE_CELL_CONV_CREATE_AG), nullptr, false, arrayEnabled)) {
+ mTable->AddArrayGroup(pos.y, pos.x, pos.x); // Use automatically generated name
+ ResetAS(pos);
+ }
+
+ bool leftEnabled = mSS.HasLeftAG && arrayEnabled;
+ if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the left", L10N_TABLE_CELL_CONV_ADD_AG_LEFT), nullptr, false, leftEnabled)) {
+ auto& leftCell = mTable->GetCell({ pos.x - 1, pos.y });
+ mTable->ExtendArrayGroupRight(leftCell.DataId, 1);
+ ResetAS(pos);
+ }
+
+ bool rightEnabled = mSS.HasRightAG && arrayEnabled;
+ if (ImGui::MenuItem(I18N_TEXT("Add to the array group to the right", L10N_TABLE_CELL_CONV_ADD_AG_RIGHT), nullptr, false, rightEnabled)) {
+ auto& rightCell = mTable->GetCell({ pos.x + 1, pos.y });
+ mTable->ExtendArrayGroupLeft(rightCell.DataId, 1);
+ ResetAS(pos);
+ }
+
+ ImGui::EndPopup();
+ }
+
+ ImGui::Spacing();
+
+ constexpr auto kLeft = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_LEFT);
+ constexpr auto kCenter = I18N_TEXT("Center", L10N_TABLE_CELL_ALIGN_CENTER);
+ constexpr auto kRight = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_RIGHT);
+
+ const char* horizontalText;
+ switch (cell.HorizontalAlignment) {
+ case TableCell::AlignAxisMin: horizontalText = kLeft; break;
+ case TableCell::AlignCenter: horizontalText = kCenter; break;
+ case TableCell::AlignAxisMax: horizontalText = kRight; break;
+ }
+
+ if (ImGui::BeginCombo(I18N_TEXT("Horizontal alignment", L10N_TABLE_CELL_HORIZONTAL_ALIGNMENT), horizontalText)) {
+ if (ImGui::Selectable(kLeft, cell.HorizontalAlignment == TableCell::AlignAxisMin)) {
+ cell.HorizontalAlignment = TableCell::AlignAxisMin;
+ }
+ if (ImGui::Selectable(kCenter, cell.HorizontalAlignment == TableCell::AlignCenter)) {
+ cell.HorizontalAlignment = TableCell::AlignCenter;
+ }
+ if (ImGui::Selectable(kRight, cell.HorizontalAlignment == TableCell::AlignAxisMax)) {
+ cell.HorizontalAlignment = TableCell::AlignAxisMax;
+ }
+ ImGui::EndCombo();
+ }
+
+ constexpr auto kTop = I18N_TEXT("Left", L10N_TABLE_CELL_ALIGN_TOP);
+ constexpr auto kMiddle = I18N_TEXT("Middle", L10N_TABLE_CELL_ALIGN_MIDDLE);
+ constexpr auto kBottom = I18N_TEXT("Right", L10N_TABLE_CELL_ALIGN_BOTTOM);
+
+ const char* verticalText;
+ switch (cell.VerticalAlignment) {
+ case TableCell::AlignAxisMin: verticalText = kTop; break;
+ case TableCell::AlignCenter: verticalText = kMiddle; break;
+ case TableCell::AlignAxisMax: verticalText = kBottom; break;
+ }
+
+ if (ImGui::BeginCombo(I18N_TEXT("Vertical alignment", L10N_TABLE_CELL_VERTICAL_ALIGNMENT), verticalText)) {
+ if (ImGui::Selectable(kTop, cell.VerticalAlignment == TableCell::AlignAxisMin)) {
+ cell.VerticalAlignment = TableCell::AlignAxisMin;
+ }
+ if (ImGui::Selectable(kMiddle, cell.VerticalAlignment == TableCell::AlignCenter)) {
+ cell.VerticalAlignment = TableCell::AlignCenter;
+ }
+ if (ImGui::Selectable(kBottom, cell.VerticalAlignment == TableCell::AlignAxisMax)) {
+ cell.VerticalAlignment = TableCell::AlignAxisMax;
+ }
+ ImGui::EndCombo();
+ }
+
+ switch (cell.Type) {
+ case TableCell::ConstantCell:
+ ImGui::InputText(I18N_TEXT("Content", L10N_TABLE_CELL_CONTENT), &cell.Content);
+ break;
+
+ case TableCell::SingularParametricCell:
+ if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mSS.EditBuffer)) {
+ // Sync name change to table
+ bool success = mTable->UpdateParameterName(cell.Content, mSS.EditBuffer);
+ if (success) {
+ // Flush name to display content
+ cell.Content = mSS.EditBuffer;
+ mSS.ErrorDuplicateVarName = false;
+ } else {
+ mSS.ErrorDuplicateVarName = true;
+ }
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell.", L10N_TABLE_CELL_VAR_TOOLTIP));
+ }
+
+ if (mSS.ErrorDuplicateVarName) {
+ ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP));
+ }
+ break;
+
+ case TableCell::ArrayParametricCell:
+ if (ImGui::InputText(I18N_TEXT("Variable name", L10N_TABLE_CELL_VAR_NAME), &mAS.EditBuffer)) {
+ auto ag = mTable->GetArrayGroup(cell.DataId);
+ bool success = ag.UpdateCellName(cell.Content, mAS.EditBuffer);
+ if (success) {
+ cell.Content = mAS.EditBuffer;
+ mAS.ErrorDuplicateVarName = false;
+ } else {
+ mAS.ErrorDuplicateVarName = true;
+ }
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip(I18N_TEXT("Name of the parameter link to this cell; local within the array group.", L10N_TABLE_CELL_ARRAY_VAR_TOOLTIP));
+ }
+
+ if (mAS.ErrorDuplicateVarName) {
+ ImGui::ErrorMessage(I18N_TEXT("Variable name duplicated.", L10N_TABLE_CELL_VAR_NAME_DUP));
+ }
+ break;
+ }
+ }
+
+ void DisplayRangeProperties(Vec2i tl, Vec2i br)
+ {
+ // TODO
+ }
+
+ void DisplayTableContents()
+ {
+ if (ImGui::TreeNode(ICON_FA_BONG " " I18N_TEXT("Parameters", L10N_TABLE_SINGLE_PARAMS))) {
+ TableSingleParamsIter iter(*mTable);
+ while (iter.HasNext()) {
+ auto& cell = iter.Next();
+ if (ImGui::Selectable(cell.Content.c_str())) {
+ SelectCell(cell.Location);
+ }
+ }
+ ImGui::TreePop();
+ }
+ if (ImGui::TreeNode(ICON_FA_LIST " " I18N_TEXT("Array groups", L10N_TABLE_ARRAY_GROUPS))) {
+ TableArrayGroupsIter iter(*mTable);
+ // For each array group
+ while (iter.HasNext()) {
+ if (ImGui::TreeNode(iter.PeekNameCStr())) {
+ auto& ag = iter.Peek();
+ // For each cell in the array group
+ for (int x = ag.LeftCell; x <= ag.RightCell; ++x) {
+ Vec2i pos{ x, ag.Row };
+ auto& cell = mTable->GetCell(pos);
+ if (ImGui::Selectable(cell.Content.c_str())) {
+ SelectCell(pos);
+ }
+ }
+ ImGui::TreePop();
+ }
+ iter.Next();
+ }
+ ImGui::TreePop();
+ }
+ }
+
+ void DisplayTableProperties()
+ {
+ ImGui::InputInt(I18N_TEXT("Width", L10N_TABLE_WIDTH), &mNewTableWidth);
+ ImGui::InputInt(I18N_TEXT("Height", L10N_TABLE_HEIGHT), &mNewTableHeight);
+
+ if (ImGui::Button(I18N_TEXT("Confirm", L10N_CONFIRM))) {
+ ImGui::CloseCurrentPopup();
+ Resize(mNewTableWidth, mNewTableHeight);
+ }
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Cancel", L10N_CANCEL))) {
+ ImGui::CloseCurrentPopup();
+ }
+
+ // TODO
+ }
+
+ void DisplayTable()
+ {
+ struct CellPalette
+ {
+ ImU32 Regular;
+ ImU32 Hovered;
+ ImU32 Active;
+
+ ImU32 GetColorFor(const UICell& cell) const
+ {
+ if (cell.Held) {
+ return Active;
+ } else if (cell.Hovered) {
+ return Hovered;
+ } else {
+ return Regular;
+ }
+ }
+ };
+
+ CellPalette constantPalette{
+ .Regular = ImGui::GetColorU32(ImGuiCol_Button),
+ .Hovered = ImGui::GetColorU32(ImGuiCol_ButtonHovered),
+ .Active = ImGui::GetColorU32(ImGuiCol_ButtonActive),
+ };
+ CellPalette paramPalette{
+ .Regular = IM_COL32(0, 214, 4, 102),
+ .Hovered = IM_COL32(0, 214, 4, 255),
+ .Active = IM_COL32(0, 191, 2, 255),
+ };
+
+ // TODO array group color
+
+ auto navHighlight = ImGui::GetColorU32(ImGuiCol_NavHighlight);
+
+ int colCount = mTable->GetTableWidth();
+ int rowCount = mTable->GetTableHeight();
+ for (int rowIdx = 0; rowIdx < rowCount; ++rowIdx) {
+ int rowHeight = mTable->GetRowHeight(rowIdx);
+
+ for (int colIdx = 0; colIdx < colCount; ++colIdx) {
+ int colWidth = mTable->GetColumnWidth(colIdx);
+
+ int i = rowIdx * colCount + colIdx;
+ auto window = ImGui::GetCurrentWindow();
+ auto id = window->GetID(i);
+
+ Vec2i cellLoc{ colIdx, rowIdx };
+ auto& cell = mTable->GetCell(cellLoc);
+ auto& uiCell = mUICells[i];
+
+ ImVec2 size(colWidth, rowHeight);
+ ImRect rect{
+ window->DC.CursorPos,
+ window->DC.CursorPos + ImGui::CalcItemSize(size, 0.0f, 0.0f),
+ };
+
+ /* Draw cell selection */
+
+ if (uiCell.Selected) {
+ constexpr int mt = 2; // Marker Thickness
+ constexpr int ms = 8; // Marker Size
+
+ ImVec2 outerTL(rect.Min - ImVec2(mt, mt));
+ ImVec2 outerBR(rect.Max + ImVec2(mt, mt));
+
+ // Top left
+ window->DrawList->AddRectFilled(outerTL + ImVec2(0, 0), outerTL + ImVec2(ms, mt), navHighlight);
+ window->DrawList->AddRectFilled(outerTL + ImVec2(0, mt), outerTL + ImVec2(mt, ms), navHighlight);
+
+ // Top right
+ ImVec2 outerTR(outerBR.x, outerTL.y);
+ window->DrawList->AddRectFilled(outerTR + ImVec2(-ms, 0), outerTR + ImVec2(0, mt), navHighlight);
+ window->DrawList->AddRectFilled(outerTR + ImVec2(-mt, mt), outerTR + ImVec2(0, ms), navHighlight);
+
+ // Bottom right
+ window->DrawList->AddRectFilled(outerBR + ImVec2(-ms, -mt), outerBR + ImVec2(0, 0), navHighlight);
+ window->DrawList->AddRectFilled(outerBR + ImVec2(-mt, -ms), outerBR + ImVec2(0, -mt), navHighlight);
+
+ // Bottom left
+ ImVec2 outerBL(outerTL.x, outerBR.y);
+ window->DrawList->AddRectFilled(outerBL + ImVec2(0, -mt), outerBL + ImVec2(ms, 0), navHighlight);
+ window->DrawList->AddRectFilled(outerBL + ImVec2(0, -ms), outerBL + ImVec2(mt, -mt), navHighlight);
+ }
+
+ /* Draw cell body */
+
+ CellPalette* palette;
+ switch (cell.Type) {
+ case TableCell::ConstantCell:
+ palette = &constantPalette;
+ break;
+
+ case TableCell::SingularParametricCell:
+ case TableCell::ArrayParametricCell:
+ palette = &paramPalette;
+ break;
+ }
+
+ window->DrawList->AddRectFilled(rect.Min, rect.Max, palette->GetColorFor(uiCell));
+
+ /* Draw cell content */
+
+ auto content = cell.Content.c_str();
+ auto contentEnd = content + cell.Content.size();
+ auto textSize = ImGui::CalcTextSize(content, contentEnd);
+
+ ImVec2 textRenderPos;
+ switch (cell.HorizontalAlignment) {
+ case TableCell::AlignAxisMin: textRenderPos.x = rect.Min.x; break;
+ case TableCell::AlignCenter: textRenderPos.x = rect.Min.x + colWidth / 2 - textSize.x / 2; break;
+ case TableCell::AlignAxisMax: textRenderPos.x = rect.Max.x - textSize.x; break;
+ }
+ switch (cell.VerticalAlignment) {
+ case TableCell::AlignAxisMin: textRenderPos.y = rect.Min.y; break;
+ case TableCell::AlignCenter: textRenderPos.y = rect.Min.y + rowHeight / 2 - textSize.y / 2; break;
+ case TableCell::AlignAxisMax: textRenderPos.y = rect.Max.y - textSize.y; break;
+ }
+ window->DrawList->AddText(textRenderPos, IM_COL32(0, 0, 0, 255), content, contentEnd);
+
+ /* Update ImGui cursor */
+
+ ImGui::ItemSize(size);
+ if (!ImGui::ItemAdd(rect, id)) {
+ goto logicEnd;
+ }
+
+ if (mMode != ModeEditing) {
+ goto logicEnd;
+ }
+ if (ImGui::ButtonBehavior(rect, id, &uiCell.Hovered, &uiCell.Held)) {
+ if (ImGui::GetIO().KeyShift && IsSelected()) {
+ SelectRange(mSelectionTL, { colIdx, rowIdx });
+ } else {
+ SelectCell({ colIdx, rowIdx });
+ }
+ }
+
+ logicEnd:
+ // Don't SameLine() if we are on the last cell in the row
+ if (colIdx != colCount - 1) {
+ ImGui::SameLine();
+ }
+ }
+ }
+
+ for (auto& uag : mUIArrayGroups) {
+ ImGui::GetCurrentWindow()->DrawList->AddRect(
+ uag.Pos - ImVec2(1, 1),
+ uag.Pos + uag.Size + ImVec2(1, 1),
+ kArrayGroupOutline);
+ }
+ }
+
+ void DisplayResizers(
+ ImVec2 pos,
+ ImVec2 sizerDim,
+ ImVec2 sizerOffset,
+ std::type_identity_t<float ImVec2::*> vecCompGetter,
+ std::type_identity_t<int (TableTemplate::*)() const> lenGetter,
+ std::type_identity_t<int (TableTemplate::*)(int) const> dimGetter,
+ std::type_identity_t<void (TableTemplate::*)(int, int)> dimSetter)
+ {
+ auto window = ImGui::GetCurrentWindow();
+ auto spacing = ImGui::GetStyle().ItemSpacing.*vecCompGetter;
+
+ auto regularColor = ImGui::GetColorU32(ImGuiCol_Button);
+ auto hoveredColor = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
+ auto activeColor = ImGui::GetColorU32(ImGuiCol_ButtonActive);
+
+ auto GetColor = [&](const Sizer& sizer) -> ImU32 {
+ if (sizer.Held) {
+ return activeColor;
+ } else if (sizer.Hovered) {
+ return hoveredColor;
+ } else {
+ return regularColor;
+ }
+ };
+
+ for (int ix = 0; ix < (mTable.get()->*lenGetter)(); ++ix) {
+ // ImGui uses float for sizes, our table uses int (because excel uses int)
+ // Convert here to avoid mountains of narrowing warnings below
+ auto dim = (float)(mTable.get()->*dimGetter)(ix);
+
+ pos.*vecCompGetter += dim;
+ ImRect rect{
+ pos - sizerOffset,
+ pos - sizerOffset + ImGui::CalcItemSize(sizerDim, 0.0f, 0.0f),
+ };
+ pos.*vecCompGetter += spacing;
+
+ auto& sizer = mColSizers[ix];
+ auto id = window->GetID(ix);
+ window->DrawList->AddRectFilled(rect.Min, rect.Max, GetColor(sizer));
+
+ if (ImGui::ButtonBehavior(rect, id, &sizer.Hovered, &sizer.Held, ImGuiButtonFlags_PressedOnClick)) {
+ mStartDragDim = dim;
+ mStartDragMouseCoordinate = ImGui::GetMousePos().*vecCompGetter;
+ }
+ if (sizer.Held) {
+ float change = ImGui::GetMousePos().*vecCompGetter - mStartDragMouseCoordinate;
+ float colWidth = std::max(mStartDragDim + change, 1.0f);
+ (mTable.get()->*dimSetter)(ix, (int)colWidth);
+ }
+ }
+ }
+
+ void DisplayTableResizers(ImVec2 topLeftPixelPos)
+ {
+ constexpr float kExtraSideLength = 5.0f;
+ constexpr float kExtraAxialLength = 2.0f;
+
+ switch (mMode) {
+ case ModeEditing: break;
+
+ case ModeColumnResizing:
+ ImGui::PushID("Cols");
+ DisplayResizers(
+ topLeftPixelPos,
+ ImVec2(
+ ImGui::GetStyle().ItemSpacing.x + kExtraSideLength * 2,
+ CalcTablePixelHeight() + kExtraAxialLength * 2),
+ ImVec2(kExtraSideLength, kExtraAxialLength),
+ &ImVec2::x,
+ &TableTemplate::GetTableWidth,
+ &TableTemplate::GetColumnWidth,
+ &TableTemplate::SetColumnWidth);
+ ImGui::PopID();
+ break;
+
+ case ModeRowResizing:
+ ImGui::PushID("Rows");
+ DisplayResizers(
+ topLeftPixelPos,
+ ImVec2(
+ CalcTablePixelWidth() + kExtraAxialLength * 2,
+ ImGui::GetStyle().ItemSpacing.y + kExtraSideLength * 2),
+ ImVec2(kExtraAxialLength, kExtraSideLength),
+ &ImVec2::y,
+ &TableTemplate::GetTableHeight,
+ &TableTemplate::GetRowHeight,
+ &TableTemplate::SetRowHeight);
+ ImGui::PopID();
+ break;
+ }
+ }
+
+ float CalcTablePixelWidth() const
+ {
+ float horizontalSpacing = ImGui::GetStyle().ItemSpacing.x;
+ float width = 0;
+ for (int x = 0; x < mTable->GetTableWidth(); ++x) {
+ width += mTable->GetColumnWidth(x);
+ width += horizontalSpacing;
+ }
+ return width - horizontalSpacing;
+ }
+
+ float CalcTablePixelHeight() const
+ {
+ float verticalSpacing = ImGui::GetStyle().ItemSpacing.y;
+ float height = 0;
+ for (int y = 0; y < mTable->GetTableHeight(); ++y) {
+ height += mTable->GetRowHeight(y);
+ height += verticalSpacing;
+ }
+ return height - verticalSpacing;
+ }
+
+ template <class TFunction>
+ void ForeachSelectedCell(const TFunction& func)
+ {
+ for (int y = mSelectionTL.y; y <= mSelectionBR.y; ++y) {
+ for (int x = mSelectionTL.x; x <= mSelectionBR.x; ++x) {
+ int i = y * mTable->GetTableWidth() + x;
+ func(i, x, y);
+ }
+ }
+ }
+
+ bool IsSelected() const
+ {
+ return mSelectionTL.x != -1;
+ }
+
+ void ClearSelection()
+ {
+ if (IsSelected()) {
+ ForeachSelectedCell([this](int i, int, int) {
+ auto& uiCell = mUICells[i];
+ uiCell.Selected = false;
+ });
+ }
+
+ mSelectionTL = { -1, -1 };
+ mSelectionBR = { -1, -1 };
+
+ ResetIdleState();
+ }
+
+ void ResetIdleState()
+ {
+ mIdleState = {};
+ }
+
+ void SelectRange(Vec2i p1, Vec2i p2)
+ {
+ ClearSelection();
+
+ if (p2.x < p1.x) {
+ std::swap(p2.x, p1.x);
+ }
+ if (p2.y < p1.y) {
+ std::swap(p2.y, p1.y);
+ }
+
+ mSelectionTL = p1;
+ mSelectionBR = p2;
+
+ ForeachSelectedCell([this](int i, int, int) {
+ auto& uiCell = mUICells[i];
+ uiCell.Selected = true;
+ });
+
+ ResetRS();
+ }
+
+ void ResetRS()
+ {
+ mRS = {};
+ }
+
+ void SelectCell(Vec2i pos)
+ {
+ ClearSelection();
+
+ mSelectionTL = pos;
+ mSelectionBR = pos;
+
+ int i = pos.y * mTable->GetTableWidth() + pos.x;
+ mUICells[i].Selected = true;
+
+ switch (mTable->GetCell(pos).Type) {
+ case TableCell::ConstantCell: ResetCS(); break;
+ case TableCell::SingularParametricCell: ResetSS(pos); break;
+ case TableCell::ArrayParametricCell: ResetAS(pos); break;
+ }
+ }
+
+ void ResetCS()
+ {
+ mCS = {};
+ }
+
+ void ResetSS(Vec2i pos)
+ {
+ new (&mSS) SStates{
+ .EditBuffer = mTable->GetCell(pos).Content,
+ .ErrorDuplicateVarName = false,
+ .HasLeftAG = pos.x > 1 && mTable->GetCell({ pos.x - 1, pos.y }).Type == TableCell::ArrayParametricCell,
+ .HasRightAG = pos.x < mTable->GetTableWidth() - 1 && mTable->GetCell({ pos.x + 1, pos.y }).Type == TableCell::ArrayParametricCell,
+ };
+ }
+
+ void ResetAS(Vec2i pos)
+ {
+ new (&mAS) AStates{
+ .EditBuffer = mTable->GetCell(pos).Content,
+ .ErrorDuplicateVarName = false,
+ };
+ }
+};
+
+template <class TTarget>
+static auto CastTemplateAs(std::unique_ptr<Template>& input) requires std::is_base_of_v<Template, TTarget>
+{
+ return std::unique_ptr<TTarget>(static_cast<TTarget*>(input.release()));
+}
+
+std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(std::unique_ptr<Template> tmpl)
+{
+ switch (tmpl->GetKind()) {
+ case Template::KD_Table: return std::make_unique<TableTemplateUI>(CastTemplateAs<TableTemplate>(tmpl));
+ case Template::InvalidKind: break;
+ }
+ return nullptr;
+}
+
+std::unique_ptr<TemplateUI> TemplateUI::CreateByKind(Template::Kind kind)
+{
+ switch (kind) {
+ case Template::KD_Table: return std::make_unique<TableTemplateUI>(std::make_unique<TableTemplate>());
+ case Template::InvalidKind: break;
+ }
+ return nullptr;
+}
+} // namespace CPLT_UNITY_ID
+
+void UI::TemplatesTab()
+{
+ auto& project = *GlobalStates::GetInstance().GetCurrentProject();
+
+ static std::unique_ptr<CPLT_UNITY_ID::TemplateUI> openTemplate;
+ static AssetList::ListState state;
+ bool openedDummy = true;
+
+ // Toolbar item: close
+ if (ImGui::Button(ICON_FA_TIMES " " I18N_TEXT("Close", L10N_CLOSE), openTemplate == nullptr)) {
+ openTemplate->Close();
+ openTemplate = nullptr;
+ }
+
+ // Toolbar item: open...
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Open asset...", L10N_ASSET_OPEN))) {
+ ImGui::OpenPopup(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE));
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Open asset", L10N_ASSET_OPEN_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ if (ImGui::Button(ICON_FA_FOLDER_OPEN " " I18N_TEXT("Open", L10N_OPEN), state.SelectedAsset == nullptr)) {
+ ImGui::CloseCurrentPopup();
+
+ auto tmpl = project.Templates.Load(*state.SelectedAsset);
+ openTemplate = CPLT_UNITY_ID::TemplateUI::CreateByKind(std::move(tmpl));
+ }
+ ImGui::SameLine();
+ project.Templates.DisplayControls(state);
+ project.Templates.DisplayDetailsList(state);
+
+ ImGui::EndPopup();
+ }
+
+ // Toolbar item: manage...
+ ImGui::SameLine();
+ if (ImGui::Button(I18N_TEXT("Manage assets...", L10N_ASSET_MANAGE))) {
+ ImGui::OpenPopup(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE));
+ }
+ if (ImGui::BeginPopupModal(I18N_TEXT("Manage assets", L10N_ASSET_MANAGE_DIALOG_TITLE), &openedDummy, ImGuiWindowFlags_AlwaysAutoResize)) {
+ project.Templates.DisplayControls(state);
+ project.Templates.DisplayDetailsList(state);
+ ImGui::EndPopup();
+ }
+
+ if (openTemplate) {
+ openTemplate->Display();
+ }
+}