feat: Introduce card management system for dungeon editor

- Added EditorCardManager to handle registration, visibility, and management of editor cards.
- Integrated card shortcuts and dynamic menu sections for improved user experience in the dungeon editor.
- Registered multiple dungeon-related cards with associated shortcuts for quick access.
- Enhanced DungeonEditorV2 to utilize the new card management system, allowing independent control of editor cards.
- Updated UI components to support card-based editors, improving layout and usability.
This commit is contained in:
scawful
2025-10-09 09:08:17 -04:00
parent 44cabe48c9
commit 9465195956
11 changed files with 1116 additions and 86 deletions

View File

@@ -755,36 +755,6 @@ void DungeonEditor::DrawDungeonCanvas(int room_id) {
canvas_.DrawBitmap(bg2_bitmap, 0, 0, 1.0f, 200);
}
// TEMPORARY: Render all objects as primitives until proper rendering is fixed
if (current_palette_id_ < current_palette_group_.size()) {
auto room_palette = current_palette_group_[current_palette_id_];
// Render regular objects with primitive fallback
for (const auto& object : room.GetTileObjects()) {
renderer_.RenderObjectInCanvas(object, room_palette);
}
// Test manual rendering for debugging
if (room.GetTileObjects().size() > 0) {
const auto& test_object = room.GetTileObjects()[0];
int canvas_x = test_object.x_ * 8;
int canvas_y = test_object.y_ * 8;
printf("[DungeonEditor] Testing manual render for object 0x%04X at (%d,%d)\n",
test_object.id_, canvas_x, canvas_y);
printf("[DungeonEditor] Object tiles count: %zu\n", test_object.tiles().size());
manual_renderer_.RenderSimpleBlock(test_object.id_, canvas_x, canvas_y, room_palette);
// Debug graphics sheets
manual_renderer_.DebugGraphicsSheet(0);
manual_renderer_.DebugGraphicsSheet(1);
}
// Test palette rendering
manual_renderer_.TestPaletteRendering(400, 100);
}
// Render sprites as simple 16x16 squares with labels
// (Sprites are not part of the background buffers)
renderer_.RenderSprites(rooms_[room_id]);

View File

@@ -35,7 +35,6 @@
#include "dungeon_renderer.h"
#include "dungeon_room_loader.h"
#include "dungeon_usage_tracker.h"
#include "manual_object_renderer.h"
namespace yaze {
namespace editor {
@@ -201,7 +200,6 @@ class DungeonEditor : public Editor {
DungeonRenderer renderer_;
DungeonRoomLoader room_loader_;
DungeonUsageTracker usage_tracker_;
ManualObjectRenderer manual_renderer_;
absl::Status status_;

View File

@@ -72,6 +72,81 @@ absl::Status DungeonEditorV2::Load() {
&canvas_viewer_.canvas(), rom_);
printf("[DungeonEditorV2] Manual renderer initialized for debugging\n");
// Register all cards with the card manager for unified control
auto& card_manager = gui::EditorCardManager::Get();
card_manager.RegisterCard({
.card_id = "dungeon.control_panel",
.display_name = "Dungeon Controls",
.icon = ICON_MD_CASTLE,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+D",
.visibility_flag = &show_control_panel_,
.priority = 10
});
card_manager.RegisterCard({
.card_id = "dungeon.room_selector",
.display_name = "Room Selector",
.icon = ICON_MD_LIST,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+R",
.visibility_flag = &show_room_selector_,
.priority = 20
});
card_manager.RegisterCard({
.card_id = "dungeon.room_matrix",
.display_name = "Room Matrix",
.icon = ICON_MD_GRID_VIEW,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+M",
.visibility_flag = &show_room_matrix_,
.priority = 30
});
card_manager.RegisterCard({
.card_id = "dungeon.entrances",
.display_name = "Entrances",
.icon = ICON_MD_DOOR_FRONT,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+E",
.visibility_flag = &show_entrances_list_,
.priority = 40
});
card_manager.RegisterCard({
.card_id = "dungeon.room_graphics",
.display_name = "Room Graphics",
.icon = ICON_MD_IMAGE,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+G",
.visibility_flag = &show_room_graphics_,
.priority = 50
});
card_manager.RegisterCard({
.card_id = "dungeon.object_editor",
.display_name = "Object Editor",
.icon = ICON_MD_CONSTRUCTION,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+O",
.visibility_flag = &show_object_editor_,
.priority = 60
});
card_manager.RegisterCard({
.card_id = "dungeon.palette_editor",
.display_name = "Palette Editor",
.icon = ICON_MD_PALETTE,
.category = "Dungeon",
.shortcut_hint = "Ctrl+Shift+P",
.visibility_flag = &show_palette_editor_,
.priority = 70
});
printf("[DungeonEditorV2] Registered 7 cards with EditorCardManager\n");
// Wire palette changes to trigger room re-renders
palette_editor_.SetOnPaletteChanged([this](int /*palette_id*/) {
// Re-render all active rooms when palette changes
@@ -89,25 +164,46 @@ absl::Status DungeonEditorV2::Load() {
absl::Status DungeonEditorV2::Update() {
if (!is_loaded_) {
// Show minimal loading message in parent window
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Dungeon Editor Loading...");
ImGui::TextWrapped("Independent editor cards will appear once ROM data is loaded.");
// CARD-BASED EDITOR: Create a minimal loading card
gui::EditorCard loading_card("Dungeon Editor Loading", ICON_MD_CASTLE);
loading_card.SetDefaultSize(400, 200);
if (loading_card.Begin()) {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Loading dungeon data...");
ImGui::TextWrapped("Independent editor cards will appear once ROM data is loaded.");
}
loading_card.End();
return absl::OkStatus();
}
// Minimize parent window content - just show a toolbar
DrawToolset();
// CARD-BASED EDITOR: All windows are independent top-level cards
// No parent wrapper - this allows closing control panel without affecting rooms
ImGui::Separator();
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f),
"Editor cards are independent windows - dock them anywhere!");
ImGui::TextWrapped(
"Room Selector, Object Selector, and Room cards can be freely arranged. "
"This parent window can be minimized or closed.");
// Optional control panel (can be hidden/minimized)
if (show_control_panel_) {
DrawControlPanel();
} else if (control_panel_minimized_) {
// Draw floating icon button to reopen
ImGui::SetNextWindowPos(ImVec2(10, 100));
ImGui::SetNextWindowSize(ImVec2(50, 50));
ImGuiWindowFlags icon_flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking;
if (ImGui::Begin("##DungeonControlIcon", nullptr, icon_flags)) {
if (ImGui::Button(ICON_MD_CASTLE, ImVec2(40, 40))) {
show_control_panel_ = true;
control_panel_minimized_ = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Open Dungeon Controls");
}
}
ImGui::End();
}
// Render all independent cards (these create their own top-level windows)
// NOTE: Emulator preview is now integrated into ObjectEditorCard
// object_emulator_preview_.Render(); // Removed - causing performance issues
// Render all independent cards (these are ALL top-level windows now)
DrawLayout();
return absl::OkStatus();
@@ -169,35 +265,94 @@ void DungeonEditorV2::DrawToolset() {
toolbar.End();
}
void DungeonEditorV2::DrawControlPanel() {
// Small, collapsible control panel for dungeon editor
ImGui::SetNextWindowSize(ImVec2(250, 200), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(10, 100), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_None;
if (ImGui::Begin(ICON_MD_CASTLE " Dungeon Controls", &show_control_panel_, flags)) {
DrawToolset();
ImGui::Separator();
ImGui::Text("Quick Toggles:");
// Checkbox grid for quick toggles
if (ImGui::BeginTable("##QuickToggles", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Checkbox("Rooms", &show_room_selector_);
ImGui::TableNextColumn();
ImGui::Checkbox("Matrix", &show_room_matrix_);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Checkbox("Entrances", &show_entrances_list_);
ImGui::TableNextColumn();
ImGui::Checkbox("Graphics", &show_room_graphics_);
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Checkbox("Objects", &show_object_editor_);
ImGui::TableNextColumn();
ImGui::Checkbox("Palette", &show_palette_editor_);
ImGui::EndTable();
}
ImGui::Separator();
// Minimize button
if (ImGui::SmallButton(ICON_MD_MINIMIZE " Minimize to Icon")) {
control_panel_minimized_ = true;
show_control_panel_ = false;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Collapse to floating icon. Rooms stay open.");
}
}
ImGui::End();
}
void DungeonEditorV2::DrawLayout() {
// NO TABLE LAYOUT - All independent dockable EditorCards
// All cards check their visibility flags and can be closed with X button
// 1. Room Selector Card (independent, dockable)
if (show_room_selector_) {
DrawRoomsListCard();
// Card handles its own closing via &show_room_selector_ in constructor
}
// 2. Room Matrix Card (visual navigation)
if (show_room_matrix_) {
DrawRoomMatrixCard();
// Card handles its own closing via &show_room_matrix_ in constructor
}
// 3. Entrances List Card
if (show_entrances_list_) {
DrawEntrancesListCard();
// Card handles its own closing via &show_entrances_list_ in constructor
}
// 3b. Room Graphics Card
// 4. Room Graphics Card
if (show_room_graphics_) {
DrawRoomGraphicsCard();
// Card handles its own closing via &show_room_graphics_ in constructor
}
// 4. Unified Object Editor Card
// 5. Unified Object Editor Card
if (show_object_editor_ && object_editor_card_) {
object_editor_card_->Draw(&show_object_editor_);
// ObjectEditorCard handles closing via p_open parameter
}
// 5. Palette Editor Card (independent, dockable)
// 6. Palette Editor Card (independent, dockable)
if (show_palette_editor_) {
gui::EditorCard palette_card(
MakeCardTitle("Palette Editor").c_str(),
@@ -206,6 +361,7 @@ void DungeonEditorV2::DrawLayout() {
palette_editor_.Draw();
}
palette_card.End();
// Card handles its own closing via &show_palette_editor_ in constructor
}
// 6. Active Room Cards (independent, dockable, tracked for jump-to)
@@ -238,9 +394,11 @@ void DungeonEditorV2::DrawLayout() {
auto& room_card = room_cards_[room_id];
// CRITICAL: Use docking class BEFORE Begin() to make rooms tab together
// CRITICAL: Use docking class BEFORE Begin() to make rooms dock together
// This creates a separate docking space for all room cards
ImGui::SetNextWindowClass(&room_window_class_);
// Make room cards fully dockable and independent
if (room_card->Begin(&open)) {
DrawRoomTab(room_id);
}

View File

@@ -15,6 +15,7 @@
#include "dungeon_room_loader.h"
#include "object_editor_card.h"
#include "manual_object_renderer.h"
#include "app/gui/editor_card_manager.h"
#include "app/zelda3/dungeon/room.h"
#include "app/zelda3/dungeon/room_entrance.h"
#include "app/gui/editor_layout.h"
@@ -97,6 +98,7 @@ class DungeonEditorV2 : public Editor {
void DrawRoomsListCard();
void DrawEntrancesListCard();
void DrawRoomGraphicsCard();
void DrawControlPanel();
// Texture processing (critical for rendering)
void ProcessDeferredTextures();
@@ -125,6 +127,8 @@ class DungeonEditorV2 : public Editor {
bool show_room_graphics_ = false; // Room graphics card
bool show_object_editor_ = true; // Unified object editor card
bool show_palette_editor_ = true;
bool show_control_panel_ = true; // Optional control panel
bool control_panel_minimized_ = false;
// Palette management
gfx::SnesPalette current_palette_;

View File

@@ -610,6 +610,34 @@ void EditorManager::Initialize(gfx::IRenderer* renderer, const std::string& file
context_.shortcut_manager.RegisterShortcut(
"Editor Selection", {ImGuiKey_E, ImGuiMod_Ctrl},
[this]() { show_editor_selection_ = true; });
// Card Browser shortcut
context_.shortcut_manager.RegisterShortcut(
"Card Browser", {ImGuiKey_B, ImGuiMod_Ctrl, ImGuiMod_Shift},
[this]() { show_card_browser_ = true; });
// Dungeon Card shortcuts
context_.shortcut_manager.RegisterShortcut(
"Toggle Dungeon Controls", {ImGuiKey_D, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.control_panel"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Room Selector", {ImGuiKey_R, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.room_selector"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Room Matrix", {ImGuiKey_M, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.room_matrix"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Dungeon Entrances", {ImGuiKey_E, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.entrances"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Room Graphics", {ImGuiKey_G, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.room_graphics"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Object Editor", {ImGuiKey_O, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.object_editor"); });
context_.shortcut_manager.RegisterShortcut(
"Toggle Dungeon Palette", {ImGuiKey_P, ImGuiMod_Ctrl, ImGuiMod_Shift},
[]() { gui::EditorCardManager::Get().ToggleCard("dungeon.palette_editor"); });
#ifdef YAZE_WITH_GRPC
// Agent Editor shortcut
@@ -672,6 +700,11 @@ absl::Status EditorManager::Update() {
if (show_editor_selection_) {
editor_selection_dialog_.Show(&show_editor_selection_);
}
// Draw card browser
if (show_card_browser_) {
gui::EditorCardManager::Get().DrawCardBrowser(&show_card_browser_);
}
#ifdef YAZE_WITH_GRPC
// Update agent editor dashboard
@@ -771,20 +804,13 @@ absl::Status EditorManager::Update() {
}
}
// Generate unique window titles and IDs for multi-session support
std::string window_title =
GenerateUniqueEditorTitle(editor->type(), session_idx);
// CARD-BASED EDITORS: Don't wrap in Begin/End, they manage own windows
bool is_card_based_editor = (editor->type() == EditorType::kDungeon);
// TODO: Add EditorType::kGraphics, EditorType::kPalette when converted
// Note: PushID removed - window title provides sufficient uniqueness
// and PushID was causing ID stack corruption issues
// Set window to maximize on first open (use FirstUseEver instead of IsWindowAppearing check)
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos, ImGuiCond_FirstUseEver);
if (ImGui::Begin(window_title.c_str(), editor->active(),
ImGuiWindowFlags_None)) { // Allow full docking
// Temporarily switch context for this editor's update
if (is_card_based_editor) {
// Card-based editors create their own top-level windows
// No parent wrapper needed - this allows independent docking
Rom* prev_rom = current_rom_;
EditorSet* prev_editor_set = current_editor_set_;
size_t prev_session_id = context_.session_id;
@@ -792,29 +818,13 @@ absl::Status EditorManager::Update() {
current_rom_ = &session.rom;
current_editor_set_ = &session.editors;
current_editor_ = editor;
context_.session_id = session_idx; // Set session ID for child panels
context_.session_id = session_idx;
status_ = editor->Update();
// Route editor errors to toast manager
if (!status_.ok()) {
std::string editor_name =
"Editor"; // Get actual editor name if available
if (editor == &session.editors.overworld_editor_)
editor_name = "Overworld Editor";
else if (editor == &session.editors.dungeon_editor_)
editor_name = "Dungeon Editor";
else if (editor == &session.editors.sprite_editor_)
editor_name = "Sprite Editor";
else if (editor == &session.editors.graphics_editor_)
editor_name = "Graphics Editor";
else if (editor == &session.editors.music_editor_)
editor_name = "Music Editor";
else if (editor == &session.editors.palette_editor_)
editor_name = "Palette Editor";
else if (editor == &session.editors.screen_editor_)
editor_name = "Screen Editor";
std::string editor_name = GetEditorName(editor->type());
toast_manager_.Show(
absl::StrFormat("%s Error: %s", editor_name, status_.message()),
editor::ToastType::kError, 8.0f);
@@ -823,10 +833,46 @@ absl::Status EditorManager::Update() {
// Restore context
current_rom_ = prev_rom;
current_editor_set_ = prev_editor_set;
context_.session_id = prev_session_id; // Restore previous session ID
context_.session_id = prev_session_id;
} else {
// TRADITIONAL EDITORS: Wrap in Begin/End
std::string window_title =
GenerateUniqueEditorTitle(editor->type(), session_idx);
// Set window to maximize on first open
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->WorkPos, ImGuiCond_FirstUseEver);
if (ImGui::Begin(window_title.c_str(), editor->active(),
ImGuiWindowFlags_None)) { // Allow full docking
// Temporarily switch context for this editor's update
Rom* prev_rom = current_rom_;
EditorSet* prev_editor_set = current_editor_set_;
size_t prev_session_id = context_.session_id;
current_rom_ = &session.rom;
current_editor_set_ = &session.editors;
current_editor_ = editor;
context_.session_id = session_idx;
status_ = editor->Update();
// Route editor errors to toast manager
if (!status_.ok()) {
std::string editor_name = GetEditorName(editor->type());
toast_manager_.Show(
absl::StrFormat("%s Error: %s", editor_name, status_.message()),
editor::ToastType::kError, 8.0f);
}
// Restore context
current_rom_ = prev_rom;
current_editor_set_ = prev_editor_set;
context_.session_id = prev_session_id;
}
ImGui::End();
}
ImGui::End();
// PopID removed to match PushID removal above
}
}
}
@@ -962,7 +1008,7 @@ void EditorManager::BuildModernMenu() {
[this]() { show_global_search_ = true; }, "Ctrl+Shift+F")
.EndMenu();
// View Menu - editors only
// View Menu - editors and cards
menu_builder_.BeginMenu("View")
.Item("Editor Selection", ICON_MD_DASHBOARD,
[this]() { show_editor_selection_ = true; }, "Ctrl+E")
@@ -993,6 +1039,16 @@ void EditorManager::BuildModernMenu() {
.Item("Proposal Drawer", ICON_MD_PREVIEW,
[this]() { proposal_drawer_.Toggle(); }, "Ctrl+P")
#endif
.Separator();
// Dynamic card menu sections (from EditorCardManager)
auto& card_manager = gui::EditorCardManager::Get();
card_manager.DrawViewMenuAll();
menu_builder_
.Separator()
.Item("Card Browser", ICON_MD_DASHBOARD,
[this]() { show_card_browser_ = true; }, "Ctrl+Shift+B")
.Separator()
.Item("Welcome Screen", ICON_MD_HOME,
[this]() { show_welcome_screen_ = true; })

View File

@@ -189,6 +189,7 @@ class EditorManager {
bool show_session_rename_dialog_ = false;
bool show_welcome_screen_ = false;
bool welcome_screen_manually_closed_ = false;
bool show_card_browser_ = false;
size_t session_to_rename_ = 0;
char session_rename_buffer_[256] = {};

View File

@@ -0,0 +1,577 @@
#include "editor_card_manager.h"
#include <algorithm>
#include <cstdio>
#include "absl/strings/str_format.h"
#include "app/gui/icons.h"
#include "imgui/imgui.h"
#include "util/file_util.h"
namespace yaze {
namespace gui {
EditorCardManager& EditorCardManager::Get() {
static EditorCardManager instance;
return instance;
}
void EditorCardManager::RegisterCard(const CardInfo& info) {
if (info.card_id.empty()) {
printf("[EditorCardManager] Warning: Attempted to register card with empty ID\n");
return;
}
cards_[info.card_id] = info;
printf("[EditorCardManager] Registered card: %s (%s)\n",
info.card_id.c_str(), info.display_name.c_str());
}
void EditorCardManager::UnregisterCard(const std::string& card_id) {
auto it = cards_.find(card_id);
if (it != cards_.end()) {
printf("[EditorCardManager] Unregistered card: %s\n", card_id.c_str());
cards_.erase(it);
}
}
void EditorCardManager::ClearAllCards() {
printf("[EditorCardManager] Clearing all %zu registered cards\n", cards_.size());
cards_.clear();
}
bool EditorCardManager::ShowCard(const std::string& card_id) {
auto it = cards_.find(card_id);
if (it == cards_.end()) {
return false;
}
if (it->second.visibility_flag) {
*it->second.visibility_flag = true;
if (it->second.on_show) {
it->second.on_show();
}
return true;
}
return false;
}
bool EditorCardManager::HideCard(const std::string& card_id) {
auto it = cards_.find(card_id);
if (it == cards_.end()) {
return false;
}
if (it->second.visibility_flag) {
*it->second.visibility_flag = false;
if (it->second.on_hide) {
it->second.on_hide();
}
return true;
}
return false;
}
bool EditorCardManager::ToggleCard(const std::string& card_id) {
auto it = cards_.find(card_id);
if (it == cards_.end()) {
return false;
}
if (it->second.visibility_flag) {
bool new_state = !(*it->second.visibility_flag);
*it->second.visibility_flag = new_state;
if (new_state && it->second.on_show) {
it->second.on_show();
} else if (!new_state && it->second.on_hide) {
it->second.on_hide();
}
return true;
}
return false;
}
bool EditorCardManager::IsCardVisible(const std::string& card_id) const {
auto it = cards_.find(card_id);
if (it != cards_.end() && it->second.visibility_flag) {
return *it->second.visibility_flag;
}
return false;
}
void EditorCardManager::ShowAllCardsInCategory(const std::string& category) {
for (auto& [id, info] : cards_) {
if (info.category == category && info.visibility_flag) {
*info.visibility_flag = true;
if (info.on_show) info.on_show();
}
}
}
void EditorCardManager::HideAllCardsInCategory(const std::string& category) {
for (auto& [id, info] : cards_) {
if (info.category == category && info.visibility_flag) {
*info.visibility_flag = false;
if (info.on_hide) info.on_hide();
}
}
}
void EditorCardManager::ShowOnlyCard(const std::string& card_id) {
auto target = cards_.find(card_id);
if (target == cards_.end()) {
return;
}
std::string category = target->second.category;
// Hide all cards in the same category
for (auto& [id, info] : cards_) {
if (info.category == category && info.visibility_flag) {
*info.visibility_flag = (id == card_id);
if (id == card_id && info.on_show) {
info.on_show();
} else if (id != card_id && info.on_hide) {
info.on_hide();
}
}
}
}
std::vector<CardInfo> EditorCardManager::GetCardsInCategory(const std::string& category) const {
std::vector<CardInfo> result;
for (const auto& [id, info] : cards_) {
if (info.category == category) {
result.push_back(info);
}
}
// Sort by priority
std::sort(result.begin(), result.end(),
[](const CardInfo& a, const CardInfo& b) {
return a.priority < b.priority;
});
return result;
}
std::vector<std::string> EditorCardManager::GetAllCategories() const {
std::vector<std::string> categories;
for (const auto& [id, info] : cards_) {
if (std::find(categories.begin(), categories.end(), info.category) == categories.end()) {
categories.push_back(info.category);
}
}
std::sort(categories.begin(), categories.end());
return categories;
}
const CardInfo* EditorCardManager::GetCardInfo(const std::string& card_id) const {
auto it = cards_.find(card_id);
return (it != cards_.end()) ? &it->second : nullptr;
}
void EditorCardManager::DrawViewMenuSection(const std::string& category) {
auto cards_in_category = GetCardsInCategory(category);
if (cards_in_category.empty()) {
ImGui::MenuItem("(No cards registered)", nullptr, false, false);
return;
}
for (const auto& info : cards_in_category) {
if (!info.visibility_flag) continue;
std::string label = info.icon.empty()
? info.display_name
: info.icon + " " + info.display_name;
bool visible = *info.visibility_flag;
if (ImGui::MenuItem(label.c_str(),
info.shortcut_hint.empty() ? nullptr : info.shortcut_hint.c_str(),
visible)) {
ToggleCard(info.card_id);
}
}
}
void EditorCardManager::DrawViewMenuAll() {
auto categories = GetAllCategories();
if (categories.empty()) {
ImGui::TextDisabled("No cards registered");
return;
}
for (const auto& category : categories) {
if (ImGui::BeginMenu(category.c_str())) {
DrawViewMenuSection(category);
ImGui::Separator();
// Category-level actions
if (ImGui::MenuItem(absl::StrFormat("%s Show All", ICON_MD_VISIBILITY).c_str())) {
ShowAllCardsInCategory(category);
}
if (ImGui::MenuItem(absl::StrFormat("%s Hide All", ICON_MD_VISIBILITY_OFF).c_str())) {
HideAllCardsInCategory(category);
}
ImGui::EndMenu();
}
}
ImGui::Separator();
// Global actions
if (ImGui::MenuItem(absl::StrFormat("%s Show All Cards", ICON_MD_VISIBILITY).c_str())) {
ShowAll();
}
if (ImGui::MenuItem(absl::StrFormat("%s Hide All Cards", ICON_MD_VISIBILITY_OFF).c_str())) {
HideAll();
}
ImGui::Separator();
if (ImGui::MenuItem(absl::StrFormat("%s Card Browser", ICON_MD_DASHBOARD).c_str(),
"Ctrl+Shift+B")) {
// This will be shown by EditorManager
}
}
void EditorCardManager::DrawCardBrowser(bool* p_open) {
if (!p_open || !*p_open) return;
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (!ImGui::Begin(absl::StrFormat("%s Card Browser", ICON_MD_DASHBOARD).c_str(),
p_open)) {
ImGui::End();
return;
}
// Search filter
static char search_filter[256] = "";
ImGui::SetNextItemWidth(-100);
ImGui::InputTextWithHint("##CardSearch",
absl::StrFormat("%s Search cards...", ICON_MD_SEARCH).c_str(),
search_filter, sizeof(search_filter));
ImGui::SameLine();
if (ImGui::Button(absl::StrFormat("%s Clear", ICON_MD_CLEAR).c_str())) {
search_filter[0] = '\0';
}
ImGui::Separator();
// Statistics
ImGui::Text("%s Total Cards: %zu | Visible: %zu",
ICON_MD_INFO, GetCardCount(), GetVisibleCardCount());
ImGui::Separator();
// Category tabs
if (ImGui::BeginTabBar("CardBrowserTabs")) {
// All Cards tab
if (ImGui::BeginTabItem(absl::StrFormat("%s All", ICON_MD_APPS).c_str())) {
DrawCardBrowserTable(search_filter, "");
ImGui::EndTabItem();
}
// Category tabs
for (const auto& category : GetAllCategories()) {
std::string tab_label = category;
if (ImGui::BeginTabItem(tab_label.c_str())) {
DrawCardBrowserTable(search_filter, category);
ImGui::EndTabItem();
}
}
// Presets tab
if (ImGui::BeginTabItem(absl::StrFormat("%s Presets", ICON_MD_BOOKMARK).c_str())) {
DrawPresetsTab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
void EditorCardManager::DrawCardBrowserTable(const char* search_filter,
const std::string& category_filter) {
if (ImGui::BeginTable("CardBrowserTable", 4,
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg |
ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("Card", ImGuiTableColumnFlags_WidthStretch, 0.4f);
ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthStretch, 0.2f);
ImGui::TableSetupColumn("Shortcut", ImGuiTableColumnFlags_WidthStretch, 0.2f);
ImGui::TableSetupColumn("Visible", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableHeadersRow();
// Collect and sort cards
std::vector<CardInfo> display_cards;
for (const auto& [id, info] : cards_) {
// Apply filters
if (!category_filter.empty() && info.category != category_filter) {
continue;
}
if (search_filter && search_filter[0] != '\0') {
std::string search_lower = search_filter;
std::transform(search_lower.begin(), search_lower.end(),
search_lower.begin(), ::tolower);
std::string name_lower = info.display_name;
std::transform(name_lower.begin(), name_lower.end(),
name_lower.begin(), ::tolower);
if (name_lower.find(search_lower) == std::string::npos) {
continue;
}
}
display_cards.push_back(info);
}
// Sort by category then priority
std::sort(display_cards.begin(), display_cards.end(),
[](const CardInfo& a, const CardInfo& b) {
if (a.category != b.category) return a.category < b.category;
return a.priority < b.priority;
});
// Draw rows
for (const auto& info : display_cards) {
ImGui::PushID(info.card_id.c_str());
ImGui::TableNextRow();
ImGui::TableNextColumn();
// Card name with icon
std::string label = info.icon.empty()
? info.display_name
: info.icon + " " + info.display_name;
ImGui::Text("%s", label.c_str());
ImGui::TableNextColumn();
ImGui::TextDisabled("%s", info.category.c_str());
ImGui::TableNextColumn();
if (!info.shortcut_hint.empty()) {
ImGui::TextDisabled("%s", info.shortcut_hint.c_str());
}
ImGui::TableNextColumn();
if (info.visibility_flag) {
bool visible = *info.visibility_flag;
if (ImGui::Checkbox("##Visible", &visible)) {
*info.visibility_flag = visible;
if (visible && info.on_show) {
info.on_show();
} else if (!visible && info.on_hide) {
info.on_hide();
}
}
}
ImGui::PopID();
}
ImGui::EndTable();
}
}
void EditorCardManager::DrawPresetsTab() {
ImGui::Text("%s Workspace Presets", ICON_MD_BOOKMARK);
ImGui::Separator();
// Save current as preset
static char preset_name[256] = "";
static char preset_desc[512] = "";
ImGui::Text("Save Current Layout:");
ImGui::InputText("Name", preset_name, sizeof(preset_name));
ImGui::InputText("Description", preset_desc, sizeof(preset_desc));
if (ImGui::Button(absl::StrFormat("%s Save Preset", ICON_MD_SAVE).c_str())) {
if (preset_name[0] != '\0') {
SavePreset(preset_name, preset_desc);
preset_name[0] = '\0';
preset_desc[0] = '\0';
}
}
ImGui::Separator();
ImGui::Text("Saved Presets:");
// List presets
auto presets = GetPresets();
if (presets.empty()) {
ImGui::TextDisabled("No presets saved");
} else {
for (const auto& preset : presets) {
ImGui::PushID(preset.name.c_str());
if (ImGui::Button(absl::StrFormat("%s Load", ICON_MD_FOLDER_OPEN).c_str())) {
LoadPreset(preset.name);
}
ImGui::SameLine();
if (ImGui::Button(absl::StrFormat("%s Delete", ICON_MD_DELETE).c_str())) {
DeletePreset(preset.name);
}
ImGui::SameLine();
ImGui::Text("%s %s", ICON_MD_BOOKMARK, preset.name.c_str());
if (!preset.description.empty()) {
ImGui::SameLine();
ImGui::TextDisabled("- %s", preset.description.c_str());
}
ImGui::SameLine();
ImGui::TextDisabled("(%zu cards)", preset.visible_cards.size());
ImGui::PopID();
}
}
}
void EditorCardManager::SavePreset(const std::string& name, const std::string& description) {
WorkspacePreset preset;
preset.name = name;
preset.description = description;
// Save currently visible cards
for (const auto& [id, info] : cards_) {
if (info.visibility_flag && *info.visibility_flag) {
preset.visible_cards.push_back(id);
}
}
presets_[name] = preset;
SavePresetsToFile();
printf("[EditorCardManager] Saved preset '%s' with %zu cards\n",
name.c_str(), preset.visible_cards.size());
}
bool EditorCardManager::LoadPreset(const std::string& name) {
auto it = presets_.find(name);
if (it == presets_.end()) {
return false;
}
// Hide all cards first
HideAll();
// Show cards in preset
for (const auto& card_id : it->second.visible_cards) {
ShowCard(card_id);
}
printf("[EditorCardManager] Loaded preset '%s' with %zu cards\n",
name.c_str(), it->second.visible_cards.size());
return true;
}
void EditorCardManager::DeletePreset(const std::string& name) {
auto it = presets_.find(name);
if (it != presets_.end()) {
presets_.erase(it);
SavePresetsToFile();
printf("[EditorCardManager] Deleted preset '%s'\n", name.c_str());
}
}
std::vector<EditorCardManager::WorkspacePreset> EditorCardManager::GetPresets() const {
std::vector<WorkspacePreset> result;
for (const auto& [name, preset] : presets_) {
result.push_back(preset);
}
return result;
}
void EditorCardManager::ShowAll() {
for (auto& [id, info] : cards_) {
if (info.visibility_flag) {
*info.visibility_flag = true;
if (info.on_show) info.on_show();
}
}
}
void EditorCardManager::HideAll() {
for (auto& [id, info] : cards_) {
if (info.visibility_flag) {
*info.visibility_flag = false;
if (info.on_hide) info.on_hide();
}
}
}
void EditorCardManager::ResetToDefaults() {
// Default visibility based on priority
for (auto& [id, info] : cards_) {
if (info.visibility_flag) {
// Show high-priority cards (priority < 50)
*info.visibility_flag = (info.priority < 50);
if (*info.visibility_flag && info.on_show) {
info.on_show();
} else if (!*info.visibility_flag && info.on_hide) {
info.on_hide();
}
}
}
}
size_t EditorCardManager::GetVisibleCardCount() const {
size_t count = 0;
for (const auto& [id, info] : cards_) {
if (info.visibility_flag && *info.visibility_flag) {
count++;
}
}
return count;
}
void EditorCardManager::SavePresetsToFile() {
// Save presets to JSON or simple format
// TODO: Implement file I/O
printf("[EditorCardManager] Saving %zu presets to file\n", presets_.size());
}
void EditorCardManager::LoadPresetsFromFile() {
// Load presets from file
// TODO: Implement file I/O
printf("[EditorCardManager] Loading presets from file\n");
}
} // namespace gui
} // namespace yaze

View File

@@ -0,0 +1,198 @@
#ifndef YAZE_APP_GUI_EDITOR_CARD_MANAGER_H
#define YAZE_APP_GUI_EDITOR_CARD_MANAGER_H
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "imgui/imgui.h"
namespace yaze {
namespace gui {
class EditorCard; // Forward declaration
/**
* @brief Metadata for an editor card
*/
struct CardInfo {
std::string card_id; // Unique identifier (e.g., "dungeon.room_selector")
std::string display_name; // Human-readable name (e.g., "Room Selector")
std::string icon; // Material icon
std::string category; // Category (e.g., "Dungeon", "Graphics", "Palette")
std::string shortcut_hint; // Display hint (e.g., "Ctrl+Shift+R")
bool* visibility_flag; // Pointer to bool controlling visibility
EditorCard* card_instance; // Pointer to actual card (optional)
std::function<void()> on_show; // Callback when card is shown
std::function<void()> on_hide; // Callback when card is hidden
int priority; // Display priority for menus (lower = higher)
};
/**
* @brief Central registry and manager for all editor cards
*
* This singleton provides:
* - Global card registration across all editors
* - View menu integration
* - Keyboard shortcut management
* - Workspace preset system
* - Quick search/filter
* - Programmatic card control without GUI coupling
*
* Design Philosophy:
* - Cards register themselves on creation
* - Manager provides unified interface for visibility control
* - No direct GUI dependency in card logic
* - Supports dynamic card creation/destruction
*
* Usage:
* ```cpp
* // In editor initialization:
* auto& manager = EditorCardManager::Get();
* manager.RegisterCard({
* .card_id = "dungeon.room_selector",
* .display_name = "Room Selector",
* .icon = ICON_MD_LIST,
* .category = "Dungeon",
* .visibility_flag = &show_room_selector_,
* .on_show = []() { printf("Room selector opened\n"); }
* });
*
* // Programmatic control:
* manager.ShowCard("dungeon.room_selector");
* manager.HideCard("dungeon.room_selector");
* manager.ToggleCard("dungeon.room_selector");
*
* // In View menu:
* manager.DrawViewMenuSection("Dungeon");
* ```
*/
class EditorCardManager {
public:
static EditorCardManager& Get();
// Registration
void RegisterCard(const CardInfo& info);
void UnregisterCard(const std::string& card_id);
void ClearAllCards();
// Card control (programmatic, no GUI)
bool ShowCard(const std::string& card_id);
bool HideCard(const std::string& card_id);
bool ToggleCard(const std::string& card_id);
bool IsCardVisible(const std::string& card_id) const;
// Batch operations
void ShowAllCardsInCategory(const std::string& category);
void HideAllCardsInCategory(const std::string& category);
void ShowOnlyCard(const std::string& card_id); // Hide all others in category
// Query
std::vector<CardInfo> GetCardsInCategory(const std::string& category) const;
std::vector<std::string> GetAllCategories() const;
const CardInfo* GetCardInfo(const std::string& card_id) const;
// View menu integration
void DrawViewMenuSection(const std::string& category);
void DrawViewMenuAll(); // Draw all categories as submenus
// Card browser UI
void DrawCardBrowser(bool* p_open); // Visual card browser/toggler
void DrawCardBrowserTable(const char* search_filter, const std::string& category_filter);
void DrawPresetsTab();
// Workspace presets
struct WorkspacePreset {
std::string name;
std::vector<std::string> visible_cards; // Card IDs
std::string description;
};
void SavePreset(const std::string& name, const std::string& description = "");
bool LoadPreset(const std::string& name);
void DeletePreset(const std::string& name);
std::vector<WorkspacePreset> GetPresets() const;
// Quick actions
void ShowAll(); // Show all registered cards
void HideAll(); // Hide all registered cards
void ResetToDefaults(); // Reset to default visibility state
// Statistics
size_t GetCardCount() const { return cards_.size(); }
size_t GetVisibleCardCount() const;
private:
EditorCardManager() = default;
~EditorCardManager() = default;
EditorCardManager(const EditorCardManager&) = delete;
EditorCardManager& operator=(const EditorCardManager&) = delete;
std::unordered_map<std::string, CardInfo> cards_;
std::unordered_map<std::string, WorkspacePreset> presets_;
// Helper methods
void SavePresetsToFile();
void LoadPresetsFromFile();
};
/**
* @brief RAII helper for auto-registering cards
*
* Usage:
* ```cpp
* class MyEditor {
* CardRegistration room_selector_reg_;
*
* MyEditor() {
* room_selector_reg_ = RegisterCard({
* .card_id = "myeditor.room_selector",
* .display_name = "Room Selector",
* .visibility_flag = &show_room_selector_
* });
* }
* };
* ```
*/
class CardRegistration {
public:
CardRegistration() = default;
explicit CardRegistration(const std::string& card_id) : card_id_(card_id) {}
~CardRegistration() {
if (!card_id_.empty()) {
EditorCardManager::Get().UnregisterCard(card_id_);
}
}
// No copy, allow move
CardRegistration(const CardRegistration&) = delete;
CardRegistration& operator=(const CardRegistration&) = delete;
CardRegistration(CardRegistration&& other) noexcept : card_id_(std::move(other.card_id_)) {
other.card_id_.clear();
}
CardRegistration& operator=(CardRegistration&& other) noexcept {
if (this != &other) {
card_id_ = std::move(other.card_id_);
other.card_id_.clear();
}
return *this;
}
private:
std::string card_id_;
};
// Convenience function for registration
inline CardRegistration RegisterCard(const CardInfo& info) {
EditorCardManager::Get().RegisterCard(info);
return CardRegistration(info.card_id);
}
} // namespace gui
} // namespace yaze
#endif // YAZE_APP_GUI_EDITOR_CARD_MANAGER_H

View File

@@ -248,8 +248,25 @@ void EditorCard::SetPosition(Position pos) {
}
bool EditorCard::Begin(bool* p_open) {
// Handle icon-collapsed state
if (icon_collapsible_ && collapsed_to_icon_) {
DrawFloatingIconButton();
return false;
}
ImGuiWindowFlags flags = ImGuiWindowFlags_None;
// Apply headless mode
if (headless_) {
flags |= ImGuiWindowFlags_NoTitleBar;
flags |= ImGuiWindowFlags_NoCollapse;
}
// Control docking
if (!docking_allowed_) {
flags |= ImGuiWindowFlags_NoDocking;
}
// Set initial position based on position enum
if (first_draw_) {
float display_width = ImGui::GetIO().DisplaySize.x;
@@ -290,7 +307,13 @@ bool EditorCard::Begin(bool* p_open) {
ImGui::PushStyleColor(ImGuiCol_TitleBg, GetThemeColor(ImGuiCol_TitleBg));
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, GetAccentColor());
bool visible = ImGui::Begin(window_title.c_str(), p_open, flags);
// Use p_open parameter if provided, otherwise use stored p_open_
bool* actual_p_open = p_open ? p_open : p_open_;
// If closable is false, don't pass p_open (removes X button)
bool visible = ImGui::Begin(window_title.c_str(),
closable_ ? actual_p_open : nullptr,
flags);
// Register card window for test automation
if (ImGui::GetCurrentWindow() && ImGui::GetCurrentWindow()->ID != 0) {
@@ -318,6 +341,38 @@ void EditorCard::Focus() {
focused_ = true;
}
void EditorCard::DrawFloatingIconButton() {
// Draw a small floating button with the icon
ImGui::SetNextWindowPos(saved_icon_pos_, ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(50, 50));
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoCollapse;
std::string icon_window_name = window_name_ + "##IconCollapsed";
if (ImGui::Begin(icon_window_name.c_str(), nullptr, flags)) {
// Draw icon button
if (ImGui::Button(icon_.c_str(), ImVec2(40, 40))) {
collapsed_to_icon_ = false; // Expand back to full window
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Expand %s", title_.c_str());
}
// Allow dragging the icon
if (ImGui::IsWindowHovered() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
ImVec2 mouse_delta = ImGui::GetIO().MouseDelta;
saved_icon_pos_.x += mouse_delta.x;
saved_icon_pos_.y += mouse_delta.y;
}
}
ImGui::End();
}
// ============================================================================
// EditorLayout Implementation
// ============================================================================

View File

@@ -121,6 +121,9 @@ class EditorCard {
void SetPosition(Position pos);
void SetMinimizable(bool minimizable) { minimizable_ = minimizable; }
void SetClosable(bool closable) { closable_ = closable; }
void SetHeadless(bool headless) { headless_ = headless; }
void SetDockingAllowed(bool allowed) { docking_allowed_ = allowed; }
void SetIconCollapsible(bool collapsible) { icon_collapsible_ = collapsible; }
// Begin drawing the card
bool Begin(bool* p_open = nullptr);
@@ -151,6 +154,15 @@ class EditorCard {
bool first_draw_ = true;
bool focused_ = false;
bool* p_open_ = nullptr;
// UX enhancements
bool headless_ = false; // Minimal chrome, no title bar
bool docking_allowed_ = true; // Allow docking
bool icon_collapsible_ = false; // Can collapse to floating icon
bool collapsed_to_icon_ = false; // Currently collapsed
ImVec2 saved_icon_pos_ = ImVec2(10, 100); // Position when collapsed to icon
void DrawFloatingIconButton();
};
/**

View File

@@ -19,6 +19,7 @@ set(
app/gui/widgets/widget_state_capture.cc
app/gui/ui_helpers.cc
app/gui/editor_layout.cc
app/gui/editor_card_manager.cc
# Canvas system components
app/gui/canvas/canvas_modals.cc
app/gui/canvas/canvas_context_menu.cc