Implemented class-based data table and search components.

This commit is contained in:
Patrick Wuttke
2026-02-27 16:15:54 +01:00
parent 5f30b8c26d
commit d034fb5a6c
3 changed files with 904 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_COMPONENTS_COMPONENT_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_COMPONENTS_COMPONENT_HPP_INCLUDED 1
namespace raid
{
template<typename TImpl>
class Component
{
protected:
Component() noexcept = default;
TImpl& impl() noexcept { return *static_cast<TImpl*>(this); }
const TImpl& impl() const noexcept { return *static_cast<const TImpl*>(this); }
};
} // namespace raid
#endif // !defined(RAID_PUBLIC_RAID_COMPONENTS_COMPONENT_HPP_INCLUDED)

View File

@@ -0,0 +1,809 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_COMPONENTS_DATA_TABLE_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_COMPONENTS_DATA_TABLE_HPP_INCLUDED 1
#include <format>
#include <mijin/util/bitflags.hpp>
#include <mijin/util/concepts.hpp>
#include <mijin/util/traits.hpp>
#include "./component.hpp"
#include "../imraid.hpp"
namespace raid
{
template<typename TRenderer>
class SimpleDataTableColumn
{
public:
using renderer_t = TRenderer;
private:
const char* header;
TRenderer renderer;
ImGuiTableColumnFlags flags = ImGuiTableColumnFlags_None;
float initialWidthOrWeight = 0.f;
public:
};
template<typename T, typename TTable>
concept data_table_simple_object_consumer = std::is_invocable_v<T, const typename TTable::object_t&>;
template<typename T, typename TTable>
concept data_table_self_object_consumer = std::is_invocable_v<T, TTable&, const typename TTable::object_t&>;
template<typename T, typename TTable>
concept data_table_object_consumer = data_table_simple_object_consumer<T, TTable> || data_table_self_object_consumer<T, TTable>;
template<typename TRenderer>
class DataTableColumn
{
public:
using renderer_t = TRenderer;
private:
const char* mHeader;
renderer_t mRenderer;
public:
constexpr DataTableColumn(const char* header, TRenderer renderer) noexcept
: mHeader(header), mRenderer(std::move(renderer)) {}
constexpr DataTableColumn(const DataTableColumn&) = default;
constexpr DataTableColumn(DataTableColumn&&) noexcept = default;
[[nodiscard]]
const char* getHeader() const noexcept { return mHeader; }
template<typename TTable, typename TObject>
void render(TTable& table, const TObject& object) const noexcept
{
if constexpr (std::is_invocable_v<renderer_t, TTable&, const TObject&>) {
std::invoke(mRenderer, table, object);
}
else {
std::invoke(mRenderer, object);
}
}
template<typename TSelf, typename TComparator>
constexpr auto withComparator(this TSelf&& self, TComparator&& comparator)
{
return ComparableDataTableColumn<TSelf, TComparator>(std::forward<TSelf>(self), std::forward<TComparator>(comparator));
}
};
template<typename TBase, typename TComparator>
class ComparableDataTableColumn : public TBase
{
public:
using comparator_t = std::remove_cvref_t<TComparator>;
private:
comparator_t mComparator;
public:
constexpr ComparableDataTableColumn(TBase from, comparator_t comp) noexcept
: TBase(std::move(from)), mComparator(std::move(comp)) {}
constexpr ComparableDataTableColumn(const ComparableDataTableColumn&) = default;
constexpr ComparableDataTableColumn(ComparableDataTableColumn&&) noexcept = default;
template<typename TTable, typename TObject>
[[nodiscard]]
bool compare(TTable& table, const TObject& left, const TObject& right) const noexcept
{
if constexpr (std::is_invocable_v<comparator_t, TTable&, const TObject&, const TObject&>) {
return std::invoke(mComparator, table, left, right);
}
else {
return std::invoke(mComparator, left, right);
}
}
};
template<auto member>
constexpr auto makeMemberRenderer(std::format_string<mijin::member_pointer_type_t<decltype(member)>> format)
{
using base_t = std::remove_cvref_t<mijin::member_pointer_base_type_t<decltype(member)>>;
return [format](const base_t& object) {
ImRaid::Text(format, object.*member);
};
}
template<auto member>
constexpr auto makeMemberRenderer()
{
using member_t = std::remove_cvref_t<mijin::member_pointer_type_t<decltype(member)>>;
using base_t = std::remove_cvref_t<mijin::member_pointer_base_type_t<decltype(member)>>;
if constexpr (std::is_same_v<member_t, std::string>) {
return [](const base_t& object) {
ImGui::TextUnformatted((object.*member).c_str());
};
}
else if constexpr (std::is_same_v<member_t, const char*>) {
return [](const base_t& object) {
ImGui::TextUnformatted(object.*member);
};
}
else if constexpr (std::is_same_v<member_t, std::string_view>) {
return [](const base_t& object) {
ImRaid::TextUnformatted(object.*member);
};
}
else {
return [](const base_t& object) {
ImRaid::Text("{}", object.*member);
};
}
}
template<auto member, typename... TRendererArgs>
constexpr auto dataTableColumnFromMember(const char* header, TRendererArgs&&... rendererArgs)
{
using member_t = std::remove_cvref_t<mijin::member_pointer_type_t<decltype(member)>>;
using base_t = std::remove_cvref_t<mijin::member_pointer_base_type_t<decltype(member)>>;
auto renderer = makeMemberRenderer<member>(std::forward<TRendererArgs>(rendererArgs)...);
if constexpr (requires (const member_t& value) { { value < value } -> mijin::implicitly_convertible<bool>; })
{
auto comparator = [](const base_t& left, const base_t& right) {
return left.*member < right.*member;
};
return DataTableColumn(header, std::move(renderer)).withComparator(comparator);
}
else {
return DataTableColumn(header, std::move(renderer));
}
}
template<typename T, typename TTable>
concept data_table_simple_object_comparator = requires(const T& comparator, const typename TTable::object_t& object)
{
{ std::invoke(comparator, object, object) } -> mijin::implicitly_convertible<bool>;
};
template<typename T, typename TTable>
concept data_table_self_object_comparator = requires(const T& comparator, const TTable& table, const typename TTable::object_t& object)
{
{ std::invoke(comparator, table, object, object) } -> mijin::implicitly_convertible<bool>;
};
template<typename T, typename TTable>
concept data_table_object_comparator = data_table_simple_object_comparator<T, TTable> || data_table_self_object_comparator<T, TTable>;
template<typename T, typename TTable>
concept data_table_column_type = requires(const T& value, const typename TTable::object_t& object, TTable& table)
{
{ value.getHeader() } -> mijin::implicitly_convertible<const char*>;
// { &T::render } -> mijin::any_of_type<
// mijin::bind_some<std::is_invocable, mijin::TypeVar<0>, const TTable::object_t&>::template type, // static function
// mijin::bind_some<std::is_invocable, mijin::TypeVar<0>, const T*, const TTable::object_t&>::template type, // member function
// mijin::bind_some<std::is_invocable, mijin::TypeVar<0>, const TTable::object_t&, const TTable&>::template type, // static function, with table
// mijin::bind_some<std::is_invocable, mijin::TypeVar<0>, const T*, const TTable::object_t&, const TTable&>::template type // member function, with table
// >;
{ 0 } -> mijin::or_type<
requires { value.render(object); },
requires { value.render(table, object); }
>;
};
template<typename T, typename TTable>
concept data_table_comparable_column_type = data_table_column_type<T, TTable>
&& requires(const T& value, const typename TTable::object_t& object, TTable& table)
{
{ 0 } -> mijin::or_type<
requires { { value.compare(object, object) } -> mijin::implicitly_convertible<bool>; },
requires { { value.compare(table, object, object) } -> mijin::implicitly_convertible<bool>; }
>;
};
template<typename TTable, typename T>
using is_data_table_comparable_column = std::bool_constant<data_table_comparable_column_type<T, TTable>>;
struct DataTableFlags : mijin::BitFlags<DataTableFlags>
{
bool hoverable : 1 = false;
bool tree : 1 = false;
bool noHeaders : 1 = false;
bool nonUniformRows : 1 = false;
};
struct DataTableOptions
{
DataTableFlags flags = {};
ImGuiTableFlags imguiFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable
| ImGuiTableFlags_ScrollY | ImGuiTableFlags_SortMulti;
};
template<typename TImpl, typename TTraits>
class DataTableComponent : public Component<TImpl>
{
protected:
struct SortColumn
{
std::size_t index;
bool sortDescending = false;
};
using object_t = typename TTraits::object_t;
std::vector<std::size_t> mSortedIndices;
std::vector<SortColumn> mSortColumns;
bool mDirty = true;
DataTableComponent() noexcept = default; // NOLINT(bugprone-crtp-constructor-accessibility) private constructor doesn't work if TImpl isn't a direct child
using Component<TImpl>::impl;
public:
void render(ImVec2 outerSize = {})
{
if (outerSize.y <= 0.f) {
outerSize.y = ImGui::GetContentRegionAvail().y;
}
auto& self = impl();
using columns_t = std::remove_cvref_t<decltype(self.getColumns())>;
if (!ImGui::BeginTable(raid::formatTemp("{}", static_cast<void*>(this)), static_cast<int>(std::tuple_size_v<columns_t>), self.getOptions().imguiFlags, outerSize)) {
return;
}
mijin::tupleForEach([&](const auto& column) {
self.setupColumn(column);
}, self.getColumns());
self.renderHeadersRow();
self.updateSort();
self.renderContent();
ImGui::EndTable();
}
protected:
static constexpr DataTableOptions getOptions() noexcept {
return {};
}
template<data_table_column_type<TImpl> TColumn>
void setupColumn(const TColumn& column)
{
ImGuiTableColumnFlags flags = ImGuiTableColumnFlags_None;
if constexpr (requires { column.getFlags(); }) {
flags = column.getFlags();
}
if constexpr (!data_table_comparable_column_type<TColumn, TImpl>) {
flags |= ImGuiTableColumnFlags_NoSort;
}
float widthOrWeight = 0.f;
if constexpr (requires { column.getWidthOrWeight(); }) {
widthOrWeight = column.getWidthOrWeight();
}
ImGui::TableSetupColumn(column.getHeader(), flags, widthOrWeight);
}
void updateSort()
{
auto& self = impl();
decltype(auto) data = self.getData();
ImGuiTableSortSpecs* sortSpecs = ImGui::TableGetSortSpecs();
if (sortSpecs != nullptr && sortSpecs->SpecsDirty)
{
sortSpecs->SpecsDirty = false;
if (mSortColumns.size() != static_cast<std::size_t>(sortSpecs->SpecsCount))
{
mDirty = true;
mSortColumns.resize(sortSpecs->SpecsCount);
}
for (int idx = 0; idx < sortSpecs->SpecsCount; ++idx)
{
const ImGuiTableColumnSortSpecs& specs = sortSpecs->Specs[idx];
SortColumn& sortColumn = mSortColumns[idx];
mDirty |= (static_cast<std::size_t>(specs.ColumnIndex) != sortColumn.index);
mDirty |= sortColumn.sortDescending != (specs.SortDirection == ImGuiSortDirection_Descending);
sortColumn = {
.index = static_cast<std::size_t>(specs.ColumnIndex),
.sortDescending = (specs.SortDirection == ImGuiSortDirection_Descending)
};
}
}
if (mSortedIndices.size() != data.size())
{
mDirty = true;
mSortedIndices.resize(data.size());
}
if (mDirty)
{
for (std::size_t idx = 0; idx < data.size(); ++idx) {
mSortedIndices[idx] = idx;
}
std::ranges::sort(mSortedIndices, [&](std::size_t leftIdx, std::size_t rightIdx)
{
enum Result
{
LESS,
GREATER,
EQUAL
};
Result result = EQUAL;
for (const SortColumn& sortColumn : mSortColumns)
{
withColumn(sortColumn.index, [&]<typename TColumn>(const TColumn& column)
{
if constexpr (data_table_comparable_column_type<TColumn, TImpl>)
{
const bool less = column.compare(self, data[leftIdx], data[rightIdx]);
if (less)
{ // left < right
result = sortColumn.sortDescending ? LESS : GREATER;
}
if (column.compare(self, data[rightIdx], data[leftIdx]))
{ // left > right
result = sortColumn.sortDescending ? GREATER : LESS;
}
}
else {
MIJIN_ERROR("Attempting to sort by non-sortable column.");
}
});
switch (result)
{
case LESS:
return true;
case GREATER:
return false;
case EQUAL: break; // next column
}
}
// left == right
return false;
});
mDirty = false;
}
}
void renderHeadersRow()
{
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
}
template<typename TColumn>
using column_is_comparable = is_data_table_comparable_column<TImpl, TColumn>;
static constexpr bool isSortable() noexcept
{
using columns_t = std::remove_cvref_t<decltype(std::declval<TImpl>().getColumns())>; // tuple<column0, column1, ...>
using comparable_t = mijin::map_types_t<column_is_comparable, columns_t>; // tuple<bool_constant0, bool_constant1, ...>
return mijin::copy_args_t<comparable_t, std::disjunction>::value;
}
void renderContent()
{
auto& self = impl();
if (!self.getOptions().flags.nonUniformRows)
{
self.renderContentUniform();
return;
}
self.renderContentNonUniform();
}
static constexpr bool isRowVisible(const object_t&)
{
return true;
};
auto getSortedData()
{
auto& self = impl();
if constexpr (isSortable())
{
return mSortedIndices | std::views::transform([&](std::size_t idx) -> const object_t& {
return self.getData()[idx];
});
}
else
{
return self.getData();
}
}
void renderContentUniform()
{
auto& self = impl();
decltype(auto) data = self.getSortedData();
auto it = data.begin();
auto end = data.end();
// find the first non-hidden row
for (; it != end; ++it)
{
if (self.isRowVisible(*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;
self.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;
self.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 (self.isRowVisible(*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 (!self.isRowVisible(*it)) {
continue;
}
ImGui::TableNextRow(ImGuiTableRowFlags_None, rowHeight);
self.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 (self.isRowVisible(*it)) {
++itemsRemaining;
}
}
// anything remaining? -> do another fake row
if (itemsRemaining > 0)
{
const float skipHeight = static_cast<float>(itemsRemaining) * rowHeight;
ImGui::TableNextRow(ImGuiTableRowFlags_None, skipHeight);
}
};
void renderContentNonUniform()
{
auto& self = impl();
for (const object_t& object : self.getSortedData())
{
ImGui::TableNextRow();
self.renderRow(object);
}
}
void renderRow (const object_t& object)
{
auto& self = impl();
const DataTableOptions options = self.getOptions();
bool hoverNext = options.flags.hoverable || options.flags.tree; // should the next (first) column be "hovered" (depends on the table type)
// renderState.item = options.getState ? options.getState(object) : DataTableItemState{};
mijin::tupleForEach([&](const auto& column) {
self.renderCell(object, column, hoverNext);
}, self.getColumns());
}
[[nodiscard]]
static const char* const getID(const object_t& object)
{
static constexpr std::size_t kGetIdBufferSize = 19; // 16 digits + ## and \0
static std::array<char, kGetIdBufferSize> getIdBuffer = {0};
std::snprintf(getIdBuffer.data(), kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast<std::uintptr_t>(&object));
return getIdBuffer.data();
}
template<data_table_column_type<TImpl> TColumn>
void renderCell(const object_t& object, const TColumn& column, bool& hoverNext)
{
auto& self = impl();
const DataTableOptions options = self.getOptions();
if (!ImGui::TableNextColumn()) {
return; // TODO: does options.nonUniformRows apply here? -> TableNextColumn() appears to return true, even if the column is completely clipped
}
if (hoverNext)
{
hoverNext = false;
bool clicked = false;
#if 0
if (options.tree)
{
const DataTableTreeItem treeItem = getTreeItem(object);
MIJIN_ASSERT(treeItem.depth <= renderState.currentTreeDepth + 1, "Tree depth cannot increase by more than 1 per item.");
// close up to new depth
for (unsigned cnt = treeItem.depth; cnt < renderState.currentTreeDepth; ++cnt) {
ImGui::TreePop();
}
const ImGuiTreeNodeFlags flags = treeItem.flags
| (renderState.item.selected ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None);
const bool open = ImGui::TreeNodeEx(getId(object), flags, "");
renderState.currentTreeDepth = treeItem.depth + (open ? 1 : 0);
clicked = ImGui::IsItemClicked();
}
else
#endif
{
const ImGuiSelectableFlags flags = ImGuiSelectableFlags_SpanAllColumns;
// | (renderState.item.hovered ? ImGuiSelectableFlags_Highlight : ImGuiSelectableFlags_None);
clicked = ImGui::Selectable(getID(object), false /* renderState.item.selected */, flags);
}
// if (clicked && options.selectHandler)
// {
// if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) {
// options.selectHandler(object, renderState.item.selected ? SelectAction::REMOVE : SelectAction::ADD);
// }
// else if (!renderState.item.selected) {
// options.selectHandler(object, SelectAction::SET);
// }
// }
// if (options.rightClickHandler && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
// options.rightClickHandler(object);
// }
// if (options.hoverHandler && ImGui::IsItemHovered()) {
// options.hoverHandler(object);
// }
ImGui::SameLine();
}
if constexpr (requires { column.render(self, object); }) {
column.render(self, object);
}
else {
column.render(object);
}
}
template<typename TFunc>
bool withColumn(std::size_t idx, TFunc&& func)
{
decltype(auto) columns = impl().getColumns();
if (idx >= std::tuple_size_v<std::remove_cvref_t<decltype(columns)>>) {
return false;
}
mijin::tupleForEachWithIndex([&]<std::size_t I>(std::integral_constant<std::size_t, I>, const auto& column) {
if (idx == I) {
std::invoke(std::forward<TFunc>(func), column);
}
}, columns);
return true;
}
};
#if 0
static constexpr bool kSortable = std::ranges::random_access_range<TData>;
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)
{
getId = [&](arg_t object)
{
std::snprintf(getIdBuffer, kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast<std::uintptr_t>(&object));
return getIdBuffer;
};
}
typename DataTableOptions<TObject>::gettreeitem_t getTreeItem = options.getTreeItem;
if (options.tree && !getTreeItem)
{
getTreeItem = [](arg_t) -> DataTableTreeItem {
return {};
};
}
renderState.rowIdx = 0;
renderState.currentTreeDepth = 0;
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)
{
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
{
if (!options.nonUniformRows && !options.tree) { // TODO: make this work with trees
renderRowsUniform(data);
}
else {
renderRowsNonUniform(data);
}
}
for (unsigned cnt = 0; cnt < renderState.currentTreeDepth; ++cnt) {
ImGui::TreePop();
}
#endif
} // namespace raid
#endif // !defined(RAID_PUBLIC_RAID_COMPONENTS_DATA_TABLE_HPP_INCLUDED)

View File

@@ -0,0 +1,75 @@
#pragma once
#if !defined(RAID_PUBLIC_RAID_COMPONENTS_SEARCH_HPP_INCLUDED)
#define RAID_PUBLIC_RAID_COMPONENTS_SEARCH_HPP_INCLUDED 1
#include "./component.hpp"
#include "./data_table.hpp"
namespace raid
{
template<typename T>
concept search_result_component_traits_type = requires()
{
typename T::result_t;
};
template<search_result_component_traits_type TTraits>
struct SearchResultComponentDataTableTraits
{
using object_t = typename TTraits::result_t;
};
template<typename TImpl, search_result_component_traits_type TTraits>
class SearchResultComponent : public Component<TImpl>
{
protected:
using result_t = typename TTraits::result_t;
using datatable_traits_t = SearchResultComponentDataTableTraits<TTraits>;
class DataTableImpl : public DataTableComponent<DataTableImpl, datatable_traits_t>
{
public:
TImpl* mOwner;
explicit DataTableImpl(TImpl& owner) noexcept : mOwner(&owner) {}
constexpr decltype(auto) getColumns() noexcept { return mOwner->getColumns(); }
constexpr decltype(auto) getData() noexcept { return mOwner->getData(); }
};
DataTableImpl mDataTable;
SearchResultComponent() noexcept : mDataTable(impl()) {} // NOLINT(bugprone-crtp-constructor-accessibility) private constructor doesn't work if TImpl isn't a direct child
using Component<TImpl>::impl;
public:
void render(ImVec2 size = {})
{
if constexpr (requires { impl().getProgress(); })
{
static_assert(std::is_convertible_v<decltype(impl().getProgress()), float>, "SearchResultComponent: TImpl has an invalid getProgress() method.");
const float progress = impl().getProgress();
if (progress >= 0.f)
{
ImGui::ProgressBar(progress, {size.x, 0.f});
if (size.y > 0.f)
{
size.y -= ImGui::GetFontSize() + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().ItemSpacing.y;
if (size.y < 0.f) {
return;
}
}
}
}
if (ImGui::BeginChild("##results", size)) {
mDataTable.render(size);
}
ImGui::EndChild();
}
};
} // namespace raid
#endif // !defined(RAID_PUBLIC_RAID_COMPONENTS_SEARCH_HPP_INCLUDED)