#ifndef YAZE_APP_GUI_ASSET_BROWSER_H #define YAZE_APP_GUI_ASSET_BROWSER_H #include #include #include "app/gfx/bitmap.h" #include "app/rom.h" #include "imgui/imgui.h" #define IM_MIN(A, B) (((A) < (B)) ? (A) : (B)) #define IM_MAX(A, B) (((A) >= (B)) ? (A) : (B)) #define IM_CLAMP(V, MN, MX) ((V) < (MN) ? (MN) : (V) > (MX) ? (MX) : (V)) namespace yaze { namespace gui { // Extra functions to add deletion support to ImGuiSelectionBasicStorage struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage { // Find which item should be Focused after deletion. // Call _before_ item submission. Retunr an index in the before-deletion item // list, your item loop should call SetKeyboardFocusHere() on it. The // subsequent ApplyDeletionPostLoop() code will use it to apply Selection. // - We cannot provide this logic in core Dear ImGui because we don't have // access to selection data. // - We don't actually manipulate the ImVector<> here, only in // ApplyDeletionPostLoop(), but using similar API for consistency and // flexibility. // - Important: Deletion only works if the underlying ImGuiID for your items // are stable: aka not depend on their index, but on e.g. item id/ptr. // FIXME-MULTISELECT: Doesn't take account of the possibility focus target // will be moved during deletion. Need refocus or scroll offset. int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) { if (Size == 0) return -1; // If focused item is not selected... const int focused_idx = (int)ms_io->NavIdItem; // Index of currently focused item if (ms_io->NavIdSelected == false) // This is merely a shortcut, == // Contains(adapter->IndexToStorage(items, focused_idx)) { ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be // ok to reset even when NavIdSelected==true, but it would take // an extra frame to recover RangeSrc when deleting a selected // item. return focused_idx; // Request to focus same item after deletion. } // If focused item is selected: land on first unselected item after focused // item. for (int idx = focused_idx + 1; idx < items_count; idx++) if (!Contains(GetStorageIdFromIndex(idx))) return idx; // If focused item is selected: otherwise return last unselected item before // focused item. for (int idx = IM_MIN(focused_idx, items_count) - 1; idx >= 0; idx--) if (!Contains(GetStorageIdFromIndex(idx))) return idx; return -1; } // Rewrite item list (delete items) + update selection. // - Call after EndMultiSelect() // - We cannot provide this logic in core Dear ImGui because we don't have // access to your items, nor to selection data. template void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items, int item_curr_idx_to_select) { // Rewrite item list (delete items) + convert old selection index (before // deletion) to new selection index (after selection). If NavId was not part // of selection, we will stay on same item. ImVector new_items; new_items.reserve(items.Size - Size); int item_next_idx_to_select = -1; for (int idx = 0; idx < items.Size; idx++) { if (!Contains(GetStorageIdFromIndex(idx))) new_items.push_back(items[idx]); if (item_curr_idx_to_select == idx) item_next_idx_to_select = new_items.Size - 1; } items.swap(new_items); // Update selection Clear(); if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) SetItemSelected(GetStorageIdFromIndex(item_next_idx_to_select), true); } }; struct AssetObject { ImGuiID ID; int Type; AssetObject(ImGuiID id, int type) { ID = id; Type = type; } static const ImGuiTableSortSpecs* s_current_sort_specs; static void SortWithSortSpecs(ImGuiTableSortSpecs* sort_specs, AssetObject* items, int items_count) { // Store in variable accessible by the sort function. s_current_sort_specs = sort_specs; if (items_count > 1) qsort(items, (size_t)items_count, sizeof(items[0]), AssetObject::CompareWithSortSpecs); s_current_sort_specs = NULL; } // Compare function to be used by qsort() static int CompareWithSortSpecs(const void* lhs, const void* rhs) { const AssetObject* a = (const AssetObject*)lhs; const AssetObject* b = (const AssetObject*)rhs; for (int n = 0; n < s_current_sort_specs->SpecsCount; n++) { const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; int delta = 0; if (sort_spec->ColumnIndex == 0) delta = ((int)a->ID - (int)b->ID); else if (sort_spec->ColumnIndex == 1) delta = (a->Type - b->Type); if (delta > 0) return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; if (delta < 0) return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; } return ((int)a->ID - (int)b->ID); } }; struct UnsortedAsset : public AssetObject { UnsortedAsset(ImGuiID id) : AssetObject(id, 0) {} }; struct DungeonAsset : public AssetObject { DungeonAsset(ImGuiID id) : AssetObject(id, 1) {} }; struct OverworldAsset : public AssetObject { OverworldAsset(ImGuiID id) : AssetObject(id, 2) {} }; struct SpriteAsset : public AssetObject { SpriteAsset(ImGuiID id) : AssetObject(id, 3) {} }; struct GfxSheetAssetBrowser { // Options bool ShowTypeOverlay = true; bool AllowSorting = true; bool AllowDragUnselected = false; bool AllowBoxSelect = true; float IconSize = 32.0f; int IconSpacing = 10; // Increase hit-spacing if you want to make it possible to clear or // box-select from gaps. Some spacing is required to able to amend // with Shift+box-select. Value is small in Explorer. int IconHitSpacing = 4; bool StretchSpacing = true; // State ImVector Items; // (ImGuiSelectionBasicStorage + helper funcs to handle deletion) ExampleSelectionWithDeletion Selection; ImGuiID NextItemId = 0; // Unique identifier when creating new items bool RequestDelete = false; // Deferred deletion request bool RequestSort = false; // Deferred sort request // Mouse wheel accumulator to handle smooth wheels better float ZoomWheelAccum = 0.0f; // Calculated sizes for layout, output of UpdateLayoutSizes(). Could be locals // but our code is simpler this way. ImVec2 LayoutItemSize; ImVec2 LayoutItemStep; // == LayoutItemSize + LayoutItemSpacing float LayoutItemSpacing = 0.0f; float LayoutSelectableSpacing = 0.0f; float LayoutOuterPadding = 0.0f; int LayoutColumnCount = 0; int LayoutLineCount = 0; bool Initialized = false; void Initialize(const std::array& bmp_manager) { // Load the assets for (int i = 0; i < kNumGfxSheets; i++) { Items.push_back(UnsortedAsset(i)); } Initialized = true; } void AddItems(int count) { if (Items.Size == 0) NextItemId = 0; Items.reserve(Items.Size + count); for (int n = 0; n < count; n++, NextItemId++) Items.push_back(AssetObject(NextItemId, (NextItemId % 20) < 15 ? 0 : (NextItemId % 20) < 18 ? 1 : 2)); RequestSort = true; } void ClearItems() { Items.clear(); Selection.Clear(); } // Logic would be written in the main code BeginChild() and outputing to local // variables. We extracted it into a function so we can call it easily from // multiple places. void UpdateLayoutSizes(float avail_width) { // Layout: when not stretching: allow extending into right-most spacing. LayoutItemSpacing = (float)IconSpacing; if (StretchSpacing == false) avail_width += floorf(LayoutItemSpacing * 0.5f); // Layout: calculate number of icon per line and number of lines LayoutItemSize = ImVec2(floorf(IconSize * 4), floorf(IconSize)); LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1); LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount; // Layout: when stretching: allocate remaining space to more spacing. Round // before division, so item_spacing may be non-integer. if (StretchSpacing && LayoutColumnCount > 1) LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount; LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing); LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f); LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f); } void Draw(const std::array& bmp_manager); }; } // namespace gui } // namespace yaze #endif // YAZE_APP_GUI_ASSET_BROWSER_H