diff --git a/src/app/editor/graphics/graphics_editor.cc b/src/app/editor/graphics/graphics_editor.cc index d0683ff2..f7befba8 100644 --- a/src/app/editor/graphics/graphics_editor.cc +++ b/src/app/editor/graphics/graphics_editor.cc @@ -14,6 +14,7 @@ #include "app/gfx/scad_format.h" #include "app/gfx/snes_palette.h" #include "app/gfx/snes_tile.h" +#include "app/gui/asset_browser.h" #include "app/gui/canvas.h" #include "app/gui/input.h" #include "app/gui/pipeline.h" @@ -53,6 +54,19 @@ absl::Status GraphicsEditor::Update() { absl::Status GraphicsEditor::UpdateGfxEdit() { TAB_ITEM("Sheet Editor") + static bool show_sheet_browser_ = false; + static gui::GfxSheetAssetBrowser asset_browser; + + if (ImGui::Button("Sheet Browser")) { + show_sheet_browser_ = !show_sheet_browser_; + asset_browser.Initialize(rom()->mutable_bitmap_manager()); + } + + if (show_sheet_browser_) { + asset_browser.Draw("##SheetBrowser", &show_sheet_browser_, + rom()->mutable_bitmap_manager()); + } + if (ImGui::BeginTable("##GfxEditTable", 3, kGfxEditTableFlags, ImVec2(0, 0))) { for (const auto& name : diff --git a/src/app/gui/asset_browser.h b/src/app/gui/asset_browser.h new file mode 100644 index 00000000..a4587493 --- /dev/null +++ b/src/app/gui/asset_browser.h @@ -0,0 +1,606 @@ +#ifndef YAZE_APP_GUI_ASSET_BROWSER_H +#define YAZE_APP_GUI_ASSET_BROWSER_H + +#include + +#include + +#include "app/gfx/bitmap.h" +#include "imgui_internal.h" // NavMoveRequestTryWrapping() + +namespace yaze { +namespace app { +namespace gui { + +// ============================================================================ + +#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)) + +// 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 IMGUI_CDECL 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); + } +}; +const ImGuiTableSortSpecs* AssetObject::s_current_sort_specs = NULL; + +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; + + void Initialize(gfx::BitmapManager* bmp_manager) { + // Load the assets + for (int i = 0; i < bmp_manager->size(); i++) { + Items.push_back(UnsortedAsset(i)); + } + } + + 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 char* title, bool* p_open, gfx::BitmapManager* bmp_manager) { + ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), + ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar)) { + ImGui::End(); + return; + } + + // Menu bar + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) + *p_open = false; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) { + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Options")) { + ImGui::PushItemWidth(ImGui::GetFontSize() * 10); + + ImGui::SeparatorText("Contents"); + ImGui::Checkbox("Show Type Overlay", &ShowTypeOverlay); + ImGui::Checkbox("Allow Sorting", &AllowSorting); + + ImGui::SeparatorText("Selection Behavior"); + ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); + ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); + + ImGui::SeparatorText("Layout"); + ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); + ImGui::SameLine(); + ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); + ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); + ImGui::Checkbox("Stretch Spacing", &StretchSpacing); + ImGui::PopItemWidth(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Filter by types + static bool filter_type[4] = {true, true, true, true}; + ImGui::Text("Filter by type:"); + ImGui::SameLine(); + ImGui::Checkbox("Unsorted", &filter_type[0]); + ImGui::SameLine(); + ImGui::Checkbox("Dungeon", &filter_type[1]); + ImGui::SameLine(); + ImGui::Checkbox("Overworld", &filter_type[2]); + ImGui::SameLine(); + ImGui::Checkbox("Sprite", &filter_type[3]); + + // Show a table with ONLY one header row to showcase the idea/possibility of + // using this to provide a sorting UI + if (AllowSorting) { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiTableFlags table_flags_for_sort_specs = + ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | + ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("for_sort_specs_only", 2, + table_flags_for_sort_specs, + ImVec2(0.0f, ImGui::GetFrameHeight()))) { + ImGui::TableSetupColumn("Index"); + ImGui::TableSetupColumn("Type"); + ImGui::TableHeadersRow(); + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + if (sort_specs->SpecsDirty || RequestSort) { + AssetObject::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); + sort_specs->SpecsDirty = RequestSort = false; + } + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowContentSize(ImVec2( + 0.0f, LayoutOuterPadding + + LayoutLineCount * (LayoutItemSize.x + LayoutItemSpacing))); + if (ImGui::BeginChild("Assets", + ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), + ImGuiChildFlags_Border, ImGuiWindowFlags_NoMove)) { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const float avail_width = ImGui::GetContentRegionAvail().x; + UpdateLayoutSizes(avail_width); + + // Calculate and store start position. + ImVec2 start_pos = ImGui::GetCursorScreenPos(); + start_pos = ImVec2(start_pos.x + LayoutOuterPadding, + start_pos.y + LayoutOuterPadding); + ImGui::SetCursorScreenPos(start_pos); + + // Multi-select + ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | + ImGuiMultiSelectFlags_ClearOnClickVoid; + + // - Enable box-select (in 2D mode, so that changing box-select rectangle + // X1/X2 boundaries will affect clipped items) + if (AllowBoxSelect) ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; + + // - This feature allows dragging an unselected item without selecting it + // (rarely used) + if (AllowDragUnselected) + ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; + + // - Enable keyboard wrapping on X axis + // (FIXME-MULTISELECT: We haven't designed/exposed a general nav wrapping + // api yet, so this flag is provided as a courtesy to avoid doing: + // ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), + // ImGuiNavMoveFlags_WrapX); + // When we finish implementing a more general API for this, we will + // obsolete this flag in favor of the new system) + ms_flags |= ImGuiMultiSelectFlags_NavWrapX; + + ImGuiMultiSelectIO* ms_io = + ImGui::BeginMultiSelect(ms_flags, Selection.Size, Items.Size); + + // Use custom selection adapter: store ID in selection (recommended) + Selection.UserData = this; + Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, + int idx) { + GfxSheetAssetBrowser* self = (GfxSheetAssetBrowser*)self_->UserData; + return self->Items[idx].ID; + }; + Selection.ApplyRequests(ms_io); + + const bool want_delete = + (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && + (Selection.Size > 0)) || + RequestDelete; + const int item_curr_idx_to_focus = + want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; + RequestDelete = false; + + // Push LayoutSelectableSpacing (which is LayoutItemSpacing minus + // hit-spacing, if we decide to have hit gaps between items) Altering + // style ItemSpacing may seem unnecessary as we position every items using + // SetCursorScreenPos()... But it is necessary for two reasons: + // - Selectables uses it by default to visually fill the space between two + // items. + // - The vertical spacing would be measured by Clipper to calculate line + // height if we didn't provide it explicitly (here we do). + ImGui::PushStyleVar( + ImGuiStyleVar_ItemSpacing, + ImVec2(LayoutSelectableSpacing, LayoutSelectableSpacing)); + + // Rendering parameters + const ImU32 icon_type_overlay_colors[3] = {0, IM_COL32(200, 70, 70, 255), + IM_COL32(70, 170, 70, 255)}; + const ImU32 icon_bg_color = ImGui::GetColorU32(ImGuiCol_MenuBarBg); + const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); + const bool display_label = + (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); + + const int column_count = LayoutColumnCount; + ImGuiListClipper clipper; + clipper.Begin(LayoutLineCount, LayoutItemStep.y); + if (item_curr_idx_to_focus != -1) + clipper.IncludeItemByIndex( + item_curr_idx_to_focus / + column_count); // Ensure focused item line is not clipped. + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex( + (int)ms_io->RangeSrcItem / + column_count); // Ensure RangeSrc item line is not clipped. + while (clipper.Step()) { + for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; + line_idx++) { + const int item_min_idx_for_current_line = line_idx * column_count; + const int item_max_idx_for_current_line = + IM_MIN((line_idx + 1) * column_count, Items.Size); + for (int item_idx = item_min_idx_for_current_line; + item_idx < item_max_idx_for_current_line; ++item_idx) { + AssetObject* item_data = &Items[item_idx]; + ImGui::PushID((int)item_data->ID); + + // Position item + ImVec2 pos = ImVec2( + start_pos.x + (item_idx % column_count) * LayoutItemStep.x, + start_pos.y + line_idx * LayoutItemStep.y); + ImGui::SetCursorScreenPos(pos); + + ImGui::SetNextItemSelectionUserData(item_idx); + bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); + bool item_is_visible = ImGui::IsRectVisible(LayoutItemSize); + ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, + LayoutItemSize); + + // Update our selection state immediately (without waiting for + // EndMultiSelect() requests) because we use this to alter the color + // of our text/icon. + if (ImGui::IsItemToggledSelection()) + item_is_selected = !item_is_selected; + + // Focus (for after deletion) + if (item_curr_idx_to_focus == item_idx) + ImGui::SetKeyboardFocusHere(-1); + + // Drag and drop + if (ImGui::BeginDragDropSource()) { + // Create payload with full selection OR single unselected item. + // (the later is only possible when using + // ImGuiMultiSelectFlags_SelectOnClickRelease) + if (ImGui::GetDragDropPayload() == NULL) { + ImVector payload_items; + void* it = NULL; + ImGuiID id = 0; + if (!item_is_selected) + payload_items.push_back(item_data->ID); + else + while (Selection.GetNextSelectedItem(&it, &id)) + payload_items.push_back(id); + ImGui::SetDragDropPayload( + "ASSETS_BROWSER_ITEMS", payload_items.Data, + (size_t)payload_items.size_in_bytes()); + } + + // Display payload content in tooltip, by extracting it from the + // payload data (we could read from selection, but it is more + // correct and reusable to read from payload) + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + const int payload_count = + (int)payload->DataSize / (int)sizeof(ImGuiID); + ImGui::Text("%d assets", payload_count); + + ImGui::EndDragDropSource(); + } + + // Render icon (a real app would likely display an image/thumbnail + // here) Because we use ImGuiMultiSelectFlags_BoxSelect2d, clipping + // vertical may occasionally be larger, so we coarse-clip our + // rendering as well. + if (item_is_visible) { + ImVec2 box_min(pos.x - 1, pos.y - 1); + ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, + box_min.y + LayoutItemSize.y + 2); // Dubious + draw_list->AddRectFilled(box_min, box_max, + icon_bg_color); // Background color + if (ShowTypeOverlay && item_data->Type != 0) { + ImU32 type_col = icon_type_overlay_colors + [item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; + draw_list->AddRectFilled( + ImVec2(box_max.x - 2 - icon_type_overlay_size.x, + box_min.y + 2), + ImVec2(box_max.x - 2, + box_min.y + 2 + icon_type_overlay_size.y), + type_col); + } + if (display_label) { + ImU32 label_col = ImGui::GetColorU32( + item_is_selected ? ImGuiCol_Text : ImGuiCol_TextDisabled); + draw_list->AddImage( + (void*)bmp_manager->mutable_bitmap(item_data->ID) + ->texture(), + box_min, box_max, ImVec2(0, 0), ImVec2(1, 1), + ImGui::GetColorU32(ImVec4(1, 1, 1, 1))); + draw_list->AddText( + ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), + label_col, "ID"); + } + } + + ImGui::PopID(); + } + } + } + clipper.End(); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + // Context menu + if (ImGui::BeginPopupContextWindow()) { + ImGui::Text("Selection: %d items", Selection.Size); + ImGui::Separator(); + if (ImGui::MenuItem("Set Type: Unsorted")) { + void* it = NULL; + ImGuiID id = 0; + while (Selection.GetNextSelectedItem(&it, &id)) Items[id].Type = 0; + } + if (ImGui::MenuItem("Set Type: Dungeon")) { + void* it = NULL; + ImGuiID id = 0; + while (Selection.GetNextSelectedItem(&it, &id)) Items[id].Type = 1; + } + if (ImGui::MenuItem("Set Type: Overworld")) { + void* it = NULL; + ImGuiID id = 0; + while (Selection.GetNextSelectedItem(&it, &id)) Items[id].Type = 2; + } + if (ImGui::MenuItem("Set Type: Sprite")) { + void* it = NULL; + ImGuiID id = 0; + while (Selection.GetNextSelectedItem(&it, &id)) Items[id].Type = 3; + } + ImGui::Separator(); + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndPopup(); + } + + ms_io = ImGui::EndMultiSelect(); + Selection.ApplyRequests(ms_io); + if (want_delete) + Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); + + // Zooming with CTRL+Wheel + if (ImGui::IsWindowAppearing()) ZoomWheelAccum = 0.0f; + if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && + ImGui::IsKeyDown(ImGuiMod_Ctrl) && + ImGui::IsAnyItemActive() == false) { + ZoomWheelAccum += io.MouseWheel; + if (fabsf(ZoomWheelAccum) >= 1.0f) { + // Calculate hovered item index from mouse location + // FIXME: Locking aiming on 'hovered_item_idx' (with a cool-down + // timer) would ensure zoom keeps on it. + const float hovered_item_nx = + (io.MousePos.x - start_pos.x + LayoutItemSpacing * 0.5f) / + LayoutItemStep.x; + const float hovered_item_ny = + (io.MousePos.y - start_pos.y + LayoutItemSpacing * 0.5f) / + LayoutItemStep.y; + const int hovered_item_idx = + ((int)hovered_item_ny * LayoutColumnCount) + (int)hovered_item_nx; + // ImGui::SetTooltip("%f,%f -> item %d", hovered_item_nx, + // hovered_item_ny, hovered_item_idx); // Move those 4 lines in block + // above for easy debugging + + // Zoom + IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); + IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); + ZoomWheelAccum -= (int)ZoomWheelAccum; + UpdateLayoutSizes(avail_width); + + // Manipulate scroll to that we will land at the same Y location of + // currently hovered item. + // - Calculate next frame position of item under mouse + // - Set new scroll position to be used in next ImGui::BeginChild() + // call. + float hovered_item_rel_pos_y = + ((float)(hovered_item_idx / LayoutColumnCount) + + fmodf(hovered_item_ny, 1.0f)) * + LayoutItemStep.y; + hovered_item_rel_pos_y += ImGui::GetStyle().WindowPadding.y; + float mouse_local_y = io.MousePos.y - ImGui::GetWindowPos().y; + ImGui::SetScrollY(hovered_item_rel_pos_y - mouse_local_y); + } + } + } + ImGui::EndChild(); + + ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size); + ImGui::End(); + } +}; + +} // namespace gui +} // namespace app +} // namespace yaze + +#endif // YAZE_APP_GUI_ASSET_BROWSER_H \ No newline at end of file