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)