- 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
This commit is contained in:
Patrick Wuttke
2026-02-27 16:11:02 +01:00
parent a3c393b4b1
commit 163c4c06f3
3 changed files with 301 additions and 52 deletions

View File

@@ -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

33
private/raid/imraid.cpp Normal file
View File

@@ -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);
}
}
}

View File

@@ -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<bool small>
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<false>(label, size, flags);
return BeginPopupButtonImpl<false>(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<true>(label, size, flags);
return BeginPopupButtonImpl<true>(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<bool ellipsisLeft = false, typename... TArgs>
void TextEllipsis(float maxWidth, std::format_string<TArgs...> fmt, TArgs&&... args)
{
const raid::TempText formatted = raid::formatTemp(fmt, std::forward<TArgs>(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<typename TObject>
using object_arg_t = std::conditional_t<std::is_class_v<TObject>, const TObject&, TObject>;
template<typename TObject>
struct DataTableColumn
{
struct CellRendererArgs
{
const TObject& object;
};
using renderer_t = std::function<void(const CellRendererArgs&)>;
using comparator_t = std::function<bool(const TObject&, const TObject&)>;
using arg_t = object_arg_t<TObject>;
using renderer_t = std::function<void(arg_t)>;
using comparator_t = std::function<bool(arg_t, arg_t)>;
const char* header;
renderer_t renderer;
@@ -248,16 +292,23 @@ struct DataTableTreeItem
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_SpanAllColumns;
};
struct DataTableRowInfo
{
bool visible = false;
};
template<typename TObject>
struct DataTableOptions
{
using filter_t = std::function<bool(const TObject&)>;
using getid_t = std::function<const char*(const TObject&)>;
using click_handler_t = std::function<void(const TObject&)>;
using getstate_t = std::function<DataTableItemState(const TObject&)>;
using select_handler_t = std::function<void(const TObject&, SelectAction)>;
using gettreeitem_t = std::function<DataTableTreeItem(const TObject&)>;
using arg_t = object_arg_t<TObject>;
using filter_t = std::function<bool(arg_t)>;
using getid_t = std::function<const char*(arg_t)>;
using click_handler_t = std::function<void(arg_t)>;
using hover_handler_t = std::function<void(arg_t)>;
using getstate_t = std::function<DataTableItemState(arg_t)>;
using select_handler_t = std::function<void(arg_t, SelectAction)>;
using gettreeitem_t = std::function<DataTableTreeItem(arg_t)>;
using beginrow_t = std::function<void(arg_t, const DataTableRowInfo&)>;
std::span<const DataTableColumn<TObject>> 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<typename TObject, typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, TData&& data, DataTableState& state, DataTableRenderState<TObject>& renderState, ImVec2 outerSize = {})
{
using arg_t = typename DataTableOptions<TObject>::arg_t;
if (outerSize.y <= 0.f) {
outerSize.y = ImGui::GetContentRegionAvail().y;
}
@@ -333,7 +389,7 @@ inline void DataTable(const char* strId, const DataTableOptions<TObject>& option
char getIdBuffer[kGetIdBufferSize] = {0};
if (!getId)
{
getId = [&](const TObject& object)
getId = [&](arg_t object)
{
std::snprintf(getIdBuffer, kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast<std::uintptr_t>(&object));
return getIdBuffer;
@@ -341,9 +397,9 @@ inline void DataTable(const char* strId, const DataTableOptions<TObject>& option
}
typename DataTableOptions<TObject>::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<TObject>& 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<TObject>& 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<TObject>& 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<unsigned>(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 <skippedItems> regular rows
const float skipHeight = static_cast<float>(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<unsigned>((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<float>(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<TObject>& 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<TObject>& option
});
state.dirty = false;
}
for (const std::size_t dataIdx : state.sortedIndices) {
renderRow(std::forward<TData>(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<TData>(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<TObject>& options, TDat
template<typename TObject>
DataTableColumn<TObject> MakeStringColumn(const char* header, const char* TObject::* member)
{
using arg_t = typename DataTableColumn<TObject>::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<typename TObject>
DataTableColumn<TObject> MakeStringColumn(const char* header, std::string TObject::* member)
{
using arg_t = typename DataTableColumn<TObject>::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<typename TObject, typename TMember>
DataTableColumn<TObject> MakeColumn(const char* header, const char* fmt, TMember TObject::* member)
{
using arg_t = typename DataTableColumn<TObject>::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<T>, value, &step, &stepFast, format, flags);
}
template<std::size_t N>
void InputText(const char* label, std::array<char, N>& 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)