ImRaid: added TextUnformatted() for string_views; added hovering/clicking and some more minor improvements to data tables; fixed signature of c_MessageBox and added c_MessageBoxText.

This commit is contained in:
Patrick Wuttke
2025-10-28 12:05:17 +01:00
parent e354e20e54
commit 561a92d557

View File

@@ -5,11 +5,14 @@
#define RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED 1
#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <format>
#include <functional>
#include <ranges>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include <imgui.h>
@@ -73,6 +76,11 @@ inline bool BeginPopupButton(const char* label)
return ImGui::BeginPopup(popupId, ImGuiWindowFlags_NoNav);
}
inline void TextUnformatted(std::string_view stringView)
{
ImGui::TextUnformatted(stringView.data(), stringView.data() + stringView.size());
}
struct DataTableState
{
std::vector<std::size_t> sortedIndices;
@@ -99,18 +107,40 @@ struct DataTableColumn
const char* header;
renderer_t renderer;
comparator_t comparator;
ImGuiTableColumnFlags flags = ImGuiTableColumnFlags_None;
float initialWidthOrWeight = 0.f;
};
enum class SelectAction
{
ADD,
REMOVE,
SET
};
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 is_selected_t = std::function<bool(const TObject&)>;
using select_handler_t = std::function<void(const TObject&, SelectAction)>;
std::span<const DataTableColumn<TObject>> columns;
ImGuiTableFlags tableFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_SortMulti;
bool hoverable = false;
filter_t filter = {};
getid_t getId = {};
click_handler_t rightClickHandler = {};
is_selected_t isSelected = {};
select_handler_t selectHandler = {};
};
template<typename TObject,typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, const TData& data, DataTableState& state, ImVec2 outerSize = {})
template<typename TObject, typename TData>
inline void DataTable(const char* strId, const DataTableOptions<TObject>& options, TData&& data, DataTableState& state, ImVec2 outerSize = {})
{
if (outerSize.y <= 0.f) {
outerSize.y = ImGui::GetContentRegionAvail().y;
@@ -122,12 +152,12 @@ inline void DataTable(const char* strId, const DataTableOptions<TObject>& option
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGuiTableColumnFlags flags = 0;
ImGuiTableColumnFlags flags = column.flags;
MIJIN_ASSERT(column.renderer, "Missing column renderer.");
if (!column.comparator) {
flags |= ImGuiTableColumnFlags_NoSort;
}
ImGui::TableSetupColumn(column.header, flags);
ImGui::TableSetupColumn(column.header, flags, column.initialWidthOrWeight);
}
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
@@ -156,49 +186,102 @@ inline void DataTable(const char* strId, const DataTableOptions<TObject>& option
}
}
if (state.sortedIndices.size() != data.size())
{
state.dirty = true;
state.sortedIndices.resize(data.size());
}
static constexpr bool kSortable = std::ranges::random_access_range<TData>;
if (state.dirty)
typename DataTableOptions<TObject>::getid_t getId = options.getId;
static constexpr std::size_t kGetIdBufferSize = 19; // 16 digits + ## and \0
char getIdBuffer[kGetIdBufferSize] = {0};
if (!getId)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
state.sortedIndices[idx] = idx;
}
std::ranges::sort(state.sortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx)
getId = [&](const TObject& object)
{
for (const DataTableState::SortColumn& column : state.sortColumns)
{
const bool less = options.columns[column.index].comparator(data[leftIdx], data[rightIdx]);
if (less)
{ // left < right
return !column.sortDescending;
}
if (options.columns[column.index].comparator(data[rightIdx], data[leftIdx]))
{ // left > right
return column.sortDescending;
}
}
// left == right
return false;
});
state.dirty = false;
std::snprintf(getIdBuffer, kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast<std::uintptr_t>(&object));
return getIdBuffer;
};
}
std::size_t rowIdx = 0;
for (const std::size_t dataIdx : state.sortedIndices)
auto renderRow = [&](const TObject& object)
{
const TObject& object = data[dataIdx];
if (options.filter && !options.filter(object)) {
return;
}
ImGui::TableNextRow();
bool hoverNext = options.hoverable;
const bool selected = options.isSelected ? options.isSelected(object) : false;
for (const DataTableColumn<TObject>& column : options.columns)
{
ImGui::TableNextColumn();
if (hoverNext)
{
hoverNext = false;
if (ImGui::Selectable(getId(object), selected, ImGuiSelectableFlags_SpanAllColumns) && options.selectHandler)
{
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
options.selectHandler(object, selected ? SelectAction::REMOVE : SelectAction::ADD);
}
else if (!selected) {
options.selectHandler(object, SelectAction::SET);
}
}
if (options.rightClickHandler && ImGui::IsMouseReleased(ImGuiMouseButton_Right) && ImGui::IsItemHovered()) {
options.rightClickHandler(object);
}
ImGui::SameLine();
}
column.renderer({
.object = object
});
}
++rowIdx;
};
if constexpr (kSortable)
{
if (state.sortedIndices.size() != data.size())
{
state.dirty = true;
state.sortedIndices.resize(data.size());
}
if (state.dirty)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
state.sortedIndices[idx] = idx;
}
std::ranges::sort(state.sortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx)
{
for (const DataTableState::SortColumn& column : state.sortColumns)
{
const bool less = options.columns[column.index].comparator(data[leftIdx], data[rightIdx]);
if (less)
{ // left < right
return !column.sortDescending;
}
if (options.columns[column.index].comparator(data[rightIdx], data[leftIdx]))
{ // left > right
return column.sortDescending;
}
}
// left == right
return false;
});
state.dirty = false;
}
for (const std::size_t dataIdx : state.sortedIndices) {
renderRow(std::forward<TData>(data)[dataIdx]);
}
}
else // constexpr kSortable
{
for (const TObject& object : std::forward<TData>(data)) {
renderRow(object);
}
}
ImGui::EndTable();
@@ -249,7 +332,7 @@ mijin::Task<> c_MessageBox(const char* titleId, TFunc&& renderFunc)
}
template<typename... TFormatArgs>
mijin::Task<int> c_MessageBox(const char* titleId, std::span<const char*> buttons, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
mijin::Task<int> c_MessageBox(const char* titleId, std::span<const char* const> buttons, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
const std::string message = std::format(format, std::forward<TFormatArgs>(formatArgs)...);
int buttonIdx = -1;
@@ -307,7 +390,7 @@ struct YesNo
template<YesNo::Value DefaultResult = YesNo::NO, typename... TFormatArgs>
mijin::Task<YesNo> c_MessageBoxYesNo(const char* titleId, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
static std::array<const char*, 2> BUTTONS = {"Yes", "No"};
static const std::array<const char*, 2> BUTTONS = {"Yes", "No"};
const int idx = co_await c_MessageBox(titleId, BUTTONS, format, std::forward<TFormatArgs>(formatArgs)...);
switch (idx)
{
@@ -319,6 +402,13 @@ mijin::Task<YesNo> c_MessageBoxYesNo(const char* titleId, std::format_string<TFo
co_return DefaultResult;
}
}
template<typename... TFormatArgs>
mijin::Task<> c_MessageBoxText(const char* titleId, std::format_string<TFormatArgs...> format, TFormatArgs&&... formatArgs)
{
static const char* const BUTTON_TEXT = "OK";
co_await c_MessageBox(titleId, {&BUTTON_TEXT, 1}, format, std::forward<TFormatArgs>(formatArgs)...);
}
} // namespace ImRaid
#endif // !defined(RAID_PUBLIC_RAID_IMRAID_HPP_INCLUDED)