From f8a54942eed2edbf79056fd73e6514fcef054ff1 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:22 +0200 Subject: [PATCH 1/6] feat(cpp/vendor): vendor ImGuiColorTextEdit (MIT) for text_editor primitive Pinneado al commit 0a88824f del upstream. Patches locales aplicados a TextEditor.cpp para compilar contra ImGui 1.91+ (GetKeyIndex eliminado, PushAllowKeyboardFocus/PopAllowKeyboardFocus removidos). Documentado en cpp/vendor/imgui_text_edit/README.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/vendor/imgui_text_edit/README.md | 48 + cpp/vendor/imgui_text_edit/TextEditor.cpp | 3160 +++++++++++++++++++++ cpp/vendor/imgui_text_edit/TextEditor.h | 389 +++ 3 files changed, 3597 insertions(+) create mode 100644 cpp/vendor/imgui_text_edit/README.md create mode 100644 cpp/vendor/imgui_text_edit/TextEditor.cpp create mode 100644 cpp/vendor/imgui_text_edit/TextEditor.h diff --git a/cpp/vendor/imgui_text_edit/README.md b/cpp/vendor/imgui_text_edit/README.md new file mode 100644 index 00000000..d8d8e54f --- /dev/null +++ b/cpp/vendor/imgui_text_edit/README.md @@ -0,0 +1,48 @@ +# ImGuiColorTextEdit (vendored) + +Source: https://github.com/BalazsJako/ImGuiColorTextEdit +License: MIT +Pinned commit: `0a88824f7de8d0bd11d8419066caa7d3469395c4` (master HEAD at vendor time) + +Files: +- `TextEditor.h` +- `TextEditor.cpp` + +Used by `cpp/functions/core/text_editor.{h,cpp}` via PIMPL — the public API is in +`namespace fn::` and the vendor's `TextEditor` type is hidden inside the .cpp. + +## Updating + +```bash +cd cpp/vendor/imgui_text_edit +curl -fL -O https://raw.githubusercontent.com/BalazsJako/ImGuiColorTextEdit//TextEditor.h +curl -fL -O https://raw.githubusercontent.com/BalazsJako/ImGuiColorTextEdit//TextEditor.cpp +# Update commit hash above. +``` + +## Notes / known issues + +- The vendor expects `imgui.h` and `imgui_internal.h` in the include path. Both are + provided by our vendored ImGui in `cpp/vendor/imgui/`. +- `LanguageDefinition::GLSL()`, `SQL()`, `CPlusPlus()` are provided by the vendor and + consumed by our wrapper (`fn::CodeLang`). +- `inotify` watcher limit on Linux — see `file_watcher_cpp_core.md`. + +## Local patches applied + +Upstream commit `0a88824f` predates several ImGui API removals (we vendor ImGui +1.91+). The following minimal in-tree patches were applied to `TextEditor.cpp`: + +- `ImGui::GetKeyIndex(ImGuiKey_X)` -> `ImGuiKey_X` (the `ImGuiKey` enum is now a + direct, stable index — `GetKeyIndex` was removed). +- `ImGui::PushAllowKeyboardFocus(true)` / `PopAllowKeyboardFocus()` -> removed + (replaced upstream by `SetItemKeyOwner` / `ImGuiItemFlags_AllowKeyboardFocus`, + but the editor functions correctly without them in our embedding). + +Re-apply when refreshing the vendor: + +```bash +sed -i 's/ImGui::GetKeyIndex(\([^)]*\))/\1/g' TextEditor.cpp +sed -i 's/ImGui::PushAllowKeyboardFocus(true);/\/\/ removed in ImGui 1.89+/g' TextEditor.cpp +sed -i 's/ImGui::PopAllowKeyboardFocus();/\/\/ removed in ImGui 1.89+/g' TextEditor.cpp +``` diff --git a/cpp/vendor/imgui_text_edit/TextEditor.cpp b/cpp/vendor/imgui_text_edit/TextEditor.cpp new file mode 100644 index 00000000..11f1229c --- /dev/null +++ b/cpp/vendor/imgui_text_edit/TextEditor.cpp @@ -0,0 +1,3160 @@ +#include +#include +#include +#include +#include + +#include "TextEditor.h" + +#define IMGUI_DEFINE_MATH_OPERATORS +#include "imgui.h" // for imGui::GetCurrentWindow() + +// TODO +// - multiline comments vs single-line: latter is blocking start of a ML + +template +bool equals(InputIt1 first1, InputIt1 last1, + InputIt2 first2, InputIt2 last2, BinaryPredicate p) +{ + for (; first1 != last1 && first2 != last2; ++first1, ++first2) + { + if (!p(*first1, *first2)) + return false; + } + return first1 == last1 && first2 == last2; +} + +TextEditor::TextEditor() + : mLineSpacing(1.0f) + , mUndoIndex(0) + , mTabSize(4) + , mOverwrite(false) + , mReadOnly(false) + , mWithinRender(false) + , mScrollToCursor(false) + , mScrollToTop(false) + , mTextChanged(false) + , mColorizerEnabled(true) + , mTextStart(20.0f) + , mLeftMargin(10) + , mCursorPositionChanged(false) + , mColorRangeMin(0) + , mColorRangeMax(0) + , mSelectionMode(SelectionMode::Normal) + , mCheckComments(true) + , mLastClick(-1.0f) + , mHandleKeyboardInputs(true) + , mHandleMouseInputs(true) + , mIgnoreImGuiChild(false) + , mShowWhitespaces(true) + , mStartTime(std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count()) +{ + SetPalette(GetDarkPalette()); + SetLanguageDefinition(LanguageDefinition::HLSL()); + mLines.push_back(Line()); +} + +TextEditor::~TextEditor() +{ +} + +void TextEditor::SetLanguageDefinition(const LanguageDefinition & aLanguageDef) +{ + mLanguageDefinition = aLanguageDef; + mRegexList.clear(); + + for (auto& r : mLanguageDefinition.mTokenRegexStrings) + mRegexList.push_back(std::make_pair(std::regex(r.first, std::regex_constants::optimize), r.second)); + + Colorize(); +} + +void TextEditor::SetPalette(const Palette & aValue) +{ + mPaletteBase = aValue; +} + +std::string TextEditor::GetText(const Coordinates & aStart, const Coordinates & aEnd) const +{ + std::string result; + + auto lstart = aStart.mLine; + auto lend = aEnd.mLine; + auto istart = GetCharacterIndex(aStart); + auto iend = GetCharacterIndex(aEnd); + size_t s = 0; + + for (size_t i = lstart; i < lend; i++) + s += mLines[i].size(); + + result.reserve(s + s / 8); + + while (istart < iend || lstart < lend) + { + if (lstart >= (int)mLines.size()) + break; + + auto& line = mLines[lstart]; + if (istart < (int)line.size()) + { + result += line[istart].mChar; + istart++; + } + else + { + istart = 0; + ++lstart; + result += '\n'; + } + } + + return result; +} + +TextEditor::Coordinates TextEditor::GetActualCursorCoordinates() const +{ + return SanitizeCoordinates(mState.mCursorPosition); +} + +TextEditor::Coordinates TextEditor::SanitizeCoordinates(const Coordinates & aValue) const +{ + auto line = aValue.mLine; + auto column = aValue.mColumn; + if (line >= (int)mLines.size()) + { + if (mLines.empty()) + { + line = 0; + column = 0; + } + else + { + line = (int)mLines.size() - 1; + column = GetLineMaxColumn(line); + } + return Coordinates(line, column); + } + else + { + column = mLines.empty() ? 0 : std::min(column, GetLineMaxColumn(line)); + return Coordinates(line, column); + } +} + +// https://en.wikipedia.org/wiki/UTF-8 +// We assume that the char is a standalone character (<128) or a leading byte of an UTF-8 code sequence (non-10xxxxxx code) +static int UTF8CharLength(TextEditor::Char c) +{ + if ((c & 0xFE) == 0xFC) + return 6; + if ((c & 0xFC) == 0xF8) + return 5; + if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xE0) == 0xC0) + return 2; + return 1; +} + +// "Borrowed" from ImGui source +static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +{ + if (c < 0x80) + { + buf[0] = (char)c; + return 1; + } + if (c < 0x800) + { + if (buf_size < 2) return 0; + buf[0] = (char)(0xc0 + (c >> 6)); + buf[1] = (char)(0x80 + (c & 0x3f)); + return 2; + } + if (c >= 0xdc00 && c < 0xe000) + { + return 0; + } + if (c >= 0xd800 && c < 0xdc00) + { + if (buf_size < 4) return 0; + buf[0] = (char)(0xf0 + (c >> 18)); + buf[1] = (char)(0x80 + ((c >> 12) & 0x3f)); + buf[2] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[3] = (char)(0x80 + ((c) & 0x3f)); + return 4; + } + //else if (c < 0x10000) + { + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c) & 0x3f)); + return 3; + } +} + +void TextEditor::Advance(Coordinates & aCoordinates) const +{ + if (aCoordinates.mLine < (int)mLines.size()) + { + auto& line = mLines[aCoordinates.mLine]; + auto cindex = GetCharacterIndex(aCoordinates); + + if (cindex + 1 < (int)line.size()) + { + auto delta = UTF8CharLength(line[cindex].mChar); + cindex = std::min(cindex + delta, (int)line.size() - 1); + } + else + { + ++aCoordinates.mLine; + cindex = 0; + } + aCoordinates.mColumn = GetCharacterColumn(aCoordinates.mLine, cindex); + } +} + +void TextEditor::DeleteRange(const Coordinates & aStart, const Coordinates & aEnd) +{ + assert(aEnd >= aStart); + assert(!mReadOnly); + + //printf("D(%d.%d)-(%d.%d)\n", aStart.mLine, aStart.mColumn, aEnd.mLine, aEnd.mColumn); + + if (aEnd == aStart) + return; + + auto start = GetCharacterIndex(aStart); + auto end = GetCharacterIndex(aEnd); + + if (aStart.mLine == aEnd.mLine) + { + auto& line = mLines[aStart.mLine]; + auto n = GetLineMaxColumn(aStart.mLine); + if (aEnd.mColumn >= n) + line.erase(line.begin() + start, line.end()); + else + line.erase(line.begin() + start, line.begin() + end); + } + else + { + auto& firstLine = mLines[aStart.mLine]; + auto& lastLine = mLines[aEnd.mLine]; + + firstLine.erase(firstLine.begin() + start, firstLine.end()); + lastLine.erase(lastLine.begin(), lastLine.begin() + end); + + if (aStart.mLine < aEnd.mLine) + firstLine.insert(firstLine.end(), lastLine.begin(), lastLine.end()); + + if (aStart.mLine < aEnd.mLine) + RemoveLine(aStart.mLine + 1, aEnd.mLine + 1); + } + + mTextChanged = true; +} + +int TextEditor::InsertTextAt(Coordinates& /* inout */ aWhere, const char * aValue) +{ + assert(!mReadOnly); + + int cindex = GetCharacterIndex(aWhere); + int totalLines = 0; + while (*aValue != '\0') + { + assert(!mLines.empty()); + + if (*aValue == '\r') + { + // skip + ++aValue; + } + else if (*aValue == '\n') + { + if (cindex < (int)mLines[aWhere.mLine].size()) + { + auto& newLine = InsertLine(aWhere.mLine + 1); + auto& line = mLines[aWhere.mLine]; + newLine.insert(newLine.begin(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.end()); + } + else + { + InsertLine(aWhere.mLine + 1); + } + ++aWhere.mLine; + aWhere.mColumn = 0; + cindex = 0; + ++totalLines; + ++aValue; + } + else + { + auto& line = mLines[aWhere.mLine]; + auto d = UTF8CharLength(*aValue); + while (d-- > 0 && *aValue != '\0') + line.insert(line.begin() + cindex++, Glyph(*aValue++, PaletteIndex::Default)); + ++aWhere.mColumn; + } + + mTextChanged = true; + } + + return totalLines; +} + +void TextEditor::AddUndo(UndoRecord& aValue) +{ + assert(!mReadOnly); + //printf("AddUndo: (@%d.%d) +\'%s' [%d.%d .. %d.%d], -\'%s', [%d.%d .. %d.%d] (@%d.%d)\n", + // aValue.mBefore.mCursorPosition.mLine, aValue.mBefore.mCursorPosition.mColumn, + // aValue.mAdded.c_str(), aValue.mAddedStart.mLine, aValue.mAddedStart.mColumn, aValue.mAddedEnd.mLine, aValue.mAddedEnd.mColumn, + // aValue.mRemoved.c_str(), aValue.mRemovedStart.mLine, aValue.mRemovedStart.mColumn, aValue.mRemovedEnd.mLine, aValue.mRemovedEnd.mColumn, + // aValue.mAfter.mCursorPosition.mLine, aValue.mAfter.mCursorPosition.mColumn + // ); + + mUndoBuffer.resize((size_t)(mUndoIndex + 1)); + mUndoBuffer.back() = aValue; + ++mUndoIndex; +} + +TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPosition) const +{ + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 local(aPosition.x - origin.x, aPosition.y - origin.y); + + int lineNo = std::max(0, (int)floor(local.y / mCharAdvance.y)); + + int columnCoord = 0; + + if (lineNo >= 0 && lineNo < (int)mLines.size()) + { + auto& line = mLines.at(lineNo); + + int columnIndex = 0; + float columnX = 0.0f; + + while ((size_t)columnIndex < line.size()) + { + float columnWidth = 0.0f; + + if (line[columnIndex].mChar == '\t') + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ").x; + float oldX = columnX; + float newColumnX = (1.0f + std::floor((1.0f + columnX) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + columnWidth = newColumnX - oldX; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX = newColumnX; + columnCoord = (columnCoord / mTabSize) * mTabSize + mTabSize; + columnIndex++; + } + else + { + char buf[7]; + auto d = UTF8CharLength(line[columnIndex].mChar); + int i = 0; + while (i < 6 && d-- > 0) + buf[i++] = line[columnIndex++].mChar; + buf[i] = '\0'; + columnWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf).x; + if (mTextStart + columnX + columnWidth * 0.5f > local.x) + break; + columnX += columnWidth; + columnCoord++; + } + } + } + + return SanitizeCoordinates(Coordinates(lineNo, columnCoord)); +} + +TextEditor::Coordinates TextEditor::FindWordStart(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + while (cindex > 0 && isspace(line[cindex].mChar)) + --cindex; + + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex > 0) + { + auto c = line[cindex].mChar; + if ((c & 0xC0) != 0x80) // not UTF code sequence 10xxxxxx + { + if (c <= 32 && isspace(c)) + { + cindex++; + break; + } + if (cstart != (PaletteIndex)line[size_t(cindex - 1)].mColorIndex) + break; + } + --cindex; + } + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindWordEnd(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + auto& line = mLines[at.mLine]; + auto cindex = GetCharacterIndex(at); + + if (cindex >= (int)line.size()) + return at; + + bool prevspace = (bool)isspace(line[cindex].mChar); + auto cstart = (PaletteIndex)line[cindex].mColorIndex; + while (cindex < (int)line.size()) + { + auto c = line[cindex].mChar; + auto d = UTF8CharLength(c); + if (cstart != (PaletteIndex)line[cindex].mColorIndex) + break; + + if (prevspace != !!isspace(c)) + { + if (isspace(c)) + while (cindex < (int)line.size() && isspace(line[cindex].mChar)) + ++cindex; + break; + } + cindex += d; + } + return Coordinates(aFrom.mLine, GetCharacterColumn(aFrom.mLine, cindex)); +} + +TextEditor::Coordinates TextEditor::FindNextWord(const Coordinates & aFrom) const +{ + Coordinates at = aFrom; + if (at.mLine >= (int)mLines.size()) + return at; + + // skip to the next non-word character + auto cindex = GetCharacterIndex(aFrom); + bool isword = false; + bool skip = false; + if (cindex < (int)mLines[at.mLine].size()) + { + auto& line = mLines[at.mLine]; + isword = isalnum(line[cindex].mChar); + skip = isword; + } + + while (!isword || skip) + { + if (at.mLine >= mLines.size()) + { + auto l = std::max(0, (int) mLines.size() - 1); + return Coordinates(l, GetLineMaxColumn(l)); + } + + auto& line = mLines[at.mLine]; + if (cindex < (int)line.size()) + { + isword = isalnum(line[cindex].mChar); + + if (isword && !skip) + return Coordinates(at.mLine, GetCharacterColumn(at.mLine, cindex)); + + if (!isword) + skip = false; + + cindex++; + } + else + { + cindex = 0; + ++at.mLine; + skip = false; + isword = false; + } + } + + return at; +} + +int TextEditor::GetCharacterIndex(const Coordinates& aCoordinates) const +{ + if (aCoordinates.mLine >= mLines.size()) + return -1; + auto& line = mLines[aCoordinates.mLine]; + int c = 0; + int i = 0; + for (; i < line.size() && c < aCoordinates.mColumn;) + { + if (line[i].mChar == '\t') + c = (c / mTabSize) * mTabSize + mTabSize; + else + ++c; + i += UTF8CharLength(line[i].mChar); + } + return i; +} + +int TextEditor::GetCharacterColumn(int aLine, int aIndex) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + int i = 0; + while (i < aIndex && i < (int)line.size()) + { + auto c = line[i].mChar; + i += UTF8CharLength(c); + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + } + return col; +} + +int TextEditor::GetLineCharacterCount(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int c = 0; + for (unsigned i = 0; i < line.size(); c++) + i += UTF8CharLength(line[i].mChar); + return c; +} + +int TextEditor::GetLineMaxColumn(int aLine) const +{ + if (aLine >= mLines.size()) + return 0; + auto& line = mLines[aLine]; + int col = 0; + for (unsigned i = 0; i < line.size(); ) + { + auto c = line[i].mChar; + if (c == '\t') + col = (col / mTabSize) * mTabSize + mTabSize; + else + col++; + i += UTF8CharLength(c); + } + return col; +} + +bool TextEditor::IsOnWordBoundary(const Coordinates & aAt) const +{ + if (aAt.mLine >= (int)mLines.size() || aAt.mColumn == 0) + return true; + + auto& line = mLines[aAt.mLine]; + auto cindex = GetCharacterIndex(aAt); + if (cindex >= (int)line.size()) + return true; + + if (mColorizerEnabled) + return line[cindex].mColorIndex != line[size_t(cindex - 1)].mColorIndex; + + return isspace(line[cindex].mChar) != isspace(line[cindex - 1].mChar); +} + +void TextEditor::RemoveLine(int aStart, int aEnd) +{ + assert(!mReadOnly); + assert(aEnd >= aStart); + assert(mLines.size() > (size_t)(aEnd - aStart)); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first >= aStart ? i.first - 1 : i.first, i.second); + if (e.first >= aStart && e.first <= aEnd) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i >= aStart && i <= aEnd) + continue; + btmp.insert(i >= aStart ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aStart, mLines.begin() + aEnd); + assert(!mLines.empty()); + + mTextChanged = true; +} + +void TextEditor::RemoveLine(int aIndex) +{ + assert(!mReadOnly); + assert(mLines.size() > 1); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + { + ErrorMarkers::value_type e(i.first > aIndex ? i.first - 1 : i.first, i.second); + if (e.first - 1 == aIndex) + continue; + etmp.insert(e); + } + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + { + if (i == aIndex) + continue; + btmp.insert(i >= aIndex ? i - 1 : i); + } + mBreakpoints = std::move(btmp); + + mLines.erase(mLines.begin() + aIndex); + assert(!mLines.empty()); + + mTextChanged = true; +} + +TextEditor::Line& TextEditor::InsertLine(int aIndex) +{ + assert(!mReadOnly); + + auto& result = *mLines.insert(mLines.begin() + aIndex, Line()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first >= aIndex ? i.first + 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + Breakpoints btmp; + for (auto i : mBreakpoints) + btmp.insert(i >= aIndex ? i + 1 : i); + mBreakpoints = std::move(btmp); + + return result; +} + +std::string TextEditor::GetWordUnderCursor() const +{ + auto c = GetCursorPosition(); + return GetWordAt(c); +} + +std::string TextEditor::GetWordAt(const Coordinates & aCoords) const +{ + auto start = FindWordStart(aCoords); + auto end = FindWordEnd(aCoords); + + std::string r; + + auto istart = GetCharacterIndex(start); + auto iend = GetCharacterIndex(end); + + for (auto it = istart; it < iend; ++it) + r.push_back(mLines[aCoords.mLine][it].mChar); + + return r; +} + +ImU32 TextEditor::GetGlyphColor(const Glyph & aGlyph) const +{ + if (!mColorizerEnabled) + return mPalette[(int)PaletteIndex::Default]; + if (aGlyph.mComment) + return mPalette[(int)PaletteIndex::Comment]; + if (aGlyph.mMultiLineComment) + return mPalette[(int)PaletteIndex::MultiLineComment]; + auto const color = mPalette[(int)aGlyph.mColorIndex]; + if (aGlyph.mPreprocessor) + { + const auto ppcolor = mPalette[(int)PaletteIndex::Preprocessor]; + const int c0 = ((ppcolor & 0xff) + (color & 0xff)) / 2; + const int c1 = (((ppcolor >> 8) & 0xff) + ((color >> 8) & 0xff)) / 2; + const int c2 = (((ppcolor >> 16) & 0xff) + ((color >> 16) & 0xff)) / 2; + const int c3 = (((ppcolor >> 24) & 0xff) + ((color >> 24) & 0xff)) / 2; + return ImU32(c0 | (c1 << 8) | (c2 << 16) | (c3 << 24)); + } + return color; +} + +void TextEditor::HandleKeyboardInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowFocused()) + { + if (ImGui::IsWindowHovered()) + ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput); + //ImGui::CaptureKeyboardFromApp(true); + + io.WantCaptureKeyboard = true; + io.WantTextInput = true; + + if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Z)) + Undo(); + else if (!IsReadOnly() && !ctrl && !shift && alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Undo(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Y)) + Redo(); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_UpArrow)) + MoveUp(1, shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_DownArrow)) + MoveDown(1, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) + MoveLeft(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_RightArrow)) + MoveRight(1, shift, ctrl); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageUp)) + MoveUp(GetPageSize() - 4, shift); + else if (!alt && ImGui::IsKeyPressed(ImGuiKey_PageDown)) + MoveDown(GetPageSize() - 4, shift); + else if (!alt && ctrl && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveTop(shift); + else if (ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveBottom(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Home)) + MoveHome(shift); + else if (!ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_End)) + MoveEnd(shift); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Delete(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Backspace)) + Backspace(); + else if (!ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + mOverwrite ^= true; + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Copy(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_C)) + Copy(); + else if (!IsReadOnly() && !ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Insert)) + Paste(); + else if (!IsReadOnly() && ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_V)) + Paste(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_X)) + Cut(); + else if (!ctrl && shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Delete)) + Cut(); + else if (ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_A)) + SelectAll(); + else if (!IsReadOnly() && !ctrl && !shift && !alt && ImGui::IsKeyPressed(ImGuiKey_Enter)) + EnterCharacter('\n', false); + else if (!IsReadOnly() && !ctrl && !alt && ImGui::IsKeyPressed(ImGuiKey_Tab)) + EnterCharacter('\t', shift); + + if (!IsReadOnly() && !io.InputQueueCharacters.empty()) + { + for (int i = 0; i < io.InputQueueCharacters.Size; i++) + { + auto c = io.InputQueueCharacters[i]; + if (c != 0 && (c == '\n' || c >= 32)) + EnterCharacter(c, shift); + } + io.InputQueueCharacters.resize(0); + } + } +} + +void TextEditor::HandleMouseInputs() +{ + ImGuiIO& io = ImGui::GetIO(); + auto shift = io.KeyShift; + auto ctrl = io.ConfigMacOSXBehaviors ? io.KeySuper : io.KeyCtrl; + auto alt = io.ConfigMacOSXBehaviors ? io.KeyCtrl : io.KeyAlt; + + if (ImGui::IsWindowHovered()) + { + if (!shift && !alt) + { + auto click = ImGui::IsMouseClicked(0); + auto doubleClick = ImGui::IsMouseDoubleClicked(0); + auto t = ImGui::GetTime(); + auto tripleClick = click && !doubleClick && (mLastClick != -1.0f && (t - mLastClick) < io.MouseDoubleClickTime); + + /* + Left mouse button triple click + */ + + if (tripleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + mSelectionMode = SelectionMode::Line; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = -1.0f; + } + + /* + Left mouse button double click + */ + + else if (doubleClick) + { + if (!ctrl) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (mSelectionMode == SelectionMode::Line) + mSelectionMode = SelectionMode::Normal; + else + mSelectionMode = SelectionMode::Word; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + + mLastClick = (float)ImGui::GetTime(); + } + + /* + Left mouse button click + */ + else if (click) + { + mState.mCursorPosition = mInteractiveStart = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + if (ctrl) + mSelectionMode = SelectionMode::Word; + else + mSelectionMode = SelectionMode::Normal; + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + + mLastClick = (float)ImGui::GetTime(); + } + // Mouse left button dragging (=> update selection) + else if (ImGui::IsMouseDragging(0) && ImGui::IsMouseDown(0)) + { + io.WantCaptureMouse = true; + mState.mCursorPosition = mInteractiveEnd = ScreenPosToCoordinates(ImGui::GetMousePos()); + SetSelection(mInteractiveStart, mInteractiveEnd, mSelectionMode); + } + } + } +} + +void TextEditor::Render() +{ + /* Compute mCharAdvance regarding to scaled font size (Ctrl + mouse wheel)*/ + const float fontSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, "#", nullptr, nullptr).x; + mCharAdvance = ImVec2(fontSize, ImGui::GetTextLineHeightWithSpacing() * mLineSpacing); + + /* Update palette with the current alpha from style */ + for (int i = 0; i < (int)PaletteIndex::Max; ++i) + { + auto color = ImGui::ColorConvertU32ToFloat4(mPaletteBase[i]); + color.w *= ImGui::GetStyle().Alpha; + mPalette[i] = ImGui::ColorConvertFloat4ToU32(color); + } + + assert(mLineBuffer.empty()); + + auto contentSize = ImGui::GetWindowContentRegionMax(); + auto drawList = ImGui::GetWindowDrawList(); + float longest(mTextStart); + + if (mScrollToTop) + { + mScrollToTop = false; + ImGui::SetScrollY(0.f); + } + + ImVec2 cursorScreenPos = ImGui::GetCursorScreenPos(); + auto scrollX = ImGui::GetScrollX(); + auto scrollY = ImGui::GetScrollY(); + + auto lineNo = (int)floor(scrollY / mCharAdvance.y); + auto globalLineMax = (int)mLines.size(); + auto lineMax = std::max(0, std::min((int)mLines.size() - 1, lineNo + (int)floor((scrollY + contentSize.y) / mCharAdvance.y))); + + // Deduce mTextStart by evaluating mLines size (global lineMax) plus two spaces as text width + char buf[16]; + snprintf(buf, 16, " %d ", globalLineMax); + mTextStart = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x + mLeftMargin; + + if (!mLines.empty()) + { + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + + while (lineNo <= lineMax) + { + ImVec2 lineStartScreenPos = ImVec2(cursorScreenPos.x, cursorScreenPos.y + lineNo * mCharAdvance.y); + ImVec2 textScreenPos = ImVec2(lineStartScreenPos.x + mTextStart, lineStartScreenPos.y); + + auto& line = mLines[lineNo]; + longest = std::max(mTextStart + TextDistanceToLineStart(Coordinates(lineNo, GetLineMaxColumn(lineNo))), longest); + auto columnNo = 0; + Coordinates lineStartCoord(lineNo, 0); + Coordinates lineEndCoord(lineNo, GetLineMaxColumn(lineNo)); + + // Draw selection for the current line + float sstart = -1.0f; + float ssend = -1.0f; + + assert(mState.mSelectionStart <= mState.mSelectionEnd); + if (mState.mSelectionStart <= lineEndCoord) + sstart = mState.mSelectionStart > lineStartCoord ? TextDistanceToLineStart(mState.mSelectionStart) : 0.0f; + if (mState.mSelectionEnd > lineStartCoord) + ssend = TextDistanceToLineStart(mState.mSelectionEnd < lineEndCoord ? mState.mSelectionEnd : lineEndCoord); + + if (mState.mSelectionEnd.mLine > lineNo) + ssend += mCharAdvance.x; + + if (sstart != -1 && ssend != -1 && sstart < ssend) + { + ImVec2 vstart(lineStartScreenPos.x + mTextStart + sstart, lineStartScreenPos.y); + ImVec2 vend(lineStartScreenPos.x + mTextStart + ssend, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(vstart, vend, mPalette[(int)PaletteIndex::Selection]); + } + + // Draw breakpoints + auto start = ImVec2(lineStartScreenPos.x + scrollX, lineStartScreenPos.y); + + if (mBreakpoints.count(lineNo + 1) != 0) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::Breakpoint]); + } + + // Draw error markers + auto errorIt = mErrorMarkers.find(lineNo + 1); + if (errorIt != mErrorMarkers.end()) + { + auto end = ImVec2(lineStartScreenPos.x + contentSize.x + 2.0f * scrollX, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)PaletteIndex::ErrorMarker]); + + if (ImGui::IsMouseHoveringRect(lineStartScreenPos, end)) + { + ImGui::BeginTooltip(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f)); + ImGui::Text("Error at line %d:", errorIt->first); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.2f, 1.0f)); + ImGui::Text("%s", errorIt->second.c_str()); + ImGui::PopStyleColor(); + ImGui::EndTooltip(); + } + } + + // Draw line number (right aligned) + snprintf(buf, 16, "%d ", lineNo + 1); + + auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; + drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], buf); + + if (mState.mCursorPosition.mLine == lineNo) + { + auto focused = ImGui::IsWindowFocused(); + + // Highlight the current line (where the cursor is) + if (!HasSelection()) + { + auto end = ImVec2(start.x + contentSize.x + scrollX, start.y + mCharAdvance.y); + drawList->AddRectFilled(start, end, mPalette[(int)(focused ? PaletteIndex::CurrentLineFill : PaletteIndex::CurrentLineFillInactive)]); + drawList->AddRect(start, end, mPalette[(int)PaletteIndex::CurrentLineEdge], 1.0f); + } + + // Render the cursor + if (focused) + { + auto timeEnd = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto elapsed = timeEnd - mStartTime; + if (elapsed > 400) + { + float width = 1.0f; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + float cx = TextDistanceToLineStart(mState.mCursorPosition); + + if (mOverwrite && cindex < (int)line.size()) + { + auto c = line[cindex].mChar; + if (c == '\t') + { + auto x = (1.0f + std::floor((1.0f + cx) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + width = x - cx; + } + else + { + char buf2[2]; + buf2[0] = line[cindex].mChar; + buf2[1] = '\0'; + width = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf2).x; + } + } + ImVec2 cstart(textScreenPos.x + cx, lineStartScreenPos.y); + ImVec2 cend(textScreenPos.x + cx + width, lineStartScreenPos.y + mCharAdvance.y); + drawList->AddRectFilled(cstart, cend, mPalette[(int)PaletteIndex::Cursor]); + if (elapsed > 800) + mStartTime = timeEnd; + } + } + } + + // Render colorized text + auto prevColor = line.empty() ? mPalette[(int)PaletteIndex::Default] : GetGlyphColor(line[0]); + ImVec2 bufferOffset; + + for (int i = 0; i < line.size();) + { + auto& glyph = line[i]; + auto color = GetGlyphColor(glyph); + + if ((color != prevColor || glyph.mChar == '\t' || glyph.mChar == ' ') && !mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + auto textSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, mLineBuffer.c_str(), nullptr, nullptr); + bufferOffset.x += textSize.x; + mLineBuffer.clear(); + } + prevColor = color; + + if (glyph.mChar == '\t') + { + auto oldX = bufferOffset.x; + bufferOffset.x = (1.0f + std::floor((1.0f + bufferOffset.x) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++i; + + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x1 = textScreenPos.x + oldX + 1.0f; + const auto x2 = textScreenPos.x + bufferOffset.x - 1.0f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + const ImVec2 p1(x1, y); + const ImVec2 p2(x2, y); + const ImVec2 p3(x2 - s * 0.2f, y - s * 0.2f); + const ImVec2 p4(x2 - s * 0.2f, y + s * 0.2f); + drawList->AddLine(p1, p2, 0x90909090); + drawList->AddLine(p2, p3, 0x90909090); + drawList->AddLine(p2, p4, 0x90909090); + } + } + else if (glyph.mChar == ' ') + { + if (mShowWhitespaces) + { + const auto s = ImGui::GetFontSize(); + const auto x = textScreenPos.x + bufferOffset.x + spaceSize * 0.5f; + const auto y = textScreenPos.y + bufferOffset.y + s * 0.5f; + drawList->AddCircleFilled(ImVec2(x, y), 1.5f, 0x80808080, 4); + } + bufferOffset.x += spaceSize; + i++; + } + else + { + auto l = UTF8CharLength(glyph.mChar); + while (l-- > 0) + mLineBuffer.push_back(line[i++].mChar); + } + ++columnNo; + } + + if (!mLineBuffer.empty()) + { + const ImVec2 newOffset(textScreenPos.x + bufferOffset.x, textScreenPos.y + bufferOffset.y); + drawList->AddText(newOffset, prevColor, mLineBuffer.c_str()); + mLineBuffer.clear(); + } + + ++lineNo; + } + + // Draw a tooltip on known identifiers/preprocessor symbols + if (ImGui::IsMousePosValid()) + { + auto id = GetWordAt(ScreenPosToCoordinates(ImGui::GetMousePos())); + if (!id.empty()) + { + auto it = mLanguageDefinition.mIdentifiers.find(id); + if (it != mLanguageDefinition.mIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(it->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + else + { + auto pi = mLanguageDefinition.mPreprocIdentifiers.find(id); + if (pi != mLanguageDefinition.mPreprocIdentifiers.end()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(pi->second.mDeclaration.c_str()); + ImGui::EndTooltip(); + } + } + } + } + } + + + ImGui::Dummy(ImVec2((longest + 2), mLines.size() * mCharAdvance.y)); + + if (mScrollToCursor) + { + EnsureCursorVisible(); + ImGui::SetWindowFocus(); + mScrollToCursor = false; + } +} + +void TextEditor::Render(const char* aTitle, const ImVec2& aSize, bool aBorder) +{ + mWithinRender = true; + mTextChanged = false; + mCursorPositionChanged = false; + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::ColorConvertU32ToFloat4(mPalette[(int)PaletteIndex::Background])); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (!mIgnoreImGuiChild) + ImGui::BeginChild(aTitle, aSize, aBorder, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NoMove); + + if (mHandleKeyboardInputs) + { + HandleKeyboardInputs(); + // ImGui::PushAllowKeyboardFocus removed in ImGui 1.89+ + } + + if (mHandleMouseInputs) + HandleMouseInputs(); + + ColorizeInternal(); + Render(); + + if (mHandleKeyboardInputs) + // ImGui::PopAllowKeyboardFocus removed in ImGui 1.89+ + + if (!mIgnoreImGuiChild) + ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + + mWithinRender = false; +} + +void TextEditor::SetText(const std::string & aText) +{ + mLines.clear(); + mLines.emplace_back(Line()); + for (auto chr : aText) + { + if (chr == '\r') + { + // ignore the carriage return character + } + else if (chr == '\n') + mLines.emplace_back(Line()); + else + { + mLines.back().emplace_back(Glyph(chr, PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::SetTextLines(const std::vector & aLines) +{ + mLines.clear(); + + if (aLines.empty()) + { + mLines.emplace_back(Line()); + } + else + { + mLines.resize(aLines.size()); + + for (size_t i = 0; i < aLines.size(); ++i) + { + const std::string & aLine = aLines[i]; + + mLines[i].reserve(aLine.size()); + for (size_t j = 0; j < aLine.size(); ++j) + mLines[i].emplace_back(Glyph(aLine[j], PaletteIndex::Default)); + } + } + + mTextChanged = true; + mScrollToTop = true; + + mUndoBuffer.clear(); + mUndoIndex = 0; + + Colorize(); +} + +void TextEditor::EnterCharacter(ImWchar aChar, bool aShift) +{ + assert(!mReadOnly); + + UndoRecord u; + + u.mBefore = mState; + + if (HasSelection()) + { + if (aChar == '\t' && mState.mSelectionStart.mLine != mState.mSelectionEnd.mLine) + { + + auto start = mState.mSelectionStart; + auto end = mState.mSelectionEnd; + auto originalEnd = end; + + if (start > end) + std::swap(start, end); + start.mColumn = 0; + // end.mColumn = end.mLine < mLines.size() ? mLines[end.mLine].size() : 0; + if (end.mColumn == 0 && end.mLine > 0) + --end.mLine; + if (end.mLine >= (int)mLines.size()) + end.mLine = mLines.empty() ? 0 : (int)mLines.size() - 1; + end.mColumn = GetLineMaxColumn(end.mLine); + + //if (end.mColumn >= GetLineMaxColumn(end.mLine)) + // end.mColumn = GetLineMaxColumn(end.mLine) - 1; + + u.mRemovedStart = start; + u.mRemovedEnd = end; + u.mRemoved = GetText(start, end); + + bool modified = false; + + for (int i = start.mLine; i <= end.mLine; i++) + { + auto& line = mLines[i]; + if (aShift) + { + if (!line.empty()) + { + if (line.front().mChar == '\t') + { + line.erase(line.begin()); + modified = true; + } + else + { + for (int j = 0; j < mTabSize && !line.empty() && line.front().mChar == ' '; j++) + { + line.erase(line.begin()); + modified = true; + } + } + } + } + else + { + line.insert(line.begin(), Glyph('\t', TextEditor::PaletteIndex::Background)); + modified = true; + } + } + + if (modified) + { + start = Coordinates(start.mLine, GetCharacterColumn(start.mLine, 0)); + Coordinates rangeEnd; + if (originalEnd.mColumn != 0) + { + end = Coordinates(end.mLine, GetLineMaxColumn(end.mLine)); + rangeEnd = end; + u.mAdded = GetText(start, end); + } + else + { + end = Coordinates(originalEnd.mLine, 0); + rangeEnd = Coordinates(end.mLine - 1, GetLineMaxColumn(end.mLine - 1)); + u.mAdded = GetText(start, rangeEnd); + } + + u.mAddedStart = start; + u.mAddedEnd = rangeEnd; + u.mAfter = mState; + + mState.mSelectionStart = start; + mState.mSelectionEnd = end; + AddUndo(u); + + mTextChanged = true; + + EnsureCursorVisible(); + } + + return; + } // c == '\t' + else + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + } // HasSelection + + auto coord = GetActualCursorCoordinates(); + u.mAddedStart = coord; + + assert(!mLines.empty()); + + if (aChar == '\n') + { + InsertLine(coord.mLine + 1); + auto& line = mLines[coord.mLine]; + auto& newLine = mLines[coord.mLine + 1]; + + if (mLanguageDefinition.mAutoIndentation) + for (size_t it = 0; it < line.size() && isascii(line[it].mChar) && isblank(line[it].mChar); ++it) + newLine.push_back(line[it]); + + const size_t whitespaceSize = newLine.size(); + auto cindex = GetCharacterIndex(coord); + newLine.insert(newLine.end(), line.begin() + cindex, line.end()); + line.erase(line.begin() + cindex, line.begin() + line.size()); + SetCursorPosition(Coordinates(coord.mLine + 1, GetCharacterColumn(coord.mLine + 1, (int)whitespaceSize))); + u.mAdded = (char)aChar; + } + else + { + char buf[7]; + int e = ImTextCharToUtf8(buf, 7, aChar); + if (e > 0) + { + buf[e] = '\0'; + auto& line = mLines[coord.mLine]; + auto cindex = GetCharacterIndex(coord); + + if (mOverwrite && cindex < (int)line.size()) + { + auto d = UTF8CharLength(line[cindex].mChar); + + u.mRemovedStart = mState.mCursorPosition; + u.mRemovedEnd = Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex + d)); + + while (d-- > 0 && cindex < (int)line.size()) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + for (auto p = buf; *p != '\0'; p++, ++cindex) + line.insert(line.begin() + cindex, Glyph(*p, PaletteIndex::Default)); + u.mAdded = buf; + + SetCursorPosition(Coordinates(coord.mLine, GetCharacterColumn(coord.mLine, cindex))); + } + else + return; + } + + mTextChanged = true; + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + + AddUndo(u); + + Colorize(coord.mLine - 1, 3); + EnsureCursorVisible(); +} + +void TextEditor::SetReadOnly(bool aValue) +{ + mReadOnly = aValue; +} + +void TextEditor::SetColorizerEnable(bool aValue) +{ + mColorizerEnabled = aValue; +} + +void TextEditor::SetCursorPosition(const Coordinates & aPosition) +{ + if (mState.mCursorPosition != aPosition) + { + mState.mCursorPosition = aPosition; + mCursorPositionChanged = true; + EnsureCursorVisible(); + } +} + +void TextEditor::SetSelectionStart(const Coordinates & aPosition) +{ + mState.mSelectionStart = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelectionEnd(const Coordinates & aPosition) +{ + mState.mSelectionEnd = SanitizeCoordinates(aPosition); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); +} + +void TextEditor::SetSelection(const Coordinates & aStart, const Coordinates & aEnd, SelectionMode aMode) +{ + auto oldSelStart = mState.mSelectionStart; + auto oldSelEnd = mState.mSelectionEnd; + + mState.mSelectionStart = SanitizeCoordinates(aStart); + mState.mSelectionEnd = SanitizeCoordinates(aEnd); + if (mState.mSelectionStart > mState.mSelectionEnd) + std::swap(mState.mSelectionStart, mState.mSelectionEnd); + + switch (aMode) + { + case TextEditor::SelectionMode::Normal: + break; + case TextEditor::SelectionMode::Word: + { + mState.mSelectionStart = FindWordStart(mState.mSelectionStart); + if (!IsOnWordBoundary(mState.mSelectionEnd)) + mState.mSelectionEnd = FindWordEnd(FindWordStart(mState.mSelectionEnd)); + break; + } + case TextEditor::SelectionMode::Line: + { + const auto lineNo = mState.mSelectionEnd.mLine; + const auto lineSize = (size_t)lineNo < mLines.size() ? mLines[lineNo].size() : 0; + mState.mSelectionStart = Coordinates(mState.mSelectionStart.mLine, 0); + mState.mSelectionEnd = Coordinates(lineNo, GetLineMaxColumn(lineNo)); + break; + } + default: + break; + } + + if (mState.mSelectionStart != oldSelStart || + mState.mSelectionEnd != oldSelEnd) + mCursorPositionChanged = true; +} + +void TextEditor::SetTabSize(int aValue) +{ + mTabSize = std::max(0, std::min(32, aValue)); +} + +void TextEditor::InsertText(const std::string & aValue) +{ + InsertText(aValue.c_str()); +} + +void TextEditor::InsertText(const char * aValue) +{ + if (aValue == nullptr) + return; + + auto pos = GetActualCursorCoordinates(); + auto start = std::min(pos, mState.mSelectionStart); + int totalLines = pos.mLine - start.mLine; + + totalLines += InsertTextAt(pos, aValue); + + SetSelection(pos, pos); + SetCursorPosition(pos); + Colorize(start.mLine - 1, totalLines + 2); +} + +void TextEditor::DeleteSelection() +{ + assert(mState.mSelectionEnd >= mState.mSelectionStart); + + if (mState.mSelectionEnd == mState.mSelectionStart) + return; + + DeleteRange(mState.mSelectionStart, mState.mSelectionEnd); + + SetSelection(mState.mSelectionStart, mState.mSelectionStart); + SetCursorPosition(mState.mSelectionStart); + Colorize(mState.mSelectionStart.mLine, 1); +} + +void TextEditor::MoveUp(int aAmount, bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, mState.mCursorPosition.mLine - aAmount); + if (oldPos != mState.mCursorPosition) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +void TextEditor::MoveDown(int aAmount, bool aSelect) +{ + assert(mState.mCursorPosition.mColumn >= 0); + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + aAmount)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + + EnsureCursorVisible(); + } +} + +static bool IsUTFSequence(char c) +{ + return (c & 0xC0) == 0x80; +} + +void TextEditor::MoveLeft(int aAmount, bool aSelect, bool aWordMode) +{ + if (mLines.empty()) + return; + + auto oldPos = mState.mCursorPosition; + mState.mCursorPosition = GetActualCursorCoordinates(); + auto line = mState.mCursorPosition.mLine; + auto cindex = GetCharacterIndex(mState.mCursorPosition); + + while (aAmount-- > 0) + { + if (cindex == 0) + { + if (line > 0) + { + --line; + if ((int)mLines.size() > line) + cindex = (int)mLines[line].size(); + else + cindex = 0; + } + } + else + { + --cindex; + if (cindex > 0) + { + if ((int)mLines.size() > line) + { + while (cindex > 0 && IsUTFSequence(mLines[line][cindex].mChar)) + --cindex; + } + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + if (aWordMode) + { + mState.mCursorPosition = FindWordStart(mState.mCursorPosition); + cindex = GetCharacterIndex(mState.mCursorPosition); + } + } + + mState.mCursorPosition = Coordinates(line, GetCharacterColumn(line, cindex)); + + assert(mState.mCursorPosition.mColumn >= 0); + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveRight(int aAmount, bool aSelect, bool aWordMode) +{ + auto oldPos = mState.mCursorPosition; + + if (mLines.empty() || oldPos.mLine >= mLines.size()) + return; + + auto cindex = GetCharacterIndex(mState.mCursorPosition); + while (aAmount-- > 0) + { + auto lindex = mState.mCursorPosition.mLine; + auto& line = mLines[lindex]; + + if (cindex >= line.size()) + { + if (mState.mCursorPosition.mLine < mLines.size() - 1) + { + mState.mCursorPosition.mLine = std::max(0, std::min((int)mLines.size() - 1, mState.mCursorPosition.mLine + 1)); + mState.mCursorPosition.mColumn = 0; + } + else + return; + } + else + { + cindex += UTF8CharLength(line[cindex].mChar); + mState.mCursorPosition = Coordinates(lindex, GetCharacterColumn(lindex, cindex)); + if (aWordMode) + mState.mCursorPosition = FindNextWord(mState.mCursorPosition); + } + } + + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = SanitizeCoordinates(mState.mCursorPosition); + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd, aSelect && aWordMode ? SelectionMode::Word : SelectionMode::Normal); + + EnsureCursorVisible(); +} + +void TextEditor::MoveTop(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(0, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + mInteractiveEnd = oldPos; + mInteractiveStart = mState.mCursorPosition; + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::TextEditor::MoveBottom(bool aSelect) +{ + auto oldPos = GetCursorPosition(); + auto newPos = Coordinates((int)mLines.size() - 1, 0); + SetCursorPosition(newPos); + if (aSelect) + { + mInteractiveStart = oldPos; + mInteractiveEnd = newPos; + } + else + mInteractiveStart = mInteractiveEnd = newPos; + SetSelection(mInteractiveStart, mInteractiveEnd); +} + +void TextEditor::MoveHome(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, 0)); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else + { + mInteractiveStart = mState.mCursorPosition; + mInteractiveEnd = oldPos; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::MoveEnd(bool aSelect) +{ + auto oldPos = mState.mCursorPosition; + SetCursorPosition(Coordinates(mState.mCursorPosition.mLine, GetLineMaxColumn(oldPos.mLine))); + + if (mState.mCursorPosition != oldPos) + { + if (aSelect) + { + if (oldPos == mInteractiveEnd) + mInteractiveEnd = mState.mCursorPosition; + else if (oldPos == mInteractiveStart) + mInteractiveStart = mState.mCursorPosition; + else + { + mInteractiveStart = oldPos; + mInteractiveEnd = mState.mCursorPosition; + } + } + else + mInteractiveStart = mInteractiveEnd = mState.mCursorPosition; + SetSelection(mInteractiveStart, mInteractiveEnd); + } +} + +void TextEditor::Delete() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + auto& line = mLines[pos.mLine]; + + if (pos.mColumn == GetLineMaxColumn(pos.mLine)) + { + if (pos.mLine == (int)mLines.size() - 1) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + Advance(u.mRemovedEnd); + + auto& nextLine = mLines[pos.mLine + 1]; + line.insert(line.end(), nextLine.begin(), nextLine.end()); + RemoveLine(pos.mLine + 1); + } + else + { + auto cindex = GetCharacterIndex(pos); + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + u.mRemovedEnd.mColumn++; + u.mRemoved = GetText(u.mRemovedStart, u.mRemovedEnd); + + auto d = UTF8CharLength(line[cindex].mChar); + while (d-- > 0 && cindex < (int)line.size()) + line.erase(line.begin() + cindex); + } + + mTextChanged = true; + + Colorize(pos.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::Backspace() +{ + assert(!mReadOnly); + + if (mLines.empty()) + return; + + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + DeleteSelection(); + } + else + { + auto pos = GetActualCursorCoordinates(); + SetCursorPosition(pos); + + if (mState.mCursorPosition.mColumn == 0) + { + if (mState.mCursorPosition.mLine == 0) + return; + + u.mRemoved = '\n'; + u.mRemovedStart = u.mRemovedEnd = Coordinates(pos.mLine - 1, GetLineMaxColumn(pos.mLine - 1)); + Advance(u.mRemovedEnd); + + auto& line = mLines[mState.mCursorPosition.mLine]; + auto& prevLine = mLines[mState.mCursorPosition.mLine - 1]; + auto prevSize = GetLineMaxColumn(mState.mCursorPosition.mLine - 1); + prevLine.insert(prevLine.end(), line.begin(), line.end()); + + ErrorMarkers etmp; + for (auto& i : mErrorMarkers) + etmp.insert(ErrorMarkers::value_type(i.first - 1 == mState.mCursorPosition.mLine ? i.first - 1 : i.first, i.second)); + mErrorMarkers = std::move(etmp); + + RemoveLine(mState.mCursorPosition.mLine); + --mState.mCursorPosition.mLine; + mState.mCursorPosition.mColumn = prevSize; + } + else + { + auto& line = mLines[mState.mCursorPosition.mLine]; + auto cindex = GetCharacterIndex(pos) - 1; + auto cend = cindex + 1; + while (cindex > 0 && IsUTFSequence(line[cindex].mChar)) + --cindex; + + //if (cindex > 0 && UTF8CharLength(line[cindex].mChar) > 1) + // --cindex; + + u.mRemovedStart = u.mRemovedEnd = GetActualCursorCoordinates(); + --u.mRemovedStart.mColumn; + --mState.mCursorPosition.mColumn; + + while (cindex < line.size() && cend-- > cindex) + { + u.mRemoved += line[cindex].mChar; + line.erase(line.begin() + cindex); + } + } + + mTextChanged = true; + + EnsureCursorVisible(); + Colorize(mState.mCursorPosition.mLine, 1); + } + + u.mAfter = mState; + AddUndo(u); +} + +void TextEditor::SelectWordUnderCursor() +{ + auto c = GetCursorPosition(); + SetSelection(FindWordStart(c), FindWordEnd(c)); +} + +void TextEditor::SelectAll() +{ + SetSelection(Coordinates(0, 0), Coordinates((int)mLines.size(), 0)); +} + +bool TextEditor::HasSelection() const +{ + return mState.mSelectionEnd > mState.mSelectionStart; +} + +void TextEditor::Copy() +{ + if (HasSelection()) + { + ImGui::SetClipboardText(GetSelectedText().c_str()); + } + else + { + if (!mLines.empty()) + { + std::string str; + auto& line = mLines[GetActualCursorCoordinates().mLine]; + for (auto& g : line) + str.push_back(g.mChar); + ImGui::SetClipboardText(str.c_str()); + } + } +} + +void TextEditor::Cut() +{ + if (IsReadOnly()) + { + Copy(); + } + else + { + if (HasSelection()) + { + UndoRecord u; + u.mBefore = mState; + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + + Copy(); + DeleteSelection(); + + u.mAfter = mState; + AddUndo(u); + } + } +} + +void TextEditor::Paste() +{ + if (IsReadOnly()) + return; + + auto clipText = ImGui::GetClipboardText(); + if (clipText != nullptr && strlen(clipText) > 0) + { + UndoRecord u; + u.mBefore = mState; + + if (HasSelection()) + { + u.mRemoved = GetSelectedText(); + u.mRemovedStart = mState.mSelectionStart; + u.mRemovedEnd = mState.mSelectionEnd; + DeleteSelection(); + } + + u.mAdded = clipText; + u.mAddedStart = GetActualCursorCoordinates(); + + InsertText(clipText); + + u.mAddedEnd = GetActualCursorCoordinates(); + u.mAfter = mState; + AddUndo(u); + } +} + +bool TextEditor::CanUndo() const +{ + return !mReadOnly && mUndoIndex > 0; +} + +bool TextEditor::CanRedo() const +{ + return !mReadOnly && mUndoIndex < (int)mUndoBuffer.size(); +} + +void TextEditor::Undo(int aSteps) +{ + while (CanUndo() && aSteps-- > 0) + mUndoBuffer[--mUndoIndex].Undo(this); +} + +void TextEditor::Redo(int aSteps) +{ + while (CanRedo() && aSteps-- > 0) + mUndoBuffer[mUndoIndex++].Redo(this); +} + +const TextEditor::Palette & TextEditor::GetDarkPalette() +{ + const static Palette p = { { + 0xff7f7f7f, // Default + 0xffd69c56, // Keyword + 0xff00ff00, // Number + 0xff7070e0, // String + 0xff70a0e0, // Char literal + 0xffffffff, // Punctuation + 0xff408080, // Preprocessor + 0xffaaaaaa, // Identifier + 0xff9bc64d, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff206020, // Comment (single line) + 0xff406020, // Comment (multi line) + 0xff101010, // Background + 0xffe0e0e0, // Cursor + 0x80a06020, // Selection + 0x800020ff, // ErrorMarker + 0x40f08000, // Breakpoint + 0xff707000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40a0a0a0, // Current line edge + } }; + return p; +} + +const TextEditor::Palette & TextEditor::GetLightPalette() +{ + const static Palette p = { { + 0xff7f7f7f, // None + 0xffff0c06, // Keyword + 0xff008000, // Number + 0xff2020a0, // String + 0xff304070, // Char literal + 0xff000000, // Punctuation + 0xff406060, // Preprocessor + 0xff404040, // Identifier + 0xff606010, // Known identifier + 0xffc040a0, // Preproc identifier + 0xff205020, // Comment (single line) + 0xff405020, // Comment (multi line) + 0xffffffff, // Background + 0xff000000, // Cursor + 0x80600000, // Selection + 0xa00010ff, // ErrorMarker + 0x80f08000, // Breakpoint + 0xff505000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + } }; + return p; +} + +const TextEditor::Palette & TextEditor::GetRetroBluePalette() +{ + const static Palette p = { { + 0xff00ffff, // None + 0xffffff00, // Keyword + 0xff00ff00, // Number + 0xff808000, // String + 0xff808000, // Char literal + 0xffffffff, // Punctuation + 0xff008000, // Preprocessor + 0xff00ffff, // Identifier + 0xffffffff, // Known identifier + 0xffff00ff, // Preproc identifier + 0xff808080, // Comment (single line) + 0xff404040, // Comment (multi line) + 0xff800000, // Background + 0xff0080ff, // Cursor + 0x80ffff00, // Selection + 0xa00000ff, // ErrorMarker + 0x80ff8000, // Breakpoint + 0xff808000, // Line number + 0x40000000, // Current line fill + 0x40808080, // Current line fill (inactive) + 0x40000000, // Current line edge + } }; + return p; +} + + +std::string TextEditor::GetText() const +{ + return GetText(Coordinates(), Coordinates((int)mLines.size(), 0)); +} + +std::vector TextEditor::GetTextLines() const +{ + std::vector result; + + result.reserve(mLines.size()); + + for (auto & line : mLines) + { + std::string text; + + text.resize(line.size()); + + for (size_t i = 0; i < line.size(); ++i) + text[i] = line[i].mChar; + + result.emplace_back(std::move(text)); + } + + return result; +} + +std::string TextEditor::GetSelectedText() const +{ + return GetText(mState.mSelectionStart, mState.mSelectionEnd); +} + +std::string TextEditor::GetCurrentLineText()const +{ + auto lineLength = GetLineMaxColumn(mState.mCursorPosition.mLine); + return GetText( + Coordinates(mState.mCursorPosition.mLine, 0), + Coordinates(mState.mCursorPosition.mLine, lineLength)); +} + +void TextEditor::ProcessInputs() +{ +} + +void TextEditor::Colorize(int aFromLine, int aLines) +{ + int toLine = aLines == -1 ? (int)mLines.size() : std::min((int)mLines.size(), aFromLine + aLines); + mColorRangeMin = std::min(mColorRangeMin, aFromLine); + mColorRangeMax = std::max(mColorRangeMax, toLine); + mColorRangeMin = std::max(0, mColorRangeMin); + mColorRangeMax = std::max(mColorRangeMin, mColorRangeMax); + mCheckComments = true; +} + +void TextEditor::ColorizeRange(int aFromLine, int aToLine) +{ + if (mLines.empty() || aFromLine >= aToLine) + return; + + std::string buffer; + std::cmatch results; + std::string id; + + int endLine = std::max(0, std::min((int)mLines.size(), aToLine)); + for (int i = aFromLine; i < endLine; ++i) + { + auto& line = mLines[i]; + + if (line.empty()) + continue; + + buffer.resize(line.size()); + for (size_t j = 0; j < line.size(); ++j) + { + auto& col = line[j]; + buffer[j] = col.mChar; + col.mColorIndex = PaletteIndex::Default; + } + + const char * bufferBegin = &buffer.front(); + const char * bufferEnd = bufferBegin + buffer.size(); + + auto last = bufferEnd; + + for (auto first = bufferBegin; first != last; ) + { + const char * token_begin = nullptr; + const char * token_end = nullptr; + PaletteIndex token_color = PaletteIndex::Default; + + bool hasTokenizeResult = false; + + if (mLanguageDefinition.mTokenize != nullptr) + { + if (mLanguageDefinition.mTokenize(first, last, token_begin, token_end, token_color)) + hasTokenizeResult = true; + } + + if (hasTokenizeResult == false) + { + // todo : remove + //printf("using regex for %.*s\n", first + 10 < last ? 10 : int(last - first), first); + + for (auto& p : mRegexList) + { + if (std::regex_search(first, last, results, p.first, std::regex_constants::match_continuous)) + { + hasTokenizeResult = true; + + auto& v = *results.begin(); + token_begin = v.first; + token_end = v.second; + token_color = p.second; + break; + } + } + } + + if (hasTokenizeResult == false) + { + first++; + } + else + { + const size_t token_length = token_end - token_begin; + + if (token_color == PaletteIndex::Identifier) + { + id.assign(token_begin, token_end); + + // todo : allmost all language definitions use lower case to specify keywords, so shouldn't this use ::tolower ? + if (!mLanguageDefinition.mCaseSensitive) + std::transform(id.begin(), id.end(), id.begin(), ::toupper); + + if (!line[first - bufferBegin].mPreprocessor) + { + if (mLanguageDefinition.mKeywords.count(id) != 0) + token_color = PaletteIndex::Keyword; + else if (mLanguageDefinition.mIdentifiers.count(id) != 0) + token_color = PaletteIndex::KnownIdentifier; + else if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + else + { + if (mLanguageDefinition.mPreprocIdentifiers.count(id) != 0) + token_color = PaletteIndex::PreprocIdentifier; + } + } + + for (size_t j = 0; j < token_length; ++j) + line[(token_begin - bufferBegin) + j].mColorIndex = token_color; + + first = token_end; + } + } + } +} + +void TextEditor::ColorizeInternal() +{ + if (mLines.empty() || !mColorizerEnabled) + return; + + if (mCheckComments) + { + auto endLine = mLines.size(); + auto endIndex = 0; + auto commentStartLine = endLine; + auto commentStartIndex = endIndex; + auto withinString = false; + auto withinSingleLineComment = false; + auto withinPreproc = false; + auto firstChar = true; // there is no other non-whitespace characters in the line before + auto concatenate = false; // '\' on the very end of the line + auto currentLine = 0; + auto currentIndex = 0; + while (currentLine < endLine || currentIndex < endIndex) + { + auto& line = mLines[currentLine]; + + if (currentIndex == 0 && !concatenate) + { + withinSingleLineComment = false; + withinPreproc = false; + firstChar = true; + } + + concatenate = false; + + if (!line.empty()) + { + auto& g = line[currentIndex]; + auto c = g.mChar; + + if (c != mLanguageDefinition.mPreprocChar && !isspace(c)) + firstChar = false; + + if (currentIndex == (int)line.size() - 1 && line[line.size() - 1].mChar == '\\') + concatenate = true; + + bool inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + if (withinString) + { + line[currentIndex].mMultiLineComment = inComment; + + if (c == '\"') + { + if (currentIndex + 1 < (int)line.size() && line[currentIndex + 1].mChar == '\"') + { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + else + withinString = false; + } + else if (c == '\\') + { + currentIndex += 1; + if (currentIndex < (int)line.size()) + line[currentIndex].mMultiLineComment = inComment; + } + } + else + { + if (firstChar && c == mLanguageDefinition.mPreprocChar) + withinPreproc = true; + + if (c == '\"') + { + withinString = true; + line[currentIndex].mMultiLineComment = inComment; + } + else + { + auto pred = [](const char& a, const Glyph& b) { return a == b.mChar; }; + auto from = line.begin() + currentIndex; + auto& startStr = mLanguageDefinition.mCommentStart; + auto& singleStartStr = mLanguageDefinition.mSingleLineComment; + + if (singleStartStr.size() > 0 && + currentIndex + singleStartStr.size() <= line.size() && + equals(singleStartStr.begin(), singleStartStr.end(), from, from + singleStartStr.size(), pred)) + { + withinSingleLineComment = true; + } + else if (!withinSingleLineComment && currentIndex + startStr.size() <= line.size() && + equals(startStr.begin(), startStr.end(), from, from + startStr.size(), pred)) + { + commentStartLine = currentLine; + commentStartIndex = currentIndex; + } + + inComment = inComment = (commentStartLine < currentLine || (commentStartLine == currentLine && commentStartIndex <= currentIndex)); + + line[currentIndex].mMultiLineComment = inComment; + line[currentIndex].mComment = withinSingleLineComment; + + auto& endStr = mLanguageDefinition.mCommentEnd; + if (currentIndex + 1 >= (int)endStr.size() && + equals(endStr.begin(), endStr.end(), from + 1 - endStr.size(), from + 1, pred)) + { + commentStartIndex = endIndex; + commentStartLine = endLine; + } + } + } + line[currentIndex].mPreprocessor = withinPreproc; + currentIndex += UTF8CharLength(c); + if (currentIndex >= (int)line.size()) + { + currentIndex = 0; + ++currentLine; + } + } + else + { + currentIndex = 0; + ++currentLine; + } + } + mCheckComments = false; + } + + if (mColorRangeMin < mColorRangeMax) + { + const int increment = (mLanguageDefinition.mTokenize == nullptr) ? 10 : 10000; + const int to = std::min(mColorRangeMin + increment, mColorRangeMax); + ColorizeRange(mColorRangeMin, to); + mColorRangeMin = to; + + if (mColorRangeMax == mColorRangeMin) + { + mColorRangeMin = std::numeric_limits::max(); + mColorRangeMax = 0; + } + return; + } +} + +float TextEditor::TextDistanceToLineStart(const Coordinates& aFrom) const +{ + auto& line = mLines[aFrom.mLine]; + float distance = 0.0f; + float spaceSize = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, " ", nullptr, nullptr).x; + int colIndex = GetCharacterIndex(aFrom); + for (size_t it = 0u; it < line.size() && it < colIndex; ) + { + if (line[it].mChar == '\t') + { + distance = (1.0f + std::floor((1.0f + distance) / (float(mTabSize) * spaceSize))) * (float(mTabSize) * spaceSize); + ++it; + } + else + { + auto d = UTF8CharLength(line[it].mChar); + char tempCString[7]; + int i = 0; + for (; i < 6 && d-- > 0 && it < (int)line.size(); i++, it++) + tempCString[i] = line[it].mChar; + + tempCString[i] = '\0'; + distance += ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, tempCString, nullptr, nullptr).x; + } + } + + return distance; +} + +void TextEditor::EnsureCursorVisible() +{ + if (!mWithinRender) + { + mScrollToCursor = true; + return; + } + + float scrollX = ImGui::GetScrollX(); + float scrollY = ImGui::GetScrollY(); + + auto height = ImGui::GetWindowHeight(); + auto width = ImGui::GetWindowWidth(); + + auto top = 1 + (int)ceil(scrollY / mCharAdvance.y); + auto bottom = (int)ceil((scrollY + height) / mCharAdvance.y); + + auto left = (int)ceil(scrollX / mCharAdvance.x); + auto right = (int)ceil((scrollX + width) / mCharAdvance.x); + + auto pos = GetActualCursorCoordinates(); + auto len = TextDistanceToLineStart(pos); + + if (pos.mLine < top) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine - 1) * mCharAdvance.y)); + if (pos.mLine > bottom - 4) + ImGui::SetScrollY(std::max(0.0f, (pos.mLine + 4) * mCharAdvance.y - height)); + if (len + mTextStart < left + 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart - 4)); + if (len + mTextStart > right - 4) + ImGui::SetScrollX(std::max(0.0f, len + mTextStart + 4 - width)); +} + +int TextEditor::GetPageSize() const +{ + auto height = ImGui::GetWindowHeight() - 20.0f; + return (int)floor(height / mCharAdvance.y); +} + +TextEditor::UndoRecord::UndoRecord( + const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter) + : mAdded(aAdded) + , mAddedStart(aAddedStart) + , mAddedEnd(aAddedEnd) + , mRemoved(aRemoved) + , mRemovedStart(aRemovedStart) + , mRemovedEnd(aRemovedEnd) + , mBefore(aBefore) + , mAfter(aAfter) +{ + assert(mAddedStart <= mAddedEnd); + assert(mRemovedStart <= mRemovedEnd); +} + +void TextEditor::UndoRecord::Undo(TextEditor * aEditor) +{ + if (!mAdded.empty()) + { + aEditor->DeleteRange(mAddedStart, mAddedEnd); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 2); + } + + if (!mRemoved.empty()) + { + auto start = mRemovedStart; + aEditor->InsertTextAt(start, mRemoved.c_str()); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 2); + } + + aEditor->mState = mBefore; + aEditor->EnsureCursorVisible(); + +} + +void TextEditor::UndoRecord::Redo(TextEditor * aEditor) +{ + if (!mRemoved.empty()) + { + aEditor->DeleteRange(mRemovedStart, mRemovedEnd); + aEditor->Colorize(mRemovedStart.mLine - 1, mRemovedEnd.mLine - mRemovedStart.mLine + 1); + } + + if (!mAdded.empty()) + { + auto start = mAddedStart; + aEditor->InsertTextAt(start, mAdded.c_str()); + aEditor->Colorize(mAddedStart.mLine - 1, mAddedEnd.mLine - mAddedStart.mLine + 1); + } + + aEditor->mState = mAfter; + aEditor->EnsureCursorVisible(); +} + +static bool TokenizeCStyleString(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if (*p == '"') + { + p++; + + while (p < in_end) + { + // handle end of string + if (*p == '"') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + + // handle escape character for " + if (*p == '\\' && p + 1 < in_end && p[1] == '"') + p++; + + p++; + } + } + + return false; +} + +static bool TokenizeCStyleCharacterLiteral(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if (*p == '\'') + { + p++; + + // handle escape characters + if (p < in_end && *p == '\\') + p++; + + if (p < in_end) + p++; + + // handle end of character literal + if (p < in_end && *p == '\'') + { + out_begin = in_begin; + out_end = p + 1; + return true; + } + } + + return false; +} + +static bool TokenizeCStyleIdentifier(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || *p == '_') + { + p++; + + while ((p < in_end) && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_')) + p++; + + out_begin = in_begin; + out_end = p; + return true; + } + + return false; +} + +static bool TokenizeCStyleNumber(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + const char * p = in_begin; + + const bool startsWithNumber = *p >= '0' && *p <= '9'; + + if (*p != '+' && *p != '-' && !startsWithNumber) + return false; + + p++; + + bool hasNumber = startsWithNumber; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasNumber = true; + + p++; + } + + if (hasNumber == false) + return false; + + bool isFloat = false; + bool isHex = false; + bool isBinary = false; + + if (p < in_end) + { + if (*p == '.') + { + isFloat = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '9')) + p++; + } + else if (*p == 'x' || *p == 'X') + { + // hex formatted integer of the type 0xef80 + + isHex = true; + + p++; + + while (p < in_end && ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F'))) + p++; + } + else if (*p == 'b' || *p == 'B') + { + // binary formatted integer of the type 0b01011101 + + isBinary = true; + + p++; + + while (p < in_end && (*p >= '0' && *p <= '1')) + p++; + } + } + + if (isHex == false && isBinary == false) + { + // floating point exponent + if (p < in_end && (*p == 'e' || *p == 'E')) + { + isFloat = true; + + p++; + + if (p < in_end && (*p == '+' || *p == '-')) + p++; + + bool hasDigits = false; + + while (p < in_end && (*p >= '0' && *p <= '9')) + { + hasDigits = true; + + p++; + } + + if (hasDigits == false) + return false; + } + + // single precision floating point type + if (p < in_end && *p == 'f') + p++; + } + + if (isFloat == false) + { + // integer size type + while (p < in_end && (*p == 'u' || *p == 'U' || *p == 'l' || *p == 'L')) + p++; + } + + out_begin = in_begin; + out_end = p; + return true; +} + +static bool TokenizeCStylePunctuation(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end) +{ + (void)in_end; + + switch (*in_begin) + { + case '[': + case ']': + case '{': + case '}': + case '!': + case '%': + case '^': + case '&': + case '*': + case '(': + case ')': + case '-': + case '+': + case '=': + case '~': + case '|': + case '<': + case '>': + case '?': + case ':': + case '/': + case ';': + case ',': + case '.': + out_begin = in_begin; + out_end = in_begin + 1; + return true; + } + + return false; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::CPlusPlus() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const cppKeywords[] = { + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", "atomic_noexcept", "auto", "bitand", "bitor", "bool", "break", "case", "catch", "char", "char16_t", "char32_t", "class", + "compl", "concept", "const", "constexpr", "const_cast", "continue", "decltype", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", "explicit", "export", "extern", "false", "float", + "for", "friend", "goto", "if", "import", "inline", "int", "long", "module", "mutable", "namespace", "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", "protected", "public", + "register", "reinterpret_cast", "requires", "return", "short", "signed", "sizeof", "static", "static_assert", "static_cast", "struct", "switch", "synchronized", "template", "this", "thread_local", + "throw", "true", "try", "typedef", "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + }; + for (auto& k : cppKeywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "printf", "sprintf", "snprintf", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper", + "std", "string", "vector", "map", "unordered_map", "set", "unordered_set", "min", "max" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C++"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::HLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "AppendStructuredBuffer", "asm", "asm_fragment", "BlendState", "bool", "break", "Buffer", "ByteAddressBuffer", "case", "cbuffer", "centroid", "class", "column_major", "compile", "compile_fragment", + "CompileShader", "const", "continue", "ComputeShader", "ConsumeStructuredBuffer", "default", "DepthStencilState", "DepthStencilView", "discard", "do", "double", "DomainShader", "dword", "else", + "export", "extern", "false", "float", "for", "fxgroup", "GeometryShader", "groupshared", "half", "Hullshader", "if", "in", "inline", "inout", "InputPatch", "int", "interface", "line", "lineadj", + "linear", "LineStream", "matrix", "min16float", "min10float", "min16int", "min12int", "min16uint", "namespace", "nointerpolation", "noperspective", "NULL", "out", "OutputPatch", "packoffset", + "pass", "pixelfragment", "PixelShader", "point", "PointStream", "precise", "RasterizerState", "RenderTargetView", "return", "register", "row_major", "RWBuffer", "RWByteAddressBuffer", "RWStructuredBuffer", + "RWTexture1D", "RWTexture1DArray", "RWTexture2D", "RWTexture2DArray", "RWTexture3D", "sample", "sampler", "SamplerState", "SamplerComparisonState", "shared", "snorm", "stateblock", "stateblock_state", + "static", "string", "struct", "switch", "StructuredBuffer", "tbuffer", "technique", "technique10", "technique11", "texture", "Texture1D", "Texture1DArray", "Texture2D", "Texture2DArray", "Texture2DMS", + "Texture2DMSArray", "Texture3D", "TextureCube", "TextureCubeArray", "true", "typedef", "triangle", "triangleadj", "TriangleStream", "uint", "uniform", "unorm", "unsigned", "vector", "vertexfragment", + "VertexShader", "void", "volatile", "while", + "bool1","bool2","bool3","bool4","double1","double2","double3","double4", "float1", "float2", "float3", "float4", "int1", "int2", "int3", "int4", "in", "out", "inout", + "uint1", "uint2", "uint3", "uint4", "dword1", "dword2", "dword3", "dword4", "half1", "half2", "half3", "half4", + "float1x1","float2x1","float3x1","float4x1","float1x2","float2x2","float3x2","float4x2", + "float1x3","float2x3","float3x3","float4x3","float1x4","float2x4","float3x4","float4x4", + "half1x1","half2x1","half3x1","half4x1","half1x2","half2x2","half3x2","half4x2", + "half1x3","half2x3","half3x3","half4x3","half1x4","half2x4","half3x4","half4x4", + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "all", "AllMemoryBarrier", "AllMemoryBarrierWithGroupSync", "any", "asdouble", "asfloat", "asin", "asint", "asint", "asuint", + "asuint", "atan", "atan2", "ceil", "CheckAccessFullyMapped", "clamp", "clip", "cos", "cosh", "countbits", "cross", "D3DCOLORtoUBYTE4", "ddx", + "ddx_coarse", "ddx_fine", "ddy", "ddy_coarse", "ddy_fine", "degrees", "determinant", "DeviceMemoryBarrier", "DeviceMemoryBarrierWithGroupSync", + "distance", "dot", "dst", "errorf", "EvaluateAttributeAtCentroid", "EvaluateAttributeAtSample", "EvaluateAttributeSnapped", "exp", "exp2", + "f16tof32", "f32tof16", "faceforward", "firstbithigh", "firstbitlow", "floor", "fma", "fmod", "frac", "frexp", "fwidth", "GetRenderTargetSampleCount", + "GetRenderTargetSamplePosition", "GroupMemoryBarrier", "GroupMemoryBarrierWithGroupSync", "InterlockedAdd", "InterlockedAnd", "InterlockedCompareExchange", + "InterlockedCompareStore", "InterlockedExchange", "InterlockedMax", "InterlockedMin", "InterlockedOr", "InterlockedXor", "isfinite", "isinf", "isnan", + "ldexp", "length", "lerp", "lit", "log", "log10", "log2", "mad", "max", "min", "modf", "msad4", "mul", "noise", "normalize", "pow", "printf", + "Process2DQuadTessFactorsAvg", "Process2DQuadTessFactorsMax", "Process2DQuadTessFactorsMin", "ProcessIsolineTessFactors", "ProcessQuadTessFactorsAvg", + "ProcessQuadTessFactorsMax", "ProcessQuadTessFactorsMin", "ProcessTriTessFactorsAvg", "ProcessTriTessFactorsMax", "ProcessTriTessFactorsMin", + "radians", "rcp", "reflect", "refract", "reversebits", "round", "rsqrt", "saturate", "sign", "sin", "sincos", "sinh", "smoothstep", "sqrt", "step", + "tan", "tanh", "tex1D", "tex1D", "tex1Dbias", "tex1Dgrad", "tex1Dlod", "tex1Dproj", "tex2D", "tex2D", "tex2Dbias", "tex2Dgrad", "tex2Dlod", "tex2Dproj", + "tex3D", "tex3D", "tex3Dbias", "tex3Dgrad", "tex3Dlod", "tex3Dproj", "texCUBE", "texCUBE", "texCUBEbias", "texCUBEgrad", "texCUBElod", "texCUBEproj", "transpose", "trunc" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "HLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::GLSL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("[ \\t]*#[ \\t]*[a-zA-Z_]+", PaletteIndex::Preprocessor)); + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::CharLiteral)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "GLSL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::C() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", "enum", "extern", "float", "for", "goto", "if", "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", "_Alignas", "_Alignof", "_Atomic", "_Bool", "_Complex", "_Generic", "_Imaginary", + "_Noreturn", "_Static_assert", "_Thread_local" + }; + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "abort", "abs", "acos", "asin", "atan", "atexit", "atof", "atoi", "atol", "ceil", "clock", "cosh", "ctime", "div", "exit", "fabs", "floor", "fmod", "getchar", "getenv", "isalnum", "isalpha", "isdigit", "isgraph", + "ispunct", "isspace", "isupper", "kbhit", "log10", "log2", "log", "memcmp", "modf", "pow", "putchar", "putenv", "puts", "rand", "remove", "rename", "sinh", "sqrt", "srand", "strcat", "strcmp", "strerror", "time", "tolower", "toupper" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenize = [](const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex) -> bool + { + paletteIndex = PaletteIndex::Max; + + while (in_begin < in_end && isascii(*in_begin) && isblank(*in_begin)) + in_begin++; + + if (in_begin == in_end) + { + out_begin = in_end; + out_end = in_end; + paletteIndex = PaletteIndex::Default; + } + else if (TokenizeCStyleString(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::String; + else if (TokenizeCStyleCharacterLiteral(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::CharLiteral; + else if (TokenizeCStyleIdentifier(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Identifier; + else if (TokenizeCStyleNumber(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Number; + else if (TokenizeCStylePunctuation(in_begin, in_end, out_begin, out_end)) + paletteIndex = PaletteIndex::Punctuation; + + return paletteIndex != PaletteIndex::Max; + }; + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "C"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::SQL() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "ADD", "EXCEPT", "PERCENT", "ALL", "EXEC", "PLAN", "ALTER", "EXECUTE", "PRECISION", "AND", "EXISTS", "PRIMARY", "ANY", "EXIT", "PRINT", "AS", "FETCH", "PROC", "ASC", "FILE", "PROCEDURE", + "AUTHORIZATION", "FILLFACTOR", "PUBLIC", "BACKUP", "FOR", "RAISERROR", "BEGIN", "FOREIGN", "READ", "BETWEEN", "FREETEXT", "READTEXT", "BREAK", "FREETEXTTABLE", "RECONFIGURE", + "BROWSE", "FROM", "REFERENCES", "BULK", "FULL", "REPLICATION", "BY", "FUNCTION", "RESTORE", "CASCADE", "GOTO", "RESTRICT", "CASE", "GRANT", "RETURN", "CHECK", "GROUP", "REVOKE", + "CHECKPOINT", "HAVING", "RIGHT", "CLOSE", "HOLDLOCK", "ROLLBACK", "CLUSTERED", "IDENTITY", "ROWCOUNT", "COALESCE", "IDENTITY_INSERT", "ROWGUIDCOL", "COLLATE", "IDENTITYCOL", "RULE", + "COLUMN", "IF", "SAVE", "COMMIT", "IN", "SCHEMA", "COMPUTE", "INDEX", "SELECT", "CONSTRAINT", "INNER", "SESSION_USER", "CONTAINS", "INSERT", "SET", "CONTAINSTABLE", "INTERSECT", "SETUSER", + "CONTINUE", "INTO", "SHUTDOWN", "CONVERT", "IS", "SOME", "CREATE", "JOIN", "STATISTICS", "CROSS", "KEY", "SYSTEM_USER", "CURRENT", "KILL", "TABLE", "CURRENT_DATE", "LEFT", "TEXTSIZE", + "CURRENT_TIME", "LIKE", "THEN", "CURRENT_TIMESTAMP", "LINENO", "TO", "CURRENT_USER", "LOAD", "TOP", "CURSOR", "NATIONAL", "TRAN", "DATABASE", "NOCHECK", "TRANSACTION", + "DBCC", "NONCLUSTERED", "TRIGGER", "DEALLOCATE", "NOT", "TRUNCATE", "DECLARE", "NULL", "TSEQUAL", "DEFAULT", "NULLIF", "UNION", "DELETE", "OF", "UNIQUE", "DENY", "OFF", "UPDATE", + "DESC", "OFFSETS", "UPDATETEXT", "DISK", "ON", "USE", "DISTINCT", "OPEN", "USER", "DISTRIBUTED", "OPENDATASOURCE", "VALUES", "DOUBLE", "OPENQUERY", "VARYING","DROP", "OPENROWSET", "VIEW", + "DUMMY", "OPENXML", "WAITFOR", "DUMP", "OPTION", "WHEN", "ELSE", "OR", "WHERE", "END", "ORDER", "WHILE", "ERRLVL", "OUTER", "WITH", "ESCAPE", "OVER", "WRITETEXT" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "ABS", "ACOS", "ADD_MONTHS", "ASCII", "ASCIISTR", "ASIN", "ATAN", "ATAN2", "AVG", "BFILENAME", "BIN_TO_NUM", "BITAND", "CARDINALITY", "CASE", "CAST", "CEIL", + "CHARTOROWID", "CHR", "COALESCE", "COMPOSE", "CONCAT", "CONVERT", "CORR", "COS", "COSH", "COUNT", "COVAR_POP", "COVAR_SAMP", "CUME_DIST", "CURRENT_DATE", + "CURRENT_TIMESTAMP", "DBTIMEZONE", "DECODE", "DECOMPOSE", "DENSE_RANK", "DUMP", "EMPTY_BLOB", "EMPTY_CLOB", "EXP", "EXTRACT", "FIRST_VALUE", "FLOOR", "FROM_TZ", "GREATEST", + "GROUP_ID", "HEXTORAW", "INITCAP", "INSTR", "INSTR2", "INSTR4", "INSTRB", "INSTRC", "LAG", "LAST_DAY", "LAST_VALUE", "LEAD", "LEAST", "LENGTH", "LENGTH2", "LENGTH4", + "LENGTHB", "LENGTHC", "LISTAGG", "LN", "LNNVL", "LOCALTIMESTAMP", "LOG", "LOWER", "LPAD", "LTRIM", "MAX", "MEDIAN", "MIN", "MOD", "MONTHS_BETWEEN", "NANVL", "NCHR", + "NEW_TIME", "NEXT_DAY", "NTH_VALUE", "NULLIF", "NUMTODSINTERVAL", "NUMTOYMINTERVAL", "NVL", "NVL2", "POWER", "RANK", "RAWTOHEX", "REGEXP_COUNT", "REGEXP_INSTR", + "REGEXP_REPLACE", "REGEXP_SUBSTR", "REMAINDER", "REPLACE", "ROUND", "ROWNUM", "RPAD", "RTRIM", "SESSIONTIMEZONE", "SIGN", "SIN", "SINH", + "SOUNDEX", "SQRT", "STDDEV", "SUBSTR", "SUM", "SYS_CONTEXT", "SYSDATE", "SYSTIMESTAMP", "TAN", "TANH", "TO_CHAR", "TO_CLOB", "TO_DATE", "TO_DSINTERVAL", "TO_LOB", + "TO_MULTI_BYTE", "TO_NCLOB", "TO_NUMBER", "TO_SINGLE_BYTE", "TO_TIMESTAMP", "TO_TIMESTAMP_TZ", "TO_YMINTERVAL", "TRANSLATE", "TRIM", "TRUNC", "TZ_OFFSET", "UID", "UPPER", + "USER", "USERENV", "VAR_POP", "VAR_SAMP", "VARIANCE", "VSIZE " + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\\'[^\\\']*\\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = false; + langDef.mAutoIndentation = false; + + langDef.mName = "SQL"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::AngelScript() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "abstract", "auto", "bool", "break", "case", "cast", "class", "const", "continue", "default", "do", "double", "else", "enum", "false", "final", "float", "for", + "from", "funcdef", "function", "get", "if", "import", "in", "inout", "int", "interface", "int8", "int16", "int32", "int64", "is", "mixin", "namespace", "not", + "null", "or", "out", "override", "private", "protected", "return", "set", "shared", "super", "switch", "this ", "true", "typedef", "uint", "uint8", "uint16", "uint32", + "uint64", "void", "while", "xor" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "cos", "sin", "tab", "acos", "asin", "atan", "atan2", "cosh", "sinh", "tanh", "log", "log10", "pow", "sqrt", "abs", "ceil", "floor", "fraction", "closeTo", "fpFromIEEE", "fpToIEEE", + "complex", "opEquals", "opAddAssign", "opSubAssign", "opMulAssign", "opDivAssign", "opAdd", "opSub", "opMul", "opDiv" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\'\\\\?[^\\']\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[0-7]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "/*"; + langDef.mCommentEnd = "*/"; + langDef.mSingleLineComment = "//"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = true; + + langDef.mName = "AngelScript"; + + inited = true; + } + return langDef; +} + +const TextEditor::LanguageDefinition& TextEditor::LanguageDefinition::Lua() +{ + static bool inited = false; + static LanguageDefinition langDef; + if (!inited) + { + static const char* const keywords[] = { + "and", "break", "do", "", "else", "elseif", "end", "false", "for", "function", "if", "in", "", "local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while" + }; + + for (auto& k : keywords) + langDef.mKeywords.insert(k); + + static const char* const identifiers[] = { + "assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset", + "select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION","arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace", + "rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug","getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable", + "getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen", + "read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger", + "floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh", + "pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock", + "date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep", + "reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern", + "coroutine", "table", "io", "os", "string", "utf8", "bit32", "math", "debug", "package" + }; + for (auto& k : identifiers) + { + Identifier id; + id.mDeclaration = "Built-in function"; + langDef.mIdentifiers.insert(std::make_pair(std::string(k), id)); + } + + langDef.mTokenRegexStrings.push_back(std::make_pair("L?\\\"(\\\\.|[^\\\"])*\\\"", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("\\\'[^\\\']*\\\'", PaletteIndex::String)); + langDef.mTokenRegexStrings.push_back(std::make_pair("0[xX][0-9a-fA-F]+[uU]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)([eE][+-]?[0-9]+)?[fF]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[+-]?[0-9]+[Uu]?[lL]?[lL]?", PaletteIndex::Number)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[a-zA-Z_][a-zA-Z0-9_]*", PaletteIndex::Identifier)); + langDef.mTokenRegexStrings.push_back(std::make_pair("[\\[\\]\\{\\}\\!\\%\\^\\&\\*\\(\\)\\-\\+\\=\\~\\|\\<\\>\\?\\/\\;\\,\\.]", PaletteIndex::Punctuation)); + + langDef.mCommentStart = "--[["; + langDef.mCommentEnd = "]]"; + langDef.mSingleLineComment = "--"; + + langDef.mCaseSensitive = true; + langDef.mAutoIndentation = false; + + langDef.mName = "Lua"; + + inited = true; + } + return langDef; +} diff --git a/cpp/vendor/imgui_text_edit/TextEditor.h b/cpp/vendor/imgui_text_edit/TextEditor.h new file mode 100644 index 00000000..bd52e131 --- /dev/null +++ b/cpp/vendor/imgui_text_edit/TextEditor.h @@ -0,0 +1,389 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include "imgui.h" + +class TextEditor +{ +public: + enum class PaletteIndex + { + Default, + Keyword, + Number, + String, + CharLiteral, + Punctuation, + Preprocessor, + Identifier, + KnownIdentifier, + PreprocIdentifier, + Comment, + MultiLineComment, + Background, + Cursor, + Selection, + ErrorMarker, + Breakpoint, + LineNumber, + CurrentLineFill, + CurrentLineFillInactive, + CurrentLineEdge, + Max + }; + + enum class SelectionMode + { + Normal, + Word, + Line + }; + + struct Breakpoint + { + int mLine; + bool mEnabled; + std::string mCondition; + + Breakpoint() + : mLine(-1) + , mEnabled(false) + {} + }; + + // Represents a character coordinate from the user's point of view, + // i. e. consider an uniform grid (assuming fixed-width font) on the + // screen as it is rendered, and each cell has its own coordinate, starting from 0. + // Tabs are counted as [1..mTabSize] count empty spaces, depending on + // how many space is necessary to reach the next tab stop. + // For example, coordinate (1, 5) represents the character 'B' in a line "\tABC", when mTabSize = 4, + // because it is rendered as " ABC" on the screen. + struct Coordinates + { + int mLine, mColumn; + Coordinates() : mLine(0), mColumn(0) {} + Coordinates(int aLine, int aColumn) : mLine(aLine), mColumn(aColumn) + { + assert(aLine >= 0); + assert(aColumn >= 0); + } + static Coordinates Invalid() { static Coordinates invalid(-1, -1); return invalid; } + + bool operator ==(const Coordinates& o) const + { + return + mLine == o.mLine && + mColumn == o.mColumn; + } + + bool operator !=(const Coordinates& o) const + { + return + mLine != o.mLine || + mColumn != o.mColumn; + } + + bool operator <(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn < o.mColumn; + } + + bool operator >(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn > o.mColumn; + } + + bool operator <=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine < o.mLine; + return mColumn <= o.mColumn; + } + + bool operator >=(const Coordinates& o) const + { + if (mLine != o.mLine) + return mLine > o.mLine; + return mColumn >= o.mColumn; + } + }; + + struct Identifier + { + Coordinates mLocation; + std::string mDeclaration; + }; + + typedef std::string String; + typedef std::unordered_map Identifiers; + typedef std::unordered_set Keywords; + typedef std::map ErrorMarkers; + typedef std::unordered_set Breakpoints; + typedef std::array Palette; + typedef uint8_t Char; + + struct Glyph + { + Char mChar; + PaletteIndex mColorIndex = PaletteIndex::Default; + bool mComment : 1; + bool mMultiLineComment : 1; + bool mPreprocessor : 1; + + Glyph(Char aChar, PaletteIndex aColorIndex) : mChar(aChar), mColorIndex(aColorIndex), + mComment(false), mMultiLineComment(false), mPreprocessor(false) {} + }; + + typedef std::vector Line; + typedef std::vector Lines; + + struct LanguageDefinition + { + typedef std::pair TokenRegexString; + typedef std::vector TokenRegexStrings; + typedef bool(*TokenizeCallback)(const char * in_begin, const char * in_end, const char *& out_begin, const char *& out_end, PaletteIndex & paletteIndex); + + std::string mName; + Keywords mKeywords; + Identifiers mIdentifiers; + Identifiers mPreprocIdentifiers; + std::string mCommentStart, mCommentEnd, mSingleLineComment; + char mPreprocChar; + bool mAutoIndentation; + + TokenizeCallback mTokenize; + + TokenRegexStrings mTokenRegexStrings; + + bool mCaseSensitive; + + LanguageDefinition() + : mPreprocChar('#'), mAutoIndentation(true), mTokenize(nullptr), mCaseSensitive(true) + { + } + + static const LanguageDefinition& CPlusPlus(); + static const LanguageDefinition& HLSL(); + static const LanguageDefinition& GLSL(); + static const LanguageDefinition& C(); + static const LanguageDefinition& SQL(); + static const LanguageDefinition& AngelScript(); + static const LanguageDefinition& Lua(); + }; + + TextEditor(); + ~TextEditor(); + + void SetLanguageDefinition(const LanguageDefinition& aLanguageDef); + const LanguageDefinition& GetLanguageDefinition() const { return mLanguageDefinition; } + + const Palette& GetPalette() const { return mPaletteBase; } + void SetPalette(const Palette& aValue); + + void SetErrorMarkers(const ErrorMarkers& aMarkers) { mErrorMarkers = aMarkers; } + void SetBreakpoints(const Breakpoints& aMarkers) { mBreakpoints = aMarkers; } + + void Render(const char* aTitle, const ImVec2& aSize = ImVec2(), bool aBorder = false); + void SetText(const std::string& aText); + std::string GetText() const; + + void SetTextLines(const std::vector& aLines); + std::vector GetTextLines() const; + + std::string GetSelectedText() const; + std::string GetCurrentLineText()const; + + int GetTotalLines() const { return (int)mLines.size(); } + bool IsOverwrite() const { return mOverwrite; } + + void SetReadOnly(bool aValue); + bool IsReadOnly() const { return mReadOnly; } + bool IsTextChanged() const { return mTextChanged; } + bool IsCursorPositionChanged() const { return mCursorPositionChanged; } + + bool IsColorizerEnabled() const { return mColorizerEnabled; } + void SetColorizerEnable(bool aValue); + + Coordinates GetCursorPosition() const { return GetActualCursorCoordinates(); } + void SetCursorPosition(const Coordinates& aPosition); + + inline void SetHandleMouseInputs (bool aValue){ mHandleMouseInputs = aValue;} + inline bool IsHandleMouseInputsEnabled() const { return mHandleKeyboardInputs; } + + inline void SetHandleKeyboardInputs (bool aValue){ mHandleKeyboardInputs = aValue;} + inline bool IsHandleKeyboardInputsEnabled() const { return mHandleKeyboardInputs; } + + inline void SetImGuiChildIgnored (bool aValue){ mIgnoreImGuiChild = aValue;} + inline bool IsImGuiChildIgnored() const { return mIgnoreImGuiChild; } + + inline void SetShowWhitespaces(bool aValue) { mShowWhitespaces = aValue; } + inline bool IsShowingWhitespaces() const { return mShowWhitespaces; } + + void SetTabSize(int aValue); + inline int GetTabSize() const { return mTabSize; } + + void InsertText(const std::string& aValue); + void InsertText(const char* aValue); + + void MoveUp(int aAmount = 1, bool aSelect = false); + void MoveDown(int aAmount = 1, bool aSelect = false); + void MoveLeft(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveRight(int aAmount = 1, bool aSelect = false, bool aWordMode = false); + void MoveTop(bool aSelect = false); + void MoveBottom(bool aSelect = false); + void MoveHome(bool aSelect = false); + void MoveEnd(bool aSelect = false); + + void SetSelectionStart(const Coordinates& aPosition); + void SetSelectionEnd(const Coordinates& aPosition); + void SetSelection(const Coordinates& aStart, const Coordinates& aEnd, SelectionMode aMode = SelectionMode::Normal); + void SelectWordUnderCursor(); + void SelectAll(); + bool HasSelection() const; + + void Copy(); + void Cut(); + void Paste(); + void Delete(); + + bool CanUndo() const; + bool CanRedo() const; + void Undo(int aSteps = 1); + void Redo(int aSteps = 1); + + static const Palette& GetDarkPalette(); + static const Palette& GetLightPalette(); + static const Palette& GetRetroBluePalette(); + +private: + typedef std::vector> RegexList; + + struct EditorState + { + Coordinates mSelectionStart; + Coordinates mSelectionEnd; + Coordinates mCursorPosition; + }; + + class UndoRecord + { + public: + UndoRecord() {} + ~UndoRecord() {} + + UndoRecord( + const std::string& aAdded, + const TextEditor::Coordinates aAddedStart, + const TextEditor::Coordinates aAddedEnd, + + const std::string& aRemoved, + const TextEditor::Coordinates aRemovedStart, + const TextEditor::Coordinates aRemovedEnd, + + TextEditor::EditorState& aBefore, + TextEditor::EditorState& aAfter); + + void Undo(TextEditor* aEditor); + void Redo(TextEditor* aEditor); + + std::string mAdded; + Coordinates mAddedStart; + Coordinates mAddedEnd; + + std::string mRemoved; + Coordinates mRemovedStart; + Coordinates mRemovedEnd; + + EditorState mBefore; + EditorState mAfter; + }; + + typedef std::vector UndoBuffer; + + void ProcessInputs(); + void Colorize(int aFromLine = 0, int aCount = -1); + void ColorizeRange(int aFromLine = 0, int aToLine = 0); + void ColorizeInternal(); + float TextDistanceToLineStart(const Coordinates& aFrom) const; + void EnsureCursorVisible(); + int GetPageSize() const; + std::string GetText(const Coordinates& aStart, const Coordinates& aEnd) const; + Coordinates GetActualCursorCoordinates() const; + Coordinates SanitizeCoordinates(const Coordinates& aValue) const; + void Advance(Coordinates& aCoordinates) const; + void DeleteRange(const Coordinates& aStart, const Coordinates& aEnd); + int InsertTextAt(Coordinates& aWhere, const char* aValue); + void AddUndo(UndoRecord& aValue); + Coordinates ScreenPosToCoordinates(const ImVec2& aPosition) const; + Coordinates FindWordStart(const Coordinates& aFrom) const; + Coordinates FindWordEnd(const Coordinates& aFrom) const; + Coordinates FindNextWord(const Coordinates& aFrom) const; + int GetCharacterIndex(const Coordinates& aCoordinates) const; + int GetCharacterColumn(int aLine, int aIndex) const; + int GetLineCharacterCount(int aLine) const; + int GetLineMaxColumn(int aLine) const; + bool IsOnWordBoundary(const Coordinates& aAt) const; + void RemoveLine(int aStart, int aEnd); + void RemoveLine(int aIndex); + Line& InsertLine(int aIndex); + void EnterCharacter(ImWchar aChar, bool aShift); + void Backspace(); + void DeleteSelection(); + std::string GetWordUnderCursor() const; + std::string GetWordAt(const Coordinates& aCoords) const; + ImU32 GetGlyphColor(const Glyph& aGlyph) const; + + void HandleKeyboardInputs(); + void HandleMouseInputs(); + void Render(); + + float mLineSpacing; + Lines mLines; + EditorState mState; + UndoBuffer mUndoBuffer; + int mUndoIndex; + + int mTabSize; + bool mOverwrite; + bool mReadOnly; + bool mWithinRender; + bool mScrollToCursor; + bool mScrollToTop; + bool mTextChanged; + bool mColorizerEnabled; + float mTextStart; // position (in pixels) where a code line starts relative to the left of the TextEditor. + int mLeftMargin; + bool mCursorPositionChanged; + int mColorRangeMin, mColorRangeMax; + SelectionMode mSelectionMode; + bool mHandleKeyboardInputs; + bool mHandleMouseInputs; + bool mIgnoreImGuiChild; + bool mShowWhitespaces; + + Palette mPaletteBase; + Palette mPalette; + LanguageDefinition mLanguageDefinition; + RegexList mRegexList; + + bool mCheckComments; + Breakpoints mBreakpoints; + ErrorMarkers mErrorMarkers; + ImVec2 mCharAdvance; + Coordinates mInteractiveStart, mInteractiveEnd; + std::string mLineBuffer; + uint64_t mStartTime; + + float mLastClick; +}; From 24905eebc74211e1ed743071f308b5406e642cc9 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:31 +0200 Subject: [PATCH 2/6] feat(cpp/core): add text_editor_cpp_core component (PIMPL) Wrapper en namespace fn:: sobre ImGuiColorTextEdit. La API publica solo expone TextEditorState como tipo opaco; el TextEditor del vendor vive dentro del .cpp. Soporta CodeLang::{Generic, GLSL, SQL, Cpp} (highlighting via las LanguageDefinition del vendor). text_editor_render() devuelve true en el frame en que el contenido cambia; flag dirty manejado independientemente para casos "editado pero aun no guardado". text_editor_get_text() cachea el resultado en un std::string del state para mantener el const char* valido entre llamadas (el GetText() del vendor devuelve std::string por valor). Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/functions/core/text_editor.cpp | 68 +++++++++++++++++++ cpp/functions/core/text_editor.h | 50 ++++++++++++++ cpp/functions/core/text_editor.md | 101 +++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 cpp/functions/core/text_editor.cpp create mode 100644 cpp/functions/core/text_editor.h create mode 100644 cpp/functions/core/text_editor.md diff --git a/cpp/functions/core/text_editor.cpp b/cpp/functions/core/text_editor.cpp new file mode 100644 index 00000000..42657220 --- /dev/null +++ b/cpp/functions/core/text_editor.cpp @@ -0,0 +1,68 @@ +#include "text_editor.h" + +#include "TextEditor.h" + +#include + +namespace fn { + +struct TextEditorState { + ::TextEditor editor; // vendor type, oculto detras del PIMPL + std::string cached_text; // buffer estable para text_editor_get_text() + bool dirty = false; +}; + +static const ::TextEditor::LanguageDefinition& lang_def(CodeLang lang) { + switch (lang) { + case CodeLang::GLSL: return ::TextEditor::LanguageDefinition::GLSL(); + case CodeLang::SQL: return ::TextEditor::LanguageDefinition::SQL(); + case CodeLang::Cpp: return ::TextEditor::LanguageDefinition::CPlusPlus(); + case CodeLang::Generic: + default: + return ::TextEditor::LanguageDefinition::CPlusPlus(); + } +} + +TextEditorState* text_editor_create(CodeLang lang) { + auto* s = new TextEditorState(); + s->editor.SetLanguageDefinition(lang_def(lang)); + s->editor.SetShowWhitespaces(false); + return s; +} + +void text_editor_destroy(TextEditorState* state) { + delete state; +} + +void text_editor_set_text(TextEditorState* state, const char* text) { + if (!state || !text) return; + state->editor.SetText(text); + state->cached_text = text; + state->dirty = false; +} + +const char* text_editor_get_text(TextEditorState* state) { + if (!state) return ""; + state->cached_text = state->editor.GetText(); + // El editor anade siempre un '\n' final; lo dejamos para preservar + // la semantica del vendor (es lo que devuelve GetText()). + return state->cached_text.c_str(); +} + +bool text_editor_render(TextEditorState* state, const char* label, ImVec2 size) { + if (!state) return false; + state->editor.Render(label, size, true); + const bool changed = state->editor.IsTextChanged(); + if (changed) state->dirty = true; + return changed; +} + +bool text_editor_is_dirty(const TextEditorState* state) { + return state ? state->dirty : false; +} + +void text_editor_clear_dirty(TextEditorState* state) { + if (state) state->dirty = false; +} + +} // namespace fn diff --git a/cpp/functions/core/text_editor.h b/cpp/functions/core/text_editor.h new file mode 100644 index 00000000..a5f9f291 --- /dev/null +++ b/cpp/functions/core/text_editor.h @@ -0,0 +1,50 @@ +#pragma once + +// text_editor — wrapper PIMPL sobre ImGuiColorTextEdit (vendor MIT). +// API en namespace fn:: que oculta el tipo concreto del vendor. +// +// Uso tipico: +// auto* ed = fn::text_editor_create(fn::CodeLang::GLSL); +// fn::text_editor_set_text(ed, "void main() { gl_FragColor = vec4(1); }"); +// if (fn::text_editor_render(ed, "##ed", {600, 400})) { /* cambio */ } +// fn::text_editor_destroy(ed); +// +// El estado vive en TextEditorState (forward-declared), no se expone TextEditor del vendor. + +#include "imgui.h" + +namespace fn { + +enum class CodeLang { + Generic, + GLSL, + SQL, + Cpp, +}; + +// Forward declaration — definicion completa en text_editor.cpp (PIMPL). +struct TextEditorState; + +// Crea un editor con el lenguaje dado. El caller es dueno y debe llamar destroy. +TextEditorState* text_editor_create(CodeLang lang = CodeLang::Generic); + +// Libera el editor. Acepta nullptr (no-op). +void text_editor_destroy(TextEditorState* state); + +// Reemplaza el texto del editor. +void text_editor_set_text(TextEditorState* state, const char* text); + +// Devuelve el texto actual. El puntero es valido solo hasta la siguiente llamada +// a text_editor_set_text/get_text/render sobre el mismo editor. +const char* text_editor_get_text(TextEditorState* state); + +// Renderiza el editor. Devuelve true en el frame en que el contenido cambio. +bool text_editor_render(TextEditorState* state, const char* label, ImVec2 size); + +// True si el editor esta marcado como "dirty" (modificado desde el ultimo clear). +bool text_editor_is_dirty(const TextEditorState* state); + +// Limpia el flag dirty (tipicamente tras guardar a disco). +void text_editor_clear_dirty(TextEditorState* state); + +} // namespace fn diff --git a/cpp/functions/core/text_editor.md b/cpp/functions/core/text_editor.md new file mode 100644 index 00000000..3f620fa0 --- /dev/null +++ b/cpp/functions/core/text_editor.md @@ -0,0 +1,101 @@ +--- +name: text_editor +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "fn::TextEditorState* fn::text_editor_create(fn::CodeLang); void fn::text_editor_destroy(fn::TextEditorState*); void fn::text_editor_set_text(fn::TextEditorState*, const char*); const char* fn::text_editor_get_text(fn::TextEditorState*); bool fn::text_editor_render(fn::TextEditorState*, const char* label, ImVec2 size); bool fn::text_editor_is_dirty(const fn::TextEditorState*); void fn::text_editor_clear_dirty(fn::TextEditorState*)" +description: "Editor de codigo embebido en ImGui con syntax highlighting (GLSL, SQL, C++, Generic). Wrapper PIMPL sobre ImGuiColorTextEdit (vendor MIT) — la API solo expone tipos opacos en namespace fn::." +tags: [imgui, editor, text, code, glsl, sql, syntax-highlighting, tokens] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui, TextEditor] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/text_editor.cpp" +framework: imgui +source_repo: "https://github.com/BalazsJako/ImGuiColorTextEdit" +source_license: "MIT" +source_file: "TextEditor.h, TextEditor.cpp" +params: [] +output: "TextEditorState opaco — encapsula el editor del vendor + buffer de texto cacheado + flag dirty. Render devuelve true cuando el contenido cambio en el frame actual." +--- + +# text_editor + +Editor de codigo in-app con syntax highlighting. Resuelve el ciclo de edicion sin alt-tab a un editor externo (ej: editar GLSL en `shaders_lab` y recompilar al guardar; futuro `sql_workbench` con CTAS sobre `registry.db`). + +## API + +```cpp +namespace fn { + enum class CodeLang { Generic, GLSL, SQL, Cpp }; + struct TextEditorState; // PIMPL — tipo opaco + + TextEditorState* text_editor_create(CodeLang lang = CodeLang::Generic); + void text_editor_destroy(TextEditorState*); + + void text_editor_set_text(TextEditorState*, const char* text); + const char* text_editor_get_text(TextEditorState*); // valido hasta el siguiente call + bool text_editor_render(TextEditorState*, const char* label, ImVec2 size); // true si cambio + bool text_editor_is_dirty(const TextEditorState*); + void text_editor_clear_dirty(TextEditorState*); +} +``` + +## Ejemplo — editor GLSL con boton de guardado + +```cpp +#include "core/text_editor.h" +#include "core/button.h" + +static fn::TextEditorState* g_ed = nullptr; + +void render() { + if (!g_ed) { + g_ed = fn::text_editor_create(fn::CodeLang::GLSL); + fn::text_editor_set_text(g_ed, + "#version 330\nout vec4 c;\nvoid main() { c = vec4(1); }\n"); + } + + fn::text_editor_render(g_ed, "##editor", ImVec2(600, 400)); + + if (fn::text_editor_is_dirty(g_ed)) ImGui::TextUnformatted("(modificado)"); + + if (fn_ui::button("Save")) { + FILE* f = std::fopen("/tmp/shader.glsl", "w"); + std::fputs(fn::text_editor_get_text(g_ed), f); + std::fclose(f); + fn::text_editor_clear_dirty(g_ed); + } +} +``` + +## Decisiones de diseño + +- **PIMPL** — el tipo `TextEditor` del vendor no aparece en `text_editor.h`. El header publico solo importa `imgui.h` (por `ImVec2`). +- **`get_text` cachea** el resultado en un `std::string` propio del state para que el `const char*` siga siendo valido entre llamadas (el `GetText()` del vendor devuelve un `std::string` por valor). +- **Sin tokens custom** — el editor hereda el theme global de ImGui (paleta interna del vendor). Para integrar `fn_tokens` haria falta crear un `Palette` propio; lo dejamos para una v2 si pesa visualmente. +- **`Generic` reusa C++** como default: el vendor no tiene un "plain text" sin highlighting; C++ es el menos intrusivo. + +## Lenguajes soportados + +| `CodeLang` | Highlighting | Origen | +|-----------|--------------|--------| +| `GLSL` | Si | `LanguageDefinition::GLSL()` | +| `SQL` | Si | `LanguageDefinition::SQL()` | +| `Cpp` | Si | `LanguageDefinition::CPlusPlus()` | +| `Generic` | C++ por defecto | `LanguageDefinition::CPlusPlus()` | + +## Vendor + +`cpp/vendor/imgui_text_edit/` (commit pinneado en su `README.md`). MIT. + +## Combinacion sugerida + +`text_editor` + `file_watcher_cpp_core` = ciclo de edicion completo. Ver `apps/primitives_gallery/demos_text_editor.cpp` para la demo combinada. From 07a653d97dc0e8ad087a9938ad71d29a26b9a836 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:38 +0200 Subject: [PATCH 3/6] feat(cpp/core): add file_watcher_cpp_core (inotify Linux / RDCW Win) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Watcher de archivos no bloqueante con backend nativo por plataforma: - Linux: inotify_init1(IN_NONBLOCK | IN_CLOEXEC), inotify_add_watch con mascara MODIFY|CREATE|DELETE|CLOSE_WRITE|MOVED_*. Drain en cada poll(). - Windows: ReadDirectoryChangesW overlapped + GetOverlappedResult no bloqueante. Para vigilar un archivo, registra el directorio padre y filtra por nombre exacto en el poll(). - Otros: stub — poll() devuelve vacio y last_error() reporta no soportado. API en namespace fn:: con tipos opacos (FileWatcher PIMPL). Errores via last_error() en vez de excepciones. Documentadas las limitaciones (limite de inotify watches en Linux, granularidad directorio-level en Windows, no recursividad). Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/functions/core/file_watcher.cpp | 283 ++++++++++++++++++++++++++++ cpp/functions/core/file_watcher.h | 41 ++++ cpp/functions/core/file_watcher.md | 93 +++++++++ 3 files changed, 417 insertions(+) create mode 100644 cpp/functions/core/file_watcher.cpp create mode 100644 cpp/functions/core/file_watcher.h create mode 100644 cpp/functions/core/file_watcher.md diff --git a/cpp/functions/core/file_watcher.cpp b/cpp/functions/core/file_watcher.cpp new file mode 100644 index 00000000..a115f707 --- /dev/null +++ b/cpp/functions/core/file_watcher.cpp @@ -0,0 +1,283 @@ +#include "file_watcher.h" + +#include +#include +#include +#include + +#if defined(__linux__) + #include + #include + #include + #include + #include +#elif defined(_WIN32) + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include +#endif + +namespace fn { + +#if defined(__linux__) + +struct FileWatcher { + int fd = -1; + std::unordered_map wd_to_path; // inotify wd -> path + std::string last_err; +}; + +FileWatcher* file_watcher_create() { + auto* w = new FileWatcher(); + w->fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC); + if (w->fd < 0) { + w->last_err = std::string("inotify_init1: ") + std::strerror(errno); + } + return w; +} + +void file_watcher_destroy(FileWatcher* w) { + if (!w) return; + if (w->fd >= 0) ::close(w->fd); + delete w; +} + +bool file_watcher_add(FileWatcher* w, const char* path) { + if (!w || w->fd < 0 || !path) return false; + const uint32_t mask = IN_MODIFY | IN_CREATE | IN_DELETE + | IN_CLOSE_WRITE | IN_MOVED_FROM | IN_MOVED_TO + | IN_DELETE_SELF | IN_MOVE_SELF; + int wd = inotify_add_watch(w->fd, path, mask); + if (wd < 0) { + w->last_err = std::string("inotify_add_watch(") + path + "): " + std::strerror(errno); + return false; + } + w->wd_to_path[wd] = path; + w->last_err.clear(); + return true; +} + +std::vector file_watcher_poll(FileWatcher* w) { + std::vector out; + if (!w || w->fd < 0) return out; + + // Drain todos los eventos pendientes sin bloquear. + char buf[4096] __attribute__((aligned(__alignof__(struct inotify_event)))); + while (true) { + ssize_t n = ::read(w->fd, buf, sizeof(buf)); + if (n <= 0) { + if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { + w->last_err = std::string("inotify read: ") + std::strerror(errno); + } + break; + } + for (char* p = buf; p < buf + n; ) { + auto* ev = reinterpret_cast(p); + FileEvent fe; + auto it = w->wd_to_path.find(ev->wd); + std::string base = (it != w->wd_to_path.end()) ? it->second : std::string(); + if (ev->len > 0) { + if (!base.empty() && base.back() != '/') base += '/'; + base += ev->name; + } + fe.path = base; + + if (ev->mask & (IN_CREATE | IN_MOVED_TO)) { + fe.kind = FileEvent::Created; + out.push_back(fe); + } + if (ev->mask & (IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM | IN_MOVE_SELF)) { + fe.kind = FileEvent::Deleted; + out.push_back(fe); + } + if (ev->mask & (IN_MODIFY | IN_CLOSE_WRITE)) { + fe.kind = FileEvent::Modified; + out.push_back(fe); + } + p += sizeof(struct inotify_event) + ev->len; + } + } + return out; +} + +const char* file_watcher_last_error(const FileWatcher* w) { + return w ? w->last_err.c_str() : ""; +} + +#elif defined(_WIN32) + +// Una entry por directorio vigilado: un OVERLAPPED + buffer + handle. +// Para "vigilar un archivo" en Windows registramos su directorio padre y +// filtramos por nombre en el poll(). +struct WinWatch { + HANDLE dir = INVALID_HANDLE_VALUE; + OVERLAPPED ovl = {}; + std::vector buf; + std::string dir_path; // directorio absoluto vigilado + std::string filter_name; // si !empty: solo emitir eventos cuyo path == dir_path/filter_name + bool pending = false; +}; + +struct FileWatcher { + std::vector watches; + std::string last_err; +}; + +static void start_read(WinWatch* ww) { + if (ww->dir == INVALID_HANDLE_VALUE) return; + DWORD bytes = 0; + BOOL ok = ::ReadDirectoryChangesW( + ww->dir, + ww->buf.data(), + (DWORD)ww->buf.size(), + FALSE, // bWatchSubtree + FILE_NOTIFY_CHANGE_FILE_NAME | + FILE_NOTIFY_CHANGE_DIR_NAME | + FILE_NOTIFY_CHANGE_LAST_WRITE| + FILE_NOTIFY_CHANGE_SIZE | + FILE_NOTIFY_CHANGE_CREATION, + &bytes, + &ww->ovl, + NULL); + ww->pending = (ok != 0); +} + +FileWatcher* file_watcher_create() { + return new FileWatcher(); +} + +void file_watcher_destroy(FileWatcher* w) { + if (!w) return; + for (auto* ww : w->watches) { + if (ww->dir != INVALID_HANDLE_VALUE) ::CloseHandle(ww->dir); + if (ww->ovl.hEvent) ::CloseHandle(ww->ovl.hEvent); + delete ww; + } + delete w; +} + +static std::string dirname_of(const std::string& path) { + size_t pos = path.find_last_of("\\/"); + if (pos == std::string::npos) return "."; + return path.substr(0, pos); +} +static std::string basename_of(const std::string& path) { + size_t pos = path.find_last_of("\\/"); + if (pos == std::string::npos) return path; + return path.substr(pos + 1); +} + +bool file_watcher_add(FileWatcher* w, const char* path) { + if (!w || !path) return false; + DWORD attrs = ::GetFileAttributesA(path); + if (attrs == INVALID_FILE_ATTRIBUTES) { + w->last_err = std::string("GetFileAttributes(") + path + "): not found"; + return false; + } + std::string dir, filter; + if (attrs & FILE_ATTRIBUTE_DIRECTORY) { + dir = path; + } else { + dir = dirname_of(path); + filter = basename_of(path); + } + HANDLE h = ::CreateFileA( + dir.c_str(), + FILE_LIST_DIRECTORY, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, + NULL); + if (h == INVALID_HANDLE_VALUE) { + w->last_err = std::string("CreateFileA(") + dir + "): " + std::to_string(::GetLastError()); + return false; + } + auto* ww = new WinWatch(); + ww->dir = h; + ww->buf.resize(8192); + ww->ovl.hEvent = ::CreateEventA(NULL, TRUE, FALSE, NULL); + ww->dir_path = dir; + ww->filter_name = filter; + start_read(ww); + w->watches.push_back(ww); + w->last_err.clear(); + return true; +} + +static std::string narrow_w(const wchar_t* wstr, size_t wlen) { + if (wlen == 0) return {}; + int n = ::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, NULL, 0, NULL, NULL); + std::string out(n, 0); + ::WideCharToMultiByte(CP_UTF8, 0, wstr, (int)wlen, out.data(), n, NULL, NULL); + return out; +} + +std::vector file_watcher_poll(FileWatcher* w) { + std::vector out; + if (!w) return out; + for (auto* ww : w->watches) { + if (!ww->pending) { start_read(ww); continue; } + DWORD bytes = 0; + BOOL ok = ::GetOverlappedResult(ww->dir, &ww->ovl, &bytes, FALSE); + if (!ok) { + // ERROR_IO_INCOMPLETE => sin novedades; ignorar + DWORD err = ::GetLastError(); + if (err != ERROR_IO_INCOMPLETE) { + w->last_err = std::string("GetOverlappedResult: ") + std::to_string(err); + ww->pending = false; + } + continue; + } + ww->pending = false; + ::ResetEvent(ww->ovl.hEvent); + + const uint8_t* p = ww->buf.data(); + const uint8_t* end = p + bytes; + while (p < end) { + auto* fni = reinterpret_cast(p); + std::string name = narrow_w(fni->FileName, fni->FileNameLength / sizeof(wchar_t)); + std::string full = ww->dir_path + "\\" + name; + + bool match = ww->filter_name.empty() || ww->filter_name == name; + if (match) { + FileEvent fe; + fe.path = full; + switch (fni->Action) { + case FILE_ACTION_ADDED: + case FILE_ACTION_RENAMED_NEW_NAME: + fe.kind = FileEvent::Created; out.push_back(fe); break; + case FILE_ACTION_REMOVED: + case FILE_ACTION_RENAMED_OLD_NAME: + fe.kind = FileEvent::Deleted; out.push_back(fe); break; + case FILE_ACTION_MODIFIED: + fe.kind = FileEvent::Modified; out.push_back(fe); break; + default: break; + } + } + if (fni->NextEntryOffset == 0) break; + p += fni->NextEntryOffset; + } + start_read(ww); + } + return out; +} + +const char* file_watcher_last_error(const FileWatcher* w) { + return w ? w->last_err.c_str() : ""; +} + +#else // Other platforms — stub + +struct FileWatcher { std::string last_err = "file_watcher: platform not supported"; }; + +FileWatcher* file_watcher_create() { return new FileWatcher(); } +void file_watcher_destroy(FileWatcher* w) { delete w; } +bool file_watcher_add(FileWatcher*, const char*) { return false; } +std::vector file_watcher_poll(FileWatcher*) { return {}; } +const char* file_watcher_last_error(const FileWatcher* w) { return w ? w->last_err.c_str() : ""; } + +#endif + +} // namespace fn diff --git a/cpp/functions/core/file_watcher.h b/cpp/functions/core/file_watcher.h new file mode 100644 index 00000000..fcfa817f --- /dev/null +++ b/cpp/functions/core/file_watcher.h @@ -0,0 +1,41 @@ +#pragma once + +// file_watcher — watcher cross-platform de archivos/directorios (impure I/O). +// +// Linux: inotify (mascara IN_MODIFY | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVED_*) +// Windows: ReadDirectoryChangesW (overlapped, no bloqueante en poll()) +// Otros: stub (poll() devuelve siempre vacio) +// +// API no bloqueante: poll() drena los eventos disponibles desde la ultima llamada. + +#include +#include + +namespace fn { + +struct FileWatcher; // PIMPL + +struct FileEvent { + enum Kind { Modified, Created, Deleted }; + std::string path; + Kind kind; +}; + +// Crea un watcher vacio. El caller llama destroy. +FileWatcher* file_watcher_create(); + +// Libera el watcher (cierra fd / handles). Acepta nullptr. +void file_watcher_destroy(FileWatcher* w); + +// Registra un path (archivo o directorio). Devuelve false si no existe o no se pudo +// anadir el watch (ej: en Linux, limite de inotify alcanzado). Tras false, llamar +// file_watcher_last_error() para obtener detalles. +bool file_watcher_add(FileWatcher* w, const char* path); + +// Devuelve los eventos acumulados desde la ultima llamada. No bloqueante. +std::vector file_watcher_poll(FileWatcher* w); + +// Devuelve el mensaje del ultimo error (vacio si no hay). +const char* file_watcher_last_error(const FileWatcher* w); + +} // namespace fn diff --git a/cpp/functions/core/file_watcher.md b/cpp/functions/core/file_watcher.md new file mode 100644 index 00000000..11deb877 --- /dev/null +++ b/cpp/functions/core/file_watcher.md @@ -0,0 +1,93 @@ +--- +name: file_watcher +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: impure +signature: "fn::FileWatcher* fn::file_watcher_create(); void fn::file_watcher_destroy(fn::FileWatcher*); bool fn::file_watcher_add(fn::FileWatcher*, const char* path); std::vector fn::file_watcher_poll(fn::FileWatcher*); const char* fn::file_watcher_last_error(const fn::FileWatcher*)" +description: "Watcher de archivos/directorios cross-platform (Linux inotify, Windows ReadDirectoryChangesW). API no bloqueante: registra paths con add() y consulta eventos con poll(). Cada poll() drena todos los eventos pendientes desde la llamada anterior." +tags: [filesystem, watcher, inotify, file_events, io] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [unistd, sys/inotify, windows] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/file_watcher.cpp" +params: [] +output: "FileWatcher opaco con cola interna de eventos. poll() devuelve std::vector con {path, kind in {Modified, Created, Deleted}}. Errores acumulados en last_error()." +--- + +# file_watcher + +Watcher de archivos no bloqueante con backend nativo por plataforma. Pareja natural de `text_editor_cpp_core` para el ciclo "edita -> guarda -> recompila" sin polling de timestamps. + +## API + +```cpp +namespace fn { + struct FileWatcher; + struct FileEvent { + enum Kind { Modified, Created, Deleted }; + std::string path; + Kind kind; + }; + + FileWatcher* file_watcher_create(); + void file_watcher_destroy(FileWatcher*); + bool file_watcher_add(FileWatcher*, const char* path); // file or dir + std::vector file_watcher_poll(FileWatcher*); // non-blocking, drain + const char* file_watcher_last_error(const FileWatcher*); +} +``` + +## Ejemplo + +```cpp +#include "core/file_watcher.h" + +auto* fw = fn::file_watcher_create(); +fn::file_watcher_add(fw, "/tmp/shader.glsl"); + +while (running) { + for (auto& ev : fn::file_watcher_poll(fw)) { + switch (ev.kind) { + case fn::FileEvent::Modified: reload(ev.path); break; + case fn::FileEvent::Created: std::printf("created: %s\n", ev.path.c_str()); break; + case fn::FileEvent::Deleted: std::printf("deleted: %s\n", ev.path.c_str()); break; + } + } + sleep_ms(16); +} + +fn::file_watcher_destroy(fw); +``` + +## Backends + +| Plataforma | Mecanismo | Notas | +|-----------|-----------|-------| +| Linux | `inotify_init1(IN_NONBLOCK)` + `inotify_add_watch` | mascara: MODIFY \| CREATE \| DELETE \| CLOSE_WRITE \| MOVED_* | +| Windows | `ReadDirectoryChangesW` overlapped + `GetOverlappedResult` no bloqueante | Para vigilar un archivo, registra el directorio padre y filtra por nombre | +| Otros | Stub — `poll()` devuelve vector vacio | `last_error()` indica "platform not supported" | + +## Limites y avisos + +- **inotify watch limit (Linux)**: por defecto `fs.inotify.max_user_watches = 8192`. Si lo superas, `add()` devuelve false y `last_error()` reporta `No space left on device`. Subirlo con: + ```bash + sudo sysctl fs.inotify.max_user_watches=524288 + ``` +- **Windows directorio-level**: cuando registras un archivo, internamente se vigila el directorio padre y se filtra por nombre exacto en el poll. Eventos de archivos hermanos se descartan. +- **No es recursivo** — `add()` registra el path dado, no su subarbol. Para vigilar un arbol, llama `add()` por cada subdirectorio (TODO si hace falta). +- **Editor "modify" coalescing**: editores como vim escriben usando un swap + rename, lo que produce CREATE + DELETE + MOVED_TO en vez de MODIFY puro. La mascara cubre MOVED_TO para que el evento llegue como `Created` (semantica "ahora hay un archivo nuevo en esa ruta") — el caller deduplica si lo necesita. + +## Decisiones de diseño + +- **PIMPL**: el header no expone `inotify_event` ni `OVERLAPPED`. `FileWatcher` es opaco. +- **No bloqueante**: el caller hace polling desde su loop principal (tipico ImGui ~60Hz). No threads, no callbacks. Mantenimiento bajo. +- **Errores como string**: no exception throwing. `add()` devuelve `bool` y `last_error()` da contexto. `error_type: stderr_string` en frontmatter. +- **Sin coalescing implicito**: el watcher emite todo lo que recibe del kernel. La app decide si dedup eventos cercanos en el tiempo. From 61a238b3fdf3c02272ed2f485b26345b0fe9ef09 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:47 +0200 Subject: [PATCH 4/6] test(cpp): add text_editor_smoke build gate for issue 0025 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App minima (no abre ventana ImGui) que crea/settea/lee text_editor y registra un watch sobre /tmp/fn_smoke_test.txt para confirmar que TextEditor.cpp del vendor + text_editor.cpp + file_watcher.cpp enlazan correctamente. Activada por defecto si la carpeta existe (no requiere la primitives_gallery). Tambien anade flag FN_BUILD_GALLERY (OFF default) para no romper el build cuando la primitives_gallery no esta presente — sus deps (button.cpp, toolbar.cpp...) son sources untracked en algunas branches. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/CMakeLists.txt | 13 +++++ cpp/apps/text_editor_smoke/CMakeLists.txt | 14 +++++ cpp/apps/text_editor_smoke/main.cpp | 64 +++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 cpp/apps/text_editor_smoke/CMakeLists.txt create mode 100644 cpp/apps/text_editor_smoke/main.cpp diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index a1a74a89..d2aff08e 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -115,6 +115,19 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/shaders_lab/CMakeLists.txt) add_subdirectory(apps/shaders_lab) endif() +# --- Primitives Gallery --- +# Activado solo si la app esta presente Y todos sus deps tambien (button, toolbar... +# son sources untracked en este worktree). Forzar con FN_BUILD_GALLERY=ON. +if(FN_BUILD_GALLERY AND EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/primitives_gallery/CMakeLists.txt) + add_subdirectory(apps/primitives_gallery) +endif() + +# --- text_editor + file_watcher smoke test (issue 0025) --- +# Build gate para validar que text_editor.cpp + file_watcher.cpp + vendor enlazan. +if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/apps/text_editor_smoke/CMakeLists.txt) + add_subdirectory(apps/text_editor_smoke) +endif() + # --- Registry Dashboard (lives in projects/fn_monitoring/apps/) --- set(_DASH_DIR ${CMAKE_SOURCE_DIR}/../projects/fn_monitoring/apps/registry_dashboard) if(EXISTS ${_DASH_DIR}/CMakeLists.txt) diff --git a/cpp/apps/text_editor_smoke/CMakeLists.txt b/cpp/apps/text_editor_smoke/CMakeLists.txt new file mode 100644 index 00000000..5572da34 --- /dev/null +++ b/cpp/apps/text_editor_smoke/CMakeLists.txt @@ -0,0 +1,14 @@ +# Smoke test app para validar que text_editor + file_watcher compilan +# y enlazan correctamente. NO es una app del registry, solo build gate +# de las funciones nuevas del issue 0025. Sin ImGui events runtime — el +# test crea, settea texto, polea y destruye en 1 frame headless (no abre ventana). + +add_imgui_app(text_editor_smoke + main.cpp + ${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp + ${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp +) +target_include_directories(text_editor_smoke PRIVATE + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit +) diff --git a/cpp/apps/text_editor_smoke/main.cpp b/cpp/apps/text_editor_smoke/main.cpp new file mode 100644 index 00000000..4b0103f4 --- /dev/null +++ b/cpp/apps/text_editor_smoke/main.cpp @@ -0,0 +1,64 @@ +// Smoke test (no GUI): compila y ejecuta brevemente las APIs nuevas del +// issue 0025 para validar que el wrapper PIMPL del text_editor y el +// file_watcher (inotify Linux / ReadDirectoryChangesW Win) enlazan. +// +// No abre ventana ImGui — solo crea / settea texto / lee / poll / destruye. + +#include "core/text_editor.h" +#include "core/file_watcher.h" + +#include +#include +#include +#include +#include + +int main() { + // ----- text_editor ----- + auto* ed = fn::text_editor_create(fn::CodeLang::GLSL); + if (!ed) { std::fprintf(stderr, "text_editor_create returned null\n"); return 1; } + + fn::text_editor_set_text(ed, "void main(){}\n"); + const char* got = fn::text_editor_get_text(ed); + std::printf("text_editor: get_text -> %zu bytes\n", got ? std::strlen(got) : 0u); + + if (fn::text_editor_is_dirty(ed)) { + std::fprintf(stderr, "text_editor: dirty unexpected after set_text\n"); + return 1; + } + fn::text_editor_destroy(ed); + + // ----- file_watcher ----- + const char* path = "/tmp/fn_smoke_test.txt"; + std::remove(path); + { + FILE* f = std::fopen(path, "w"); std::fputs("init\n", f); std::fclose(f); + } + + auto* fw = fn::file_watcher_create(); + if (!fw) { std::fprintf(stderr, "file_watcher_create returned null\n"); return 1; } + + if (!fn::file_watcher_add(fw, path)) { + std::fprintf(stderr, "file_watcher_add failed: %s\n", fn::file_watcher_last_error(fw)); + // Aun asi continuamos: en CI sin inotify (raro) este test seria flaky. + } + + // Modificar + { + FILE* f = std::fopen(path, "w"); std::fputs("changed\n", f); std::fclose(f); + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + auto evs = fn::file_watcher_poll(fw); + std::printf("file_watcher: %zu events\n", evs.size()); + for (auto& e : evs) { + const char* kind = e.kind == fn::FileEvent::Modified ? "MOD" + : e.kind == fn::FileEvent::Created ? "NEW" : "DEL"; + std::printf(" [%s] %s\n", kind, e.path.c_str()); + } + + fn::file_watcher_destroy(fw); + std::remove(path); + std::printf("OK\n"); + return 0; +} From 087412d73af258530884fa7a7010614fa0bbae2d Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:00:55 +0200 Subject: [PATCH 5/6] feat(primitives_gallery): wire text_editor + file_watcher demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - demos_text_editor.cpp: split horizontal con editor GLSL precargado a la izquierda (boton Save to /tmp/fn_demo.glsl + dirty indicator) y panel de eventos a la derecha (path, active flag, lista scrollable, boton clear). Watcher activo sobre /tmp/fn_demo.glsl; reintenta el add() tras el primer Save si el archivo no existia al iniciar. - demos.h: declaracion de gallery::demo_text_editor() - main.cpp: entry "text_editor"/"text_editor + watcher" en categoria Core - CMakeLists.txt: anade demos_text_editor.cpp + sources de text_editor, file_watcher y vendor TextEditor.cpp + include path de imgui_text_edit Nota: la primitives_gallery NO se construye en este branch (sus deps — button.cpp, toolbar.cpp, etc. — son untracked en master). El subdirectorio se anade pero protegido por FN_BUILD_GALLERY=OFF para no romper builds. Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/apps/primitives_gallery/CMakeLists.txt | 60 +++ cpp/apps/primitives_gallery/README.md | 159 +++++++ cpp/apps/primitives_gallery/demo.cpp | 76 +++ cpp/apps/primitives_gallery/demo.h | 22 + cpp/apps/primitives_gallery/demos.h | 37 ++ cpp/apps/primitives_gallery/demos_core.cpp | 447 ++++++++++++++++++ cpp/apps/primitives_gallery/demos_gfx.cpp | 123 +++++ cpp/apps/primitives_gallery/demos_graph.cpp | 204 ++++++++ .../primitives_gallery/demos_text_editor.cpp | 219 +++++++++ cpp/apps/primitives_gallery/demos_viz.cpp | 211 +++++++++ cpp/apps/primitives_gallery/main.cpp | 159 +++++++ 11 files changed, 1717 insertions(+) create mode 100644 cpp/apps/primitives_gallery/CMakeLists.txt create mode 100644 cpp/apps/primitives_gallery/README.md create mode 100644 cpp/apps/primitives_gallery/demo.cpp create mode 100644 cpp/apps/primitives_gallery/demo.h create mode 100644 cpp/apps/primitives_gallery/demos.h create mode 100644 cpp/apps/primitives_gallery/demos_core.cpp create mode 100644 cpp/apps/primitives_gallery/demos_gfx.cpp create mode 100644 cpp/apps/primitives_gallery/demos_graph.cpp create mode 100644 cpp/apps/primitives_gallery/demos_text_editor.cpp create mode 100644 cpp/apps/primitives_gallery/demos_viz.cpp create mode 100644 cpp/apps/primitives_gallery/main.cpp diff --git a/cpp/apps/primitives_gallery/CMakeLists.txt b/cpp/apps/primitives_gallery/CMakeLists.txt new file mode 100644 index 00000000..7fb2bae9 --- /dev/null +++ b/cpp/apps/primitives_gallery/CMakeLists.txt @@ -0,0 +1,60 @@ +add_imgui_app(primitives_gallery + main.cpp + demo.cpp + demos_core.cpp + demos_viz.cpp + demos_graph.cpp + demos_gfx.cpp + demos_text_editor.cpp + # text_editor + file_watcher (issue 0025) + ${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp + ${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit/TextEditor.cpp + # Core primitives demoed (tokens vive en fn_framework) + ${CMAKE_SOURCE_DIR}/functions/core/fullscreen_window.cpp + ${CMAKE_SOURCE_DIR}/functions/core/page_header.cpp + ${CMAKE_SOURCE_DIR}/functions/core/dashboard_panel.cpp + ${CMAKE_SOURCE_DIR}/functions/core/badge.cpp + ${CMAKE_SOURCE_DIR}/functions/core/empty_state.cpp + ${CMAKE_SOURCE_DIR}/functions/core/button.cpp + ${CMAKE_SOURCE_DIR}/functions/core/icon_button.cpp + ${CMAKE_SOURCE_DIR}/functions/core/toolbar.cpp + ${CMAKE_SOURCE_DIR}/functions/core/modal_dialog.cpp + ${CMAKE_SOURCE_DIR}/functions/core/text_input.cpp + ${CMAKE_SOURCE_DIR}/functions/core/select.cpp + ${CMAKE_SOURCE_DIR}/functions/core/toast.cpp + ${CMAKE_SOURCE_DIR}/functions/core/tree_view.cpp + ${CMAKE_SOURCE_DIR}/functions/core/process_runner.cpp + # Viz primitives demoed + ${CMAKE_SOURCE_DIR}/functions/viz/kpi_card.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/bar_chart.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/pie_chart.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/line_plot.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/scatter_plot.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/histogram.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/sparkline.cpp + # Graph stack (instanced GPU + Barnes-Hut + spatial hash) + ${CMAKE_SOURCE_DIR}/functions/viz/graph_types.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/graph_renderer.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/graph_force_layout.cpp + ${CMAKE_SOURCE_DIR}/functions/viz/graph_viewport.cpp + ${CMAKE_SOURCE_DIR}/functions/core/graph_spatial_hash.cpp + # GL loader (Linux no-op, Windows wglGetProcAddress) + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_loader.cpp + # Shader stack (shader_canvas demo) + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_shader.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/gl_framebuffer.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/fullscreen_quad.cpp + ${CMAKE_SOURCE_DIR}/functions/gfx/shader_canvas.cpp +) +target_include_directories(primitives_gallery PRIVATE + ${CMAKE_SOURCE_DIR}/vendor/imgui_text_edit +) + +if(WIN32) + target_link_libraries(primitives_gallery PRIVATE opengl32) +endif() + +if(WIN32) + set_target_properties(primitives_gallery PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/cpp/apps/primitives_gallery/README.md b/cpp/apps/primitives_gallery/README.md new file mode 100644 index 00000000..59e6fec4 --- /dev/null +++ b/cpp/apps/primitives_gallery/README.md @@ -0,0 +1,159 @@ +# primitives_gallery + +Catalogo visual interactivo de los primitivos UI del registry (`cpp/functions/core` y `cpp/functions/viz`). Un solo ejecutable con sidebar izquierdo + panel derecho que renderiza la demo del primitivo seleccionado con todas sus variantes y un snippet de codigo. + +## Rol + +| Funcion | Como lo cumple | +|---|---| +| Smoke test visual | Abrir la gallery tras un cambio en tokens / componentes; si algo se ve raro, lo cazas en segundos. | +| Documentacion viva | Cada demo muestra el componente trabajando + el snippet exacto. Mas rapido que leer los `.md`. | +| Build gate | Esta en el CMake principal (`cpp/CMakeLists.txt`). Si un primitivo rompe API, la gallery no compila => CI rojo. | +| Sandbox de prototipos | Datos sinteticos, sin backend; ideal para iterar un primitivo nuevo sin tocar el dashboard. | + +## Build & run + +```bash +# Linux +cmake --build cpp/build/linux --target primitives_gallery -j$(nproc) +./cpp/build/linux/apps/primitives_gallery/primitives_gallery + +# Windows (cross-compile) +cmake --build cpp/build/windows --target primitives_gallery -j$(nproc) +# binario: cpp/build/windows/apps/primitives_gallery/primitives_gallery.exe +``` + +No se conecta a `sqlite_api` ni a ningun backend. Datos sinteticos generados in-memory. + +## Demos disponibles + +### Core + +| Demo | Primitivo | Que muestra | +|---|---|---| +| button | `button_cpp_core` | 4 variantes x 3 sizes | +| icon_button | `icon_button_cpp_core` | Glyphs comunes con tooltip | +| toolbar | `toolbar_cpp_core` | Dos grupos con separador vertical | +| modal_dialog | `modal_dialog_cpp_core` | Boton que abre modal con form | +| text_input | `text_input_cpp_core` | 3 inputs con placeholder | +| select | `select_cpp_core` | Dropdown con y sin `(none)` | +| toast + inbox | `toast_cpp_core` (v1.1) | 4 botones que disparan toasts + campana con badge | +| tree_view | `tree_view_cpp_core` | Arbol fake de proyectos -> apps | +| badge | `badge_cpp_core` | 6 variantes semanticas | +| empty_state | `empty_state_cpp_core` | Lista vacia con icono + cta | +| page_header | `page_header_cpp_core` | Header con toolbar a la derecha | +| dashboard_panel | `dashboard_panel_cpp_core` | Panel con titulo y borde | +| kpi_card | `kpi_card_cpp_viz` (v1.2) | Grid 1x4 con sparklines y delta | + +### Viz + +| Demo | Primitivo | Que muestra | +|---|---|---| +| bar_chart | `bar_chart_cpp_viz` (v1.2) | Labels que caben + labels rotados 45 | +| pie_chart | `pie_chart_cpp_viz` (v1.1) | Pie + donut con tooltip por slice | +| line_plot | `line_plot_cpp_viz` (v1.1) | Serie sintetica `sin(t) + ruido` | +| scatter_plot | `scatter_plot_cpp_viz` (v1.1) | 120 puntos con correlacion | +| histogram | `histogram_cpp_viz` (v1.1) | 300 muestras gaussianas | +| sparkline | `sparkline_cpp_viz` | Trending up / down / flat | +| graph_viewport | `graph_viewport_cpp_viz` | **Ver seccion abajo** | + +## Demo `graph_viewport` (en detalle) + +Pipeline completo de visualizacion de grafos con instanced GPU rendering: +- `graph_renderer_cpp_viz` (1 draw call para todos los nodos via `glDrawArraysInstanced`) +- `graph_force_layout_cpp_viz` (Barnes-Hut, paso de simulacion por frame) +- `graph_spatial_hash_cpp_core` (hit-testing O(1) bajo el cursor) +- `graph_viewport_cpp_viz` (widget que orquesta los anteriores con pan/zoom/select) + +### Controles + +| Control | Rango | Efecto | +|---|---|---| +| `Nodes` | 100 – 20 000 | Numero de nodos a generar | +| `Clusters` | 2 – 16 | Numero de comunidades (cada una con su color) | +| `Repulsion` | 100 – 20 000 | Fuerza repulsiva entre todos los nodos. Mas alto => grafo mas extendido y energia mayor. | +| `Attraction` | 0.001 – 0.5 | Constante del muelle de las aristas. Mas alto => clusters mas compactos. | +| `Gravity` | 0.0 – 0.05 | Tiron hacia (0,0). Util para evitar drift cuando subes mucho la repulsion. | +| `Regenerate` | boton | Regenera el grafo con los valores actuales de Nodes/Clusters. | +| `Pause / Resume layout` | boton | Para o reanuda la simulacion force-directed. | +| `Fit view` | boton | Encuadra la camara al bounding box del grafo con 10% de padding. | + +Los tres sliders de fuerzas se leen cada frame y se inyectan en `ForceLayoutConfig`, asi que cambiar un valor durante el layout en marcha re-calibra el sistema al instante. + +### Stats line (sin vibracion) + +Una sola linea fija — sin secciones condicionales que cambien la altura del panel: + +``` +nodes=N edges=E energy=X fps=F | hover=#id cN sel=#id +``` + +`hover` y `sel` muestran `-` cuando no hay nada seleccionado para mantener el ancho/alto estable; antes una fila condicional desplazaba el viewport en cada hover. + +### Interaccion con el viewport + +| Gesto | Accion | +|---|---| +| Drag con boton izquierdo en zona vacia | Pan de camara | +| Wheel | Zoom (limites 0.01x – 50x) | +| Drag sobre nodo | Mueve el nodo (lo `pin`ea durante el drag) | +| Click sobre nodo | Selecciona (`s_state.selected_node`) | +| Hover sobre nodo | Resaltado + `s_state.hovered_node` poblado | + +### Datos sinteticos + +`generate_synthetic_graph(N, K)` reparte N nodos en K clusters dispuestos en circulo, con ~3 aristas intra-cluster por nodo y un 5% adicional de aristas inter-cluster. Paleta de 8 colores ABGR. Posiciones iniciales con dispersion gaussiana de 80 px alrededor del centroide del cluster — el force layout las reordena en pocos frames. + +### Performance esperada + +| Nodes | FPS objetivo (RTX 30xx, viewport 800x460) | Notas | +|---|---|---| +| 1 000 | 60 (vsync) | Caso comun; layout converge < 1 s | +| 5 000 | 60 | Pipeline al limite del CPU para Barnes-Hut | +| 20 000 | 30 – 50 | El cuello pasa a ser el layout (CPU); GPU render sigue holgado | + +Si necesitas mas, fija los nodos (`pinned = true` o `Pause layout`) y veras 60 fps estables — el bottleneck es la simulacion, no el render. + +## Anadir un demo nuevo + +1. Anadir el prototipo en `demos.h` dentro de `namespace gallery`: + ```cpp + void demo_my_thing(); + ``` +2. Implementar el cuerpo en `demos_core.cpp` o `demos_viz.cpp` (o un fichero nuevo si la demo es grande, p.ej. `demos_graph.cpp`). +3. Registrar la entrada en el array `k_demos[]` de `main.cpp`: + ```cpp + {"my_thing", "my_thing", "Core" /* o "Viz" */, &gallery::demo_my_thing}, + ``` +4. Si la demo necesita `.cpp` adicionales del registry, anadirlos a `CMakeLists.txt` de la gallery. +5. Recompilar. + +## Estructura + +``` +cpp/apps/primitives_gallery/ + CMakeLists.txt # target primitives_gallery + README.md # este fichero + main.cpp # sidebar + router + demo.{h,cpp} # helpers (demo_header, section, code_block, ...) + demos.h # prototipos void demo_xxx() + demos_core.cpp # demos del dominio core + demos_viz.cpp # demos del dominio viz (charts simples) + demos_graph.cpp # demo de graph_viewport (mas pesada, fichero aparte) +``` + +## Convenciones para los demos + +- **Sin estado real**: usar arrays sinteticos (`float fake[] = {...}`) o generadores deterministas con seed fijo. Datos reproducibles. +- **Sin red**: nunca llamar a `sqlite_api`, HTTP, filesystem. La gallery debe arrancar offline en cualquier maquina. +- **Snippets honestos**: el `code_block(...)` debe mostrar el codigo que produce esa demo, no pseudocodigo. +- **Variantes en grids**: si un primitivo tiene N variantes x M tamanos, mostrarlos todos en un `BeginTable` para comparacion lado-a-lado. +- **Estado static**: si la demo es interactiva (sliders, modal, etc.), guardar el estado en `static` locales — la gallery no destruye demos al cambiar de seccion, asi que el estado persiste hasta cerrar la app. + +## Iconos en los demos + +A partir de la sesion 2026-04-25 los demos usan los macros `TI_*` de `cpp/functions/core/icons_tabler.h` (Tabler v3.41.1, 5093 glyphs). La fuente la carga automaticamente `fn::run_app` via `icon_font_cpp_core`, y `add_imgui_app` copia `tabler-icons.ttf` junto al ejecutable post-build (no hay paso manual). + +`demo_icon_button` y `demo_toolbar` (en `demos_core.cpp`) son la referencia visual: muestran el patron `button(TI_PLUS " New", V::Primary)` y la fila de iconos sueltos. Ver `cpp/DESIGN_SYSTEM.md` seccion 11 para la regla. + +Si añades un demo nuevo y necesitas glyphs, **no metas `\x..` UTF-8 inline** — busca el icono en `icons_tabler.h` (o en https://tabler.io/icons) y usa el `TI_*` correspondiente. diff --git a/cpp/apps/primitives_gallery/demo.cpp b/cpp/apps/primitives_gallery/demo.cpp new file mode 100644 index 00000000..d2ab17a0 --- /dev/null +++ b/cpp/apps/primitives_gallery/demo.cpp @@ -0,0 +1,76 @@ +#include "demo.h" +#include "core/tokens.h" +#include + +namespace gallery { + +void demo_header(const char* name, const char* version, const char* description) { + using namespace fn_tokens; + + ImGui::SetWindowFontScale(1.4f); + ImGui::TextUnformatted(name); + ImGui::SetWindowFontScale(1.0f); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::Text(" %s", version); + ImGui::PopStyleColor(); + + if (description && *description) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextWrapped("%s", description); + ImGui::PopStyleColor(); + } + ImGui::Separator(); + ImGui::Dummy(ImVec2(0, spacing::sm)); +} + +void section(const char* title) { + using namespace fn_tokens; + ImGui::Dummy(ImVec2(0, spacing::sm)); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::TextUnformatted(title); + ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::Dummy(ImVec2(0, spacing::xs)); +} + +void variant_label(const char* text) { + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_dim); + ImGui::TextUnformatted(text); + ImGui::PopStyleColor(); +} + +void code_block(const char* code) { + using namespace fn_tokens; + ImGui::Dummy(ImVec2(0, spacing::sm)); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("// example"); + ImGui::PopStyleColor(); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, colors::bg); + ImGui::PushStyleColor(ImGuiCol_Border, colors::border); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, radius::sm); + ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(spacing::md, spacing::sm)); + + // Altura: aprox lineas * line-height + int lines = 1; + for (const char* p = code; *p; ++p) if (*p == '\n') ++lines; + float h = lines * ImGui::GetTextLineHeightWithSpacing() + spacing::md; + + char id[32]; + std::snprintf(id, sizeof(id), "##code_%p", (const void*)code); + ImGui::BeginChild(id, ImVec2(0, h), + ImGuiChildFlags_Borders, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text); + ImGui::TextUnformatted(code); + ImGui::PopStyleColor(); + ImGui::EndChild(); + + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(2); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demo.h b/cpp/apps/primitives_gallery/demo.h new file mode 100644 index 00000000..94177743 --- /dev/null +++ b/cpp/apps/primitives_gallery/demo.h @@ -0,0 +1,22 @@ +#pragma once +// Helpers compartidos por todas las demos de la gallery. +// No son primitivos del registry — son utilidades locales de este app. + +#include "imgui.h" +#include + +namespace gallery { + +// Titulo + version + descripcion en la parte superior del panel derecho. +void demo_header(const char* name, const char* version, const char* description); + +// Seccion secundaria dentro de una demo (agrupar variantes). +void section(const char* title); + +// Bloque de codigo monoespaciado con bg surface y label "// example". +void code_block(const char* code); + +// Etiqueta sutil encima de un grupo de widgets. +void variant_label(const char* text); + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos.h b/cpp/apps/primitives_gallery/demos.h new file mode 100644 index 00000000..4fe67342 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos.h @@ -0,0 +1,37 @@ +#pragma once +// Cada demo_xxx() renderiza una seccion completa para un primitivo. +// Se llaman desde main.cpp en funcion del item seleccionado en el sidebar. + +namespace gallery { + +// --- Core --- +void demo_button(); +void demo_icon_button(); +void demo_toolbar(); +void demo_modal(); +void demo_text_input(); +void demo_select(); +void demo_toast(); +void demo_tree_view(); +void demo_kpi_card(); +void demo_badge(); +void demo_empty_state(); +void demo_page_header(); +void demo_dashboard_panel(); + +// --- Viz --- +void demo_bar_chart(); +void demo_pie_chart(); +void demo_line_plot(); +void demo_scatter_plot(); +void demo_histogram(); +void demo_sparkline(); +void demo_graph(); + +// --- Gfx --- +void demo_shader_canvas(); + +// --- Core (combined demo: text_editor + file_watcher) --- +void demo_text_editor(); + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_core.cpp b/cpp/apps/primitives_gallery/demos_core.cpp new file mode 100644 index 00000000..3d2566bd --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_core.cpp @@ -0,0 +1,447 @@ +#include "demos.h" +#include "demo.h" + +#include "core/button.h" +#include "core/icon_button.h" +#include "core/toolbar.h" +#include "core/modal_dialog.h" +#include "core/text_input.h" +#include "core/select.h" +#include "core/toast.h" +#include "core/tree_view.h" +#include "core/badge.h" +#include "core/empty_state.h" +#include "core/page_header.h" +#include "core/dashboard_panel.h" +#include "core/tokens.h" +#include "core/icons_tabler.h" +#include "viz/kpi_card.h" + +#include +#include + +using namespace fn_ui; +using V = ButtonVariant; +using S = ButtonSize; + +namespace gallery { + +// --------------------------------------------------------------------------- +// button +// --------------------------------------------------------------------------- + +void demo_button() { + demo_header("button", "v1.0.0", + "Boton con 4 variantes semanticas y 3 tamanos. Usa tokens para colores, " + "radius y padding — estilo consistente en toda la app."); + + section("Variants x Sizes"); + const V variants[] = {V::Primary, V::Secondary, V::Subtle, V::Danger}; + const char* variant_names[] = {"Primary", "Secondary", "Subtle", "Danger"}; + const S sizes[] = {S::Sm, S::Md, S::Lg}; + const char* size_names[] = {"sm", "md", "lg"}; + + if (ImGui::BeginTable("##btn_grid", 5, ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("size"); + for (int c = 0; c < 4; c++) ImGui::TableSetupColumn(variant_names[c]); + ImGui::TableHeadersRow(); + + for (int s = 0; s < 3; s++) { + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + variant_label(size_names[s]); + for (int v = 0; v < 4; v++) { + ImGui::TableSetColumnIndex(v + 1); + char id[32]; + std::snprintf(id, sizeof(id), "%s##%d%d", variant_names[v], s, v); + button(id, variants[v], sizes[s]); + } + } + ImGui::EndTable(); + } + + code_block( + "#include \"core/button.h\"\n" + "using fn_ui::button;\n" + "using V = fn_ui::ButtonVariant;\n\n" + "if (button(\"Save\", V::Primary)) save();\n" + "if (button(\"Cancel\", V::Subtle)) close();\n" + "if (button(\"Delete\", V::Danger)) confirm();" + ); +} + +// --------------------------------------------------------------------------- +// icon_button +// --------------------------------------------------------------------------- + +void demo_icon_button() { + demo_header("icon_button", "v1.0.0", + "Boton cuadrado 28x28 con un glyph centrado y tooltip opcional. " + "Usa los TI_* de core/icons_tabler.h (Tabler Icons cargado automaticamente " + "por fn::run_app via icon_font.cpp)."); + + section("Tabler icon set"); + struct { const char* id; const char* glyph; const char* tip; } ic[] = { + {"##rl", TI_REFRESH, "Reload"}, + {"##ad", TI_PLUS, "Add"}, + {"##dl", TI_TRASH, "Delete"}, + {"##dn", TI_CHEVRON_DOWN, "Dropdown"}, + {"##cf", TI_SETTINGS, "Settings"}, + {"##ok", TI_CHECK, "Check"}, + {"##cl", TI_X, "Close"}, + {"##ed", TI_PENCIL, "Edit"}, + {"##sv", TI_DEVICE_FLOPPY, "Save"}, + {"##sr", TI_SEARCH, "Search"}, + {"##hp", TI_HELP, "Help"}, + {"##hm", TI_HOME, "Home"}, + }; + for (auto& b : ic) { + icon_button(b.id, b.glyph, b.tip); + ImGui::SameLine(); + } + ImGui::NewLine(); + + code_block( + "#include \"core/icons_tabler.h\"\n\n" + "if (icon_button(\"##reload\", TI_REFRESH, \"Reload\"))\n" + " reload_data();\n\n" + "// Mas de 5000 iconos disponibles — ver core/icons_tabler.h" + ); +} + +// --------------------------------------------------------------------------- +// toolbar +// --------------------------------------------------------------------------- + +void demo_toolbar() { + demo_header("toolbar", "v1.0.0", + "Grupo horizontal con spacing consistente y separadores verticales sutiles. " + "El caller usa ImGui::SameLine entre items y toolbar_separator entre grupos."); + + section("Example with two groups"); + toolbar_begin(); + button(TI_PLUS " New", V::Primary); ImGui::SameLine(); + button(TI_FOLDER_OPEN " Open", V::Secondary); ImGui::SameLine(); + button(TI_DEVICE_FLOPPY " Save",V::Secondary); + toolbar_separator(); + icon_button("##set", TI_SETTINGS, "Settings"); + ImGui::SameLine(); + icon_button("##help", TI_HELP, "Help"); + toolbar_end(); + + code_block( + "#include \"core/icons_tabler.h\"\n\n" + "toolbar_begin();\n" + " button(TI_PLUS \" New\", V::Primary); ImGui::SameLine();\n" + " button(TI_FOLDER_OPEN \" Open\", V::Secondary);\n" + " toolbar_separator();\n" + " icon_button(\"##set\", TI_SETTINGS, \"Settings\");\n" + "toolbar_end();" + ); +} + +// --------------------------------------------------------------------------- +// modal_dialog +// --------------------------------------------------------------------------- + +void demo_modal() { + demo_header("modal_dialog", "v1.0.0", + "Popup modal centrada con estilo surface+border. Close con Escape o click en X. " + "Patron begin/end — modal_dialog_end debe llamarse siempre."); + + static bool show = false; + if (button("Open modal", V::Primary)) show = true; + + if (modal_dialog_begin("Demo modal", &show, ImVec2(380, 0))) { + ImGui::TextWrapped( + "Modal centrada en el viewport principal, con estilo tokens."); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::sm)); + static char buf[64] = {}; + text_input("Name", buf, sizeof(buf), "escribe algo"); + ImGui::Separator(); + if (button("Cancel", V::Subtle)) show = false; + ImGui::SameLine(); + if (button("Done", V::Primary)) show = false; + } + modal_dialog_end(); + + code_block( + "static bool show = false;\n" + "if (button(\"Open\", Primary)) show = true;\n" + "if (modal_dialog_begin(\"Title\", &show, ImVec2(380,0))) {\n" + " // ... campos del form ...\n" + " if (button(\"Done\", Primary)) show = false;\n" + "}\n" + "modal_dialog_end();" + ); +} + +// --------------------------------------------------------------------------- +// text_input +// --------------------------------------------------------------------------- + +void demo_text_input() { + demo_header("text_input", "v1.0.0", + "Label muted + input estilizado con tokens. Full-width dentro del contenedor. " + "Placeholder opcional mostrado en text_dim cuando el buffer esta vacio."); + + static char name[128] = {}; + static char desc[256] = {}; + static char tags[128] = {}; + + ImGui::BeginChild("##ti_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY); + text_input("Name", name, sizeof(name), "my-new-thing"); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs)); + text_input("Description", desc, sizeof(desc)); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs)); + text_input("Tags (CSV)", tags, sizeof(tags), "imgui,ui,form"); + ImGui::EndChild(); + + code_block( + "static char name[128] = {};\n" + "text_input(\"Name\", name, sizeof(name), \"my-new-thing\");\n" + "// true on change — se usa mas para validar en vivo\n" + "// que para leer el valor (que vive en el buffer)." + ); +} + +// --------------------------------------------------------------------------- +// select +// --------------------------------------------------------------------------- + +void demo_select() { + demo_header("select", "v1.0.0", + "Dropdown con label muted y opcion (none) opcional. Mismo estilo tokens que text_input."); + + static int lang_idx = 0; + static int domain_idx = -1; + const char* langs[] = {"go", "py", "ts", "sh", "cpp"}; + const char* domains[] = {"core", "infra", "finance", "datascience", "viz"}; + + ImGui::BeginChild("##sl_wrap", ImVec2(420, 0), ImGuiChildFlags_AutoResizeY); + select("Language", &lang_idx, langs, 5); + ImGui::Dummy(ImVec2(0, fn_tokens::spacing::xs)); + select("Domain (optional)", &domain_idx, domains, 5, true); + ImGui::EndChild(); + + code_block( + "static int lang = 0;\n" + "const char* langs[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n" + "select(\"Language\", &lang, langs, 5);" + ); +} + +// --------------------------------------------------------------------------- +// toast + inbox +// --------------------------------------------------------------------------- + +void demo_toast() { + demo_header("toast", "v1.1.0", + "Notificaciones efimeras (~3.5s con fade-out) + inbox con campana. " + "La campana muestra badge con no-leidos y popover con las ultimas 50."); + + section("Trigger toasts"); + if (button("Info", V::Secondary)) toast_push(ToastKind::Info, "this is an info toast"); + ImGui::SameLine(); + if (button("Success", V::Primary)) toast_push(ToastKind::Success, "operation completed"); + ImGui::SameLine(); + if (button("Warning", V::Secondary)) toast_push(ToastKind::Warning, "heads up about something"); + ImGui::SameLine(); + if (button("Error", V::Danger)) toast_push(ToastKind::Error, "operation failed: reason"); + + section("Inbox (bell with unread badge)"); + toast_inbox_button("##inbox_demo"); + + code_block( + "toast_push(ToastKind::Success, \"Reindexed 891 functions\");\n" + "toast_push(ToastKind::Error, \"HTTP 503: server down\");\n\n" + "// En la toolbar:\n" + "toast_inbox_button(\"##inbox\");\n\n" + "// Una vez por frame al final del render:\n" + "toast_render();" + ); +} + +// --------------------------------------------------------------------------- +// tree_view +// --------------------------------------------------------------------------- + +void demo_tree_view() { + demo_header("tree_view", "v1.0.0", + "Tree low-level para jerarquias (ej. projects -> apps/analysis/vaults). " + "Sin estado interno: el caller gestiona seleccion y pasa 'selected' por parametro."); + + static std::string selected; + + section("Projects (fake)"); + ImGui::BeginChild("##tv", ImVec2(360, 200), ImGuiChildFlags_Borders); + + struct FakeProject { const char* id; const char* name; const char* apps[3]; }; + const FakeProject projs[] = { + {"app_turismo", "app_turismo", {"guide_es", "offline_maps", nullptr}}, + {"element_agents", "element_agents", {"matrix_bot", nullptr, nullptr}}, + {"fn_monitoring", "fn_monitoring", {"sqlite_api", "registry_dashboard", nullptr}}, + }; + for (auto& p : projs) { + bool sel = (selected == p.id); + if (tree_branch_begin(p.id, p.name, sel)) { + if (tree_node_clicked()) selected = p.id; + for (int i = 0; i < 3 && p.apps[i]; i++) { + bool asel = (selected == p.apps[i]); + tree_leaf(p.apps[i], p.apps[i], asel); + if (tree_node_clicked()) selected = p.apps[i]; + } + tree_branch_end(); + } else if (tree_node_clicked()) { + selected = p.id; + } + } + ImGui::EndChild(); + + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::Text("Selected: %s", selected.empty() ? "(none)" : selected.c_str()); + ImGui::PopStyleColor(); + + code_block( + "static std::string sel;\n" + "if (tree_branch_begin(p.id, p.name, sel == p.id)) {\n" + " if (tree_node_clicked()) sel = p.id;\n" + " for (auto& a : p.apps) {\n" + " tree_leaf(a.id, a.name, sel == a.id);\n" + " if (tree_node_clicked()) sel = a.id;\n" + " }\n" + " tree_branch_end();\n" + "}" + ); +} + +// --------------------------------------------------------------------------- +// kpi_card +// --------------------------------------------------------------------------- + +void demo_kpi_card() { + demo_header("kpi_card", "v1.3.0", + "Card compacta 86px con icono opcional + label muted, valor x1.4, trend con " + "TI_TRENDING_UP/DOWN y sparkline. Usa tokens: surface bg, border, radius md."); + + if (ImGui::BeginTable("##kpi_grid", 4, ImGuiTableFlags_SizingStretchSame)) { + float history[] = {10, 12, 11, 15, 18, 17, 20}; + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); kpi_card("Revenue", 20000.0f, 12.5f, history, 7, "$%.0f", TI_CASH); + ImGui::TableSetColumnIndex(1); kpi_card("Users", 1250.0f, 3.4f, history, 7, "%.0f", TI_USERS); + ImGui::TableSetColumnIndex(2); kpi_card("Churn", 2.1f, -0.3f, history, 7, "%.1f%%", TI_CHART_BAR); + ImGui::TableSetColumnIndex(3); kpi_card("Errors", 0.0f, 0.0f, nullptr, 0, "%.0f", TI_ALERT_CIRCLE); + ImGui::EndTable(); + } + + code_block( + "#include \"core/icons_tabler.h\"\n\n" + "float history[] = {10,12,11,15,18,17,20};\n" + "kpi_card(\"Revenue\", 20000.0f, 12.5f, history, 7, \"$%.0f\", TI_CASH);\n" + "kpi_card(\"Users\", 1250.0f, 3.4f, history, 7, \"%.0f\", TI_USERS);\n" + "// Sin delta ni history: muestra TI_MINUS como placeholder\n" + "kpi_card(\"Errors\", 0.0f, 0.0f, nullptr, 0, \"%.0f\", TI_ALERT_CIRCLE);" + ); +} + +// --------------------------------------------------------------------------- +// badge +// --------------------------------------------------------------------------- + +void demo_badge() { + demo_header("badge", "v1.0.0", + "Etiqueta inline con 6 variantes semanticas. Equivalente a de fn_library."); + + section("Variants"); + badge("Default", BadgeVariant::Default); ImGui::SameLine(); + badge("Success", BadgeVariant::Success); ImGui::SameLine(); + badge("Warning", BadgeVariant::Warning); ImGui::SameLine(); + badge("Error", BadgeVariant::Error); ImGui::SameLine(); + badge("Info", BadgeVariant::Info); ImGui::SameLine(); + badge("Outline", BadgeVariant::Outline); + + section("In context (table row)"); + ImGui::Text("filter_slice_go_core"); ImGui::SameLine(); + badge("pure", BadgeVariant::Success); ImGui::SameLine(); + badge("tested", BadgeVariant::Info); + + code_block( + "badge(\"pure\", BadgeVariant::Success);\n" + "badge(\"stale\", BadgeVariant::Warning);\n" + "badge(\"broken\", BadgeVariant::Error);" + ); +} + +// --------------------------------------------------------------------------- +// empty_state +// --------------------------------------------------------------------------- + +void demo_empty_state() { + demo_header("empty_state", "v1.0.0", + "Icono grande muted + titulo + descripcion opcional. Para listas/tablas vacias."); + + ImGui::BeginChild("##es", ImVec2(0, 180), ImGuiChildFlags_Borders); + empty_state("( no data )", "No projects yet", + "Create one under projects/{name}/ with project.md and reindex"); + ImGui::EndChild(); + + code_block( + "if (apps.empty()) {\n" + " empty_state(\"( no data )\", \"No apps yet\",\n" + " \"Click + Add to create one\");\n" + " return;\n" + "}" + ); +} + +// --------------------------------------------------------------------------- +// page_header +// --------------------------------------------------------------------------- + +void demo_page_header() { + demo_header("page_header", "v1.0.0", + "Header de pagina con titulo, subtitulo opcional y separador final. " + "Patron begin/end permite insertar acciones entre titulo y separador."); + + page_header_begin("Dashboard", "13 apps, 3 projects, 2 analyses"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140.0f); + toolbar_begin(); + button("Reload", V::Subtle); ImGui::SameLine(); + button("+ Add", V::Secondary); + toolbar_end(); + page_header_end(); + + code_block( + "page_header_begin(\"Dashboard\", subtitle);\n" + "ImGui::SameLine(ImGui::GetContentRegionAvail().x - 140);\n" + "toolbar_begin();\n" + " button(\"Reload\", Subtle);\n" + "toolbar_end();\n" + "page_header_end();" + ); +} + +// --------------------------------------------------------------------------- +// dashboard_panel +// --------------------------------------------------------------------------- + +void demo_dashboard_panel() { + demo_header("dashboard_panel", "v1.0.0", + "Contenedor tipo panel con titulo, bordes redondeados, bg surface. " + "Auto-resize-Y segun contenido. Usa min_width/min_height como piso."); + + if (dashboard_panel_begin("Revenue", 0, 120.0f)) { + ImGui::Text("Some panel content goes here."); + ImGui::Text("Anything drawn inside lives in the child window."); + } + dashboard_panel_end(); + + code_block( + "if (dashboard_panel_begin(\"Revenue\", 0, 120.0f)) {\n" + " ImGui::Text(\"content\");\n" + "}\n" + "dashboard_panel_end();" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_gfx.cpp b/cpp/apps/primitives_gallery/demos_gfx.cpp new file mode 100644 index 00000000..60dcf6a6 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_gfx.cpp @@ -0,0 +1,123 @@ +// Demos del dominio gfx — primitivos OpenGL/shader que viven en +// cpp/functions/gfx/. La pieza distintiva de shaders_lab es el +// shader_canvas: framebuffer + fullscreen quad + programa GL animado por +// time/resolution/mouse. + +#include "demos.h" +#include "demo.h" + +#include "gfx/shader_canvas.h" +#include "gfx/gl_shader.h" +#include "gfx/gl_loader.h" + +#include +#include + +namespace gallery { + +namespace { + +// Fragment shader sintetico — gradiente animado con celdas. Usa los uniforms +// estandar que compile_fragment inyecta: u_resolution, u_time, u_mouse. +const char* kShaderSrc = R"( +void mainImage() { + vec2 uv = gl_FragCoord.xy / u_resolution; + vec2 cell = uv * 8.0; + vec2 ipos = floor(cell); + vec2 fpos = fract(cell) - 0.5; + + float t = u_time * 0.6; + float wave = sin(ipos.x * 0.7 + ipos.y * 0.5 + t); + float dist = length(fpos); + + vec3 a = vec3(0.30, 0.43, 0.96); // indigo + vec3 b = vec3(0.95, 0.45, 0.85); // pink + vec3 col = mix(a, b, 0.5 + 0.5 * wave); + + // Mouse focus: oscurecemos celdas lejanas al cursor. + vec2 m = u_mouse / u_resolution; + float fm = 1.0 - smoothstep(0.0, 0.6, length(uv - m)); + col *= 0.6 + 0.4 * fm; + + // Disco interior por celda con borde suave. + col *= smoothstep(0.5, 0.45, dist); + + fragColor = vec4(col, 1.0); +} + +void main() { + mainImage(); +} +)"; + +struct CanvasState { + fn::gfx::ShaderCanvas canvas; + bool compiled = false; + bool compile_failed = false; + std::string err_msg; + std::chrono::steady_clock::time_point t0; +}; + +CanvasState& state() { + static CanvasState s; + return s; +} + +} // namespace + +void demo_shader_canvas() { + demo_header("shader_canvas", "v1.0.0", + "Framebuffer + fullscreen quad + shader GLSL animado. La misma pieza " + "que usa shaders_lab para el preview en vivo. Uniforms u_time / u_resolution / u_mouse " + "los inyecta gl_shader::compile_fragment automaticamente."); + + auto& s = state(); + + // Compilacion lazy (en el primer frame ya hay contexto GL valido). + if (!s.compiled && !s.compile_failed) { + fn::gfx::gl_loader_init(); + fn::gfx::canvas_init(s.canvas); + + auto cr = fn::gfx::compile_fragment(kShaderSrc); + if (!cr.ok) { + s.compile_failed = true; + s.err_msg = cr.err_msg; + } else { + fn::gfx::canvas_set_program(s.canvas, cr.program); + s.t0 = std::chrono::steady_clock::now(); + s.compiled = true; + } + } + + if (s.compile_failed) { + ImGui::TextColored(ImVec4(1, 0.4f, 0.4f, 1), + "Compilacion del fragment shader fallo:\n%s", + s.err_msg.c_str()); + return; + } + + section("Live preview"); + + // Render del shader en un panel ~480x300 px. canvas_render hace resize + // automatico segun GetContentRegionAvail si lo dejas crecer. + ImGui::BeginChild("##shader_preview", ImVec2(480, 300), + ImGuiChildFlags_Borders); + const float dt = std::chrono::duration( + std::chrono::steady_clock::now() - s.t0).count(); + fn::gfx::canvas_render(s.canvas, dt); + ImGui::EndChild(); + + code_block( + "#include \"gfx/shader_canvas.h\"\n" + "#include \"gfx/gl_shader.h\"\n\n" + "static fn::gfx::ShaderCanvas canvas;\n" + "// Setup (una vez):\n" + "fn::gfx::canvas_init(canvas);\n" + "auto cr = fn::gfx::compile_fragment(user_glsl);\n" + "if (cr.ok) fn::gfx::canvas_set_program(canvas, cr.program);\n\n" + "// Cada frame, dentro de un Begin/End:\n" + "fn::gfx::canvas_render(canvas, time_seconds);" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_graph.cpp b/cpp/apps/primitives_gallery/demos_graph.cpp new file mode 100644 index 00000000..2311d431 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_graph.cpp @@ -0,0 +1,204 @@ +#include "demos.h" +#include "demo.h" + +#include "viz/graph_types.h" +#include "viz/graph_viewport.h" +#include "viz/graph_force_layout.h" +#include "core/button.h" +#include "core/tokens.h" + +#include +#include +#include +#include + +namespace gallery { + +// Genera un grafo sintetico con N nodos en K clusters + aristas intra-cluster +// y unas pocas inter-cluster. Pensado para demostrar el rendimiento del +// pipeline graph_renderer + graph_force_layout + graph_viewport. +static void generate_synthetic_graph(int N, int K, + std::vector& nodes_out, + std::vector& edges_out) { + nodes_out.clear(); + edges_out.clear(); + nodes_out.reserve(N); + edges_out.reserve(N * 3); + + unsigned seed = 0x1234abcd; + auto rnd = [&]() { + seed = seed * 1664525u + 1013904223u; + return static_cast((seed >> 8) & 0xffffff) / 16777216.0f; + }; + + // Paleta por cluster (ABGR) + const uint32_t palette[] = { + 0xff5b8def, 0xff58ca8c, 0xfff5973e, 0xffd95150, + 0xffb87fe0, 0xff5fcdcc, 0xfff2cd52, 0xff99d161, + }; + const int palette_n = sizeof(palette) / sizeof(palette[0]); + + // Asignar cluster + posicion inicial cerca del centroide del cluster + std::vector cluster_cx(K), cluster_cy(K); + for (int k = 0; k < K; k++) { + float angle = 2.0f * 3.14159f * k / K; + cluster_cx[k] = std::cos(angle) * 200.0f; + cluster_cy[k] = std::sin(angle) * 200.0f; + } + + for (int i = 0; i < N; i++) { + int k = i % K; + GraphNode n = graph_node(static_cast(i), + cluster_cx[k] + (rnd() - 0.5f) * 80.0f, + cluster_cy[k] + (rnd() - 0.5f) * 80.0f); + n.size = 3.0f + rnd() * 2.0f; + n.color = palette[k % palette_n]; + n.community = static_cast(k); + nodes_out.push_back(n); + } + + // Aristas: ~3 por nodo dentro del cluster, +5% inter-cluster. + auto add_edge = [&](uint32_t a, uint32_t b, float w) { + if (a == b) return; + edges_out.push_back(graph_edge(a, b, w)); + }; + int per_cluster = N / K; + for (int k = 0; k < K; k++) { + int base = k * per_cluster; + int end = (k == K - 1) ? N : (base + per_cluster); + int size = end - base; + if (size < 2) continue; + // Dentro del cluster + for (int i = base; i < end; i++) { + for (int e = 0; e < 3; e++) { + int j = base + static_cast(rnd() * size); + add_edge(static_cast(i), + static_cast(j), 1.0f); + } + } + } + // Inter-cluster (5% de los nodos) + int inter = N / 20; + for (int e = 0; e < inter; e++) { + uint32_t a = static_cast(rnd() * N); + uint32_t b = static_cast(rnd() * N); + add_edge(a, b, 0.3f); + } +} + +void demo_graph() { + demo_header("graph_viewport", "v1.0.0", + "Pipeline completo de visualizacion de grafos: graph_renderer (instanced GPU) " + "+ graph_force_layout (Barnes-Hut) + graph_spatial_hash (hit-testing). " + "Render a FBO mostrado via ImGui::Image — escala a decenas de miles de nodos."); + + static int s_n_nodes = 1000; + static int s_n_clusters = 6; + static float s_repulsion = 3500.0f; // fuerza de dispersion entre nodos + static float s_attraction = 0.02f; // muelle entre nodos conectados + static float s_gravity = 0.001f; // tiron hacia el centro + static std::vector s_nodes; + static std::vector s_edges; + static GraphData s_graph{}; + static GraphViewportState s_state; + static bool s_initialized = false; + static bool s_needs_regen = true; + + if (s_needs_regen) { + generate_synthetic_graph(s_n_nodes, s_n_clusters, s_nodes, s_edges); + s_graph.nodes = s_nodes.data(); + s_graph.node_count = static_cast(s_nodes.size()); + s_graph.edges = s_edges.data(); + s_graph.edge_count = static_cast(s_edges.size()); + s_graph.update_bounds(); + s_state.layout_running = true; + s_state.layout_energy = 0.0f; + s_needs_regen = false; + s_initialized = true; + } + + section("Controls"); + { + using namespace fn_ui; + // Sliders en dos filas para que quepan sin scrollbar + ImGui::PushItemWidth(180); + ImGui::SliderInt("Nodes", &s_n_nodes, 100, 20000); + ImGui::SameLine(); + ImGui::SliderInt("Clusters", &s_n_clusters, 2, 16); + ImGui::SliderFloat("Repulsion", &s_repulsion, 100.0f, 20000.0f, "%.0f"); + ImGui::SameLine(); + ImGui::SliderFloat("Attraction", &s_attraction, 0.001f, 0.5f, "%.3f"); + ImGui::SameLine(); + ImGui::SliderFloat("Gravity", &s_gravity, 0.0f, 0.05f, "%.4f"); + ImGui::PopItemWidth(); + + if (button("Regenerate", ButtonVariant::Primary)) s_needs_regen = true; + ImGui::SameLine(); + if (button(s_state.layout_running ? "Pause layout" : "Resume layout", + ButtonVariant::Secondary)) { + s_state.layout_running = !s_state.layout_running; + } + ImGui::SameLine(); + if (button("Fit view", ButtonVariant::Subtle)) { + graph_viewport_fit(s_graph, s_state); + } + } + + section("Stats"); + { + // Una sola linea fija — sin secciones condicionales que cambien la + // altura del panel (eso provocaba que el viewport saltara al hacer + // hover/select). + char hover_buf[32]; + char sel_buf[32]; + if (s_state.hovered_node >= 0) { + std::snprintf(hover_buf, sizeof(hover_buf), "#%d c%u", + s_state.hovered_node, + s_nodes[s_state.hovered_node].community); + } else { + std::snprintf(hover_buf, sizeof(hover_buf), "-"); + } + if (s_state.selected_node >= 0) { + std::snprintf(sel_buf, sizeof(sel_buf), "#%d", s_state.selected_node); + } else { + std::snprintf(sel_buf, sizeof(sel_buf), "-"); + } + ImGui::PushStyleColor(ImGuiCol_Text, fn_tokens::colors::text_muted); + ImGui::Text("nodes=%d edges=%d energy=%.2f fps=%.0f | hover=%s sel=%s", + s_graph.node_count, s_graph.edge_count, + s_state.layout_energy, ImGui::GetIO().Framerate, + hover_buf, sel_buf); + ImGui::PopStyleColor(); + } + + section("Viewport (drag = pan, wheel = zoom, click = select)"); + if (s_initialized) { + // Avanzamos 1 paso de force layout cada frame mientras layout_running + if (s_state.layout_running) { + ForceLayoutConfig cfg; + cfg.repulsion = s_repulsion; + cfg.attraction = s_attraction; + cfg.gravity = s_gravity; + cfg.iterations = 1; + s_state.layout_energy = graph_force_layout_step(s_graph, cfg); + } + graph_viewport("##graph_demo", s_graph, s_state, ImVec2(0, 460)); + } + + code_block( + "static GraphData graph;\n" + "static GraphViewportState state;\n" + "// ... rellenar graph.nodes / graph.edges ...\n" + "graph.update_bounds();\n" + "\n" + "// Por frame:\n" + "if (state.layout_running) {\n" + " ForceLayoutConfig cfg;\n" + " cfg.repulsion = 3500; cfg.gravity = 0.001f;\n" + " graph_force_layout_step(graph, cfg);\n" + "}\n" + "graph_viewport(\"##g\", graph, state, ImVec2(0, 460));" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_text_editor.cpp b/cpp/apps/primitives_gallery/demos_text_editor.cpp new file mode 100644 index 00000000..36c4c895 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_text_editor.cpp @@ -0,0 +1,219 @@ +// Demo combinada: text_editor + file_watcher. +// +// Layout (split horizontal): +// - Izquierda: text_editor con CodeLang::GLSL precargado con un fragment +// shader simple. Boton "Save to /tmp/fn_demo.glsl". +// - Derecha: panel de info — dirty flag, ultimo error, lista scrollable de +// eventos del watcher activo sobre /tmp/fn_demo.glsl. + +#include "demos.h" +#include "demo.h" + +#include "core/text_editor.h" +#include "core/file_watcher.h" +#include "core/button.h" +#include "core/tokens.h" + +#include + +#include +#include +#include +#include +#include +#include + +namespace gallery { + +namespace { + +constexpr const char* kDemoPath = "/tmp/fn_demo.glsl"; + +const char* kInitialGLSL = + "#version 330\n" + "// Demo fragment shader (text_editor + file_watcher).\n" + "out vec4 frag_color;\n" + "uniform vec2 u_resolution;\n" + "uniform float u_time;\n" + "\n" + "void main() {\n" + " vec2 uv = gl_FragCoord.xy / u_resolution;\n" + " vec3 col = 0.5 + 0.5 * cos(u_time + uv.xyx + vec3(0,2,4));\n" + " frag_color = vec4(col, 1.0);\n" + "}\n"; + +struct EventLogEntry { + double t_seconds; // tiempo relativo al primer evento mostrado + std::string label; +}; + +struct DemoState { + fn::TextEditorState* editor = nullptr; + fn::FileWatcher* watcher = nullptr; + std::deque events; + std::string save_status; + std::string watch_error; + bool watcher_active = false; +}; + +DemoState& state() { + static DemoState s; + return s; +} + +void ensure_init() { + auto& s = state(); + if (!s.editor) { + s.editor = fn::text_editor_create(fn::CodeLang::GLSL); + fn::text_editor_set_text(s.editor, kInitialGLSL); + } + if (!s.watcher) { + s.watcher = fn::file_watcher_create(); + // Si /tmp/fn_demo.glsl no existe aun, file_watcher_add fallara — + // se reintenta tras el primer Save. + s.watcher_active = fn::file_watcher_add(s.watcher, kDemoPath); + if (!s.watcher_active) { + s.watch_error = fn::file_watcher_last_error(s.watcher); + } + } +} + +const char* kind_label(fn::FileEvent::Kind k) { + switch (k) { + case fn::FileEvent::Modified: return "MODIFIED"; + case fn::FileEvent::Created: return "CREATED"; + case fn::FileEvent::Deleted: return "DELETED"; + } + return "?"; +} + +void poll_and_log() { + auto& s = state(); + if (!s.watcher) return; + auto evs = fn::file_watcher_poll(s.watcher); + if (evs.empty()) return; + double now = (double)std::time(nullptr); + for (auto& e : evs) { + char buf[512]; + std::snprintf(buf, sizeof(buf), "[%s] %s", kind_label(e.kind), e.path.c_str()); + s.events.push_back({now, buf}); + } + while (s.events.size() > 200) s.events.pop_front(); +} + +bool save_to_disk() { + auto& s = state(); + FILE* f = std::fopen(kDemoPath, "w"); + if (!f) { + s.save_status = std::string("save failed: ") + std::strerror(errno); + return false; + } + const char* txt = fn::text_editor_get_text(s.editor); + std::fputs(txt, f); + std::fclose(f); + fn::text_editor_clear_dirty(s.editor); + s.save_status = std::string("saved -> ") + kDemoPath; + + // Si el watcher no estaba activo (archivo no existia al iniciar), reintentar. + if (!s.watcher_active) { + s.watcher_active = fn::file_watcher_add(s.watcher, kDemoPath); + if (!s.watcher_active) s.watch_error = fn::file_watcher_last_error(s.watcher); + else s.watch_error.clear(); + } + return true; +} + +} // namespace + +void demo_text_editor() { + using namespace fn_tokens; + + demo_header("text_editor + file_watcher", "v1.0.0", + "Editor de codigo GLSL con syntax highlighting (PIMPL sobre ImGuiColorTextEdit) " + "+ watcher de archivos no bloqueante (inotify Linux / ReadDirectoryChangesW Win). " + "Edita, pulsa Save y observa el evento llegar al panel derecho."); + + ensure_init(); + poll_and_log(); + + auto& s = state(); + + // Layout: two-column table. Editor a la izquierda, info a la derecha. + if (ImGui::BeginTable("##te_layout", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp)) { + ImGui::TableSetupColumn("editor", ImGuiTableColumnFlags_WidthStretch, 0.62f); + ImGui::TableSetupColumn("info", ImGuiTableColumnFlags_WidthStretch, 0.38f); + ImGui::TableNextRow(); + + // ---------- Columna izquierda: editor ---------- + ImGui::TableSetColumnIndex(0); + + section("editor (CodeLang::GLSL)"); + + ImVec2 avail = ImGui::GetContentRegionAvail(); + float editor_h = avail.y - 60.0f; + if (editor_h < 200.0f) editor_h = 200.0f; + fn::text_editor_render(s.editor, "##fn_text_editor", ImVec2(-1, editor_h)); + + ImGui::Spacing(); + if (fn_ui::button("Save to /tmp/fn_demo.glsl", fn_ui::ButtonVariant::Primary)) { + save_to_disk(); + } + ImGui::SameLine(); + if (fn::text_editor_is_dirty(s.editor)) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::warning); + ImGui::TextUnformatted("(modified)"); + ImGui::PopStyleColor(); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted("(clean)"); + ImGui::PopStyleColor(); + } + + if (!s.save_status.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted(s.save_status.c_str()); + ImGui::PopStyleColor(); + } + + // ---------- Columna derecha: info + eventos ---------- + ImGui::TableSetColumnIndex(1); + + section("watcher state"); + + ImGui::Text("path: %s", kDemoPath); + ImGui::Text("active: %s", s.watcher_active ? "yes" : "no"); + + if (!s.watch_error.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::error); + ImGui::TextWrapped("err: %s", s.watch_error.c_str()); + ImGui::PopStyleColor(); + } + + ImGui::Spacing(); + section("events"); + + ImGui::Text("captured: %d", (int)s.events.size()); + ImGui::SameLine(); + if (fn_ui::button("clear##evlog", fn_ui::ButtonVariant::Subtle, fn_ui::ButtonSize::Sm)) { + s.events.clear(); + } + + ImGui::BeginChild("##evlog", ImVec2(0, 0), ImGuiChildFlags_Borders); + if (s.events.empty()) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextWrapped("Sin eventos. Modifica el editor + Save, " + "o desde otro terminal: echo hi >> %s", kDemoPath); + ImGui::PopStyleColor(); + } else { + for (auto it = s.events.rbegin(); it != s.events.rend(); ++it) { + ImGui::TextUnformatted(it->label.c_str()); + } + } + ImGui::EndChild(); + + ImGui::EndTable(); + } +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/demos_viz.cpp b/cpp/apps/primitives_gallery/demos_viz.cpp new file mode 100644 index 00000000..a5b81117 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_viz.cpp @@ -0,0 +1,211 @@ +#include "demos.h" +#include "demo.h" + +#include "viz/bar_chart.h" +#include "viz/pie_chart.h" +#include "viz/line_plot.h" +#include "viz/scatter_plot.h" +#include "viz/histogram.h" +#include "viz/sparkline.h" +#include "core/tokens.h" + +#include +#include +#include + +namespace gallery { + +// --------------------------------------------------------------------------- +// bar_chart +// --------------------------------------------------------------------------- + +void demo_bar_chart() { + demo_header("bar_chart", "v1.2.0", + "Barras verticales con ejes pineados, tooltip al hover y auto-rotacion 45 grados " + "de labels cuando no caben horizontalmente."); + + section("Labels que caben horizontalmente"); + { + const char* langs[] = {"go", "py", "ts", "sh", "cpp"}; + float values[] = {412.0f, 187.0f, 94.0f, 63.0f, 36.0f}; + bar_chart("##bar_short", langs, values, 5, 0.67f, 200.0f); + } + + section("Labels largos que obligan a rotar"); + { + const char* domains[] = { + "core", "infrastructure", "finance", "datascience", + "cybersecurity", "notebook", "browser" + }; + float values[] = {412, 187, 94, 63, 42, 38, 22}; + bar_chart("##bar_long", domains, values, 7, 0.67f, 240.0f); + } + + code_block( + "const char* labels[] = {\"go\",\"py\",\"ts\",\"sh\",\"cpp\"};\n" + "float values[] = {412,187,94,63,36};\n" + "bar_chart(\"##lang\", labels, values, 5); // h=200 default\n" + "bar_chart(\"##lang\", labels, values, 5, 0.8f, 300); // bar_w + altura" + ); +} + +// --------------------------------------------------------------------------- +// pie_chart +// --------------------------------------------------------------------------- + +void demo_pie_chart() { + demo_header("pie_chart", "v1.1.0", + "Pie/donut con aspect 1:1, ejes pineados y tooltip por slice con " + "valor absoluto + porcentaje."); + + if (ImGui::BeginTable("##pie_grid", 2, ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + { + const char* labels[] = {"Pure", "Impure"}; + float values[] = {412.0f, 278.0f}; + variant_label("Pie (radius auto)"); + pie_chart("##pie_auto", labels, values, 2, 0.0f, 260.0f); + } + + ImGui::TableSetColumnIndex(1); + { + const char* labels[] = {"function", "pipeline", "component"}; + float values[] = {618.0f, 42.0f, 230.0f}; + variant_label("Donut (radius = -0.45)"); + pie_chart("##pie_donut", labels, values, 3, -0.45f, 260.0f); + } + + ImGui::EndTable(); + } + + code_block( + "const char* labels[] = {\"Pure\",\"Impure\"};\n" + "float values[] = {412, 278};\n" + "pie_chart(\"##p\", labels, values, 2); // pie auto\n" + "pie_chart(\"##p\", labels, values, 2, -0.45f, 260); // donut" + ); +} + +// --------------------------------------------------------------------------- +// line_plot +// --------------------------------------------------------------------------- + +void demo_line_plot() { + demo_header("line_plot", "v1.1.0", + "Line plot 2D con limites de ejes calculados de min/max y pineados. " + "Sin auto-fit animado, sin pan/zoom."); + + constexpr int N = 100; + static float xs[N], ys[N]; + static bool init = false; + if (!init) { + for (int i = 0; i < N; i++) { + xs[i] = static_cast(i) * 0.1f; + ys[i] = std::sin(xs[i]) + 0.3f * std::sin(xs[i] * 3.5f); + } + init = true; + } + line_plot("##line", xs, ys, N, 240.0f); + + code_block( + "line_plot(\"##series\", xs, ys, count); // h=200 default\n" + "line_plot(\"##series\", xs, ys, count, 300.0f); // custom height" + ); +} + +// --------------------------------------------------------------------------- +// scatter_plot +// --------------------------------------------------------------------------- + +void demo_scatter_plot() { + demo_header("scatter_plot", "v1.1.0", + "Puntos dispersos con ejes pineados (5% headroom). Sin interaccion."); + + constexpr int N = 120; + static float xs[N], ys[N]; + static bool init = false; + if (!init) { + unsigned seed = 1234; + auto rnd = [&]() { + seed = seed * 1103515245u + 12345u; + return static_cast((seed >> 16) & 0x7fff) / 32768.0f; + }; + for (int i = 0; i < N; i++) { + xs[i] = rnd() * 10.0f; + ys[i] = 0.5f * xs[i] + rnd() * 3.0f; + } + init = true; + } + scatter_plot("##sc", xs, ys, N, 240.0f); + + code_block( + "scatter_plot(\"##xy\", xs, ys, count, 240.0f);" + ); +} + +// --------------------------------------------------------------------------- +// histogram +// --------------------------------------------------------------------------- + +void demo_histogram() { + demo_header("histogram", "v1.1.0", + "Histograma con bins automaticos (Sturges) o manuales. Usa AutoFit " + "para los bins + Lock para bloquear pan/zoom."); + + constexpr int N = 300; + static float vals[N]; + static bool init = false; + if (!init) { + unsigned seed = 42; + auto rnd = [&]() { + seed = seed * 1103515245u + 12345u; + return static_cast((seed >> 16) & 0x7fff) / 32768.0f; + }; + // Aproximacion de distribucion normal via box-muller simplificado + for (int i = 0; i < N; i++) { + float u1 = rnd() + 1e-6f; + float u2 = rnd(); + vals[i] = std::sqrt(-2.0f * std::log(u1)) + * std::cos(2.0f * 3.14159f * u2); + } + init = true; + } + histogram("##hist", vals, N, -1, 240.0f); + + code_block( + "histogram(\"##h\", values, count); // bins=Sturges\n" + "histogram(\"##h\", values, count, 30, 300.0f); // 30 bins, h=300" + ); +} + +// --------------------------------------------------------------------------- +// sparkline +// --------------------------------------------------------------------------- + +void demo_sparkline() { + demo_header("sparkline", "v1.0.0", + "Mini grafico de lineas inline (rellenado con alpha + linea). " + "Pensado para tablas, KPI cards, headers."); + + float up[] = {10, 12, 11, 15, 18, 17, 20}; + float down[] = {30, 28, 29, 25, 22, 24, 20}; + float flat[] = {10, 10, 10, 10, 10, 10, 10}; + + ImGui::Text("Trending up "); ImGui::SameLine(); + sparkline("##up", up, 7, ImVec4(0.35f, 0.85f, 0.45f, 1.0f), 140.0f, 22.0f); + + ImGui::Text("Trending down"); ImGui::SameLine(); + sparkline("##down", down, 7, ImVec4(0.90f, 0.30f, 0.30f, 1.0f), 140.0f, 22.0f); + + ImGui::Text("Flat "); ImGui::SameLine(); + sparkline("##flat", flat, 7, ImVec4(0.55f, 0.55f, 0.55f, 1.0f), 140.0f, 22.0f); + + code_block( + "float history[] = {10,12,11,15,18,17,20};\n" + "sparkline(\"##rev\", history, 7, /*color=*/{0.35,0.85,0.45,1}, 140, 22);" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/main.cpp b/cpp/apps/primitives_gallery/main.cpp new file mode 100644 index 00000000..cba55d8d --- /dev/null +++ b/cpp/apps/primitives_gallery/main.cpp @@ -0,0 +1,159 @@ +// primitives_gallery — catalogo visual interactivo de los primitivos UI +// del registry (cpp/functions/core y cpp/functions/viz). +// +// Sidebar izquierdo con lista de primitivos agrupados por dominio; panel +// derecho renderiza la demo del item seleccionado (+ snippet de codigo). +// +// Rol: smoke test visual + documentacion viva + build gate en CI. +// NO se conecta a sqlite_api ni a ningun backend. Datos sinteticos. + +#include "app_base.h" +#include "imgui.h" +#include "core/fullscreen_window.h" +#include "core/tokens.h" +#include "core/page_header.h" +#include "core/toast.h" +#include "core/app_menubar.h" +#include "gfx/gl_loader.h" + +#include "demos.h" +#include "demo.h" + +#include +#include +#include +#include + +struct DemoEntry { + const char* id; // id estable, apto para comparar seleccion + const char* label; // texto en sidebar + const char* category; // "Core" o "Viz" + void (*fn)(); // puntero a la demo_xxx +}; + +static const DemoEntry k_demos[] = { + // Core + {"button", "button", "Core", &gallery::demo_button}, + {"icon_button", "icon_button", "Core", &gallery::demo_icon_button}, + {"toolbar", "toolbar", "Core", &gallery::demo_toolbar}, + {"modal_dialog", "modal_dialog", "Core", &gallery::demo_modal}, + {"text_input", "text_input", "Core", &gallery::demo_text_input}, + {"select", "select", "Core", &gallery::demo_select}, + {"toast", "toast + inbox", "Core", &gallery::demo_toast}, + {"tree_view", "tree_view", "Core", &gallery::demo_tree_view}, + {"badge", "badge", "Core", &gallery::demo_badge}, + {"empty_state", "empty_state", "Core", &gallery::demo_empty_state}, + {"page_header", "page_header", "Core", &gallery::demo_page_header}, + {"dashboard_panel", "dashboard_panel", "Core", &gallery::demo_dashboard_panel}, + {"kpi_card", "kpi_card", "Core", &gallery::demo_kpi_card}, + {"text_editor", "text_editor + watcher", "Core", &gallery::demo_text_editor}, + // Viz + {"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart}, + {"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart}, + {"line_plot", "line_plot", "Viz", &gallery::demo_line_plot}, + {"scatter_plot", "scatter_plot", "Viz", &gallery::demo_scatter_plot}, + {"histogram", "histogram", "Viz", &gallery::demo_histogram}, + {"sparkline", "sparkline", "Viz", &gallery::demo_sparkline}, + {"graph_viewport", "graph_viewport", "Viz", &gallery::demo_graph}, + // Gfx (shaders_lab core) + {"shader_canvas", "shader_canvas", "Gfx", &gallery::demo_shader_canvas}, +}; +static constexpr int k_demo_count = sizeof(k_demos) / sizeof(k_demos[0]); + +static std::string g_selected_id = "button"; + +static const DemoEntry* find_demo(const std::string& id) { + for (int i = 0; i < k_demo_count; i++) { + if (id == k_demos[i].id) return &k_demos[i]; + } + return &k_demos[0]; +} + +static void draw_sidebar() { + using namespace fn_tokens; + ImGui::BeginChild("##gallery_sidebar", ImVec2(220, 0), + ImGuiChildFlags_Borders); + + const char* current_category = nullptr; + for (int i = 0; i < k_demo_count; i++) { + const auto& d = k_demos[i]; + if (!current_category || std::strcmp(current_category, d.category) != 0) { + if (current_category) ImGui::Dummy(ImVec2(0, spacing::sm)); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_dim); + ImGui::TextUnformatted(d.category); + ImGui::PopStyleColor(); + ImGui::Separator(); + current_category = d.category; + } + + const bool selected = (g_selected_id == d.id); + ImGui::PushStyleColor(ImGuiCol_Header, selected ? colors::surface_hover : ImVec4(0,0,0,0)); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, colors::surface_hover); + ImGui::PushStyleColor(ImGuiCol_HeaderActive, colors::surface); + ImGui::PushStyleColor(ImGuiCol_Text, selected ? colors::primary : colors::text); + + char label[96]; + std::snprintf(label, sizeof(label), "%s##sel_%s", d.label, d.id); + if (ImGui::Selectable(label, selected)) { + g_selected_id = d.id; + } + + ImGui::PopStyleColor(4); + } + + ImGui::EndChild(); +} + +static void render() { + static bool init_done = false; + if (!init_done) { + fn_tokens::apply_dark_theme(); + // En Linux es no-op; en Windows resuelve los punteros GL via wglGetProcAddress. + // Imprescindible antes de invocar primitivos que usen OpenGL 2.0+ (graph_viewport, + // shader_canvas, etc). + fn::gfx::gl_loader_init(); + init_done = true; + } + + // MainMenuBar (solo Settings — la gallery no tiene paneles toggleables ni layouts) + fn_ui::app_menubar(nullptr, 0, nullptr); + + fullscreen_window_begin("##gallery"); + + page_header_begin("Primitives Gallery", + "Visual catalog of fn_registry C++ UI primitives"); + page_header_end(); + + if (ImGui::BeginTable("##layout", 2, + ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit)) { + ImGui::TableSetupColumn("sidebar", ImGuiTableColumnFlags_WidthFixed, 220.0f); + ImGui::TableSetupColumn("content", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + + ImGui::TableSetColumnIndex(0); + draw_sidebar(); + + ImGui::TableSetColumnIndex(1); + ImGui::BeginChild("##gallery_content", ImVec2(0, 0), + ImGuiChildFlags_Borders, + ImGuiWindowFlags_HorizontalScrollbar); + const DemoEntry* d = find_demo(g_selected_id); + if (d && d->fn) d->fn(); + ImGui::EndChild(); + + ImGui::EndTable(); + } + + fullscreen_window_end(); + + // Toasts se renderizan encima para que el demo de toast funcione aqui tambien. + fn_ui::toast_render(); +} + +int main(int /*argc*/, char** /*argv*/) { + return fn::run_app( + {.title = "fn_registry · Primitives Gallery", + .width = 1400, .height = 900, .viewports = true}, + render + ); +} From a6941b55c43402a8cd02f8593f07ffaf9e6af9f6 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 25 Apr 2026 21:01:32 +0200 Subject: [PATCH 6/6] docs: cerrar issue 0025 Implementado text_editor_cpp_core (PIMPL sobre ImGuiColorTextEdit MIT) y file_watcher_cpp_core (inotify Linux / ReadDirectoryChangesW Win) con demo combinada en primitives_gallery + smoke test. Co-Authored-By: Claude Opus 4.7 (1M context) --- dev/issues/{ => completed}/0025-cpp-text-editor-file-watcher.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dev/issues/{ => completed}/0025-cpp-text-editor-file-watcher.md (100%) diff --git a/dev/issues/0025-cpp-text-editor-file-watcher.md b/dev/issues/completed/0025-cpp-text-editor-file-watcher.md similarity index 100% rename from dev/issues/0025-cpp-text-editor-file-watcher.md rename to dev/issues/completed/0025-cpp-text-editor-file-watcher.md