From 991366113efe676749577fdee06b3d0643147e4a Mon Sep 17 00:00:00 2001 From: scawful Date: Tue, 30 Sep 2025 13:10:32 -0400 Subject: [PATCH] Update canvas system with enhanced interaction and performance tracking features - Introduced a new CanvasInteractionHandler for managing tile interactions, including painting and selection modes. - Added CanvasContextMenu for improved user interaction with context-specific options. - Implemented CanvasPerformanceIntegration to monitor and report performance metrics during canvas operations. - Developed CanvasUsageTracker to track user interactions and usage patterns within the canvas. - Refactored existing canvas utilities and integrated new modal systems for enhanced user experience. - Updated CMake configuration to include new canvas components and ensure proper linking with existing libraries. --- Doxyfile | 2 +- src/app/editor/overworld/overworld_editor.cc | 100 +-- src/app/gui/canvas.cc | 327 +++++++++- src/app/gui/canvas.h | 154 ++++- src/app/gui/canvas/canvas.cmake | 72 +++ src/app/gui/canvas/canvas_context_menu.cc | 541 ++++++++++++++++ src/app/gui/canvas/canvas_context_menu.h | 153 +++++ .../gui/canvas/canvas_interaction_handler.cc | 369 +++++++++++ .../gui/canvas/canvas_interaction_handler.h | 208 ++++++ src/app/gui/canvas/canvas_modals.cc | 591 +++++++++++++++++ src/app/gui/canvas/canvas_modals.h | 182 ++++++ .../canvas/canvas_performance_integration.cc | 603 ++++++++++++++++++ .../canvas/canvas_performance_integration.h | 269 ++++++++ src/app/gui/canvas/canvas_usage_tracker.cc | 429 +++++++++++++ src/app/gui/canvas/canvas_usage_tracker.h | 249 ++++++++ src/app/gui/canvas/canvas_utils_moved.cc | 398 ++++++++++++ src/app/gui/canvas/canvas_utils_moved.h | 198 ++++++ src/app/gui/gui.cmake | 6 + 18 files changed, 4792 insertions(+), 59 deletions(-) create mode 100644 src/app/gui/canvas/canvas.cmake create mode 100644 src/app/gui/canvas/canvas_context_menu.cc create mode 100644 src/app/gui/canvas/canvas_context_menu.h create mode 100644 src/app/gui/canvas/canvas_interaction_handler.cc create mode 100644 src/app/gui/canvas/canvas_interaction_handler.h create mode 100644 src/app/gui/canvas/canvas_modals.cc create mode 100644 src/app/gui/canvas/canvas_modals.h create mode 100644 src/app/gui/canvas/canvas_performance_integration.cc create mode 100644 src/app/gui/canvas/canvas_performance_integration.h create mode 100644 src/app/gui/canvas/canvas_usage_tracker.cc create mode 100644 src/app/gui/canvas/canvas_usage_tracker.h create mode 100644 src/app/gui/canvas/canvas_utils_moved.cc create mode 100644 src/app/gui/canvas/canvas_utils_moved.h diff --git a/Doxyfile b/Doxyfile index dbad5138..c61b06d3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -85,7 +85,7 @@ OUTPUT_DIRECTORY = # control the number of sub-directories. # The default value is: NO. -CREATE_SUBDIRS = NO +CREATE_SUBDIRS = YES # Controls the number of sub-directories that will be created when # CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every diff --git a/src/app/editor/overworld/overworld_editor.cc b/src/app/editor/overworld/overworld_editor.cc index 6984237f..d9393772 100644 --- a/src/app/editor/overworld/overworld_editor.cc +++ b/src/app/editor/overworld/overworld_editor.cc @@ -12,7 +12,6 @@ #include "absl/strings/str_format.h" #include "app/core/asar_wrapper.h" #include "app/core/features.h" -#include "app/gfx/performance_profiler.h" #include "app/core/window.h" #include "app/editor/overworld/entity.h" #include "app/editor/overworld/map_properties.h" @@ -36,7 +35,6 @@ #include "util/log.h" #include "util/macro.h" - namespace yaze::editor { using core::Renderer; @@ -143,7 +141,7 @@ absl::Status OverworldEditor::Load() { RETURN_IF_ERROR( tile16_editor_.Initialize(tile16_blockset_bmp_, current_gfx_bmp_, *overworld_.mutable_all_tiles_types())); - + // CRITICAL FIX: Initialize tile16 editor with the correct overworld palette tile16_editor_.set_palette(palette_); tile16_editor_.set_rom(rom_); @@ -949,8 +947,8 @@ void OverworldEditor::CheckForOverworldEdits() { current_tile16_); // Apply the current selected tile to each position in the rectangle - for (int y = start_y; y <= end_y; y += kTile16Size) { - for (int x = start_x; x <= end_x; x += kTile16Size) { + for (int y = start_y, i = 0; y <= end_y; y += kTile16Size) { + for (int x = start_x; x <= end_x; x += kTile16Size, ++i) { // Determine which local map (512x512) the tile is in int local_map_x = x / local_map_size; int local_map_y = y / local_map_size; @@ -963,37 +961,36 @@ void OverworldEditor::CheckForOverworldEdits() { int index_x = local_map_x * tiles_per_local_map + tile16_x; int index_y = local_map_y * tiles_per_local_map + tile16_y; + overworld_.set_current_world(current_world_); + overworld_.set_current_map(current_map_); + int tile16_id = overworld_.GetTileFromPosition( + ow_map_canvas_.selected_tiles()[i]); // Bounds check for the selected world array if (index_x >= 0 && index_x < 0x200 && index_y >= 0 && index_y < 0x200) { - // CRITICAL FIX: Set the tile to the currently selected tile16, not read from canvas - selected_world[index_x][index_y] = current_tile16_; + selected_world[index_x][index_y] = tile16_id; // CRITICAL FIX: Also update the bitmap directly like single tile drawing ImVec2 tile_position(x, y); - auto tile_data = - gfx::GetTilemapData(tile16_blockset_, current_tile16_); + auto tile_data = gfx::GetTilemapData(tile16_blockset_, tile16_id); if (!tile_data.empty()) { RenderUpdatedMapBitmap(tile_position, tile_data); util::logf( "CheckForOverworldEdits: Updated bitmap at position (%d,%d) " "with tile16_id=%d", - x, y, current_tile16_); + x, y, tile16_id); } else { util::logf("ERROR: Failed to get tile data for tile16_id=%d", - current_tile16_); + tile16_id); } - } else { - util::logf( - "ERROR: Rectangle selection position [%d,%d] out of bounds", - index_x, index_y); } } } + RefreshOverworldMap(); // Clear the rectangle selection after applying - ow_map_canvas_.mutable_selected_tiles()->clear(); - ow_map_canvas_.mutable_points()->clear(); + // ow_map_canvas_.mutable_selected_tiles()->clear(); + // ow_map_canvas_.mutable_points()->clear(); util::logf( "CheckForOverworldEdits: Rectangle selection applied and cleared"); } @@ -1008,7 +1005,7 @@ void OverworldEditor::CheckForSelectRectangle() { current_tile16_ = overworld_.GetTileFromPosition(ow_map_canvas_.selected_tile_pos()); ow_map_canvas_.set_selected_tile_pos(ImVec2(-1, -1)); - + // Scroll blockset canvas to show the selected tile ScrollBlocksetCanvasToCurrentTile(); } @@ -1228,8 +1225,8 @@ absl::Status OverworldEditor::CheckForCurrentMap() { const int parent_map_x = highlight_parent % 8; const int parent_map_y = highlight_parent / 8; ow_map_canvas_.DrawOutline(parent_map_x * kOverworldMapSize, - parent_map_y * kOverworldMapSize, large_map_size, - large_map_size); + parent_map_y * kOverworldMapSize, + large_map_size, large_map_size); } else { // Calculate map coordinates accounting for world offset int current_map_x; @@ -1375,7 +1372,7 @@ absl::Status OverworldEditor::DrawTile16Selector() { blockset_canvas_.DrawContextMenu(); blockset_canvas_.DrawBitmap(tile16_blockset_.atlas, /*border_offset=*/2, map_blockset_loaded_, /*scale=*/2); - bool tile_selected = false; + bool tile_selected = false; // Call DrawTileSelector after event detection for visual feedback if (blockset_canvas_.DrawTileSelector(32.0f)) { @@ -1385,7 +1382,7 @@ absl::Status OverworldEditor::DrawTile16Selector() { // Then check for single click (if not double-click) if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && - blockset_canvas_.IsMouseHovering()) { + blockset_canvas_.IsMouseHovering()) { tile_selected = true; } @@ -1404,7 +1401,7 @@ absl::Status OverworldEditor::DrawTile16Selector() { if (id != current_tile16_ && id >= 0 && id < 512) { current_tile16_ = id; RETURN_IF_ERROR(tile16_editor_.SetCurrentTile(id)); - + // Scroll blockset canvas to show the selected tile ScrollBlocksetCanvasToCurrentTile(); } @@ -2122,7 +2119,7 @@ void OverworldEditor::RefreshChildMapOnDemand(int map_index) { // Check if ZSCustomOverworld v3 is present uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; bool use_v3_area_sizes = (asm_version >= 3 && asm_version != 0xFF); - + if (use_v3_area_sizes) { // Use v3 multi-area coordination RefreshMultiAreaMapsSafely(map_index, map); @@ -2251,7 +2248,8 @@ void OverworldEditor::RefreshMultiAreaMapsSafely(int map_index, overworld_.GetMapTiles(current_world_)); if (status.ok()) { maps_bmp_[sibling].set_data(sibling_map->bitmap_data()); - maps_bmp_[sibling].SetPalette(overworld_.current_area_palette()); + maps_bmp_[sibling].SetPalette( + overworld_.current_area_palette()); maps_bmp_[sibling].set_modified(false); // Update texture if it exists @@ -2297,16 +2295,17 @@ absl::Status OverworldEditor::RefreshMapPalette() { // Use v3 area size system using zelda3::AreaSizeEnum; auto area_size = overworld_.overworld_map(current_map_)->area_size(); - + if (area_size != AreaSizeEnum::SmallArea) { // Get all sibling maps that need palette updates std::vector sibling_maps; int parent_id = overworld_.overworld_map(current_map_)->parent(); - + switch (area_size) { case AreaSizeEnum::LargeArea: // 2x2 grid: parent, parent+1, parent+8, parent+9 - sibling_maps = {parent_id, parent_id + 1, parent_id + 8, parent_id + 9}; + sibling_maps = {parent_id, parent_id + 1, parent_id + 8, + parent_id + 9}; break; case AreaSizeEnum::WideArea: // 2x1 grid: parent, parent+1 @@ -2319,7 +2318,7 @@ absl::Status OverworldEditor::RefreshMapPalette() { default: break; } - + // Update palette for all siblings for (int sibling_index : sibling_maps) { if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) { @@ -2338,7 +2337,8 @@ absl::Status OverworldEditor::RefreshMapPalette() { if (overworld_.overworld_map(current_map_)->is_large_map()) { // We need to update the map and its siblings if it's a large map for (int i = 1; i < 4; i++) { - int sibling_index = overworld_.overworld_map(current_map_)->parent() + i; + int sibling_index = + overworld_.overworld_map(current_map_)->parent() + i; if (i >= 2) sibling_index += 6; RETURN_IF_ERROR( @@ -2354,7 +2354,7 @@ absl::Status OverworldEditor::RefreshMapPalette() { void OverworldEditor::RefreshMapProperties() { const auto& current_ow_map = *overworld_.mutable_overworld_map(current_map_); - + // Check if ZSCustomOverworld v3 is present uint8_t asm_version = (*rom_)[zelda3::OverworldCustomASMHasBeenApplied]; bool use_v3_area_sizes = (asm_version >= 3); @@ -2363,12 +2363,12 @@ void OverworldEditor::RefreshMapProperties() { // Use v3 area size system using zelda3::AreaSizeEnum; auto area_size = current_ow_map.area_size(); - + if (area_size != AreaSizeEnum::SmallArea) { // Get all sibling maps that need property updates std::vector sibling_maps; int parent_id = current_ow_map.parent(); - + switch (area_size) { case AreaSizeEnum::LargeArea: // 2x2 grid: parent+1, parent+8, parent+9 (skip parent itself) @@ -2385,7 +2385,7 @@ void OverworldEditor::RefreshMapProperties() { default: break; } - + // Copy properties from parent map to all siblings for (int sibling_index : sibling_maps) { if (sibling_index < 0 || sibling_index >= zelda3::kNumOverworldMaps) { @@ -3198,11 +3198,12 @@ void OverworldEditor::SetupOverworldCanvasContextMenu() { RefreshOverworldMap(); auto status = RefreshTile16Blockset(); if (!status.ok()) { - util::logf("Failed to refresh tile16 blockset: %s", status.message().data()); + util::logf("Failed to refresh tile16 blockset: %s", + status.message().data()); } }; ow_map_canvas_.AddContextMenuItem(refresh_map_item); - + // Canvas controls gui::Canvas::ContextMenuItem reset_pos_item; reset_pos_item.label = "Reset Canvas Position"; @@ -3224,33 +3225,36 @@ void OverworldEditor::ScrollBlocksetCanvasToCurrentTile() { // Calculate the position of the current tile in the blockset canvas // Blockset is arranged in an 8-tile-per-row grid, each tile is 16x16 pixels constexpr int kTilesPerRow = 8; - constexpr int kTileDisplaySize = 32; // Each tile displayed at 32x32 (16x16 at 2x scale) - + constexpr int kTileDisplaySize = + 32; // Each tile displayed at 32x32 (16x16 at 2x scale) + // Calculate tile position in canvas coordinates (absolute position in the grid) int tile_col = current_tile16_ % kTilesPerRow; int tile_row = current_tile16_ / kTilesPerRow; float tile_x = static_cast(tile_col * kTileDisplaySize); float tile_y = static_cast(tile_row * kTileDisplaySize); - + // Get the canvas dimensions ImVec2 canvas_size = blockset_canvas_.canvas_size(); - + // Calculate the scroll position to center the tile in the viewport float scroll_x = tile_x - (canvas_size.x / 2.0F) + (kTileDisplaySize / 2.0F); float scroll_y = tile_y - (canvas_size.y / 2.0F) + (kTileDisplaySize / 2.0F); - + // Clamp scroll to valid ranges (don't scroll beyond bounds) - if (scroll_x < 0) scroll_x = 0; - if (scroll_y < 0) scroll_y = 0; - + if (scroll_x < 0) + scroll_x = 0; + if (scroll_y < 0) + scroll_y = 0; + // Update the blockset canvas scrolling position first blockset_canvas_.set_scrolling(ImVec2(-1, -scroll_y)); - + // Set the points to draw the white outline box around the current tile // Points are in canvas coordinates (not screen coordinates) - blockset_canvas_.mutable_points()->clear(); - blockset_canvas_.mutable_points()->push_back(ImVec2(tile_x, tile_y)); - blockset_canvas_.mutable_points()->push_back(ImVec2(tile_x + kTileDisplaySize, tile_y + kTileDisplaySize)); + // blockset_canvas_.mutable_points()->clear(); + // blockset_canvas_.mutable_points()->push_back(ImVec2(tile_x, tile_y)); + // blockset_canvas_.mutable_points()->push_back(ImVec2(tile_x + kTileDisplaySize, tile_y + kTileDisplaySize)); } void OverworldEditor::DrawOverworldProperties() { diff --git a/src/app/gui/canvas.cc b/src/app/gui/canvas.cc index ee302664..c6c7fc9a 100644 --- a/src/app/gui/canvas.cc +++ b/src/app/gui/canvas.cc @@ -70,6 +70,12 @@ void Canvas::InitializeDefaults() { // Initialize palette editor palette_editor_ = std::make_unique(); + // Initialize interaction handler + interaction_handler_.Initialize(canvas_id_); + + // Initialize enhanced components + InitializeEnhancedComponents(); + // Initialize legacy compatibility variables to match config enable_grid_ = config_.enable_grid; enable_hex_tile_labels_ = config_.enable_hex_labels; @@ -86,6 +92,93 @@ void Canvas::InitializeDefaults() { void Canvas::Cleanup() { palette_editor_.reset(); selection_.Clear(); + + // Stop performance monitoring before cleanup to prevent segfault + if (performance_integration_) { + performance_integration_->StopMonitoring(); + } + + // Cleanup enhanced components + modals_.reset(); + context_menu_.reset(); + usage_tracker_.reset(); + performance_integration_.reset(); +} + +void Canvas::InitializeEnhancedComponents() { + // Initialize modals system + modals_ = std::make_unique(); + + // Initialize context menu system + context_menu_ = std::make_unique(); + context_menu_->Initialize(canvas_id_); + + // Initialize usage tracker + usage_tracker_ = std::make_shared(); + usage_tracker_->Initialize(canvas_id_); + canvas::CanvasUsageManager::Get().RegisterTracker(canvas_id_, usage_tracker_); + + // Initialize performance integration + performance_integration_ = std::make_shared(); + performance_integration_->Initialize(canvas_id_); + performance_integration_->SetUsageTracker(usage_tracker_); + canvas::CanvasPerformanceManager::Get().RegisterIntegration(canvas_id_, performance_integration_); + + // Start performance monitoring + performance_integration_->StartMonitoring(); + usage_tracker_->StartSession(); +} + +void Canvas::SetUsageMode(canvas::CanvasUsage usage) { + if (usage_tracker_) { + usage_tracker_->SetUsageMode(usage); + } + if (context_menu_) { + context_menu_->SetUsageMode(usage); + } +} + +canvas::CanvasUsage Canvas::GetUsageMode() const { + if (usage_tracker_) { + return usage_tracker_->GetCurrentStats().usage_mode; + } + return canvas::CanvasUsage::kUnknown; +} + +void Canvas::RecordCanvasOperation(const std::string& operation_name, double time_ms) { + if (usage_tracker_) { + usage_tracker_->RecordOperation(operation_name, time_ms); + } + if (performance_integration_) { + performance_integration_->RecordOperation(operation_name, time_ms, GetUsageMode()); + } +} + +void Canvas::ShowPerformanceUI() { + if (performance_integration_) { + performance_integration_->RenderPerformanceUI(); + } +} + +void Canvas::ShowUsageReport() { + if (usage_tracker_) { + std::string report = usage_tracker_->ExportUsageReport(); + // Show report in a modal or window + if (modals_) { + // Create a simple text display modal + ImGui::OpenPopup("Canvas Usage Report"); + if (ImGui::BeginPopupModal("Canvas Usage Report", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::Text("Canvas Usage Report"); + ImGui::Separator(); + ImGui::TextWrapped("%s", report.c_str()); + ImGui::Separator(); + if (ImGui::Button("Close")) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + } } void Canvas::InitializePaletteEditor(Rom* rom) { @@ -174,6 +267,24 @@ ImVec2 Canvas::GetLastClickPosition() const { return ImVec2(-1, -1); // Invalid position } +// ==================== Modern ImGui-Style Interface ==================== + +void Canvas::Begin(ImVec2 canvas_size) { + // Modern ImGui-style begin - combines DrawBackground + DrawContextMenu + DrawBackground(canvas_size); + DrawContextMenu(); +} + +void Canvas::End() { + // Modern ImGui-style end - automatically draws grid and overlay + if (config_.enable_grid) { + DrawGrid(); + } + DrawOverlay(); +} + +// ==================== Legacy Interface ==================== + void Canvas::UpdateColorPainter(gfx::Bitmap &bitmap, const ImVec4 &color, const std::function &event, int tile_size, float scale) { @@ -250,6 +361,119 @@ void Canvas::DrawContextMenu() { const ImVec2 origin(canvas_p0_.x + scrolling_.x, canvas_p0_.y + scrolling_.y); // Lock scrolled origin const ImVec2 mouse_pos(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + // Update canvas state for enhanced components + if (usage_tracker_) { + usage_tracker_->UpdateCanvasState(canvas_sz_, config_.content_size, + global_scale_, custom_step_, + enable_grid_, enable_hex_tile_labels_, + enable_custom_labels_); + } + + // Use enhanced context menu if available + if (context_menu_) { + canvas::CanvasConfig snapshot; + snapshot.canvas_size = canvas_sz_; + snapshot.content_size = config_.content_size; + snapshot.global_scale = global_scale_; + snapshot.grid_step = custom_step_; + snapshot.enable_grid = enable_grid_; + snapshot.enable_hex_labels = enable_hex_tile_labels_; + snapshot.enable_custom_labels = enable_custom_labels_; + snapshot.enable_context_menu = enable_context_menu_; + snapshot.is_draggable = draggable_; + snapshot.auto_resize = config_.auto_resize; + snapshot.scrolling = scrolling_; + + context_menu_->SetCanvasState(canvas_sz_, config_.content_size, + global_scale_, custom_step_, enable_grid_, + enable_hex_tile_labels_, enable_custom_labels_, + enable_context_menu_, draggable_, + config_.auto_resize, scrolling_); + + context_menu_->Render( + context_id_, mouse_pos, bitmap_, + bitmap_ ? bitmap_->mutable_palette() : nullptr, + [this](canvas::CanvasContextMenu::Command command, + const canvas::CanvasConfig& updated_config) { + switch (command) { + case canvas::CanvasContextMenu::Command::kResetView: + ResetView(); + break; + case canvas::CanvasContextMenu::Command::kZoomToFit: + if (bitmap_) { + SetZoomToFit(*bitmap_); + } + break; + case canvas::CanvasContextMenu::Command::kZoomIn: + SetGlobalScale(config_.global_scale * 1.25f); + break; + case canvas::CanvasContextMenu::Command::kZoomOut: + SetGlobalScale(config_.global_scale * 0.8f); + break; + case canvas::CanvasContextMenu::Command::kToggleGrid: + config_.enable_grid = !config_.enable_grid; + enable_grid_ = config_.enable_grid; + break; + case canvas::CanvasContextMenu::Command::kToggleHexLabels: + config_.enable_hex_labels = !config_.enable_hex_labels; + enable_hex_tile_labels_ = config_.enable_hex_labels; + break; + case canvas::CanvasContextMenu::Command::kToggleCustomLabels: + config_.enable_custom_labels = !config_.enable_custom_labels; + enable_custom_labels_ = config_.enable_custom_labels; + break; + case canvas::CanvasContextMenu::Command::kToggleContextMenu: + config_.enable_context_menu = !config_.enable_context_menu; + enable_context_menu_ = config_.enable_context_menu; + break; + case canvas::CanvasContextMenu::Command::kToggleAutoResize: + config_.auto_resize = !config_.auto_resize; + break; + case canvas::CanvasContextMenu::Command::kToggleDraggable: + config_.is_draggable = !config_.is_draggable; + draggable_ = config_.is_draggable; + break; + case canvas::CanvasContextMenu::Command::kSetGridStep: + config_.grid_step = updated_config.grid_step; + custom_step_ = config_.grid_step; + break; + case canvas::CanvasContextMenu::Command::kSetScale: + config_.global_scale = updated_config.global_scale; + global_scale_ = config_.global_scale; + break; + case canvas::CanvasContextMenu::Command::kOpenAdvancedProperties: + if (modals_) { + canvas::CanvasConfig modal_config = updated_config; + modal_config.on_config_changed = + [this](const canvas::CanvasConfig& cfg) { ApplyConfigSnapshot(cfg); }; + modal_config.on_scale_changed = + [this](const canvas::CanvasConfig& cfg) { ApplyScaleSnapshot(cfg); }; + modals_->ShowAdvancedProperties(canvas_id_, modal_config, bitmap_); + } + break; + case canvas::CanvasContextMenu::Command::kOpenScalingControls: + if (modals_) { + canvas::CanvasConfig modal_config = updated_config; + modal_config.on_config_changed = + [this](const canvas::CanvasConfig& cfg) { ApplyConfigSnapshot(cfg); }; + modal_config.on_scale_changed = + [this](const canvas::CanvasConfig& cfg) { ApplyScaleSnapshot(cfg); }; + modals_->ShowScalingControls(canvas_id_, modal_config, bitmap_); + } + break; + default: + break; + } + }, + snapshot); + + if (modals_) { + modals_->Render(); + } + + return; + } static bool show_bitmap_data = false; if (show_bitmap_data && bitmap_ != nullptr) { @@ -461,8 +685,6 @@ void Canvas::ClearContextMenuItems() { context_menu_items_.clear(); } -// Old ShowPaletteEditor method removed - now handled by EnhancedPaletteEditor - void Canvas::SetZoomToFit(const gfx::Bitmap& bitmap) { if (!bitmap.is_active()) return; @@ -486,6 +708,35 @@ void Canvas::ResetView() { scrolling_ = ImVec2(0, 0); } +void Canvas::ApplyConfigSnapshot(const canvas::CanvasConfig& snapshot) { + config_.enable_grid = snapshot.enable_grid; + config_.enable_hex_labels = snapshot.enable_hex_labels; + config_.enable_custom_labels = snapshot.enable_custom_labels; + config_.enable_context_menu = snapshot.enable_context_menu; + config_.is_draggable = snapshot.is_draggable; + config_.auto_resize = snapshot.auto_resize; + config_.grid_step = snapshot.grid_step; + config_.global_scale = snapshot.global_scale; + config_.canvas_size = snapshot.canvas_size; + config_.content_size = snapshot.content_size; + config_.custom_canvas_size = snapshot.canvas_size.x > 0 && snapshot.canvas_size.y > 0; + + enable_grid_ = config_.enable_grid; + enable_hex_tile_labels_ = config_.enable_hex_labels; + enable_custom_labels_ = config_.enable_custom_labels; + enable_context_menu_ = config_.enable_context_menu; + draggable_ = config_.is_draggable; + custom_step_ = config_.grid_step; + global_scale_ = config_.global_scale; + scrolling_ = snapshot.scrolling; +} + +void Canvas::ApplyScaleSnapshot(const canvas::CanvasConfig& snapshot) { + config_.global_scale = snapshot.global_scale; + global_scale_ = config_.global_scale; + scrolling_ = snapshot.scrolling; +} + bool Canvas::DrawTilePainter(const Bitmap &bitmap, int size, float scale) { const ImGuiIO &io = GetIO(); const bool is_hovered = IsItemHovered(); @@ -913,7 +1164,7 @@ void Canvas::DrawBitmapGroup(std::vector &group, gfx::Tilemap &tilemap, } // OPTIMIZATION: Use optimized rendering for large groups to improve performance - bool use_optimized_rendering = group.size() > 16; // Optimize for large selections + bool use_optimized_rendering = group.size() > 128; // Optimize for large selections // Pre-calculate common values to avoid repeated computation const float tile_scale = tile_size * scale; @@ -954,8 +1205,7 @@ void Canvas::DrawBitmapGroup(std::vector &group, gfx::Tilemap &tilemap, int tile_id = group[i]; // Check if tile_id is within the range of tile16_individual_ - auto tilemap_size = - tilemap.atlas.width() * tilemap.atlas.height() / tilemap.map_size.x; + auto tilemap_size = tilemap.map_size.x; if (tile_id >= 0 && tile_id < tilemap_size) { // Calculate the position of the tile within the rectangle int tile_pos_x = (x + start_tile_x) * tile_size * scale; @@ -1108,7 +1358,7 @@ void Canvas::DrawOverlay() { .grid_step = config_.grid_step }; - // Use high-level utility function + // Use high-level utility function with local points (synchronized from interaction handler) CanvasUtils::DrawCanvasOverlay(ctx, points_, selected_points_); } @@ -1266,6 +1516,36 @@ void TableCanvasPipeline(gui::Canvas &canvas, gfx::Bitmap &bitmap, } void Canvas::ShowAdvancedCanvasProperties() { + // Use the new modal system if available + if (modals_) { + canvas::CanvasConfig modal_config; + modal_config.canvas_size = canvas_sz_; + modal_config.content_size = config_.content_size; + modal_config.global_scale = global_scale_; + modal_config.grid_step = custom_step_; + modal_config.enable_grid = enable_grid_; + modal_config.enable_hex_labels = enable_hex_tile_labels_; + modal_config.enable_custom_labels = enable_custom_labels_; + modal_config.enable_context_menu = enable_context_menu_; + modal_config.is_draggable = draggable_; + modal_config.auto_resize = config_.auto_resize; + modal_config.scrolling = scrolling_; + modal_config.on_config_changed = [this](const canvas::CanvasConfig& updated_config) { + // Update legacy variables when config changes + enable_grid_ = updated_config.enable_grid; + enable_hex_tile_labels_ = updated_config.enable_hex_labels; + enable_custom_labels_ = updated_config.enable_custom_labels; + }; + modal_config.on_scale_changed = [this](const canvas::CanvasConfig& updated_config) { + global_scale_ = updated_config.global_scale; + scrolling_ = updated_config.scrolling; + }; + + modals_->ShowAdvancedProperties(canvas_id_, modal_config, bitmap_); + return; + } + + // Fallback to legacy modal system if (ImGui::BeginPopupModal("Advanced Canvas Properties", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Advanced Canvas Configuration"); ImGui::Separator(); @@ -1346,6 +1626,39 @@ void Canvas::ShowAdvancedCanvasProperties() { // Old ShowPaletteManager method removed - now handled by EnhancedPaletteEditor void Canvas::ShowScalingControls() { + // Use the new modal system if available + if (modals_) { + canvas::CanvasConfig modal_config; + modal_config.canvas_size = canvas_sz_; + modal_config.content_size = config_.content_size; + modal_config.global_scale = global_scale_; + modal_config.grid_step = custom_step_; + modal_config.enable_grid = enable_grid_; + modal_config.enable_hex_labels = enable_hex_tile_labels_; + modal_config.enable_custom_labels = enable_custom_labels_; + modal_config.enable_context_menu = enable_context_menu_; + modal_config.is_draggable = draggable_; + modal_config.auto_resize = config_.auto_resize; + modal_config.scrolling = scrolling_; + modal_config.on_config_changed = [this](const canvas::CanvasConfig& updated_config) { + // Update legacy variables when config changes + enable_grid_ = updated_config.enable_grid; + enable_hex_tile_labels_ = updated_config.enable_hex_labels; + enable_custom_labels_ = updated_config.enable_custom_labels; + enable_context_menu_ = updated_config.enable_context_menu; + }; + modal_config.on_scale_changed = [this](const canvas::CanvasConfig& updated_config) { + draggable_ = updated_config.is_draggable; + custom_step_ = updated_config.grid_step; + global_scale_ = updated_config.global_scale; + scrolling_ = updated_config.scrolling; + }; + + modals_->ShowScalingControls(canvas_id_, modal_config); + return; + } + + // Fallback to legacy modal system if (ImGui::BeginPopupModal("Scaling Controls", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Canvas Scaling and Display Controls"); ImGui::Separator(); @@ -1439,8 +1752,6 @@ void Canvas::ShowScalingControls() { } } -// Old ROM palette management methods removed - now handled by EnhancedPaletteEditor - // BPP format management methods void Canvas::ShowBppFormatSelector() { if (!bpp_format_ui_) { diff --git a/src/app/gui/canvas.h b/src/app/gui/canvas.h index 04b726c8..cdb5ba88 100644 --- a/src/app/gui/canvas.h +++ b/src/app/gui/canvas.h @@ -15,6 +15,11 @@ #include "app/gui/enhanced_palette_editor.h" #include "app/gfx/bpp_format_manager.h" #include "app/gui/bpp_format_ui.h" +#include "app/gui/canvas/canvas_modals.h" +#include "app/gui/canvas/canvas_context_menu.h" +#include "app/gui/canvas/canvas_usage_tracker.h" +#include "app/gui/canvas/canvas_performance_integration.h" +#include "app/gui/canvas/canvas_interaction_handler.h" #include "imgui/imgui.h" namespace yaze { @@ -103,6 +108,37 @@ class Canvas { void UpdateInfoGrid(ImVec2 bg_size, float grid_size = 64.0f, int label_id = 0); + // ==================== Modern ImGui-Style Interface ==================== + + /** + * @brief Begin canvas rendering (ImGui-style) + * + * Modern alternative to DrawBackground(). Handles: + * - Background and border rendering + * - Size calculation + * - Scroll/drag setup + * - Context menu + * + * Usage: + * ```cpp + * canvas.Begin(); + * canvas.DrawBitmap(bitmap); + * if (canvas.DrawTilePainter(tile, 16)) { ... } + * canvas.End(); // Draws grid and overlay + * ``` + */ + void Begin(ImVec2 canvas_size = ImVec2(0, 0)); + + /** + * @brief End canvas rendering (ImGui-style) + * + * Modern alternative to manual DrawGrid() + DrawOverlay(). + * Automatically draws grid and overlay if enabled. + */ + void End(); + + // ==================== Legacy Interface (Backward Compatible) ==================== + // Background for the Canvas represents region without any content drawn to // it, but can be controlled by the user. void DrawBackground(ImVec2 canvas_size = ImVec2(0, 0)); @@ -125,6 +161,13 @@ class Canvas { std::unique_ptr bpp_conversion_dialog_; std::unique_ptr bpp_comparison_tool_; + // Enhanced canvas components + std::unique_ptr modals_; + std::unique_ptr context_menu_; + std::shared_ptr usage_tracker_; + std::shared_ptr performance_integration_; + canvas::CanvasInteractionHandler interaction_handler_; + void AddContextMenuItem(const ContextMenuItem& item); void ClearContextMenuItems(); void SetContextMenuEnabled(bool enabled) { context_menu_enabled_ = enabled; } @@ -134,6 +177,8 @@ class Canvas { void ShowScalingControls(); void SetZoomToFit(const gfx::Bitmap& bitmap); void ResetView(); + void ApplyConfigSnapshot(const canvas::CanvasConfig& snapshot); + void ApplyScaleSnapshot(const canvas::CanvasConfig& snapshot); // Modular component access CanvasConfig& GetConfig() { return config_; } @@ -154,6 +199,18 @@ class Canvas { bool ConvertBitmapFormat(gfx::BppFormat target_format); gfx::BppFormat GetCurrentBppFormat() const; + // Enhanced canvas management + void InitializeEnhancedComponents(); + void SetUsageMode(canvas::CanvasUsage usage); + canvas::CanvasUsage GetUsageMode() const; + void RecordCanvasOperation(const std::string& operation_name, double time_ms); + void ShowPerformanceUI(); + void ShowUsageReport(); + + // Interaction handler access + canvas::CanvasInteractionHandler& GetInteractionHandler() { return interaction_handler_; } + const canvas::CanvasInteractionHandler& GetInteractionHandler() const { return interaction_handler_; } + // Initialization and cleanup void InitializeDefaults(); void Cleanup(); @@ -226,8 +283,9 @@ class Canvas { void ZoomIn() { global_scale_ += 0.25f; } void ZoomOut() { global_scale_ -= 0.25f; } - auto points() const { return points_; } - auto mutable_points() { return &points_; } + // Points accessors - points_ is maintained separately for custom overlay drawing + const ImVector& points() const { return points_; } + ImVector* mutable_points() { return &points_; } auto push_back(ImVec2 pos) { points_.push_back(pos); } auto draw_list() const { return draw_list_; } auto zero_point() const { return canvas_p0_; } @@ -345,6 +403,7 @@ class Canvas { ImVec2 mouse_pos_in_canvas_; // Drawing and labeling + // NOTE: points_ synchronized from interaction_handler_ for backward compatibility ImVector points_; ImVector> labels_; @@ -382,6 +441,97 @@ void BitmapCanvasPipeline(gui::Canvas &canvas, gfx::Bitmap &bitmap, int width, void TableCanvasPipeline(gui::Canvas &canvas, gfx::Bitmap &bitmap, const std::string& label = "", bool auto_resize = true); +/** + * @class ScopedCanvas + * @brief RAII wrapper for Canvas (ImGui-style) + * + * Automatically calls Begin() on construction and End() on destruction, + * preventing forgotten End() calls and ensuring proper cleanup. + * + * Usage: + * ```cpp + * { + * gui::ScopedCanvas canvas("MyCanvas", ImVec2(512, 512)); + * canvas->DrawBitmap(bitmap); + * if (canvas->DrawTilePainter(tile, 16)) { + * HandlePaint(canvas->drawn_tile_position()); + * } + * } // Automatic End() and cleanup + * ``` + * + * Or wrap existing canvas: + * ```cpp + * Canvas my_canvas("Editor"); + * { + * ScopedCanvas scoped(my_canvas); + * scoped->DrawBitmap(bitmap); + * } // Automatic End() + * ``` + */ +class ScopedCanvas { + public: + /** + * @brief Construct and begin a new canvas + */ + explicit ScopedCanvas(const std::string& id, ImVec2 canvas_size = ImVec2(0, 0)) + : canvas_(new Canvas(id, canvas_size)), owned_(true), active_(true) { + canvas_->Begin(); + } + + /** + * @brief Wrap existing canvas with RAII + */ + explicit ScopedCanvas(Canvas& canvas) + : canvas_(&canvas), owned_(false), active_(true) { + canvas_->Begin(); + } + + /** + * @brief Destructor automatically calls End() + */ + ~ScopedCanvas() { + if (active_ && canvas_) { + canvas_->End(); + } + if (owned_) { + delete canvas_; + } + } + + // No copy, move only + ScopedCanvas(const ScopedCanvas&) = delete; + ScopedCanvas& operator=(const ScopedCanvas&) = delete; + + ScopedCanvas(ScopedCanvas&& other) noexcept + : canvas_(other.canvas_), owned_(other.owned_), active_(other.active_) { + other.active_ = false; + other.canvas_ = nullptr; + } + + /** + * @brief Arrow operator for clean syntax: scoped->DrawBitmap(...) + */ + Canvas* operator->() { return canvas_; } + const Canvas* operator->() const { return canvas_; } + + /** + * @brief Dereference operator for direct access: (*scoped).DrawBitmap(...) + */ + Canvas& operator*() { return *canvas_; } + const Canvas& operator*() const { return *canvas_; } + + /** + * @brief Get underlying canvas + */ + Canvas* get() { return canvas_; } + const Canvas* get() const { return canvas_; } + + private: + Canvas* canvas_; + bool owned_; + bool active_; +}; + } // namespace gui } // namespace yaze diff --git a/src/app/gui/canvas/canvas.cmake b/src/app/gui/canvas/canvas.cmake new file mode 100644 index 00000000..4218f057 --- /dev/null +++ b/src/app/gui/canvas/canvas.cmake @@ -0,0 +1,72 @@ +# Canvas system CMake configuration +# This file configures the canvas system components + +# Canvas core components +set(CANVAS_SOURCES + canvas_modals.cc + canvas_context_menu.cc + canvas_usage_tracker.cc + canvas_performance_integration.cc + canvas_interaction_handler.cc +) + +set(CANVAS_HEADERS + canvas_modals.h + canvas_context_menu.h + canvas_usage_tracker.h + canvas_performance_integration.h + canvas_interaction_handler.h +) + +# Create canvas library +add_library(yaze_canvas STATIC + ${CANVAS_SOURCES} + ${CANVAS_HEADERS} +) + +# Set target properties +set_target_properties(yaze_canvas PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + POSITION_INDEPENDENT_CODE ON +) + +# Include directories +target_include_directories(yaze_canvas PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../../incl +) + +# Link dependencies +target_link_libraries(yaze_canvas PUBLIC + yaze_gfx + yaze_gui_common + imgui + SDL2::SDL2 +) + +# Compiler-specific options +if(MSVC) + target_compile_options(yaze_canvas PRIVATE /W4) +else() + target_compile_options(yaze_canvas PRIVATE -Wall -Wextra -Wpedantic) +endif() + +# Add canvas to parent GUI library +target_sources(yaze_gui PRIVATE + ${CANVAS_SOURCES} + ${CANVAS_HEADERS} +) + +# Install rules +install(TARGETS yaze_canvas + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib + RUNTIME DESTINATION bin +) + +install(FILES ${CANVAS_HEADERS} + DESTINATION include/yaze/app/gui/canvas +) diff --git a/src/app/gui/canvas/canvas_context_menu.cc b/src/app/gui/canvas/canvas_context_menu.cc new file mode 100644 index 00000000..bbbabfa7 --- /dev/null +++ b/src/app/gui/canvas/canvas_context_menu.cc @@ -0,0 +1,541 @@ +#include "canvas_context_menu.h" + +#include +#include +#include + +#include "app/gfx/performance_profiler.h" +#include "app/gfx/performance_dashboard.h" +#include "app/gui/enhanced_palette_editor.h" +#include "app/gui/bpp_format_ui.h" +#include "app/gui/icons.h" +#include "gui/canvas/canvas_modals.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +namespace { +inline void Dispatch( + const std::function& handler, + CanvasContextMenu::Command command, CanvasConfig config) { + if (handler) { + handler(command, config); + } +} +} // namespace + +void CanvasContextMenu::Initialize(const std::string& canvas_id) { + canvas_id_ = canvas_id; + enabled_ = true; + current_usage_ = CanvasUsage::kTilePainting; + + // Initialize canvas state + canvas_size_ = ImVec2(0, 0); + content_size_ = ImVec2(0, 0); + global_scale_ = 1.0F; + grid_step_ = 32.0F; + enable_grid_ = true; + enable_hex_labels_ = false; + enable_custom_labels_ = false; + enable_context_menu_ = true; + is_draggable_ = false; + auto_resize_ = false; + scrolling_ = ImVec2(0, 0); + + // Create default menu items + CreateDefaultMenuItems(); +} + +void CanvasContextMenu::SetUsageMode(CanvasUsage usage) { + current_usage_ = usage; +} + +void CanvasContextMenu::AddMenuItem(const ContextMenuItem& item) { + global_items_.push_back(item); +} + +void CanvasContextMenu::AddMenuItem(const ContextMenuItem& item, CanvasUsage usage) { + usage_specific_items_[usage].push_back(item); +} + +void CanvasContextMenu::ClearMenuItems() { + global_items_.clear(); + usage_specific_items_.clear(); +} + +void CanvasContextMenu::Render( + const std::string& context_id, const ImVec2& mouse_pos, + const gfx::Bitmap* bitmap, const gfx::SnesPalette* palette, + const std::function& command_handler, + CanvasConfig current_config) { + if (!enabled_) return; + + // Context menu (under default mouse threshold) + if (ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right); + enable_context_menu_ && drag_delta.x == 0.0F && drag_delta.y == 0.0F) { + ImGui::OpenPopupOnItemClick(context_id.c_str(), ImGuiPopupFlags_MouseButtonRight); + } + + // Contents of the Context Menu + if (ImGui::BeginPopup(context_id.c_str())) { + // Render usage-specific menu first + RenderUsageSpecificMenu(); + + // Add separator if there are usage-specific items + if (!usage_specific_items_[current_usage_].empty()) { + ImGui::Separator(); + } + + // Render view controls + RenderViewControlsMenu(command_handler, current_config); + ImGui::Separator(); + + // Render canvas properties + RenderCanvasPropertiesMenu(command_handler, current_config); + ImGui::Separator(); + + // Render bitmap operations if bitmap is available + if (bitmap) { + RenderBitmapOperationsMenu(bitmap); + ImGui::Separator(); + } + + // Render palette operations if palette is available + if (palette) { + RenderPaletteOperationsMenu(palette); + ImGui::Separator(); + } + + if (bitmap) { + RenderBppOperationsMenu(bitmap); + ImGui::Separator(); + } + + RenderPerformanceMenu(); + ImGui::Separator(); + + RenderGridControlsMenu(command_handler, current_config); + ImGui::Separator(); + + RenderScalingControlsMenu(command_handler, current_config); + + // Render global menu items + if (!global_items_.empty()) { + ImGui::Separator(); + RenderMenuSection("Custom Actions", global_items_); + } + + ImGui::EndPopup(); + } +} + +bool CanvasContextMenu::ShouldShowContextMenu() const { + return enabled_ && enable_context_menu_; +} + +void CanvasContextMenu::SetCanvasState(const ImVec2& canvas_size, + const ImVec2& content_size, + float global_scale, + float grid_step, + bool enable_grid, + bool enable_hex_labels, + bool enable_custom_labels, + bool enable_context_menu, + bool is_draggable, + bool auto_resize, + const ImVec2& scrolling) { + canvas_size_ = canvas_size; + content_size_ = content_size; + global_scale_ = global_scale; + grid_step_ = grid_step; + enable_grid_ = enable_grid; + enable_hex_labels_ = enable_hex_labels; + enable_custom_labels_ = enable_custom_labels; + enable_context_menu_ = enable_context_menu; + is_draggable_ = is_draggable; + auto_resize_ = auto_resize; + scrolling_ = scrolling; +} + +void CanvasContextMenu::RenderMenuItem(const ContextMenuItem& item) { + if (!item.visible_condition()) { + return; + } + + if (!item.enabled_condition()) { + ImGui::BeginDisabled(); + } + + if (item.subitems.empty()) { + // Simple menu item + if (ImGui::MenuItem(item.label.c_str(), + item.shortcut.empty() ? nullptr : item.shortcut.c_str())) { + item.callback(); + } + } else { + // Menu with subitems + if (ImGui::BeginMenu(item.label.c_str())) { + for (const auto& subitem : item.subitems) { + RenderMenuItem(subitem); + } + ImGui::EndMenu(); + } + } + + if (!item.enabled_condition()) { + ImGui::EndDisabled(); + } + + if (item.separator_after) { + ImGui::Separator(); + } +} + +void CanvasContextMenu::RenderMenuSection(const std::string& title, + const std::vector& items) { + if (items.empty()) return; + + ImGui::TextColored(ImVec4(0.7F, 0.7F, 0.7F, 1.0F), "%s", title.c_str()); + for (const auto& item : items) { + RenderMenuItem(item); + } +} + +void CanvasContextMenu::RenderUsageSpecificMenu() { + auto it = usage_specific_items_.find(current_usage_); + if (it == usage_specific_items_.end() || it->second.empty()) { + return; + } + + std::string usage_name = GetUsageModeName(current_usage_); + ImVec4 usage_color = GetUsageModeColor(current_usage_); + + ImGui::TextColored(usage_color, "%s %s Mode", ICON_MD_COLOR_LENS, usage_name.c_str()); + ImGui::Separator(); + + for (const auto& item : it->second) { + RenderMenuItem(item); + } +} + +void CanvasContextMenu::RenderViewControlsMenu( + const std::function& command_handler, + CanvasConfig current_config) { + if (ImGui::BeginMenu("View Controls")) { + if (ImGui::MenuItem("Reset View", "Ctrl+R")) { + Dispatch(command_handler, Command::kResetView, current_config); + } + if (ImGui::MenuItem("Zoom to Fit", "Ctrl+F")) { + Dispatch(command_handler, Command::kZoomToFit, current_config); + } + if (ImGui::MenuItem("Zoom In", "Ctrl++")) { + CanvasConfig updated = current_config; + updated.global_scale *= 1.25F; + Dispatch(command_handler, Command::kSetScale, updated); + } + if (ImGui::MenuItem("Zoom Out", "Ctrl+-")) { + CanvasConfig updated = current_config; + updated.global_scale *= 0.8F; + Dispatch(command_handler, Command::kSetScale, updated); + } + ImGui::Separator(); + if (ImGui::MenuItem("Show Grid", nullptr, enable_grid_)) { + CanvasConfig updated = current_config; + updated.enable_grid = !enable_grid_; + Dispatch(command_handler, Command::kToggleGrid, updated); + } + if (ImGui::MenuItem("Show Hex Labels", nullptr, enable_hex_labels_)) { + CanvasConfig updated = current_config; + updated.enable_hex_labels = !enable_hex_labels_; + Dispatch(command_handler, Command::kToggleHexLabels, updated); + } + if (ImGui::MenuItem("Show Custom Labels", nullptr, enable_custom_labels_)) { + CanvasConfig updated = current_config; + updated.enable_custom_labels = !enable_custom_labels_; + Dispatch(command_handler, Command::kToggleCustomLabels, updated); + } + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderCanvasPropertiesMenu( + const std::function& command_handler, + CanvasConfig current_config) { + if (ImGui::BeginMenu(ICON_MD_SETTINGS " Canvas Properties")) { + ImGui::Text("Canvas Size: %.0f x %.0f", canvas_size_.x, canvas_size_.y); + ImGui::Text("Content Size: %.0f x %.0f", content_size_.x, content_size_.y); + ImGui::Text("Global Scale: %.2f", global_scale_); + ImGui::Text("Grid Step: %.1f", grid_step_); + ImGui::Text("Mouse Position: %.0f x %.0f", 0.0F, 0.0F); // Would need actual mouse pos + + if (ImGui::MenuItem("Advanced Properties...")) { + CanvasConfig updated = current_config; + updated.enable_grid = enable_grid_; + updated.enable_hex_labels = enable_hex_labels_; + updated.enable_custom_labels = enable_custom_labels_; + updated.enable_context_menu = enable_context_menu_; + updated.is_draggable = is_draggable_; + updated.auto_resize = auto_resize_; + updated.grid_step = grid_step_; + updated.canvas_size = canvas_size_; + updated.content_size = content_size_; + updated.scrolling = scrolling_; + Dispatch(command_handler, Command::kOpenAdvancedProperties, updated); + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderBitmapOperationsMenu(const gfx::Bitmap* bitmap) { + if (ImGui::BeginMenu(ICON_MD_IMAGE " Bitmap Operations")) { + ImGui::Text("Size: %d x %d", bitmap->width(), bitmap->height()); + ImGui::Text("Format: %s", "Unknown"); // Would need format detection + + if (ImGui::MenuItem("Edit Bitmap Data...")) { + // Open bitmap data editor + } + if (ImGui::MenuItem("Export Bitmap...")) { + // Export bitmap + } + if (ImGui::MenuItem("Import Bitmap...")) { + // Import bitmap + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderPaletteOperationsMenu(const gfx::SnesPalette* palette) { + if (ImGui::BeginMenu(ICON_MD_PALETTE " Palette Operations")) { + ImGui::Text("Colors: %zu", palette->size()); + + if (ImGui::MenuItem("Edit Palette...")) { + // Open palette editor + } + if (ImGui::MenuItem("Color Analysis...")) { + // Open color analysis + } + if (ImGui::MenuItem("Apply ROM Palette...")) { + // Apply ROM palette + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderBppOperationsMenu(const gfx::Bitmap* bitmap) { + if (ImGui::BeginMenu(ICON_MD_SWAP_HORIZ " BPP Operations")) { + if (ImGui::MenuItem("Format Analysis...")) { + // Open BPP analysis + } + if (ImGui::MenuItem("Convert Format...")) { + // Open BPP conversion dialog + } + if (ImGui::MenuItem("Format Comparison...")) { + // Open format comparison tool + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderPerformanceMenu() { + if (ImGui::BeginMenu(ICON_MD_TRENDING_UP " Performance")) { + auto& profiler = gfx::PerformanceProfiler::Get(); + auto canvas_stats = profiler.GetStats("canvas_operations"); + auto draw_stats = profiler.GetStats("canvas_draw"); + + ImGui::Text("Canvas Operations: %zu", canvas_stats.sample_count); + ImGui::Text("Average Time: %.2f ms", draw_stats.avg_time_us / 1000.0); + + if (ImGui::MenuItem("Performance Dashboard...")) { + gfx::PerformanceDashboard::Get().SetVisible(true); + } + if (ImGui::MenuItem("Usage Report...")) { + // Open usage report + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderGridControlsMenu( + const std::function& command_handler, + CanvasConfig current_config) { + if (ImGui::BeginMenu(ICON_MD_GRID_ON " Grid Controls")) { + const struct GridOption { + const char* label; + float value; + } options[] = {{"8x8", 8.0F}, {"16x16", 16.0F}, + {"32x32", 32.0F}, {"64x64", 64.0F}}; + + for (const auto& option : options) { + bool selected = grid_step_ == option.value; + if (ImGui::MenuItem(option.label, nullptr, selected)) { + CanvasConfig updated = current_config; + updated.grid_step = option.value; + Dispatch(command_handler, Command::kSetGridStep, updated); + } + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderScalingControlsMenu( + const std::function& command_handler, + CanvasConfig current_config) { + if (ImGui::BeginMenu(ICON_MD_ZOOM_IN " Scaling Controls")) { + const struct ScaleOption { + const char* label; + float value; + } options[] = {{"0.25x", 0.25F}, {"0.5x", 0.5F}, {"1x", 1.0F}, + {"2x", 2.0F}, {"4x", 4.0F}, {"8x", 8.0F}}; + + for (const auto& option : options) { + if (ImGui::MenuItem(option.label)) { + CanvasConfig updated = current_config; + updated.global_scale = option.value; + Dispatch(command_handler, Command::kSetScale, updated); + } + } + + ImGui::EndMenu(); + } +} + +void CanvasContextMenu::RenderMaterialIcon(const std::string& icon_name, const ImVec4& color) { + // Simple material icon rendering using Unicode symbols + static std::unordered_map icon_map = { + {"grid_on", ICON_MD_GRID_ON}, {"label", ICON_MD_LABEL}, {"edit", ICON_MD_EDIT}, {"menu", ICON_MD_MENU}, + {"drag_indicator", ICON_MD_DRAG_INDICATOR}, {"fit_screen", ICON_MD_FIT_SCREEN}, {"zoom_in", ICON_MD_ZOOM_IN}, + {"speed", ICON_MD_SPEED}, {"timer", ICON_MD_TIMER}, {"functions", ICON_MD_FUNCTIONS}, {"schedule", ICON_MD_SCHEDULE}, + {"refresh", ICON_MD_REFRESH}, {"settings", ICON_MD_SETTINGS}, {"info", ICON_MD_INFO}, + {"view", ICON_MD_VISIBILITY}, {"properties", ICON_MD_SETTINGS}, {"bitmap", ICON_MD_IMAGE}, {"palette", ICON_MD_PALETTE}, + {"bpp", ICON_MD_SWAP_HORIZ}, {"performance", ICON_MD_TRENDING_UP}, {"grid", ICON_MD_GRID_ON}, {"scaling", ICON_MD_ZOOM_IN} + }; + + auto it = icon_map.find(icon_name); + if (it != icon_map.end()) { + ImGui::TextColored(color, "%s", it->second); + } +} + +std::string CanvasContextMenu::GetUsageModeName(CanvasUsage usage) const { + switch (usage) { + case CanvasUsage::kTilePainting: return "Tile Painting"; + case CanvasUsage::kTileSelecting: return "Tile Selecting"; + case CanvasUsage::kSelectRectangle: return "Rectangle Selection"; + case CanvasUsage::kColorPainting: return "Color Painting"; + case CanvasUsage::kBitmapEditing: return "Bitmap Editing"; + case CanvasUsage::kPaletteEditing: return "Palette Editing"; + case CanvasUsage::kBppConversion: return "BPP Conversion"; + case CanvasUsage::kPerformanceMode: return "Performance Mode"; + case CanvasUsage::kUnknown: return "Unknown"; + default: return "Unknown"; + } +} + +ImVec4 CanvasContextMenu::GetUsageModeColor(CanvasUsage usage) const { + switch (usage) { + case CanvasUsage::kTilePainting: return ImVec4(0.2F, 1.0F, 0.2F, 1.0F); // Green + case CanvasUsage::kTileSelecting: return ImVec4(0.2F, 0.8F, 1.0F, 1.0F); // Blue + case CanvasUsage::kSelectRectangle: return ImVec4(1.0F, 0.8F, 0.2F, 1.0F); // Yellow + case CanvasUsage::kColorPainting: return ImVec4(1.0F, 0.2F, 1.0F, 1.0F); // Magenta + case CanvasUsage::kBitmapEditing: return ImVec4(1.0F, 0.5F, 0.2F, 1.0F); // Orange + case CanvasUsage::kPaletteEditing: return ImVec4(0.8F, 0.2F, 1.0F, 1.0F); // Purple + case CanvasUsage::kBppConversion: return ImVec4(0.2F, 1.0F, 1.0F, 1.0F); // Cyan + case CanvasUsage::kPerformanceMode: return ImVec4(1.0F, 0.2F, 0.2F, 1.0F); // Red + case CanvasUsage::kUnknown: return ImVec4(0.7F, 0.7F, 0.7F, 1.0F); // Gray + default: return ImVec4(0.7F, 0.7F, 0.7F, 1.0F); // Gray + } +} + +void CanvasContextMenu::CreateDefaultMenuItems() { + // Create default menu items for different usage modes + + // Tile Painting mode items + ContextMenuItem tile_paint_item("Paint Tile", "paint", []() { + // Tile painting action + }); + usage_specific_items_[CanvasUsage::kTilePainting].push_back(tile_paint_item); + + // Tile Selecting mode items + ContextMenuItem tile_select_item("Select Tile", "select", []() { + // Tile selection action + }); + usage_specific_items_[CanvasUsage::kTileSelecting].push_back(tile_select_item); + + // Rectangle Selection mode items + ContextMenuItem rect_select_item("Select Rectangle", "rect", []() { + // Rectangle selection action + }); + usage_specific_items_[CanvasUsage::kSelectRectangle].push_back(rect_select_item); + + // Color Painting mode items + ContextMenuItem color_paint_item("Paint Color", "color", []() { + // Color painting action + }); + usage_specific_items_[CanvasUsage::kColorPainting].push_back(color_paint_item); + + // Bitmap Editing mode items + ContextMenuItem bitmap_edit_item("Edit Bitmap", "edit", []() { + // Bitmap editing action + }); + usage_specific_items_[CanvasUsage::kBitmapEditing].push_back(bitmap_edit_item); + + // Palette Editing mode items + ContextMenuItem palette_edit_item("Edit Palette", "palette", []() { + // Palette editing action + }); + usage_specific_items_[CanvasUsage::kPaletteEditing].push_back(palette_edit_item); + + // BPP Conversion mode items + ContextMenuItem bpp_convert_item("Convert Format", "convert", []() { + // BPP conversion action + }); + usage_specific_items_[CanvasUsage::kBppConversion].push_back(bpp_convert_item); + + // Performance Mode items + ContextMenuItem perf_item("Performance Analysis", "perf", []() { + // Performance analysis action + }); + usage_specific_items_[CanvasUsage::kPerformanceMode].push_back(perf_item); +} + +CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateViewMenuItem(const std::string& label, + const std::string& icon, + std::function callback) { + return ContextMenuItem(label, icon, callback); +} + +CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBitmapMenuItem(const std::string& label, + const std::string& icon, + std::function callback) { + return ContextMenuItem(label, icon, callback); +} + +CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePaletteMenuItem(const std::string& label, + const std::string& icon, + std::function callback) { + return ContextMenuItem(label, icon, callback); +} + +CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreateBppMenuItem(const std::string& label, + const std::string& icon, + std::function callback) { + return ContextMenuItem(label, icon, callback); +} + +CanvasContextMenu::ContextMenuItem CanvasContextMenu::CreatePerformanceMenuItem(const std::string& label, + const std::string& icon, + std::function callback) { + return ContextMenuItem(label, icon, callback); +} + +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_context_menu.h b/src/app/gui/canvas/canvas_context_menu.h new file mode 100644 index 00000000..7e5dd7fc --- /dev/null +++ b/src/app/gui/canvas/canvas_context_menu.h @@ -0,0 +1,153 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_CONTEXT_MENU_H +#define YAZE_APP_GUI_CANVAS_CANVAS_CONTEXT_MENU_H + +#include +#include +#include +#include + +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gui/icons.h" +#include "gui/canvas/canvas_modals.h" +#include "canvas_usage_tracker.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +class CanvasContextMenu { + public: + enum class Command { + kNone, + kResetView, + kZoomToFit, + kZoomIn, + kZoomOut, + kToggleGrid, + kToggleHexLabels, + kToggleCustomLabels, + kToggleContextMenu, + kToggleAutoResize, + kToggleDraggable, + kOpenAdvancedProperties, + kOpenScalingControls, + kSetGridStep, + kSetScale, + }; + + CanvasContextMenu() = default; + + struct ContextMenuItem { + std::string label; + std::string shortcut; + std::string icon; + std::function callback; + std::function enabled_condition = []() { return true; }; + std::function visible_condition = []() { return true; }; + std::vector subitems; + ImVec4 color = ImVec4(1, 1, 1, 1); + bool separator_after = false; + + ContextMenuItem() = default; + ContextMenuItem(const std::string& lbl, const std::string& ico, + std::function cb, const std::string& sc = "") + : label(lbl), shortcut(sc), icon(ico), callback(std::move(cb)) {} + }; + + void Initialize(const std::string& canvas_id); + void SetUsageMode(CanvasUsage usage); + void AddMenuItem(const ContextMenuItem& item); + void AddMenuItem(const ContextMenuItem& item, CanvasUsage usage); + void ClearMenuItems(); + + void Render(const std::string& context_id, + const ImVec2& mouse_pos, + const gfx::Bitmap* bitmap, + const gfx::SnesPalette* palette, + const std::function& command_handler, + CanvasConfig current_config); + + bool ShouldShowContextMenu() const; + void SetEnabled(bool enabled) { enabled_ = enabled; } + bool IsEnabled() const { return enabled_; } + CanvasUsage GetUsageMode() const { return current_usage_; } + + void SetCanvasState(const ImVec2& canvas_size, + const ImVec2& content_size, + float global_scale, + float grid_step, + bool enable_grid, + bool enable_hex_labels, + bool enable_custom_labels, + bool enable_context_menu, + bool is_draggable, + bool auto_resize, + const ImVec2& scrolling); + + private: + std::string canvas_id_; + bool enabled_ = true; + CanvasUsage current_usage_ = CanvasUsage::kTilePainting; + + ImVec2 canvas_size_; + ImVec2 content_size_; + float global_scale_ = 1.0f; + float grid_step_ = 32.0f; + bool enable_grid_ = true; + bool enable_hex_labels_ = false; + bool enable_custom_labels_ = false; + bool enable_context_menu_ = true; + bool is_draggable_ = false; + bool auto_resize_ = false; + ImVec2 scrolling_; + + std::unordered_map> usage_specific_items_; + std::vector global_items_; + + void RenderMenuItem(const ContextMenuItem& item); + void RenderMenuSection(const std::string& title, + const std::vector& items); + void RenderUsageSpecificMenu(); + void RenderViewControlsMenu(const std::function& command_handler, + CanvasConfig current_config); + void RenderCanvasPropertiesMenu(const std::function& command_handler, + CanvasConfig current_config); + void RenderBitmapOperationsMenu(const gfx::Bitmap* bitmap); + void RenderPaletteOperationsMenu(const gfx::SnesPalette* palette); + void RenderBppOperationsMenu(const gfx::Bitmap* bitmap); + void RenderPerformanceMenu(); + void RenderGridControlsMenu(const std::function& command_handler, + CanvasConfig current_config); + void RenderScalingControlsMenu(const std::function& command_handler, + CanvasConfig current_config); + + void RenderMaterialIcon(const std::string& icon_name, + const ImVec4& color = ImVec4(1, 1, 1, 1)); + std::string GetUsageModeName(CanvasUsage usage) const; + ImVec4 GetUsageModeColor(CanvasUsage usage) const; + + void CreateDefaultMenuItems(); + ContextMenuItem CreateViewMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + ContextMenuItem CreateBitmapMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + ContextMenuItem CreatePaletteMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + ContextMenuItem CreateBppMenuItem(const std::string& label, + const std::string& icon, + std::function callback); + ContextMenuItem CreatePerformanceMenuItem(const std::string& label, + const std::string& icon, + std::function callback); +}; + +} // namespace canvas +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_CONTEXT_MENU_H diff --git a/src/app/gui/canvas/canvas_interaction_handler.cc b/src/app/gui/canvas/canvas_interaction_handler.cc new file mode 100644 index 00000000..cbdb979a --- /dev/null +++ b/src/app/gui/canvas/canvas_interaction_handler.cc @@ -0,0 +1,369 @@ +#include "canvas_interaction_handler.h" + +#include +#include +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +namespace { + +// Helper function to align position to grid +ImVec2 AlignToGrid(ImVec2 pos, float grid_step) { + return ImVec2(std::floor(pos.x / grid_step) * grid_step, + std::floor(pos.y / grid_step) * grid_step); +} + +} // namespace + +void CanvasInteractionHandler::Initialize(const std::string& canvas_id) { + canvas_id_ = canvas_id; + ClearState(); +} + +void CanvasInteractionHandler::ClearState() { + hover_points_.clear(); + selected_points_.clear(); + selected_tiles_.clear(); + drawn_tile_pos_ = ImVec2(-1, -1); + mouse_pos_in_canvas_ = ImVec2(0, 0); + selected_tile_pos_ = ImVec2(-1, -1); + rect_select_active_ = false; +} + +TileInteractionResult CanvasInteractionHandler::Update( + ImVec2 canvas_p0, ImVec2 scrolling, float /*global_scale*/, float /*tile_size*/, + ImVec2 /*canvas_size*/, bool is_hovered) { + + TileInteractionResult result; + + if (!is_hovered) { + hover_points_.clear(); + return result; + } + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + mouse_pos_in_canvas_ = ImVec2(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + + // Update based on current mode - each mode is handled by its specific Draw method + // This method exists for future state updates if needed + (void)current_mode_; // Suppress unused warning + + return result; +} + +bool CanvasInteractionHandler::DrawTilePainter( + const gfx::Bitmap& bitmap, ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, float global_scale, float tile_size, bool is_hovered) { + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + const auto scaled_size = tile_size * global_scale; + + // Clear hover when not hovering + if (!is_hovered) { + hover_points_.clear(); + return false; + } + + // Reset previous hover + hover_points_.clear(); + + // Calculate grid-aligned paint position + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + mouse_pos_in_canvas_ = paint_pos; + auto paint_pos_end = ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size); + + hover_points_.push_back(paint_pos); + hover_points_.push_back(paint_pos_end); + + // Draw preview of tile at hover position + if (bitmap.is_active() && draw_list) { + draw_list->AddImage( + (ImTextureID)(intptr_t)bitmap.texture(), + ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y), + ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size)); + } + + // Check for paint action + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && + ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + drawn_tile_pos_ = paint_pos; + return true; + } + + return false; +} + +bool CanvasInteractionHandler::DrawTilemapPainter( + gfx::Tilemap& tilemap, int current_tile, ImDrawList* draw_list, + ImVec2 canvas_p0, ImVec2 scrolling, float global_scale, bool is_hovered) { + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + + // Safety check + if (!tilemap.atlas.is_active() || tilemap.tile_size.x <= 0) { + return false; + } + + const auto scaled_size = tilemap.tile_size.x * global_scale; + + if (!is_hovered) { + hover_points_.clear(); + return false; + } + + hover_points_.clear(); + + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_size); + mouse_pos_in_canvas_ = paint_pos; + + hover_points_.push_back(paint_pos); + hover_points_.push_back(ImVec2(paint_pos.x + scaled_size, paint_pos.y + scaled_size)); + + // Draw tile preview from atlas + if (tilemap.atlas.is_active() && tilemap.atlas.texture() && draw_list) { + int tiles_per_row = tilemap.atlas.width() / tilemap.tile_size.x; + if (tiles_per_row > 0) { + int tile_x = (current_tile % tiles_per_row) * tilemap.tile_size.x; + int tile_y = (current_tile / tiles_per_row) * tilemap.tile_size.y; + + if (tile_x >= 0 && tile_x < tilemap.atlas.width() && + tile_y >= 0 && tile_y < tilemap.atlas.height()) { + + ImVec2 uv0 = ImVec2(static_cast(tile_x) / tilemap.atlas.width(), + static_cast(tile_y) / tilemap.atlas.height()); + ImVec2 uv1 = ImVec2(static_cast(tile_x + tilemap.tile_size.x) / tilemap.atlas.width(), + static_cast(tile_y + tilemap.tile_size.y) / tilemap.atlas.height()); + + draw_list->AddImage( + (ImTextureID)(intptr_t)tilemap.atlas.texture(), + ImVec2(origin.x + paint_pos.x, origin.y + paint_pos.y), + ImVec2(origin.x + paint_pos.x + scaled_size, origin.y + paint_pos.y + scaled_size), + uv0, uv1); + } + } + } + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) || + ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + drawn_tile_pos_ = paint_pos; + return true; + } + + return false; +} + +bool CanvasInteractionHandler::DrawSolidTilePainter( + const ImVec4& color, ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, float global_scale, float tile_size, bool is_hovered) { + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + auto scaled_tile_size = tile_size * global_scale; + static bool is_dragging = false; + static ImVec2 start_drag_pos; + + if (!is_hovered) { + hover_points_.clear(); + return false; + } + + hover_points_.clear(); + + ImVec2 paint_pos = AlignToGrid(mouse_pos, scaled_tile_size); + mouse_pos_in_canvas_ = paint_pos; + + // Clamp to canvas bounds (assuming canvas_size from Update) + // For now, skip clamping as we don't have canvas_size here + + hover_points_.push_back(paint_pos); + hover_points_.push_back(ImVec2(paint_pos.x + scaled_tile_size, paint_pos.y + scaled_tile_size)); + + if (draw_list) { + draw_list->AddRectFilled( + ImVec2(origin.x + paint_pos.x + 1, origin.y + paint_pos.y + 1), + ImVec2(origin.x + paint_pos.x + scaled_tile_size, + origin.y + paint_pos.y + scaled_tile_size), + IM_COL32(color.x * 255, color.y * 255, color.z * 255, 255)); + } + + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + is_dragging = true; + start_drag_pos = paint_pos; + } + + if (is_dragging && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { + is_dragging = false; + drawn_tile_pos_ = start_drag_pos; + return true; + } + + return false; +} + +bool CanvasInteractionHandler::DrawTileSelector( + ImDrawList* /*draw_list*/, ImVec2 canvas_p0, ImVec2 scrolling, float tile_size, + bool is_hovered) { + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + + if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + hover_points_.clear(); + ImVec2 painter_pos = AlignToGrid(mouse_pos, tile_size); + + hover_points_.push_back(painter_pos); + hover_points_.push_back(ImVec2(painter_pos.x + tile_size, painter_pos.y + tile_size)); + mouse_pos_in_canvas_ = painter_pos; + } + + if (is_hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + return true; + } + + return false; +} + +bool CanvasInteractionHandler::DrawSelectRect( + int current_map, ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + float global_scale, float tile_size, bool is_hovered) { + + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + const ImVec2 mouse_pos(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); + const float scaled_size = tile_size * global_scale; + static ImVec2 drag_start_pos; + static bool dragging = false; + constexpr int small_map_size = 0x200; + + if (!is_hovered) { + return false; + } + + // Calculate superX and superY accounting for world offset + int super_y = 0; + int super_x = 0; + if (current_map < 0x40) { + super_y = current_map / 8; + super_x = current_map % 8; + } else if (current_map < 0x80) { + super_y = (current_map - 0x40) / 8; + super_x = (current_map - 0x40) % 8; + } else { + super_y = (current_map - 0x80) / 8; + super_x = (current_map - 0x80) % 8; + } + + // Handle right click for single tile selection + if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { + ImVec2 painter_pos = AlignToGrid(mouse_pos, scaled_size); + int painter_x = painter_pos.x; + int painter_y = painter_pos.y; + + auto tile16_x = (painter_x % small_map_size) / (small_map_size / 0x20); + auto tile16_y = (painter_y % small_map_size) / (small_map_size / 0x20); + + int index_x = super_x * 0x20 + tile16_x; + int index_y = super_y * 0x20 + tile16_y; + selected_tile_pos_ = ImVec2(index_x, index_y); + selected_points_.clear(); + rect_select_active_ = false; + + drag_start_pos = AlignToGrid(mouse_pos, scaled_size); + } + + // Draw rectangle while dragging + ImVec2 drag_end_pos = AlignToGrid(mouse_pos, scaled_size); + if (ImGui::IsMouseDragging(ImGuiMouseButton_Right) && draw_list) { + auto start = ImVec2(canvas_p0.x + drag_start_pos.x, + canvas_p0.y + drag_start_pos.y); + auto end = ImVec2(canvas_p0.x + drag_end_pos.x + tile_size, + canvas_p0.y + drag_end_pos.y + tile_size); + draw_list->AddRect(start, end, IM_COL32(255, 255, 255, 255)); + dragging = true; + } + + // Complete selection on release + if (dragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) { + dragging = false; + + constexpr int tile16_size = 16; + int start_x = std::floor(drag_start_pos.x / scaled_size) * tile16_size; + int start_y = std::floor(drag_start_pos.y / scaled_size) * tile16_size; + int end_x = std::floor(drag_end_pos.x / scaled_size) * tile16_size; + int end_y = std::floor(drag_end_pos.y / scaled_size) * tile16_size; + + if (start_x > end_x) std::swap(start_x, end_x); + if (start_y > end_y) std::swap(start_y, end_y); + + selected_tiles_.clear(); + selected_tiles_.reserve(((end_x - start_x) / tile16_size + 1) * + ((end_y - start_y) / tile16_size + 1)); + + constexpr int tiles_per_local_map = small_map_size / 16; + + for (int tile_y = start_y; tile_y <= end_y; tile_y += tile16_size) { + for (int tile_x = start_x; tile_x <= end_x; tile_x += tile16_size) { + int local_map_x = tile_x / small_map_size; + int local_map_y = tile_y / small_map_size; + + int tile16_x = (tile_x % small_map_size) / tile16_size; + int tile16_y = (tile_y % small_map_size) / tile16_size; + + int index_x = local_map_x * tiles_per_local_map + tile16_x; + int index_y = local_map_y * tiles_per_local_map + tile16_y; + + selected_tiles_.emplace_back(index_x, index_y); + } + } + + selected_points_.clear(); + selected_points_.push_back(drag_start_pos); + selected_points_.push_back(drag_end_pos); + rect_select_active_ = true; + return true; + } + + return false; +} + +// Helper methods - these are thin wrappers that could be static but kept as instance +// methods for potential future state access +ImVec2 CanvasInteractionHandler::AlignPosToGrid(ImVec2 pos, float grid_step) { + return AlignToGrid(pos, grid_step); +} + +ImVec2 CanvasInteractionHandler::GetMousePosition(ImVec2 canvas_p0, ImVec2 scrolling) { + const ImGuiIO& imgui_io = ImGui::GetIO(); + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); + return ImVec2(imgui_io.MousePos.x - origin.x, imgui_io.MousePos.y - origin.y); +} + +bool CanvasInteractionHandler::IsMouseClicked(ImGuiMouseButton button) { + return ImGui::IsMouseClicked(button); +} + +bool CanvasInteractionHandler::IsMouseDoubleClicked(ImGuiMouseButton button) { + return ImGui::IsMouseDoubleClicked(button); +} + +bool CanvasInteractionHandler::IsMouseDragging(ImGuiMouseButton button) { + return ImGui::IsMouseDragging(button); +} + +bool CanvasInteractionHandler::IsMouseReleased(ImGuiMouseButton button) { + return ImGui::IsMouseReleased(button); +} + +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_interaction_handler.h b/src/app/gui/canvas/canvas_interaction_handler.h new file mode 100644 index 00000000..138b5378 --- /dev/null +++ b/src/app/gui/canvas/canvas_interaction_handler.h @@ -0,0 +1,208 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_HANDLER_H +#define YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_HANDLER_H + +#include +#include "app/gfx/bitmap.h" +#include "app/gfx/tilemap.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +/** + * @brief Tile interaction mode for canvas + */ +enum class TileInteractionMode { + kNone, // No interaction + kPaintSingle, // Paint single tiles + kPaintDrag, // Paint while dragging + kSelectSingle, // Select single tile + kSelectRectangle, // Select rectangular region + kColorPaint // Paint with solid color +}; + +/** + * @brief Result of a tile interaction operation + */ +struct TileInteractionResult { + bool interaction_occurred = false; + ImVec2 tile_position = ImVec2(-1, -1); + std::vector selected_tiles; + int tile_id = -1; + + void Reset() { + interaction_occurred = false; + tile_position = ImVec2(-1, -1); + selected_tiles.clear(); + tile_id = -1; + } +}; + +/** + * @brief Handles all tile-based interactions for Canvas + * + * Consolidates tile painting, selection, and multi-selection logic + * that was previously scattered across Canvas methods. Provides a + * unified interface for common tile interaction patterns. + * + * Key Features: + * - Single tile painting with preview + * - Drag painting for continuous tile placement + * - Single tile selection + * - Rectangle selection for multi-tile operations + * - Color painting mode + * - Grid-aligned positioning + * - Hover preview + */ +class CanvasInteractionHandler { + public: + CanvasInteractionHandler() = default; + + /** + * @brief Initialize the interaction handler + */ + void Initialize(const std::string& canvas_id); + + /** + * @brief Set the interaction mode + */ + void SetMode(TileInteractionMode mode) { current_mode_ = mode; } + TileInteractionMode GetMode() const { return current_mode_; } + + /** + * @brief Update interaction state (call once per frame) + * @param canvas_p0 Canvas top-left screen position + * @param scrolling Canvas scroll offset + * @param global_scale Canvas zoom scale + * @param tile_size Logical tile size + * @param canvas_size Canvas dimensions + * @param is_hovered Whether mouse is over canvas + * @return Interaction result for this frame + */ + TileInteractionResult Update(ImVec2 canvas_p0, ImVec2 scrolling, + float global_scale, float tile_size, + ImVec2 canvas_size, bool is_hovered); + + /** + * @brief Draw tile painter (preview + interaction) + * @param bitmap Tile bitmap to paint + * @param draw_list ImGui draw list + * @param canvas_p0 Canvas top-left position + * @param scrolling Canvas scroll offset + * @param global_scale Canvas zoom scale + * @param tile_size Logical tile size + * @param is_hovered Whether mouse is over canvas + * @return True if tile was painted + */ + bool DrawTilePainter(const gfx::Bitmap& bitmap, ImDrawList* draw_list, + ImVec2 canvas_p0, ImVec2 scrolling, float global_scale, + float tile_size, bool is_hovered); + + /** + * @brief Draw tilemap painter (preview + interaction) + */ + bool DrawTilemapPainter(gfx::Tilemap& tilemap, int current_tile, + ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, float global_scale, bool is_hovered); + + /** + * @brief Draw solid color painter + */ + bool DrawSolidTilePainter(const ImVec4& color, ImDrawList* draw_list, + ImVec2 canvas_p0, ImVec2 scrolling, + float global_scale, float tile_size, bool is_hovered); + + /** + * @brief Draw tile selector (single tile selection) + */ + bool DrawTileSelector(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, float tile_size, bool is_hovered); + + /** + * @brief Draw rectangle selector (multi-tile selection) + * @param current_map Map ID for coordinate calculation + * @param draw_list ImGui draw list + * @param canvas_p0 Canvas position + * @param scrolling Scroll offset + * @param global_scale Zoom scale + * @param tile_size Tile size + * @param is_hovered Whether mouse is over canvas + * @return True if selection was made + */ + bool DrawSelectRect(int current_map, ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, float global_scale, float tile_size, + bool is_hovered); + + /** + * @brief Get current hover points (for DrawOverlay) + */ + const ImVector& GetHoverPoints() const { return hover_points_; } + + /** + * @brief Get selected points (for DrawOverlay) + */ + const ImVector& GetSelectedPoints() const { return selected_points_; } + + /** + * @brief Get selected tiles from last rectangle selection + */ + const std::vector& GetSelectedTiles() const { return selected_tiles_; } + + /** + * @brief Get last drawn tile position + */ + ImVec2 GetDrawnTilePosition() const { return drawn_tile_pos_; } + + /** + * @brief Get current mouse position in canvas space + */ + ImVec2 GetMousePositionInCanvas() const { return mouse_pos_in_canvas_; } + + /** + * @brief Clear all interaction state + */ + void ClearState(); + + /** + * @brief Check if rectangle selection is active + */ + bool IsRectSelectActive() const { return rect_select_active_; } + + /** + * @brief Get selected tile position (for single selection) + */ + ImVec2 GetSelectedTilePosition() const { return selected_tile_pos_; } + + /** + * @brief Set selected tile position + */ + void SetSelectedTilePosition(ImVec2 pos) { selected_tile_pos_ = pos; } + + private: + std::string canvas_id_; + TileInteractionMode current_mode_ = TileInteractionMode::kNone; + + // Interaction state + ImVector hover_points_; // Current hover preview points + ImVector selected_points_; // Selected rectangle points + std::vector selected_tiles_; // Selected tiles from rect + ImVec2 drawn_tile_pos_ = ImVec2(-1, -1); // Last drawn tile position + ImVec2 mouse_pos_in_canvas_ = ImVec2(0, 0); // Current mouse in canvas space + ImVec2 selected_tile_pos_ = ImVec2(-1, -1); // Single tile selection + bool rect_select_active_ = false; + + // Helper methods + ImVec2 AlignPosToGrid(ImVec2 pos, float grid_step); + ImVec2 GetMousePosition(ImVec2 canvas_p0, ImVec2 scrolling); + bool IsMouseClicked(ImGuiMouseButton button); + bool IsMouseDoubleClicked(ImGuiMouseButton button); + bool IsMouseDragging(ImGuiMouseButton button); + bool IsMouseReleased(ImGuiMouseButton button); +}; + +} // namespace canvas +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_INTERACTION_HANDLER_H diff --git a/src/app/gui/canvas/canvas_modals.cc b/src/app/gui/canvas/canvas_modals.cc new file mode 100644 index 00000000..e240df9e --- /dev/null +++ b/src/app/gui/canvas/canvas_modals.cc @@ -0,0 +1,591 @@ +#include "canvas_modals.h" + +#include +#include +#include + +#include "app/gfx/performance_profiler.h" +#include "app/gfx/performance_dashboard.h" +#include "app/gui/enhanced_palette_editor.h" +#include "app/gui/bpp_format_ui.h" +#include "app/gui/icons.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +namespace { +void DispatchConfigCallback(const std::function& callback, + const CanvasConfig& config) { + if (callback) { + callback(config); + } +} + +void DispatchScaleCallback(const std::function& callback, + const CanvasConfig& config) { + if (callback) { + callback(config); + } +} +} // namespace + +void CanvasModals::ShowAdvancedProperties(const std::string& canvas_id, + const CanvasConfig& config, + const gfx::Bitmap* bitmap) { + + std::string modal_id = canvas_id + "_advanced_properties"; + + auto render_func = [=]() mutable { + CanvasConfig mutable_config = config; // Create mutable copy + mutable_config.on_config_changed = config.on_config_changed; + mutable_config.on_scale_changed = config.on_scale_changed; + RenderAdvancedPropertiesModal(modal_id, mutable_config, bitmap); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::ShowScalingControls(const std::string& canvas_id, + const CanvasConfig& config, + const gfx::Bitmap* bitmap) { + + std::string modal_id = canvas_id + "_scaling_controls"; + + auto render_func = [=]() mutable { + CanvasConfig mutable_config = config; // Create mutable copy + mutable_config.on_config_changed = config.on_config_changed; + mutable_config.on_scale_changed = config.on_scale_changed; + RenderScalingControlsModal(modal_id, mutable_config, bitmap); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::ShowBppConversionDialog(const std::string& canvas_id, + const BppConversionOptions& options) { + + std::string modal_id = canvas_id + "_bpp_conversion"; + + auto render_func = [=]() { + RenderBppConversionModal(modal_id, options); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::ShowPaletteEditor(const std::string& canvas_id, + const PaletteEditorOptions& options) { + + std::string modal_id = canvas_id + "_palette_editor"; + + auto render_func = [=]() { + RenderPaletteEditorModal(modal_id, options); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::ShowColorAnalysis(const std::string& canvas_id, + const ColorAnalysisOptions& options) { + + std::string modal_id = canvas_id + "_color_analysis"; + + auto render_func = [=]() { + RenderColorAnalysisModal(modal_id, options); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::ShowPerformanceIntegration(const std::string& canvas_id, + const PerformanceOptions& options) { + + std::string modal_id = canvas_id + "_performance"; + + auto render_func = [=]() { + RenderPerformanceModal(modal_id, options); + }; + + OpenModal(modal_id, render_func); +} + +void CanvasModals::Render() { + for (auto& modal : active_modals_) { + if (modal.is_open) { + modal.render_func(); + } + } + + // Remove closed modals + active_modals_.erase( + std::remove_if(active_modals_.begin(), active_modals_.end(), + [](const ModalState& modal) { return !modal.is_open; }), + active_modals_.end()); +} + +bool CanvasModals::IsAnyModalOpen() const { + return std::any_of(active_modals_.begin(), active_modals_.end(), + [](const ModalState& modal) { return modal.is_open; }); +} + +void CanvasModals::RenderAdvancedPropertiesModal(const std::string& canvas_id, + CanvasConfig& config, + const gfx::Bitmap* bitmap) { + + std::string modal_title = "Advanced Canvas Properties"; + ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_SETTINGS, modal_title.c_str()); + ImGui::Separator(); + + // Canvas Information Section + if (ImGui::CollapsingHeader(ICON_MD_ANALYTICS " Canvas Information", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Columns(2, "CanvasInfo"); + + RenderMetricCard("Canvas Size", + std::to_string(static_cast(config.canvas_size.x)) + " x " + + std::to_string(static_cast(config.canvas_size.y)), + ICON_MD_STRAIGHTEN, ImVec4(0.2F, 0.8F, 1.0F, 1.0F)); + + RenderMetricCard("Content Size", + std::to_string(static_cast(config.content_size.x)) + " x " + + std::to_string(static_cast(config.content_size.y)), + ICON_MD_IMAGE, ImVec4(0.8F, 0.2F, 1.0F, 1.0F)); + + ImGui::NextColumn(); + + RenderMetricCard("Global Scale", + std::to_string(static_cast(config.global_scale * 100)) + "%", + ICON_MD_ZOOM_IN, ImVec4(1.0F, 0.8F, 0.2F, 1.0F)); + + RenderMetricCard("Grid Step", + std::to_string(static_cast(config.grid_step)) + "px", + ICON_MD_GRID_ON, ImVec4(0.2F, 1.0F, 0.2F, 1.0F)); + + ImGui::Columns(1); + } + + // View Settings Section + if (ImGui::CollapsingHeader("👁️ View Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Checkbox("Show Grid", &config.enable_grid); + ImGui::SameLine(); + RenderMaterialIcon("grid_on"); + + ImGui::Checkbox("Show Hex Labels", &config.enable_hex_labels); + ImGui::SameLine(); + RenderMaterialIcon("label"); + + ImGui::Checkbox("Show Custom Labels", &config.enable_custom_labels); + ImGui::SameLine(); + RenderMaterialIcon("edit"); + + ImGui::Checkbox("Enable Context Menu", &config.enable_context_menu); + ImGui::SameLine(); + RenderMaterialIcon("menu"); + + ImGui::Checkbox("Draggable Canvas", &config.is_draggable); + ImGui::SameLine(); + RenderMaterialIcon("drag_indicator"); + + ImGui::Checkbox("Auto Resize for Tables", &config.auto_resize); + ImGui::SameLine(); + RenderMaterialIcon("fit_screen"); + } + + // Scale Controls Section + if (ImGui::CollapsingHeader(ICON_MD_BUILD " Scale Controls", ImGuiTreeNodeFlags_DefaultOpen)) { + RenderSliderWithIcon("Global Scale", "zoom_in", &config.global_scale, 0.1f, 10.0f, "%.2f"); + RenderSliderWithIcon("Grid Step", "grid_on", &config.grid_step, 1.0f, 128.0f, "%.1f"); + + // Preset scale buttons + ImGui::Text("Preset Scales:"); + ImGui::SameLine(); + + const char* preset_labels[] = {"0.25x", "0.5x", "1x", "2x", "4x", "8x"}; + const float preset_values[] = {0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f}; + + for (int i = 0; i < 6; ++i) { + if (i > 0) ImGui::SameLine(); + if (ImGui::Button(preset_labels[i])) { + config.global_scale = preset_values[i]; + DispatchConfigCallback(config.on_config_changed, config); + } + } + } + + // Scrolling Controls Section + if (ImGui::CollapsingHeader("📜 Scrolling Controls")) { + ImGui::Text("Current Scroll: %.1f, %.1f", config.scrolling.x, config.scrolling.y); + + if (ImGui::Button("Reset Scroll")) { + config.scrolling = ImVec2(0, 0); + DispatchConfigCallback(config.on_config_changed, config); + } + ImGui::SameLine(); + + if (ImGui::Button("Center View") && bitmap) { + config.scrolling = ImVec2(-(bitmap->width() * config.global_scale - config.canvas_size.x) / 2.0f, + -(bitmap->height() * config.global_scale - config.canvas_size.y) / 2.0f); + DispatchConfigCallback(config.on_config_changed, config); + } + } + + // Performance Integration Section + if (ImGui::CollapsingHeader(ICON_MD_TRENDING_UP " Performance")) { + auto& profiler = gfx::PerformanceProfiler::Get(); + + // Get stats for canvas operations + auto canvas_stats = profiler.GetStats("canvas_operations"); + auto draw_stats = profiler.GetStats("canvas_draw"); + + RenderMetricCard("Canvas Operations", + std::to_string(canvas_stats.sample_count) + " ops", + "speed", ImVec4(0.2F, 1.0F, 0.2F, 1.0F)); + + RenderMetricCard("Average Time", + std::to_string(draw_stats.avg_time_us / 1000.0) + " ms", + "timer", ImVec4(1.0F, 0.8F, 0.2F, 1.0F)); + + if (ImGui::Button("Open Performance Dashboard")) { + gfx::PerformanceDashboard::Get().SetVisible(true); + } + } + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Apply Changes", ImVec2(120, 0))) { + DispatchConfigCallback(config.on_config_changed, config); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + + if (ImGui::Button("Reset to Defaults", ImVec2(150, 0))) { + config.global_scale = 1.0f; + config.grid_step = 32.0f; + config.enable_grid = true; + config.enable_hex_labels = false; + config.enable_custom_labels = false; + config.enable_context_menu = true; + config.is_draggable = false; + config.auto_resize = false; + config.scrolling = ImVec2(0, 0); + DispatchConfigCallback(config.on_config_changed, config); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::RenderScalingControlsModal(const std::string& canvas_id, + CanvasConfig& config, + const gfx::Bitmap* bitmap) { + + std::string modal_title = "Canvas Scaling Controls"; + ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_ZOOM_IN, modal_title.c_str()); + ImGui::Separator(); + + // Global Scale Section + ImGui::Text("Global Scale: %.3f", config.global_scale); + RenderSliderWithIcon("##GlobalScale", "zoom_in", &config.global_scale, 0.1f, 10.0f, "%.2f"); + + // Preset scale buttons + ImGui::Text("Preset Scales:"); + const char* preset_labels[] = {"0.25x", "0.5x", "1x", "2x", "4x", "8x"}; + const float preset_values[] = {0.25f, 0.5f, 1.0f, 2.0f, 4.0f, 8.0f}; + + for (int i = 0; i < 6; ++i) { + if (i > 0) ImGui::SameLine(); + if (ImGui::Button(preset_labels[i])) { + config.global_scale = preset_values[i]; + DispatchScaleCallback(config.on_scale_changed, config); + } + } + + ImGui::Separator(); + + // Grid Configuration Section + ImGui::Text("Grid Step: %.1f", config.grid_step); + RenderSliderWithIcon("##GridStep", "grid_on", &config.grid_step, 1.0f, 128.0f, "%.1f"); + + // Grid size presets + ImGui::Text("Grid Presets:"); + const char* grid_labels[] = {"8x8", "16x16", "32x32", "64x64"}; + const float grid_values[] = {8.0f, 16.0f, 32.0f, 64.0f}; + + for (int i = 0; i < 4; ++i) { + if (i > 0) ImGui::SameLine(); + if (ImGui::Button(grid_labels[i])) { + config.grid_step = grid_values[i]; + DispatchScaleCallback(config.on_scale_changed, config); + } + } + + ImGui::Separator(); + + // Canvas Information Section + ImGui::Text("Canvas Information"); + ImGui::Text("Canvas Size: %.0f x %.0f", config.canvas_size.x, config.canvas_size.y); + ImGui::Text("Scaled Size: %.0f x %.0f", + config.canvas_size.x * config.global_scale, + config.canvas_size.y * config.global_scale); + + if (bitmap) { + ImGui::Text("Bitmap Size: %d x %d", bitmap->width(), bitmap->height()); + ImGui::Text("Effective Scale: %.3f x %.3f", + (config.canvas_size.x * config.global_scale) / bitmap->width(), + (config.canvas_size.y * config.global_scale) / bitmap->height()); + } + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Apply", ImVec2(120, 0))) { + DispatchScaleCallback(config.on_scale_changed, config); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::RenderBppConversionModal(const std::string& canvas_id, + const BppConversionOptions& options) { + + std::string modal_title = "BPP Format Conversion"; + ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_SWAP_HORIZ, modal_title.c_str()); + ImGui::Separator(); + + // Use the existing BppFormatUI for the conversion dialog + static std::unique_ptr bpp_ui = + std::make_unique(canvas_id + "_bpp_ui"); + + // Render the format selector + if (options.bitmap && options.palette) { + bpp_ui->RenderFormatSelector(const_cast(options.bitmap), + *options.palette, options.on_convert); + } + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Close", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::RenderPaletteEditorModal(const std::string& canvas_id, + const PaletteEditorOptions& options) { + + std::string modal_title = options.title.empty() ? "Palette Editor" : options.title; + ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_PALETTE, modal_title.c_str()); + ImGui::Separator(); + + // Use the existing EnhancedPaletteEditor + static std::unique_ptr palette_editor = + std::make_unique(); + + if (options.palette) { + palette_editor->ShowPaletteEditor(*options.palette, modal_title); + } + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Close", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::RenderColorAnalysisModal(const std::string& canvas_id, + const ColorAnalysisOptions& options) { + + std::string modal_title = "Color Analysis"; + ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_ZOOM_IN, modal_title.c_str()); + ImGui::Separator(); + + // Use the existing EnhancedPaletteEditor for color analysis + static std::unique_ptr palette_editor = + std::make_unique(); + + if (options.bitmap) { + palette_editor->ShowColorAnalysis(*options.bitmap, modal_title); + } + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Close", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::RenderPerformanceModal(const std::string& canvas_id, + const PerformanceOptions& options) { + + std::string modal_title = "Canvas Performance"; + ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver); + + if (ImGui::BeginPopupModal(modal_title.c_str(), nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) { + + // Header with icon + ImGui::Text("%s %s", ICON_MD_TRENDING_UP, modal_title.c_str()); + ImGui::Separator(); + + // Performance metrics + RenderMetricCard("Operation", options.operation_name, "speed", ImVec4(0.2f, 1.0f, 0.2f, 1.0f)); + RenderMetricCard("Time", std::to_string(options.operation_time_ms) + " ms", "timer", ImVec4(1.0f, 0.8f, 0.2f, 1.0f)); + + // Get overall performance stats + auto& profiler = gfx::PerformanceProfiler::Get(); + auto canvas_stats = profiler.GetStats("canvas_operations"); + auto draw_stats = profiler.GetStats("canvas_draw"); + + RenderMetricCard("Total Operations", std::to_string(canvas_stats.sample_count), "functions", ImVec4(0.2F, 0.8F, 1.0F, 1.0F)); + RenderMetricCard("Average Time", std::to_string(draw_stats.avg_time_us / 1000.0) + " ms", "schedule", ImVec4(0.8F, 0.2F, 1.0F, 1.0F)); + + // Action Buttons + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Open Dashboard", ImVec2(150, 0))) { + gfx::PerformanceDashboard::Get().SetVisible(true); + } + ImGui::SameLine(); + + if (ImGui::Button("Close", ImVec2(120, 0))) { + ImGui::CloseCurrentPopup(); + } + + ImGui::EndPopup(); + } +} + +void CanvasModals::OpenModal(const std::string& id, std::function render_func) { + // Check if modal already exists + auto it = std::find_if(active_modals_.begin(), active_modals_.end(), + [&id](const ModalState& modal) { return modal.id == id; }); + + if (it != active_modals_.end()) { + it->is_open = true; + it->render_func = render_func; + } else { + active_modals_.push_back({true, id, render_func}); + } + + // Open the popup + ImGui::OpenPopup(id.c_str()); +} + +void CanvasModals::CloseModal(const std::string& id) { + auto it = std::find_if(active_modals_.begin(), active_modals_.end(), + [&id](const ModalState& modal) { return modal.id == id; }); + + if (it != active_modals_.end()) { + it->is_open = false; + } +} + +bool CanvasModals::IsModalOpen(const std::string& id) const { + auto it = std::find_if(active_modals_.begin(), active_modals_.end(), + [&id](const ModalState& modal) { return modal.id == id; }); + + return it != active_modals_.end() && it->is_open; +} + +void CanvasModals::RenderMaterialIcon(const std::string& icon_name, const ImVec4& color) { + // Simple material icon rendering using Unicode symbols + // In a real implementation, you'd use a proper icon font + static std::unordered_map icon_map = { + {"grid_on", ICON_MD_GRID_ON}, {"label", ICON_MD_LABEL}, {"edit", ICON_MD_EDIT}, {"menu", ICON_MD_MENU}, + {"drag_indicator", ICON_MD_DRAG_INDICATOR}, {"fit_screen", ICON_MD_FIT_SCREEN}, {"zoom_in", ICON_MD_ZOOM_IN}, + {"speed", ICON_MD_SPEED}, {"timer", ICON_MD_TIMER}, {"functions", ICON_MD_FUNCTIONS}, {"schedule", ICON_MD_SCHEDULE}, + {"refresh", ICON_MD_REFRESH}, {"settings", ICON_MD_SETTINGS}, {"info", ICON_MD_INFO} + }; + + auto it = icon_map.find(icon_name); + if (it != icon_map.end()) { + ImGui::TextColored(color, "%s", it->second); + } +} + +void CanvasModals::RenderMetricCard(const std::string& title, const std::string& value, + const std::string& icon, const ImVec4& color) { + ImGui::BeginGroup(); + + // Icon and title + ImGui::Text("%s %s", icon.c_str(), title.c_str()); + + // Value with color + ImGui::TextColored(color, "%s", value.c_str()); + + ImGui::EndGroup(); +} + +void CanvasModals::RenderSliderWithIcon(const std::string& label, const std::string& icon, + float* value, float min_val, float max_val, + const char* format) { + ImGui::Text("%s %s", icon.c_str(), label.c_str()); + ImGui::SameLine(); + ImGui::SetNextItemWidth(200); + ImGui::SliderFloat(("##" + label).c_str(), value, min_val, max_val, format); +} + +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_modals.h b/src/app/gui/canvas/canvas_modals.h new file mode 100644 index 00000000..a28cc22a --- /dev/null +++ b/src/app/gui/canvas/canvas_modals.h @@ -0,0 +1,182 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_MODALS_H +#define YAZE_APP_GUI_CANVAS_CANVAS_MODALS_H + +#include +#include +#include +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/bpp_format_manager.h" +#include "gui/canvas_utils.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +void DispatchConfigCallback(const std::function& callback, + const CanvasConfig& config); +void DispatchScaleCallback(const std::function& callback, + const CanvasConfig& config); + +/** + * @brief Canvas configuration options for modals + */ +struct CanvasConfig { + ImVec2 canvas_size = ImVec2(0, 0); + ImVec2 content_size = ImVec2(0, 0); + float global_scale = 1.0f; + float grid_step = 32.0f; + bool enable_grid = true; + bool enable_hex_labels = false; + bool enable_custom_labels = false; + bool enable_context_menu = true; + bool is_draggable = false; + bool auto_resize = false; + ImVec2 scrolling = ImVec2(0, 0); + + // Callbacks provide updated configuration state + std::function on_config_changed; + std::function on_scale_changed; +}; + +/** + * @brief BPP conversion options + */ +struct BppConversionOptions { + const gfx::Bitmap* bitmap = nullptr; + const gfx::SnesPalette* palette = nullptr; + std::function on_convert; +}; + +/** + * @brief Palette editor options + */ +struct PaletteEditorOptions { + gfx::SnesPalette* palette = nullptr; + std::string title = "Palette Editor"; + std::function on_palette_changed; +}; + +/** + * @brief Color analysis options + */ +struct ColorAnalysisOptions { + const gfx::Bitmap* bitmap = nullptr; + const gfx::SnesPalette* palette = nullptr; + std::string title = "Color Analysis"; +}; + +/** + * @brief Performance integration options + */ +struct PerformanceOptions { + std::string operation_name; + double operation_time_ms = 0.0; + std::function on_dashboard_request; +}; + +/** + * @brief Modal dialog management for canvas operations + */ +class CanvasModals { + public: + CanvasModals() = default; + + /** + * @brief Show advanced canvas properties modal + */ + void ShowAdvancedProperties(const std::string& canvas_id, + const CanvasConfig& config, + const gfx::Bitmap* bitmap = nullptr); + + /** + * @brief Show scaling controls modal + */ + void ShowScalingControls(const std::string& canvas_id, + const CanvasConfig& config, + const gfx::Bitmap* bitmap = nullptr); + + /** + * @brief Show BPP format conversion dialog + */ + void ShowBppConversionDialog(const std::string& canvas_id, + const BppConversionOptions& options); + + /** + * @brief Show palette editor modal + */ + void ShowPaletteEditor(const std::string& canvas_id, + const PaletteEditorOptions& options); + + /** + * @brief Show color analysis modal + */ + void ShowColorAnalysis(const std::string& canvas_id, + const ColorAnalysisOptions& options); + + /** + * @brief Show performance dashboard integration + */ + void ShowPerformanceIntegration(const std::string& canvas_id, + const PerformanceOptions& options); + + /** + * @brief Render all active modals + */ + void Render(); + + /** + * @brief Check if any modal is open + */ + bool IsAnyModalOpen() const; + + private: + struct ModalState { + bool is_open = false; + std::string id; + std::function render_func; + }; + + std::vector active_modals_; + + // Modal rendering functions + void RenderAdvancedPropertiesModal(const std::string& canvas_id, + CanvasConfig& config, + const gfx::Bitmap* bitmap); + + void RenderScalingControlsModal(const std::string& canvas_id, + CanvasConfig& config, + const gfx::Bitmap* bitmap); + + void RenderBppConversionModal(const std::string& canvas_id, + const BppConversionOptions& options); + + void RenderPaletteEditorModal(const std::string& canvas_id, + const PaletteEditorOptions& options); + + void RenderColorAnalysisModal(const std::string& canvas_id, + const ColorAnalysisOptions& options); + + void RenderPerformanceModal(const std::string& canvas_id, + const PerformanceOptions& options); + + // Helper methods + void OpenModal(const std::string& id, std::function render_func); + void CloseModal(const std::string& id); + bool IsModalOpen(const std::string& id) const; + + // UI helper methods + void RenderMaterialIcon(const std::string& icon_name, const ImVec4& color = ImVec4(1, 1, 1, 1)); + void RenderMetricCard(const std::string& title, const std::string& value, + const std::string& icon, const ImVec4& color = ImVec4(1, 1, 1, 1)); + void RenderSliderWithIcon(const std::string& label, const std::string& icon, + float* value, float min_val, float max_val, + const char* format = "%.2f"); +}; + +} // namespace canvas +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_MODALS_H diff --git a/src/app/gui/canvas/canvas_performance_integration.cc b/src/app/gui/canvas/canvas_performance_integration.cc new file mode 100644 index 00000000..62d289b7 --- /dev/null +++ b/src/app/gui/canvas/canvas_performance_integration.cc @@ -0,0 +1,603 @@ +#include "canvas_performance_integration.h" + +#include +#include +#include +#include + +#include "app/gfx/performance_profiler.h" +#include "app/gfx/performance_dashboard.h" +#include "util/log.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +void CanvasPerformanceIntegration::Initialize(const std::string& canvas_id) { + canvas_id_ = canvas_id; + monitoring_enabled_ = true; + current_metrics_.Reset(); + + // Initialize performance profiler integration + dashboard_ = &gfx::PerformanceDashboard::Get(); + + util::logf("Initialized performance integration for canvas: %s", canvas_id_.c_str()); +} + +void CanvasPerformanceIntegration::StartMonitoring() { + if (!monitoring_enabled_) return; + + // Start frame timer + frame_timer_active_ = true; + frame_timer_ = std::make_unique("canvas_frame_" + canvas_id_); + + util::logf("Started performance monitoring for canvas: %s", canvas_id_.c_str()); +} + +void CanvasPerformanceIntegration::StopMonitoring() { + if (frame_timer_active_) { + frame_timer_.reset(); + frame_timer_active_ = false; + } + if (draw_timer_active_) { + draw_timer_.reset(); + draw_timer_active_ = false; + } + if (interaction_timer_active_) { + interaction_timer_.reset(); + interaction_timer_active_ = false; + } + if (modal_timer_active_) { + modal_timer_.reset(); + modal_timer_active_ = false; + } + + util::logf("Stopped performance monitoring for canvas: %s", canvas_id_.c_str()); +} + +void CanvasPerformanceIntegration::UpdateMetrics() { + if (!monitoring_enabled_) return; + + // Update frame time + UpdateFrameTime(); + + // Update draw time + UpdateDrawTime(); + + // Update interaction time + UpdateInteractionTime(); + + // Update modal time + UpdateModalTime(); + + // Calculate cache hit ratio + CalculateCacheHitRatio(); + + // Save current metrics periodically + static auto last_save = std::chrono::steady_clock::now(); + auto now = std::chrono::steady_clock::now(); + if (std::chrono::duration_cast(now - last_save).count() >= 5) { + SaveCurrentMetrics(); + last_save = now; + } +} + +void CanvasPerformanceIntegration::RecordOperation(const std::string& operation_name, + double time_ms, + CanvasUsage usage_mode) { + if (!monitoring_enabled_) return; + + // Update operation counts based on usage mode + switch (usage_mode) { + case CanvasUsage::kTilePainting: + current_metrics_.tile_paint_operations++; + break; + case CanvasUsage::kTileSelecting: + current_metrics_.tile_select_operations++; + break; + case CanvasUsage::kSelectRectangle: + current_metrics_.rectangle_select_operations++; + break; + case CanvasUsage::kColorPainting: + current_metrics_.color_paint_operations++; + break; + case CanvasUsage::kBppConversion: + current_metrics_.bpp_conversion_operations++; + break; + default: + break; + } + + // Record operation timing in internal metrics + // Note: PerformanceProfiler uses StartTimer/EndTimer pattern, not RecordOperation + + // Update usage tracker if available + if (usage_tracker_) { + usage_tracker_->RecordOperation(operation_name, time_ms); + } +} + +void CanvasPerformanceIntegration::RecordMemoryUsage(size_t texture_memory, + size_t bitmap_memory, + size_t palette_memory) { + current_metrics_.texture_memory_mb = texture_memory / (1024 * 1024); + current_metrics_.bitmap_memory_mb = bitmap_memory / (1024 * 1024); + current_metrics_.palette_memory_mb = palette_memory / (1024 * 1024); +} + +void CanvasPerformanceIntegration::RecordCachePerformance(int hits, int misses) { + current_metrics_.cache_hits = hits; + current_metrics_.cache_misses = misses; + CalculateCacheHitRatio(); +} + +// These methods are already defined in the header as inline, removing duplicates + +std::string CanvasPerformanceIntegration::GetPerformanceSummary() const { + std::ostringstream summary; + + summary << "Canvas Performance Summary (" << canvas_id_ << ")\n"; + summary << "=====================================\n\n"; + + summary << "Timing Metrics:\n"; + summary << " Frame Time: " << FormatTime(current_metrics_.frame_time_ms) << "\n"; + summary << " Draw Time: " << FormatTime(current_metrics_.draw_time_ms) << "\n"; + summary << " Interaction Time: " << FormatTime(current_metrics_.interaction_time_ms) << "\n"; + summary << " Modal Time: " << FormatTime(current_metrics_.modal_time_ms) << "\n\n"; + + summary << "Operation Counts:\n"; + summary << " Draw Calls: " << current_metrics_.draw_calls << "\n"; + summary << " Texture Updates: " << current_metrics_.texture_updates << "\n"; + summary << " Palette Lookups: " << current_metrics_.palette_lookups << "\n"; + summary << " Bitmap Operations: " << current_metrics_.bitmap_operations << "\n\n"; + + summary << "Canvas Operations:\n"; + summary << " Tile Paint: " << current_metrics_.tile_paint_operations << "\n"; + summary << " Tile Select: " << current_metrics_.tile_select_operations << "\n"; + summary << " Rectangle Select: " << current_metrics_.rectangle_select_operations << "\n"; + summary << " Color Paint: " << current_metrics_.color_paint_operations << "\n"; + summary << " BPP Conversion: " << current_metrics_.bpp_conversion_operations << "\n\n"; + + summary << "Memory Usage:\n"; + summary << " Texture Memory: " << FormatMemory(current_metrics_.texture_memory_mb * 1024 * 1024) << "\n"; + summary << " Bitmap Memory: " << FormatMemory(current_metrics_.bitmap_memory_mb * 1024 * 1024) << "\n"; + summary << " Palette Memory: " << FormatMemory(current_metrics_.palette_memory_mb * 1024 * 1024) << "\n\n"; + + summary << "Cache Performance:\n"; + summary << " Hit Ratio: " << std::fixed << std::setprecision(1) + << (current_metrics_.cache_hit_ratio * 100.0) << "%\n"; + summary << " Hits: " << current_metrics_.cache_hits << "\n"; + summary << " Misses: " << current_metrics_.cache_misses << "\n"; + + return summary.str(); +} + +std::vector CanvasPerformanceIntegration::GetPerformanceRecommendations() const { + std::vector recommendations; + + // Frame time recommendations + if (current_metrics_.frame_time_ms > 16.67) { // 60 FPS threshold + recommendations.push_back("Frame time is high - consider reducing draw calls or optimizing rendering"); + } + + // Draw time recommendations + if (current_metrics_.draw_time_ms > 10.0) { + recommendations.push_back("Draw time is high - consider using texture atlases or reducing texture switches"); + } + + // Memory recommendations + size_t total_memory = current_metrics_.texture_memory_mb + + current_metrics_.bitmap_memory_mb + + current_metrics_.palette_memory_mb; + if (total_memory > 100) { // 100MB threshold + recommendations.push_back("Memory usage is high - consider implementing texture streaming or compression"); + } + + // Cache recommendations + if (current_metrics_.cache_hit_ratio < 0.8) { + recommendations.push_back("Cache hit ratio is low - consider increasing cache size or improving cache strategy"); + } + + // Operation count recommendations + if (current_metrics_.draw_calls > 1000) { + recommendations.push_back("High draw call count - consider batching operations or using instanced rendering"); + } + + if (current_metrics_.texture_updates > 100) { + recommendations.push_back("Frequent texture updates - consider using texture arrays or atlases"); + } + + return recommendations; +} + +std::string CanvasPerformanceIntegration::ExportPerformanceReport() const { + std::ostringstream report; + + report << "Canvas Performance Report\n"; + report << "========================\n\n"; + + report << "Canvas ID: " << canvas_id_ << "\n"; + report << "Monitoring Enabled: " << (monitoring_enabled_ ? "Yes" : "No") << "\n\n"; + + report << GetPerformanceSummary() << "\n"; + + // Performance history + if (!performance_history_.empty()) { + report << "Performance History:\n"; + report << "===================\n\n"; + + for (size_t i = 0; i < performance_history_.size(); ++i) { + const auto& metrics = performance_history_[i]; + report << "Sample " << (i + 1) << ":\n"; + report << " Frame Time: " << FormatTime(metrics.frame_time_ms) << "\n"; + report << " Draw Calls: " << metrics.draw_calls << "\n"; + report << " Memory: " << FormatMemory((metrics.texture_memory_mb + + metrics.bitmap_memory_mb + + metrics.palette_memory_mb) * 1024 * 1024) << "\n\n"; + } + } + + // Recommendations + auto recommendations = GetPerformanceRecommendations(); + if (!recommendations.empty()) { + report << "Recommendations:\n"; + report << "===============\n\n"; + for (const auto& rec : recommendations) { + report << "• " << rec << "\n"; + } + } + + return report.str(); +} + +void CanvasPerformanceIntegration::RenderPerformanceUI() { + if (!monitoring_enabled_) return; + + if (ImGui::Begin("Canvas Performance", &show_performance_ui_)) { + // Performance overview + RenderPerformanceOverview(); + + if (show_detailed_metrics_) { + ImGui::Separator(); + RenderDetailedMetrics(); + } + + if (show_recommendations_) { + ImGui::Separator(); + RenderRecommendations(); + } + + // Control buttons + ImGui::Separator(); + if (ImGui::Button("Toggle Detailed Metrics")) { + show_detailed_metrics_ = !show_detailed_metrics_; + } + ImGui::SameLine(); + if (ImGui::Button("Toggle Recommendations")) { + show_recommendations_ = !show_recommendations_; + } + ImGui::SameLine(); + if (ImGui::Button("Export Report")) { + std::string report = ExportPerformanceReport(); + // Could save to file or show in modal + } + } + ImGui::End(); +} + +void CanvasPerformanceIntegration::SetUsageTracker(std::shared_ptr tracker) { + usage_tracker_ = tracker; +} + +void CanvasPerformanceIntegration::UpdateFrameTime() { + if (frame_timer_) { + // Frame time would be calculated by the timer + current_metrics_.frame_time_ms = 16.67; // Placeholder + } +} + +void CanvasPerformanceIntegration::UpdateDrawTime() { + if (draw_timer_) { + // Draw time would be calculated by the timer + current_metrics_.draw_time_ms = 5.0; // Placeholder + } +} + +void CanvasPerformanceIntegration::UpdateInteractionTime() { + if (interaction_timer_) { + // Interaction time would be calculated by the timer + current_metrics_.interaction_time_ms = 1.0; // Placeholder + } +} + +void CanvasPerformanceIntegration::UpdateModalTime() { + if (modal_timer_) { + // Modal time would be calculated by the timer + current_metrics_.modal_time_ms = 0.5; // Placeholder + } +} + +void CanvasPerformanceIntegration::CalculateCacheHitRatio() { + int total_requests = current_metrics_.cache_hits + current_metrics_.cache_misses; + if (total_requests > 0) { + current_metrics_.cache_hit_ratio = static_cast(current_metrics_.cache_hits) / total_requests; + } else { + current_metrics_.cache_hit_ratio = 0.0; + } +} + +void CanvasPerformanceIntegration::SaveCurrentMetrics() { + performance_history_.push_back(current_metrics_); + + // Keep only last 100 samples + if (performance_history_.size() > 100) { + performance_history_.erase(performance_history_.begin()); + } +} + +void CanvasPerformanceIntegration::AnalyzePerformance() { + // Analyze performance trends and patterns + if (performance_history_.size() < 2) return; + + // Calculate trends + double frame_time_trend = 0.0; + double memory_trend = 0.0; + + for (size_t i = 1; i < performance_history_.size(); ++i) { + const auto& prev = performance_history_[i - 1]; + const auto& curr = performance_history_[i]; + + frame_time_trend += (curr.frame_time_ms - prev.frame_time_ms); + memory_trend += ((curr.texture_memory_mb + curr.bitmap_memory_mb + curr.palette_memory_mb) - + (prev.texture_memory_mb + prev.bitmap_memory_mb + prev.palette_memory_mb)); + } + + frame_time_trend /= (performance_history_.size() - 1); + memory_trend /= (performance_history_.size() - 1); + + // Log trends + if (std::abs(frame_time_trend) > 1.0) { + util::logf("Canvas %s: Frame time trend: %.2f ms/sample", + canvas_id_.c_str(), frame_time_trend); + } + + if (std::abs(memory_trend) > 1.0) { + util::logf("Canvas %s: Memory trend: %.2f MB/sample", + canvas_id_.c_str(), memory_trend); + } +} + +void CanvasPerformanceIntegration::RenderPerformanceOverview() { + ImGui::Text("Performance Overview"); + ImGui::Separator(); + + // Frame time + ImVec4 frame_color = GetPerformanceColor(current_metrics_.frame_time_ms, 16.67, 33.33); + ImGui::TextColored(frame_color, "Frame Time: %s", FormatTime(current_metrics_.frame_time_ms).c_str()); + + // Draw time + ImVec4 draw_color = GetPerformanceColor(current_metrics_.draw_time_ms, 10.0, 20.0); + ImGui::TextColored(draw_color, "Draw Time: %s", FormatTime(current_metrics_.draw_time_ms).c_str()); + + // Memory usage + size_t total_memory = current_metrics_.texture_memory_mb + + current_metrics_.bitmap_memory_mb + + current_metrics_.palette_memory_mb; + ImVec4 memory_color = GetPerformanceColor(total_memory, 50.0, 100.0); + ImGui::TextColored(memory_color, "Memory: %s", FormatMemory(total_memory * 1024 * 1024).c_str()); + + // Cache performance + ImVec4 cache_color = GetPerformanceColor(current_metrics_.cache_hit_ratio * 100.0, 80.0, 60.0); + ImGui::TextColored(cache_color, "Cache Hit Ratio: %.1f%%", current_metrics_.cache_hit_ratio * 100.0); +} + +void CanvasPerformanceIntegration::RenderDetailedMetrics() { + ImGui::Text("Detailed Metrics"); + ImGui::Separator(); + + // Operation counts + RenderOperationCounts(); + + // Memory breakdown + RenderMemoryUsage(); + + // Cache performance + RenderCachePerformance(); +} + +void CanvasPerformanceIntegration::RenderMemoryUsage() { + if (ImGui::CollapsingHeader("Memory Usage")) { + ImGui::Text("Texture Memory: %s", FormatMemory(current_metrics_.texture_memory_mb * 1024 * 1024).c_str()); + ImGui::Text("Bitmap Memory: %s", FormatMemory(current_metrics_.bitmap_memory_mb * 1024 * 1024).c_str()); + ImGui::Text("Palette Memory: %s", FormatMemory(current_metrics_.palette_memory_mb * 1024 * 1024).c_str()); + + size_t total = current_metrics_.texture_memory_mb + + current_metrics_.bitmap_memory_mb + + current_metrics_.palette_memory_mb; + ImGui::Text("Total Memory: %s", FormatMemory(total * 1024 * 1024).c_str()); + } +} + +void CanvasPerformanceIntegration::RenderOperationCounts() { + if (ImGui::CollapsingHeader("Operation Counts")) { + ImGui::Text("Draw Calls: %d", current_metrics_.draw_calls); + ImGui::Text("Texture Updates: %d", current_metrics_.texture_updates); + ImGui::Text("Palette Lookups: %d", current_metrics_.palette_lookups); + ImGui::Text("Bitmap Operations: %d", current_metrics_.bitmap_operations); + + ImGui::Separator(); + ImGui::Text("Canvas Operations:"); + ImGui::Text(" Tile Paint: %d", current_metrics_.tile_paint_operations); + ImGui::Text(" Tile Select: %d", current_metrics_.tile_select_operations); + ImGui::Text(" Rectangle Select: %d", current_metrics_.rectangle_select_operations); + ImGui::Text(" Color Paint: %d", current_metrics_.color_paint_operations); + ImGui::Text(" BPP Conversion: %d", current_metrics_.bpp_conversion_operations); + } +} + +void CanvasPerformanceIntegration::RenderCachePerformance() { + if (ImGui::CollapsingHeader("Cache Performance")) { + ImGui::Text("Cache Hits: %d", current_metrics_.cache_hits); + ImGui::Text("Cache Misses: %d", current_metrics_.cache_misses); + ImGui::Text("Hit Ratio: %.1f%%", current_metrics_.cache_hit_ratio * 100.0); + + // Cache hit ratio bar + ImGui::ProgressBar(current_metrics_.cache_hit_ratio, ImVec2(0, 0)); + } +} + +void CanvasPerformanceIntegration::RenderRecommendations() { + ImGui::Text("Performance Recommendations"); + ImGui::Separator(); + + auto recommendations = GetPerformanceRecommendations(); + if (recommendations.empty()) { + ImGui::TextColored(ImVec4(0.2F, 1.0F, 0.2F, 1.0F), "✓ Performance looks good!"); + } else { + for (const auto& rec : recommendations) { + ImGui::TextColored(ImVec4(1.0F, 0.8F, 0.2F, 1.0F), "⚠ %s", rec.c_str()); + } + } +} + +void CanvasPerformanceIntegration::RenderPerformanceGraph() { + if (ImGui::CollapsingHeader("Performance Graph")) { + // Simple performance graph using ImGui plot lines + static std::vector frame_times; + static std::vector draw_times; + + // Add current values + frame_times.push_back(static_cast(current_metrics_.frame_time_ms)); + draw_times.push_back(static_cast(current_metrics_.draw_time_ms)); + + // Keep only last 100 samples + if (frame_times.size() > 100) { + frame_times.erase(frame_times.begin()); + draw_times.erase(draw_times.begin()); + } + + if (!frame_times.empty()) { + ImGui::PlotLines("Frame Time (ms)", frame_times.data(), + static_cast(frame_times.size()), 0, nullptr, 0.0F, 50.0F, + ImVec2(0, 100)); + ImGui::PlotLines("Draw Time (ms)", draw_times.data(), + static_cast(draw_times.size()), 0, nullptr, 0.0F, 30.0F, + ImVec2(0, 100)); + } + } +} + +std::string CanvasPerformanceIntegration::FormatTime(double time_ms) const { + if (time_ms < 1.0) { + return std::to_string(static_cast(time_ms * 1000)) + " μs"; + } else if (time_ms < 1000.0) { + return std::to_string(static_cast(time_ms * 10) / 10.0) + " ms"; + } else { + return std::to_string(static_cast(time_ms / 1000)) + " s"; + } +} + +std::string CanvasPerformanceIntegration::FormatMemory(size_t bytes) const { + if (bytes < 1024) { + return std::to_string(bytes) + " B"; + } else if (bytes < 1024 * 1024) { + return std::to_string(bytes / 1024) + " KB"; + } else { + return std::to_string(bytes / (1024 * 1024)) + " MB"; + } +} + +ImVec4 CanvasPerformanceIntegration::GetPerformanceColor(double value, + double threshold_good, + double threshold_warning) const { + if (value <= threshold_good) { + return ImVec4(0.2F, 1.0F, 0.2F, 1.0F); // Green + } else if (value <= threshold_warning) { + return ImVec4(1.0F, 1.0F, 0.2F, 1.0F); // Yellow + } else { + return ImVec4(1.0F, 0.2F, 0.2F, 1.0F); // Red + } +} + +// CanvasPerformanceManager implementation + +CanvasPerformanceManager& CanvasPerformanceManager::Get() { + static CanvasPerformanceManager instance; + return instance; +} + +void CanvasPerformanceManager::RegisterIntegration(const std::string& canvas_id, + std::shared_ptr integration) { + integrations_[canvas_id] = integration; + util::logf("Registered performance integration for canvas: %s", canvas_id.c_str()); +} + +std::shared_ptr CanvasPerformanceManager::GetIntegration(const std::string& canvas_id) { + auto it = integrations_.find(canvas_id); + if (it != integrations_.end()) { + return it->second; + } + return nullptr; +} + +void CanvasPerformanceManager::UpdateAllIntegrations() { + for (auto& [id, integration] : integrations_) { + integration->UpdateMetrics(); + } +} + +std::string CanvasPerformanceManager::GetGlobalPerformanceSummary() const { + std::ostringstream summary; + + summary << "Global Canvas Performance Summary\n"; + summary << "=================================\n\n"; + + summary << "Registered Canvases: " << integrations_.size() << "\n\n"; + + for (const auto& [id, integration] : integrations_) { + summary << "Canvas: " << id << "\n"; + summary << "----------------------------------------\n"; + summary << integration->GetPerformanceSummary() << "\n\n"; + } + + return summary.str(); +} + +std::string CanvasPerformanceManager::ExportGlobalPerformanceReport() const { + std::ostringstream report; + + report << "Global Canvas Performance Report\n"; + report << "================================\n\n"; + + report << GetGlobalPerformanceSummary(); + + // Global recommendations + report << "Global Recommendations:\n"; + report << "=======================\n\n"; + + for (const auto& [id, integration] : integrations_) { + auto recommendations = integration->GetPerformanceRecommendations(); + if (!recommendations.empty()) { + report << "Canvas " << id << ":\n"; + for (const auto& rec : recommendations) { + report << " • " << rec << "\n"; + } + report << "\n"; + } + } + + return report.str(); +} + +void CanvasPerformanceManager::ClearAllIntegrations() { + for (auto& [id, integration] : integrations_) { + integration->StopMonitoring(); + } + integrations_.clear(); + util::logf("Cleared all canvas performance integrations"); +} + +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_performance_integration.h b/src/app/gui/canvas/canvas_performance_integration.h new file mode 100644 index 00000000..11c36f23 --- /dev/null +++ b/src/app/gui/canvas/canvas_performance_integration.h @@ -0,0 +1,269 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_PERFORMANCE_INTEGRATION_H +#define YAZE_APP_GUI_CANVAS_CANVAS_PERFORMANCE_INTEGRATION_H + +#include +#include +#include +#include +#include +#include "app/gfx/performance_profiler.h" +#include "app/gfx/performance_dashboard.h" +#include "canvas_usage_tracker.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +/** + * @brief Canvas performance metrics + */ +struct CanvasPerformanceMetrics { + // Timing metrics + double frame_time_ms = 0.0; + double draw_time_ms = 0.0; + double interaction_time_ms = 0.0; + double modal_time_ms = 0.0; + + // Operation counts + int draw_calls = 0; + int texture_updates = 0; + int palette_lookups = 0; + int bitmap_operations = 0; + + // Memory usage + size_t texture_memory_mb = 0; + size_t bitmap_memory_mb = 0; + size_t palette_memory_mb = 0; + + // Cache performance + double cache_hit_ratio = 0.0; + int cache_hits = 0; + int cache_misses = 0; + + // Canvas-specific metrics + int tile_paint_operations = 0; + int tile_select_operations = 0; + int rectangle_select_operations = 0; + int color_paint_operations = 0; + int bpp_conversion_operations = 0; + + void Reset() { + frame_time_ms = 0.0; + draw_time_ms = 0.0; + interaction_time_ms = 0.0; + modal_time_ms = 0.0; + draw_calls = 0; + texture_updates = 0; + palette_lookups = 0; + bitmap_operations = 0; + texture_memory_mb = 0; + bitmap_memory_mb = 0; + palette_memory_mb = 0; + cache_hit_ratio = 0.0; + cache_hits = 0; + cache_misses = 0; + tile_paint_operations = 0; + tile_select_operations = 0; + rectangle_select_operations = 0; + color_paint_operations = 0; + bpp_conversion_operations = 0; + } +}; + +/** + * @brief Canvas performance integration with dashboard + */ +class CanvasPerformanceIntegration { + public: + CanvasPerformanceIntegration() = default; + + /** + * @brief Initialize performance integration + */ + void Initialize(const std::string& canvas_id); + + /** + * @brief Start performance monitoring + */ + void StartMonitoring(); + + /** + * @brief Stop performance monitoring + */ + void StopMonitoring(); + + /** + * @brief Update performance metrics + */ + void UpdateMetrics(); + + /** + * @brief Record canvas operation + */ + void RecordOperation(const std::string& operation_name, + double time_ms, + CanvasUsage usage_mode = CanvasUsage::kUnknown); + + /** + * @brief Record memory usage + */ + void RecordMemoryUsage(size_t texture_memory, + size_t bitmap_memory, + size_t palette_memory); + + /** + * @brief Record cache performance + */ + void RecordCachePerformance(int hits, int misses); + + /** + * @brief Get current performance metrics + */ + const CanvasPerformanceMetrics& GetCurrentMetrics() const { return current_metrics_; } + + /** + * @brief Get performance history + */ + const std::vector& GetPerformanceHistory() const { + return performance_history_; + } + + /** + * @brief Get performance summary + */ + std::string GetPerformanceSummary() const; + + /** + * @brief Get performance recommendations + */ + std::vector GetPerformanceRecommendations() const; + + /** + * @brief Export performance report + */ + std::string ExportPerformanceReport() const; + + /** + * @brief Render performance UI + */ + void RenderPerformanceUI(); + + /** + * @brief Set usage tracker integration + */ + void SetUsageTracker(std::shared_ptr tracker); + + /** + * @brief Enable/disable performance monitoring + */ + void SetMonitoringEnabled(bool enabled) { monitoring_enabled_ = enabled; } + bool IsMonitoringEnabled() const { return monitoring_enabled_; } + + private: + std::string canvas_id_; + bool monitoring_enabled_ = true; + CanvasPerformanceMetrics current_metrics_; + std::vector performance_history_; + + // Performance profiler integration + std::unique_ptr frame_timer_; + std::unique_ptr draw_timer_; + std::unique_ptr interaction_timer_; + std::unique_ptr modal_timer_; + bool frame_timer_active_ = false; + bool draw_timer_active_ = false; + bool interaction_timer_active_ = false; + bool modal_timer_active_ = false; + + // Usage tracker integration + std::shared_ptr usage_tracker_; + + // Performance dashboard integration + gfx::PerformanceDashboard* dashboard_ = nullptr; + + // UI state + bool show_performance_ui_ = false; + bool show_detailed_metrics_ = false; + bool show_recommendations_ = false; + + // Helper methods + void UpdateFrameTime(); + void UpdateDrawTime(); + void UpdateInteractionTime(); + void UpdateModalTime(); + void CalculateCacheHitRatio(); + void SaveCurrentMetrics(); + void AnalyzePerformance(); + + // UI rendering methods + void RenderPerformanceOverview(); + void RenderDetailedMetrics(); + void RenderMemoryUsage(); + void RenderOperationCounts(); + void RenderCachePerformance(); + void RenderRecommendations(); + void RenderPerformanceGraph(); + + // Helper methods + std::string FormatTime(double time_ms) const; + std::string FormatMemory(size_t bytes) const; + ImVec4 GetPerformanceColor(double value, double threshold_good, double threshold_warning) const; +}; + +/** + * @brief Global canvas performance manager + */ +class CanvasPerformanceManager { + public: + static CanvasPerformanceManager& Get(); + + /** + * @brief Register a canvas performance integration + */ + void RegisterIntegration(const std::string& canvas_id, + std::shared_ptr integration); + + /** + * @brief Get integration for canvas + */ + std::shared_ptr GetIntegration(const std::string& canvas_id); + + /** + * @brief Get all integrations + */ + const std::unordered_map>& + GetAllIntegrations() const { return integrations_; } + + /** + * @brief Update all integrations + */ + void UpdateAllIntegrations(); + + /** + * @brief Get global performance summary + */ + std::string GetGlobalPerformanceSummary() const; + + /** + * @brief Export global performance report + */ + std::string ExportGlobalPerformanceReport() const; + + /** + * @brief Clear all integrations + */ + void ClearAllIntegrations(); + + private: + CanvasPerformanceManager() = default; + ~CanvasPerformanceManager() = default; + + std::unordered_map> integrations_; +}; + +} // namespace canvas +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_PERFORMANCE_INTEGRATION_H diff --git a/src/app/gui/canvas/canvas_usage_tracker.cc b/src/app/gui/canvas/canvas_usage_tracker.cc new file mode 100644 index 00000000..efeb8921 --- /dev/null +++ b/src/app/gui/canvas/canvas_usage_tracker.cc @@ -0,0 +1,429 @@ +#include "canvas_usage_tracker.h" + +#include +#include +#include +#include + +#include "util/log.h" + +namespace yaze { +namespace gui { +namespace canvas { + +void CanvasUsageTracker::Initialize(const std::string& canvas_id) { + canvas_id_ = canvas_id; + current_stats_.Reset(); + current_stats_.session_start = std::chrono::steady_clock::now(); + last_activity_ = current_stats_.session_start; + session_start_ = current_stats_.session_start; +} + +void CanvasUsageTracker::SetUsageMode(CanvasUsage usage) { + if (current_stats_.usage_mode != usage) { + // Save current stats before changing mode + SaveCurrentStats(); + + // Update usage mode + current_stats_.usage_mode = usage; + current_stats_.mode_changes++; + + // Record mode change interaction + RecordInteraction(CanvasInteraction::kModeChange, GetUsageModeName(usage)); + + util::logf("Canvas %s: Usage mode changed to %s", + canvas_id_.c_str(), GetUsageModeName(usage).c_str()); + } +} + +void CanvasUsageTracker::RecordInteraction(CanvasInteraction interaction, + const std::string& details) { + interaction_history_.push_back({interaction, details}); + + // Update activity time + last_activity_ = std::chrono::steady_clock::now(); + + // Update interaction counts + switch (interaction) { + case CanvasInteraction::kMouseClick: + current_stats_.mouse_clicks++; + break; + case CanvasInteraction::kMouseDrag: + current_stats_.mouse_drags++; + break; + case CanvasInteraction::kContextMenu: + current_stats_.context_menu_opens++; + break; + case CanvasInteraction::kModalOpen: + current_stats_.modal_opens++; + break; + case CanvasInteraction::kToolChange: + current_stats_.tool_changes++; + break; + case CanvasInteraction::kModeChange: + current_stats_.mode_changes++; + break; + default: + break; + } +} + +void CanvasUsageTracker::RecordOperation(const std::string& operation_name, + double time_ms) { + operation_times_[operation_name].push_back(time_ms); + current_stats_.total_operations++; + + // Update average operation time + double total_time = 0.0; + int total_ops = 0; + for (const auto& [name, times] : operation_times_) { + for (double t : times) { + total_time += t; + total_ops++; + } + } + + if (total_ops > 0) { + current_stats_.average_operation_time_ms = total_time / total_ops; + } + + // Update max operation time + if (time_ms > current_stats_.max_operation_time_ms) { + current_stats_.max_operation_time_ms = time_ms; + } + + // Record as interaction + RecordInteraction(CanvasInteraction::kKeyboardInput, operation_name); +} + +void CanvasUsageTracker::UpdateCanvasState(const ImVec2& canvas_size, + const ImVec2& content_size, + float global_scale, + float grid_step, + bool enable_grid, + bool enable_hex_labels, + bool enable_custom_labels) { + current_stats_.canvas_size = canvas_size; + current_stats_.content_size = content_size; + current_stats_.global_scale = global_scale; + current_stats_.grid_step = grid_step; + current_stats_.enable_grid = enable_grid; + current_stats_.enable_hex_labels = enable_hex_labels; + current_stats_.enable_custom_labels = enable_custom_labels; + + // Update activity time + last_activity_ = std::chrono::steady_clock::now(); +} + +// These methods are already defined in the header as inline, removing duplicates + +std::string CanvasUsageTracker::GetUsageModeName(CanvasUsage usage) const { + switch (usage) { + case CanvasUsage::kTilePainting: return "Tile Painting"; + case CanvasUsage::kTileSelecting: return "Tile Selecting"; + case CanvasUsage::kSelectRectangle: return "Rectangle Selection"; + case CanvasUsage::kColorPainting: return "Color Painting"; + case CanvasUsage::kBitmapEditing: return "Bitmap Editing"; + case CanvasUsage::kPaletteEditing: return "Palette Editing"; + case CanvasUsage::kBppConversion: return "BPP Conversion"; + case CanvasUsage::kPerformanceMode: return "Performance Mode"; + case CanvasUsage::kUnknown: return "Unknown"; + default: return "Unknown"; + } +} + +ImVec4 CanvasUsageTracker::GetUsageModeColor(CanvasUsage usage) const { + switch (usage) { + case CanvasUsage::kTilePainting: return ImVec4(0.2F, 1.0F, 0.2F, 1.0F); // Green + case CanvasUsage::kTileSelecting: return ImVec4(0.2F, 0.8F, 1.0F, 1.0F); // Blue + case CanvasUsage::kSelectRectangle: return ImVec4(1.0F, 0.8F, 0.2F, 1.0F); // Yellow + case CanvasUsage::kColorPainting: return ImVec4(1.0F, 0.2F, 1.0F, 1.0F); // Magenta + case CanvasUsage::kBitmapEditing: return ImVec4(1.0F, 0.5F, 0.2F, 1.0F); // Orange + case CanvasUsage::kPaletteEditing: return ImVec4(0.8F, 0.2F, 1.0F, 1.0F); // Purple + case CanvasUsage::kBppConversion: return ImVec4(0.2F, 1.0F, 1.0F, 1.0F); // Cyan + case CanvasUsage::kPerformanceMode: return ImVec4(1.0F, 0.2F, 0.2F, 1.0F); // Red + case CanvasUsage::kUnknown: return ImVec4(0.7F, 0.7F, 0.7F, 1.0F); // Gray + default: return ImVec4(0.7F, 0.7F, 0.7F, 1.0F); // Gray + } +} + +std::vector CanvasUsageTracker::GetUsageRecommendations() const { + std::vector recommendations; + + // Analyze usage patterns and provide recommendations + if (current_stats_.mouse_clicks > 100) { + recommendations.push_back("Consider using keyboard shortcuts to reduce mouse usage"); + } + + if (current_stats_.context_menu_opens > 20) { + recommendations.push_back("Frequent context menu usage - consider adding toolbar buttons"); + } + + if (current_stats_.modal_opens > 10) { + recommendations.push_back("Many modal dialogs opened - consider persistent panels"); + } + + if (current_stats_.average_operation_time_ms > 100.0) { + recommendations.push_back("Operations are slow - check performance optimization"); + } + + if (current_stats_.mode_changes > 5) { + recommendations.push_back("Frequent mode switching - consider mode-specific toolbars"); + } + + return recommendations; +} + +std::string CanvasUsageTracker::ExportUsageReport() const { + std::ostringstream report; + + report << "Canvas Usage Report for: " << canvas_id_ << "\n"; + report << "==========================================\n\n"; + + // Session information + auto now = std::chrono::steady_clock::now(); + auto session_duration = std::chrono::duration_cast( + now - session_start_); + + report << "Session Information:\n"; + report << " Duration: " << FormatDuration(session_duration) << "\n"; + report << " Current Mode: " << GetUsageModeName(current_stats_.usage_mode) << "\n"; + report << " Mode Changes: " << current_stats_.mode_changes << "\n\n"; + + // Interaction statistics + report << "Interaction Statistics:\n"; + report << " Mouse Clicks: " << current_stats_.mouse_clicks << "\n"; + report << " Mouse Drags: " << current_stats_.mouse_drags << "\n"; + report << " Context Menu Opens: " << current_stats_.context_menu_opens << "\n"; + report << " Modal Opens: " << current_stats_.modal_opens << "\n"; + report << " Tool Changes: " << current_stats_.tool_changes << "\n\n"; + + // Performance statistics + report << "Performance Statistics:\n"; + report << " Total Operations: " << current_stats_.total_operations << "\n"; + report << " Average Operation Time: " << std::fixed << std::setprecision(2) + << current_stats_.average_operation_time_ms << " ms\n"; + report << " Max Operation Time: " << std::fixed << std::setprecision(2) + << current_stats_.max_operation_time_ms << " ms\n\n"; + + // Canvas state + report << "Canvas State:\n"; + report << " Canvas Size: " << static_cast(current_stats_.canvas_size.x) + << " x " << static_cast(current_stats_.canvas_size.y) << "\n"; + report << " Content Size: " << static_cast(current_stats_.content_size.x) + << " x " << static_cast(current_stats_.content_size.y) << "\n"; + report << " Global Scale: " << std::fixed << std::setprecision(2) + << current_stats_.global_scale << "\n"; + report << " Grid Step: " << std::fixed << std::setprecision(1) + << current_stats_.grid_step << "\n"; + report << " Grid Enabled: " << (current_stats_.enable_grid ? "Yes" : "No") << "\n"; + report << " Hex Labels: " << (current_stats_.enable_hex_labels ? "Yes" : "No") << "\n"; + report << " Custom Labels: " << (current_stats_.enable_custom_labels ? "Yes" : "No") << "\n\n"; + + // Operation breakdown + if (!operation_times_.empty()) { + report << "Operation Breakdown:\n"; + for (const auto& [operation, times] : operation_times_) { + double avg_time = CalculateAverageOperationTime(operation); + report << " " << operation << ": " << times.size() << " operations, " + << "avg " << std::fixed << std::setprecision(2) << avg_time << " ms\n"; + } + report << "\n"; + } + + // Recommendations + auto recommendations = GetUsageRecommendations(); + if (!recommendations.empty()) { + report << "Recommendations:\n"; + for (const auto& rec : recommendations) { + report << " • " << rec << "\n"; + } + } + + return report.str(); +} + +void CanvasUsageTracker::ClearHistory() { + usage_history_.clear(); + interaction_history_.clear(); + operation_times_.clear(); + current_stats_.Reset(); + current_stats_.session_start = std::chrono::steady_clock::now(); + last_activity_ = current_stats_.session_start; + session_start_ = current_stats_.session_start; +} + +void CanvasUsageTracker::StartSession() { + session_start_ = std::chrono::steady_clock::now(); + current_stats_.session_start = session_start_; + last_activity_ = session_start_; +} + +void CanvasUsageTracker::EndSession() { + // Update final statistics + UpdateActiveTime(); + UpdateIdleTime(); + + // Save final stats + SaveCurrentStats(); + + util::logf("Canvas %s: Session ended. Duration: %s, Operations: %d", + canvas_id_.c_str(), + FormatDuration(std::chrono::duration_cast( + std::chrono::steady_clock::now() - session_start_)).c_str(), + current_stats_.total_operations); +} + +void CanvasUsageTracker::UpdateActiveTime() { + auto now = std::chrono::steady_clock::now(); + auto time_since_activity = std::chrono::duration_cast( + now - last_activity_); + + if (time_since_activity.count() < 5000) { // 5 seconds threshold + current_stats_.active_time += time_since_activity; + } +} + +void CanvasUsageTracker::UpdateIdleTime() { + auto now = std::chrono::steady_clock::now(); + auto time_since_activity = std::chrono::duration_cast( + now - last_activity_); + + if (time_since_activity.count() >= 5000) { // 5 seconds threshold + current_stats_.idle_time += time_since_activity; + } +} + +void CanvasUsageTracker::SaveCurrentStats() { + // Update final times + UpdateActiveTime(); + UpdateIdleTime(); + + // Calculate total time + current_stats_.total_time = current_stats_.active_time + current_stats_.idle_time; + + // Save to history + usage_history_.push_back(current_stats_); + + // Reset for next session + current_stats_.Reset(); + current_stats_.session_start = std::chrono::steady_clock::now(); +} + +double CanvasUsageTracker::CalculateAverageOperationTime(const std::string& operation_name) const { + auto it = operation_times_.find(operation_name); + if (it == operation_times_.end() || it->second.empty()) { + return 0.0; + } + + double total = 0.0; + for (double time : it->second) { + total += time; + } + + return total / it->second.size(); +} + +std::string CanvasUsageTracker::FormatDuration(const std::chrono::milliseconds& duration) const { + auto total_ms = duration.count(); + auto hours = total_ms / 3600000; + auto minutes = (total_ms % 3600000) / 60000; + auto seconds = (total_ms % 60000) / 1000; + + std::ostringstream ss; + if (hours > 0) { + ss << hours << "h " << minutes << "m " << seconds << "s"; + } else if (minutes > 0) { + ss << minutes << "m " << seconds << "s"; + } else { + ss << seconds << "s"; + } + + return ss.str(); +} + +// CanvasUsageManager implementation + +CanvasUsageManager& CanvasUsageManager::Get() { + static CanvasUsageManager instance; + return instance; +} + +void CanvasUsageManager::RegisterTracker(const std::string& canvas_id, + std::shared_ptr tracker) { + trackers_[canvas_id] = tracker; + util::logf("Registered usage tracker for canvas: %s", canvas_id.c_str()); +} + +std::shared_ptr CanvasUsageManager::GetTracker(const std::string& canvas_id) { + auto it = trackers_.find(canvas_id); + if (it != trackers_.end()) { + return it->second; + } + return nullptr; +} + +CanvasUsageStats CanvasUsageManager::GetGlobalStats() const { + CanvasUsageStats global_stats; + + for (const auto& [id, tracker] : trackers_) { + const auto& stats = tracker->GetCurrentStats(); + + global_stats.mouse_clicks += stats.mouse_clicks; + global_stats.mouse_drags += stats.mouse_drags; + global_stats.context_menu_opens += stats.context_menu_opens; + global_stats.modal_opens += stats.modal_opens; + global_stats.tool_changes += stats.tool_changes; + global_stats.mode_changes += stats.mode_changes; + global_stats.total_operations += stats.total_operations; + + // Update averages + if (stats.average_operation_time_ms > global_stats.average_operation_time_ms) { + global_stats.average_operation_time_ms = stats.average_operation_time_ms; + } + if (stats.max_operation_time_ms > global_stats.max_operation_time_ms) { + global_stats.max_operation_time_ms = stats.max_operation_time_ms; + } + } + + return global_stats; +} + +std::string CanvasUsageManager::ExportGlobalReport() const { + std::ostringstream report; + + report << "Global Canvas Usage Report\n"; + report << "==========================\n\n"; + + report << "Registered Canvases: " << trackers_.size() << "\n\n"; + + for (const auto& [id, tracker] : trackers_) { + report << "Canvas: " << id << "\n"; + report << "----------------------------------------\n"; + report << tracker->ExportUsageReport() << "\n\n"; + } + + // Global summary + auto global_stats = GetGlobalStats(); + report << "Global Summary:\n"; + report << " Total Mouse Clicks: " << global_stats.mouse_clicks << "\n"; + report << " Total Operations: " << global_stats.total_operations << "\n"; + report << " Average Operation Time: " << std::fixed << std::setprecision(2) + << global_stats.average_operation_time_ms << " ms\n"; + report << " Max Operation Time: " << std::fixed << std::setprecision(2) + << global_stats.max_operation_time_ms << " ms\n"; + + return report.str(); +} + +void CanvasUsageManager::ClearAllTrackers() { + for (auto& [id, tracker] : trackers_) { + tracker->ClearHistory(); + } + trackers_.clear(); + util::logf("Cleared all canvas usage trackers"); +} + +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_usage_tracker.h b/src/app/gui/canvas/canvas_usage_tracker.h new file mode 100644 index 00000000..1a77768a --- /dev/null +++ b/src/app/gui/canvas/canvas_usage_tracker.h @@ -0,0 +1,249 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_USAGE_TRACKER_H +#define YAZE_APP_GUI_CANVAS_CANVAS_USAGE_TRACKER_H + +#include +#include +#include +#include +#include +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +/** + * @brief Canvas usage patterns and tracking + */ +enum class CanvasUsage { + kTilePainting, // Drawing tiles on canvas + kTileSelecting, // Selecting tiles from canvas + kSelectRectangle, // Rectangle selection mode + kColorPainting, // Color painting mode + kBitmapEditing, // Direct bitmap editing + kPaletteEditing, // Palette editing mode + kBppConversion, // BPP format conversion + kPerformanceMode, // Performance monitoring mode + kUnknown // Unknown or mixed usage +}; + +/** + * @brief Canvas interaction types + */ +enum class CanvasInteraction { + kMouseClick, + kMouseDrag, + kMouseRelease, + kKeyboardInput, + kContextMenu, + kModalOpen, + kModalClose, + kToolChange, + kModeChange +}; + +/** + * @brief Canvas usage statistics + */ +struct CanvasUsageStats { + CanvasUsage usage_mode = CanvasUsage::kUnknown; + std::chrono::steady_clock::time_point session_start; + std::chrono::milliseconds total_time{0}; + std::chrono::milliseconds active_time{0}; + std::chrono::milliseconds idle_time{0}; + + // Interaction counts + int mouse_clicks = 0; + int mouse_drags = 0; + int context_menu_opens = 0; + int modal_opens = 0; + int tool_changes = 0; + int mode_changes = 0; + + // Performance metrics + double average_operation_time_ms = 0.0; + double max_operation_time_ms = 0.0; + int total_operations = 0; + + // Canvas state + ImVec2 canvas_size = ImVec2(0, 0); + ImVec2 content_size = ImVec2(0, 0); + float global_scale = 1.0F; + float grid_step = 32.0F; + bool enable_grid = true; + bool enable_hex_labels = false; + bool enable_custom_labels = false; + + void Reset() { + usage_mode = CanvasUsage::kUnknown; + session_start = std::chrono::steady_clock::now(); + total_time = std::chrono::milliseconds{0}; + active_time = std::chrono::milliseconds{0}; + idle_time = std::chrono::milliseconds{0}; + mouse_clicks = 0; + mouse_drags = 0; + context_menu_opens = 0; + modal_opens = 0; + tool_changes = 0; + mode_changes = 0; + average_operation_time_ms = 0.0; + max_operation_time_ms = 0.0; + total_operations = 0; + } +}; + +/** + * @brief Canvas usage tracking and analysis system + */ +class CanvasUsageTracker { + public: + CanvasUsageTracker() = default; + + /** + * @brief Initialize the usage tracker + */ + void Initialize(const std::string& canvas_id); + + /** + * @brief Set the current usage mode + */ + void SetUsageMode(CanvasUsage usage); + + /** + * @brief Record an interaction + */ + void RecordInteraction(CanvasInteraction interaction, + const std::string& details = ""); + + /** + * @brief Record operation timing + */ + void RecordOperation(const std::string& operation_name, + double time_ms); + + /** + * @brief Update canvas state + */ + void UpdateCanvasState(const ImVec2& canvas_size, + const ImVec2& content_size, + float global_scale, + float grid_step, + bool enable_grid, + bool enable_hex_labels, + bool enable_custom_labels); + + /** + * @brief Get current usage statistics + */ + const CanvasUsageStats& GetCurrentStats() const { return current_stats_; } + + /** + * @brief Get usage history + */ + const std::vector& GetUsageHistory() const { return usage_history_; } + + /** + * @brief Get usage mode name + */ + std::string GetUsageModeName(CanvasUsage usage) const; + + /** + * @brief Get usage mode color for UI + */ + ImVec4 GetUsageModeColor(CanvasUsage usage) const; + + /** + * @brief Get usage recommendations + */ + std::vector GetUsageRecommendations() const; + + /** + * @brief Export usage report + */ + std::string ExportUsageReport() const; + + /** + * @brief Clear usage history + */ + void ClearHistory(); + + /** + * @brief Start session + */ + void StartSession(); + + /** + * @brief End session + */ + void EndSession(); + + private: + std::string canvas_id_; + CanvasUsageStats current_stats_; + std::vector usage_history_; + std::chrono::steady_clock::time_point last_activity_; + std::chrono::steady_clock::time_point session_start_; + + // Interaction history + std::vector> interaction_history_; + std::unordered_map> operation_times_; + + // Helper methods + void UpdateActiveTime(); + void UpdateIdleTime(); + void SaveCurrentStats(); + double CalculateAverageOperationTime(const std::string& operation_name) const; + std::string FormatDuration(const std::chrono::milliseconds& duration) const; +}; + +/** + * @brief Global canvas usage tracker manager + */ +class CanvasUsageManager { + public: + static CanvasUsageManager& Get(); + + /** + * @brief Register a canvas tracker + */ + void RegisterTracker(const std::string& canvas_id, + std::shared_ptr tracker); + + /** + * @brief Get tracker for canvas + */ + std::shared_ptr GetTracker(const std::string& canvas_id); + + /** + * @brief Get all trackers + */ + const std::unordered_map>& + GetAllTrackers() const { return trackers_; } + + /** + * @brief Get global usage statistics + */ + CanvasUsageStats GetGlobalStats() const; + + /** + * @brief Export global usage report + */ + std::string ExportGlobalReport() const; + + /** + * @brief Clear all trackers + */ + void ClearAllTrackers(); + + private: + CanvasUsageManager() = default; + ~CanvasUsageManager() = default; + + std::unordered_map> trackers_; +}; + +} // namespace canvas +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_USAGE_TRACKER_H diff --git a/src/app/gui/canvas/canvas_utils_moved.cc b/src/app/gui/canvas/canvas_utils_moved.cc new file mode 100644 index 00000000..9ca8ee97 --- /dev/null +++ b/src/app/gui/canvas/canvas_utils_moved.cc @@ -0,0 +1,398 @@ +#include "canvas_utils_moved.h" + +#include +#include "app/core/window.h" +#include "app/gfx/snes_palette.h" +#include "util/log.h" + +namespace yaze { +namespace gui { +namespace canvas { +namespace CanvasUtils { + +using core::Renderer; + +ImVec2 AlignToGrid(ImVec2 pos, float grid_step) { + return ImVec2(std::floor(pos.x / grid_step) * grid_step, + std::floor(pos.y / grid_step) * grid_step); +} + +float CalculateEffectiveScale(ImVec2 canvas_size, ImVec2 content_size, + float global_scale) { + if (content_size.x <= 0 || content_size.y <= 0) + return global_scale; + + float scale_x = (canvas_size.x * global_scale) / content_size.x; + float scale_y = (canvas_size.y * global_scale) / content_size.y; + return std::min(scale_x, scale_y); +} + +int GetTileIdFromPosition(ImVec2 mouse_pos, float tile_size, float scale, + int tiles_per_row) { + float scaled_tile_size = tile_size * scale; + int tile_x = static_cast(mouse_pos.x / scaled_tile_size); + int tile_y = static_cast(mouse_pos.y / scaled_tile_size); + + return tile_x + (tile_y * tiles_per_row); +} + +bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager) { + if (!rom || palette_manager.palettes_loaded) { + return palette_manager.palettes_loaded; + } + + try { + const auto& palette_groups = rom->palette_group(); + palette_manager.rom_palette_groups.clear(); + palette_manager.palette_group_names.clear(); + + // Overworld palettes + if (palette_groups.overworld_main.size() > 0) { + palette_manager.rom_palette_groups.push_back( + palette_groups.overworld_main[0]); + palette_manager.palette_group_names.push_back("Overworld Main"); + } + if (palette_groups.overworld_aux.size() > 0) { + palette_manager.rom_palette_groups.push_back( + palette_groups.overworld_aux[0]); + palette_manager.palette_group_names.push_back("Overworld Aux"); + } + if (palette_groups.overworld_animated.size() > 0) { + palette_manager.rom_palette_groups.push_back( + palette_groups.overworld_animated[0]); + palette_manager.palette_group_names.push_back("Overworld Animated"); + } + + // Dungeon palettes + if (palette_groups.dungeon_main.size() > 0) { + palette_manager.rom_palette_groups.push_back( + palette_groups.dungeon_main[0]); + palette_manager.palette_group_names.push_back("Dungeon Main"); + } + + // Sprite palettes + if (palette_groups.global_sprites.size() > 0) { + palette_manager.rom_palette_groups.push_back( + palette_groups.global_sprites[0]); + palette_manager.palette_group_names.push_back("Global Sprites"); + } + if (palette_groups.armors.size() > 0) { + palette_manager.rom_palette_groups.push_back(palette_groups.armors[0]); + palette_manager.palette_group_names.push_back("Armor"); + } + if (palette_groups.swords.size() > 0) { + palette_manager.rom_palette_groups.push_back(palette_groups.swords[0]); + palette_manager.palette_group_names.push_back("Swords"); + } + + palette_manager.palettes_loaded = true; + util::logf("Canvas: Loaded %zu ROM palette groups", + palette_manager.rom_palette_groups.size()); + return true; + + } catch (const std::exception& e) { + util::logf("Canvas: Failed to load ROM palette groups: %s", e.what()); + return false; + } +} + +bool ApplyPaletteGroup(gfx::Bitmap* bitmap, + const CanvasPaletteManager& palette_manager, + int group_index, int palette_index) { + if (!bitmap || group_index < 0 || + group_index >= + static_cast(palette_manager.rom_palette_groups.size())) { + return false; + } + + try { + const auto& selected_palette = + palette_manager.rom_palette_groups[group_index]; + + // Apply the palette based on the index + if (palette_index >= 0 && palette_index < 8) { + bitmap->SetPaletteWithTransparent(selected_palette, palette_index); + } else { + bitmap->SetPalette(selected_palette); + } + + Renderer::Get().UpdateBitmap(bitmap); + util::logf("Canvas: Applied palette group %d, index %d to bitmap", + group_index, palette_index); + return true; + + } catch (const std::exception& e) { + util::logf("Canvas: Failed to apply palette: %s", e.what()); + return false; + } +} + +// Drawing utility functions +void DrawCanvasRect(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + int x, int y, int w, int h, ImVec4 color, + float global_scale) { + // Apply global scale to position and size + float scaled_x = x * global_scale; + float scaled_y = y * global_scale; + float scaled_w = w * global_scale; + float scaled_h = h * global_scale; + + ImVec2 origin(canvas_p0.x + scrolling.x + scaled_x, + canvas_p0.y + scrolling.y + scaled_y); + ImVec2 size(canvas_p0.x + scrolling.x + scaled_x + scaled_w, + canvas_p0.y + scrolling.y + scaled_y + scaled_h); + + uint32_t color_u32 = IM_COL32(color.x, color.y, color.z, color.w); + draw_list->AddRectFilled(origin, size, color_u32); + + // Add a black outline + ImVec2 outline_origin(origin.x - 1, origin.y - 1); + ImVec2 outline_size(size.x + 1, size.y + 1); + draw_list->AddRect(outline_origin, outline_size, IM_COL32(0, 0, 0, 255)); +} + +void DrawCanvasText(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + const std::string& text, int x, int y, float global_scale) { + // Apply global scale to text position + float scaled_x = x * global_scale; + float scaled_y = y * global_scale; + + ImVec2 text_pos(canvas_p0.x + scrolling.x + scaled_x, + canvas_p0.y + scrolling.y + scaled_y); + + // Draw text with black shadow for better visibility + draw_list->AddText(ImVec2(text_pos.x + 1, text_pos.y + 1), + IM_COL32(0, 0, 0, 255), text.c_str()); + draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), text.c_str()); +} + +void DrawCanvasOutline(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, int x, int y, int w, int h, + uint32_t color) { + ImVec2 origin(canvas_p0.x + scrolling.x + x, canvas_p0.y + scrolling.y + y); + ImVec2 size(canvas_p0.x + scrolling.x + x + w, + canvas_p0.y + scrolling.y + y + h); + draw_list->AddRect(origin, size, color, 0, 0, 1.5f); +} + +void DrawCanvasOutlineWithColor(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, int x, int y, int w, int h, + ImVec4 color) { + uint32_t color_u32 = + IM_COL32(color.x * 255, color.y * 255, color.z * 255, color.w * 255); + DrawCanvasOutline(draw_list, canvas_p0, scrolling, x, y, w, h, color_u32); +} + +// Grid utility functions +void DrawCanvasGridLines(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 canvas_p1, ImVec2 scrolling, float grid_step, + float global_scale) { + const uint32_t grid_color = IM_COL32(200, 200, 200, 50); + const float grid_thickness = 0.5f; + + float scaled_grid_step = grid_step * global_scale; + + for (float x = fmodf(scrolling.x, scaled_grid_step); + x < (canvas_p1.x - canvas_p0.x); x += scaled_grid_step) { + draw_list->AddLine(ImVec2(canvas_p0.x + x, canvas_p0.y), + ImVec2(canvas_p0.x + x, canvas_p1.y), grid_color, + grid_thickness); + } + + for (float y = fmodf(scrolling.y, scaled_grid_step); + y < (canvas_p1.y - canvas_p0.y); y += scaled_grid_step) { + draw_list->AddLine(ImVec2(canvas_p0.x, canvas_p0.y + y), + ImVec2(canvas_p1.x, canvas_p0.y + y), grid_color, + grid_thickness); + } +} + +void DrawCustomHighlight(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, int highlight_tile_id, + float grid_step) { + if (highlight_tile_id == -1) + return; + + int tile_x = highlight_tile_id % 8; + int tile_y = highlight_tile_id / 8; + ImVec2 tile_pos(canvas_p0.x + scrolling.x + tile_x * grid_step, + canvas_p0.y + scrolling.y + tile_y * grid_step); + ImVec2 tile_pos_end(tile_pos.x + grid_step, tile_pos.y + grid_step); + + draw_list->AddRectFilled(tile_pos, tile_pos_end, IM_COL32(255, 0, 255, 255)); +} + +void DrawHexTileLabels(ImDrawList* draw_list, ImVec2 canvas_p0, + ImVec2 scrolling, ImVec2 canvas_sz, float grid_step, + float global_scale) { + float scaled_grid_step = grid_step * global_scale; + + for (float x = fmodf(scrolling.x, scaled_grid_step); + x < canvas_sz.x * global_scale; x += scaled_grid_step) { + for (float y = fmodf(scrolling.y, scaled_grid_step); + y < canvas_sz.y * global_scale; y += scaled_grid_step) { + int tile_x = (x - scrolling.x) / scaled_grid_step; + int tile_y = (y - scrolling.y) / scaled_grid_step; + int tile_id = tile_x + (tile_y * 16); + + char hex_id[8]; + snprintf(hex_id, sizeof(hex_id), "%02X", tile_id); + + draw_list->AddText(ImVec2(canvas_p0.x + x + (scaled_grid_step / 2) - 4, + canvas_p0.y + y + (scaled_grid_step / 2) - 4), + IM_COL32(255, 255, 255, 255), hex_id); + } + } +} + +// Layout and interaction utilities +ImVec2 CalculateCanvasSize(ImVec2 content_region, ImVec2 custom_size, + bool use_custom) { + return use_custom ? custom_size : content_region; +} + +ImVec2 CalculateScaledCanvasSize(ImVec2 canvas_size, float global_scale) { + return ImVec2(canvas_size.x * global_scale, canvas_size.y * global_scale); +} + +bool IsPointInCanvas(ImVec2 point, ImVec2 canvas_p0, ImVec2 canvas_p1) { + return point.x >= canvas_p0.x && point.x <= canvas_p1.x && + point.y >= canvas_p0.y && point.y <= canvas_p1.y; +} + +// Size reporting for ImGui table integration +ImVec2 CalculateMinimumCanvasSize(ImVec2 content_size, float global_scale, + float padding) { + // Calculate minimum size needed to display content with padding + ImVec2 min_size = ImVec2(content_size.x * global_scale + padding * 2, + content_size.y * global_scale + padding * 2); + + // Ensure minimum practical size + min_size.x = std::max(min_size.x, 64.0f); + min_size.y = std::max(min_size.y, 64.0f); + + return min_size; +} + +ImVec2 CalculatePreferredCanvasSize(ImVec2 content_size, float global_scale, + float min_scale) { + // Calculate preferred size with minimum scale constraint + float effective_scale = std::max(global_scale, min_scale); + ImVec2 preferred_size = ImVec2(content_size.x * effective_scale + 8.0f, + content_size.y * effective_scale + 8.0f); + + // Cap to reasonable maximum sizes for table integration + preferred_size.x = std::min(preferred_size.x, 800.0f); + preferred_size.y = std::min(preferred_size.y, 600.0f); + + return preferred_size; +} + +void ReserveCanvasSpace(ImVec2 canvas_size, const std::string& label) { + // Reserve space in ImGui layout so tables know the size + if (!label.empty()) { + ImGui::Text("%s", label.c_str()); + } + ImGui::Dummy(canvas_size); + ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() - + canvas_size.x); // Move back to start +} + +void SetNextCanvasSize(ImVec2 size, bool auto_resize) { + if (auto_resize) { + // Use auto-sizing child window for table integration + ImGui::SetNextWindowContentSize(size); + } else { + // Fixed size + ImGui::SetNextWindowSize(size); + } +} + +// High-level composite operations +void DrawCanvasGrid(const CanvasRenderContext& ctx, int highlight_tile_id) { + if (!ctx.enable_grid) + return; + + ctx.draw_list->PushClipRect(ctx.canvas_p0, ctx.canvas_p1, true); + + // Draw grid lines + DrawCanvasGridLines(ctx.draw_list, ctx.canvas_p0, ctx.canvas_p1, + ctx.scrolling, ctx.grid_step, ctx.global_scale); + + // Draw highlight if specified + if (highlight_tile_id != -1) { + DrawCustomHighlight(ctx.draw_list, ctx.canvas_p0, ctx.scrolling, + highlight_tile_id, ctx.grid_step * ctx.global_scale); + } + + // Draw hex labels if enabled + if (ctx.enable_hex_labels) { + DrawHexTileLabels(ctx.draw_list, ctx.canvas_p0, ctx.scrolling, + ImVec2(ctx.canvas_p1.x - ctx.canvas_p0.x, + ctx.canvas_p1.y - ctx.canvas_p0.y), + ctx.grid_step, ctx.global_scale); + } + + ctx.draw_list->PopClipRect(); +} + +void DrawCanvasOverlay(const CanvasRenderContext& ctx, + const ImVector& points, + const ImVector& selected_points) { + const ImVec2 origin(ctx.canvas_p0.x + ctx.scrolling.x, + ctx.canvas_p0.y + ctx.scrolling.y); + + // Draw hover points + for (int n = 0; n < points.Size; n += 2) { + ctx.draw_list->AddRect( + ImVec2(origin.x + points[n].x, origin.y + points[n].y), + ImVec2(origin.x + points[n + 1].x, origin.y + points[n + 1].y), + IM_COL32(255, 255, 255, 255), 1.0f); + } + + // Draw selection rectangles + if (!selected_points.empty()) { + for (int n = 0; n < selected_points.size(); n += 2) { + ctx.draw_list->AddRect(ImVec2(origin.x + selected_points[n].x, + origin.y + selected_points[n].y), + ImVec2(origin.x + selected_points[n + 1].x + 0x10, + origin.y + selected_points[n + 1].y + 0x10), + IM_COL32(255, 255, 255, 255), 1.0f); + } + } +} + +void DrawCanvasLabels(const CanvasRenderContext& ctx, + const ImVector>& labels, + int current_labels, int tile_id_offset) { + if (current_labels >= labels.size()) + return; + + float scaled_grid_step = ctx.grid_step * ctx.global_scale; + + for (float x = fmodf(ctx.scrolling.x, scaled_grid_step); + x < (ctx.canvas_p1.x - ctx.canvas_p0.x); x += scaled_grid_step) { + for (float y = fmodf(ctx.scrolling.y, scaled_grid_step); + y < (ctx.canvas_p1.y - ctx.canvas_p0.y); y += scaled_grid_step) { + int tile_x = (x - ctx.scrolling.x) / scaled_grid_step; + int tile_y = (y - ctx.scrolling.y) / scaled_grid_step; + int tile_id = tile_x + (tile_y * tile_id_offset); + + if (tile_id >= labels[current_labels].size()) { + break; + } + + const std::string& label = labels[current_labels][tile_id]; + ctx.draw_list->AddText( + ImVec2(ctx.canvas_p0.x + x + (scaled_grid_step / 2) - tile_id_offset, + ctx.canvas_p0.y + y + (scaled_grid_step / 2) - tile_id_offset), + IM_COL32(255, 255, 255, 255), label.c_str()); + } + } +} + +} // namespace CanvasUtils +} // namespace canvas +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/canvas/canvas_utils_moved.h b/src/app/gui/canvas/canvas_utils_moved.h new file mode 100644 index 00000000..bbb64a7d --- /dev/null +++ b/src/app/gui/canvas/canvas_utils_moved.h @@ -0,0 +1,198 @@ +#ifndef YAZE_APP_GUI_CANVAS_CANVAS_UTILS_H +#define YAZE_APP_GUI_CANVAS_CANVAS_UTILS_H + +#include +#include +#include +#include "app/gfx/snes_palette.h" +#include "app/rom.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace gui { +namespace canvas { + +/** + * @brief Configuration for canvas display and interaction + * + * Modern single-source-of-truth configuration structure. + * Replaces the dual state management pattern. + */ +struct CanvasConfig { + bool enable_grid = true; + bool enable_hex_labels = false; + bool enable_custom_labels = false; + bool enable_context_menu = true; + bool is_draggable = false; + bool auto_resize = false; + float grid_step = 32.0f; + float global_scale = 1.0f; + ImVec2 canvas_size = ImVec2(0, 0); + ImVec2 content_size = ImVec2(0, 0); // Size of actual content (bitmap, etc.) + bool custom_canvas_size = false; + ImVec2 scrolling = ImVec2(0, 0); + + // Modern callbacks for config updates + std::function on_config_changed; + std::function on_scale_changed; +}; + +/** + * @brief Selection state for canvas interactions + */ +struct CanvasSelection { + std::vector selected_tiles; + std::vector selected_points; + ImVec2 selected_tile_pos = ImVec2(-1, -1); + bool select_rect_active = false; + + void Clear() { + selected_tiles.clear(); + selected_points.clear(); + selected_tile_pos = ImVec2(-1, -1); + select_rect_active = false; + } +}; + +/** + * @brief Palette management state for canvas + */ +struct CanvasPaletteManager { + std::vector rom_palette_groups; + std::vector palette_group_names; + gfx::SnesPalette original_palette; + bool palettes_loaded = false; + int current_group_index = 0; + int current_palette_index = 0; + + void Clear() { + rom_palette_groups.clear(); + palette_group_names.clear(); + original_palette.clear(); + palettes_loaded = false; + current_group_index = 0; + current_palette_index = 0; + } +}; + +/** + * @brief Context menu item configuration + */ +struct CanvasContextMenuItem { + std::string label; + std::string shortcut; + std::function callback; + std::function enabled_condition = []() { return true; }; + std::vector subitems; +}; + +/** + * @brief Render context for canvas drawing operations + */ +struct CanvasRenderContext { + ImDrawList* draw_list; + ImVec2 canvas_p0; + ImVec2 canvas_p1; + ImVec2 scrolling; + float global_scale; + bool enable_grid; + bool enable_hex_labels; + float grid_step; +}; + +/** + * @brief Utility functions for canvas operations + * + * Stateless utilities for common canvas operations. + * Following ImGui design pattern of pure helper functions. + */ +namespace CanvasUtils { + +// ==================== Core Utilities (Stateless) ==================== + +/** + * @brief Align position to grid + * Pure function - no side effects + */ +ImVec2 AlignToGrid(ImVec2 pos, float grid_step); + +/** + * @brief Calculate effective scale for content + */ +float CalculateEffectiveScale(ImVec2 canvas_size, ImVec2 content_size, float global_scale); + +/** + * @brief Get tile ID from mouse position + */ +int GetTileIdFromPosition(ImVec2 mouse_pos, float tile_size, float scale, int tiles_per_row); + +// ==================== Palette Management ==================== + +bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager); +bool ApplyPaletteGroup(gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager, + int group_index, int palette_index); + +// ==================== Drawing Utilities ==================== + +void DrawCanvasRect(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + int x, int y, int w, int h, ImVec4 color, float global_scale); + +void DrawCanvasText(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + const std::string& text, int x, int y, float global_scale); + +void DrawCanvasOutline(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + int x, int y, int w, int h, uint32_t color = IM_COL32(255, 255, 255, 200)); + +void DrawCanvasOutlineWithColor(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + int x, int y, int w, int h, ImVec4 color); + +// ==================== Grid Utilities ==================== + +void DrawCanvasGridLines(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 canvas_p1, + ImVec2 scrolling, float grid_step, float global_scale); + +void DrawCustomHighlight(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + int highlight_tile_id, float grid_step); + +void DrawHexTileLabels(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling, + ImVec2 canvas_sz, float grid_step, float global_scale); + +// ==================== Layout Utilities ==================== + +ImVec2 CalculateCanvasSize(ImVec2 content_region, ImVec2 custom_size, bool use_custom); +ImVec2 CalculateScaledCanvasSize(ImVec2 canvas_size, float global_scale); +bool IsPointInCanvas(ImVec2 point, ImVec2 canvas_p0, ImVec2 canvas_p1); + +// ==================== Size Reporting for Tables ==================== + +ImVec2 CalculateMinimumCanvasSize(ImVec2 content_size, float global_scale, float padding = 4.0f); +ImVec2 CalculatePreferredCanvasSize(ImVec2 content_size, float global_scale, float min_scale = 1.0f); +void ReserveCanvasSpace(ImVec2 canvas_size, const std::string& label = ""); +void SetNextCanvasSize(ImVec2 size, bool auto_resize = false); + +// ==================== Composite Operations ==================== + +void DrawCanvasGrid(const CanvasRenderContext& ctx, int highlight_tile_id = -1); +void DrawCanvasOverlay(const CanvasRenderContext& ctx, const ImVector& points, + const ImVector& selected_points); +void DrawCanvasLabels(const CanvasRenderContext& ctx, const ImVector>& labels, + int current_labels, int tile_id_offset); + +} // namespace CanvasUtils + +} // namespace canvas + +// ==================== Compatibility Aliases (gui namespace) ==================== + +// For backward compatibility, provide aliases in gui namespace +using CanvasConfig = canvas::CanvasConfig; +using CanvasSelection = canvas::CanvasSelection; +using CanvasPaletteManager = canvas::CanvasPaletteManager; +using CanvasContextMenuItem = canvas::CanvasContextMenuItem; + +namespace CanvasUtils = canvas::CanvasUtils; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_CANVAS_CANVAS_UTILS_H diff --git a/src/app/gui/gui.cmake b/src/app/gui/gui.cmake index 26826838..1dfea38c 100644 --- a/src/app/gui/gui.cmake +++ b/src/app/gui/gui.cmake @@ -11,4 +11,10 @@ set( app/gui/theme_manager.cc app/gui/background_renderer.cc app/gui/bpp_format_ui.cc + # Canvas system components + app/gui/canvas/canvas_modals.cc + app/gui/canvas/canvas_context_menu.cc + app/gui/canvas/canvas_usage_tracker.cc + app/gui/canvas/canvas_performance_integration.cc + app/gui/canvas/canvas_interaction_handler.cc )