From d034fb5a6cfb9af0b4d2f25e85dc4aed5a0a0f98 Mon Sep 17 00:00:00 2001 From: Patrick Wuttke Date: Fri, 27 Feb 2026 16:15:54 +0100 Subject: [PATCH] Implemented class-based data table and search components. --- public/raid/components/component.hpp | 20 + public/raid/components/data_table.hpp | 809 ++++++++++++++++++++++++++ public/raid/components/search.hpp | 75 +++ 3 files changed, 904 insertions(+) create mode 100644 public/raid/components/component.hpp create mode 100644 public/raid/components/data_table.hpp create mode 100644 public/raid/components/search.hpp diff --git a/public/raid/components/component.hpp b/public/raid/components/component.hpp new file mode 100644 index 0000000..e80bc1a --- /dev/null +++ b/public/raid/components/component.hpp @@ -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 +class Component +{ +protected: + Component() noexcept = default; + + TImpl& impl() noexcept { return *static_cast(this); } + const TImpl& impl() const noexcept { return *static_cast(this); } +}; +} // namespace raid + +#endif // !defined(RAID_PUBLIC_RAID_COMPONENTS_COMPONENT_HPP_INCLUDED) diff --git a/public/raid/components/data_table.hpp b/public/raid/components/data_table.hpp new file mode 100644 index 0000000..055f670 --- /dev/null +++ b/public/raid/components/data_table.hpp @@ -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 +#include +#include +#include +#include "./component.hpp" + +#include "../imraid.hpp" + +namespace raid +{ +template +class SimpleDataTableColumn +{ +public: + using renderer_t = TRenderer; +private: + const char* header; + TRenderer renderer; + ImGuiTableColumnFlags flags = ImGuiTableColumnFlags_None; + float initialWidthOrWeight = 0.f; +public: + +}; + +template +concept data_table_simple_object_consumer = std::is_invocable_v; + +template +concept data_table_self_object_consumer = std::is_invocable_v; + +template +concept data_table_object_consumer = data_table_simple_object_consumer || data_table_self_object_consumer; + +template +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 + void render(TTable& table, const TObject& object) const noexcept + { + if constexpr (std::is_invocable_v) { + std::invoke(mRenderer, table, object); + } + else { + std::invoke(mRenderer, object); + } + } + + template + constexpr auto withComparator(this TSelf&& self, TComparator&& comparator) + { + return ComparableDataTableColumn(std::forward(self), std::forward(comparator)); + } +}; + +template +class ComparableDataTableColumn : public TBase +{ +public: + using comparator_t = std::remove_cvref_t; +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 + [[nodiscard]] + bool compare(TTable& table, const TObject& left, const TObject& right) const noexcept + { + if constexpr (std::is_invocable_v) { + return std::invoke(mComparator, table, left, right); + } + else { + return std::invoke(mComparator, left, right); + } + } +}; + +template +constexpr auto makeMemberRenderer(std::format_string> format) +{ + using base_t = std::remove_cvref_t>; + return [format](const base_t& object) { + ImRaid::Text(format, object.*member); + }; +} + +template +constexpr auto makeMemberRenderer() +{ + using member_t = std::remove_cvref_t>; + using base_t = std::remove_cvref_t>; + + if constexpr (std::is_same_v) { + return [](const base_t& object) { + ImGui::TextUnformatted((object.*member).c_str()); + }; + } + else if constexpr (std::is_same_v) { + return [](const base_t& object) { + ImGui::TextUnformatted(object.*member); + }; + } + else if constexpr (std::is_same_v) { + return [](const base_t& object) { + ImRaid::TextUnformatted(object.*member); + }; + } + else { + return [](const base_t& object) { + ImRaid::Text("{}", object.*member); + }; + } +} + +template +constexpr auto dataTableColumnFromMember(const char* header, TRendererArgs&&... rendererArgs) +{ + using member_t = std::remove_cvref_t>; + using base_t = std::remove_cvref_t>; + auto renderer = makeMemberRenderer(std::forward(rendererArgs)...); + + if constexpr (requires (const member_t& value) { { value < value } -> mijin::implicitly_convertible; }) + { + 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 +concept data_table_simple_object_comparator = requires(const T& comparator, const typename TTable::object_t& object) +{ + { std::invoke(comparator, object, object) } -> mijin::implicitly_convertible; +}; + +template +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; +}; + +template +concept data_table_object_comparator = data_table_simple_object_comparator || data_table_self_object_comparator; + +template +concept data_table_column_type = requires(const T& value, const typename TTable::object_t& object, TTable& table) +{ + { value.getHeader() } -> mijin::implicitly_convertible; + // { &T::render } -> mijin::any_of_type< + // mijin::bind_some, const TTable::object_t&>::template type, // static function + // mijin::bind_some, const T*, const TTable::object_t&>::template type, // member function + // mijin::bind_some, const TTable::object_t&, const TTable&>::template type, // static function, with table + // mijin::bind_some, 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 +concept data_table_comparable_column_type = data_table_column_type + && requires(const T& value, const typename TTable::object_t& object, TTable& table) +{ + { 0 } -> mijin::or_type< + requires { { value.compare(object, object) } -> mijin::implicitly_convertible; }, + requires { { value.compare(table, object, object) } -> mijin::implicitly_convertible; } + >; +}; + +template +using is_data_table_comparable_column = std::bool_constant>; + +struct DataTableFlags : mijin::BitFlags +{ + 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 +class DataTableComponent : public Component +{ +protected: + struct SortColumn + { + std::size_t index; + bool sortDescending = false; + }; + + using object_t = typename TTraits::object_t; + + std::vector mSortedIndices; + std::vector 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::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; + if (!ImGui::BeginTable(raid::formatTemp("{}", static_cast(this)), static_cast(std::tuple_size_v), 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 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) { + 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(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(specs.ColumnIndex) != sortColumn.index); + mDirty |= sortColumn.sortDescending != (specs.SortDirection == ImGuiSortDirection_Descending); + sortColumn = { + .index = static_cast(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, [&](const TColumn& column) + { + if constexpr (data_table_comparable_column_type) + { + 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 + using column_is_comparable = is_data_table_comparable_column; + static constexpr bool isSortable() noexcept + { + using columns_t = std::remove_cvref_t().getColumns())>; // tuple + using comparable_t = mijin::map_types_t; // tuple + return mijin::copy_args_t::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(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 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 (!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(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 getIdBuffer = {0}; + std::snprintf(getIdBuffer.data(), kGetIdBufferSize, "##%016" PRIXPTR, reinterpret_cast(&object)); + return getIdBuffer.data(); + } + + template 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 + bool withColumn(std::size_t idx, TFunc&& func) + { + decltype(auto) columns = impl().getColumns(); + if (idx >= std::tuple_size_v>) { + return false; + } + mijin::tupleForEachWithIndex([&](std::integral_constant, const auto& column) { + if (idx == I) { + std::invoke(std::forward(func), column); + } + }, columns); + return true; + } +}; +#if 0 + + + static constexpr bool kSortable = std::ranges::random_access_range; + + typename DataTableOptions::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(&object)); + return getIdBuffer; + }; + } + + typename DataTableOptions::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(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) + { + + 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) diff --git a/public/raid/components/search.hpp b/public/raid/components/search.hpp new file mode 100644 index 0000000..3c65103 --- /dev/null +++ b/public/raid/components/search.hpp @@ -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 +concept search_result_component_traits_type = requires() +{ + typename T::result_t; +}; + +template +struct SearchResultComponentDataTableTraits +{ + using object_t = typename TTraits::result_t; +}; + +template +class SearchResultComponent : public Component +{ +protected: + using result_t = typename TTraits::result_t; + using datatable_traits_t = SearchResultComponentDataTableTraits; + + class DataTableImpl : public DataTableComponent + { + 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::impl; +public: + void render(ImVec2 size = {}) + { + if constexpr (requires { impl().getProgress(); }) + { + static_assert(std::is_convertible_v, "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)