From 8bc95be39d6c9b09b5a19f9d15514a741b000e63 Mon Sep 17 00:00:00 2001 From: scawful Date: Sun, 21 Jul 2024 17:21:34 -0400 Subject: [PATCH] add asset_browser source file, move Draw --- src/CMakeLists.txt | 1 + src/app/gui/asset_browser.cc | 349 ++++++++++++++++++++++++++++++++ src/app/gui/asset_browser.h | 374 +---------------------------------- 3 files changed, 358 insertions(+), 366 deletions(-) create mode 100644 src/app/gui/asset_browser.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 771ffd96..f7893786 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -48,6 +48,7 @@ set( set( YAZE_GUI_SRC + app/gui/asset_browser.cc app/gui/canvas.cc app/gui/input.cc app/gui/style.cc diff --git a/src/app/gui/asset_browser.cc b/src/app/gui/asset_browser.cc new file mode 100644 index 00000000..9c4b2050 --- /dev/null +++ b/src/app/gui/asset_browser.cc @@ -0,0 +1,349 @@ +#include "asset_browser.h" + +namespace yaze { +namespace app { +namespace gui { + +const ImGuiTableSortSpecs* AssetObject::s_current_sort_specs = NULL; + +void GfxSheetAssetBrowser::Draw(gfx::BitmapManager* bmp_manager) { + // Menu bar + 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(); + } + + // 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, absl::StrFormat("%X", item_data->ID).c_str()); + } + } + + 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); +} + +} // namespace gui +} // namespace app +} // namespace yaze \ No newline at end of file diff --git a/src/app/gui/asset_browser.h b/src/app/gui/asset_browser.h index a2038afc..c36c9a34 100644 --- a/src/app/gui/asset_browser.h +++ b/src/app/gui/asset_browser.h @@ -6,18 +6,15 @@ #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)) +namespace yaze { +namespace app { +namespace gui { + // Extra functions to add deletion support to ImGuiSelectionBasicStorage struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage { // Find which item should be Focused after deletion. @@ -115,8 +112,7 @@ struct AssetObject { } // Compare function to be used by qsort() - static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, - const void* rhs) { + static int __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++) { @@ -137,7 +133,6 @@ struct AssetObject { 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) {} @@ -190,12 +185,14 @@ struct GfxSheetAssetBrowser { float LayoutOuterPadding = 0.0f; int LayoutColumnCount = 0; int LayoutLineCount = 0; + bool Initialized = false; void Initialize(gfx::BitmapManager* bmp_manager) { // Load the assets for (int i = 0; i < bmp_manager->size(); i++) { Items.push_back(UnsortedAsset(i)); } + Initialized = true; } void AddItems(int count) { @@ -241,362 +238,7 @@ struct GfxSheetAssetBrowser { 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, absl::StrFormat("%X", item_data->ID).c_str()); - } - } - - 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(); - } + void Draw(gfx::BitmapManager* bmp_manager); }; } // namespace gui