Implemented class-based data table and search components.
This commit is contained in:
20
public/raid/components/component.hpp
Normal file
20
public/raid/components/component.hpp
Normal 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)
|
||||
809
public/raid/components/data_table.hpp
Normal file
809
public/raid/components/data_table.hpp
Normal 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)
|
||||
75
public/raid/components/search.hpp
Normal file
75
public/raid/components/search.hpp
Normal 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)
|
||||
Reference in New Issue
Block a user