add asset_browser source file, move Draw

This commit is contained in:
scawful
2024-07-21 17:21:34 -04:00
parent ef3a258e25
commit 8bc95be39d
3 changed files with 358 additions and 366 deletions

View File

@@ -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

View File

@@ -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<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, 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

View File

@@ -6,18 +6,15 @@
#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))
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<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, 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