diff options
Diffstat (limited to 'app/source/Cplt/UI/UI_Templates.cpp')
-rw-r--r-- | app/source/Cplt/UI/UI_Templates.cpp | 977 |
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 = ¶mPalette; + 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(); + } +} |