add GfxSheetAssetBrowser to GraphicsEditor
This commit is contained in:
@@ -14,6 +14,7 @@
|
|||||||
#include "app/gfx/scad_format.h"
|
#include "app/gfx/scad_format.h"
|
||||||
#include "app/gfx/snes_palette.h"
|
#include "app/gfx/snes_palette.h"
|
||||||
#include "app/gfx/snes_tile.h"
|
#include "app/gfx/snes_tile.h"
|
||||||
|
#include "app/gui/asset_browser.h"
|
||||||
#include "app/gui/canvas.h"
|
#include "app/gui/canvas.h"
|
||||||
#include "app/gui/input.h"
|
#include "app/gui/input.h"
|
||||||
#include "app/gui/pipeline.h"
|
#include "app/gui/pipeline.h"
|
||||||
@@ -53,6 +54,19 @@ absl::Status GraphicsEditor::Update() {
|
|||||||
absl::Status GraphicsEditor::UpdateGfxEdit() {
|
absl::Status GraphicsEditor::UpdateGfxEdit() {
|
||||||
TAB_ITEM("Sheet Editor")
|
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,
|
if (ImGui::BeginTable("##GfxEditTable", 3, kGfxEditTableFlags,
|
||||||
ImVec2(0, 0))) {
|
ImVec2(0, 0))) {
|
||||||
for (const auto& name :
|
for (const auto& name :
|
||||||
|
|||||||
606
src/app/gui/asset_browser.h
Normal file
606
src/app/gui/asset_browser.h
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
#ifndef YAZE_APP_GUI_ASSET_BROWSER_H
|
||||||
|
#define YAZE_APP_GUI_ASSET_BROWSER_H
|
||||||
|
|
||||||
|
#include <imgui/imgui.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#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 <typename ITEM_TYPE>
|
||||||
|
void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io,
|
||||||
|
ImVector<ITEM_TYPE>& 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<ITEM_TYPE> 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<AssetObject> 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<ImGuiID> 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
|
||||||
Reference in New Issue
Block a user