From 163c4c06f3e1b98b7d5efc2119ba19e7b230196e Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Fri, 27 Feb 2026 16:11:02 +0100 Subject: [PATCH] - popup buttons: moved arguments into a struct and added size parameter for the button in addition to the popup - added functions for rendering clipped text/text with ellipsis - data table: added special rendering for tables with "uniform" rows that skips the drawing of invisible items - added InputText() function that takes an std::array as buffer - added SizedSeparator() functions that work similar to ImGui::Separator() but allow you to set its size --- private/raid/SModule | 1 + private/raid/imraid.cpp | 33 +++++ public/raid/imraid.hpp | 319 +++++++++++++++++++++++++++++++++------- 3 files changed, 301 insertions(+), 52 deletions(-) create mode 100644 private/raid/imraid.cpp diff --git a/private/raid/SModule b/private/raid/SModule index 7f62d86..6f05aa6 100644 --- a/private/raid/SModule +++ b/private/raid/SModule @@ -9,6 +9,7 @@ if not hasattr(env, 'Jinja'): src_files = Split(""" application.cpp config.cpp + imraid.cpp imutil.cpp fonts.gen.cpp stb_image.cpp diff --git a/private/raid/imraid.cpp b/private/raid/imraid.cpp new file mode 100644 index 0000000..39d6ea1 --- /dev/null +++ b/private/raid/imraid.cpp @@ -0,0 +1,33 @@ + +#include "raid/imraid.hpp" + +namespace ImRaid +{ +void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) +{ + // Perform CPU side clipping for single clipped element to avoid using scissor state + ImVec2 pos = pos_min; + const ImVec2 text_size = text_size_if_known ? *text_size_if_known : ImGui::CalcTextSize(text, text_display_end, false, 0.0f); + + const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min; + const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max; + bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y); + if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min + need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y); + + // Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment. + if (align.x > 0.0f) pos.x = pos.x + ((pos_max.x - pos.x - text_size.x) * align.x); + if (align.y > 0.0f) pos.y = pos.y + ((pos_max.y - pos.y - text_size.y) * align.y); + + // Render + if (need_clipping) + { + ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y); + draw_list->AddText(NULL, 0.0f, pos, ImGui::GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect); + } + else + { + draw_list->AddText(NULL, 0.0f, pos, ImGui::GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, NULL); + } +} +} \ No newline at end of file diff --git a/public/raid/imraid.hpp b/public/raid/imraid.hpp index bd4afd4..8e5ae33 100644 --- a/public/raid/imraid.hpp +++ b/public/raid/imraid.hpp @@ -110,32 +110,39 @@ inline bool ToggleImageButton(const char* strId, ImTextureID textureId, const Im return clicked; } +struct PopupButtonArgs +{ + ImVec2 buttonSize = {}; + ImVec2 popupSize = {}; + ImGuiWindowFlags popupFlags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings; +}; + template -inline bool BeginPopupButtonImpl(const char* label, const ImVec2& size = {} , ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings) +inline bool BeginPopupButtonImpl(const char* label, const PopupButtonArgs& args = {}) { char popupId[128] = {"popup##"}; std::strcat(popupId, label); const float popupX = ImGui::GetCursorScreenPos().x; - const bool open = small ? ImGui::SmallButton(label) : ImGui::Button(label); + const bool open = small ? ImGui::SmallButton(label) : ImGui::Button(label, args.buttonSize); if (open) { ImGui::OpenPopup(popupId); } const float popupY = ImGui::GetCursorScreenPos().y; ImGui::SetNextWindowPos({popupX, popupY}); - ImGui::SetNextWindowSize(size); - return ImGui::BeginPopup(popupId, flags); + ImGui::SetNextWindowSize(args.popupSize); + return ImGui::BeginPopup(popupId, args.popupFlags); } -inline bool BeginPopupButton(const char* label, const ImVec2& size = {} , ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings) +inline bool BeginPopupButton(const char* label, const PopupButtonArgs& args = {}) { - return BeginPopupButtonImpl(label, size, flags); + return BeginPopupButtonImpl(label, args); } -inline bool BeginSmallPopupButton(const char* label, const ImVec2& size = {} , ImGuiWindowFlags flags = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings) +inline bool BeginSmallPopupButton(const char* label, const PopupButtonArgs& args = {}) { - return BeginPopupButtonImpl(label, size, flags); + return BeginPopupButtonImpl(label, args); } inline void TextUnformatted(std::string_view stringView) @@ -158,6 +165,43 @@ inline void TextWithBackground(std::string_view stringView, const ImU32 color, f TextUnformatted(stringView); } +void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known = nullptr, const ImVec2& align = ImVec2(0.f, 0.f), const ImRect* clip_rect = nullptr); + +template +void TextEllipsis(float maxWidth, std::format_string fmt, TArgs&&... args) +{ + const raid::TempText formatted = raid::formatTemp(fmt, std::forward(args)...); + const ImVec2 textSize = ImGui::CalcTextSize(formatted, formatted.end()); + + if (textSize.x > maxWidth) + { + static constexpr const char* kEllipsis = "..."; + const float ellipsisWidth = ImGui::CalcTextSize(kEllipsis, nullptr).x; + const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + if constexpr (!ellipsisLeft) + { + const ImVec2 posMin = cursorPos; + const ImVec2 posMax = cursorPos + ImVec2(maxWidth - ellipsisWidth, textSize.y); + RenderTextClippedEx(ImGui::GetWindowDrawList(), posMin, posMax, formatted, formatted.end(), &textSize); + ImGui::RenderText(posMin + ImVec2(maxWidth - ellipsisWidth, 0.f), kEllipsis); + } + else + { + ImGui::RenderText(cursorPos, kEllipsis); + + const ImVec2 posMin = cursorPos + ImVec2(ellipsisWidth, 0.f); + const ImVec2 posMax = posMin + ImVec2(maxWidth - ellipsisWidth, textSize.y); + RenderTextClippedEx(ImGui::GetWindowDrawList(), posMin, posMax, formatted, formatted.end(), &textSize, /* align = */ ImVec2(1.f, 0.f)); + } + + ImGui::SetCursorScreenPos(cursorPos + ImVec2(maxWidth + ImGui::GetStyle().ItemSpacing.x, 0.f)); + ImGui::NewLine(); + } + else { + ImGui::TextUnformatted(formatted, formatted.end()); + } +} + inline float CalcButtonWidth(const char* text) { return ImGui::CalcTextSize(text).x + (2.f * ImGui::GetStyle().FramePadding.x); @@ -203,15 +247,15 @@ struct DataTableState bool dirty = true; }; +template +using object_arg_t = std::conditional_t, const TObject&, TObject>; + template struct DataTableColumn { - struct CellRendererArgs - { - const TObject& object; - }; - using renderer_t = std::function; - using comparator_t = std::function; + using arg_t = object_arg_t; + using renderer_t = std::function; + using comparator_t = std::function; const char* header; renderer_t renderer; @@ -248,16 +292,23 @@ struct DataTableTreeItem ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_SpanAllColumns; }; +struct DataTableRowInfo +{ + bool visible = false; +}; + template struct DataTableOptions { - - using filter_t = std::function; - using getid_t = std::function; - using click_handler_t = std::function; - using getstate_t = std::function; - using select_handler_t = std::function; - using gettreeitem_t = std::function; + using arg_t = object_arg_t; + using filter_t = std::function; + using getid_t = std::function; + using click_handler_t = std::function; + using hover_handler_t = std::function; + using getstate_t = std::function; + using select_handler_t = std::function; + using gettreeitem_t = std::function; + using beginrow_t = std::function; std::span> columns; ImGuiTableFlags tableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable @@ -265,18 +316,23 @@ struct DataTableOptions bool hoverable = false; bool tree = false; bool noHeaders = false; + bool nonUniformRows = false; filter_t filter = {}; getid_t getId = {}; click_handler_t rightClickHandler = {}; + hover_handler_t hoverHandler = {}; getstate_t getState = {}; select_handler_t selectHandler = {}; gettreeitem_t getTreeItem = {}; + beginrow_t beginRow = {}; }; template inline void DataTable(const char* strId, const DataTableOptions& options, TData&& data, DataTableState& state, DataTableRenderState& renderState, ImVec2 outerSize = {}) { + using arg_t = typename DataTableOptions::arg_t; + if (outerSize.y <= 0.f) { outerSize.y = ImGui::GetContentRegionAvail().y; } @@ -333,7 +389,7 @@ inline void DataTable(const char* strId, const DataTableOptions& option char getIdBuffer[kGetIdBufferSize] = {0}; if (!getId) { - getId = [&](const TObject& object) + getId = [&](arg_t object) { std::snprintf(getIdBuffer, kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast(&object)); return getIdBuffer; @@ -341,9 +397,9 @@ inline void DataTable(const char* strId, const DataTableOptions& option } typename DataTableOptions::gettreeitem_t getTreeItem = options.getTreeItem; - if (!getTreeItem) + if (options.tree && !getTreeItem) { - getTreeItem = [](const TObject&) -> DataTableTreeItem { + getTreeItem = [](arg_t) -> DataTableTreeItem { return {}; }; } @@ -351,24 +407,19 @@ inline void DataTable(const char* strId, const DataTableOptions& option renderState.rowIdx = 0; renderState.currentTreeDepth = 0; - auto renderRow = [&](const TObject& object) + auto renderRow = [&](arg_t object) { - if (options.tree && getTreeItem(object).depth > renderState.currentTreeDepth) { - return; - } - - if (options.filter && !options.filter(object)) { - return; - } - - ImGui::TableNextRow(); renderState.currentObject = &object; - bool hoverNext = options.hoverable || options.tree; + bool hoverNext = options.hoverable || options.tree; // should the next (first) column be "hovered" (depends on the table type) renderState.item = options.getState ? options.getState(object) : DataTableItemState{}; + for (const DataTableColumn& column : options.columns) { - ImGui::TableNextColumn(); + if (!ImGui::TableNextColumn()) { + continue; // TODO: does options.nonUniformRows apply here? -> TableNextColumn() appears to return true, even if the column is completely clipped + } + if (hoverNext) { hoverNext = false; @@ -409,15 +460,145 @@ inline void DataTable(const char* strId, const DataTableOptions& option if (options.rightClickHandler && ImGui::IsItemClicked(ImGuiMouseButton_Right)) { options.rightClickHandler(object); } + if (options.hoverHandler && ImGui::IsItemHovered()) { + options.hoverHandler(object); + } ImGui::SameLine(); } - column.renderer({ - .object = object - }); + column.renderer(object); } ++renderState.rowIdx; }; + + auto itemHidden = [&](arg_t object) -> bool + { + // when rendering a tree, skip any non-expanded items + if (options.tree && getTreeItem(object).depth > renderState.currentTreeDepth) { + return true; + } + + // if there is a filter, apply it + if (options.filter && !options.filter(object)) { + return true; + } + + return false; + }; + + auto renderRowsUniform = [&](auto&& range) + { + auto it = range.begin(); + auto end = range.end(); + + // find the first non-hidden row + for (; it != end; ++it) + { + if (!itemHidden(*it)) { + break; + } + } + + // nothing to render? fine + if (it == end) { + return; + } + + // time to render the first row and measure its height + ImGui::TableNextRow(); + const float startY = ImGui::GetCursorScreenPos().y; + renderRow(*it); + ++it; + + // just one row? also fine + if (it == end) { + return; + } + + // jump to the next row and measure the distance + ImGui::TableNextRow(); + const float rowHeight = ImGui::GetCursorScreenPos().y - startY; + renderRow(*it); + ++it; + + // skip over items until we reach the clip rect + const float clipStartY = ImGui::GetWindowDrawList()->GetClipRectMin().y; + const float clippedY = clipStartY - ImGui::GetCursorScreenPos().y; + if (clippedY >= rowHeight) // only if at least one item could be skipped + { + // number of items that can be skipped + const unsigned itemsToSkip = static_cast(clippedY / rowHeight); + + // count the number of items we'll actually skip + unsigned skippedItems = 0; + for (; it != end && skippedItems < itemsToSkip; ++it) + { + if (!itemHidden(*it)) { + ++skippedItems; + } + } + + if (skippedItems == 0) + { + // nothing to skip means everything was hidden -> we can stop here + return; + } + + // create a fake row with the height of regular rows + const float skipHeight = static_cast(skippedItems) * rowHeight; + ImGui::TableNextRow(ImGuiTableRowFlags_None, skipHeight); + } + + // calculate the number of rows within the clip region that can actually be rendered + const float clipEndY = ImGui::GetWindowDrawList()->GetClipRectMax().y; + const unsigned visibleRows = static_cast((clipEndY - clipStartY) / rowHeight) + 2; // always +2 for the (partially occluded) items at the top and bottom + + // finally we can actually render the rows + unsigned rowsRendered = 0; + for (; it != end && rowsRendered < visibleRows; ++it) + { + if (itemHidden(*it)) { + continue; + } + ImGui::TableNextRow(ImGuiTableRowFlags_None, rowHeight); + renderRow(*it); + ++rowsRendered; + } + + // finally, count the number of rows that could have been rendered, but are outside the clip region + unsigned itemsRemaining = 0; + for (; it != end; ++it) + { + if (!itemHidden(*it)) { + ++itemsRemaining; + } + } + + // anything remaining? -> do another fake row + if (itemsRemaining > 0) + { + const float skipHeight = static_cast(itemsRemaining) * rowHeight; + ImGui::TableNextRow(ImGuiTableRowFlags_None, skipHeight); + } + }; + + auto renderRowsNonUniform = [&](auto&& range) + { + auto it = range.begin(); + auto end = range.end(); + + // if rows are non-uniform, all of them need to be properly rendered to determine the table height + // simpler, but a lot less effective + + for (; it != end; ++it) + { + if (!itemHidden(*it)) + { + ImGui::TableNextRow(); + renderRow(*it); + } + } + }; if constexpr (kSortable) { @@ -436,12 +617,12 @@ inline void DataTable(const char* strId, const DataTableOptions& option { for (const DataTableState::SortColumn& column : state.sortColumns) { - const bool less = options.columns[column.index].comparator(data[leftIdx], data[rightIdx]); + const bool less = options.columns[column.index].comparator(data.at(leftIdx), data.at(rightIdx)); if (less) { // left < right return !column.sortDescending; } - if (options.columns[column.index].comparator(data[rightIdx], data[leftIdx])) + if (options.columns[column.index].comparator(data.at(rightIdx), data.at(leftIdx))) { // left > right return column.sortDescending; } @@ -451,15 +632,24 @@ inline void DataTable(const char* strId, const DataTableOptions& option }); state.dirty = false; } - - for (const std::size_t dataIdx : state.sortedIndices) { - renderRow(std::forward(data)[dataIdx]); + + auto range = std::views::transform(state.sortedIndices, [&](std::size_t idx) -> decltype(auto) { + return data.at(idx); + }); + if (!options.nonUniformRows && !options.tree) { // TODO: make this work with trees + renderRowsUniform(range); + } + else { + renderRowsNonUniform(range); } } else // constexpr kSortable { - for (const TObject& object : std::forward(data)) { - renderRow(object); + if (!options.nonUniformRows && !options.tree) { // TODO: make this work with trees + renderRowsUniform(data); + } + else { + renderRowsNonUniform(data); } } @@ -479,30 +669,33 @@ void DataTable(const char* strId, const DataTableOptions& options, TDat template DataTableColumn MakeStringColumn(const char* header, const char* TObject::* member) { + using arg_t = typename DataTableColumn::arg_t; return { .header = header, - .renderer = [member](const auto& args) { ImGui::TextUnformatted(args.object.*member); }, - .comparator = [member](const TObject& left, const TObject& right) { return std::string_view(left.*member) < std::string_view(right.*member); } + .renderer = [member](arg_t object) { ImGui::TextUnformatted(object.*member); }, + .comparator = [member](arg_t left, arg_t right) { return std::string_view(left.*member) < std::string_view(right.*member); } }; } template DataTableColumn MakeStringColumn(const char* header, std::string TObject::* member) { + using arg_t = typename DataTableColumn::arg_t; return { .header = header, - .renderer = [member](const auto& args) { ImGui::TextUnformatted((args.object.*member).c_str()); }, - .comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; } + .renderer = [member](arg_t object) { ImGui::TextUnformatted((object.*member).c_str()); }, + .comparator = [member](arg_t left, arg_t right) { return left.*member < right.*member; } }; } template DataTableColumn MakeColumn(const char* header, const char* fmt, TMember TObject::* member) { + using arg_t = typename DataTableColumn::arg_t; return { .header = header, - .renderer = [fmt, member](const auto& args) { ImGui::Text(fmt, args.object.*member); }, - .comparator = [member](const TObject& left, const TObject& right) { return left.*member < right.*member; } + .renderer = [fmt, member](arg_t object) { ImGui::Text(fmt, object.*member); }, + .comparator = [member](arg_t left, arg_t right) { return left.*member < right.*member; } }; } @@ -907,6 +1100,28 @@ void InputScalar(const char* label, T* value, T step = T(0), T stepFast = T(0), { ImGui::InputScalar(label, raid::kImGuiDataType, value, &step, &stepFast, format, flags); } + +template +void InputText(const char* label, std::array& buffer, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = nullptr, void* userdata = nullptr) +{ + ImGui::InputText(label, buffer.data(), buffer.size(), flags, callback, userdata); +} + +inline void SizedSeparator(ImVec2 size) +{ + const ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + const ImRect rect = {cursorPos, cursorPos + size}; + + ImGui::ItemSize(size); + if (ImGui::ItemAdd(rect, 0)) { + ImGui::GetWindowDrawList()->AddRectFilled(rect.Min, rect.Max, ImGui::GetColorU32(ImGuiCol_Separator)); + } +} + +inline void SizedSeparator(float width) +{ + SizedSeparator({width, 1.f}); +} } // namespace ImRaid #endif // !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)